1. Dotnet Core IHttpClientFactory深度研究

        共 5686字,需瀏覽 12分鐘

         ·

        2020-10-11 04:02

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

        ?

        一、前言

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

        HttpClient給我們提供了與HTTP交互的基本方式。但這個(gè)HttpClient在大量頻繁使用時(shí),也會(huì)給我們拋出兩個(gè)大坑:一方面,如果我們頻繁創(chuàng)建和釋放HttpClient實(shí)例,會(huì)導(dǎo)致Socket套接字資源耗盡,原因是因?yàn)?code style="font-size: inherit;line-height: inherit;word-wrap: break-word;padding: 2px 4px;border-top-left-radius: 4px;border-top-right-radius: 4px;border-bottom-right-radius: 4px;border-bottom-left-radius: 4px;margin-right: 2px;margin-left: 2px;color: rgb(233, 105, 0);background-color: rgb(248, 248, 248);">Socket關(guān)閉后的TIME_WAIT時(shí)間。這個(gè)問(wèn)題不展開(kāi)說(shuō),如果需要可以去查TCP的生命周期。而另一方面,如果我們創(chuàng)建一個(gè)HttpClient單例,那當(dāng)被訪問(wèn)的HTTPDNS記錄發(fā)生改變時(shí),會(huì)拋出異常,因?yàn)?code style="font-size: inherit;line-height: inherit;word-wrap: break-word;padding: 2px 4px;border-top-left-radius: 4px;border-top-right-radius: 4px;border-bottom-right-radius: 4px;border-bottom-left-radius: 4px;margin-right: 2px;margin-left: 2px;color: rgb(233, 105, 0);background-color: rgb(248, 248, 248);">HttpClient并不會(huì)允許這種改變。

        現(xiàn)在,對(duì)于這個(gè)內(nèi)容,有了更優(yōu)的解決方案。

        從Dotnet Core 2.1開(kāi)始,框架提供了一個(gè)新的內(nèi)容:IHttpClientFactory。

        IHttpClientFactory用來(lái)創(chuàng)建HTTP交互的HttpClient實(shí)例。它通過(guò)將HttpClient的管理和用于發(fā)送內(nèi)容的HttpMessageHandler鏈分離出來(lái),來(lái)解決上面提到的兩個(gè)問(wèn)題。這里面,重要的是管理管道終端HttpClientHandler的生命周期,而這個(gè)就是實(shí)際連接的處理程序。

        除此之外,IHttpClientFactory還可以使用IHttpClientBuilder方便地來(lái)定制HttpClient和內(nèi)容處理管道,通過(guò)前置配置創(chuàng)建出的HttpClient,實(shí)現(xiàn)諸如設(shè)置基地址或添加HTTP頭等操作。

        先來(lái)看一個(gè)簡(jiǎn)單的例子:

        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");
        ????});
        }

        在這個(gè)例子中,當(dāng)調(diào)用ConfigureHttpClient()AddHttpMessageHandler()來(lái)配置HttpClient時(shí),實(shí)際上是在向IOptions的實(shí)例HttpClientFactoryOptions添加配置。這個(gè)方法提供了非常多的配置選項(xiàng),具體可以去看微軟的文檔,這兒不多說(shuō)。

        ?

        在類中使用IHttpClientFactory時(shí),也是同樣的方式:創(chuàng)建一個(gè)IHttpClientFactory的單例實(shí)例,然后調(diào)用CreateClient(name)創(chuàng)建一個(gè)具有名稱WangPlusHttpClient

        看下面的例子:

        public?class?MyService
        {

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

        用法很簡(jiǎn)單。

        ?

        下面,我們會(huì)針對(duì)CreateClient()進(jìn)行剖析,來(lái)深入理解IHttpClientFactory背后的內(nèi)容。

        二、HttpClient & HttpMessageHandler的創(chuàng)建過(guò)程

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

        看一下CreateClient()的代碼實(shí)現(xiàn):

        private?readonly?IOptionsMonitor?_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[i](client);
        ????}

        ????return?client;
        }

        代碼看上去很簡(jiǎn)單。首先通過(guò)CreateHandler()創(chuàng)建了一個(gè)HttpMessageHandler的處理管道,并傳入要?jiǎng)?chuàng)建的HttpClient的名稱。

        有了這個(gè)處理管道,就可以創(chuàng)建HttpClient并傳遞給處理管道。這兒需要注意的是disposeHandler:false,這個(gè)參數(shù)用來(lái)保證當(dāng)我們釋放HttpClient的時(shí)候,處理管理不會(huì)被釋放掉,因?yàn)?code style="font-size: inherit;line-height: inherit;word-wrap: break-word;padding: 2px 4px;border-top-left-radius: 4px;border-top-right-radius: 4px;border-bottom-right-radius: 4px;border-bottom-left-radius: 4px;margin-right: 2px;margin-left: 2px;color: rgb(233, 105, 0);background-color: rgb(248, 248, 248);">IHttpClientFactory會(huì)自己完成這個(gè)管道的處理。

        然后,從IOptionsMonitor的實(shí)例中獲取已命名的客戶機(jī)的HttpClientFactoryOptions。它來(lái)自Startup.ConfigureServices()中添加的HttpClient配置函數(shù),并設(shè)置了BaseAddressHeader等內(nèi)容。

        最后,將HttpClient返回給調(diào)用者。

        ?

        理解了這個(gè)內(nèi)容,下面我們來(lái)看看CreateHandler(name)方法,研究一下HttpMessageHandler管道是如何創(chuàng)建的。

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

        readonly?Func<string,?Lazy>?_entryFactory?=?(name)?=>
        ????{
        ????????return?new?Lazy(()?=>
        ????????{
        ????????????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. 創(chuàng)建或獲取ActiveHandlerTrackingEntry;

        2. 開(kāi)始一個(gè)計(jì)時(shí)器。

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

        ActiveHandlerTrackingEntry是一個(gè)不可變的對(duì)象,包含HttpMessageHandlerIServiceScope注入。此外,它還包含一個(gè)與StartExpiryTimer()一起使用的內(nèi)部計(jì)時(shí)器,用于在計(jì)時(shí)器過(guò)期時(shí)調(diào)用回調(diào)函數(shù)。

        看一下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方法要么創(chuàng)建一個(gè)新的ActiveHandlerTrackingEntry,要么從字典中檢索條目,然后啟動(dòng)計(jì)時(shí)器。

        ?

        下一節(jié),我們來(lái)看看CreateHandlerEntry()方法如何創(chuàng)建ActiveHandlerTrackingEntry實(shí)例。

        三、在CreateHandlerEntry中創(chuàng)建和跟蹤HttpMessageHandler

        CreateHandlerEntry方法是創(chuàng)建HttpClient處理管道的地方。

        這個(gè)部分代碼有點(diǎn)復(fù)雜,我們簡(jiǎn)化一下,以研究過(guò)程為主:

        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();
        ????builder.Name?=?name;

        ????Action?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[i](b);
        ????????}
        ????}
        }

        先用根DI容器創(chuàng)建一個(gè)IServiceScope,從關(guān)聯(lián)的IServiceProvider中獲取關(guān)聯(lián)的服務(wù),再?gòu)?code style="font-size: inherit;line-height: inherit;word-wrap: break-word;padding: 2px 4px;border-top-left-radius: 4px;border-top-right-radius: 4px;border-bottom-right-radius: 4px;border-bottom-left-radius: 4px;margin-right: 2px;margin-left: 2px;color: rgb(233, 105, 0);background-color: rgb(248, 248, 248);">HttpClientFactoryOptions中找到對(duì)應(yīng)名稱的HttpClient和它的配置。

        從容器中查找的下一項(xiàng)是HttpMessageHandlerBuilder,默認(rèn)值是DefaultHttpMessageHandlerBuilder,這個(gè)值通過(guò)創(chuàng)建一個(gè)主處理程序(負(fù)責(zé)建立Socket套接字和發(fā)送請(qǐng)求的HttpClientHandler)來(lái)構(gòu)建處理管道。我們可以通過(guò)添加附加的委托來(lái)包裝這個(gè)主處理程序,來(lái)為請(qǐng)求和響應(yīng)創(chuàng)建自定義管理。

        ?

        附加的委托DelegatingHandlers類似于Core的中間件管道:

        1. Configure()根據(jù)Startup.ConfigureServices()提供的配置構(gòu)建DelegatingHandlers管道;

        2. IHttpMessageHandlerBuilderFilter是注入到IHttpClientFactory構(gòu)造函數(shù)中的過(guò)濾器,用于在委托處理管道中添加額外的處理程序。

        ?

        IHttpMessageHandlerBuilderFilter類似于IStartupFilters,默認(rèn)注冊(cè)的是LoggingHttpMessageHandlerBuilderFilter。這個(gè)過(guò)濾器向委托管道添加了兩個(gè)額外的處理程序:

        1. 管道開(kāi)始位置的LoggingScopeHttpMessageHandler,會(huì)啟動(dòng)一個(gè)新的日志Scope

        2. 管道末端的LoggingHttpMessageHandler,在請(qǐng)求被發(fā)送到主HttpClientHandler之前,記錄有關(guān)請(qǐng)求和響應(yīng)的日志;

        ?

        最后,整個(gè)管道被包裝在一個(gè)LifetimeTrackingHttpMessageHandler中。管道處理完成后,將與用于創(chuàng)建它的IServiceScope一起保存在一個(gè)新的ActiveHandlerTrackingEntry實(shí)例中,并給定HttpClientFactoryOptions中定義的生存期(默認(rèn)為兩分鐘)。

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

        在接下來(lái)的生存期(兩分鐘)內(nèi),每當(dāng)您調(diào)用CreateClient()時(shí),您將獲得一個(gè)新的HttpClient實(shí)例,但是它具有與最初創(chuàng)建時(shí)相同的處理程序管道。

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

        ?

        下一節(jié),我們研究下計(jì)時(shí)器過(guò)期后的清理處理。

        三、過(guò)期清理

        以默認(rèn)時(shí)間來(lái)說(shuō),兩分鐘后,存儲(chǔ)在ActiveHandlerTrackingEntry中的計(jì)時(shí)器將過(guò)期,并觸發(fā)StartExpiryTimer()的回調(diào)方法ExpiryTimer_Tick()

        ExpiryTimer_Tick負(fù)責(zé)從ConcurrentDictionary<>池中刪除處理程序記錄,并將其添加到過(guò)期處理程序隊(duì)列中:

        readonly?ConcurrentQueue?_expiredHandlers;

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

        ?????_activeHandlers.TryRemove(active.Name,?out?Lazy?found);

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

        ????StartCleanupTimer();
        }

        當(dāng)一個(gè)處理程序從_activeHandlers集合中刪除后,當(dāng)調(diào)用CreateClient()時(shí),它將不再與新的HttpClient一起分發(fā),但會(huì)保持在內(nèi)存存,直到引用此處理程序的所有HttpClient實(shí)例全部被清除后,IHttpClientFactory才會(huì)最終釋放這個(gè)處理程序管道。

        ?

        IHttpClientFactory使用LifetimeTrackingHttpMessageHandlerExpiredHandlerTrackingEntry來(lái)跟蹤處理程序是否不再被引用。

        看下面的代碼:

        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;?}
        }

        根據(jù)這段代碼,ExpiredHandlerTrackingEntry創(chuàng)建了對(duì)LifetimeTrackingHttpMessageHandler的弱引用。根據(jù)上一節(jié)所寫的,LifetimeTrackingHttpMessageHandler是管道中的“最外層”處理程序,因此它是HttpClient直接引用的處理程序。

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

        在將一個(gè)記錄添加到_expiredHandlers隊(duì)列之后,StartCleanupTimer()將啟動(dòng)一個(gè)計(jì)時(shí)器,該計(jì)時(shí)器將在10秒后觸發(fā)。觸發(fā)后調(diào)用CleanupTimer_Tick()方法,檢查是否對(duì)處理程序的所有引用都已過(guò)期。如果是,處理程序和IServiceScope將被釋放。如果沒(méi)有,它們被添加回隊(duì)列,清理計(jì)時(shí)器再次啟動(dòng):

        internal?void?CleanupTimer_Tick()
        {
        ????StopCleanupTimer();

        ????int?initialCount?=?_expiredHandlers.Count;
        ????for?(int?i?=?0;?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();
        ????}
        }

        為了看清代碼的流程,這個(gè)代碼我簡(jiǎn)單了。原始的代碼中還有日志記錄和線程鎖相關(guān)的內(nèi)容。

        這個(gè)方法比較簡(jiǎn)單:遍歷ExpiredHandlerTrackingEntry記錄,并檢查是否刪除了對(duì)LifetimeTrackingHttpMessageHandler處理程序的所有引用。如果有,處理程序和IServiceScope就會(huì)被釋放。

        如果仍然有對(duì)任何LifetimeTrackingHttpMessageHandler處理程序的活動(dòng)引用,則將條目放回隊(duì)列,并再次啟動(dòng)清理計(jì)時(shí)器。

        四、總結(jié)

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

        這篇文章是一個(gè)對(duì)源代碼的研究,能夠幫我們理解IHttpClientFactory的運(yùn)行方式,以及它是以什么樣的方式填補(bǔ)了舊的HttpClient的坑。

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

        喜歡就來(lái)個(gè)三連,讓更多人因你而受益

        瀏覽 48
        點(diǎn)贊
        評(píng)論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報(bào)
        評(píng)論
        圖片
        表情
        推薦
        點(diǎn)贊
        評(píng)論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報(bào)
          
          

            1. 亚洲第八页 | 成人啪啪视频在线 | 欧美日韩福利在线欧美日韩福利在线 | zzijzzij亚洲日本成熟少妇 | 99re这里只有国产精品视频 |