具有多个代理的 HttpClient,同时处理套接字耗尽和 DNS 回收
HttpClient with multiple proxies while handling socket exhaustion and DNS recycling
我们正在和一个朋友一起做一个有趣的项目,我们必须执行数百个 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();
}
}
}
}
顺便说一下,我们正在使用 .NET Core(目前是控制台应用程序)。我知道有很多关于套接字耗尽和处理 DNS 回收的线程,但是这个特定的线程是不同的,因为使用了多个代理。
如果我们使用 HttpClient 的单例实例,就像大家建议的那样:
- 我们不能设置多个代理,因为它是在 HttpClient 实例化期间设置的,之后无法更改。
- 它不考虑 DNS 更改。 Re-using HttpClient 的一个实例意味着它会保留套接字直到它关闭,因此如果您在服务器上发生 DNS 记录更新,客户端将永远不知道该套接字关闭。一种解决方法是将
keep-alive
header 设置为 false
,这样套接字将在每次请求后关闭。它导致 sub-optimal 性能。第二种方法是使用 ServicePoint
:
ServicePointManager.FindServicePoint("http://x.com")
.ConnectionLeaseTimeout = Convert.ToInt32(TimeSpan.FromSeconds(15).TotalMilliseconds);
ServicePointManager.DnsRefreshTimeout = Convert.ToInt32(TimeSpan.FromSeconds(5).TotalMilliseconds);
另一方面,处理 HttpClient(就像我上面的例子),换句话说,HttpClient 的多个实例,导致多个套接字处于 TIME_WAIT
状态。 TIME_WAIT表示本地端点(本端)已关闭连接。
我知道 SocketsHttpHandler
和 IHttpClientFactory
,但他们无法解决不同的代理。
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);
可以做出的最明智的决定是什么?
将我的评论收集到答案中。但这些是改进建议,而不是解决方案,因为你的问题在很大程度上取决于上下文:有多少代理、每分钟多少请求、每个请求的平均时间是多少等。
免责声明:我不熟悉 IHttpClientFactory
但据我所知,这是解决 Socket 耗尽和 DNS 问题的唯一方法。
注意: ServicePointManager
不影响 .NET Core 中的 HttpClient
因为它打算与未使用的 HttpWebRequest
一起使用通过 HttpClient
在 .NET Core 中。
正如@GuruStron 所建议的,每个代理 HttpClient
个实例看起来是合理的解决方案。
HttpResponseMessage
是 IDisposable
。为其应用 using 语句。它将影响套接字使用行为。
您可以申请 HttpCompletionOption.ResponseHeadersRead
到 SendAsync
,因为在发送请求时不阅读整个响应。然后如果服务器返回不成功状态代码,您可能无法读取响应。
为了提高内部性能,您还可以在 SendAsync()
和 ReadAsStringAsync()
行附加 .ConfigureAwait(false)
。如果当前 SynchronizationContext
不是 null
(例如它不是控制台应用程序),它最有用。
这里是稍微优化过的代码 (C# 8.0):
private static async Task<string> GetHttpResponseAsync(HttpClient client, string url)
{
using HttpResponseMessage response = await client.GetAsync(url, HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false);
if (response.IsSuccessStatusCode)
{
return await response.Content.ReadAsStringAsync().ConfigureAwait(false);
}
return null;
}
将合并的 HttpClient
和 URL 传递给方法。
重用 HttpClient
个实例(或更具体地说,重用最后一个 HttpMessageHandler
)的要点是重用套接字连接。不同的代理意味着不同的套接字连接,因此尝试在 不同的 代理上重用 HttpClient
/HttpMessageHandler
没有意义,因为它必须是一个不同的连接。
we have to execute hundreds of HTTP requests, all using different proxies
如果每个请求都是真正唯一的代理,并且任何其他请求之间不共享任何代理,那么您最好保留单独的 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 });
}
ConfigurePrimaryHttpMessageHandler
控制 IHttpClientFactory
如何创建池化的主要 HttpMessageHandler
实例。我从你问题的代码中复制了 HttpClientHandler
,但大多数现代应用程序使用 SocketsHttpHandler
,它也有 Proxy
/UseProxy
属性。
然后,当您想使用一个时,调用 IHttpClientFactory.CreateClient
并传递您想要的 HttpClient
的名称:
for (int i = 0; i < 20; i++)
{
var client = _httpClientFactory.CreateClient(randomProxyName);
...
}
首先,我想提一下,如果代理在 compile-time 已知,@Stephen Cleary 的示例可以正常工作,但在我的例子中,它们在运行时已知。我忘了在问题中提到这一点,所以这是我的错。
感谢@aepot 指出这些内容。
这就是我想出的解决方案(感谢@mcont):
/// <summary>
/// A wrapper class for <see cref="FlurlClient"/>, which solves socket exhaustion and DNS recycling.
/// </summary>
public class FlurlClientManager
{
/// <summary>
/// Static collection, which stores the clients that are going to be reused.
/// </summary>
private static readonly ConcurrentDictionary<string, IFlurlClient> _clients = new ConcurrentDictionary<string, IFlurlClient>();
/// <summary>
/// Gets the available clients.
/// </summary>
/// <returns></returns>
public ConcurrentDictionary<string, IFlurlClient> GetClients()
=> _clients;
/// <summary>
/// Creates a new client or gets an existing one.
/// </summary>
/// <param name="clientName">The client name.</param>
/// <param name="proxy">The proxy URL.</param>
/// <returns>The <see cref="FlurlClient"/>.</returns>
public IFlurlClient CreateOrGetClient(string clientName, string proxy = null)
{
return _clients.AddOrUpdate(clientName, CreateClient(proxy), (_, client) =>
{
return client.IsDisposed ? CreateClient(proxy) : client;
});
}
/// <summary>
/// Disposes a client. This leaves a socket in TIME_WAIT state for 240 seconds but it's necessary in case a client has to be removed from the list.
/// </summary>
/// <param name="clientName">The client name.</param>
/// <returns>Returns true if the operation is successful.</returns>
public bool DeleteClient(string clientName)
{
var client = _clients[clientName];
client.Dispose();
return _clients.TryRemove(clientName, out _);
}
private IFlurlClient CreateClient(string proxy = null)
{
var handler = new SocketsHttpHandler()
{
Proxy = proxy != null ? new WebProxy(proxy, true) : null,
PooledConnectionLifetime = TimeSpan.FromMinutes(10)
};
var client = new HttpClient(handler);
return new FlurlClient(client);
}
}
每个请求一个代理意味着每个请求一个额外的套接字(另一个 HttpClient 实例)。
在上面的解决方案中,ConcurrentDictionary
用于存储HttpClients,所以我可以重用它们,这就是HttpClient的确切意义。在它被 API 限制阻止之前,我可以对 5 个请求使用相同的代理。我也忘了在问题中提到这一点。
如您所见,有两种解决套接字耗尽和 DNS 回收的解决方案:IHttpClientFactory
和 SocketsHttpHandler
。第一个不适合我的情况,因为我使用的代理在运行时是已知的,而不是在 compile-time。上面的解决方案使用了第二种方式。
有同样问题的朋友可以阅读GitHub上的following issue。它解释了一切。
我open-minded求改进,戳我吧
我们正在和一个朋友一起做一个有趣的项目,我们必须执行数百个 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();
}
}
}
}
顺便说一下,我们正在使用 .NET Core(目前是控制台应用程序)。我知道有很多关于套接字耗尽和处理 DNS 回收的线程,但是这个特定的线程是不同的,因为使用了多个代理。
如果我们使用 HttpClient 的单例实例,就像大家建议的那样:
- 我们不能设置多个代理,因为它是在 HttpClient 实例化期间设置的,之后无法更改。
- 它不考虑 DNS 更改。 Re-using HttpClient 的一个实例意味着它会保留套接字直到它关闭,因此如果您在服务器上发生 DNS 记录更新,客户端将永远不知道该套接字关闭。一种解决方法是将
keep-alive
header 设置为false
,这样套接字将在每次请求后关闭。它导致 sub-optimal 性能。第二种方法是使用ServicePoint
:
ServicePointManager.FindServicePoint("http://x.com")
.ConnectionLeaseTimeout = Convert.ToInt32(TimeSpan.FromSeconds(15).TotalMilliseconds);
ServicePointManager.DnsRefreshTimeout = Convert.ToInt32(TimeSpan.FromSeconds(5).TotalMilliseconds);
另一方面,处理 HttpClient(就像我上面的例子),换句话说,HttpClient 的多个实例,导致多个套接字处于 TIME_WAIT
状态。 TIME_WAIT表示本地端点(本端)已关闭连接。
我知道 SocketsHttpHandler
和 IHttpClientFactory
,但他们无法解决不同的代理。
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);
可以做出的最明智的决定是什么?
将我的评论收集到答案中。但这些是改进建议,而不是解决方案,因为你的问题在很大程度上取决于上下文:有多少代理、每分钟多少请求、每个请求的平均时间是多少等。
免责声明:我不熟悉 IHttpClientFactory
但据我所知,这是解决 Socket 耗尽和 DNS 问题的唯一方法。
注意: ServicePointManager
不影响 .NET Core 中的 HttpClient
因为它打算与未使用的 HttpWebRequest
一起使用通过 HttpClient
在 .NET Core 中。
正如@GuruStron 所建议的,每个代理 HttpClient
个实例看起来是合理的解决方案。
HttpResponseMessage
是 IDisposable
。为其应用 using 语句。它将影响套接字使用行为。
您可以申请 HttpCompletionOption.ResponseHeadersRead
到 SendAsync
,因为在发送请求时不阅读整个响应。然后如果服务器返回不成功状态代码,您可能无法读取响应。
为了提高内部性能,您还可以在 SendAsync()
和 ReadAsStringAsync()
行附加 .ConfigureAwait(false)
。如果当前 SynchronizationContext
不是 null
(例如它不是控制台应用程序),它最有用。
这里是稍微优化过的代码 (C# 8.0):
private static async Task<string> GetHttpResponseAsync(HttpClient client, string url)
{
using HttpResponseMessage response = await client.GetAsync(url, HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false);
if (response.IsSuccessStatusCode)
{
return await response.Content.ReadAsStringAsync().ConfigureAwait(false);
}
return null;
}
将合并的 HttpClient
和 URL 传递给方法。
重用 HttpClient
个实例(或更具体地说,重用最后一个 HttpMessageHandler
)的要点是重用套接字连接。不同的代理意味着不同的套接字连接,因此尝试在 不同的 代理上重用 HttpClient
/HttpMessageHandler
没有意义,因为它必须是一个不同的连接。
we have to execute hundreds of HTTP requests, all using different proxies
如果每个请求都是真正唯一的代理,并且任何其他请求之间不共享任何代理,那么您最好保留单独的 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 });
}
ConfigurePrimaryHttpMessageHandler
控制 IHttpClientFactory
如何创建池化的主要 HttpMessageHandler
实例。我从你问题的代码中复制了 HttpClientHandler
,但大多数现代应用程序使用 SocketsHttpHandler
,它也有 Proxy
/UseProxy
属性。
然后,当您想使用一个时,调用 IHttpClientFactory.CreateClient
并传递您想要的 HttpClient
的名称:
for (int i = 0; i < 20; i++)
{
var client = _httpClientFactory.CreateClient(randomProxyName);
...
}
首先,我想提一下,如果代理在 compile-time 已知,@Stephen Cleary 的示例可以正常工作,但在我的例子中,它们在运行时已知。我忘了在问题中提到这一点,所以这是我的错。
感谢@aepot 指出这些内容。
这就是我想出的解决方案(感谢@mcont):
/// <summary>
/// A wrapper class for <see cref="FlurlClient"/>, which solves socket exhaustion and DNS recycling.
/// </summary>
public class FlurlClientManager
{
/// <summary>
/// Static collection, which stores the clients that are going to be reused.
/// </summary>
private static readonly ConcurrentDictionary<string, IFlurlClient> _clients = new ConcurrentDictionary<string, IFlurlClient>();
/// <summary>
/// Gets the available clients.
/// </summary>
/// <returns></returns>
public ConcurrentDictionary<string, IFlurlClient> GetClients()
=> _clients;
/// <summary>
/// Creates a new client or gets an existing one.
/// </summary>
/// <param name="clientName">The client name.</param>
/// <param name="proxy">The proxy URL.</param>
/// <returns>The <see cref="FlurlClient"/>.</returns>
public IFlurlClient CreateOrGetClient(string clientName, string proxy = null)
{
return _clients.AddOrUpdate(clientName, CreateClient(proxy), (_, client) =>
{
return client.IsDisposed ? CreateClient(proxy) : client;
});
}
/// <summary>
/// Disposes a client. This leaves a socket in TIME_WAIT state for 240 seconds but it's necessary in case a client has to be removed from the list.
/// </summary>
/// <param name="clientName">The client name.</param>
/// <returns>Returns true if the operation is successful.</returns>
public bool DeleteClient(string clientName)
{
var client = _clients[clientName];
client.Dispose();
return _clients.TryRemove(clientName, out _);
}
private IFlurlClient CreateClient(string proxy = null)
{
var handler = new SocketsHttpHandler()
{
Proxy = proxy != null ? new WebProxy(proxy, true) : null,
PooledConnectionLifetime = TimeSpan.FromMinutes(10)
};
var client = new HttpClient(handler);
return new FlurlClient(client);
}
}
每个请求一个代理意味着每个请求一个额外的套接字(另一个 HttpClient 实例)。
在上面的解决方案中,ConcurrentDictionary
用于存储HttpClients,所以我可以重用它们,这就是HttpClient的确切意义。在它被 API 限制阻止之前,我可以对 5 个请求使用相同的代理。我也忘了在问题中提到这一点。
如您所见,有两种解决套接字耗尽和 DNS 回收的解决方案:IHttpClientFactory
和 SocketsHttpHandler
。第一个不适合我的情况,因为我使用的代理在运行时是已知的,而不是在 compile-time。上面的解决方案使用了第二种方式。
有同样问题的朋友可以阅读GitHub上的following issue。它解释了一切。
我open-minded求改进,戳我吧