关于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中断:

关于c井的HttpClient的特性

如果Timeout时间足够短,可能是产生TaskCanceledException异常,如果Timeout时间足够长,也可能产生HttpRequestException异常,表示无法建立连接。

实验2:

服务器打开,但是服务器不做任何应答,代码中task.Wait()部分注释掉,也就是连续不断的立即进行20个POST。

关于c井的HttpClient的特性

这个实验的结论是:如果服务器不对POST信息做任何回答,那么,HttpClient最多维护2个tcp连接,如果产生超过2个POST那么,后续的POST被阻塞(个人猜测是进入某种队列等待中),超时时间到来之后(此处的超时是由于发了POST后,超时没由收到来自服务器对该POST的任何应答),这些未收到任何应答的POST任务会被取消并产生TaskCanceledException异常,然后,之前被阻塞的POST的任务会立即重新建立一个新连接(端口不一样)。

实验3):

服务器随便给个不符合Http协议的应答。此时,任务会报HttpRequestException异常,再细查内部的异常是WebException异常,提示"服务器提交了协议冲突",因此,服务器必须应答的是HTTP协议的报文。

产生这个异常后,HttpClient会自行关闭掉这个连接。如果后续还有POST,那么它会立即建立一个新连接。

关于c井的HttpClient的特性

实验4):

服务器随便给个符合Http协议的应答(需要用户自行写一个tcp应答程序,或者找一个可以自动应答的工具):

HTTP/1.1 200 OK
Content-Length: 5
 
abcde

此时,如果是注释掉task.Wait(),那么结果是每次都能得到这个应答,并且最多维护2个连接。如下图:

关于c井的HttpClient的特性

如果保留task.Wait(),最符合我们想象的情形出现了,就是我们会发现网络稳定的情形下HttpClient保持仅仅1个连接!!!! 此时是最省资源的情形了,也就是所有的POST信息都是在一个连接上进行的,系统也是最节约资源的!!! 如下图:

关于c井的HttpClient的特性

实验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信息:

关于c井的HttpClient的特性

服务器这边,在对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业务不被阻塞。也因此需要对网络情况,服务器应答的及时性进行优化。

相关推荐