HttpClient和HttpClientHandler在请求之间需要被释放吗?

416

.NET Framework 4.5 中的 System.Net.Http.HttpClientSystem.Net.Http.HttpClientHandler 实现了 IDisposable 接口(通过 System.Net.Http.HttpMessageInvoker)。

using 语句的文档说明如下:

通常情况下,当您使用一个 IDisposable 对象时,应在 using 语句中声明和实例化它。

这个答案 使用了这种模式:

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()。例如:

此发布说明的评论中,有人问这位微软员工:

在查看您的示例后,我发现您没有在 HttpClient 实例上执行 dispose 操作。我已经在我的应用程序中使用了所有 HttpClient 实例,并且认为这是正确的方式,因为 HttpClient 实现了 IDisposable 接口。我走上了正确的道路吗?

他的回答是:

通常情况下是正确的,虽然您必须小心 "using" 和 async 的混合使用,在 .NET 4 中它们不能真正地混合使用。在 .NET 4.5 中,您可以在 "using" 语句中使用 "await"。

顺便说一下,您可以重复使用相同的 HttpClient,所以通常不需要创建/处理它们所有的时间。

第二段对于这个问题来说是多余的,因为这个问题并不关心您可以使用 HttpClient 实例的次数,而是关于在您不再需要它时是否需要释放它。

(更新:实际上,正如 @DPeden 提供的答案所述,第二段是答案的关键。)

所以我的问题是:

  1. 根据当前的实现 (.NET Framework 4.5),是否需要调用 HttpClient 和 HttpClientHandler 实例的 Dispose() 方法?澄清一下:我所说的 "需要" 是指没有释放会导致任何负面影响,例如资源泄漏或数据损坏风险。

  2. 如果不需要,在实现 IDisposable 的情况下,是否仍然是“良好的实践”?

  3. 如果需要(或推荐),上面提到的 代码 是否安全实现了它 (对于 .NET Framework 4.5)?

  4. 如果这些类不需要调用 Dispose(),为什么还要实现 IDisposable 接口?

  5. 如果它们需要,或者如果这是一种推荐实践,请问微软的示例是否具有误导性或不安全?


2
@Damien_The_Unbeliever,感谢您的反馈。您有什么建议可以帮助我澄清问题吗?我想知道它是否会导致通常与未处理资源相关的问题,例如资源泄漏和数据损坏。 - Fernando Correia
10
不正确。特别是流编写器必须被处置以具有正确的行为。 - Stephen Cleary
1
@StephenCleary - 你考虑的是哪些方面?当然,你可以在每次写操作后调用Flush,除了它持续保持底层资源比必要时间长的不便之外,还需要发生什么才能实现“正确的行为”? - Damien_The_Unbeliever
2
这是完全错误的:“通常情况下,当您使用IDisposable对象时,应在using语句中声明和实例化它。” 在决定是否应该为其使用using之前,我会始终阅读实现IDisposable的类的文档。作为我实现IDisposable的库的作者,因为需要释放未管理的资源,如果消费者每次都创建已释放的实例而不是重用现有实例,我会感到震惊。这并不是说最终不要处理实例。 - markmnl
1
我已向微软提交了一个PR,以更新他们的文档:https://github.com/dotnet/docs/pull/2470 - markmnl
显示剩余7条评论
12个回答

302
一般的共识是你不需要(也不应该)处理 HttpClient。许多深入了解其工作方式的人都已经表明了这一点。参考 Darrel Miller 的博客文章 和相关的 SO 文章:HttpClient 爬取结果导致内存泄漏
我还强烈建议你阅读 ASP.NET 中可演化 Web API 的设计 中的 HttpClient 章节,以便了解底层发生了什么,特别是引用了“生命周期”部分的内容。
虽然HttpClient间接实现了IDisposable接口,但标准使用方法并不是在每个请求之后都将其释放。HttpClient对象的目的是在应用程序需要发出HTTP请求时一直存在。一个对象存在于多个请求中可以提供一个设置DefaultRequestHeaders的位置,并且可以避免像HttpWebRequest一样在每个请求上重新指定CredentialCache和CookieContainer这样的信息。或者甚至打开DotPeek。

