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頭等操作。
先來看一個簡單的例子:
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>?_optionsMonitorpublic?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的坑。
有些時候,看看源代碼,還是很有益處的。
喜歡就來個三連,讓更多人因你而受益
總結
以上是生活随笔為你收集整理的Dotnet Core IHttpClientFactory深度研究的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 使用Azure Functions玩转S
- 下一篇: 秋天 | 等疫情过后,我们继续背起相机去