单例模式下的httpclient与创建新的httpclient请求之间的区别

57

我正试图在我的Xamarin.Forms移动应用程序中使用HttpClient创建Web服务的层。

  1. 不使用单例模式
  2. 使用单例模式

第一种方法中,我在移动应用程序发出的每个新请求中创建新的http客户端对象。

这是我的代码:

  public HttpClient GetConnection()
        {

            HttpClient httpClient = new HttpClient();
            httpClient.BaseAddress = new Uri(baseAddress); 
            httpClient.Timeout = System.TimeSpan.FromMilliseconds(timeout);


            return httpClient;

        }

发送Post请求的代码

 public async Task<TResult> PostAsync<TRequest, TResult>(String url, TRequest requestData)
        {
            HttpClient client = GetConnection();
            String responseData = null;
            if (client != null)
            {

                String serializedObject = await Task.Run(() => JsonConvert.SerializeObject(requestData, _jsonSerializerSettings));
                var jsonContent = new StringContent(serializedObject, System.Text.Encoding.UTF8, "application/json");
                HttpResponseMessage response = await client.PostAsync(new Uri(url, UriKind.Relative), jsonContent);
                responseData = await HandleResponse(response);


                return await Task.Run(() => JsonConvert.DeserializeObject<TResult>(responseData, _jsonSerializerSettings));


            }
            else
            {

                throw new NullReferenceException("NullReferenceException @ PostAsync  httpclient is null WebRequest.cs");

            }

        }

客户端将使用以下代码执行请求

new LoginService(new WebRequest()).UserLogin(userRequest);

实现 IWebRequest 接口的类内部

_webRequest.PostAsync<UserRequest,bool>(Constants.USER_LOGIN, userRequest);

在第二种方法中,我在每个新请求中都重复使用同一HTTP客户端对象。

在这里,我的单例类也是线程安全的。

private static readonly Lazy<HttpService> lazy =
        new Lazy<HttpService>(() => new HttpService());

        public static HttpService Instance { get { return lazy.Value; } }



        private HttpClient getConnection()
        {

            client = new HttpClient();
            client.Timeout = System.TimeSpan.FromMilliseconds(timeout);

            //client.MaxResponseContentBufferSize = 500000;
            client.BaseAddress = new Uri(baseAddress);
            return client;
        }

提交请求的代码

public Task<HttpResponseMessage> sendData(String url,String jsonData)
        {

            var jsonContent = new StringContent(jsonData, System.Text.Encoding.UTF8, "application/json");

            return getConnection().PostAsync(new Uri(url, UriKind.Relative), jsonContent);
        }

客户端将使用以下代码执行

HttpService.Instance.sendData(...)

我已经浏览了许多像 RestSharp 这样的库,只是为了探寻最好的库,但我发现它们大多数都会在每个请求中创建新的对象。因此,我很困惑哪种模式最适合。


11
不要不断创建 HttpClient,原因在于:这篇文章 - DavidG
HttpClient实现了IDisposable接口,必须满足该接口(除非您正在使用它的静态实例)。 - taiji123
这个回答解决了你的问题吗?在WebAPI客户端中每次调用创建一个新的HttpClient的开销是多少? - Michael Freidgeim
5个回答

91
更新:看起来使用单个静态实例的HttpClient不会遵守DNS更改,因此解决方案是使用HttpClientFactory。请参见此处有关Microsoft文档的详细信息。
要使用HttpClientFactory,您必须使用Microsoft的依赖注入。这是ASP.NET Core项目的默认设置,但对于其他项目,您将不得不引用Microsoft.Extensions.Http和Microsoft.Extensions.DependencyInjection。
然后,在创建服务容器时,只需调用AddHttpClient():
var services = new ServiceCollection();
services.AddHttpClient()
var serviceProvider = services.BuildServiceProvider();

然后,您可以将IHttpClientFactory注入到您的服务中,幕后HttpClientFactory将维护一组HttpClientHandler对象 - 保持您的DNS新鲜并防止连接池耗尽问题。


新答案:

使用单例模式是正确使用 HttpClient 的方式。请参考这篇文章获取完整细节。

微软 文档 表示:

HttpClient 应该在应用程序的整个生命周期内只被实例化一次并重复使用。每次请求都实例化一个 HttpClient 类将在重负载下耗尽可用套接字数量。这将导致 SocketException 错误。以下是正确使用 HttpClient 的示例。

事实上,我们在应用程序中发现了这个问题。我们的代码可能会在 foreach 循环中进行数百个 API 请求,并且对于每次迭代,我们都会创建一个包装在 using 中的 HttpClient。我们很快就开始从我们的 MongoClient 中得到红鲱鱼错误,说它在尝试连接到数据库时超时了。阅读链接的文章后,我们发现即使在释放了 HttpClient 后,仍然会耗尽可用的套接字。

唯一需要注意的是像 DefaultRequestHeadersBaseAddress 这样的东西将应用于任何使用 HttpClient 的地方。作为单例,这可能遍布整个应用程序。您仍然可以在应用程序中创建多个 HttpClient 实例,但请注意每次这样做都会创建一个新的连接池,因此应该谨慎创建。

正如hvaughan3指出的那样,您也不能更改HttpClient使用的HttpMessageHandler实例,因此如果这对您很重要,您需要使用具有该处理程序的单独实例。

