384

.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();
}

但是微软最明显的例子并没有Dispose()显式或隐式调用。例如:

公告的评论中,有人问微软员工:

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

他的回答是:

通常这是正确的,尽管您必须小心“使用”和异步,因为它们不会真正混合在 .Net 4 中,在 .Net 4.5 中,您可以在“使用”语句中使用“等待”。

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

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

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

所以我的问题是:

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

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

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

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

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

4

12 回答 12

285

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

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

请参阅Darrel Miller 的博客文章和相关的 SO 文章:HttpClient crawling results in memory leak以供参考。

我还强烈建议您阅读Designing Evolvable Web APIs with ASP.NET中的 HttpClient 章节,了解幕后情况,特别是此处引用的“生命周期”部分:

尽管 HttpClient 确实间接实现了 IDisposable 接口,但 HttpClient 的标准用法并不是在每次请求后都将其丢弃。只要您的应用程序需要发出 HTTP 请求,HttpClient 对象就会一直存在。让一个对象存在于多个请求中可以设置 DefaultRequestHeaders 并防止您必须在每个请求上重新指定诸如 HttpWebRequest 所必需的 CredentialCache 和 CookieContainer 之类的东西。

甚至打开 DotPeek。

于 2013-03-29T17:49:38.927 回答
67

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

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

由于似乎没有人在这里提到它,因此在 .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 博客中的系列文章,了解更多功能。

于 2019-07-08T05:14:41.027 回答
19

Dispose()据我了解,只有在锁定您以后需要的资源(例如特定连接)时才需要调用。始终建议释放您不再使用的资源,即使您不再需要它们,仅仅是因为您通常不应该持有您不使用的资源(双关语)。

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

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

另请参阅: IDisposable.Dispose 方法何时调用 Dispose

于 2013-03-29T14:52:09.040 回答
12

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

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

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

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

从我的另一个答案中引用的更长的答案:

在一些博客文章中看到人们指责HttpClient's IDisposableinterface 如何使他们倾向于使用该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 并选择不处置它在概念上是不正确的。

于 2018-05-31T20:19:22.057 回答
9

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

HttpClientHandler.cs - 处置

ServicePointManager.CloseConnectionGroups(this.connectionGroupName);

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

服务点管理器.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 并让空闲连接计时器启动并为您关闭连接似乎是安全的,尽管如果您在 using 语句中调用 dispose 会更好你知道你已经完成了一个 HttpClient 实例并更快地释放资源。

于 2017-10-26T22:49:51.623 回答
4

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

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

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

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

于 2013-04-26T14:06:31.437 回答
3

在典型用法(响应<2GB)中,不需要处理 HttpResponseMessages。

如果 HttpClient 方法的流内容未完全读取,则其返回类型应为 Disposed。否则,CLR 无法知道这些流可以关闭,直到它们被垃圾收集。

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

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

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

于 2013-12-26T15:14:07.650 回答
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("base url")).

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

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

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

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

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

于 2018-05-08T17:37:16.930 回答
0

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

在 WebAPI 客户端中每次调用创建一个新的 HttpClient 的开销是多少?

于 2019-10-15T21:25:16.853 回答
-2

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

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;
于 2013-10-04T12:37:54.087 回答