在.Net 4.0之前,一直是依靠HttpWebRequest實現Http操作的。它默認有一個非常保守的同一站點下最大2並發數限制,導致默認情況下HttpWebRequest往往得不到理想的速度,必須修改App.config或ServicePointManager.DefaultConnectionLimit的值。所以對於需要高並發請求的場景HttpWebRequest不是一個理想的選擇。
MS在.Net 4.5中引入了一個HttpClient類專門處理Http操作,HttpClient不受HttpWebRequest並發策略控制,也沒有系統級的並發限制。
關於HttpClient和HttpWebRequest的一些區別參考(https://stackoverflow.com/questions/22214930/httpclient-vs-httpwebrequest)
下面開始進入重點,在本文之前對於HttpClient有過很多錯誤的使用,最開始使用的如下代碼:
寫一個靜態方法,每次調用該方法都會new一個新的HttpClient對象,使用完成后釋放HttpClient對象
public static HttpResponseMessage GetRequest(string requestUri, string accessToken) { using (HttpClient client = new HttpClient()) { if (!string.IsNullOrEmpty(accessToken)) { client.DefaultRequestHeaders.Add("Authorization", "Bearer " + accessToken); } client.DefaultRequestHeaders.Add("Accept", "application/json"); try { var response = client.GetAsync(requestUri).Result; if (response.IsSuccessStatusCode) { return response; } else { throw new Exception(response.Content.ReadAsStringAsync().Result); } } catch (Exception ex) { throw ex; } } }
那這就完成沒有發揮出HttpClient的一大優勢(同一HttpClient可以發送多個請求),而以上代碼一個請求就會創建一個HttpClient對象,同時我們調用HttpClient的Dispose()方法銷毀它時,它就啟動一個進程,關閉在它控制之下的套接字。也就是說,你下次請求連接時,必須重復整個連接新建過程。如果網絡延遲很高,或者連接是受保護的(需要新一輪的SSL/TLS協商),就會非常痛苦。
所以HttpClient關閉套接字的過程並不快。當“關閉”套接字時,你真正做的是將TCP連接狀態置為TIME_WAIT。在一個預先配置好的時間窗口內,Windows將保持該套接字的狀態不變,默認情況下是4分鍾。這是為了防止有任何剩余的數據包仍在傳輸。
所以以上代碼是一個錯誤的使用方式,如果在一個高並發場景,服務器資源也會迅速耗盡。
所以我們的正確做法應該是:HttpClient應該只初始化一次,並在應用程序的整個生存期內重用。在負載很高的情況下,為每個請求初始化一個HttpClient類會耗盡可用的套接字數量。
以下展示正確的做法,就是對HttpClient做單例處理,使得應用生命周期內只有一個HttpClient對象,並可以重用,並且不調用Dispose()方法。
internal class HttpClientUtils { private static HttpClient _httpClient; private readonly static object _lock = new object(); private readonly static HttpClientUtils _httpClientUtils = new HttpClientUtils(); static HttpClientUtils() { _httpClient = GetHttpClient(); } public static HttpClientUtils GetHttpClientUtils() { return _httpClientUtils; } private HttpClientUtils() { } private static HttpClient GetHttpClient() { lock (_lock) { if (_httpClient == null) { _httpClient = new HttpClient(); //_httpClient.DefaultRequestHeaders.Add("Accept", "application/json"); _httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); _httpClient.Timeout = TimeSpan.FromSeconds(1800); } } return _httpClient; }
}
以上代碼解決了HttpClient重用問題,但是竟然所有Http請求都使用同一HttpClient對象,那么新的問題也隨之而來,可能我們會遇到不同的HttpClient請求需要帶不同的Http請求頭,
那么如下所示代碼,就會造成很大問題,因為並不是所有的Http請求都需要此請求頭。
client.DefaultRequestHeaders.Add("Accept", "application/json"); //....
那么問題的關鍵就在於,我們不能將請求頭直接賦值到HttpClient對象的DefaultRequestHeaders屬性上。
那么應該如何講請求頭和HttpClient對象分離。我們可以使用SendAsync(HttpRequestMessage request); ,在HttpRequestMessage中添加請求頭。
以Get請求為例,示例如下:
public async Task<HttpResponseMessage> GetRequestAsync(string requestUri, string accessToken) { HttpRequestMessage message = new HttpRequestMessage(HttpMethod.Get, requestUri); if (!string.IsNullOrEmpty(accessToken)) { message.Headers.Add("Authorization", "Bearer " + accessToken); } try { var response = await _httpClient.SendAsync(message); //var response = await this._client.GetAsync(requestUri); if (response.IsSuccessStatusCode) { return response; } else { throw new Exception(response.Content.ReadAsStringAsync().Result); } } catch (Exception ex) { throw ex; } }
可以看到我們將Http請求方法,Http請求Url以及請求頭信息,封裝到HttpRequestMessage對象中,使用 SendAsync(HttpRequestMessage request); 發送請求
而這正好解決高並發下Http請求發送的問題,避免不同Http請求互相造成干擾。