原文地址:C#中HttpClient使用注意:預熱與長鏈接html
最近在測試一個第三方API,準備集成在咱們的網站應用中。API的調用使用的是.NET中的HttpClient,因爲這個API會在關鍵業務中用到,對調用API的總體響應速度有嚴格要求,因此對HttpClient有了格外的關注。安全
開始測試的時候,只在客戶端經過HttpClient用PostAsync發了一個http post請求。測試時發現,從建立HttpClient實例,到發出請求,到讀取到服務器的響應數據總耗時在2s左右,並且屢次測試都是這樣。2s的響應速度固然是沒法讓人接受的,咱們但願至少控制在100ms之內。因而開始追查這個問題的緣由。服務器
在API的返回數據中包含了該請求在服務端執行的耗時,這個耗時都在20ms之內,問題與服務端API無關。因而把懷疑點放到了網絡延遲上,但ping服務器的響應時間都在10ms左右,網絡延遲的可能性也不大。網絡
當咱們正準備換一個網絡環境進行測試時,忽然想到,咱們的測試方式有些問題。咱們只經過HttpClient發了一個PostAsync請求,假如HttpClient在第一次調用時存在某種預熱機制(好比在EF中就有這樣的機制),如今2s的總耗時可能大多消耗在HttpClient的預熱上。異步
因而修改測試代碼,將調用由1次改成100次,而後恍然大悟地發現——只有第1次是2s,接下來的99次都在100ms之內。果真是HttpClient的某種預熱機制在搞鬼!async
既然知道了是HttpClient預熱機制的緣由,那咱們能夠幫HttpClient進行熱身,減小第一次請求的耗時。咱們嘗試了一種預熱方式,在正式發http post請求以前,先發一個http head請求,代碼以下:post
_httpClient.SendAsync(new HttpRequestMessage { Method = new HttpMethod("HEAD"), RequestUri = new Uri(BASE_ADDRESS + "/") }) .Result.EnsureSuccessStatusCode();
經測試,經過這種熱身方法,能夠將第一次請求的耗時由2s左右降到1s之內(測試結果是700多ms)。測試
在知道第1次HttpClient請求耗時2s的真相以後,咱們將目光轉向了剩下的99次耗時100ms之內的請求,發現絕大部分請求都在50ms以上。有沒有可能將之降至50ms如下?並且,以前一直有這樣的糾結:每次調用是否是必定要對HttpClient進行Dispose()?是否是要將HttpClient單例或者靜態化(聲明爲靜態變量)?藉此機會一塊兒研究一下。網站
在HttpClient的背後,有一個對請求響應速度有着不容忽視影響的東東——TCP鏈接。一個HttpClient實例會關聯一個TCP鏈接,在對HttpClient進行Dispose時,會關閉TCP鏈接(咱們用Wireshark進行網絡抓包也驗證了這一點)。spa
在以前的測試中,咱們每次用HttpClient發請求時,都是新建一個HttpClient實例,用完就對它進行Dispose,代碼以下:
using (var httpClient = new HttpClient() { BaseAddress = new Uri(BASE_ADDRESS) }) { httpClient.PostAsync("/", new FormUrlEncodedContent(parameters)); }
因此每次請求時都要經歷新建TCP鏈接->傳數據->關閉鏈接(也就是一般所說的短鏈接),並且雪上加霜的是請求用的是https,創建TCP鏈接時還須要一個基於公私鑰加解密的key exchange過程:Client Hello -> Server Hello -> Certificate -> Client Key Exchange -> New Session Ticket。
若是咱們想將請求響應時間降至50ms如下,就必須從這個地方下手——重用TCP鏈接(也就是一般所說的長鏈接)。要實現長鏈接,首先須要的就是在HttpClient第1次請求後不關閉TCP鏈接(不調用Dispose方法);而要讓後續的請求繼續使用這個未關閉的TCP鏈接,咱們必需要使用同一個HttpClient實例;而要使用同一個HttpClient實例,就得實現HttpClient的單例或者靜態化。以前的3 個問題,因爲要解決第1個問題,後2個問題變成了別無選擇。
爲了實現長鏈接,咱們將HttpClient的調用代碼改成以下的樣子:
public class HttpClientTest { private static readonly HttpClient _httpClient; static HttpClientTest() { _httpClient = new HttpClient() { BaseAddress = new Uri(BASE_ADDRESS) }; //幫HttpClient熱身 _httpClient.SendAsync(new HttpRequestMessage { Method = new HttpMethod("HEAD"), RequestUri = new Uri(BASE_ADDRESS + "/") }) .Result.EnsureSuccessStatusCode(); } public async Task<string> PostAsync() { var response = await _httpClient.PostAsync("/", new FormUrlEncodedContent(parameters)); return await response.Content.ReadAsStringAsync(); } }
而後測試一下請求響應時間:
Elapsed:750ms Elapsed:31ms Elapsed:30ms Elapsed:43ms Elapsed:27ms Elapsed:29ms Elapsed:28ms Elapsed:35ms Elapsed:36ms Elapsed:31ms ....
除了第1次請求,接下來的99次請求絕大多數都在50ms之內。TCP長鏈接的效果必須的!
經過Wireshak抓包也驗證了長鏈接的效果:
這時,你也許會產生這樣的疑問:將HttpClient聲明爲靜態變量,會不會存在線程安全問題?咱們當時也有這樣的疑問,後來在stackoverflow上找到了答案:
As per the comments below (thanks @ischell), the following instance methods are thread safe (all async): CancelPendingRequests DeleteAsync GetAsync GetByteArrayAsync GetStreamAsync GetStringAsync PostAsync PutAsync SendAsync
HttpClient的全部異步方法都是線程安全的,放心使用。
到這裏,HttpClient的問題是否是能夠完美收官了?。。。稍等,還有一個問題。
客戶端雖然保持着TCP鏈接,但TCP鏈接是兩口子的事,服務器端呢?你不告訴服務器,服務器怎麼知道你要一直保持TCP鏈接呢?對於客戶端,保持TCP鏈接的開銷不大;可是對於服務器,則徹底不同的,若是默認都保持TCP鏈接,那但是要保持成千上萬客戶端的鏈接啊。因此,通常的Web服務器都會根據客戶端的訴求來決定是否保持TCP鏈接,這就是keep-alive存在的理由。
因此,咱們還要給HttpClient增長一個Connection:keep-alive的請求頭,代碼以下:
_httpClient.DefaultRequestHeaders.Connection.Add("keep-alive");
如今終於能夠收官了。可是確定不完美,分享的只是解決問題的過程。