Прежде всего, я хочу упомянуть, что пример @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. Я мог бы использовать один и тот же прокси для 5 запросов, прежде чем он будет заблокирован ограничениями API. Я забыл упомянуть и об этом в вопросе.
Как вы видели, есть два решения, решающих проблему нехватки сокетов и повторного использования DNS: IHttpClientFactory
и SocketsHttpHandler
. Первый не подходит для моего случая, потому что прокси, которые я использую, известны во время выполнения, а не во время компиляции. В приведенном выше решении используется второй способ.
Для тех, у кого такая же проблема, вы можете прочитать следующую проблему на GitHub. Это все объясняет.
Я открыт для улучшений, так что ткните меня.