Bootstrap

.net core 中构造函数注入 IHttpClientFactory 和 HttpClient 的区别,使用 HttpClient 注入有什么隐患,以及如何证明?

.net core 中构造函数注入 IHttpClientFactoryHttpClient 的区别有哪些,使用 HttpClient 注入有什么隐患,以及如何证明?

先说推荐

官方文档介绍的大概意思如下:

HttpClient 类使用比较简单,但在某些情况下,许多开发人员却并未正确使用该类;虽然此类实现 IDisposable,但在 using 语句中声明和实例化它并非首选操作,因为释放 HttpClient 对象时,基础套接字不会立即释放,这可能会导致套接字耗尽问题,最终可能会导致 SocketException 错误。要解决此问题,推荐的方法是将 HttpClient 对象创建为单一对象或静态对象。

简单的说直接创建使用 HttpClient 对象,不能立即关闭连接、性能消耗严重等的问题。.NET Core 2.1+ 开始引入的 HttpClientFactory 解决了 HttpClient 的所有痛点。

IHttpClientFactory 是在 .NETCore 2.1 开始提供的,默认实现为 DefaultHttpClientFactory,专门用于创建在应用程序中用到的 HttpClient 实例,自动维护内部的 HttpMessageHandler 池及其生命周期。

HttpClientFactory

从微软源码分析,HttpClient 继承自 HttpMessageInvoker,而HttpMessageInvoker 实质就是 HttpClientHandler

HttpClientFactory 创建的 HttpClient,也即是 HttpClientHandler,只是这些个 HttpClient 被放到了 Pool “池子” 中,工厂每次在 create 的时候会自动判断是新建还是复用(默认生命周期为 2min)。

使用 IHttpClientFactory 的好处

同时实现 IHttpMessageHandlerFactoryIHttpClientFactory 当前实现具有以下优势:

  • 提供一个中心位置,用于命名和配置逻辑 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 文件添加服务

  • 项目安装 nugetMicrosoft.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);
  }
}

优点

  1. 直接使用:直接注入一个已经配置好的 HttpClient 实例,使用起来更加直观简洁,无需额外的工厂方法调用。

隐患

  1. 资源泄漏:如果在整个应用程序生命周期内保持单个 HttpClient 实例的静态或单例模式,可能会导致 TCP 连接资源无法释放,尤其是在高并发场景下,可能导致端口耗尽。
  2. DNS 刷新问题:如前所述,静态或单例的 HttpClient 实例不会自动更新 DNS 解析结果,当目标服务器 IP 发生变化时,应用可能无法正确连接到服务。
  3. 配置局限:直接注入的 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);
  }
}

优点:

  1. 资源管理优化IHttpClientFactory 作为专门设计用于管理 HttpClient 实例的工厂类,遵循最佳实践,能够有效地复用HttpClient 底层的 TCP 连接,避免因频繁创建和销毁 HttpClient 实例导致的端口耗尽问题。
  2. DNS刷新IHttpClientFactory 创建的 HttpClient 实例能够自动响应 DNS 更改,解决了静态或单例 HttpClient 不刷新 DNS 缓存可能导致的问题。
  3. 可配置性与扩展性:通过使用IHttpClientFactory,可以轻松地为不同的服务或 API 定义命名的客户端,每个客户端都可以独立配置(如超时、重试策略、消息处理器等)。此外,还可以使用 Polly 等库进行更复杂的故障恢复和熔断策略。
  4. 生命周期管理IHttpClientFactory 遵循依赖注入容器的生命周期,可以根据需要创建和释放 HttpClient 实例,减轻开发者对 HttpClient 生命周期管理的负担。

推荐使用:由于上述诸多优势,特别是在需要频繁或长期执行 HTTP 请求的场景中,强烈推荐使用 IHttpClientFactory 来管理 HttpClient 的创建和使用。

4、如何证明 HttpClient 隐患

要证明直接注入 HttpClient 的隐患,可以通过以下方式:

  1. 端口耗尽模拟:编写一个程序,直接注入并频繁创建新的HttpClient 实例(或长时间保持单个实例但进行大量并发请求)。监控系统资源,特别是网络端口使用情况。在高并发请求下,可能会观察到可用端口数量快速减少直至耗尽,这证明了资源管理问题的存在。

  2. 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 状态下等待 2MSLmaximum segment lifetime)后才能释放连接(端口),目的是为了在 TCP 四次挥手关闭连接机制中,保证 ACK 重发和丢弃延迟数据。按照解释来看,这种做法也算是合情合理,保证数据传输嘛,但的确就是占用资源啦;具体关于网络的相关知识,小伙伴们再去查阅一下。

3、对比 HashCode

除了上面的测试方式,我们还可以简单的对比下构造函数 DI 注入 HttpClientIHttpClientFactory 对象分别生产 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 和构造函数 DIHttpClient 对象,它们的哈希代码是一样的。

  • 使用 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
  • 《使用 .NETIHttpClientFactory》,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
;