88
为了澄清你的答案,可以这样说吗:“如果您保留实例以便以后重用,则无需处理HttpClient”?例如,如果一个方法被重复调用并创建一个新的HttpClient实例(即使在大多数情况下这不是推荐的模式),那么仍然可以说该方法不应该处置不会被重用的实例吗?这可能会导致数千个未处置的实例。换句话说,您应该尝试重用实例,但如果不重用,则最好处置它们(以释放连接)? - Fernando Correia
9
我认为令人理解但令人沮丧但正确的答案是“这取决于情况”。如果我必须给出适用于大多数情况(我从不说全部)的一般建议,我建议您使用一个IoC容器,并注册一个HttpClient实例作为单例。该实例的生命周期将被限定在容器的生命周期内。这可以在应用程序级别或Web应用程序中每个请求之间进行范围定义。 - David Peden
30
如果由于某些原因你需要反复创建和销毁HttpClient实例,那么是的,你应该将其Dispose。我并不建议忽略IDisposable接口,只是试图鼓励人们重复使用实例。 - Darrel Miller
23
为了进一步证明这个答案的可信性,我今天与 HttpClient 团队进行了交流,他们确认 HttpClient 并不是为每个请求而设计的。在客户端应用程序与特定主机继续交互时,应该保持 HttpClient 的实例处于活动状态。 - Darrel Miller
27
在我看来,将HttpClient注册为单例听起来很危险,因为它是可变的。例如,每个人都分配给Timeout属性时不会互相干扰吗? - Jon-Eric
显示剩余4条评论

79

目前的答案有些混乱和误导,并且缺少一些重要的DNS影响。我将尝试清楚地总结现状。

  1. 一般来说,大多数实现了 IDisposable 接口的对象在使用完后应该被及时处理掉,尤其是那些拥有命名/共享操作系统资源的对象。HttpClient 也不例外,因为正如 Darrel Miller 所指出的那样,它会分配取消令牌,并且请求/响应体可能是非托管流。
  2. 然而,HttpClient 的最佳实践 建议您尽可能创建一个实例并重复使用它(在多线程场景中使用其 线程安全成员)。因此,在大多数情况下,您永远不会将其处理掉,因为您始终需要它
  3. 重复使用同一个 HttpClient "永远" 的问题在于,底层 HTTP 连接可能会针对最初解析的 DNS IP 保持打开状态,而不考虑 DNS 更改。这可能在像蓝/绿部署和基于 DNS 的故障转移等方案中成为问题。有各种方法来处理这个问题,其中最可靠的方法涉及服务器在 DNS 更改发生后发送 Connection:close 标头。另一种可能性涉及在客户端上定期或通过某种机制回收 HttpClient,以了解 DNS 更改。有关更多信息,请参见 https://github.com/dotnet/corefx/issues/11224(我建议在盲目使用链接的博客文章中提供的代码之前仔细阅读它)。

我总是处理它,因为我无法在实例上切换代理 ;) - ed22
2
如果出于任何原因确实需要处理 HttpClient,您应该保留 HttpMessageHandler 的静态实例,因为处理它实际上是导致将 HttpClient 处理掉的问题的原因。HttpClient 有一个构造函数重载,允许您指定提供的处理程序不应被处理,在这种情况下,您可以将 HttpMessageHandler 与其他 HttpClient 实例重用。 - Tom Lint
2
你应该保留你的HttpClient,但是你可以使用类似System.Net.ServicePointManager.DnsRefreshTimeout = 3000;这样的东西。例如,如果你正在使用可能随时在wifi和4G之间切换的移动设备,这将非常有用。 - Johan Franzén

40

鉴于似乎还没有人在这里提到,管理 .NET Core >=2.1和.NET 5.0+ 中的 HttpClient 和 HttpClientHandler 的最佳方法是使用 HttpClientFactory

它以简洁易用的方式解决了大部分之前提到的问题。来自 Steve Gordon 的精彩博客文章

将以下包添加到您的 .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博客中的一系列帖子,了解更多特性。


19
在我的理解中,只有在锁定你之后需要使用的资源(例如特定连接)时才需要调用Dispose()。即使您不再需要这些资源,也始终建议释放您不再使用的资源,因为您通常不应该持有您不使用的资源(刻意为之)。
Microsoft的示例并不一定不正确。当应用程序退出时,将释放使用的所有资源。在该示例中,这几乎是在HttpClient完成使用后立即发生的。在类似情况下,显式调用Dispose()有点多余。
但是,一般来说,当一个类实现IDisposable时,我们应该在完全准备好并能够这样做的情况下Dispose()它的实例。我认为,这在像HttpClient这样的情况下尤其如此,因为没有明确说明是否保留或打开资源或连接。如果连接很快就会被重用,您将希望放弃对其进行Dipose()操作--在这种情况下,您还没有“完全准备好”。
另请参见:IDisposable.Dispose方法何时调用Dispose

11
如果有人带香蕉到你家,把它吃完后还留下了皮,该怎么办呢?如果他们要离开你家,就让他们拿着皮走吧。但如果他们还要逗留,就得要求他们把皮扔进垃圾桶,以免弄臭了房间。 - svidgen
只是为了澄清这个答案,您是在说:“如果程序使用后立即结束,那么不需要处理”?而且,如果预计程序将继续执行其他操作,则应进行处理? - Fernando Correia
@FernandoCorreia 是的,除非我忘了什么,我认为这是一个安全的原则。不过在每种情况下都要考虑一下。例如,如果你正在使用连接,你不想过早地 Dispose() 它,然后在几秒钟后不得不重新连接 如果 现有的连接是可重用的。同样,你不想不必要地 Dispose() 图像或其他结构,因为你可能会在一两分钟内需要重新构建它们。 - svidgen
1
@DPeden,你的回答并不与我的回答相冲突。请注意,我说过,“只有当你已经完全准备好并且能够Dispose()其实例时,才应该这样做”。如果你计划再次使用该实例,那就意味着你还没准备好 - svidgen
@DarrelMiller 可能是因为他们认为另一个进程使用相同的连接或需要相同的 cookies 的机会很小(我假设它会保留 cookies)。所以,除了与其他进程共享连接可能存在潜在的安全风险外,留下没有人需要的连接也是“不考虑他人”的。这不像 Web 应用程序的数据库连接,例如,其中每个东西都可以使用相同的连接。这是一个 Web 客户端。很有可能,您的进程需要其他进程无关的东西。 - svidgen
显示剩余12条评论

14
简短回答:不,目前接受的答案中的陈述是不准确的:“普遍共识是您不需要(也不应该)处置 HttpClient。”
长答案:以下两个陈述都是真实且可同时实现的:
1. "HttpClient 旨在被实例化一次并在应用程序的整个生命周期内重复使用",引用自 官方文档。 2. IDisposable 对象应该/建议进行处理。
它们不一定相互冲突。这只是一个如何组织代码以重用 HttpClient 并正确处理它的问题。
更长的答案引用自 另一个答案
不难看到 某些博客文章 中的人责备 HttpClient 的 IDisposable 接口会导致他们倾向于使用 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并选择不处理它是不正确的。

谢谢您的解释。它清楚地阐明了共识。根据您的意见,您认为在什么情况下调用 HttpClient.Dispose 是适当的? - Jeson Martajaya
@JesonMartajaya,当您的应用程序不再需要使用httpClient实例时,请将其处理掉。您可能认为这样的建议听起来很模糊,但实际上它可以完美地与您的HttpClient client变量的生命周期保持一致,这是您可能已经在做的编程101事情。您甚至仍然可以使用using (...) {...}。例如,请参见我的答案中的Hello World示例。 - RayLuo

10

Dispose()方法调用以下代码,关闭由HttpClient实例打开的连接。该代码是通过使用dotPeek反编译创建的。

HttpClientHandler.cs - Dispose

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,让空闲连接计时器启动并为您关闭连接,尽管如果您知道已经完成了HttpClient实例,请在using语句中调用dispose以更快地释放资源,这样会更好。

1
你也可以在 Github 上查看代码。https://github.com/dotnet/corefx/blob/master/src/System.Net.Http/src/System/Net/Http/HttpClient.cs - TamusJRoyce

5
在我的情况下,我正在方法内创建一个HttpClient来执行服务调用。就像这样:
public void DoServiceCall() {
  var client = new HttpClient();
  await client.PostAsync();
}

在Azure工作角色中,如果反复调用此方法(未处理HttpClient),最终会失败并显示SocketException(连接尝试失败)。
我将HttpClient设置为实例变量(在类级别处进行处理),问题就消失了。因此,我会建议您在安全的情况下(没有未完成的异步调用)处理HttpClient。

7
“HttpClient实例应该在应用程序生命周期中重复使用”这并不适用于许多应用程序。我考虑的是使用HttpClient的Web应用程序。HttpClient保存状态(例如它将使用的请求标头),因此一个Web请求线程可能会轻易地干扰另一个线程正在进行的操作。在大型Web应用程序中,我也看到了HttpClient成为主要连接问题的原因。当有疑问时,我建议Dispose。” - bytedev
1
@nashwan 你不能在每个请求之前清除头部并添加新的吗? - Mandeep Janjua
微软还建议重复使用HttpClient实例-https://learn.microsoft.com/en-us/aspnet/web-api/overview/advanced/calling-a-web-api-from-a-net-client - Mandeep Janjua
@MandeepJanjua 那个例子似乎是一个控制台应用程序作为客户端。我指的是一个 Web 应用程序作为客户端。 - bytedev
嗨@nashwan,请在此处找到Web应用程序的正确链接-https://msdn.microsoft.com/en-us/library/system.net.http.httpclient(v=vs.110).aspx#Anchor_5 - Mandeep Janjua
显示剩余2条评论

3

在典型的用法(responses<2GB)中,不需要Dispose HttpResponseMessages。

如果HttpClient方法的Stream内容没有完全读取,则应Dispose其返回类型。否则,CLR无法知道这些流何时关闭,直到它们被垃圾回收为止。

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

如果将HttpCompletionOption设置为ResponseHeadersRead或响应大于2GB,则应进行清理。可以通过调用HttpResponseMessage上的Dispose或通过在获取自HttpResonseMessage Content的Stream时调用Dispose/Close来完成此操作,或者通过完全读取内容来完成此操作。

是否对HttpClient调用Dispose取决于您是否想要取消挂起的请求。


2
如果您想处理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("基础 URL")).

  • 作为接口,HttpClient无法调用Dispose()。
  • Dispose()将由垃圾回收器以延迟的方式调用。 或者当程序通过其析构函数清理对象时。
  • 使用弱引用+延迟清理逻辑,因此只要经常重复使用,它就会保持使用状态。
  • 它仅为传递给它的每个基本URL分配一个新的HttpClient。Ohad Schneider的答案解释了原因。更改基础URL时的不良行为。
  • HttpClientHandle允许在测试中进行模拟

太好了。我看到你在注册GC上的方法时调用了Dispose。这应该排名更高。 - Jeson Martajaya
请注意,HttpClient会对每个基本URL进行资源池化。因此,如果您在列表中访问数千个不同的网站,则性能将会降低,而不清理这些单独的站点。这暴露了处理每个基本URL的能力。但是,如果您只使用一个网站,可能仅出于学术原因才调用dispose。 - TamusJRoyce

1
在构造函数中使用依赖注入可以使您更轻松地管理HttpClient的生命周期 - 将生命周期管理移到需要它的代码之外,并使其在以后更容易更改。 我目前的首选是为每个目标终端域创建一个从HttpClient继承的单独的http客户端类,然后使用依赖注入将其作为单例。 public class ExampleHttpClient:HttpClient {...} 然后,在我需要访问该API的服务类中,我对自定义http客户端进行构造函数依赖项。 这解决了生命周期问题,并且在连接池方面具有优势。 您可以在相关答案https://dev59.com/hFgQ5IYBdhLWcg3wOBSw#50238944中看到一个实际示例。

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