应长期复用 HttpClient 而非每次 new,因其线程安全且内部 HttpMessageHandler 持有连接池与 DNS 缓存;频繁创建会导致端口耗尽、连接复用失败和 Socket 资源泄漏。

为什么不能每次 new HttpClient()?
直接 new HttpClient() 会导致端口耗尽、DNS 缓存失效、连接复用失败等问题。.NET 的 HttpClient 是线程安全且设计为长期复用的,频繁创建销毁会触发底层 Socket 资源泄漏(尤其在高并发下),错误日志里常出现 SocketException: An operation on a socket could not be performed because the system lacked sufficient buffer space or because a queue was full。
根本原因不是 HttpClient 本身没 Dispose,而是它内部的 HttpMessageHandler(默认是 HttpClientHandler)持有连接池和 DNS 缓存,过早释放会打断复用链路。
HttpClientFactory 是怎么解决这个问题的?
HttpClientFactory 不是帮你“创建 HttpClient”,而是管理背后的 HttpMessageHandler 生命周期,并按需提供轻量级、短命的 HttpClient 实例(每个实例只持有一个 handler 引用,不拥有 handler)。handler 由工厂统一维护,默认 2 分钟空闲后回收,支持 DNS 刷新、连接池自动调优。
使用时你拿到的是“借用”的 HttpClient,不用 Dispose,也不该缓存它——下次 GetClient("api") 拿到的可能是新实例,但底层 handler 很可能复用。
关键点:
-
AddHttpClient注册时传入的配置(如 BaseAddress、Timeout、DefaultRequestHeaders)只影响新生成的 client 实例,不影响 handler - handler 级别配置(如
MaxConnectionsPerServer)必须通过ConfigurePrimaryHttpMessageHandler或自定义IHttpMessageHandlerBuilderFilter设置 - 命名客户端(如
services.AddHttpClient("github", c => c.BaseAddress = new Uri("https://api.github.com/")))适合多服务场景,类型化客户端更利于单元测试
类型化客户端(Typed Client)怎么写才对?
类型化客户端本质是一个普通类,构造函数接收 IHttpClientFactory 或直接注入 HttpClient(推荐后者,更简洁)。它不是单例,每次解析都新建,但内部的 HttpClient 由工厂供应,生命周期解耦。
示例:
动态WEB网站中的PHP和MySQL详细反映实际程序的需求,仔细地探讨外部数据的验证(例如信用卡卡号的格式)、用户登录以及如何使用模板建立网页的标准外观。动态WEB网站中的PHP和MySQL的内容不仅仅是这些。书中还提到如何串联JavaScript与PHP让用户操作时更快、更方便。还有正确处理用户输入错误的方法,让网站看起来更专业。另外还引入大量来自PEAR外挂函数库的强大功能,对常用的、强大的包
public class GitHubService
{
private readonly HttpClient _httpClient;
public GitHubService(HttpClient httpClient)
{
_httpClient = httpClient;
_httpClient.DefaultRequestHeaders.UserAgent.ParseAdd("myapp/1.0");
}
public async Task GetLatestRelease(string repo)
{
var response = await _httpClient.GetAsync($"/repos/{repo}/releases/latest");
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsStringAsync();
}
}
注册方式:
services.AddHttpClient(client => { client.BaseAddress = new Uri("https://api.github.com/"); client.Timeout = TimeSpan.FromSeconds(10); });
注意:AddHttpClient 注册的是服务类型,不是 HttpClient;T 构造函数中注入的 HttpClient 已被工厂包装,无需再处理重试或超时逻辑(这些应放在 factory 配置或 Polly 策略里)。
超时、重试、认证这些怎么加?
超时建议设在 HttpClient 实例级别(通过 AddHttpClient 配置),而不是在每次请求时用 CancellationToken 控制——后者只中断当前请求,不释放连接;前者能触发 handler 层连接清理。
重试和断路器必须用 IHttpClientBuilder.AddPolicyHandler(基于 Polly),例如:
services.AddHttpClient() .SetHandlerLifetime(TimeSpan.FromMinutes(5)) .AddPolicyHandler(Policy.Handle () .OrResult (r => !r.IsSuccessStatusCode) .WaitAndRetryAsync(3, retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt))));
认证头推荐用 AddHttpMessageHandler 注入自定义 DelegatingHandler,避免每次手动设置 DefaultRequestHeaders:
- Token 过期需刷新?不要在 handler 里同步调用
GetAccessTokenAsync,改用IAsyncAuthorizationHeaderProvider模式或配合ITokenProvider+RefreshTokenHandler - Basic Auth 明文密码?把凭据存在
IConfiguration或ISecretsManager,handler 中读取后编码 - Bearer Token 需动态更新?handler 中检查
DateTimeOffset.Now ,过期则异步刷新并更新Authorization头
handler 的 SendAsync 是同步入口,所有 await 必须显式声明,否则会阻塞连接池线程。









