最近,我遇到了来自asp.net monsters的一篇博客文章,该文章以下列方式讨论了使用HttpClient的问题:

using(var client = new HttpClient())
{
}


根据博客文章,如果我们处理掉HttpClient在每个请求之后,可以保持TCP连接打开。这可能会导致System.Net.Sockets.SocketException

按照本文所述的正确方法是创建HttpClient的单个实例,因为它有助于减少套接字的浪费。




如果共享一个HttpClient实例,则可以通过重用它们来减少套接字的浪费:

namespace ConsoleApplication
{
    public class Program
    {
        private static HttpClient Client = new HttpClient();
        public static void Main(string[] args)
        {
            Console.WriteLine("Starting connections");
            for(int i = 0; i<10; i++)
            {
                var result = Client.GetAsync("http://aspnetmonsters.com").Result;
                Console.WriteLine(result.StatusCode);
            }
            Console.WriteLine("Connections done");
            Console.ReadLine();
        }
    }
}



我一直在使用HttpClient对象后将其丢弃,因为我认为这是使用它的最佳方法。但是,现在这篇博客文章让我感到我一直做错了。

我们是否应该为所有请求创建新的HttpClient单个实例?使用静态实例有什么陷阱吗?

评论

您是否因使用方式遇到任何问题?

也许检查这个答案,也这个。

@whatsisname不,我没有,但是看着博客,我感觉到我可能一直在使用这个错误。因此,想从其他开发人员那里了解他们是否在任何一种方法中都遇到了问题。

我没有亲自尝试过(因此未提供答案),但是根据Microsoft的.NET Core 2.1版本,您应该按照docs.microsoft.com/zh-cn/dotnet/standard/所述使用HttpClientFactory …

(正如我的回答中所述,只是想使其更加可见,所以我写了一条简短的注释。)一旦您执行Close()或启动新的Get(),静态实例将正确处理tcp连接关闭握手。 。如果仅在处理完客户端后就将其丢弃,那么将没有人来处理关闭握手,因此,您的端口都将处于TIME_WAIT状态。

#1 楼

似乎是一篇引人注目的博客文章。但是,在做出决定之前,我将首先运行博客编写者运行的相同测试,但是使用您自己的代码。我还将尝试查找有关HttpClient及其行为的更多信息。
这篇文章指出:

HttpClient实例是应用于该实例执行的所有请求的设置的集合。此外,每个HttpClient实例都使用自己的连接池,将其请求与其他HttpClient实例执行的请求隔离。

因此,共享HttpClient时可能发生的情况是连接被重用了,这如果您不需要持久连接,那就很好。您要确定是否适合您的情况的唯一方法是运行自己的性能测试。
如果进行深入研究,您会发现其他一些解决此问题的资源(包括Microsoft最佳做法文章),因此无论如何(有一些预防措施)实施,可能是一个好主意。
参考文献
您使用的Httpclient错误并且破坏了软件的安全性Singleton HttpClient?谨防这种严重的行为及其解决方法Microsoft模式和实践-性能优化:不正确的实例化代码审查上的可重用HttpClient的单个实例Singleton HttpClient不尊重DNS更改(CoreFX)使用HttpClient的一般建议

评论


那是一个很好的广泛清单。这是我周末读的书。

– Ankit Vijay
16 Sep 8'在5:24



“如果您进行挖掘,您会发现其他一些资源可以解决此问题...”,您是要说TCP连接打开问题?

– Ankit Vijay
16年8月8日在5:30

简短的答案:使用静态HttpClient。如果需要支持(您的Web服务器或其他服务器的)DNS更改,则需要担心超时设置。

–杰西
17-3-28在20:36



这证明了HttpClient多么混乱,使用它是@AnkitVijay所说的“周末阅读”。

–usr
17-10-22在14:11

@Jess除了DNS更改外-通过单个套接字抛出所有客户端的通信量是否还会破坏负载平衡?

–伊恩
17年12月11日在21:26

#2 楼

我参加聚会很晚,但这是我在这个棘手的话题上的学习之旅。

1。

我的意思是,如果重用HttpClient是故意的
,并且这样做很重要,
这样的拥护者最好记录在自己的API中文档,
而不是隐藏在许多“高级主题”,“性能(反)模式”中,或者其他博客文章中。
否则,新学习者应该怎么知道它

