【问题标题】:Do HttpClient and HttpClientHandler have to be disposed between requests?HttpClient 和 HttpClientHandler 是否必须在请求之间进行处理?
【发布时间】:2013-03-20 06:19:37
【问题描述】:

System.Net.Http.HttpClientSystem.Net.Http.HttpClientHandler 在 .NET Framework 4.5 中实现 IDisposable(通过 System.Net.Http.HttpMessageInvoker)。

using 声明文档说:

通常,当您使用 IDisposable 对象时,您应该声明并 在 using 语句中实例化它。

This answer 使用这种模式:

var baseAddress = new Uri("http://example.com");
var cookieContainer = new CookieContainer();
using (var handler = new HttpClientHandler() { CookieContainer = cookieContainer })
using (var client = new HttpClient(handler) { BaseAddress = baseAddress })
{
    var content = new FormUrlEncodedContent(new[]
    {
        new KeyValuePair<string, string>("foo", "bar"),
        new KeyValuePair<string, string>("baz", "bazinga"),
    });
    cookieContainer.Add(baseAddress, new Cookie("CookieName", "cookie_value"));
    var result = client.PostAsync("/test", content).Result;
    result.EnsureSuccessStatusCode();
}

但来自 Microsoft 的最明显示例并没有显式或隐式调用 Dispose()。例如:

announcement的cmets中,有人问微软员工:

检查您的样品后,我发现您没有执行处置 对 HttpClient 实例的操作。我已经使用了 HttpClient 的所有实例 在我的应用程序上使用 using 声明,我认为这是正确的方法 因为 HttpClient 实现了 IDisposable 接口。我在吗 正确的道路?

他的回答是:

一般来说这是正确的,尽管你必须小心 “使用”和异步,因为它们并没有真正混合在 .Net 4 中,在 .Net 4.5 中你 可以在“using”语句中使用“await”。

顺便说一句,您可以根据自己的喜好多次重复使用相同的 HttpClient 通常您不会一直创建/处置它们。

第二段对于这个问题是多余的,它不是关心你可以使用多少次HttpClient实例,而是关心在你不再需要它之后是否需要释放它。

(更新:事实上,第二段是答案的关键,@DPeden 在下面提供。)

所以我的问题是:

  1. 考虑到当前的实现 (.NET Framework 4.5),是否有必要在 HttpClient 和 HttpClientHandler 实例上调用 Dispose()?澄清:“必要”是指不处置是否有任何负面后果,例如资源泄漏或数据损坏风险。

  2. 如果没有必要,由于他们实现了 IDisposable,这是否是一种“好习惯”?

  3. 如果有必要(或推荐),上面提到的this code 是否安全地实现它(对于 .NET Framework 4.5)?

  4. 如果这些类不需要调用 Dispose(),为什么它们被实现为 IDisposable?

  5. 如果他们需要,或者如果这是推荐做法,Microsoft 示例是否具有误导性或不安全?

【问题讨论】:

  • @Damien_The_Unbeliever,感谢您的反馈。你对我如何澄清这个问题有什么建议吗?我想知道它是否会导致通常与不处置资源相关的问题,例如资源泄漏和数据损坏。
  • @Damien_The_Unbeliever:不正确。特别是,流编写器必须具备正确的行为。
  • @StephenCleary - 您在考虑哪些方面?当然,您可以在每次写入后调用Flush,除了它继续持有底层资源超过必要的时间带来不便之外,“正确行为”所需的什么不会发生?
  • 这是完全错误的:“通常,当您使用 IDisposable 对象时,您应该在 using 语句中声明和实例化它”。在决定是否应该使用 using 之前,我总是会阅读有关实现 IDisposable 的类的文档。作为我实现 IDisposable 的库的作者,因为需要释放未管理的资源,如果消费者每次都创建处置实例而不是重新使用现有实例,我会感到震惊。这并不是说最终不要处理实例..
  • 我已经向微软提交了一个 PR 来更新他们的文档:github.com/dotnet/docs/pull/2470

