Dotnet Core IHttpClientFactory深度研究

今天,我們深度研究一下IHttpClientFactory。

一、前言

最早,我們是在Dotnet Framework中接觸到HttpClient

HttpClient給我們提供了與HTTP交互的基本方式。但這個HttpClient在大量頻繁使用時,也會給我們拋出兩個大坑:一方面,如果我們頻繁創建和釋放HttpClient實例,會導致Socket套接字資源耗盡,原因是因爲Socket關閉後的TIME_WAIT時間。這個問題不展開說,如果需要可以去查TCP的生命週期。而另一方面,如果我們創建一個HttpClient單例,那當被訪問的HTTPDNS記錄發生改變時,會拋出異常,因爲HttpClient並不會允許這種改變。

現在,對於這個內容,有了更優的解決方案。

從Dotnet Core 2.1開始,框架提供了一個新的內容:IHttpClientFactory

IHttpClientFactory用來創建HTTP交互的HttpClient實例。它通過將HttpClient的管理和用於發送內容的HttpMessageHandler鏈分離出來,來解決上面提到的兩個問題。這裏面,重要的是管理管道終端HttpClientHandler的生命週期,而這個就是實際連接的處理程序。

除此之外,IHttpClientFactory還可以使用IHttpClientBuilder方便地來定製HttpClient和內容處理管道,通過前置配置創建出的HttpClient,實現諸如設置基地址或添加HTTP頭等操作。

    爲防止非授權轉發,這兒給出本文的原文鏈接:https://www.cnblogs.com/tiger-wang/p/13752297.html

先來看一個簡單的例子:

public void ConfigureServices(IServiceCollection services)
{
    services.AddHttpClient("WangPlus", c =>
    {
        c.BaseAddress = new Uri("https://github.com/humornif");
    })
    .ConfigureHttpClient(c =>
    {
        c.DefaultRequestHeaders.Add("Accept""application/vnd.github.v3+json");
        c.DefaultRequestHeaders.Add("User-Agent""HttpClientFactory-Sample");
    });
}

在這個例子中,當調用ConfigureHttpClient()AddHttpMessageHandler()來配置HttpClient時,實際上是在向IOptions的實例HttpClientFactoryOptions添加配置。這個方法提供了非常多的配置選項,具體可以去看微軟的文檔,這兒不多說。

在類中使用IHttpClientFactory時,也是同樣的方式:創建一個IHttpClientFactory的單例實例,然後調用CreateClient(name)創建一個具有名稱WangPlusHttpClient

看下面的例子:

public class MyService
{

    private readonly IHttpClientFactory _factory;
    public MyService(IHttpClientFactory factory)
    
{
        _factory = factory;
    }
    public async Task DoSomething()
    
{
        HttpClient client = _factory.CreateClient("WangPlus");
    }
}

用法很簡單。

下面,我們會針對CreateClient()進行剖析,來深入理解IHttpClientFactory背後的內容。

二、HttpClient & HttpMessageHandler的創建過程

CreateClient()方法是與IHttpClientFactory交互的主要方法。

看一下CreateClient()的代碼實現:

private readonly IOptionsMonitor<HttpClientFactoryOptions> _optionsMonitor

public HttpClient CreateClient(string name)
{
    HttpMessageHandler handler = CreateHandler(name);
    var client = new HttpClient(handler, disposeHandler: false);

    HttpClientFactoryOptions options = _optionsMonitor.Get(name);
    for (int i = 0; i < options.HttpClientActions.Count; i++)
    {
        options.HttpClientActions[i](client);
    }

    return client;
}

代碼看上去很簡單。首先通過CreateHandler()創建了一個HttpMessageHandler的處理管道,並傳入要創建的HttpClient的名稱。

有了這個處理管道,就可以創建HttpClient並傳遞給處理管道。這兒需要注意的是disposeHandler:false,這個參數用來保證當我們釋放HttpClient的時候,處理管理不會被釋放掉,因爲IHttpClientFactory會自己完成這個管道的處理。

然後,從IOptionsMonitor的實例中獲取已命名的客戶機的HttpClientFactoryOptions。它來自Startup.ConfigureServices()中添加的HttpClient配置函數,並設置了BaseAddressHeader等內容。

最後,將HttpClient返回給調用者。

理解了這個內容,下面我們來看看CreateHandler(name)方法,研究一下HttpMessageHandler管道是如何創建的。

readonly ConcurrentDictionary<string, Lazy<ActiveHandlerTrackingEntry>> _activeHandlers;;

readonly Func<string, Lazy<ActiveHandlerTrackingEntry>> _entryFactory = (name) =>
    {
        return new Lazy<ActiveHandlerTrackingEntry>(() =>
        {
            return CreateHandlerEntry(name);
        }, LazyThreadSafetyMode.ExecutionAndPublication);
    };

public HttpMessageHandler CreateHandler(string name)
{
    ActiveHandlerTrackingEntry entry = _activeHandlers.GetOrAdd(name, _entryFactory).Value;

    entry.StartExpiryTimer(_expiryCallback);

    return entry.Handler;
}

