具有多个代理的 HttpClient,同时处理套接字耗尽和 DNS 回收

Ele*_*ron 4 c# httpclient time-wait .net-core

我们正在与朋友一起开发一个有趣的项目,我们必须执行数百个 HTTP 请求,所有请求都使用不同的代理。想象一下,它类似于以下内容:

for (int i = 0; i < 20; i++)
{
    HttpClientHandler handler = new HttpClientHandler { Proxy = new WebProxy(randomProxy, true) };

    using (var client = new HttpClient(handler))
    {
        using (var request = new HttpRequestMessage(HttpMethod.Get, "http://x.com"))
        {
            var response = await client.SendAsync(request);

            if (response.IsSuccessStatusCode)
            {
                string content = await response.Content.ReadAsStringAsync();
            }
        }

        using (var request2 = new HttpRequestMessage(HttpMethod.Get, "http://x.com/news"))
        {
            var response = await client.SendAsync(request2);

            if (response.IsSuccessStatusCode)
            {
                string content = await response.Content.ReadAsStringAsync();
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

顺便说一下,我们使用的是.NET Core(现在是控制台应用程序)。我知道有很多关于套接字耗尽和处理 DNS 回收的线程,但由于使用了多个代理,因此这个特定的线程有所不同。

如果我们使用 HttpClient 的单例实例,就像大家建议的那样:

  • 我们不能设置多个代理,因为它是在 HttpClient 实例化期间设置的,之后无法更改。
  • 它不尊重 DNS 更改。重用 HttpClient 的实例意味着它会保留套接字直到关闭,因此如果服务器上发生 DNS 记录更新,客户端将永远不会知道,直到该套接字关闭。一种解决方法是将keep-alive标头设置为false,这样套接字将在每次请求后关闭。它会导致性能次优。第二种方法是使用ServicePoint
ServicePointManager.FindServicePoint("http://x.com")  
    .ConnectionLeaseTimeout = Convert.ToInt32(TimeSpan.FromSeconds(15).TotalMilliseconds);

ServicePointManager.DnsRefreshTimeout = Convert.ToInt32(TimeSpan.FromSeconds(5).TotalMilliseconds);
Run Code Online (Sandbox Code Playgroud)

另一方面,处置 HttpClient(就像上面的示例一样),换句话说,处理 HttpClient 的多个实例,会导致多个套接字处于TIME_WAIT状态。TIME_WAIT 表示本地端点(本端)已关闭连接。

我知道SocketsHttpHandlerIHttpClientFactory,但他们无法解决不同的代理问题。

var socketsHandler = new SocketsHttpHandler
{
    PooledConnectionLifetime = TimeSpan.FromMinutes(10),
    PooledConnectionIdleTimeout = TimeSpan.FromMinutes(5),
    MaxConnectionsPerServer = 10
};

// Cannot set a different proxy for each request
var client = new HttpClient(socketsHandler);
Run Code Online (Sandbox Code Playgroud)

可以做出的最明智的决定是什么?

Ste*_*ary 5

重用HttpClient实例(或更具体地说,重用最后一个实例HttpMessageHandler)的目的是重用套接字连接。不同的代理意味着不同的套接字连接,因此尝试在不同的HttpClient代理上重用/是没有意义的,因为它必须是不同的连接。HttpMessageHandler

我们必须执行数百个 HTTP 请求,所有请求都使用不同的代理

如果每个请求确实是一个唯一的代理,并且没有代理在任何其他请求之间共享,那么您也可以只保留各个HttpClient实例并与TIME_WAIT.

但是,如果多个请求可能通过同一个代理,并且您想重新使用这些连接,那么这当然是可能的。

我建议使用IHttpClientFactory. 它允许您定义可以池化和重用的命名HttpClient实例(同样,技术上是最后一个实例)。HttpMessageHandler只需为每个代理创建一个:

var proxies = new Dictionary<string, IWebProxy>(); // TODO: populate with proxies.
foreach (var proxy in proxies)
{
  services.AddHttpClient(proxy.Key)
      .ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler { Proxy = proxy.Value });
}
Run Code Online (Sandbox Code Playgroud)

控制ConfigurePrimaryHttpMessageHandler如何IHttpClientFactory创建池化的主HttpMessageHandler实例。我HttpClientHandler从您问题中的代码复制而来,但大多数现代应用程序都使用SocketsHttpHandler,它也具有Proxy/UseProxy属性。

然后,当您想使用它时,调用IHttpClientFactory.CreateClient并传递您想要的名称HttpClient

for (int i = 0; i < 20; i++)
{
  var client = _httpClientFactory.CreateClient(randomProxyName);
  ...
}
Run Code Online (Sandbox Code Playgroud)