标签: c# .net-4.5 idisposable using dotnet-httpclient


【解决方案1】:

普遍的共识是您不需要(不应该)处理 HttpClient。

许多密切参与其工作方式的人都表示过这一点。

请参阅Darrel Miller's blog post 和相关的 SO 帖子:HttpClient crawling results in memory leak 以供参考。

我还强烈建议您阅读 the HttpClient chapter from Designing Evolvable Web APIs with ASP.NET 以了解幕后情况,尤其是此处引用的“生命周期”部分:

虽然 HttpClient 确实间接实现了 IDisposable 接口,HttpClient的标准用法是不要dispose 在每个请求之后。 HttpClient 对象旨在为 只要您的应用程序需要发出 HTTP 请求。有一个对象 存在于多个请求中启用设置的地方 DefaultRequestHeaders 并防止您重新指定 每个请求上的 CredentialCache 和 CookieContainer 之类的东西 HttpWebRequest 是必需的。

或者甚至打开 DotPeek。

【讨论】:

  • 为了澄清您的答案,是否可以说“如果您持有该实例以稍后重用它,您不需要处置 HttpClient”?例如,如果一个方法被重复调用并创建一个新的 HttpClient 实例(即使在大多数情况下它不是推荐的模式),那么说这个方法不应该释放该实例(不会被重用)是否仍然正确?它可能导致数千个未处理的实例。换句话说,您应该尝试重用实例,但如果您不重用,您最好将它们处置(释放连接)?
  • 我认为可以理解的令人沮丧但正确的答案取决于它。如果我不得不给出在大多数情况下(我从来没有说全部)有效的一般建议,我建议您使用 IoC 容器并将 HttpClient 的实例注册为单例。然后实例的生命周期将限定为容器的生命周期。这可以在应用程序级别或 Web 应用程序中的每个请求范围内进行。
  • @FernandoCorreia 是的。如果由于某种原因您确实重复创建和销毁 HttpClient 实例,那么是的,您应该处置它。我并不是建议忽略 IDisposable 接口,只是试图鼓励人们重用实例。
  • 只是为了让这个答案更加可信,我今天与 HttpClient 团队进行了交谈,他们确认 HttpClient 不是为每个请求而设计的。当客户端应用程序继续与特定主机交互时,应保持 HttpClient 实例处于活动状态。
  • @DavidPeden 将 HttpClient 注册为单例对我来说听起来很危险,因为它是可变的。例如,分配给Timeout 属性的每个人不会互相踩踏吗?
【解决方案2】:

当前的答案有点令人困惑和误导,并且缺少一些重要的 DNS 含义。我将尝试清楚地总结情况。

  1. 一般来说,大多数IDisposable 对象最好在您处理完它们后处理,尤其是那些own Named/shared OS resourcesHttpClient 也不例外,因为正如 Darrel Miller 指出的那样,它分配取消令牌,请求/响应主体可以是非托管流。
  2. 但是,best practice for HttpClient 说您应该创建一个实例并尽可能地重用它(在多线程场景中使用它的thread-safe members)。因此,在大多数情况下,您永远不会仅仅因为您将一直需要它而将其丢弃
  3. “永远”重复使用同一个 HttpClient 的问题在于 the underlying HTTP connection might remain open against the originally DNS-resolved IP, regardless of DNS changes。在蓝/绿部署和基于 DNS 的故障转移等场景中,这可能是一个问题。有多种方法可以解决这个问题,最可靠的方法是在 DNS 更改发生后服务器发送 Connection:close 标头。另一种可能性涉及在客户端回收HttpClient,或者定期或通过一些了解DNS更改的机制。请参阅https://github.com/dotnet/corefx/issues/11224 了解更多信息(我建议在盲目使用链接博客文章中建议的代码之前仔细阅读它)。

