HttpClientFactory的套路,你知多少?

背景

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 來控制retryCircuit 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: 自定義對伺服器證書的認證邏輯