去年9月份的時候我看到過外國朋友關於.NET Framework下HttpClient缺陷的分析後對HttpClient有了必定的瞭解。前幾日也有園友寫了一篇關於HttpClient的分析文章, 因而我想深刻探索一下在.NET下使用HTTP請求的正確姿式。姿式不是越多越好, 而在於精不精。若是不深刻了解, 小朋友可能會這樣想: 啊, 這個姿式不High, 那我換一個吧, 卻不知那一個姿式也有問題啊, 親。html
中文版: https://oschina.net/news/77036/httpclientgit
英文版: https://www.infoq.com/news/2016/09/HttpClient程序員
張大大版: http://www.cnblogs.com/lori/p/7692152.htmlgithub
2、準備好牀和各類姿式web
1. 研究姿式必然是要先準備好支撐點, 做爲一個傳統的人, 仍是比較喜歡牀。多線程
.NET Framework, .NET CORE Windows, .NET CORE Linux, .NET CORE Maccurl
2. 姿式有如下幾種, 若是小朋友們有各特別的能夠告訴我呀, 我很樂於嘗試的。post
HttpClient, WebClient, HttpWebRequest性能
3、讓咱們大幹一場吧測試
Windows下統計端口使用的命令: netstat -ano | find "{port}" /c
Linux 下統計端口使用的命令: netstat -nat|grep -i "{port}"|wc -l
HttpWebRequest 測試代碼以下
class Program { static void Main(string[] args) { Parallel.For(0, 10, (i) => { while (true) { var webRequest = (HttpWebRequest)WebRequest.CreateHttp("http://"); var response = webRequest.GetResponse(); response.Dispose(); Console.WriteLine($"Process: {i}."); Thread.Sleep(5); } }); Console.Read(); } }
.NET Framework | .NET Core Windows | .NET Core Linux | .NET Core Mac | |
HttpWebRequest | 2 | 端口占用數迅速攀升到1000+ | 性能不好, 端口占用數攀升到70+並穩定 |
WebClient由於有IDisposable接口, 因而我作兩份測試
static void Main(string[] args) { Parallel.For(0, 10, (i) => { while (true) { using (WebClient client = new WebClient()) { client.DownloadString("http://"); Console.WriteLine($"Process: {i}."); } Thread.Sleep(5); } }); Console.Read(); }
.NET Framework | .NET Core Windows | .NET Core Linux | .NET Core Mac | |
WebClient | 2 | 端口占用數迅速攀升到1000+ | 性能較差, 端口占用數攀升到400+穩定 |
static void Main(string[] args) { Parallel.For(0, 10, (i) => { WebClient client = new WebClient(); while (true) { client.DownloadString("http://"); Console.WriteLine($"Process: {i}."); Thread.Sleep(5); } }); Console.Read(); }
.NET Framework | .NET Core Windows | .NET Core Linux | .NET Core Mac | |
WebClient | 2 | 端口占用數迅速攀升到1000+ | 端口占用數迅速攀升到1000+ |
HttpClient有IDisposable接口, 也作兩份測試
static void Main(string[] args) { Parallel.For(0, 10, (i) => { HttpClient client = new HttpClient(); while (true) { var html = client.GetStringAsync("http://").Result; Console.WriteLine($"Process: {i}."); Thread.Sleep(5); } }); Console.Read(); }
.NET Framework | .NET Core Windows | .NET Core Linux | .NET Core Mac | |
HttpClient | 10 | 10 | 10 |
static void Main(string[] args) { Parallel.For(0, 10, (i) => { while (true) { using (HttpClient client = new HttpClient()) { var html = client.GetStringAsync("http://").Result; Console.WriteLine($"Process: {i}."); } Thread.Sleep(5); } }); Console.Read(); }
.NET Framework | .NET Core Windows | .NET Core Linux | .NET Core Mac | |
HttpClient | 端口占用數迅速攀升到1000+ | 端口占用數迅速攀升到1000+ | 性能較差, 端口占用數攀升到200+ |
結論
.NET Framework | .NET Core Windows | .NET Core Linux | .NET Core Mac | |
HttpWebRequest | OK | Abnormal | Abnormal | |
WebClient | OK | Abnormal | Abnormal | |
HttpClient(每一個線程一個對象) | OK | OK | OK | |
HttpClient(using) | Abnormal | Abnormal | Abnormal |
有意思的細節與疑問
1. WebClient和HttpWebRequest爲何在10個線程下端口數爲2而且都爲2
2. Linux下並行性能明顯變差
4、追根溯源
下載.net45源碼和corefx源碼
http://referencesource.microsoft.com/ 右上角Download
https://github.com/dotnet/corefx
1. 分析.NET Core下WebClient的代碼, 發現它是使用WebRequest即HttpWebRequest來請求數據
2. 分析.NET Core下HttpWebRequest的代碼找到SendRequest方法
熟悉嗎?!!原來.NET Core一切的根源都出在HttpClient身上...
3. 順着HttpClient代碼咱們能夠發現, 微軟爲Windows, Unix各自實現了WinHttpHandler和CurlHandler, 猜想Uniux下使用的是Curl. 最終確實能查到Windows下是DLLImport了winhttp.dll, 但Unix系統是DLLImport的 System.Net.Http.Native, 這是個什麼我暫時不清楚, 也不清楚它跟curl的關係, 也許是一箇中轉調用。
4. 咱們再回過頭來看.NET Framework下爲何HttpWebRequest和WebClient是正常的, WebClient依然是使用的HttpWebRequest, 所以推斷.NET Framework的HttpWebRequest的實現與.NET Core是不一致的。簡單的查找代碼, 果真每個Http請求是由ServicePointManager管理的ServicePoint來實現的, 而且ServicePoint是使用.NET下Socket來實現的, 一切就明瞭了。如今對剛纔說的 「WebClient和HttpWebRequest爲何在10個線程下端口數爲2而且都爲2」有感受了吧?咱們把剛纔的測試代碼再加上一行
static void Main(string[] args) { ServicePointManager.DefaultConnectionLimit = 10; Parallel.For(0, 10, (i) => { while (true) { var webRequest = (HttpWebRequest)WebRequest.CreateHttp("http://"); var response = webRequest.GetResponse(); response.Dispose(); Console.WriteLine($"Process: {i}."); Thread.Sleep(5); } }); Console.Read(); }
.NET Framework | .NET Core Windows | .NET core Linux | .NET Core Mac | |
HttpWebRequest | 10 | 端口占用迅速攀升到1000+ | 性能不好, 端口占用攀升到70+並穩定 |
你們看.NET Core下雖然能夠設置 ServicePointManager.DefaultConnectionLimit = 10; 可是依然沒什麼卵用... 緣由也很明顯, HttpWebRequest根本沒有使用ServicePointManager作管理。在我查了源碼後雖然.NET Core實現了ServicePointManager和ServicePoint, 不過已經遷到另一個項目下面, 也未發現有什麼做用。因此你們千萬要注意,不要覺得在.NET Core能夠設置ServicePointManager.DefaultConnectionLimit這個值了, 就覺得.NET Framework下的效果會一致( 其它地方同理)
5. HttpClient在.NET Framework下的代碼我沒有找到, ILSpy也查看不了, 但猜測應該是和.NET Core下一致的, 因此纔會有同樣的表象, 有大神知道的能夠告訴我一下。
5、好累啊, 終於交差了, 就是不知道知足不知足
1. 在.NET Framework下儘可能使用HttpWebRequest或者WebClient, 而且根據你本身的多線程狀況設置 ServicePointManager.DefaultConnectionLimit的值, 以及ThreadPool.SetMinThreads(200, 200)的值
2. 在.NET Framework下若是必定要使用HttpClient, 則應該一個線程使用一個HttpClient對象, 這樣不會出現端口被耗盡的狀況
3. 在.NET Core 2.0下只有HttpClient一條路選, 而且一個線程使用一個HttpClient對象, 固然也許咱們能夠參照.NET Framework下的代碼從新實現一個ServicePointManager管理的HttpWebRequest, 這是後話了
6、抽一根菸吧
1. 大膽猜測一下, 微軟應該是趕進度才偷懶使用HttpClient來實現HttpWebRequest致使的吧。
2. Linux並行性能好像差不少, 緣由不明, 請聽下回分解
3. 這也就是開源的魅力所在了吧! 咱們能夠順藤摸瓜, 查明真相。讓咱們一塊兒爲.NET Core的開源事業奉獻本身的一份力吧(其實我只是不想丟飯碗好吧:::)
4. 若是有說錯請指正, 不接受漫罵
5. 歡迎各路大神和做品加入 https://github.com/dotnetcore (中國 .net core 開源小分隊)
6. 月收入低於3萬的也是程序員!!!!
============================================================================================================================================================
接上回咱們留下了一個性能疑問, Linux下性能明顯有問題。爲了映證我所看到的,作以下測試
static void Main(string[] args) { Stopwatch watch = new Stopwatch(); HttpClient client = new HttpClient(); watch.Start(); for (int i = 0; i < 50; ++i) { var html = client.GetStringAsync("http://").Result; Console.WriteLine($"Process: {i}."); } watch.Stop(); Console.WriteLine($"Cost: {watch.ElapsedMilliseconds}"); Console.Read(); }
.NET Framework | .NET Core Windows | .NET core Linux | ||
HttpClient | 400+ (屢次) | 450+(屢次) | 1230+(屢次), 5333+屢次, 350+(屢次)很是不穩定 |
Linux下性能的波動徹底不可理解..... 因而調整測試代碼
static void Main(string[] args) { Stopwatch watch = new Stopwatch(); HttpClient client = new HttpClient(new HttpClientHandler()); watch.Start(); for (int i = 0; i < 50; ++i) { Stopwatch watch1 = new Stopwatch(); watch1.Start(); var html = client.GetStringAsync("http://").Result; Console.WriteLine($"Process: {i}."); watch1.Stop(); Console.WriteLine($"Cost: {watch1.ElapsedMilliseconds}"); } watch.Stop(); Console.WriteLine($"Cost: {watch.ElapsedMilliseconds}"); Console.Read(); }
測試結果一目瞭然... 全是第一次請求消耗的時間, 後面的請求基本都是6左右, 仍是很是快的。