5

我需要创建一个多线程应用程序来发出请求(发布、获取等)为此我选择了Httpclient.

默认情况下,它不支持 Socks 代理。所以我发现Sockshandlerhttps://github.com/extremecodetv/SocksSharp)可以用来代替基本的HttpClientHandler。它允许我使用袜子。

但我有一个问题。我所有的请求都应该通过我从互联网上解析的不同代理发送。但是 httpclient 处理程序不支持动态更改代理。如果我没有有效的代理,我需要重新创建一个 httclient,这没关系,但如果我有 200 个线程,则需要大量的 cpu。那么在这种情况下我该怎么办呢?

第二个问题。我发现这篇文章 ( https://aspnetmonsters.com/2016/08/2016-08-27-httpclientwrong/ ) 可以HttpClient作为单个实例使用以获得更好的性能,但在多线程程序中是不可能的。在这种情况下哪种方式更好?

谢谢帮助

4

4 回答 4

3

httpclient 处理程序不支持动态更改代理。

我不确定这在技术上是否属实。代理是一个读/写属性,所以我相信你可以改变它(除非这会导致运行时错误......老实说我还没有真正尝试过)。

更新:我现在已经尝试过了,你的断言技术上是正确的。在下面的示例中,更新UseProxy将失败并显示“System.InvalidOperationException:'此实例已启动一个或多个请求。只能在发送第一个请求之前修改属性。'”在 .NET Core 和完整框架上确认。

var hch = new HttpClientHandler { UseProxy = false };
var hc = new HttpClient(hch);
var resp = await hc.GetAsync(someUri);

hch.UseProxy = true; // fail!
hch.Proxy = new WebProxy(someProxy);
resp = await hc.GetAsync(someUri);

但事实是,您不能以线程安全的方式为每个请求设置不同的属性,这很不幸。

如果我有 200 个线程,则需要很多 cpu

并发异步 HTTP 调用不应消耗额外的线程或 CPU。使用await Task.WhenAll或类似方法将它们关闭,并且在返回响应之前不会消耗线程。

第二个问题。我发现这篇文章...

这绝对是您需要注意的事情。但是,即使您可以HttpClient为每个请求设置不同的代理,底层网络堆栈仍然需要为每个代理打开一个套接字,因此就套接字耗尽问题而言,您不会比每个代理的实例获得任何收益。

最佳解决方案取决于您在这里谈论的代理数量。在文章中,作者描述了当服务器打开大约 4000-5000 个打开的套接字时遇到问题,并且在 400 个或更少左右没有问题。YMMV,但如果代理的数量不超过几百个,您应该可以安全地为HttpClient每个代理创建一个新实例。如果更多,我会考虑限制您的并发性并对其进行测试,直到找到一个您的服务器资源可以跟上的数字。在任何情况下,请确保如果您需要对同一个代理进行多次调用,您正在HttpClient为它们重用实例。AConcurrentDictionary对于懒惰地创建和重用这些实例可能很有用。

于 2018-10-08T20:53:28.990 回答
2

我同意Todd Menier的回答。但是,如果您使用 .Net 核心,我建议您阅读这篇文章以及微软所说的这篇文章:

为每个请求实例化一个 HttpClient 类将耗尽重负载下可用的套接字数量。该问题将导致 SocketException 错误。

这很可悲,但他们提供了一个解决方案:

为了解决上述问题并简化 HttpClient 实例的管理,.NET Core 2.1 引入了一个新的 HttpClientFactory,它还可以通过将 Polly 与其集成来实现弹性 HTTP 调用。

我查看了IHttpClientFactory摘要块并看到:

每次调用 System.Net.Http.IHttpClientFactory.CreateClient(System.String)都保证返回一个新的 System.Net.Http.HttpClient 实例。调用者可以无限期地缓存返回的 System.Net.Http.HttpClient 实例,或者将其使用包围在 using 块中,以便在需要时对其进行处理。默认 System.Net.Http.IHttpClientFactory实现可能会缓存底层 System.Net.Http.HttpMessageHandler 实例以提高性能。 调用者还可以根据需要自由地改变返回的 System.Net.Http.HttpClient 实例的公共属性。

我们来看图片 在此处输入图像描述

IHttpClientFactory实现注入某些服务(CatalogueService 或您制作的任何东西),然后在每次需要发出请求时HttpClient通过实例化(您甚至可以将其包装成块),但将缓存在某种连接池中。IHttpClientFactoryusing(...)HttpMessageHandler

因此,您可以根据HttpClientFactory需要创建许多HttpClient实例并在拨打电话之前设置代理。如果有帮助,我会很高兴。

更新: 我试过了,它实际上不是你需要的。您可以IHttpClientFactory像这样实现自己的:

public class Program
{
    public interface IHttpClientFactory
    {
        HttpClient CreateClientWithProxy(IWebProxy webProxy);
    }

    internal class HttpClientFactory : IHttpClientFactory
    {
        private readonly Func<HttpClientHandler> makeHandler;

        public HttpClientFactory(Func<HttpClientHandler> makeHandler)
        {
            this.makeHandler = makeHandler;
        }

        public HttpClient CreateClientWithProxy(IWebProxy webProxy)
        {
            var handler = this.makeHandler();
            handler.Proxy = webProxy;
            return new HttpClient(handler, true);
        }
    }

    internal class CachedHttpClientFactory : IHttpClientFactory
    {
        private readonly IHttpClientFactory httpClientFactory;
        private readonly Dictionary<int, HttpClient> cache = new Dictionary<int, HttpClient>();

        public CachedHttpClientFactory(IHttpClientFactory httpClientFactory)
        {
            this.httpClientFactory = httpClientFactory;
        }

        public HttpClient CreateClientWithProxy(IWebProxy webProxy)
        {
            var key = webProxy.GetHashCode();
            lock (this.cache)
            {
                if (this.cache.ContainsKey(key))
                {
                    return this.cache[key];
                }

                var result = this.httpClientFactory.CreateClientWithProxy(webProxy);
                this.cache.Add(key, result);
                return result;
            }
        }
    }

    public static void Main(string[] args)
    {
        var httpClientFactory = new HttpClientFactory(() => new HttpClientHandler
        {
            UseCookies = true,
            UseDefaultCredentials = true,
        });

        var cachedhttpClientFactory = new CachedHttpClientFactory(httpClientFactory);
        var proxies = new[] {
            new WebProxy()
            {
                Address = new Uri("https://contoso.com"),
            },
            new WebProxy()
            {
                Address = new Uri("https://microsoft.com"),
            },
        };

        foreach (var item in proxies)
        {
            var client = cachedhttpClientFactory.CreateClientWithProxy(item);
            client.GetAsync("http://someAddress.com");
        }
    }
}

但要小心可能占用池中所有连接的大量 WebProxy 集合。

于 2019-10-03T10:04:59.893 回答
2

HttpClientHandler 的 Proxy 属性接受一个实现 IWebProxy 的对象。IWebProxy 接口有一个 GetProxy 方法,它返回代理的 Uri。因此,您可以创建自己的类来实现该接口,并使用 GetProxy 控制它如何返回代理的 Uri。您可以让它包装另一个 IWebProxy,并在 GetProxy 中返回内部 IWebProxy 的 GetProxy。这样,您不必更改 HttpClientHandler 的 Proxy 属性,只需更改内部 IWebProxy。我的这个解决方案的实现可以在这里找到:https ://github.com/M-Boukhlouf/WebProxyService

于 2020-06-30T11:22:14.887 回答
0

通过一些测试,我确认您可以Address通过WebProxy. 诀窍是您必须在切换到另一个代理之前启动一个 http 请求。这是示例代码:

    private static async Task CommonHttpClient(List<string> proxyList)
    {
        var webproxy = new WebProxy("http://8.8.8.8:8080", false);
        var handler = new HttpClientHandler()
        {
            Proxy = webproxy,
            UseProxy = true,
        };
        var client = new HttpClient(handler) {Timeout = NetworkUtils.AcceptableTimeoutTimeSpan};
        var data = new Dictionary<Task<HttpResponseMessage>, string>();
        foreach (var proxy in proxyList)
        {
            webproxy.Address = new Uri($"http://{proxy}");
            var uri = new Uri(
                "https://api.ipify.org");
            data.Add(client.GetAsync(uri, HttpCompletionOption.ResponseHeadersRead), proxy);
        }

        while (data.Count > 0)
        {
            var taskFinished = await Task.WhenAny(data.Keys).ConfigureAwait(false);
            var address = data[taskFinished];
            using var resp = await taskFinished.ConfigureAwait(false);
            resp.EnsureSuccessStatusCode();
            var ip = await resp.Content.ReadAsStringAsync().ConfigureAwait(false);
            Assert.Equals(address, ip);
            data.Remove(taskFinished);
        }

        handler.Dispose();
        client.Dispose();
    }
    private static async Task SeperateHttpClient(List<string> proxyList)
    {
        await Task.WhenAll(proxyList.Select(async proxy =>
        {
            var webproxy = new WebProxy($"http://{proxy}", false);
            using var handler = new HttpClientHandler()
            {
                Proxy = webproxy,
                UseProxy = true,
            };
            using var client = new HttpClient(handler) {Timeout = NetworkUtils.AcceptableTimeoutTimeSpan};
            var uri = new Uri("https://api.ipify.org");
            var resp = await client.GetAsync(uri).ConfigureAwait(false);
            resp.EnsureSuccessStatusCode();
            var ip = await resp.Content.ReadAsStringAsync().ConfigureAwait(false);
            Assert.Equals(proxy, ip);

        })).ConfigureAwait(false);
    }

    private static async Task TestAsync1()
    {
        // Your list of proxy
        var proxyList = new List<string>() {"1.2.3.4", "5.6.7.8"};
        
        var start = DateTimeOffset.UtcNow;
        await SeperateHttpClient(proxyList).ConfigureAwait(false);
        Console.WriteLine(start.TotalSecondsSince());

        start = DateTimeOffset.UtcNow;
        await CommonHttpClient(proxyList).ConfigureAwait(false);
        Console.WriteLine(start.TotalSecondsSince());
        
    }

在我的测试过程中,我没有看到共享一个HttpClient实例可以提高性能。即使它具有更优化的代码(即使用ResponseHeaderReadhttps://www.stevejgordon.co.uk/using-httpcompletionoption-responseheadersread-to-improve-httpclient-performance-dotnet)) ,它甚至需要更长的时间才能完成

于 2020-06-23T01:25:41.047 回答