【讨论】:

  • 我一直在处理它,因为我无法在实例上切换代理;)
  • 如果您出于某种原因确实需要处置 HttpClient,则应保留 HttpMessageHandler 的静态实例,因为处置该实例实际上是导致处置 HttpClient 问题的原因。 HttpClient 有一个构造函数重载,允许您指定不应释放提供的处理程序,在这种情况下,您可以将 HttpMessageHandler 与其他 HttpClient 实例重用。
  • 您应该保留您的 HttpClient,但您可以使用类似 System.Net.ServicePointManager.DnsRefreshTimeout = 3000;这很有用,例如如果您使用的是随时可以在 wifi 和 4G 之间切换的移动设备。
【解决方案3】:

由于似乎没有人在此处提及它,因此在 .NET Core >=2.1 和 .NET 5.0+ 中管理 HttpClient 和 HttpClientHandler 的最佳新方法是使用 HttpClientFactory

它以简洁易用的方式解决了大部分上述问题和问题。来自Steve Gordon's great blog post

将以下包添加到您的 .Net Core(2.1.1 或更高版本)项目中:

Microsoft.AspNetCore.All
Microsoft.Extensions.Http

将此添加到 Startup.cs:

services.AddHttpClient();

注入使用:

[Route("api/[controller]")]
public class ValuesController : Controller
{
    private readonly IHttpClientFactory _httpClientFactory;

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

    [HttpGet]
    public async Task<ActionResult> Get()
    {
        var client = _httpClientFactory.CreateClient();
        var result = await client.GetStringAsync("http://www.google.com");
        return Ok(result);
    }
}

探索 Steve 博客中的系列文章,了解更多功能。

