最近在测试一个第三方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的调用代码改为如下的样子:

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();
}
}

然后测试一下请求响应时间:

  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抓包也验证了长连接的效果:

这时,你也许会产生这样的疑问:将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使用注意:预热与长连接的更多相关文章

  1. [转]C#中HttpClient使用注意:预热与长连接

    最近在测试一个第三方API,准备集成在我们的网站应用中.API的调用使用的是.NET中的HttpClient,由于这个API会在关键业务中用到,对调用API的整体响应速度有严格要求,所以对HttpCl ...

  2. 解决httpclient因为保持永久长连接造成连接吊死的问题

    httpclient使用了连接池,如果没有设置keep-alive策略,PoolingHttpClientConnectionManager会默认使用永久连接. 最近在调用京东api时,发现一个请求开 ...

  3. 【转】HTTP中的长连接和短连接分析

    1. HTTP协议与TCP/IP协议的关系 HTTP的长连接和短连接本质上是TCP长连接和短连接.HTTP属于应用层协议,在传输层使用TCP协议,在网络层使用IP协议.IP协议主要解决网络路由和寻址问 ...

  4. (转)HTTP 长连接和短连接

    1. HTTP协议与TCP/IP协议的关系 HTTP的长连接和短连接本质上是TCP长连接和短连接.HTTP属于应用层协议,在传输层使用TCP协议,在网络层使用IP协议.IP协议主要解决网络路由和寻址问 ...

  5. HTTP 长连接和短连接

    1. HTTP协议与TCP/IP协议的关系 HTTP的长连接和短连接本质上是TCP长连接和短连接.HTTP属于应用层协议,在传输层使用TCP协议,在网络层使用IP协议.IP协议主要解决网络路由和寻址问 ...

  6. (转)HTTP长连接和短连接

    1. HTTP协议与TCP/IP协议的关系 HTTP的长连接和短连接本质上是TCP长连接和短连接.HTTP属于应用层协议,在传输层使用TCP协议,在网络层使用IP协议.IP协议主要解决网络路由和寻址问 ...

  7. 【转】HTTP长连接与短连接

    1. HTTP协议与TCP/IP协议的关系 HTTP的长连接和短连接本质上是TCP长连接和短连接.HTTP属于应用层协议,在传输层使用TCP协议,在网络层使用IP协议.IP协议主要解决网络路由和寻址问 ...

  8. 转:基于ASP.NET的Comet长连接技术解析

    原文来自于: Comet技术原理 来自维基百科:Comet是一种用于web的技术,能使服务器能实时地将更新的信息传送到客户端,而无须客户端发出请求,目前有两种实现方式,长轮询和iframe流. 简单的 ...

  9. HTTP长连接和短连接(转)

    1. HTTP协议与TCP/IP协议的关系 HTTP的长连接和短连接本质上是TCP长连接和短连接.HTTP属于应用层协议,在传输层使用TCP协议,在网络层使用IP协议.IP协议主要解决网络路由和寻址问 ...

随机推荐

  1. HDU1671——前缀树的一点感触

    题目http://acm.hdu.edu.cn/showproblem.php?pid=1671 题目本身不难,一棵前缀树OK,但是前两次提交都没有成功. 第一次Memory Limit Exceed ...

  2. CRL快速开发框架系列教程七(使用事务)

    本系列目录 CRL快速开发框架系列教程一(Code First数据表不需再关心) CRL快速开发框架系列教程二(基于Lambda表达式查询) CRL快速开发框架系列教程三(更新数据) CRL快速开发框 ...

  3. [C#] C# 知识回顾 - 表达式树 Expression Trees

    C# 知识回顾 - 表达式树 Expression Trees 目录 简介 Lambda 表达式创建表达式树 API 创建表达式树 解析表达式树 表达式树的永久性 编译表达式树 执行表达式树 修改表达 ...

  4. 关于font-family

    在设置页面字体的时候,你会发现在 font-family 属性中会设置好多个字体,想看懂它们都是什么字体吗?不好意思,我不是搞设计的,我也不知道.那么,现在写的东西,只是对于一个前端人员来说,要了解的 ...

  5. 14门Linux课程,打通你Linux的任督二脉!

    Linux有很多优点:安全.自主.开源--,也正是这些优点使得很多人都在学Linux. 虽说网上有大把的Linux课程资源,但是对很多小白来说网上的课程资源比较零散并不适合新手学习. 正因为此,总结了 ...

  6. 敏捷转型历程 - Sprint3 一团糟的演示会

    我: Tech Leader 团队:团队成员分布在两个城市,我所在的城市包括我有4个成员,另外一个城市包括SM有7个成员.另外由于我们的BA离职了,我暂代IT 的PO 职位.PM和我在一个城市,但他不 ...

  7. Linux命令【第三篇】

    执行下面命令时发现提示需要输入密码,请问提示输入的密码是哪个用户的密码. [test@oldboy ~]$ sudo su - oldboy 解答: 输入当前执行命令test账户的密码. 相关说明: ...

  8. Linux实战教学笔记03:操作系统发展历程及系统版本选择

    标签(空格分隔): Linux实战教学笔记-陈思齐 第1章 Linux简介 1.1 什么是操作系统? 简单讲:操作系统就是一个人与计算机硬件的中介. 操作系统,英文名称Operating System ...

  9. BRDF 光照模型

    http://blog.csdn.net/liu_lin_xm/article/details/4846144

  10. 一不小心写了个WEB服务器

    开场 Web服务器是啥玩意? 是那个托管了我的网站的机器么? No,虽然那个也是服务器,但是我们今天要说的Web服务器主要是指像IIS这样一类的,用于处理request并返回response的工具,没 ...