.net core
中构造函数注入IHttpClientFactory
和HttpClient
的区别有哪些,使用HttpClient
注入有什么隐患,以及如何证明?
构造函数注入 IHttpClientFactory 和 HttpClient 的区别
先说推荐
官方文档介绍的大概意思如下:
HttpClient
类使用比较简单,但在某些情况下,许多开发人员却并未正确使用该类;虽然此类实现IDisposable
,但在using
语句中声明和实例化它并非首选操作,因为释放HttpClient
对象时,基础套接字不会立即释放,这可能会导致套接字耗尽问题,最终可能会导致SocketException
错误。要解决此问题,推荐的方法是将HttpClient
对象创建为单一对象或静态对象。
简单的说直接创建使用 HttpClient
对象,不能立即关闭连接、性能消耗严重等的问题。.NET Core 2.1+
开始引入的 HttpClientFactory
解决了 HttpClient
的所有痛点。
IHttpClientFactory
是在 .NETCore 2.1
开始提供的,默认实现为 DefaultHttpClientFactory
,专门用于创建在应用程序中用到的 HttpClient
实例,自动维护内部的 HttpMessageHandler
池及其生命周期。
从微软源码分析,HttpClient
继承自 HttpMessageInvoker
,而HttpMessageInvoker
实质就是 HttpClientHandler
。
HttpClientFactory
创建的 HttpClient
,也即是 HttpClientHandler
,只是这些个 HttpClient
被放到了 Pool “池子”
中,工厂每次在 create
的时候会自动判断是新建还是复用(默认生命周期为 2min
)。
使用 IHttpClientFactory
的好处
同时实现 IHttpMessageHandlerFactory
的 IHttpClientFactory
当前实现具有以下优势:
- 提供一个中心位置,用于命名和配置逻辑
HttpClient
对象。 例如,可以配置预配置的客户端(服务代理)以访问特定微服务。 - 通过后列方式整理出站中间件的概念:在
HttpClient
中委托处理程序并实现基于Polly
的中间件以利用Polly
的复原策略。 HttpClient
已经具有委托处理程序的概念,这些委托处理程序可以链接在一起,处理出站HTTP
请求。 将HTTP
客户端注册到工厂后,可使用一个Polly
处理程序将Polly
策略用于重试、断路器等。- 管理
HttpMessageHandler
的生存期,避免在自行管理HttpClient
生存期时出现上述问题。
官方文档:
- https://learn.microsoft.com/zh-cn/dotnet/architecture/microservices/implement-resilient-applications/use-httpclientfactory-to-implement-resilient-http-requests
Service 服务准备(模拟外部接口服务)
此处使用 asp.net core webapi
默认(WeatherForecast
)接口:
using Microsoft.AspNetCore.Mvc;
namespace WebApp1.Controllers;
[ApiController]
[Route("[controller]")]
public class WeatherForecastController(ILogger<WeatherForecastController> logger) : ControllerBase
{
private static readonly string[] Summaries = new[]
{
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
};
[HttpGet(Name = "GetWeatherForecast")]
public IEnumerable<WeatherForecast> Get()
{
return Enumerable.Range(1, 3).Select(index => new WeatherForecast {
Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
TemperatureC = Random.Shared.Next(-20, 55),
Summary = Summaries[Random.Shared.Next(Summaries.Length)]
}).ToArray();
}
}
public class WeatherForecast
{
public DateOnly Date { get; set; }
public int TemperatureC { get; set; }
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
public string? Summary { get; set; }
}
返回数据格式如下:
[
{
"date": "2024-04-16",
"temperatureC": -11,
"temperatureF": 13,
"summary": "Scorching"
},
{
"date": "2024-04-17",
"temperatureC": -18,
"temperatureF": 0,
"summary": "Balmy"
},
{
"date": "2024-04-18",
"temperatureC": 46,
"temperatureF": 114,
"summary": "Mild"
}
]
模拟外部服务接口正常就绪。
构造函数注入 HttpClient & IHttpClientFactory
对比
1、Program.cs
文件添加服务
- 项目安装
nuget
包Microsoft.Extensions.Http.Polly
; - 添加
AddHttpClient
命名服务,并使用TransientHttpErrorPolicy
策略;
string clientName = "local";
string uri = "http://localhost:5234/";
builder.Services.AddHttpClient(clientName, client => {
client.BaseAddress = new Uri(uri);
client.DefaultRequestHeaders.TryAddWithoutValidation("Accept", "application/vnd.github.v3+json");
client.DefaultRequestHeaders.TryAddWithoutValidation("client-name", clientName);
}).AddTransientHttpErrorPolicy(builder => builder.WaitAndRetryAsync(new[] {
TimeSpan.FromSeconds(1),
TimeSpan.FromSeconds(5),
TimeSpan.FromSeconds(10)
}));
说明:此处为了方便,统一配置如上所示,如果只是单纯使用 HttpClient
方法,直接 builder.Services.AddHttpClient()
即可。
2、构造函数注入 HttpClient
- 示例
public class SomeController(ILogger<SomeController> logger,
HttpClient httpClient): ControllerBase
{
[HttpGet]
public async Task<IActionResult> SomeActionAsync()
{
logger.LogInformation($"time={DateTime.Now},使用 HttpClient");
string uri = "http://localhost:5234/";
httpClient.BaseAddress = new Uri(uri);
var resp = await httpClient.GetStringAsync("/WeatherForecast");
//httpClient.Dispose();
return Ok(resp);
}
}
优点:
- 直接使用:直接注入一个已经配置好的
HttpClient
实例,使用起来更加直观简洁,无需额外的工厂方法调用。
隐患:
- 资源泄漏:如果在整个应用程序生命周期内保持单个
HttpClient
实例的静态或单例模式,可能会导致TCP
连接资源无法释放,尤其是在高并发场景下,可能导致端口耗尽。 - DNS 刷新问题:如前所述,静态或单例的
HttpClient
实例不会自动更新DNS
解析结果,当目标服务器IP
发生变化时,应用可能无法正确连接到服务。 - 配置局限:直接注入的
HttpClient
实例通常具有固定配置,难以根据不同场景或服务需求动态调整。
不推荐使用:考虑到潜在的资源管理问题和可扩展性限制,一般情况下不推荐直接在构造函数中注入HttpClient
实例。
3、构造函数注入 IHttpClientFactory
- 示例
public class SomeController(ILogger<SomeController> logger,
IHttpClientFactory httpClientFactory) : ControllerBase
{
[HttpGet]
public async Task<IActionResult> SomeActionAsync()
{
logger.LogInformation($"time={DateTime.Now},使用 IHttpClientFactory");
var httpClient = httpClientFactory.CreateClient("local");
var resp = await httpClient.GetStringAsync("/WeatherForecast");
return Ok(resp);
}
}
优点:
- 资源管理优化:
IHttpClientFactory
作为专门设计用于管理HttpClient
实例的工厂类,遵循最佳实践,能够有效地复用HttpClient
底层的TCP
连接,避免因频繁创建和销毁HttpClient
实例导致的端口耗尽问题。 - DNS刷新:
IHttpClientFactory
创建的HttpClient
实例能够自动响应DNS
更改,解决了静态或单例HttpClient
不刷新DNS
缓存可能导致的问题。 - 可配置性与扩展性:通过使用
IHttpClientFactory
,可以轻松地为不同的服务或API
定义命名的客户端,每个客户端都可以独立配置(如超时、重试策略、消息处理器等)。此外,还可以使用Polly
等库进行更复杂的故障恢复和熔断策略。 - 生命周期管理:
IHttpClientFactory
遵循依赖注入容器的生命周期,可以根据需要创建和释放HttpClient
实例,减轻开发者对HttpClient
生命周期管理的负担。
推荐使用:由于上述诸多优势,特别是在需要频繁或长期执行 HTTP
请求的场景中,强烈推荐使用 IHttpClientFactory
来管理 HttpClient
的创建和使用。
4、如何证明 HttpClient
隐患
要证明直接注入 HttpClient
的隐患,可以通过以下方式:
-
端口耗尽模拟:编写一个程序,直接注入并频繁创建新的
HttpClient
实例(或长时间保持单个实例但进行大量并发请求)。监控系统资源,特别是网络端口使用情况。在高并发请求下,可能会观察到可用端口数量快速减少直至耗尽,这证明了资源管理问题的存在。 -
DNS
刷新测试:设置一个测试环境,使得目标服务器的IP
地址可以在运行时动态变更。编写一个使用静态或单例HttpClient
实例的应用程序,持续向该服务器发送请求。当服务器IP
变更后,观察应用程序是否能及时适应新的IP
地址。如果请求仍然发往旧IP
,说明HttpClient
未能自动刷新DNS
。
说明:代码中访问的地址(
http://localhost:5234/WeatherForecast
)是用本地dotnet run
服务(也可以在阿里云上Nginx
的服务)站点,此处没有做负载,所以看Socket
状态比较直观。
上面代码执行完成之后就退出了,理论情况来说,上面程序执行完毕之后,就不应该占用资源啦,但通过 netstat
查看,的确还有 Socket
被占用,测试如下:
Service 测试
1、Windows 环境
运行服务:
# 进入到对应的服务目录
dotnet run
查看端口占用情况:
netstat -ano | findstr TIME_WAIT | findstr 127.0.0.1
输出信息如下:
2、Linux 环境
把 .Net Core
运行环境安装好,创建一个目录,将编译之后的文件通过 Xftp/FileZilla Client
将文件传到云服务器,执行以下命令即可:
# 注意,这里指定启动的是 dll 文件
dotnet HttpClientDemo.dll
查看端口占用情况:
# 查看端口占用情况,找到状态为 TIME_WAIT
netstat -ant | grep TIME_WAIT | grep 127.0.0.1
感兴趣的请自行验证,此处就不在截图演示了。
说明:
TIME_WAIT
是主动关闭TCP
连接方出现的状态,系统会在TIME_WAIT
状态下等待2MSL
(maximum segment lifetime
)后才能释放连接(端口),目的是为了在TCP
四次挥手关闭连接机制中,保证ACK
重发和丢弃延迟数据。按照解释来看,这种做法也算是合情合理,保证数据传输嘛,但的确就是占用资源啦;具体关于网络的相关知识,小伙伴们再去查阅一下。
3、对比 HashCode
除了上面的测试方式,我们还可以简单的对比下构造函数 DI
注入 HttpClient
和 IHttpClientFactory
对象分别生产 httpclient
的哈希码 GetHashCode()
。
using Microsoft.AspNetCore.Mvc;
namespace HttpClientDemo.Controllers;
[Route("api/[controller]/[action]")]
[ApiController]
public class DemoController(HttpClient httpClient, IHttpClientFactory httpClientFactory) : ControllerBase
{
[HttpGet]
public async Task<IActionResult> GetHttpClientHashCodeAsync()
{
var hc1 = httpClient.GetHashCode();
var httpClient2 = httpClientFactory.CreateClient();
var hc2 = httpClient2.GetHashCode();
var httpClient3 = httpClientFactory.CreateClient("local");
var hc3 = httpClient3.GetHashCode();
await Task.CompletedTask;
return Ok($"hc1={hc1},hc2={hc2},hc3={hc3}");
}
}
输出信息:
hc1=41300193,hc2=15586314,hc3=35059110
通过上面三种方式创建的对象,分别获取哈希代码均不相同,说明他们不是同一个对象,如果使用 DefaultHttpClientFactory
对象创建的 HttpClient
和构造函数 DI
的 HttpClient
对象,它们的哈希代码是一样的。
- 使用
IHttpClientFactory
创建HttpClient
的两种方式。
namespace System.Net.Http
{
public static class HttpClientFactoryExtensions
{
public static HttpClient CreateClient(this IHttpClientFactory factory);
}
public interface IHttpClientFactory
{
HttpClient CreateClient(string name);
}
}
推荐使用 IHttpClientFactory
综上所述,出于性能、资源管理、可配置性和扩展性的考虑,推荐在 .NET Core
中使用构造函数注入 IHttpClientFactory
而非直接注入 HttpClient
。若要证明直接注入 HttpClient
的隐患,可以通过模拟高并发场景引发端口耗尽问题,以及进行 DNS
刷新测试来验证其对 DNS
变更的响应能力。
注意:
IHttpClientFactory
创建的HttpClient
实例的生存期管理与手动创建的实例完全不同。 策略是使用由IHttpClientFactory
创建的短期客户端,或设置了PooledConnectionLifetime
的长期客户端。 有关详细信息,请参阅 《HttpClient
生存期管理》部分和《使用HttpClient
的指南》。
- 《
HttpClient
生存期管理》,https://learn.microsoft.com/zh-cn/dotnet/core/extensions/httpclient-factory#httpclient-lifetime-management
- 《
HttpClient
的使用准则/指南》,https://learn.microsoft.com/zh-cn/dotnet/fundamentals/networking/http/httpclient-guidelines
相关博文参考:
- 《使用
IHttpClientFactory
实现复原HTTP
请求》,https://learn.microsoft.com/zh-cn/dotnet/architecture/microservices/implement-resilient-applications/use-httpclientfactory-to-implement-resilient-http-requests
- 《使用
.NET
的IHttpClientFactory
》,https://learn.microsoft.com/zh-cn/dotnet/core/extensions/httpclient-factory
- 《
.net core HttpClient
使用之掉坑解析(一)》,https://www.cnblogs.com/jlion/p/12813692.html
- 《把
HttpClient
换成IHttpClientFactory
之后,放心多了》,https://zhuanlan.zhihu.com/p/381738224