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 拿到所有的 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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM