使用 .NET Flurl/HttpClient 设置按请求代理(或轮换代理)

Setting a per-request proxy (or rotating proxies) with .NET Flurl/HttpClient

我知道Flurl HTTP .NET library I can set a global proxy by using a custom 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 不是一个选项,因为 socket exhaustion issue,我自己过去也经历过.

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

因此,在与 Flurl 创建者 (#228 and #374) 讨论后,我们提出的解决方案是使用自定义 FlurlClient 管理器 class,它将负责创建所需的FlurlClients 和链接的 HttpClient 个实例。这是必需的,因为每个 FlurlClient 一次只能使用一个代理,以限制 .NET HttpClient 的设计方式。

如果你正在寻找实际的解决方案(和代码),你可以跳到这个答案的末尾。如果你想更好地理解,下面的部分仍然有帮助.

[更新:我还构建了一个 HTTP 客户端库来处理以下所有内容,允许开箱即用地设置每个请求的代理。叫做PlainHttp.]

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

工厂保留了一个 FlurlClient 的池,当需要发送新请求时,以 Url 作为输入参数调用工厂。然后执行一些逻辑来决定请求是否应该通过代理。 URL 可能被用作选择用于特定请求的代理的鉴别器。在我的例子中,将为每个请求选择一个随机代理,然后缓存 FlurlClient 将被 returned.

最后,工厂会创建:

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

可以找到此解决方案的一些代码 here。注册自定义工厂后,就没有其他事可做了。像 await "http://random.org".GetAsync(); 这样的标准请求将被 自动 代理, 如果 工厂决定这样做。

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

解决方案

解决方案是构建自定义 FlurlClientManager class,以完全绕过 FlurlClient 工厂机制并保留提供的自定义客户端池按需。

虽然这个解决方案是专门为与令人敬畏的 Flurl 库一起工作而构建的,但可以直接使用 HttpClient class 来完成非常相似的事情。

/// <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);
    }
}

这个静态 class 保留了一个 FlurlClient 的全局池。与之前的解决方案一样,该池包括:

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

在 class 的这个实现中,代理由 class 本身选择(使用任何你想要的策略,例如循环法或随机法),但它可以适应采取代理 URL 作为输入。在那种情况下,请记住,使用此实现,客户端在创建后永远不会被释放,因此您可能需要考虑一下。

此实现还使用了自 .NET Core 2.1 起可用的新 SocketsHttpHandler.PooledConnectionLifetime 选项,以解决当您的 HttpClient 实例具有较长生命周期时出现的 DNS 问题。在 .NET Framework 上,应该使用 ServicePoint.ConnectionLeaseTimeout 属性。

使用管理器 class 很简单。对于普通请求,使用:

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

对于代理请求,使用:

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