Итак, после некоторого обсуждения с создателем Flurl ( # 228 и # 374 ), решение, с которым мы столкнулись, заключается в использовании пользовательского класса менеджера FlurlClient, которыйотвечает за создание необходимых FlurlClient
s и связанных HttpClient
экземпляров.Это необходимо, потому что каждый FlurlClient
может использовать только один прокси-сервер за раз, потому что ограничения .NET HttpClient
разработаны.
Если вы ищете реальное решение (икод), вы можете пропустить этот ответ до конца. Следующий раздел по-прежнему помогает, если вы хотите лучше понять.
Итак, первая исследуемая идея заключалась в создании пользовательского FlurlClientFactory
, который реализуетинтерфейс IFlurlClientFactory
.
Фабрика поддерживает пул FlurlClient
с, а когда требуется отправить новый запрос, фабрика вызывается с Url
в качестве входного параметра.Затем выполняется некоторая логика, чтобы решить, должен ли запрос проходить через прокси или нет.URL-адрес потенциально может быть использован как дискриминатор для выбора прокси-сервера для конкретного запроса.В моем случае для каждого запроса будет выбран случайный прокси, а затем будет возвращено кэшированное FlurlClient
.
В итоге фабрика создаст:
- максимум один
FlurlClient
для каждого URL-адреса прокси (который затем будет использоваться для всех запросов, которые должны проходить через этот прокси-сервер); - набор клиентов для«нормальные» запросы.
Некоторый код для этого решения можно найти здесь .После регистрации кастомной фабрики делать больше нечего.Стандартные запросы, такие как await "http://random.org".GetAsync();
, будут автоматически прокси, , если фабрика решила сделать это.
К сожалению, это решение имеет недостаток.Оказывается, пользовательская фабрика вызывается несколько раз в процессе построения запроса с Flurl.По моему опыту, это называется как минимум 3 раза .Это может привести к проблемам, так как фабрика может не вернуть тот же FlurlClient
для того же входного URL .
Решение
Решение состоит в том, чтобы создать пользовательскийFlurlClientManager
класса, чтобы полностью обойти механизм фабрики FlurlClient и сохранить пользовательский пул клиентов, которые предоставляются по запросу.
Хотя это решение специально создано для работы с потрясающей библиотекой Flurlочень похожую вещь можно сделать, используя класс HttpClient
напрямую.
/// <summary>
/// Static class that manages cached IFlurlClient instances
/// </summary>
public static class FlurlClientManager
{
/// <summary>
/// Cache for the clients
/// </summary>
private static readonly ConcurrentDictionary<string, IFlurlClient> Clients =
new ConcurrentDictionary<string, IFlurlClient>();
/// <summary>
/// Gets a cached client for the host associated to the input URL
/// </summary>
/// <param name="url"><see cref="Url"/> or <see cref="string"/></param>
/// <returns>A cached <see cref="FlurlClient"/> instance for the host</returns>
public static IFlurlClient GetClient(Url url)
{
if (url == null)
{
throw new ArgumentNullException(nameof(url));
}
return PerHostClientFromCache(url);
}
/// <summary>
/// Gets a cached client with a proxy attached to it
/// </summary>
/// <returns>A cached <see cref="FlurlClient"/> instance with a proxy</returns>
public static IFlurlClient GetProxiedClient()
{
string proxyUrl = ChooseProxy();
return ProxiedClientFromCache(proxyUrl);
}
private static string ChooseProxy()
{
// Do something and return a proxy URL
return "http://myproxy";
}
private static IFlurlClient PerHostClientFromCache(Url url)
{
return Clients.AddOrUpdate(
key: url.ToUri().Host,
addValueFactory: u => {
return CreateClient();
},
updateValueFactory: (u, client) => {
return client.IsDisposed ? CreateClient() : client;
}
);
}
private static IFlurlClient ProxiedClientFromCache(string proxyUrl)
{
return Clients.AddOrUpdate(
key: proxyUrl,
addValueFactory: u => {
return CreateProxiedClient(proxyUrl);
},
updateValueFactory: (u, client) => {
return client.IsDisposed ? CreateProxiedClient(proxyUrl) : client;
}
);
}
private static IFlurlClient CreateProxiedClient(string proxyUrl)
{
HttpMessageHandler handler = new SocketsHttpHandler()
{
Proxy = new WebProxy(proxyUrl),
UseProxy = true,
PooledConnectionLifetime = TimeSpan.FromMinutes(10)
};
HttpClient client = new HttpClient(handler);
return new FlurlClient(client);
}
private static IFlurlClient CreateClient()
{
HttpMessageHandler handler = new SocketsHttpHandler()
{
PooledConnectionLifetime = TimeSpan.FromMinutes(10)
};
HttpClient client = new HttpClient(handler);
return new FlurlClient(client);
}
}
Этот статический класс поддерживает глобальный пул FlurlClient
с.Как и в предыдущем решении, пул состоит из:
- один клиент на прокси ;
- один клиент на хост для всехзапросы, которые не должны проходить через прокси (на самом деле это фабричная стратегия Flurl по умолчанию).
В этой реализации класса прокси выбирается самим классом (используя любую политику,хотите, например, циклический перебор или случайный), но он может быть адаптирован для использования прокси-URL в качестве входных данныхВ этом случае помните, что с этой реализацией клиенты никогда не удаляются после создания, поэтому вы можете подумать об этом.
В этой реализации также использовалась новая опция SocketsHttpHandler.PooledConnectionLifetime
, доступная с .NET Core.2.1, для решения проблем DNS, возникающих, когда ваши экземпляры HttpClient
имеют длительный срок службы.В .NET Framework вместо этого следует использовать свойство ServicePoint.ConnectionLeaseTimeout
.
Использовать класс менеджера легко.Для обычных запросов используйте:
await FlurlClientManager.GetClient(url).Request(url).GetAsync();
Для прокси-запросов используйте:
await FlurlClientManager.GetProxiedClient().Request(url).GetAsync();