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

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

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

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

 1     public class httpClIEntTest 2     { 3         private static Readonly httpClIEnt _httpClIEnt;     4  5         static httpClIEnttest() 6         { 7             _httpClIEnt =  Uri(BASE_ADDRESS) }; 8  9             //帮httpClIEnt热身10             _httpClIEnt.SendAsync( httpRequestMessage {11                     Method = "),12                     RequestUri = ) })13                 .Result.EnsureSuccessstatusCode();14         }15 16         async Task<string> PostAsync()17 18             var response = await _httpClIEnt.PostAsync( FormUrlEncodedContent(parameters));19 20             return await response.Content.ReadAsstringAsync();21 22     }


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

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

    现在终于可以收官了。但是肯定不完美,分享的只是解决问题的过程。

总结

以上是内存溢出为你收集整理的[转]C#中HttpClient使用注意:预热与长连接全部内容,希望文章能够帮你解决[转]C#中HttpClient使用注意:预热与长连接所遇到的程序开发问题。

如果觉得内存溢出网站内容还不错,欢迎将内存溢出网站推荐给程序员好友。

欢迎分享,转载请注明来源:内存溢出

原文地址: https://outofmemory.cn/langs/1214068.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-06-04
下一篇 2022-06-04

发表评论

登录后才能评论

评论列表(0条)

保存