3
你也不能更换处理程序,这意味着如果你需要处理不同的身份验证提供者,你需要为每个提供者创建一个HttpClient实例。 - hvaughan3
@hvaughan3 好观点。我已经将它添加到答案中,谢谢 :) - ProgrammingLlama
@Hunt 我处理请求的方式是使用 SendAsync 然后自己构建 HttpRequestMessage。请注意,您不需要将您的 WebRequest 类作为单例,只需将 HttpClient 设为单例即可。 - ProgrammingLlama
在我的问题中,我已经写了代码,请问您能否检查一下这是否是您所说的方式? - Hunt
通过将GetBaseAddress函数传递到uri中,很可能会隔离WebRequest.HttpInstance.BaseAddress = new Uri(GetBaseAddress());的基地址。 - Hunt
显示剩余9条评论

13
HttpClient应该被重用,但这并不意味着我们必须使用单例来组织我们的代码。请参阅我的答案。以下是引用:


我来晚了,但这是我在这个棘手的主题上的学习之旅。

1. 我们在哪里可以找到关于重用 HttpClient 的官方推荐?

我的意思是,如果重用 HttpClient 是有意图的, 而且这样做很重要, 那么这种推荐最好在其自己的 API 文档中得到记录, 而不是被隐藏在大量的“高级主题”、“性能(反)模式” 或其他博客文章中。 否则,一个新学习者怎么可能在为时已晚之前知道这一点呢?

截至目前(2018 年 5 月),当你在谷歌搜索“c# httpclient”时,第一个搜索结果指向 MSDN 上的此 API 参考页面,该页面根本没有提到这个意图。对于新手来说,第一课就是,始终在 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输出中可以看出,它从未关闭,连接仍然保持在已建立状态,这意味着它没有被正确关闭。如果关闭了它的状态将会是TIME_WAIT。实际上,只有一个连接泄漏并不是什么大问题,尤其是当整个程序结束时仍然存在,而且博客作者在修复后仍然看到性能提升,但是从概念上讲,归咎于IDisposable并选择不处置它是不正确的。

3. 我们必须将HttpClient放入静态属性中甚至将其作为单例吗?

根据前面部分的理解,我认为答案变得很清晰:“不一定”。这取决于您如何组织代码,只要重用一个HttpClient并(最好)最终处理掉它。

令人惊讶的是,即使是在当前官方文档的备注部分中的示例也没有完全做到这一点。它定义了一个“GoodController”类,其中包含一个静态的HttpClient属性,该属性将不会被处理掉。这违反了示例部分中的另一个示例强调的:“需要调用dispose…以便应用程序不泄漏资源”。

最后,单例模式也有其自身的挑战。

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

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

“怎么回事? 单例只不过是一堆全局变量。”

-- 引自这个激励人心的演讲,“全局状态和单例模式”

PS:SqlConnection

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

这种差异是由它们的实现方法造成的。每个HttpClient实例都使用自己的连接池(引用自此处);但是SqlConnection本身由中央连接池管理,根据此处的说明。

您仍然需要处理SqlConnection,与您应该对HttpClient做的一样。


1
我建议复制粘贴它,因为它在不同的堆栈交换上。如果他们更改URL或(听起来很疯狂)决定放弃该堆栈交换,答案将在这里。 - Erik Philips
1
作为一名开发者,我觉得我喜欢DRY原则,讨厌WET原则。但既然你要求,那就给你吧。 - RayLuo

8

.NET Core 2.1+

什么时候可以使用 DI:

    using System.Net.Http;

    public class SomeClass
    {
        private readonly IHttpClientFactory _httpClientFactory;

        public SomeClass(IHttpClientFactory httpClientFactory)
        {
            _httpClientFactory = httpClientFactory;
        }

        public void Foo()
        {
            var httpClient = _httpClientFactory.CreateClient();
            ...
        }
    }

当你无法使用依赖注入时:
    using System.Net.Http;

    public class SomeClass
    {
        private static readonly HttpClient Client;

        static SomeClass()
        {
            var handler = new SocketsHttpHandler
            {
                // Sets how long a connection can be in the pool to be considered reusable (by default - infinite)
                PooledConnectionLifetime = TimeSpan.FromMinutes(1),
            };

            Client = new HttpClient(handler, disposeHandler: false);
        }
        
        ...
    }

参考 https://learn.microsoft.com/zh-cn/aspnet/core/fundamentals/http-requests#alternatives-to-ihttpclientfactory


1
正如其他人所提到的,大多数情况下应将HttpClient用作单例,但有一个例外 - 当您使用HTTP长轮询技术时,不应将HttpClient用作单例,因为它会阻塞其他请求的执行。
对于长轮询请求,您应该创建单独的HttpClient

1
如果您在WebApi应用程序中将HttpClient用作静态属性,则可能会出现以下错误。
System.InvalidOperationException: Concurrent reads or writes are not supported.\r\n   at System.IO.Pipelines.PipeCompletion.ThrowLatchedException()\r\n   at System.IO.Pipelines.Pipe.GetReadResult(ReadResult& result)\r\n   at System.IO.Pipelines.Pipe.GetReadAsyncResult()\r\n   at Microsoft.AspNetCore.Server.IIS.Core.IISHttpContext.WriteBody(Boolean flush)","ClassName":"IISHttpContext","MethodName":"WriteBody","EventId":{"Id":3,"Name":"UnexpectedError"},"SourceContext":"Microsoft.AspNetCore.Server.IIS.Core.IISHttpServer"

当您在WebAPI控制器的操作中使用HttpClient静态实例对同一URL进行2个并发请求时,将出现错误。
因此,我认为在操作中使用_httpClientFactory.CreateClient(Guid.NewGuid().ToString())是最安全的方法。根据该方法的文档 - “通常不需要处理System.Net.Http.HttpClient,因为System.Net.Http.IHttpClientFactory跟踪和处理System.Net.Http.HttpClient使用的资源。”

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接