4

我知道使用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。

4

1 回答 1

9

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

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

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

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

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

最后,工厂将创建:

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

可以在此处找到此解决方案的一些代码。注册了定制工厂之后,就没有什么可做的了。如果工厂决定这样做,标准请求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);
    }
}

这个静态类保持一个全局池FlurlClients。与之前的解决方案一样,池包括:

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

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

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

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

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

对于代理请求,请使用:

await FlurlClientManager.GetProxiedClient().Request(url).GetAsync();
于 2018-10-08T19:24:49.810 回答