HttpClientFactory的套路,你知多少?
- 2020 年 3 月 4 日
- 筆記
背景
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 數組中的 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 拿到所有的 Handler
—>使用 LoggingHttpMessageHandlerBuilderFilter 強排 AdditionHandlers
—>創建 Handler 鏈式管道
—>用以上鏈式初始化 HttpClient 實例
—>從 Options.HttpClientActions 中提取對於 Httpclient 的基礎配置
—>返回一個基礎、HttpHandler 均正確配置的 HttpClient 實例
上述行為依賴於 ASP.NETCor 框架在 DI 階段注入的幾個服務:
- DefaultHttpClientFactory
- LoggingHttpMessageHandlerBuilderFilter:過濾並強排 AdditionHandler
- DefaultHttpMessageHandlerBuilder: Handler數組轉鏈表
我們探究System.Net.Http庫的目的:
學習精良的設計模式、理解默認的DI行為;
默認DI行為給我們提供了擴展/改造 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
附贈有關應用 HttpClient 的三個瓜:
1. AllowAutoRedirect 屬性 : 控制請求收到重定向響應 能 發起跳轉請求, 默認為 true
2. AutomaticDecompression枚舉: 支援對服務端response 壓縮的解碼
3. ServerCertificateCustomValidationCallback: 自定義對伺服器證書的認證邏輯