C#中HttpClient使用注意(预热与长连接)

 更新时间:2022年02月14日 10:29:53   作者:dudu  
本文主要介绍了C#中HttpClient使用,文中通过示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下

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

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

  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抓包

这时,你

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

也许会产生这样的疑问:将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内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • C#不改变图像长宽比例调整图像大小方式

    C#不改变图像长宽比例调整图像大小方式

    文章介绍了在UI显示图片时,当容器大小固定而图片尺寸大于容器时,可以通过编码计算长宽来重绘图像,以确保图像的长宽比例不变,C#代码示例展示了如何在Winform前端实现这一功能
    2025-01-01
  • C#中值类型和引用类型的使用与比较

    C#中值类型和引用类型的使用与比较

    在C#中,数据类型分为值类型和引用类型两种,引用类型变量存储的是数据的引用,而值类型变量直接存储数据,下面小编就来和大家讲讲这二者的使用与区别吧
    2023-07-07
  • C#通过重写Panel改变边框颜色与宽度的方法

    C#通过重写Panel改变边框颜色与宽度的方法

    这篇文章主要介绍了C#通过重写Panel改变边框颜色与宽度的方法,涉及C#针对Panel控件的重写与属性设置技巧,具有一定参考借鉴价值,需要的朋友可以参考下
    2015-08-08
  • 详解C#如何提取PDF文档中的图片

    详解C#如何提取PDF文档中的图片

    提取图片可以将这些图像资源进行单独保存,方便后续在不同的项目中使用,下面我们就来看看如何使用C#通过代码从PDF文档中提取图片吧
    2025-04-04
  • 详解c# 中的DateTime

    详解c# 中的DateTime

    这篇文章主要介绍了c# 中的DateTime的相关资料,文中讲解非常细致,代码帮助大家更好的理解和学习,感兴趣的朋友可以了解下
    2020-07-07
  • WPF如何实现日期范围选择器

    WPF如何实现日期范围选择器

    这篇文章主要为大家详细介绍了WPF如何实现日期范围选择器,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下
    2024-11-11
  • C# 通过NI-VISA操作Tektronix TBS 2000B系列示波器的实现步骤

    C# 通过NI-VISA操作Tektronix TBS 2000B系列示波器的实现步骤

    这篇文章主要介绍了C# 通过NI-VISA操作Tektronix TBS 2000B系列示波器的实现步骤,帮助大家更好的理解和学习使用c#,感兴趣的朋友可以了解下
    2021-02-02
  • 使用C#实现解析Excel公式

    使用C#实现解析Excel公式

    在日常工作中,我们经常需要在Excel中使用公式对表中数据进行计算和分析,所以本文小编主要来和大家介绍一下如何在C#中实现解析Excel公式,感兴趣的可以了解下
    2024-02-02
  • C# 并行和多线程编程——并行集合和PLinq

    C# 并行和多线程编程——并行集合和PLinq

    这篇文章主要介绍了C# 并行和多线程编程的相关资料,帮助大家更好的理解和学习使用c#,感兴趣的朋友可以了解下
    2021-02-02
  • C# 封装HtmlHelper组件:BootstrapHelper

    C# 封装HtmlHelper组件:BootstrapHelper

    这篇文章主要介绍了C# 封装HtmlHelper组件之BootstrapHelper 的相关资料,需要的朋友可以参考下
    2016-08-08

最新评论