我们是否应该为所有请求创建一个新的HttpClient单一实例?


57

最近,我从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

也许检查这个答案 ,也这个
约翰·吴

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

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

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

Answers:


39

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

这篇文章指出:

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

因此,共享HttpClient时可能发生的事情是连接被重用了,如果您不需要持久连接,那就很好。您要确定这是否对您的情况重要的唯一方法是运行自己的性能测试。

如果您进行深入研究,会发现其他一些资源可以解决此问题(包括Microsoft最佳实践文章),因此无论如何都要实施(采取一些预防措施)可能是一个好主意。

参考文献

您使用的Httpclient错误并且破坏了软件的
稳定性Singleton HttpClient?当心这种严重的行为及其解决方法
Microsoft模式和实践-性能优化:不正确的实例化
代码审查上的可重复使用HttpClient的单个实例
Singleton HttpClient不尊重DNS更改(CoreFX)
使用HttpClient的一般建议


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

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

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

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

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

16

我参加聚会很晚,但这是我关于这个棘手主题的学习旅程。

1.在哪里可以找到重用HttpClient的官方倡导者?

我的意思是,如果打算重用HttpClient这样做很重要那么可以更好地在自己的API文档中记录这样的拥护者,而不是隐藏在许多“高级主题”,“性能(反)模式”或其他博客文章中。否则,新学习者应该如何在为时已晚之前知道它?

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

我怀疑许多不熟悉该主题的开发人员也没有找到正确的文档页面,这就是为什么这种知识没有广泛传播的原因,人们后来发现它们时可能会感到很困难,这使他们感到惊讶 。

2.的(误解)概念 using IDisposable

这一个是稍微偏离主题,但仍值得指出的是,这不是巧合看到人们在前述的那些博客文章指责怎样HttpClientIDisposable界面,使他们倾向于使用using (var client = new HttpClient()) {...}模式,然后导致这个问题。

我认为可以归结为一个不言而喻的概念: “一个IDisposable对象应该是短暂的”

但是,当我们以这种方式编写代码时,这看起来似乎是一时的事情:

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。实际上,在整个程序结束后只泄漏一个打开的连接并没什么大不了的,并且在修复后,博客发布者仍然可以看到性能的提高;但是,将IDisposable归咎于并选择不处置它在概念上是不正确的。

3.我们是否必须将HttpClient放入静态属性,或者甚至将其作为单例?

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

可笑的是,即使是当前官方文档的“ 备注”部分中的示例 也不完全正确。它定义了一个“ GoodController”类,其中包含一个不会被丢弃的静态HttpClient属性。这违反了“示例”部分中另一个示例所 强调的内容:“需要调用dispose ...这样应用程序才不会泄漏资源”。

最后,单身汉并非没有自己的挑战。

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

有多少人认为单例是个好主意?一些。

是什么赋予了?单例只是一堆全局变量。”

-从这个鼓舞人心的演讲中引用“全球状态和单身人士”

PS:SqlConnection

这与当前的问答无关,但可能是一个好知识。SqlConnection的使用模式不同。您不需要重用SqlConnection,因为它将以这种方式更好地处理其连接池。

差异是由它们的实现方法引起的。每个HttpClient实例都使用其自己的连接池(从此处引用 )。但SqlConnection的本身是由一个中央连接池按照管理,这个

而且,您仍然需要像对待HttpClient一样处理SqlConnection。


14

我做了一些测试,发现static可以提高性能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%。我将性能测试结果的详细信息放在此处的博客文章中。


1

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

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

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


1

这是一个有效使用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

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

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

做好HTTP会有很多复杂性。

考虑以下:

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

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


4
这实际上并没有回答任何问题。
whatsisname 2016年

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

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

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

1
我了解这种思考过程。只是如果您从体系结构的角度考虑HTTP,并打算向同一服务发出很多请求-那么您将考虑缓存和流水线操作,然后考虑使HttpClient成为短暂的对象只是觉得不对劲。同样,如果您向不同的服务器发出请求,而保持套接字处于活动状态也不会带来任何好处,则在使用HttpClient对象后将其丢弃是有意义的。
迈克尔·肖
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.