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

6

我知道使用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个回答

12
所以,在与Flurl创作者(#228#374)讨论后,我们想出的解决方案是使用自定义的FlurlClient管理类,该类负责创建所需的FlurlClient和相关的HttpClient实例。这是必要的,因为每个FlurlClient只能一次使用一个代理,这是由于.NET HttpClient的设计限制所致。 如果您正在寻找实际的解决方案(和代码),可以跳到本答案的结尾。以下部分仍然有助于更好地理解。
[更新:我还构建了一个HTTP客户端库,可以处理下面的所有内容,允许轻松设置每个请求的代理。它被称为PlainHttp。]
因此,第一个探索的想法是创建一个自定义的FlurlClientFactory,该工厂实现了IFlurlClientFactory接口。
工厂保留了一组FlurlClients,当需要发送新请求时,将调用该工厂,并将Url作为输入参数。然后执行一些逻辑以决定是否应通过代理发送请求。URL可能被用作选择特定请求的代理的鉴别器。在我的情况下,每个请求都会选择一个随机代理,然后返回缓存的FlurlClient。
最终,工厂将创建:
- 最多一个FlurlClient每个代理URL(然后将用于必须通过该代理进行的所有请求); - 一组“正常”请求的客户端。
可以在这里找到此解决方案的一些代码。注册自定义工厂后,就没有什么其他事情要做了。如果工厂决定这样做,标准请求如`await "http://random.org".GetAsync()`将被自动代理。
很不幸,这个解决方案有一个缺点。在使用Flurl构建请求的过程中,自定义工厂会被调用多次。根据我的经验,它至少被调用3次。这可能会导致问题,因为对于相同的输入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池。与之前的解决方案一样,该池包含以下内容:
  • 每个代理一个客户端
  • 每个主机一个客户端,用于所有不需要通过代理进行的请求(实际上是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();

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接