看這段代碼:CreateHandler()做了兩件事:

  1. 創建或獲取ActiveHandlerTrackingEntry
  2. 開始一個計時器。

_activeHandlers是一個ConcurrentDictionary<>,裏面保存的是HttpClient的名稱(例如上面代碼中的WangPlus)。這裏使用Lazy<>是一個使GetOrAdd()方法保持線程安全的技巧。實際創建處理管道的工作在CreateHandlerEntry中,它創建了一個ActiveHandlerTrackingEntry

ActiveHandlerTrackingEntry是一個不可變的對象,包含HttpMessageHandlerIServiceScope注入。此外,它還包含一個與StartExpiryTimer()一起使用的內部計時器,用於在計時器過期時調用回調函數。

看一下ActiveHandlerTrackingEntry的定義:

internal class ActiveHandlerTrackingEntry
{

    public LifetimeTrackingHttpMessageHandler Handler { get; private set; }
    public TimeSpan Lifetime { get; }
    public string Name { get; }
    public IServiceScope Scope { get; }
    public void StartExpiryTimer(TimerCallback callback)
    
{
        // Starts the internal timer
        // Executes the callback after Lifetime has expired.
        // If the timer has already started, is noop
    }
}

因此CreateHandler方法要麼創建一個新的ActiveHandlerTrackingEntry,要麼從字典中檢索條目,然後啓動計時器。

下一節,我們來看看CreateHandlerEntry()方法如何創建ActiveHandlerTrackingEntry實例。

三、在CreateHandlerEntry中創建和跟蹤HttpMessageHandler

CreateHandlerEntry方法是創建HttpClient處理管道的地方。

這個部分代碼有點複雜,我們簡化一下,以研究過程爲主:

private readonly IServiceProvider _services;

private readonly IHttpMessageHandlerBuilderFilter[] _filters;

private ActiveHandlerTrackingEntry CreateHandlerEntry(string name)
{
    IServiceScope scope = _services.CreateScope(); 
    IServiceProvider services = scope.ServiceProvider;
    HttpClientFactoryOptions options = _optionsMonitor.Get(name);

    HttpMessageHandlerBuilder builder = services.GetRequiredService<HttpMessageHandlerBuilder>();
    builder.Name = name;

    Action<HttpMessageHandlerBuilder> configure = Configure;
    for (int i = _filters.Length - 1; i >= 0; i--)
    {
        configure = _filters[i].Configure(configure);
    }

    configure(builder);

    var handler = new LifetimeTrackingHttpMessageHandler(builder.Build());

    return new ActiveHandlerTrackingEntry(name, handler, scope, options.HandlerLifetime);

    void Configure(HttpMessageHandlerBuilder b)
    
{
        for (int i = 0; i < options.HttpMessageHandlerBuilderActions.Count; i++)
        {
            options.HttpMessageHandlerBuilderActions[i](b);
        }
    }
}

先用根DI容器創建一個IServiceScope,從關聯的IServiceProvider中獲取關聯的服務,再從HttpClientFactoryOptions中找到對應名稱的HttpClient和它的配置。

從容器中查找的下一項是HttpMessageHandlerBuilder,默認值是DefaultHttpMessageHandlerBuilder,這個值通過創建一個主處理程序(負責建立Socket套接字和發送請求的HttpClientHandler)來構建處理管道。我們可以通過添加附加的委託來包裝這個主處理程序,來爲請求和響應創建自定義管理。

附加的委託DelegatingHandlers類似於Core的中間件管道:

  1. Configure()根據Startup.ConfigureServices()提供的配置構建DelegatingHandlers管道;
  2. IHttpMessageHandlerBuilderFilter是注入到IHttpClientFactory構造函數中的過濾器,用於在委託處理管道中添加額外的處理程序。

IHttpMessageHandlerBuilderFilter類似於IStartupFilters,默認註冊的是LoggingHttpMessageHandlerBuilderFilter。這個過濾器向委託管道添加了兩個額外的處理程序:

  1. 管道開始位置的LoggingScopeHttpMessageHandler,會啓動一個新的日誌Scope
  2. 管道末端的LoggingHttpMessageHandler,在請求被髮送到主HttpClientHandler之前,記錄有關請求和響應的日誌;

最後,整個管道被包裝在一個LifetimeTrackingHttpMessageHandler中。管道處理完成後,將與用於創建它的IServiceScope一起保存在一個新的ActiveHandlerTrackingEntry實例中,並給定HttpClientFactoryOptions中定義的生存期(默認爲兩分鐘)。

該條目返回給調用者(CreateHandler()方法),添加到處理程序的ConcurrentDictionary<>中,添加到新的HttpClient實例中(在CreateClient()方法中),並返回給原始調用者。

在接下來的生存期(兩分鐘)內,每當您調用CreateClient()時,您將獲得一個新的HttpClient實例,但是它具有與最初創建時相同的處理程序管道。

