.NET Core 3.0之深入原始碼理解HttpClientFactory(一)

艾心❤發表於2019-07-22

寫在前面

建立HttpClient例項的時候,在內部會建立HttpMessageHandler鏈,我們知道HttpMessageHandler是負責建立連線的抽象處理程式,所以HttpClient的維護實際上就是維護HttpMessageHandler的使用,釋放HttpClient並不會及時釋放連線,而通常情況下一般是建立全域性使用的HttpClient例項,以減少重複連線的次數。當然這種方式所帶來的的弊端也是顯而易見的,因為當前的HttpClient例項所指向的伺服器發生問題或者DNS發生變更,那麼該例項是無法做到自動更新指向的。

以下為執行其流程圖:

128b3ce864895d7d13c28d382c876b7

HttpClientFactory自.NET Core 2.1引入,可以認為它是一個配置和建立HttpClient的中心化,.NET Core通過引入HttpClientFactory用於自動化維護HttpMessageHandler池及其生命週期,避免在手動管理 HttpClient生存期時出現的常見 DNS 問題。在預設情況下MessageHandler的活躍狀態是兩分鐘,也就是說,在兩分鐘後,就可以為HttpClient例項重新定位到正確的主機上。

本文的討論思路將從我們能看到的程式碼開始一步步深入。

詳細介紹

HttpClientFactory的功能主要位於Microsoft.Extensions.Http包中,它已經預設包含在Microsoft.AspNetCore.App元包中。針對HttpClientFactory的處理涉及到IHttpClientBuilder、IHttpClientFactory、IHttpMessageHandlerFactory、ITypedHttpClientFactory這幾大介面,以下將分別做討論。

services.AddHttpClient()

我們在建立或者配置HttpClient物件的時候,會在ConfigureServices方法中增加services.AddHttpClient(),即可註冊IHttpClientFactory。

這段程式碼位於Microsoft.Extensions.DependencyInjection.HttpClientFactoryServiceCollectionExtensions中,它會初始化相關資訊並註冊到IServiceCollection中,這些資訊包括日誌、選項、核心抽象功能、型別客戶端以及其他基礎設施功能。

需要注意的是,在核心抽象功能中,DefaultHttpClientFactory是單例模式的,其所繼承的介面物件的獲取也是單例的,而HttpMessageHandlerBuilder註冊方式確是每一次GetService的時候都會建立一個新的HttpMessageHandlerBuilder例項。

以下為services.AddHttpClient()的原始碼,其中標紅部分為核心抽象功能的註冊:

   1:  public static IServiceCollection AddHttpClient(this IServiceCollection services)
   2:  {
   3:      if (services == null)
   4:      {
   5:          throw new ArgumentNullException(nameof(services));
   6:      }
   7:   
   8:      services.AddLogging();
   9:      services.AddOptions();
  10:     
11: services.TryAddTransient<HttpMessageHandlerBuilder, DefaultHttpMessageHandlerBuilder>(); 12: services.AddSingleton<DefaultHttpClientFactory>(); 13: services.TryAddSingleton<IHttpClientFactory>(serviceProvider => serviceProvider.GetRequiredService<DefaultHttpClientFactory>()); 14: services.TryAddSingleton<IHttpMessageHandlerFactory>(serviceProvider => serviceProvider.GetRequiredService<DefaultHttpClientFactory>());
  15:      
  16:      services.TryAdd(ServiceDescriptor.Transient(typeof(ITypedHttpClientFactory<>), typeof(DefaultTypedHttpClientFactory<>)));
  17:      services.TryAdd(ServiceDescriptor.Transient(typeof(DefaultTypedHttpClientFactory<>.Cache), typeof(DefaultTypedHttpClientFactory<>.Cache)));
  18:      
  19:      services.TryAddEnumerable(ServiceDescriptor.Singleton<IHttpMessageHandlerBuilderFilter, LoggingHttpMessageHandlerBuilderFilter>());
  20:   
  21:      return services;
  22:  }

DefaultHttpClientFactory

DefaultHttpClientFactory是一個用internal修飾的類,意味著該類只能在在其內部使用。它繼承了IHttpClientFactory、IHttpMessageHandlerFactory這兩個介面。由此可見,DefaultHttpClientFactory例項的建立被拆成了兩種行為。

IHttpClientFactory的定位是一個抽象工廠,可以為指定名稱的HttpClient例項建立自定義配置,它只有一個方法,HttpClient CreateClient(string name)。

