C#中HttpClient使用註意(預熱與長連接)
最近在測試一個第三方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的某種預熱機制在搞鬼!
既然知道瞭是HttpClient預熱機制的原因,那我們可以幫HttpClient進行熱身,減少第一次請求的耗時。我們嘗試瞭一種預熱方式,在正式發http post請求之前,先發一個http head請求,代碼如下:
_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進行網絡抓包也驗證瞭這一點)。
在之前的測試中,我們每次用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的調用代碼改為如下的樣子:
然後測試一下請求響應時間:
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抓包也驗證瞭長連接的效果:
Wireshak抓包
這時,你
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(); } }
也許會產生這樣的疑問:將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");
現在終於可以收官瞭。但是肯定不完美,分享的隻是解決問題的過程。
到此這篇關於C#中HttpClient使用註意(預熱與長連接)的文章就介紹到這瞭,更多相關C# HttpClient內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!
推薦閱讀:
- C#使用HttpClient的正確方式你瞭解嗎
- 使用HTTPclient保持長連接
- C#爬蟲基礎之HttpClient獲取HTTP請求與響應
- ASP.NET Core擴展庫之Http請求模擬功能的使用
- C# HttpClient Post參數同時上傳文件的實現