天天看點

Dotnet Core IHttpClientFactory深度研究

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

一、前言

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

HttpClient

HttpClient

給我們提供了與

HTTP

互動的基本方式。但這個

HttpClient

在大量頻繁使用時,也會給我們抛出兩個大坑:一方面,如果我們頻繁建立和釋放

HttpClient

執行個體,會導緻

Socket

套接字資源耗盡,原因是因為

Socket

關閉後的

TIME_WAIT

時間。這個問題不展開說,如果需要可以去查

TCP

的生命周期。而另一方面,如果我們建立一個

HttpClient

單例,那當被通路的

HTTP

DNS

記錄發生改變時,會抛出異常,因為

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)

建立一個具有名稱

WangPlus

HttpClient

看下面的例子:

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

配置函數,并設定了

BaseAddress

Header

等内容。

最後,将

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

是一個不可變的對象,包含

HttpMessageHandler

IServiceScope

注入。此外,它還包含一個與

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

執行個體将擁有相同的處理程式鍊,但名為

api

HttpClient

将擁有不同的處理程式鍊。

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

三、過期清理

以預設時間來說,兩分鐘後,存儲在

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

使用

LifetimeTrackingHttpMessageHandler

ExpiredHandlerTrackingEntry

來跟蹤處理程式是否不再被引用。

看下面的代碼:

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

中。一旦垃圾收集器收集了所有這些

HttpClient

LifetimeTrackingHttpMessageHandler

将沒有引用,是以也将被釋放。

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

的坑。

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

Dotnet Core IHttpClientFactory深度研究

微信公衆号:老王Plus

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

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

繼續閱讀