我知道使用 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。
因此,在与 Flurl 创建者(#228 和 #374)进行一些讨论后,我们提出的解决方案是使用自定义 FlurlClient 管理器类,该类将负责创建所需的
FlurlClient
以及链接的 HttpClient
实例。这是必需的,因为由于 .NET FlurlClient
的设计方式的限制,每个 HttpClient
一次只能使用一个代理。
如果您正在寻找实际的解决方案(和代码),您可以跳到本答案的末尾。如果您想更好地理解,以下部分仍然有帮助。
[更新:我还构建了一个 HTTP 客户端库,它负责处理下面的所有内容,允许开箱即用地设置每个请求代理。它被称为PlainHttp。]
因此,第一个探索的想法是创建一个自定义
FlurlClientFactory
来实现 IFlurlClientFactory
接口。
工厂保留了一个
FlurlClient
池,当需要发送新请求时,会以Url
作为输入参数来调用工厂。然后执行一些逻辑来决定请求是否应该通过代理。 URL 可能会被用作选择用于特定请求的代理的鉴别器。在我的例子中,将为每个请求选择一个随机代理,然后返回一个缓存的FlurlClient
。
最终,工厂将创造:
FlurlClient
(然后将用于必须通过该代理的所有请求);
here找到此解决方案的一些代码。注册了定制工厂之后,就没有什么事情要做了。像await "http://random.org".GetAsync();
这样的标准请求将被自动代理,如果工厂决定这样做。 不幸的是,这个解决方案有一个缺点。事实证明,在使用 Flurl 构建请求的过程中,
自定义工厂被多次调用。根据我的经验,被调用至少3次。这可能会导致问题,因为工厂可能不会针对相同的输入 URL 返回相同的
FlurlClient
。
解决方案FlurlClientManager
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
池。与之前的解决方案一样,池包含:
此实现还使用了自 .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();
IFlurlClientCache
现在负责在命名客户端的基础上缓存
IFlurlClient
实例。现在创建代理客户端就像这样简单:
IFlurlClient flurlClient = flurlClientCache
.GetOrAdd("CallBackWithProxyExample", "https://ineedaproxy.example.com",
b => b.ConfigureInnerHandler(h => h.Proxy = new WebProxy(proxyServer)))
其他 IFlurlClient
实例可以使用不同的缓存键创建,不需要代理。