每個命名或類型化的HttpClient都有自己的消息處理程序管道。例如,名稱爲WangPlus的兩個HttpClient實例將擁有相同的處理程序鏈,但名爲apiHttpClient將擁有不同的處理程序鏈。

下一節,我們研究下計時器過期後的清理處理。

三、過期清理

以默認時間來說,兩分鐘後,存儲在ActiveHandlerTrackingEntry中的計時器將過期,並觸發StartExpiryTimer()的回調方法ExpiryTimer_Tick()

ExpiryTimer_Tick負責從ConcurrentDictionary<>池中刪除處理程序記錄,並將其添加到過期處理程序隊列中:

readonly ConcurrentQueue<ExpiredHandlerTrackingEntry> _expiredHandlers;

internal void ExpiryTimer_Tick(object state)
{
    var active = (ActiveHandlerTrackingEntry)state;

     _activeHandlers.TryRemove(active.Name, out Lazy<ActiveHandlerTrackingEntry> found);

    var expired = new ExpiredHandlerTrackingEntry(active);
    _expiredHandlers.Enqueue(expired);

    StartCleanupTimer();
}

當一個處理程序從_activeHandlers集合中刪除後,當調用CreateClient()時,它將不再與新的HttpClient一起分發,但會保持在內存存,直到引用此處理程序的所有HttpClient實例全部被清除後,IHttpClientFactory纔會最終釋放這個處理程序管道。

IHttpClientFactory使用LifetimeTrackingHttpMessageHandlerExpiredHandlerTrackingEntry來跟蹤處理程序是否不再被引用。

看下面的代碼:

internal class ExpiredHandlerTrackingEntry
{

    private readonly WeakReference _livenessTracker;

    public ExpiredHandlerTrackingEntry(ActiveHandlerTrackingEntry other)
    
{
        Name = other.Name;
        Scope = other.Scope;

        _livenessTracker = new WeakReference(other.Handler);
        InnerHandler = other.Handler.InnerHandler;
    }

    public bool CanDispose => !_livenessTracker.IsAlive;

    public HttpMessageHandler InnerHandler { get; }
    public string Name { get; }
    public IServiceScope Scope { get; }
}

根據這段代碼,ExpiredHandlerTrackingEntry創建了對LifetimeTrackingHttpMessageHandler的弱引用。根據上一節所寫的,LifetimeTrackingHttpMessageHandler是管道中的“最外層”處理程序,因此它是HttpClient直接引用的處理程序。

LifetimeTrackingHttpMessageHandler使用WeakReference意味着對管道中最外層處理程序的直接引用只有在HttpClient中。一旦垃圾收集器收集了所有這些HttpClientLifetimeTrackingHttpMessageHandler將沒有引用,因此也將被釋放。ExpiredHandlerTrackingEntry可以通過WeakReference.IsAlive檢測到。

在將一個記錄添加到_expiredHandlers隊列之後,StartCleanupTimer()將啓動一個計時器,該計時器將在10秒後觸發。觸發後調用CleanupTimer_Tick()方法,檢查是否對處理程序的所有引用都已過期。如果是,處理程序和IServiceScope將被釋放。如果沒有,它們被添加回隊列,清理計時器再次啓動:

internal void CleanupTimer_Tick()
{
    StopCleanupTimer();

    int initialCount = _expiredHandlers.Count;
    for (int i = 0; i < initialCount; i++)
    {
        _expiredHandlers.TryDequeue(out ExpiredHandlerTrackingEntry entry);

        if (entry.CanDispose)
        {
            try
            {
                entry.InnerHandler.Dispose();
                entry.Scope?.Dispose();
            }
            catch (Exception ex)
            {
            }
        }
        else
        {
            _expiredHandlers.Enqueue(entry);
        }
    }

    if (_expiredHandlers.Count > 0)
    {
        StartCleanupTimer();
    }
}

爲了看清代碼的流程,這個代碼我簡單了。原始的代碼中還有日誌記錄和線程鎖相關的內容。

這個方法比較簡單:遍歷ExpiredHandlerTrackingEntry記錄,並檢查是否刪除了對LifetimeTrackingHttpMessageHandler處理程序的所有引用。如果有,處理程序和IServiceScope就會被釋放。

如果仍然有對任何LifetimeTrackingHttpMessageHandler處理程序的活動引用,則將條目放回隊列,並再次啓動清理計時器。

四、總結

如果你看到了這兒,那說明你還是很有耐心的。

這篇文章是一個對源代碼的研究,能夠幫我們理解IHttpClientFactory的運行方式,以及它是以什麼樣的方式填補了舊的HttpClient的坑。

有些時候,看看源代碼,還是很有益處的。

 

 


 

微信公衆號:老王Plus

掃描二維碼,關注個人公衆號,可以第一時間得到最新的個人文章和內容推送

本文版權歸作者所有,轉載請保留此聲明和原文鏈接

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章