到目前为止(2018年5月),谷歌搜索“ c#httpclient”时的第一个搜索结果指向MSDN上的该API参考页,其中没有提到
好吧,这里的新手第1课是,
总是在MSDN帮助页面标题之后始终单击“其他版本”链接,
您可能会找到“当前版本”的链接。版本”。
在此HttpClient案例中,它将带您到包含该意图描述的最新文档。

我怀疑很多对此主题不熟悉的开发人员
找不到公司要么直接整理文档页面,
这就是为什么这种知识没有广泛传播的原因,
后来人们发现它们很惊讶,
也许是很难的。
>
2。 using IDisposable的(mis?)观念


这个话题虽然有点题外话,但仍然值得指出,看到前面提到的人不是巧合。博客文章指责HttpClientIDisposable接口如何使它们倾向于使用using (var client = new HttpClient()) {...}模式
,然后导致问题。

我认为这归根结底是(mis ?)构想:
“一个IDisposable对象应该是短命的”。

但是,当我们以这种方式编写代码时,它肯定看起来是短命的: br />
using (var foo = new SomeDisposableObject())
{
    ...
}


IDisposable的官方文档
从来没有提到IDisposable对象必须是短命的。
根据定义,IDisposable只是一种允许您释放不受管资源的机制。
仅此而已。从这种意义上说,您有望最终触发处置,但它并不需要您以短暂的方式这样做。因此,正确选择何时选择是您的工作。触发处置,
根据您的真实对象的生命周期要求。
没有什么可以阻止您长期使用IDisposable:

using System;
namespace HelloWorld
{
    class Hello
    {
        static void Main()
        {
            Console.WriteLine("Hello World!");

            using (var client = new HttpClient())
            {
                for (...) { ... }  // A really long loop

                // Or you may even somehow start a daemon here

            }

            // Keep the console window open in debug mode.
            Console.WriteLine("Press any key to exit.");
            Console.ReadKey();
        }
    }
}


有了这种新的理解,现在我们重新访问该博客文章,
我们可以清楚地注意到,该“修复程序”一次初始化了HttpClient,但从未对其进行处理,
这就是为什么我们可以从其中看到netstat输出,
连接保持在ESTABLISHED状态,这表明它没有被正确关闭。
如果关闭,则其状态将处于TIME_WAIT。
实际上,它不是在整个程序结束后只泄漏一个打开的连接是很大的事情,
修复后,博客发布者仍然可以看到性能提升;
但是,从概念上讲,归咎于IDisposabl是不正确的e并选择不丢弃它。

3。我们是否必须将HttpClient置于静态属性中,或者甚至将其作为单例对象?

基于上一节的理解,
我认为这里的答案很明确:“不是
这实际上取决于您如何组织代码,
只要您重用HttpClient并最终理想地对其进行处置。

甚至不是示例在当前正式文档的“
备注”部分中
完全正确。它定义了一个“ GoodController”类,
包含一个不会被丢弃的静态HttpClient属性;
不遵守“示例”部分中的另一个示例
强调:“需要调用dispose ...因此应用不会泄漏资源”。

最后,单例并不是没有自己的挑战。


“有多少人认为全局变量是个好主意?没人。

有多少人认为单身人士是个好主意吗?

什么给了?单身人士只是一堆全局变量。“


-引用了这个鼓舞人心的演讲, “全局状态和单例”

PS:SqlConnection

这与当前的问答无关,但可能是一个很好的了解。
SqlConnection用法模式不同。
您不需要重用SqlConnection,
因为它会更好地处理其连接池。

差异是由其实现方法引起的。 br />每个HttpClient实例都使用自己的连接池(从此处引用);
SqlConnection本身由中央连接池管理,
据此。

您仍然需要处置SqlConnection,与您应该为HttpClient进行的处置相同。

#3 楼

我做了一些测试,看到使用静态HttpClient可以提高性能。我使用以下代码进行测试:
namespace HttpClientTest
{
    using System;
    using System.Net.Http;

    class Program
    {
        private static readonly int _connections = 10;
        private static readonly HttpClient _httpClient = new HttpClient();

        private static void Main()
        {
            TestHttpClientWithStaticInstance();
            TestHttpClientWithUsing();
        }

        private static void TestHttpClientWithUsing()
        {
            try
            {
                for (var i = 0; i < _connections; i++)
                {
                    using (var httpClient = new HttpClient())
                    {
                        var result = httpClient.GetAsync(new Uri("http://bing.com")).Result;
                    }
                }
            }
            catch (Exception exception)
            {
                Console.WriteLine(exception);
            }
        }

