背景
ASP.NET Core 在 2.1 之后推出了具有弹性 HTTP 请求能力的 HttpClient 工厂类 HttpClientFactory。
替换的初衷还是简单摆一下:
① using(var client = new HttpClient()) 调用的 Dispose() 方法并不会立即释放底层 Socket 连接,新建 Socket 需要时间,导致在高并发场景下 Socket 耗尽。
② 基于 ① 很多人会想到使用单例或者静态类构造 HttpClient 实例,但是这里有一个坑,HttpClient 不会反应 DNS 的变更。
HttpClientFactory 以模块化、可命名、可配置、弹性方式重建了 HttpClient 的使用方式:由 DI 框架注入 IHttpClientFactory 工厂;由工厂创建 HttpClient 并从内部的 Handler 池分配请求 Handler。
HttpClient 可在 DI 框架中通过
IHttpCLientBuilder
对象配置 Policy 策略。
我一直对这种颠覆传统 HttpClient 的代码组织方式感到好奇,今天我们带着问题来探究一下新版 HttpClient 的实现。
与码无瓜
一个完整的 HttpClient 包括三部分:
基础业务配置:BaseAddress、DefaultRequestHeaders、DefaultProxy、TimeOut.....
核心 MessageHandler:负责核心的业务请求
[可选的]附加 HttpMessageHandler
附加的 HttpMessageHandler 需要与核心 HttpMessageHandler 形成链式 Pipeline 关系,最终端点指向核心 HttpMessageHandler,
链表数据结构是 DelegatingHandler 关键类(包含 InnerHandler 链表节点指针)
刨瓜问底
很明显,HttpClientFactory 源码的解读分为 2 部分,心里藏着伪代码,带着问题思考更香(手动狗头)。
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 数组中的 PrimaryHandlerHttpClientBuilder 对象的
AddPolicyHandler
扩展方法也会将 PolicyHttpMessageHandler 插到 Options 对象的 HttpMessageHandlerBuilderActions 数组,但是作为 AdditionHandler
// An options class for configuring the default System.Net.Http.IHttpClientFactorypublic 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数组转链表
我们探究System.Net.Http库的目的:
学习精良的设计模式、理解默认的DI行为;
默认DI行为给我们提供了扩展/改造 HttpClientFactory 的一个思路:HttpClientFactory日志不好用,自己扩展一个?
https://github.com/dotnet/extensions/blob/master/src/HttpClientFactory/Http/src/DependencyInjection/HttpClientFactoryServiceCollectionExtensions.cs
https://github.com/dotnet/extensions/blob/master/src/HttpClientFactory/Http/src/DefaultHttpClientFactory.cs
推荐阅读
● 程序员应对浏览器同源策略的姿势
● 临近年关,修复ASP.NET Core因浏览器内核版本引发的单点登录故障
● ASP.NET Core跨平台技术内幕
● TPL Dataflow组件应对高并发,低延迟要求
● 实例解读Docker Swarm
● 基于docker-compose的Gitlab CI/CD实践&排坑指南