【讨论】:

    【解决方案4】:

    在我的理解中,调用Dispose() 仅在它锁定您稍后需要的资源(如特定连接)时才需要。始终建议释放您不再使用的资源,即使您不再需要它们,仅仅是因为您不应该一般持有您的资源'不使用(双关语)。

    微软的例子不一定是不正确的。当应用程序退出时,所有使用的资源都将被释放。在该示例中,这几乎是在使用完 HttpClient 之后立即发生的。在类似的情况下,显式调用Dispose() 有点多余。

    但是,一般来说,当一个类实现IDisposable 时,理解是,一旦你完全准备好并且能够做到,就应该对其实例的Dispose() 进行处理。我认为在HttpClient 这样的情况下尤其如此,其中没有明确记录资源或连接是否被保留/打开。在 [很快] 将再次重用连接的情况下,您需要放弃 Dipose()ing ——在这种情况下,您还没有“完全准备好”。

    另请参阅: IDisposable.Dispose MethodWhen to call Dispose

    【讨论】:

    • 这就像有人把香蕉带到你家,吃了它,然后拿着果皮站着。他们应该如何处理果皮? ......如果他们带着它出门,让他们走。如果他们在附近逗留,让他们把它扔进垃圾箱,这样就不会臭臭了。
    • 只是为了澄清这个答案,您是说“如果程序在您使用后立即结束,则无需处理”?如果程序预计会持续一段时间做其他事情,你应该处理吗?
    • @FernandoCorreia 是的,除非我忘记了什么,否则我认为这是一个安全的原则。不过,在每种情况下都要考虑一下。例如,如果您正在使用连接,您不想过早地Dispose() 并且必须在几秒钟后重新连接如果现有连接是可重用的。同样,您不希望不必要地Dispose() 图像或其他结构,您可能最终不得不在一两分钟内重建。
    • 我明白了。但是在这个问题所涉及的 HttpClient 和 HttpClientHandler 的特殊情况下,它们是否持有开放的资源,例如 HTTP 连接?如果发生这种情况,我可能不得不重新考虑使用它们的模式。
    • @DPeden 您的回答与我的完全没有冲突。请注意,我说过,您应该在完全准备好并且能够后立即 Dispose() 处理它的实例。如果您打算再次使用该实例,那么您还没有准备好
    【解决方案5】:

    简短回答:不,当前接受的答案中的陈述不准确:“普遍的共识是您不需要(不应该)处理 HttpClient”。

    长答案:以下两个陈述都是正确且可同时实现的:

    1. “HttpClient 旨在实例化一次并在应用程序的整个生命周期内重复使用”,引自official documentation
    2. 应该/建议处置一个 IDisposable 对象。

    而且他们不一定会相互冲突。这只是您如何组织代码以重用 HttpClient 并且仍然正确处理它的问题。

    引用自我的another answer 的更更长的答案

    见到人不是巧合 在some blog posts 责备HttpClientIDisposable 接口 使他们倾向于使用using (var client = new HttpClient()) {...} 模式 然后导致耗尽套接字处理程序问题。

    我相信这归结为一个不言而喻的(错误?)概念: "an IDisposable object is expected to be short-lived".

    然而,当我们以这种风格编写代码时,它看起来确实是一件昙花一现的事情:

    using (var foo = new SomeDisposableObject())
    {
        ...
    }
    

    official documentation on 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();
            }
        }
    }
    

    有了这个新的认识,现在我们重温that blog post, 我们可以清楚地注意到“修复”初始化HttpClient 一次但从不释放它, 这就是为什么我们可以从它的 netstat 输出中看到, 连接保持在 ESTABLISHED 状态,这意味着它没有被正确关闭。 如果它被关闭,它的状态将改为 TIME_WAIT。 实际上,在整个程序结束后仅泄漏一个打开的连接并不是什么大问题, 并且博客发布者在修复后仍然看到性能提升; 但是,归咎于 IDisposable 并选择不处置它在概念上是不正确的。

    【讨论】:

    • 感谢您的解释。这显然为达成共识提供了一些启示。在您看来,您认为何时适合致电HttpClient.Dispose ?.
    • @JesonMartajaya,当您的应用程序不再需要使用 httpClient 实例时释放它。您可能会认为这样的建议听起来很模糊,但实际上它可以与您的 HttpClient client 变量的生命周期完全一致,这是您可能已经在做的 Programming-101 事情。您甚至还可以使用using (...) {...}。例如,请参阅我的答案中的 Hello World 示例。
    【解决方案6】:

    Dispose() 调用下面的代码,关闭由 HttpClient 实例打开的连接。代码是用dotPeek反编译生成的。

    HttpClientHandler.cs - 处理

    ServicePointManager.CloseConnectionGroups(this.connectionGroupName);
    

    如果您不调用 dispose,则由计时器运行的 ServicePointManager.MaxServicePointIdleTime 将关闭 http 连接。默认值为 100 秒。

    ServicePointManager.cs

    internal static readonly TimerThread.Callback s_IdleServicePointTimeoutDelegate = new TimerThread.Callback(ServicePointManager.IdleServicePointTimeoutCallback);
    private static volatile TimerThread.Queue s_ServicePointIdlingQueue = TimerThread.GetOrCreateQueue(100000);
    
    private static void IdleServicePointTimeoutCallback(TimerThread.Timer timer, int timeNoticed, object context)
    {
      ServicePoint servicePoint = (ServicePoint) context;
      if (Logging.On)
        Logging.PrintInfo(Logging.Web, SR.GetString("net_log_closed_idle", (object) "ServicePoint", (object) servicePoint.GetHashCode()));
      lock (ServicePointManager.s_ServicePointTable)
        ServicePointManager.s_ServicePointTable.Remove((object) servicePoint.LookupString);
      servicePoint.ReleaseAllConnectionGroups();
    }
    

    如果您没有将空闲时间设置为无限,那么不调用 dispose 并让空闲连接计时器启动并为您关闭连接似乎是安全的,尽管您最好调用 dispose如果您知道您已完成 HttpClient 实例并更快地释放资源,则使用 using 语句。

    【讨论】:

    【解决方案7】:

    在我的例子中,我在一个实际执行服务调用的方法中创建了一个 HttpClient。就像是:

    public void DoServiceCall() {
      var client = new HttpClient();
      await client.PostAsync();
    }
    

    在 Azure worker 角色中,在反复调用此方法(不释放 HttpClient)后,最终会失败并显示SocketException(连接尝试失败)。

    我将 HttpClient 设为实例变量(在类级别处理它),问题就消失了。所以我会说,是的,处置 HttpClient,假设它是安全的(你没有未完成的异步调用)这样做。

    【讨论】:

    • 感谢您的反馈。这是一个有点复杂的问题。我建议阅读 DPeden 答案中链接的文章。简而言之,HttpClient 实例应该在整个应用程序生命周期中重复使用。如果您确实重复创建新实例,则可能需要处理它们。
    • “HttpClient 实例应该在整个应用程序生命周期中重复使用”这对于很多应用程序来说并不是一个好主意。我正在考虑使用 HttpClient 的 Web 应用程序。 HttpClient 持有状态(例如它将使用的请求标头),因此一个 Web 请求线程可以轻松地践踏另一个正在执行的操作。在大型 Web 应用程序中,我也将 HttpClient 视为主要连接问题的问题。如有疑问,我会说 Dispose。
    • @nashwan 您不能在每次请求之前清除标题并添加新标题?
    • 微软还建议重用 HttpClient 实例 - docs.microsoft.com/en-us/aspnet/web-api/overview/advanced/…
    • @MandeepJanjua 该示例似乎是作为控制台应用程序的客户端。我指的是作为客户端的 Web 应用程序。
    【解决方案8】:

    在典型用法(响应

    如果 HttpClient 方法的 Stream Content 未完全读取,则应将其返回类型。否则,CLR 无法知道这些 Streams 可以被关闭,直到它们被垃圾回收。

    • 如果您将数据读入 byte[](例如 GetByteArrayAsync)或字符串,则所有数据都会被读取,因此无需处理。
    • 其他重载将默认读取最大 2GB 的 Stream(HttpCompletionOption 为 ResponseContentRead,HttpClient.MaxResponseContentBufferSize 默认为 2GB)

    如果将 HttpCompletionOption 设置为 ResponseHeadersRead 或响应大于 2GB,则应清理。这可以通过在 HttpResponseMessage 上调用 Dispose 或通过在从 HttpResonseMessage 内容获得的 Stream 上调用 Dispose/Close 或通过完全读取内容来完成。

    是否在 HttpClient 上调用 Dispose 取决于是否要取消挂起的请求。

    【讨论】:

      【解决方案9】:

      如果你想处理 HttpClient,你可以将其设置为资源池。在你的应用程序结束时,你会释放你的资源池。

      代码:

      // Notice that IDisposable is not implemented here!
      public interface HttpClientHandle
      {
          HttpRequestHeaders DefaultRequestHeaders { get; }
          Uri BaseAddress { get; set; }
          // ...
          // All the other methods from peeking at HttpClient
      }
      
      public class HttpClientHander : HttpClient, HttpClientHandle, IDisposable
      {
          public static ConditionalWeakTable<Uri, HttpClientHander> _httpClientsPool;
          public static HashSet<Uri> _uris;
      
          static HttpClientHander()
          {
              _httpClientsPool = new ConditionalWeakTable<Uri, HttpClientHander>();
              _uris = new HashSet<Uri>();
              SetupGlobalPoolFinalizer();
          }
      
          private DateTime _delayFinalization = DateTime.MinValue;
          private bool _isDisposed = false;
      
          public static HttpClientHandle GetHttpClientHandle(Uri baseUrl)
          {
              HttpClientHander httpClient = _httpClientsPool.GetOrCreateValue(baseUrl);
              _uris.Add(baseUrl);
              httpClient._delayFinalization = DateTime.MinValue;
              httpClient.BaseAddress = baseUrl;
      
              return httpClient;
          }
      
          void IDisposable.Dispose()
          {
              _isDisposed = true;
              GC.SuppressFinalize(this);
      
              base.Dispose();
          }
      
          ~HttpClientHander()
          {
              if (_delayFinalization == DateTime.MinValue)
                  _delayFinalization = DateTime.UtcNow;
              if (DateTime.UtcNow.Subtract(_delayFinalization) < base.Timeout)
                  GC.ReRegisterForFinalize(this);
          }
      
          private static void SetupGlobalPoolFinalizer()
          {
              AppDomain.CurrentDomain.ProcessExit +=
                  (sender, eventArgs) => { FinalizeGlobalPool(); };
          }
      
          private static void FinalizeGlobalPool()
          {
              foreach (var key in _uris)
              {
                  HttpClientHander value = null;
                  if (_httpClientsPool.TryGetValue(key, out value))
                      try { value.Dispose(); } catch { }
              }
      
              _uris.Clear();
              _httpClientsPool = null;
          }
      }
      

      var handler = HttpClientHander.GetHttpClientHandle(new Uri("base url")).

      • HttpClient作为接口,不能调用Dispose()。
      • Dispose() 将被垃圾收集器以延迟方式调用。 或者当程序通过其析构函数清理对象时。
      • 使用弱引用 + 延迟清理逻辑,因此只要经常重复使用,它就会一直使用。
      • 它只为传递给它的每个基本 URL 分配一个新的 HttpClient。 Ohad Schneider 解释的原因如下。更改基本网址时的不良行为。
      • HttpClientHandle 允许在测试中进行 Mocking

      【讨论】:

      • 完美。我看到您在 GC 上注册的方法上调用了 Dispose。这应该在顶部得到更高的评价。
      • 请注意,HttpClient 对每个基本 URL 进行资源池。因此,如果您在列表中访问数千个不同的网站,您的性能将会下降,而无需清理这些单独的网站。这暴露了处理每个基本 URL 的能力。但是,如果您只使用一个网站,则可能只是出于学术原因调用 dispose。
      【解决方案10】:

      在构造函数中使用依赖注入可以更轻松地管理 HttpClient 的生命周期 - 将生命周期管理器置于需要它的代码之外,并使其在以后可以轻松更改。

      我目前的偏好是为每个目标端点域创建一个从HttpClient 继承一次的单独 http 客户端类,然后使用依赖注入使其成为单例。 public class ExampleHttpClient : HttpClient { ... }

      然后,我在需要访问该 API 的服务类中对自定义 http 客户端进行构造函数依赖。这解决了生命周期问题,并且在连接池方面具有优势。

      您可以在https://stackoverflow.com/a/50238944/3140853 的相关答案中看到一个工作示例

      【讨论】:

        【解决方案11】:

        请阅读我对下面发布的一个非常相似的问题的回答。应该清楚的是,您应该将 HttpClient 实例视为单例并跨请求重用。

        What is the overhead of creating a new HttpClient per call in a WebAPI client?

        【讨论】:

          【解决方案12】:

          我认为应该使用单例模式来避免必须创建 HttpClient 的实例并一直关闭它。如果您使用的是 .Net 4.0,您可以使用如下示例代码。有关单例模式检查的更多信息here

          class HttpClientSingletonWrapper : HttpClient
          {
              private static readonly Lazy<HttpClientSingletonWrapper> Lazy= new Lazy<HttpClientSingletonWrapper>(()=>new HttpClientSingletonWrapper()); 
          
              public static HttpClientSingletonWrapper Instance {get { return Lazy.Value; }}
          
              private HttpClientSingletonWrapper()
              {
              }
          }
          

          使用如下代码。

          var client = HttpClientSingletonWrapper.Instance;
          

          【讨论】:

          • 执行此操作(以及其他类似方案)时要注意的事项:“Any instance members are not guaranteed to be thread safe.
          • 这个答案是否正确应该完全取决于您想要使用 HttpClient 的应用程序。如果您有一个 Web 应用程序并创建一个所有 Web 请求都将从您那里共享的单例 HttpClient,那么您可能会得到很多连接异常(取决于您的网站有多受欢迎!:-))。 (见 David Faivre 的回答)
          猜你喜欢
          • 2013-03-20
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2010-11-14
          • 2012-04-29
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多