今天,我們深度研究一下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()
做了兩件事:
- 建立或擷取
;ActiveHandlerTrackingEntry
- 開始一個計時器。
_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的中間件管道:
-
根據Configure()
提供的配置建構Startup.ConfigureServices()
管道;DelegatingHandlers
-
是注入到IHttpMessageHandlerBuilderFilter
構造函數中的過濾器,用于在委托處理管道中添加額外的處理程式。IHttpClientFactory
IHttpMessageHandlerBuilderFilter
類似于
IStartupFilters
,預設注冊的是
LoggingHttpMessageHandlerBuilderFilter
。這個過濾器向委托管道添加了兩個額外的處理程式:
- 管道開始位置的
,會啟動一個新的日志LoggingScopeHttpMessageHandler
Scope
- 管道末端的
,在請求被發送到主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
的坑。
有些時候,看看源代碼,還是很有益處的。
![]() | 微信公衆号:老王Plus 掃描二維碼,關注個人公衆号,可以第一時間得到最新的個人文章和内容推送 本文版權歸作者所有,轉載請保留此聲明和原文連結 |