新工作入職不滿半周,目前仍然還在交接工作,適應環境當中,筆者不得不說看別人的源碼實在是令人痛苦。所幸今天終於將大部分工作流暢地看了一遍,接下來就是熟悉框架技術的階段了。 也正是在看源碼的過程當中,有一個比較明顯的用法細節引起了我的註意,我發現一位同事在請求遠程Web Api時,雖然使用了 類,但是在 ...
新工作入職不滿半周,目前仍然還在交接工作,適應環境當中,筆者不得不說看別人的源碼實在是令人痛苦。所幸今天終於將大部分工作流暢地看了一遍,接下來就是熟悉框架技術的階段了。
也正是在看源碼的過程當中,有一個比較明顯的用法細節引起了我的註意,我發現一位同事在請求遠程Web Api時,雖然使用了 HttpClient
類,但是在用法上似乎有些欠考慮。代碼抽象出來就是以下的模樣:
using(var client = new HttpClient())
{
//do something
}
我們知道 using
關鍵字常常和實現了 IDisposable
介面的類型一起使用(如資料庫連接和文件流操作),用於釋放對象機資源(關於GC回收的相關知識可參考我的另一篇博文《CLR和.Net對象生存周期》),但是對於 HttpClient
這樣直接和TCP/IP協議打交道的類型卻是未必( HttpClient
繼承了 HttpMessageInvoker
類, HttpMessageInvoker
實現了 IDisposable
介面,實現上是比較經典的代理模式),翻看一些國內外的文章都能看到對在 using
關鍵字中使用 HttpClient
的吐槽。事實是不是真的這樣呢,其實只要做一個小實驗就可以了。
讓我們先試著運行以下代碼
class Program
{
static void Main(string[] args)
{
for (int i = 0; i < 10000; i++)
{
using (var client = new HttpClient())
{
var result = client.GetAsync("http://www.baidu.com").Result;
Console.WriteLine(result.StatusCode);
}
}
Console.ReadKey();
}
}
不出意外就會提示以下錯誤:
單純為瞭解決問題而言,我們可以通過減小 HttpClient
的 Timeout
屬性加快回收速度(修改系統變數可能會引發其他的問題),但實際上,這還是因為 HttpClient
消耗了太多套接字連接的關係。為了驗證這個問題,我們可以使用TcpView這個小工具來查看下項目運行時的 TCP 連接數,如果你下載了代碼運行後,會發現 TCP 連接和瘋狗一樣向上猛躥。雖然還會有套接字回收的現象,但是和增加的速度相比確實是杯水車薪。
所以這時候我們需要換一種寫法:
class Program
{
private static readonly HttpClient Client=new HttpClient();
static void Main(string[] args)
{
for (int i = 0; i < 10000; i++)
{
var result = Client.GetAsync("http://www.baidu.com").Result;
Console.WriteLine(result.StatusCode);
}
Console.ReadKey();
}
}
更換以上寫法後,我們會發現無論我們將迴圈上限如何調整,也不會出現套接字連接資源不足的情況了,而TCPView的結果也好看得多,甚至如果我們每次都測試傳輸時間的話,我們會發現單次調用 HttpClient
而言,第二種代碼比第一種代碼要快得多。其實這很好理解,HttpClient內部維持一個專有的連接池,每個HttpClient實例的請求相互隔絕,加快速度的原因是因為重用了套接字,去除了套接字重新建立連接的過程。這也很好地解釋了dudu園長的那一篇博客 《C#中HttpClient使用註意:預熱與長連接》中的“預熱”說法。盜一張圖來說明一下套接字的使用情況。
因此,在使用 HttpClient
時我們知道以下幾件小事
- 將其定義為單例模式(由單獨的HttpClient維護連接池)
- 不要使用using關鍵字包裹(無效,套接字資源不會跟隨釋放)
- 儘量不要額外改變
HttpClient
的一些特殊行為(如上文中的TimeOut)
其實HttpClient還有一種使用隱患,DNS-Bug,這種做法國外也有同僚給出了相應的解釋和解決方案,詳情請見《Singleton HttpClient? Beware of this serious behaviour and how to fix it》
單例模式擴展開來也有很多的說法,根據C#的一些規範,在編程中我推薦兩種做法
A. 靜態構造器
這種方式適用於如上代碼場景,使用靜態構造器確保靜態欄位的實例化。
class Program
{
private static readonly HttpClient Client;
static Program()
{
Client=new HttpClient();
}
static void Main(string[] args)
{
//do something
}
}
B. HttpClientHelper
單例模式中,經典的雙重檢查鎖定機制。
public static class HttpClientHelper
{
private static readonly object LockObj = new object();
private static HttpClient _client;
public static HttpClient HttpClient {
get
{
if (_client == null)
{
lock (LockObj)
{
if (_client == null)
{
_client= new HttpClient();
}
}
}
return _client;
}
}
}
寄語
多點實踐多點總結,為認識更深刻的代碼世界而奮鬥。