HttpClient必须复用,否则会耗尽端口、触发SocketException;.NET 6+中应使用静态实例或IHttpClientFactory,禁止单次请求新建实例。

HttpClient 不是“用一次 new 一次”的工具,它必须复用,否则会快速耗尽端口、触发 SocketException 或 HttpRequestException: Connection refused。在 .NET 6+ 项目中,直接 new 静态实例或走 IHttpClientFactory 是唯二靠谱选择。
为什么不能每次请求都 new HttpClient()
每次 new HttpClient() 都会创建独立的连接池和底层 ServicePoint,Windows 默认每个 ServicePoint 最多维持 2 个空闲连接;高并发下大量短命实例会导致:
- TCP 端口快速耗尽(AddressAlreadyInUse 或 SocketException 10048)
- DNS 缓存不共享,重复解析增加延迟
- SSL/TLS 握手无法复用,显著拖慢 HTTPS 请求
- Dispose() 后连接不会立即关闭,而是进入 TIME_WAIT 状态,持续占用资源
推荐的 HttpClient 实例管理方式
根据项目结构选一种,别混用:
- ASP.NET Core 项目(含 Minimal API):注册
IHttpClientFactory,在 DI 容器中配置命名/类型化客户端 - 控制台或非 DI 场景:声明
private static readonly HttpClient,设好BaseAddress和Timeout - 需要精细控制生命周期(如插件系统):用
HttpClientHandler+using,但仅限极低频、明确隔离的场景
错误示例(别这么写):
public async Task<string> GetAsync(string url) {
using var client = new HttpClient(); // ❌ 每次都 new,危险
return await client.GetStringAsync(url);
}</string>
正确示例(静态单例):
private static readonly HttpClient _client = new() {
BaseAddress = new Uri("https://api.example.com/"),
Timeout = TimeSpan.FromSeconds(15)
};
_client.DefaultRequestHeaders.UserAgent.ParseAdd("MyApp/2.1");
GET 和 POST 的典型写法与坑点
GetStringAsync 看起来方便,但它只返回响应体,丢掉状态码和头信息,调试时容易误判 4xx/5xx 响应为成功。生产代码建议统一用 GetAsync / PostAsync + EnsureSuccessStatusCode()。
- GET 请求:用
GetAsync获取完整HttpResponseMessage,再读内容 - POST 提交表单:用
FormUrlEncodedContent,不是手动拼 query string - POST 提交 JSON:用
JsonSerializer.Serialize+StringContent,注意指定"application/json"类型 - 不要在
PostAsync后直接调response.Content.ReadAsStringAsync()—— 先检查response.IsSuccessStatusCode或调EnsureSuccessStatusCode(),否则 400 响应也会进读取逻辑,可能抛出意外异常
示例(健壮 POST):
var user = new { name = "Alice", email = "a@example.com" };
var json = JsonSerializer.Serialize(user);
var content = new StringContent(json, Encoding.UTF8, "application/json");
<p>var response = await _client.PostAsync("users", content);
response.EnsureSuccessStatusCode(); // 显式失败检查
var result = await response.Content.ReadAsStringAsync();</p>
超时、重试和取消令牌的实际处理
HttpClient.Timeout 是整个请求周期上限(DNS + 连接 + 发送 + 接收),不是单个阶段。它无法中断已建立的连接上的阻塞读取(.NET 6+ 有部分改进,但仍不完美)。真正可控的是 CancellationToken。
- 所有
GetAsync、PostAsync等方法都支持CancellationToken参数,这是唯一能可靠中断挂起请求的方式 - 不要依赖
Timeout做业务级超时控制,它更适合作为兜底保护 - 重试逻辑不要塞在
HttpClient层 —— 它不内置重试;用Polly或自己封装,且只对幂等操作(如 GET)重试 - 上传大文件时,记得设置
HttpClient.Timeout = Timeout.InfiniteTimeSpan,改用CancellationToken控制用户主动取消
最常被忽略的一点:HttpClient 的 BaseAddress 末尾是否带斜杠,直接影响相对路径拼接结果。比如 BaseAddress = new Uri("https://api.com/v1"),再调 GetAsync("users"),实际发的是 https://api.com/v1users(缺 / 导致路径粘连)。务必写成 "https://api.com/v1/"。











