所以,在与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类完成非常相似的操作。
public static class FlurlClientManager
{
private static readonly ConcurrentDictionary<string, IFlurlClient> Clients =
new ConcurrentDictionary<string, IFlurlClient>();
public static IFlurlClient GetClient(Url url)
{
if (url == null)
{
throw new ArgumentNullException(nameof(url));
}
return PerHostClientFromCache(url);
}
public static IFlurlClient GetProxiedClient()
{
string proxyUrl = ChooseProxy();
return ProxiedClientFromCache(proxyUrl);
}
private static string ChooseProxy()
{
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();