Настройка прокси для каждого запроса (или вращающихся прокси) с помощью .NET Flurl / HttpClient - PullRequest
0 голосов
/ 08 октября 2018

Я знаю, что с помощью библиотеки Flurl HTTP .NET я могу установить глобальный прокси-сервер с помощью пользовательского HttpClientFactory, но есть способ выбратьнастраиваемый прокси для каждого запроса ?

Со многими другими языками программирования установить прокси так же просто, как установить параметр.Например, с Node.js я могу сделать:

const request = require('request');
let opts = { url: 'http://random.org', proxy: 'http://myproxy' };
request(opts, callback);

идеальный способ сделать это с Flurl будет примерно таким, что в настоящее время невозможно:

await "http://random.org".WithProxy("http://myproxy").GetAsync();

Я также знаю, что создание FlurlClient / HttpClient для каждого запроса не вариант, из-за проблемы исчерпания сокета , с которой я тоже сталкивался в прошлом.

Сценарий для этого заключается в том, что вам нужно иметь пул прокси-серверов, которые каким-то образом поворачиваются, чтобы каждый HTTP-запрос потенциально использовал другой URL-адрес прокси.

1 Ответ

0 голосов
/ 08 октября 2018

Итак, после некоторого обсуждения с создателем 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();
...