关于c井的HttpClient的特性
在项目的开发过程中,需要用到POST机制将信息发送给服务器。翻阅微软资料,可以采用HttpClient类实现。实际使用过程中,发现这个类挺有脾气的,用不好特别容易出现的问题,最容易碰到的问题是下面两个:
1)会建立大量的Tcp链接,耗费系统资源;
2)容易出现TaskCanceledException异常,表示这个链接的发送任务被HttpClient强行取消了;
翻阅了一些资料,查了下百度,特别感谢的是dudu在网上的分享,网址链接如下:
C#中HttpClient使用注意:预热与长连接
基于这篇文章,特别对这个类的性能做了一个测试。先直接贴上自己的测试代码:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Net.Http; using System.Threading; namespace ConsoleApplication2 { class Program { static void Main(string[] args) { HttpClientTest cl = new HttpClientTest(); int cnt = 0; while (true) { for (int i = 0; i < 20; i++) { try { Task tsk = cl.PostAsync(cnt); cnt++; //tsk.Wait(); // 取消这个注释,则将等待上一个POST任务结束,然后再进行下一次POST Thread.Sleep(300); } catch (Exception ex) { Console.WriteLine(ex.Message); } } Console.ReadLine(); } } } public class HttpClientTest { private HttpClient _httpClient; public HttpClientTest() { _httpClient = new HttpClient() { BaseAddress = new Uri("http://192.168.101.233:2317") };//这里需要改为您自己的服务器 _httpClient.Timeout = new TimeSpan(0,0,0,3);//默认超时100s,太长,改为3秒 } /// <summary> /// 控制台打印Exception信息 /// </summary> /// <param name="ex"></param> /// <param name="cc"></param> private void PrintException(Exception ex, ConsoleColor cc, string mystr) { ConsoleColor temp = Console.ForegroundColor; Console.ForegroundColor = cc; StringBuilder str = new StringBuilder(); Exception extemp = ex; do { str.AppendLine(extemp.GetType() + "__" + extemp.Message); extemp = extemp.InnerException; } while (extemp != null); Console.WriteLine(DateTime.Now.ToString() + mystr + str); Console.ForegroundColor = temp; } public async Task<string> PostAsync(int cnt)/**/ { string rst = ""; HttpResponseMessage response = new HttpResponseMessage(); try { Console.ForegroundColor = ConsoleColor.Red; Console.WriteLine(DateTime.Now + $" ({cnt}) Post: ..."); response = await _httpClient.PostAsync("/", new StringContent("Hello")); } catch(Exception ex) { PrintException(ex, ConsoleColor.Green, $" ({cnt}) HttpClient.PostAsync()异常)"); return rst; } try { Console.ForegroundColor = ConsoleColor.White; rst = await response.Content.ReadAsStringAsync(); Console.WriteLine(DateTime.Now + $" ({cnt}) 收到应答:" + rst); } catch(Exception ex) { PrintException(ex, ConsoleColor.Yellow, $" ({cnt}) ReadAsStringAsync()异常)"); } return rst; } } }
上面的代码主要功能就是连续不断的向服务器POST一个数据"Hello", 如果用TCP工具抓取服务器端实际上收到的完整的HTTP数据如下:
POST /HTTP/1.1 Content-Type: text/plain;charset=utf-8 Host: 192.168.101.233:2317 Content-Length: 5 Expect: 100-continue Connection: Keep-Alive Hello
实验0:
这个实验这里不详细展开了,大家可以自行试试如果将代码改成在每次POST发送时采用using方式生成HttpClient来POST,这不是一个好习惯,这会导致大量的TCP连接。大家可以使用SocketTool这个工具来查看到这些tcp连接。这些tcp连接大量消耗系统的端口资源(我们知道端口最多有65535个,看起来很大,但如果POST的频率很快,还是会比较容易被耗光)。
实验1:
服务器关闭,代码中task.Wait()部分注释掉,此时运行代码,我们发现程序不停的POST,但是由于服务器未打开服务端口,所有的POST都会被HttpClient自行取消,实际上是因为超时了,超时的代码
_httpClient.Timeout = new TimeSpan(0,0,0,3);
表示3秒。默认Timeout属性值是100秒。
下图就是这个试验下,程序产生的现象,20个POST任务均被HttpClient中断:
如果Timeout时间足够短,可能是产生TaskCanceledException异常,如果Timeout时间足够长,也可能产生HttpRequestException异常,表示无法建立连接。
实验2:
服务器打开,但是服务器不做任何应答,代码中task.Wait()部分注释掉,也就是连续不断的立即进行20个POST。
这个实验的结论是:如果服务器不对POST信息做任何回答,那么,HttpClient最多维护2个tcp连接,如果产生超过2个POST那么,后续的POST被阻塞(个人猜测是进入某种队列等待中),超时时间到来之后(此处的超时是由于发了POST后,超时没由收到来自服务器对该POST的任何应答),这些未收到任何应答的POST任务会被取消并产生TaskCanceledException异常,然后,之前被阻塞的POST的任务会立即重新建立一个新连接(端口不一样)。
实验3):
服务器随便给个不符合Http协议的应答。此时,任务会报HttpRequestException异常,再细查内部的异常是WebException异常,提示"服务器提交了协议冲突",因此,服务器必须应答的是HTTP协议的报文。
产生这个异常后,HttpClient会自行关闭掉这个连接。如果后续还有POST,那么它会立即建立一个新连接。
实验4):
服务器随便给个符合Http协议的应答(需要用户自行写一个tcp应答程序,或者找一个可以自动应答的工具):
HTTP/1.1 200 OK Content-Length: 5 abcde
此时,如果是注释掉task.Wait(),那么结果是每次都能得到这个应答,并且最多维护2个连接。如下图:
如果保留task.Wait(),最符合我们想象的情形出现了,就是我们会发现网络稳定的情形下HttpClient保持仅仅1个连接!!!! 此时是最省资源的情形了,也就是所有的POST信息都是在一个连接上进行的,系统也是最节约资源的!!! 如下图:
实验4)的测试过程中还发现一个有趣的现象,也就是POST到服务器的数据:
<span style="color:#ff0000;">POST /HTTP/1.1 Content-Type: text/plain;charset=utf-8 Host: 192.168.101.233:2317 Content-Length: 5 Expect: 100-continue Connection: Keep-Alive</span> Hello
红色部分和黑色部分在实际的tcp发送过程中被tcp传输层分片了,如下图(wireshark抓包)所示,1是红色部分信息,2是Hello信息:
服务器这边,在对POST进行应答时,必须特别注意,应当是在收到完整的POST报文后再做应答,否则,如果服务器不按照HTTP协议的要求操作的话(比如,在收到红色部分就立即给应答),那么HttpClient可能会自行断开tcp连接,这意味着HttpClinet内部对协议做了严格分析,如果协议时序或者报文不符合http协议,HttpClient随时会断开连接(抓包发现断开连接是HttpClinet向服务器发送RST复位报文所致)。
总结,对HttpClient的新的认识是:
1)HttpClient并不一定要static静态的,实际上连接数与HttpClient的实例个数有关系,并且一个HttpClient最多就同时维护2个tcp连接。因此,如果想增加连接,可以考虑适当增加HttpClient实例个数。
2) 本实验没有做延时性测试,但个人认为预热的问题不存在,HttpClient内部实现可能比较严格,服务器稍微有些伺候不好(比如服务器应答的不及时),HttpClient可能就会阻塞后续的POST任务,因此使用HttpClient的时候可能需要更多考虑服务器端的应答性能;
3) HttpClient POST数据之后,服务器如果做了非法应答(不符合Http协议),那么HttpClient会断开本次连接,并报告非法协议的异常。
4) 建议缩短Timeout的设置,默认的100秒太长,因为一个HttpClient最多同时维护两个tcp连接,因此如果由于超时时间太长,那么后续的大量POST会被阻塞,导致严重的POST不及时。因此,应当根据业务性质综合考虑,建议Timeout设置小一些,尽量让POST业务不被阻塞。也因此需要对网络情况,服务器应答的及时性进行优化。
相关推荐
创建一个 HttpClient 实例,这个实例需要调用 Dispose 方法释放资源,这里使用了 using 语句。接着调用 GetAsync,给它传递要调用的方法的地址,向服务器发送 Get 请求。