C# HttpClient使用和注意事项,.NET Framework连接池并发限制

Posted China soft

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C# HttpClient使用和注意事项,.NET Framework连接池并发限制相关的知识,希望对你有一定的参考价值。

System.Net.Http.HttpClient 类用于发送 HTTP 请求以及从 URI 所标识的资源接收 HTTP 响应。 HttpClient 实例是应用于该实例执行的所有请求的设置集合,每个实例使用自身的连接池,该池将其请求与其他请求隔离开来。 从 .NET Core 2.1 开始,SocketsHttpHandler 类提供实现,使行为在所有平台上保持一致。

HttpClient实例是执行网络请求的设置集合,每个实例会使用一个连接池。通过这段描述我们知道实际使用HttpClient的时候我们只需要实例化一个就行了,在处理程序实例内池连接,并在多个请求之间重复使用连接。也就是官方提倡的使用单个实例,如果每次请求就实例化一个HttpClient,则会创建不必要的连接降低性能,并且TCP 端口不会在连接关闭后立即释放。

所以如果是大批量创建HttpClient请求则大量负载下可用的套接字数将耗尽,这种耗尽将导致 SocketException 错误。

使用方式

  1. 使用静态变量。
static readonly HttpClient httpClient = new HttpClient();

  1. 使用单例模式
    public class HttpClientInstance
    
        private static readonly HttpClient _HttpClient;

        static HttpClientInstance()
        
            _HttpClient = new HttpClient();
        

        public static HttpClient GetHttpClient()
        
            return _HttpClient;
        
    

实例化参数

可以通过构造参数(如 HttpClientHandler (或 SocketsHttpHandler .NET Core 2.1 或更高版本) )作为构造函数的一部分来配置其他选项。 实例化HttpClient后无法更连接属性,因此,如果需要更改连接属性,则需要创建新的 HttpClient 实例。

配置可以在构造期间配置 HttpClientHandler 或 SocketsHttpHandler 传入,SocketsHttpHandler可以设置额外参数包括 MaxConnectionsPerServer, PooledConnectionIdleTimeout、PooledConnectionLifetime、ConnectTimeout。

  • MaxConnectionsPerServer:获取或设置使用 HttpClient 对象发出请求时允许的最大并发连接数(每个服务器终结点)。 请注意,该限制针对每个服务器终结点,例如,值为 256 表示允许 256 个到 http://www.adatum.com/ 的并发连接,以及另外 256 个到 http://www.adventure-works.com/ 的并发连接。
  • PooledConnectionIdleTimeout: PooledConnectionLifetime 指定的时间范围过后,系统会关闭连接,然后创建一个新连接。
  • PooledConnectionLifetime:指定要用于连接池中每个连接的超时值。 如果连接处于空闲状态,则连接会立即关闭;否则,连接在当前请求结束时关闭。
  • ConnectTimeout:指定在请求需要创建新的 TCP 连接时使用的超时。 如果发生超时,将取消请求 Task 。
var handler = new SocketsHttpHandler

    PooledConnectionLifetime = TimeSpan.FromMinutes(15)
;
var httpClient = new HttpClient(handler);

在.NET Framework 只能使用HttpClientHandler,且没有PooledConnectionIdleTimeout和PooledConnectionLifetime等参数。

HttpClientHandler httpClientHandler = new HttpClientHandler();
//最大并发连接数
httpClientHandler.MaxConnectionsPerServer = 100;

HttpClient httpClient=new HttpClient(httpClientHandler);
//超时设置
httpClient.Timeout = new TimeSpan(5000);

可以是设置MaxConnectionsPerServer,可以设置Timeout。Timeout 为来自 HttpClient 实例的所有 HTTP 请求设置默认超时。 超时仅适用于导致启动请求/响应的 xxxAsync 方法。 如果达到超时,则会 Task 取消该请求。这个超时时间是包含从请求到响应的整个时间段,而不像HttpClientHandler参数可以设置连接超时。

请求实现

HttpClient这是一个高级 API,用于包装其运行的每个平台上可用的较低级别功能。

在每个平台上, HttpClient 尝试使用最佳可用传输:

注意事项

在上面实现可以看到在不同的框架下HttpClient的实现是不一样的,在.NET Framework下是使用HttpWebRequest支持。

所以还会受限HttpWebRequest的实现,如果我们要启用多线程高频率调用接口,那么这里要注意HttpWebRequest的连接并发的数量限制。HttpWebRequest通过ServicePoint设置,我们通过反编译看到HttpWebRequest构造函数。

ServicePoint.DefaultConnectionLimit获取允许的最大并发连接数。 对于 ASP.NET 托管的应用程序,默认连接限制为 10,对于所有其他应用程序,默认连接限制为 2。DefaultConnectionLimit 对现有 ServicePoint 对象没有影响;它只影响更改后初始化的对象。如果未直接或通过配置设置此属性的值,则该值默认为常量 DefaultPersistentConnectionLimit

如果是应用连接池默认只有2个并发,所以当你启用很多线程访问同一服务的时候实际效率是不会提升的,一直只有两个并发在阻塞排队,如果请求比较耗时后面的请求还有异常的可能。

因此当你使用多线程的时候要注意初始化HttpClient的httpClientHandler.MaxConnectionsPerServer = n;该参数用于设置。

RestSharp

平时我们可能使用RestSharp 用于网络请求,实际也是在HttpWebRequest上的封装,在官网我们可以看到如下说明:

在最新的v107换成了HttpClient,以前的版本也是HttpWebRequest。如果要设置RestSharp的连接池并发数需要修改默认值。

System.Net.ServicePointManager.DefaultConnectionLimit = n;
INI 复制 全屏

然后再实例化RestClient。

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

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

 

最近在测试一个第三方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使用和注意事项,.NET Framework连接池并发限制的主要内容,如果未能解决你的问题,请参考以下文章

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

C# 9+ 中的 HttpClient 空警告

.NET 4.5 和 C# 中带有 HttpClient 的 HTTP HEAD 请求

在 c# (.net core) 中使用 httpclient 时无法建立 SSL 连接,但在 Postman 和 Browser 中有效

使用 HttpClient C# .NET 获取 URL

如何在 C# (.NET 4.5) 中为 HttpClient.GetAsync(URI) 创建回调?