        private static void TestHttpClientWithStaticInstance()
        {
            try
            {
                for (var i = 0; i < _connections; i++)
                {
                    var result = _httpClient.GetAsync(new Uri("http://bing.com")).Result;
                }
            }
            catch (Exception exception)
            {
                Console.WriteLine(exception);
            }
        }
    }
}

用于测试:

我运行了具有10、100、1000和1000个连接的代码。
分别运行3个一次找出一种平均值。
一次执行一种方法

我发现使用静态HttpClient而不是将其用于HttpClient请求可以将性能提高40%到60%。我已经将性能测试结果的详细信息放在此处的博客文章中。

#4 楼

这是一个有效使用HttpClient和HttpClientHandler的基本API客户端。创建新的HttpClient进行请求时,会产生很多开销。不要为每个请求重新创建HttpClient。尽可能重用HttpClient ...

using System;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Threading.Tasks;
//You need to install package Newtonsoft.Json > https://www.nuget.org/packages/Newtonsoft.Json/
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;


public class MyApiClient : IDisposable
{
    private readonly TimeSpan _timeout;
    private HttpClient _httpClient;
    private HttpClientHandler _httpClientHandler;
    private readonly string _baseUrl;
    private const string ClientUserAgent = "my-api-client-v1";
    private const string MediaTypeJson = "application/json";

    public MyApiClient(string baseUrl, TimeSpan? timeout = null)
    {
        _baseUrl = NormalizeBaseUrl(baseUrl);
        _timeout = timeout ?? TimeSpan.FromSeconds(90);    
    }

    public async Task<string> PostAsync(string url, object input)
    {
        EnsureHttpClientCreated();

        using (var requestContent = new StringContent(ConvertToJsonString(input), Encoding.UTF8, MediaTypeJson))
        {
            using (var response = await _httpClient.PostAsync(url, requestContent))
            {
                response.EnsureSuccessStatusCode();
                return await response.Content.ReadAsStringAsync();
            }
        }
    }

    public async Task<TResult> PostAsync<TResult>(string url, object input) where TResult : class, new()
    {
        var strResponse = await PostAsync(url, input);

        return JsonConvert.DeserializeObject<TResult>(strResponse, new JsonSerializerSettings
        {
            ContractResolver = new CamelCasePropertyNamesContractResolver()
        });
    }

    public async Task<TResult> GetAsync<TResult>(string url) where TResult : class, new()
    {
        var strResponse = await GetAsync(url);

        return JsonConvert.DeserializeObject<TResult>(strResponse, new JsonSerializerSettings
        {
            ContractResolver = new CamelCasePropertyNamesContractResolver()
        });
    }

    public async Task<string> GetAsync(string url)
    {
        EnsureHttpClientCreated();

        using (var response = await _httpClient.GetAsync(url))
        {
            response.EnsureSuccessStatusCode();
            return await response.Content.ReadAsStringAsync();
        }
    }

    public async Task<string> PutAsync(string url, object input)
    {
        return await PutAsync(url, new StringContent(JsonConvert.SerializeObject(input), Encoding.UTF8, MediaTypeJson));
    }

    public async Task<string> PutAsync(string url, HttpContent content)
    {
        EnsureHttpClientCreated();

        using (var response = await _httpClient.PutAsync(url, content))
        {
            response.EnsureSuccessStatusCode();
            return await response.Content.ReadAsStringAsync();
        }
    }

    public async Task<string> DeleteAsync(string url)
    {
        EnsureHttpClientCreated();

        using (var response = await _httpClient.DeleteAsync(url))
        {
            response.EnsureSuccessStatusCode();
            return await response.Content.ReadAsStringAsync();
        }
    }

    public void Dispose()
    {
        _httpClientHandler?.Dispose();
        _httpClient?.Dispose();
    }

    private void CreateHttpClient()
    {
        _httpClientHandler = new HttpClientHandler
        {
            AutomaticDecompression = DecompressionMethods.Deflate | DecompressionMethods.GZip
        };

        _httpClient = new HttpClient(_httpClientHandler, false)
        {
            Timeout = _timeout
        };

        _httpClient.DefaultRequestHeaders.UserAgent.ParseAdd(ClientUserAgent);

        if (!string.IsNullOrWhiteSpace(_baseUrl))
        {
            _httpClient.BaseAddress = new Uri(_baseUrl);
        }

        _httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue(MediaTypeJson));
    }

    private void EnsureHttpClientCreated()
    {
        if (_httpClient == null)
        {
            CreateHttpClient();
        }
    }

    private static string ConvertToJsonString(object obj)
    {
        if (obj == null)
        {
            return string.Empty;
        }

        return JsonConvert.SerializeObject(obj, new JsonSerializerSettings
        {
            ContractResolver = new CamelCasePropertyNamesContractResolver()
        });
    }

    private static string NormalizeBaseUrl(string url)
    {
        return url.EndsWith("/") ? url : url + "/";
    }
}



