因此,在与 Flurl 创建者(#228和#374)讨论之后,我们提出的解决方案是使用自定义 FlurlClient 管理器类,该管理器类将负责创建所需FlurlClient
的 s 和链接的HttpClient
实例。这是必需的,因为每个FlurlClient
代理一次只能使用一个代理,以限制 .NETHttpClient
的设计方式。
如果您正在寻找实际的解决方案(和代码),您可以跳到此答案的末尾。如果您想更好地理解,以下部分仍然会有所帮助。
[更新:我还构建了一个 HTTP 客户端库,负责处理以下所有内容,允许开箱即用地设置每个请求的代理。它被称为PlainHttp。]
因此,第一个探索的想法是创建一个FlurlClientFactory
实现IFlurlClientFactory
接口的自定义。
工厂保留一个FlurlClient
s 池,当需要发送新请求时,以 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);
}
}
这个静态类保持一个全局池FlurlClient
s。与之前的解决方案一样,池包括:
- 每个代理一个客户端;
- 每个主机一个客户端,用于所有不能通过代理的请求(这实际上是 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();