c# HttpClient設(shè)置超時的步驟
HttpClient作為官方推薦的http客戶端,相比之前的WebClient和WebRequest好用了很多,但默認(rèn)無法為每個請求單獨(dú)設(shè)置超時,只能給HttpClient設(shè)置默認(rèn)超時,使用起來不太方便。
聲明:本文主要是翻譯自THOMAS LEVESQUE'S .NET BLOG的文章:Better timeout handling with HttpClient。
由于工作原因,需要用c#,就語法層而言,c#確實(shí)比java優(yōu)秀,一些庫接口封裝也更方便簡潔。特別是HttpClient,結(jié)合了task異步模型,使用起來非常順手。
本人水平有限,如有問題,還望各位多多海涵,不吝賜教
問題
如果你經(jīng)常用HttpClient去調(diào)用Restfull接口或傳送文件,你可能會對HttpClient這個類處理Request(請求)超時的方式感到惱火,因?yàn)榇嬖谶@兩個問題:
- timeout(超時)只能在HttpClient的class級別處理。也就是說,一旦設(shè)置好了,所有httpClient下的請求都會應(yīng)用同樣的超時設(shè)置,這顯然不靈活,如果能夠?yàn)槊總€request請求分別指定一個超時時間,將非常方便。
- 當(dāng)請求超時時,拋出的異常很不好辨認(rèn)。你認(rèn)為請求超時時,httpclient會拋出TimeoutException?,不,其實(shí)它會拋出一個TaskCanceledException,而單看這個異常,你一時還無法分辨是取消導(dǎo)致的還是真正超時導(dǎo)致的。
幸運(yùn)的是,得益于HttpClient的靈活設(shè)計,可以非常容易的彌補(bǔ)此缺陷。
因此,我們將針對這兩個問題做出解決方案。讓我們回顧一下我們想要的:
- 可以為每個request請求單獨(dú)設(shè)置超時時間
- 當(dāng)超時發(fā)生時,catch的異常是TimeoutException而不是TaskCanceledException。
為每個request設(shè)置超時值
怎樣將超時時間值和Request請求關(guān)聯(lián)起來呢?HttpRequestMessage這個類有個Properties的屬性,它是一個字典(Dictionary)類型的屬性,我們可以放入我們?nèi)魏巫远x需要的內(nèi)容到這個屬性中。我們將使用這個屬性存儲請求(request)的超時時間,為了便于實(shí)現(xiàn)此功能,我們給HttpRequestMessage創(chuàng)建一個擴(kuò)展方法:
public static class HttpRequestExtensions { private static string TimeoutPropertyKey = "RequestTimeout"; public static void SetTimeout( this HttpRequestMessage request, TimeSpan? timeout) { if (request == null) throw new ArgumentNullException(nameof(request)); request.Properties[TimeoutPropertyKey] = timeout; } public static TimeSpan? GetTimeout(this HttpRequestMessage request) { if (request == null) throw new ArgumentNullException(nameof(request)); if (request.Properties.TryGetValue( TimeoutPropertyKey, out var value) && value is TimeSpan timeout) return timeout; return null; } }
這是一段很普通的代碼,timout參數(shù)是可null的TimeSpan值,我們現(xiàn)在可以給請求設(shè)置超時值,但是目前還沒有實(shí)際使用到這段代碼。
Http Handler
HttpClient使用 管道體系( pipeline architecture) 結(jié)構(gòu):每個請求都通過一系列類型為HttpMessageHandler的Handler處理,并且以相反順序逐級返回響應(yīng)。有了這種機(jī)制,我們可以非常方便的加入我們自己的Handler來具體處理超時問題。如果您想了解更多,本文將對此進(jìn)行更詳細(xì)的說明。
我們的自己的超時Handler將繼承DelegatingHandler,DelegatingHandler是一種設(shè)計為鏈?zhǔn)秸{(diào)用其他Handler的類(簡單提一下:DelegatingHandler內(nèi)部有個InnerHandler成員變量,我們可以在調(diào)用innerHandler.SendAsync()前后對request、CancellationToken和response做相應(yīng)處理)。要實(shí)現(xiàn)我們的Handler,我們重寫SendAsync方法。最小的實(shí)現(xiàn)如下所示:
class TimeoutHandler : DelegatingHandler { protected async override Task<HttpResponseMessage> SendAsync( HttpRequestMessage request, CancellationToken cancellationToken) { return await base.SendAsync(request, cancellationToken); } }
上述代碼并沒有任何用處,因?yàn)橹皇菍?shí)際處理丟給了base.SendAsync,目前還沒有對TimeoutHandler進(jìn)行任何加工處理,我們將逐步對其加強(qiáng)擴(kuò)充,以達(dá)到我們的目的。
給Request加上超時處理
首先,讓我們給TimeoutHandler添加一個TimeSpan類型的DefaultTimeout屬性,這個默認(rèn)超時時間是給沒有特意設(shè)置超時時間的請求使用的:
public TimeSpan DefaultTimeout { get; set; } = TimeSpan.FromSeconds(100);
就像HttpClient.Timeout一樣,我們也設(shè)置默認(rèn)超時時間為100秒。
為了實(shí)現(xiàn)我們的超時處理,我們需要從request中獲取超時時間(如果request中沒有設(shè)置,則應(yīng)用DefaultTimeout的值)。接著,我們創(chuàng)建一個在指定時間(超時時間)后將會被取消的CancellationToken,并把這個CancellationToken傳入到鏈的下一個Handler。這樣之后,如果指定超時時間內(nèi)沒有獲取到response響應(yīng),我們剛剛創(chuàng)建的CancellationToken就會被取消(cancel)。
我們創(chuàng)建一個CancellationTokenSource,這個類可以創(chuàng)建和控制CancellationToken。它將根據(jù)超時時間來創(chuàng)建:
private CancellationTokenSource GetCancellationTokenSource( HttpRequestMessage request, CancellationToken cancellationToken) { var timeout = request.GetTimeout() ?? DefaultTimeout; if (timeout == Timeout.InfiniteTimeSpan) { // No need to create a CTS if there's no timeout //不需要創(chuàng)建CTS,因?yàn)椴惶幚沓瑫r(下面會講到) return null; } else { var cts = CancellationTokenSource .CreateLinkedTokenSource(cancellationToken); cts.CancelAfter(timeout); return cts; } }
這里主要關(guān)注兩個點(diǎn):
- 如果request超時值為Timeout.InfiniteTimeSpan,程序并不會創(chuàng)建CancellationTokenSource,它將不會被取消,因此節(jié)省了無用的分配。也就是說在這種情況下,我們將不會處理超時。
- 以上相反,我們創(chuàng)建了一個在指定timeout后被自動取消的CancellationTokenSource(因?yàn)檎{(diào)用了CancelAfter)。請注意,這個CTS連接了傳入?yún)?shù)的cancellationToken,這個cancellationToken其實(shí)來自SendAsync方法的實(shí)參。這樣做之后,當(dāng)真正的超時發(fā)生,或者參數(shù)的cancellationToken自身被取消,CTS都會被取消。如果想要獲取跟多CancellationToken的內(nèi)容,請訪問這篇文章
最后,我們修改下SendAsync方法,應(yīng)用剛剛創(chuàng)建的CancellationTokenSource。
protected async override Task<HttpResponseMessage> SendAsync( HttpRequestMessage request, CancellationToken cancellationToken) { using (var cts = GetCancellationTokenSource(request, cancellationToken)) { return await base.SendAsync( request, cts?.Token ?? cancellationToken); } }
我們創(chuàng)建了CTS后,把CTS的token傳入到base.SendAsync中,注意,我們使用cts?.Token是因?yàn)镚etCancellationTokenSource返回的cts可能為null,如果cts為null,則直接使用參數(shù)自己的cancellationToken,我們就不做任何超時處理。
通過這一步,我們有了自己的超時Handler,可以為每個請求指定不同的超時時間。但是,當(dāng)超時發(fā)生時,我們?nèi)匀恢荒懿东@到TaskCanceledException異常,這個問題很容易修復(fù)它。
拋出正確的異常
我們需要捕獲TaskCanceledException(或者它的基類OperationCanceledException),然后檢測cancellationToken參數(shù)是否是被取消的:
- 如果是,說明這個cancel是調(diào)用者自身導(dǎo)致的,對此直接將異常上拋不處理
- 如果不是,這意味著是因?yàn)槲覀兊某瑫r導(dǎo)致的cancel,因此,我們將拋出一個TimeoutException
這是最終的SendAsync方法:
protected async override Task<HttpResponseMessage> SendAsync( HttpRequestMessage request, CancellationToken cancellationToken) { using (var cts = GetCancellationTokenSource(request, cancellationToken)) { try { return await base.SendAsync( request, cts?.Token ?? cancellationToken); } catch(OperationCanceledException) when (!cancellationToken.IsCancellationRequested) { throw new TimeoutException(); } } }
我們使用了一個exception filter,通過這種方式,我們只cactch我們符合我們情況需要的異常,然后做相應(yīng)處理。
至此,我們的超時Handler已經(jīng)完成了,接下來看看怎么使用它
使用Handler
當(dāng)創(chuàng)建一個HttpClient時,可以指定一個自己的Handler作為管道(pipeline)的第一個Handler。如果沒有指定,默認(rèn)使用的是HttpClientHandler,這個handler直接發(fā)送請求到網(wǎng)絡(luò)上。為了使用我們自己的TimeoutHandler,我們需要先創(chuàng)建它,然后將timeoutHandler指定為httpClient的handler。在timeoutHandler中,指定InnerHandler為我們自己創(chuàng)建的HttpClientHandler,這樣實(shí)際的網(wǎng)絡(luò)請求就委托到了HttpClientHandler中。
var handler = new TimeoutHandler { InnerHandler = new HttpClientHandler() }; using (var client = new HttpClient(handler)) { client.Timeout = Timeout.InfiniteTimeSpan; ... }
通過將httpclient的timeout設(shè)置為InfiniteTimeSpan來禁用默認(rèn)的超時設(shè)置,如果不這樣做,默認(rèn)超時會干擾我們自己的超時
現(xiàn)在,我們嘗試發(fā)送一個設(shè)定了5秒超時的請求到需要很久才能響應(yīng)的服務(wù)器
var request = new HttpRequestMessage(HttpMethod.Get, "http://foo/"); request.SetTimeout(TimeSpan.FromSeconds(5)); var response = await client.SendAsync(request);
如果服務(wù)器在5秒內(nèi)響應(yīng)數(shù)據(jù),我們將會捕獲到一個TimeoutException,而不是TaskCanceledException,因此事情似乎按預(yù)期進(jìn)行。
為了檢測cancellation是否正確運(yùn)行,我們傳入一個在2秒(比超時實(shí)際小)后會被取消的CancellationToken:
var request = new HttpRequestMessage(HttpMethod.Get, "http://foo/"); request.SetTimeout(TimeSpan.FromSeconds(5)); var cts = new CancellationTokenSource(TimeSpan.FromSeconds(2)); var response = await client.SendAsync(request, cts.Token);
這時,我們可以捕獲到TaskCanceledException,這正是我們期望的。
總結(jié)
通過實(shí)現(xiàn)我們自己的Http Handler,我們可以用一個智能的timout handler來解決開始我們提出的問題。
這篇文章的所有代碼在這
以上就是c# HttpClient設(shè)置超時的步驟的詳細(xì)內(nèi)容,更多關(guān)于c# HttpClient設(shè)置超時的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
C#實(shí)現(xiàn)XOR密碼(異或密碼)的示例代碼
XOR密碼(異或密碼)是一種簡單的加密算法,它使用異或(XOR)操作來對明文和密鑰進(jìn)行加密和解密,本文為大家介紹了C#實(shí)現(xiàn)XOR密碼的相關(guān)知識,希望對大家有所幫助2024-01-01WPF實(shí)現(xiàn)手風(fēng)琴式輪播圖切換效果
這篇文章主要為大家詳細(xì)介紹了WPF實(shí)現(xiàn)手風(fēng)琴式輪播圖切換效果,文中示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下2020-09-09C#判斷頁面中的多個文本框輸入值是否有重復(fù)的實(shí)現(xiàn)方法
這篇文章主要介紹了C#判斷頁面中的多個文本框輸入值是否有重復(fù)的實(shí)現(xiàn)方法,是一個非常簡單實(shí)用的技巧,需要的朋友可以參考下2014-10-10