用法:

using (var client = new MyApiClient("http://localhost:8080"))
{
    var response = client.GetAsync("api/users/findByUsername?username=alper").Result;
    var userResponse = client.GetAsync<MyUser>("api/users/findByUsername?username=alper").Result;
}


#5 楼

要正确关闭TCP连接,我们需要完成FIN-FIN + ACK-ACK数据包序列(就像打开TCP连接时就像SYN-SYN + ACK-ACK一样)。
如果我们仅调用.Close ()方法(通常是在处理HttpClient时发生),并且我们不等待远程端确认关闭请求(使用FIN + ACK),我们最终在本地TCP端口上使用TIME_WAIT状态,因为一旦远程对等方向我们发送FIN + ACK数据包,我们就释放了侦听器(HttpClient),并且再也没有机会将端口状态重置为正确的关闭状态。

关闭TCP的正确方法连接将是调用.Close()方法并等待另一端(FIN + ACK)的close事件到达我们这一端。只有这样,我们才能发送最终的ACK并处理HttpClient。

只需添加一下,就可以使TCP连接保持打开状态(如果您正在执行HTTP请求),这是因为“ Connection:Keep-Alive “ HTTP标头。此外,您可能会要求远程对等方通过设置HTTP标头“ Connection:Close”来为您关闭连接。这样,您的本地端口将始终正确关闭,而不是处于TIME_WAIT状态。

#6 楼

没有使用HttpClient类的方法。关键是要以对应用程序的环境和约束有意义的方式来构建应用程序。

HTTP是需要公开公共API时使用的出色协议。它也可以有效地用于轻量级,低延迟的内部服务-尽管RPC消息队列模式通常是内部服务的更好选择。

做好HTTP会有很多复杂性。

请考虑以下内容:


创建套接字并建立TCP连接会占用网络带宽和时间。
HTTP / 1.1支持在同一套接字上进行管道请求。一个接一个地发送多个请求,而无需等待先前的响应-这可能是Blog帖子所报告的速度提高的原因。缓存和负载均衡器-如果您在负载均衡器前面有一个负载均衡器服务器,然后确保您的请求具有适当的缓存头可以减少服务器的负载,并更快地获得对客户端的响应。
永远不要轮询资源,请使用HTTP分块返回定期响应。

但最重要的是,进行测试,测量和确认。如果它的行为不符合设计要求,那么我们可以回答有关如何实现您的预​​期结果的特定问题。

评论


这实际上并没有回答任何问题。

–whatsisname
2016年9月7日在21:43

您似乎假设有一种正确的方法。我认为没有。我知道您必须以适当的方式使用它,然后测试和衡量它的行为,然后调整方法直到满意为止。

–迈克尔·肖(Michael Shaw)
2016年9月7日在21:46

您写了一些有关使用是否使用HTTP进行通信的信息。 OP询问了如何最好地使用特定的库组件。

–whatsisname
2016年9月7日在22:33

@MichaelShaw:HttpClient实现IDisposable。因此,可以期望它是一个短命的对象,该对象知道如何进行自我清理,适合于每次需要时都包装在using语句中。不幸的是,这实际上并不是这样的。 OP链接的博客文章清楚地表明,在using语句超出范围并且HttpClient对象可能已处置之后,仍然存在很长的资源(特别是TCP套接字连接)。

–罗伯特·哈维(Robert Harvey)
2016年9月7日23:05

我了解这种思考过程。只是如果您从体系结构的角度考虑HTTP,并打算向同一服务发出很多请求-那么您将考虑缓存和流水线操作,然后考虑使HttpClient成为短暂的对象只是觉得不对劲。同样,如果您向不同的服务器发出请求,而保持套接字处于活动状态将不会带来任何好处,则在使用HttpClient对象后将其丢弃是有意义的。

–迈克尔·肖(Michael Shaw)
2016年9月8日下午6:00