使用.NET Flurl / HttpClient设置每个请求的代理(或旋转代理)

时间:2018-10-08 19:24:50

标签: c# flurl

我知道,使用Flurl HTTP .NET库,我可以使用自定义HttpClientFactory来设置全局代理,但是有一种方法可以为每个请求选择自定义代理< / strong>?

使用许多其他编程语言,设置代理就像设置选项一样容易。例如,使用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();

我也知道,由于socket exhaustion issue(我过去也经历过),因此无法为每个请求创建FlurlClient / HttpClient。 / p>

这种情况的情况是,您需要一个以某种方式轮换的代理池,以便每个HTTP请求可能使用不同的代理URL。

1 个答案:

答案 0 :(得分:1)

因此,在与Flurl创建者(#228#374)进行了讨论之后,我们想到的解决方案是使用自定义的FlurlClient管理器类,该类负责创建所需的FlurlClient和链接的HttpClient实例。之所以需要这样做是因为每个FlurlClient一次只能使用一个代理,这限制了.NET HttpClient的设计方式。

如果您正在寻找实际的解决方案(和代码),则可以跳到该答案的结尾。如果您想更好地理解,以下部分仍然会有所帮助。

因此,第一个探索的想法是创建实现FlurlClientFactory接口的自定义IFlurlClientFactory

工厂保留FlurlClient的池,并且当需要发送新请求时,将工厂以Url作为输入参数来调用。然后执行一些逻辑,以确定请求是否应通过代理。该URL可能用作选择用于特定请求的代理的区分符。在我的情况下,将为每个请求选择一个随机代理,然后返回缓存的FlurlClient

最后,工厂将创建:

  • 最多 每个代理URL一个FlurlClient (然后将用于所有必须通过该代理的请求);
  • 一组用于“正常”请求的客户端。

可以找到此解决方案的某些代码here。注册自定义工厂后,将无需执行其他任何操作。像await "http://random.org".GetAsync();这样的标准请求会被自动代理,如果工厂决定这样做。

不幸的是,该解决方案有一个缺点。事实证明,在使用Flurl构建请求的过程中,自定义工厂被多次调用。根据我的经验,它称为at least 3 times。这可能会导致问题,因为工厂对于相同的输入URL可能不会返回相同的FlurlClient

解决方案

解决方案是构建自定义的 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 s的全局池。与以前的解决方案一样,该池包括:

  • 每个代理一个客户端
  • 每个主机一个客户端,用于所有不通过代理的请求(这实际上是Flurl的默认出厂策略)。

在该类的此实现中,代理由类本身选择(使用您想要的任何策略,例如循环或随机),但可以进行调整以将代理URL作为输入。在这种情况下,请记住,使用此实现,客户端在创建之后就不会被丢弃,因此您可能需要考虑一下。

此实现还使用了自.NET Core 2.1之后可用的新SocketsHttpHandler.PooledConnectionLifetime选项,以解决HttpClient实例的寿命很长时出现的DNS问题。在.NET Framework上,应改为使用ServicePoint.ConnectionLeaseTimeout属性。

使用经理类很容易。对于正常请求,请使用:

await FlurlClientManager.GetClient(url).Request(url).GetAsync();

对于代理请求,请使用:

await FlurlClientManager.GetProxiedClient().Request(url).GetAsync();