HttpClient vs HttpClientFactory
早期HttpClient---> .NET 3.0 HttpClient/ IHttpCientFactory
1. 早期.NET HttpClient遇到的Socket滥用/DNS解析问题
使用早期 .NET的HttpClient
时,开发者可能会遇到套接字滥用和 DNS解析相关的问题, 而这两个问题是非此即彼的出现。
1>. Socket滥用
“滥用” 指的是使用者不明白Httpclient底层实现和底层tcp连接的背景,而囫囵吞枣的编写
using(var client = new HttpClient()) // httpclient 实现IDispose接口,开发者会自然而然地使用using语法糖
{
} 这样的代码导致的 Socket耗尽和端口耗尽问题。
① 早期版本的HttpClient有tcp连接池化技术, 但是并不提供对于池内连接的生命周期管理能力, 高并发频繁创建tcp实例, 最终会导致套接字耗尽。
② HttpClient实现了IDispose接口,频繁释放HttpClient实例,会关闭tcp连接; tcp连接在4次挥手后,TCP 端口不会在连接关闭后立即释放,(有关这一点的详细信息,请参阅 RFC 9293 中的 TCP TIME-WAIT), 这会导致主机的端口耗尽。
解决以上问题的思路: 重用 HttpClient
实例, 但是重用httpClient又带来了DNS解析问题。
2>. DNS 解析问题
HttpClient 仅在创建连接时解析 DNS 条目。 它不跟踪 DNS 服务器指定的任何生存时间 (TTL),这可能导致 DNS 解析结果不及时更新。
2. .NET Core2.1+ HttpClient 增强自身证明自己
意识到重用httpClient带上的dns解析副作用之后, ASP.NET团队和 .NET 团队 分别给出路线来尝试解决这个问题,
前者推出了IHttpClientFactory 具备生命周期管理能力, 后者推出了新版的HttpClient。
这个版本的HttpClient 要解决早期HttpClient 非此即彼的Socket滥用和DNS解析问题,它的思路是哪里弱, 我就强化哪里。
.NET Core 2.1在HttpClient内部核心处理链中提供SocketsHttpHandler ,这个Handler为连接池中的连接提供了生命周期管理的能力。
与连接生命周期相关的三个关键属性
var handler = new SocketsHttpHandler { PooledConnectionLifetime = TimeSpan.FromMinutes(15), // 限制连接的生命周期,默认无限 Recreate every 15 minutes, 这个配置可用于缓解DNS解析问题 PooledConnectionIdleTimeout = TimeSpan.FromMinutes(2), // 空闲连接在连接池中的存活时间, 默认2min MaxConnectionsPerServer = 100, // 定义到每个目标服务节点能建立的最大连接数 默认是int.MaxValue }; var sharedClient = new HttpClient(handler);
都聊到此了,必然推荐使用单例模式重用 HttpClient 实例,调整PooledConnectionLifetime 缓解DNS解析问题。
初始化对应的源码如下 :
/// <summary>Initializes a new instance of the <see cref="T:System.Net.Http.HttpClient" /> class with the specified handler. The handler is disposed when this instance is disposed.</summary> /// <param name="handler">The HTTP handler stack to use for sending requests.</param> /// <exception cref="T:System.ArgumentNullException">The <paramref name="handler" /> is <see langword="null" />.</exception> public HttpClient(HttpMessageHandler handler) : this(handler, true) { }
/// <summary>Initializes a new instance of the <see cref="T:System.Net.Http.HttpClient" /> class with the provided handler, and specifies whether that handler should be disposed when this instance is disposed.</summary>
/// <param name="handler">The <see cref="T:System.Net.Http.HttpMessageHandler" /> responsible for processing the HTTP response messages.</param>
/// <param name="disposeHandler">
/// <see langword="true" /> if the inner handler should be disposed of by HttpClient.Dispose; <see langword="false" /> if you intend to reuse the inner handler.</param>
/// <exception cref="T:System.ArgumentNullException">The <paramref name="handler" /> is <see langword="null" />.</exception>
public HttpClient(HttpMessageHandler handler, bool disposeHandler)
: base(handler, disposeHandler)
{
this._timeout = HttpClient.s_defaultTimeout;
this._maxResponseContentBufferSize = int.MaxValue;
this._pendingRequestsCts = new CancellationTokenSource();
}
参数1: 用于发出请求的handler;
参数2:传入的是true, 意味着HttpClient.Dispose时,释放内部的Handler, 因为是单例重用, 故给一次dispose的时机也是合适的。
3. IHttpClientFactory: 曲线救国
IHttpClientFactory从HttpClient上层解决了该问题,它的思路是: 哪里弱,我绕开他,从上层封装。
IHttpClientFactory在HttpClient级别上提供HttpClientHandler 生命周期的跟踪和管理能力,它的管理方式更像是对于HttpClientHandler实例的缓存管理。
- 默认的生命周期是2min, 跟踪handler生命周期并非池化技术, 故构建器只提供了 SetHandlerLifetime(TimeSpan.FromMinutes(5) ) 这一个配置函数。
- 上层用定时缓存的思路实现了统一管理HttpClientHandler生命周期, 也就意味着工厂产生的瞬态HttpClient 并不会被瞬态dispose
new HttpClient(handler, disposeHandler: false)
传入的是false, 意味着HttpClient.Dispose时,不释放内部的Handler
public HttpClient CreateClient(string name) { ThrowHelper.ThrowIfNull(name); HttpMessageHandler handler = CreateHandler(name); var client = new HttpClient(handler, disposeHandler: false); //dispose HttpClient时, 不释放内部的Handler HttpClientFactoryOptions options = _optionsMonitor.Get(name); for (int i = 0; i < options.HttpClientActions.Count; i++) { options.HttpClientActions[i](client); } return client; }
public HttpMessageHandler CreateHandler(string name)
{
ThrowHelper.ThrowIfNull(name);
ActiveHandlerTrackingEntry entry = _activeHandlers.GetOrAdd(name, _entryFactory).Value;
StartHandlerEntryTimer(entry);
return entry.Handler;
}
最底层释放HttpClientHandler的源码在HttpMessageInvoker 类:
IHttpClientFactory 使用定时器来清除上层封装的Handler:
- 上层封装的缓存HttpClientHandler: LifetimeTrackingHttpMessageHandler
- 定时器清除 过期缓存码 https://github.com/dotnet/runtime/blob/ab7e12334f07b72e67b897e9ae7a09d04fd1efb5/src/libraries/Microsoft.Extensions.Http/src/DefaultHttpClientFactory.cs#L278C49-L278C52
IHttpCLientFactory 工厂除了具备 “管理基础 HttpClientHandler 实例的缓存和生存期,可避免手动管理 HttpClient
生存期时出现的常见域名系统 (DNS) 问题”, 还具有
- HttpClient实例的产生更符合.NET 框架的调性: DI、 以委托方式配置HttpClient中间件的惯例
- 中心化配置、 命名或者类型化客户端
- 提供基于 Polly 的中间件的扩展方法,以利用
HttpClient
中的委托处理程序。 - (通过 ILogger)添加可配置的记录体验,以处理工厂创建的客户端发送的所有请求。
https://www.stevejgordon.co.uk/httpclient-connection-pooling-in-dotnet-core
4. HttpClient 已经默认启用http2, 上述问题还存在吗 ?
http2并不能自动解决 Socket滥用和DNS解析相关的问题,但http2 多路复用和头部压缩在一定程度上改善了问题。
- 重用 HttpClient
实例仍然是关键,以避免频繁创建和销毁实例导致的套接字耗尽。
- HTTP/2 本身并不解决 DNS 缓存和解析问题。依然需要通过适当的配置来管理 DNS 解析:
PooledConnectionLifetime = TimeSpan.FromMinutes(15)
4.1 Http2协商升级
-
h2c:
- 客户端首先通过 HTTP/1.1 发起请求,并在请求头中包含
Upgrade: h2c
头部字段,表示希望升级到 HTTP/2。 - 如果服务器支持 HTTP/2,并且同意升级,则会在响应中返回状态码
101 Switching Protocols
,并随后切换到 HTTP/2 通信。 - 如果服务器不支持 HTTP/2,则继续使用 HTTP/1.1 进行通信。
- 客户端首先通过 HTTP/1.1 发起请求,并在请求头中包含
-
ALPN(Application-Layer Protocol Negotiation):
- ALPN 是在 TLS 握手过程中用于协商协议版本的扩展。
- 当客户端和服务器都支持 HTTPS 时,客户端会在 TLS ClientHello 消息中通过 ALPN 扩展指明它支持的协议(例如
h2
表示 HTTP/2)。 - 服务器在 TLS ServerHello 消息中选择一个客户端支持的协议。如果选择了 HTTP/2,接下来的通信就会使用 HTTP/2。
- ALPN 是目前最常用的协商 HTTP/2 的方法,特别是在使用 HTTPS 的情况下,因为它是无缝且高效的。
h2c
是 "HTTP/2 over cleartext" 的缩写。它指的是在没有加密的情况下,通过明文(cleartext)进行 HTTP/2 协议通信。这种方式通常用于内网环境或可信网络中,主要用于减少加密带来的性能开销。
ALPN 这是最常用的 HTTP/2 传输方式,通过加密的 TLS(传输层安全协议)进行传输。大多数现代浏览器和服务器都要求通过 TLS 使用 HTTP/2,以确保数据传输的安全性和隐私。
5. HttpClientFactory 使用方式
ASP.NET Core 在 2.1 之后推出了具有弹性 HTTP 请求能力的 HttpClient 工厂类 HttpClientFactory。
HttpClientFactory 以模块化、可命名、可配置、弹性方式重建了 HttpClient 的使用方式:
由 DI 框架注入 IHttpClientFactory 工厂;由工厂创建 HttpClient 并从内部的 Handler池分配请求 Handler (httpclient连接池、sqlclient连接池都是复用底层的tcp连接)。
HttpClient 可在 DI 框架中通过
IHttpCLientBuilder
对象配置 Policy 策略。
一个完整的 HttpClient 包括三部分:
-
基础业务配置:BaseAddress、DefaultRequestHeaders、DefaultProxy、TimeOut.....
-
核心 MessageHandler:负责核心的业务请求
-
[可选的]附加 HttpMessageHandler
附加的 HttpMessageHandler 需要与核心 HttpMessageHandler 形成链式 Pipeline 关系,最终端点指向核心 HttpMessageHandler,
链表数据结构是 DelegatingHandler 关键类(包含 InnerHandler 链表节点指针)
P1. 构建 HttpClient
在 Startup.cs 文件开始配置要用到的 HttpClient
services.AddHttpClient("bce-request", x => x.BaseAddress = new Uri(Configuration.GetSection("BCE").GetValue<string>("BaseUrl"))) .ConfigurePrimaryHttpMessageHandler(_ => new BceAuthClientHandler() { AccessKey = Configuration.GetSection("BCE").GetValue<string>("AccessKey"), SerectAccessKey = Configuration.GetSection("BCE").GetValue<string>("SecretAccessKey"), AllowAutoRedirect = true, UseDefaultCredentials = true }) .SetHandlerLifetime(TimeSpan.FromHours(12)) .AddPolicyHandler(GetRetryPolicy(3));
配置过程充分体现了.NET Core 推崇的万物皆服务,配置前移
的 DI 风格;
同对时 HttpClient 的基础、配置均通过配置即委托
来完成
Q1. 如何记录以上配置?
微软使用一个HttpClientFactoryOptions
对象来记录 HttpClient 配置,这个套路是不是很熟悉?
-
通过 DI 框架的
AddHttpClient
扩展方法产生 HttpClientBuilder 对象 -
HttpClientBuilder 对象的
ConfigurePrimaryHttpMessageHandler
扩展方法会将核心 Handler 插到 Options 对象的 HttpMessageHandlerBuilderActions 数组,作为 Handlers 数组中的 PrimaryHandler -
HttpClientBuilder 对象的
AddPolicyHandler
扩展方法也会将 PolicyHttpMessageHandler 插到 Options 对象的 HttpMessageHandlerBuilderActions 数组,作为 AdditionHandler
// An options class for configuring the default System.Net.Http.IHttpClientFactory public class HttpClientFactoryOptions { public HttpClientFactoryOptions(); // 一组用于配置HttpMessageHandlerBuilder的操作委托 public IList<Action<HttpMessageHandlerBuilder>> HttpMessageHandlerBuilderActions { get; } public IList<Action<HttpClient>> HttpClientActions { get; } public TimeSpan HandlerLifetime { get; set; } public bool SuppressHandlerScope { get; set; } }
显而易见,后期创建 HttpClient 实例时会通过 name 找到对应的 Options,从中加载配置和 Handlers。
P2. 初始化 HttpClient 实例
通过 IHttpClientFactory.CreateClient() 产生的 HttpClient 实例有一些内部行为:
标准的 HttpClient(不带 Policy 策略)除了 PrimaryHandler 之外,微软给你附加了两个 AdditionHandler:
-
LoggingScopeHttpMessageHandler:最外围 Logical 日志
-
LoggingHttpMessageHandler:核心 Http 请求日志
之后将排序后的 AdditionHanders 数组与 PrimaryHandler 通过 DelegatingHandler 数据结构转化为链表, 末节点是 PrimaryHandler
输出的日志如下:
Q2. 微软为啥要增加外围日志 Handler?
这要结合 P1 给出的带 Policy 策略的 HttpClient,带 Policy 策略的 HttpClient 会在 AdditionHandlers 插入 PolicyHttpMessageHandler 来控制retry
、Circuit Breaker
,那么就会构建这样的 Handler Pipeline:
所以微软会在 AdditionHandlers 数组最外围提供一个业务含义的日志 LogicalHandler,最内层固定 LoggingHttpHandler,这是不是很靠谱?
无图无真相,请查看带Policy策略
的 HttpClient 请求堆栈:
Q3. 何处强插、强行固定这两个日志 Handler?
微软通过在 DI 环节注入默认的 LoggingHttpMessageHandlerBuilderFilter 来重排 Handler 的位置:
// 截取自LoggingHttpMessageHandlerBuilderFilter文件
public Action<HttpMessageHandlerBuilder> Configure(Action<HttpMessageHandlerBuilder> next) { return (builder) => { next(builder); var loggerName = !string.IsNullOrEmpty(builder.Name) ? builder.Name : "Default"; // We want all of our logging message to show up as-if they are coming from HttpClient, // but also to include the name of the client for more fine-grained control. var outerLogger = _loggerFactory.CreateLogger($"System.Net.Http.HttpClient.{loggerName}.LogicalHandler"); var innerLogger = _loggerFactory.CreateLogger($"System.Net.Http.HttpClient.{loggerName}.ClientHandler"); var options = _optionsMonitor.Get(builder.Name); // The 'scope' handler goes first so it can surround everything. builder.AdditionalHandlers.Insert(0, new LoggingScopeHttpMessageHandler(outerLogger, options)); // We want this handler to be last so we can log details about the request after // service discovery and security happen. builder.AdditionalHandlers.Add(new LoggingHttpMessageHandler(innerLogger, options)); }; }
Q4. 创建 HttpClient 时,如何将 AdditionHandlers 和 PrimaryHandler 形成链式 Pipeline 关系 ?
protected internal static HttpMessageHandler CreateHandlerPipeline(HttpMessageHandler primaryHandler, IEnumerable<DelegatingHandler> additionalHandlers) { var additionalHandlersList = additionalHandlers as IReadOnlyList<DelegatingHandler> ?? additionalHandlers.ToArray(); var next = primaryHandler; for (var i = additionalHandlersList.Count - 1; i >= 0; i--) { var handler = additionalHandlersList[i]; if (handler == null) { var message = Resources.FormatHttpMessageHandlerBuilder_AdditionalHandlerIsNull(nameof(additionalHandlers)); throw new InvalidOperationException(message); } handler.InnerHandler = next; next = handler; } }
数组转链表IReadOnlyList<DelegatingHandler>
的算法与 ASP.NET Core 框架的 Middleware 构建 Pipeline 如出一辙。
伪代码演示实例创建过程:
DefaultHttpClientFactory.CreateClient()
--->构造函数由 DI 注入默认的 LoggingHttpMessageHandlerBuilderFilter
--->通过 Options.HttpMessageHandlerBuilderActions 拿到所有的 Handlers
--->使用 LoggingHttpMessageHandlerBuilderFilter 强排 AdditionHandlers
--->创建 Handler 链式管道
--->用以上链式初始化 HttpClient 实例
--->从 Options.HttpClientActions 中提取对于 Httpclient 的基础配置
--->返回一个基础、HttpHandler 均正确配置的 HttpClient 实例
上述行为依赖于 ASP.NETCor 框架在 DI 阶段注入的几个服务:
-
DefaultHttpClientFactory
-
LoggingHttpMessageHandlerBuilderFilter:过滤并强排 AdditionHandlers
-
DefaultHttpMessageHandlerBuilder:Handler数组转链表
总结
本文从早期的HttpClient带来的尴尬问题(重用HttpClient带来的DNS解析问题), 聊到.NET团队尝试解决该问题的两个思路。
.NET Core 2.1 的思路是增强HttpClient库底层的连接池,提供了SocketsHttpHandler来控制连接的生命周期,
IHttpClientFactory的思路是绕过HttpClient本身的问题,在上层用缓存的思路来控制HttpClient 实例。
https://learn.microsoft.com/en-us/dotnet/fundamentals/networking/http/httpclient-guidelines
现代的IHttpClientFactory 在框架层面提供了管理HttpClient实例的能力,是另外一个思路。
- https://github.com/dotnet/runtime/tree/main/src/libraries/Microsoft.Extensions.Http/src
本文来自博客园,作者:{有态度的马甲},转载请注明原文链接:https://www.cnblogs.com/JulianHuang/p/12405973.html
欢迎关注我的原创技术、职场公众号, 加好友谈天说地,一起进化