使用 .NET Flurl/HttpClient 设置按请求代理(或轮换代理)
Setting a per-request proxy (or rotating proxies) with .NET Flurl/HttpClient
我知道Flurl HTTP .NET library I can set a global proxy by using a custom 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
不是一个选项,因为 socket exhaustion issue,我自己过去也经历过.
这种情况是当您需要一个以某种方式轮换的代理池时,以便每个 HTTP 请求都可能使用不同的代理 URL。
因此,在与 Flurl 创建者 (#228 and #374) 讨论后,我们提出的解决方案是使用自定义 FlurlClient 管理器 class,它将负责创建所需的FlurlClient
s 和链接的 HttpClient
个实例。这是必需的,因为每个 FlurlClient
一次只能使用一个代理,以限制 .NET HttpClient
的设计方式。
如果你正在寻找实际的解决方案(和代码),你可以跳到这个答案的末尾。如果你想更好地理解,下面的部分仍然有帮助.
[更新:我还构建了一个 HTTP 客户端库来处理以下所有内容,允许开箱即用地设置每个请求的代理。叫做PlainHttp.]
因此,第一个探索的想法是创建一个实现 IFlurlClientFactory
接口的自定义 FlurlClientFactory
。
工厂保留了一个 FlurlClient
的池,当需要发送新请求时,以 Url
作为输入参数调用工厂。然后执行一些逻辑来决定请求是否应该通过代理。 URL 可能被用作选择用于特定请求的代理的鉴别器。在我的例子中,将为每个请求选择一个随机代理,然后缓存 FlurlClient
将被 returned.
最后,工厂会创建:
- 最多 每个代理一个
FlurlClient
URL(然后将用于所有请求必须通过该代理);
- 一组用于“正常”请求的客户端。
可以找到此解决方案的一些代码 here。注册自定义工厂后,就没有其他事可做了。像 await "http://random.org".GetAsync();
这样的标准请求将被 自动 代理, 如果 工厂决定这样做。
不幸的是,这个解决方案有一个缺点。事实证明,在使用 Flurl 构建请求的过程中,自定义工厂被多次调用。根据我的经验,它被称为at least 3 times。这可能会导致问题,因为 工厂可能不会 return 相同的 FlurlClient
对于相同的输入 URL.
解决方案
解决方案是构建自定义 FlurlClientManager
class,以完全绕过 FlurlClient 工厂机制并保留提供的自定义客户端池按需。
虽然这个解决方案是专门为与令人敬畏的 Flurl 库一起工作而构建的,但可以直接使用 HttpClient
class 来完成非常相似的事情。
/// <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);
}
}
这个静态 class 保留了一个 FlurlClient
的全局池。与之前的解决方案一样,该池包括:
- 每个代理一个客户端;
- 每个主机一个客户端 用于所有不能通过代理的请求(这实际上是 Flurl 的默认工厂策略)。
在 class 的这个实现中,代理由 class 本身选择(使用任何你想要的策略,例如循环法或随机法),但它可以适应采取代理 URL 作为输入。在那种情况下,请记住,使用此实现,客户端在创建后永远不会被释放,因此您可能需要考虑一下。
此实现还使用了自 .NET Core 2.1 起可用的新 SocketsHttpHandler.PooledConnectionLifetime
选项,以解决当您的 HttpClient
实例具有较长生命周期时出现的 DNS 问题。在 .NET Framework 上,应该使用 ServicePoint.ConnectionLeaseTimeout
属性。
使用管理器 class 很简单。对于普通请求,使用:
await FlurlClientManager.GetClient(url).Request(url).GetAsync();
对于代理请求,使用:
await FlurlClientManager.GetProxiedClient().Request(url).GetAsync();
我知道Flurl HTTP .NET library I can set a global proxy by using a custom 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
不是一个选项,因为 socket exhaustion issue,我自己过去也经历过.
这种情况是当您需要一个以某种方式轮换的代理池时,以便每个 HTTP 请求都可能使用不同的代理 URL。
因此,在与 Flurl 创建者 (#228 and #374) 讨论后,我们提出的解决方案是使用自定义 FlurlClient 管理器 class,它将负责创建所需的FlurlClient
s 和链接的 HttpClient
个实例。这是必需的,因为每个 FlurlClient
一次只能使用一个代理,以限制 .NET HttpClient
的设计方式。
如果你正在寻找实际的解决方案(和代码),你可以跳到这个答案的末尾。如果你想更好地理解,下面的部分仍然有帮助.
[更新:我还构建了一个 HTTP 客户端库来处理以下所有内容,允许开箱即用地设置每个请求的代理。叫做PlainHttp.]
因此,第一个探索的想法是创建一个实现 IFlurlClientFactory
接口的自定义 FlurlClientFactory
。
工厂保留了一个 FlurlClient
的池,当需要发送新请求时,以 Url
作为输入参数调用工厂。然后执行一些逻辑来决定请求是否应该通过代理。 URL 可能被用作选择用于特定请求的代理的鉴别器。在我的例子中,将为每个请求选择一个随机代理,然后缓存 FlurlClient
将被 returned.
最后,工厂会创建:
- 最多 每个代理一个
FlurlClient
URL(然后将用于所有请求必须通过该代理); - 一组用于“正常”请求的客户端。
可以找到此解决方案的一些代码 here。注册自定义工厂后,就没有其他事可做了。像 await "http://random.org".GetAsync();
这样的标准请求将被 自动 代理, 如果 工厂决定这样做。
不幸的是,这个解决方案有一个缺点。事实证明,在使用 Flurl 构建请求的过程中,自定义工厂被多次调用。根据我的经验,它被称为at least 3 times。这可能会导致问题,因为 工厂可能不会 return 相同的 FlurlClient
对于相同的输入 URL.
解决方案
解决方案是构建自定义 FlurlClientManager
class,以完全绕过 FlurlClient 工厂机制并保留提供的自定义客户端池按需。
虽然这个解决方案是专门为与令人敬畏的 Flurl 库一起工作而构建的,但可以直接使用 HttpClient
class 来完成非常相似的事情。
/// <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);
}
}
这个静态 class 保留了一个 FlurlClient
的全局池。与之前的解决方案一样,该池包括:
- 每个代理一个客户端;
- 每个主机一个客户端 用于所有不能通过代理的请求(这实际上是 Flurl 的默认工厂策略)。
在 class 的这个实现中,代理由 class 本身选择(使用任何你想要的策略,例如循环法或随机法),但它可以适应采取代理 URL 作为输入。在那种情况下,请记住,使用此实现,客户端在创建后永远不会被释放,因此您可能需要考虑一下。
此实现还使用了自 .NET Core 2.1 起可用的新 SocketsHttpHandler.PooledConnectionLifetime
选项,以解决当您的 HttpClient
实例具有较长生命周期时出现的 DNS 问题。在 .NET Framework 上,应该使用 ServicePoint.ConnectionLeaseTimeout
属性。
使用管理器 class 很简单。对于普通请求,使用:
await FlurlClientManager.GetClient(url).Request(url).GetAsync();
对于代理请求,使用:
await FlurlClientManager.GetProxiedClient().Request(url).GetAsync();