IHttpClientFactory 知多少
在编程便利度上,我们很轻松地使用HttpClient对象发出HTTP请求,只需要关注应用层协议的BaseAddr、Url、ReqHeader、timeout。
实际在HttpClient在源码级别是由HttpMessageHandler在躬身前行。
为解决重用早期.NET HttpClient带来的DNS解析副作用
1. ASP.NETCore IHttpClientFactory缓存工厂
IHttpClientFactory 充分体现了“
计算机领域的任何问题都可以通过增加一个间接的中间层来解决” 这一方法论。
为解决重用HttpClient引起的DNS解析副作用,IHttpClientFactory对实际使用的核心HttpClienthandler开启了缓存工厂模式,在外侧尝试跟踪并控制Handler的存活周期。
① 通过IHttpClientFactory注入的命名的/类型化的HttpClient实例,底层核心的Handler 来自缓存字典;
② 缓存字典中的缓存项默认2min,意味着2min时间内产生的命名HttpClient实例都是引用同一个核心HttpMessageHandler实例(LifeTimeTrackingHttpMessageHandler);
public HttpClient CreateClient(string name) { ThrowHelper.ThrowIfNull(name); HttpMessageHandler handler = CreateHandler(name); var client = new HttpClient(handler, disposeHandler: false); 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; }
缓存是用线程安全的字典ConcurrentDictionary以惰性生成的方式实现:
_activeHandlers =new ConcurrentDictionary<string, Lazy<ActiveHandlerTrackingEntry>>(StringComparer.Ordinal);_entryFactory = (name) =>
{
return new Lazy<ActiveHandlerTrackingEntry>(() =>
{
return CreateHandlerEntry(name);
}, LazyThreadSafetyMode.ExecutionAndPublication);
};
缓存的是LifeTimeTrackingHttpMessageHandler对象,这是一个托管资源。
③ 每个活跃的核心handler上外挂了存活时间, 一旦到期便从活跃字典中移出, 并移动到过期handler队列;
internal sealed class ExpiredHandlerTrackingEntry { private readonly WeakReference _livenessTracker; // IMPORTANT: don't cache a reference to `other` or `other.Handler` here. // We need to allow it to be GC'ed. public ExpiredHandlerTrackingEntry(ActiveHandlerTrackingEntry other) { Name = other.Name; Scope = other.Scope; _livenessTracker = new WeakReference(other.Handler); // 跟踪LifeTimeTrackingHttpMessageHandler 托管资源 InnerHandler = other.Handler.InnerHandler!; // InnerHandler 是托管资源内部引用的非托管资源 } public bool CanDispose => !_livenessTracker.IsAlive; public HttpMessageHandler InnerHandler { get; } public string Name { get; } public IServiceScope? Scope { get; } }
托管资源LifeTimeTrackingHttpMessageHandler 不接受dispose(httpclient)的指引,而是由gc跟踪再无HttpClient引用而被清理。
A : 这里使用了一个C#高级的用法:弱引用WeakReferance :能够在不影响gc的情况下,获得对象的“弱引用”, 并据此知道该实例是不是已经被gc清理了。
_livenessTracker弱引用了托管资源LifeTimeTrackingHttpMessageHandler, 该资源不再被引用时gc会去清理;
btw,关于弱引用,我会开一份新篇章来讲述。
④ 最后由程序内置的定时清理程序来清理底层非托管资源。
if (entry.CanDispose) //跟踪到托管对象已经被gc { try { entry.InnerHandler.Dispose();
entry.Scope?.Dispose(); disposedCount++; } catch (Exception ex) { Log.CleanupItemFailed(_logger, entry.Name, ex); } }
注意: InnerHandler并不是托管对象LifeTimeTrackingHttpMessageHandler, 而是其底层的非托管资源hander
在使用层面, IHttpClientFactory并非直接管控连接池连接,而是在外层对Handler做存活缓存,故工厂对外只提供了SetHandlerLifetime(TimeSpan.FromMinutes(5) ) 这一个配置函数。
IHttpCLientFactory 工厂除了具备 “管理基础 HttpClientHandler 实例的缓存和生存期,可避免手动管理 HttpClient
生存期时出现的常见域名系统 (DNS) 问题”, 还具有
- HttpClient实例的产生更符合.NET 框架的调性: DI、 以委托方式配置HttpClient中间件的惯例
- 中心化配置、 命名或者类型化客户端
- 提供基于 Polly 的中间件的扩展方法,以利用
HttpClient
中的委托处理程序。 - (通过 ILogger)添加可配置的记录体验,以处理工厂创建的客户端发送的所有请求。
https://www.stevejgordon.co.uk/httpclient-connection-pooling-in-dotnet-core
2. 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
本文来自博客园,作者:{有态度的马甲},转载请注明原文链接:https://www.cnblogs.com/JulianHuang/p/12405973.html
欢迎关注我的原创技术、职场公众号, 加好友谈天说地,一起进化
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?