IHttpMessageHandlerFactory的定位也是一個抽象工廠,它為指定名稱的HttpMessageHandler例項建立自定義配置,它只有一個方法,HttpMessageHandler CreateHandler(string name)。

我們先看一下這兩個方法的實現,會覺得很有意思

   1:  public HttpClient CreateClient(string name)
   2:  {
   3:      if (name == null)
   4:      {
   5:          throw new ArgumentNullException(nameof(name));
   6:      }
   7:   
   8:      var handler = CreateHandler(name);
   9:      var client = new HttpClient(handler, disposeHandler: false);
  10:   
  11:      var options = _optionsMonitor.Get(name);
  12:      for (var i = 0; i < options.HttpClientActions.Count; i++)
  13:      {
  14:          options.HttpClientActions[i](client);
  15:      }
  16:   
  17:      return client;
  18:  }
  19:   
  20:  public HttpMessageHandler CreateHandler(string name)
  21:  {
  22:      if (name == null)
  23:      {
  24:          throw new ArgumentNullException(nameof(name));
  25:      }
  26:   
  27:      var entry = _activeHandlers.GetOrAdd(name, _entryFactory).Value;
  28:   
  29:      StartHandlerEntryTimer(entry);
  30:   
  31:      return entry.Handler;
  32:  }

可以看到,我們通過名稱查詢HttpClient物件的時候,也會依照該名稱以GetOrAdd方式去查詢相應的HttpMessageHandler物件,也就說HttpClient物件和HttpMessageHandler物件可以通過名稱關聯起來。

需要注意的時候在呼叫CreateHandler方法的時候會呼叫StartHandlerEntryTimer方法,這個方法是幹嘛的呢,他維護著定時器。該方法位於Microsoft.Extensions.Http.ActiveHandlerTrackingEntry中,我們將此類視為是一個不可變的(當然,其內部的定時器是變化的),為“到期”池建立一個可以顯著簡化執行緒需求的新物件。

除了這兩個方法外,我們要需要注意DefaultHttpClientFactory對HttpMessageHandler的管理功能。DefaultHttpClientFactory內部維護者一個定時器和兩個HttpMessageHandler物件集合,這兩個集合分別是ActiveHandler和ExpiredHandler。內部定時器會定期從ExpiredHandler集合中掃描並清理無效的 HttpMessageHandler物件。

ActiveHandler集合的增加是在呼叫CreateHandler方法時增加的,其移除是在回撥的時候移除,這個移除入口也只有這一處。

ExpiredHandler集合的增加也是在呼叫CreateHandler方法時,通過內部的一個回撥機制增加的,其移除通過定時器定期掃描來實現的。這處需要注意的是,ExpiredHandlerTrackingEntry這個類中有一個屬性,程式碼如下:

   1:  private readonly WeakReference _livenessTracker;
   1:  public bool CanDispose => !_livenessTracker.IsAlive;

通過WeakReference 型別的變數來標識該HttpMessageHandler物件是否應該被從集合中移除。

定時器一般是個比較消耗資源,而且一旦用不好,就會引發執行緒問題,DefaultHttpClientFactory在處理定時器的時候,首先通過停止所有掛起的計時器,在清除後如果還需要繼續處理無效HttpMessageHandler物件,將會重新啟動計時器,雖然看似多餘了點,但是比通過鎖定整個清理機制來確定是否阻塞清理任何並啟動定時器要好多了。

   1:  internal void CleanupTimer_Tick()
   2:  {
   3:      StopCleanupTimer();
   4:   
   5:      if (!Monitor.TryEnter(_cleanupActiveLock))
   6:      {
   7:          StartCleanupTimer();
   8:          return;
   9:      }
  10:   
  11:      try
  12:      {
  13:          var initialCount = _expiredHandlers.Count;
  14:          Log.CleanupCycleStart(_logger, initialCount);
  17:   
  18:          var disposedCount = 0;
  19:          //開始清理
  20:   
  21:          Log.CleanupCycleEnd(_logger, stopwatch.GetElapsedTime(), disposedCount, _expiredHandlers.Count);
  22:      }
  23:      finally
  24:      {
  25:          Monitor.Exit(_cleanupActiveLock);
  26:      }
  27:   
  28:      if (_expiredHandlers.Count > 0)
  29:      {
  30:          StartCleanupTimer();
  31:      }
  32:  }

  以下為這兩個佇列的處理示意圖:

timer

相關文章