C#中HttpClient使用注意:预热与长链接

转自:http://www.cnblogs.com/dudu/p/csharp-httpclient-attention.htmlhtml

 

最近在测试一个第三方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的某种预热机制在搞鬼!async

既然知道了是HttpClient预热机制的缘由,那咱们能够帮HttpClient进行热身,减小第一次请求的耗时。咱们尝试了一种预热方式,在正式发http post请求以前,先发一个http head请求,代码以下:post

_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进行网络抓包也验证了这一点)。spa

在以前的测试中,咱们每次用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抓包也验证了长链接的效果:

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");

如今终于能够收官了。可是确定不完美,分享的只是解决问题的过程。

相关文章
相关标签/搜索