[TOC]php
隨着純單體項目的逐漸減小,遠程服務調用失敗變得十分常見。因爲 HTTP 協議的開放性,遠程服務調用異常的複雜度在增加。html
HTTP 狀態碼是描述響應的重要信息,參考 List of HTTP status codes。git
3XX 響應不在本文討論之列github
HTTP 狀態碼目前集中於 1XX 到 5XX 區間,這造成如下事實:web
REST 風格接口每每使用 200、400、500 描述響應,部分版本的 ASPNET Core 中將暴露的路由所在方式定義爲 void 能夠觀察到 204 狀態碼(使用 IActionResult 則能夠進行更精確的控制)。c#
在實踐中,各廠商的策略也千差萬別:api
微信篤信本身的服務器不會掛,全部非 200 響應都可認爲服務出了問題,但這作法並不另類安全
大多數部分客戶端認爲 4XX 和 5XX 爲異常響應,但各語言集成的 HTTP 客戶端或者第三方以及各版本存在部分差別。以 .net 中的 WebClient、HttpWebRequest 來講, 遇到 4XX 和 5XX 直接拋出異常,這使得即使接收到 HTTP 響應,獲取響應狀態碼及正文卻須要在 catch 語句中進行,使用起來極爲醜陋。服務器
WebClient API 看起來簡單,但**建議避免使用 WebClient ** ,理由以下:微信
HttpWebRequest 必須在異常捕獲邏輯中處理服務器的非 2xx 響應,同步版本支持超時設置,請求示例:
var url = "http://localhost:4908/api/test/2"; //url = "http://www.google.com"; var client = HttpWebRequest.CreateHttp(url); client.Method = HttpMethod.Get.Method; client.Timeout = 3000; try { var resp = client.GetResponse(); //超時生效 //var resp = await client.GetResponseAsync() as HttpWebResponse; //超時不生效 using (var stream = resp.GetResponseStream()) using (var reader = new StreamReader(stream)) { var respText = await reader.ReadToEndAsync(); Console.WriteLine(respText); } } catch (WebException ex) { //開始處理失敗請求 var resp = ex.Response as HttpWebResponse; if (resp != null) { Console.WriteLine("request failed: {0}, statusCode: {1}", resp.StatusDescription, resp.StatusCode); using (var stream = ex.Response.GetResponseStream()) using (var reader = new StreamReader(stream)) { var respText = await reader.ReadToEndAsync(); Console.WriteLine(respText); } } //服務器沒法響應,好比 DNS 查找失敗 else { throw ex.InnerException ?? ex; } }
HttpWebRequest 存在着設計和實現缺陷,都與超時相關。在開始以前必須指出:.net core 不一樣版本存在差別,.net framework 不一樣版本存在差別,.net framework 與 .net core 存在差別
首先是DNS 查找成本不計入超時時長,在 .net framework 上可以復現,在 .net core 版本上可能獲得了修正。
調用結果顯示,設置了1秒的超時時間,.net framework 版本耗時 2.261 秒,差別不容忽略,.net core 版本耗時 1.137 秒,知足預期。
接着是異步版本不支持超時,即設置了超時時長的 await HttpWebRequest.GetResponseAsync()
沒法按預期工做,參考
明明是設計與實現問題,官方卻解釋到 」The Timeout property has no effect on asynchronous requests made with the BeginGetResponse or BeginGetRequestStream method「 云云。
爲何這麼說?由於 .net core 版本修復了這個問題,請繼續閱讀。
http://localhost:13340/api/trial/11 是一個 webapi 接口,內部使用 Thread.Sleep(10000)
掛起10秒,問題在 .net framework 上可以復現,在 .net core 版本按預期工做。
這意味着咱們必須作更多的工做。超時模式本能夠解決這個問題,須要先借助 TaskFactory.FromAsync()
將 APM 模式(Asynchronous Programming Model)轉換成 TPL 模式,即基於 Task 的異步模式
async Task Main() { var url = "http://localhost:13340/api/trial/11"; var client = HttpWebRequest.CreateHttp(url); //避免干擾,沒有對 HttpWebRequest.Timeout 賦值 var timeout = TimeSpan.FromSeconds(5); var start = DateTime.UtcNow; Console.WriteLine(Environment.Version); Console.WriteLine("Start {0}", DateTime.Now); try { //await client.GetResponseAsync(); var resp = await Task.Factory.FromAsync(client.BeginGetResponse, client.EndGetResponse, null) .SetTimeout(timeout); } catch (OperationCanceledException) { Console.WriteLine("Request timeout"); } catch (WebException ex) { Console.WriteLine(ex.InnerException ?? ex); } finally { Console.WriteLine("Finish {0}", DateTime.UtcNow.Subtract(start)); } } public static class TaskExension { [System.Diagnostics.DebuggerStepThrough] public static async Task<T> SetTimeout<T>(this Task<T> task, TimeSpan timeout) { using (var cts = new CancellationTokenSource(timeout)) { var tsc = new TaskCompletionSource<T>(); using (cts.Token.Register(state => tsc.TrySetCanceled(), tsc)) { if (task != await Task.WhenAny(task, tsc.Task)) { throw new OperationCanceledException(cts.Token); } } return await task; } } }
.net core 版本一樣工做無缺,在此忽略,至此 HttpWebRequest 的坑點已經數的差很少了。
Github 上的接近 7000 星項目 restsharp/RestSharp 使用 HttpWebRequest 完成實現,關鍵代碼見 Http.Sync.cs,它支持如下模式:
項目 HttpWebRequest 完成實現,異步請求的版在回調版本基礎上藉助 TaskCompletionSource 完成實現,繞開了 await HttpWebRequest.GetResponseAsync()
的超時缺陷。但 HttpWebRequest 固有的 DNS 問題沒法避免,故項目在 Note about error handling 中特別備註到:
Note about error handling If there is a network transport error (network is down, failed DNS lookup, etc),
HttpClient 的出現使得狀況些許改觀,不考慮超時,使用4行代碼便可讀取返回非 2XX 狀態碼的響應正文:
var client = new HttpClient(); var url = "http://localhost:4908/api/test/2"; var resp = await client.GetAsync(url); //遇到4XX、5XX 也不會拋出異常 var respText = await resp.Content.ReadAsStringAsync(); Console.WriteLine(respText);
可使用
HttpResponseMessage.EnsureSuccessStatusCode()
進行成功請求斷言
添加異常處理與超時機制,代碼在 20 行左右,是 HttpWebRequest 規模的 1/3 左右。
var url = "http://localhost:4908/api/test/1"; //url = "http://www.google.com"; var client = new HttpClient(); client.Timeout = TimeSpan.FromSeconds(5); HttpResponseMessage resp = null; try { resp = await client.GetAsync(url); } catch (TaskCanceledException) { //開始處理請求超時 Console.WriteLine("Request timeout"); throw new TimeoutException(); } catch (HttpRequestException ex) { //服務器沒法響應,好比未開機,DNS if (ex.InnerException is WebException ex2) { throw ex2.InnerException ?? ex2; } throw ex; } //已獲取到響應 if (resp.IsSuccessStatusCode) { //安全地讀取 resp.Content,進行反序列化等, //也能夠直接使用 EnsureSuccessStatusCode() 斷言 } else { //開始處理失敗請求 Console.WriteLine("Request failed: {0}, statusCode: {1}", resp.ReasonPhrase, resp.StatusCode); //直接讀取不會拋出異常 var respText = await resp.Content.ReadAsStringAsync(); Console.WriteLine(respText); }
可見基於 HttpClient 易於使用,然而 HttpClient 有本身的問題,雖然偏離主題,但不得不拿出篇幅來陳述。
搜索 "HttpClient dispose" 可見一二:
簡單地說,HttpClient 和 DbConnection 同樣都從 IDispose 繼承,然而其工做方式大不同:後者將鏈接釋放回鏈接池,前者卻須要4分鐘關閉 TCP 鏈接,這致使高負載的站點可能用盡資源。
然而網上解決辦法都建議靜態或單例化 HttpClient 實例,如博客園站長 dudu 的C#中HttpClient使用注意:預熱與長鏈接,9102年了,彙總下HttpClient問題,封印一個 ,這些作法會引入了其餘問題:
但事實證實,有一個更嚴重的問題:HttpClient 不遵循 DNS 變化,它會(經過 HttpClientHandler)獨佔鏈接,直到套接字關閉。沒有時間限制!
在實際開發中 DNS 變化可能不是很大問題,雖然 HttpClient 是線程安全的,可是惟一的 HttpClient 不能知足差別化的 Http 請求,好比有時候須要自定義頭部,有時候須要使用證書發起請求,靜態或單例化的 HttpClient 不能很好地知足須要。
爲了克服以上問題,微軟在 .Net core 2.1 版本引入了 HttpClientFactory,基礎使用方法簡單,請自行閱讀再也不詳細陳述。
IHttpClientFactory 內部引用了 Policy,建議很是謹慎地使用重試策略,討論不在本篇展開。
原文出處:https://www.cnblogs.com/leoninew/p/remote-response-and-error-1.html