是否必须处理HttpClient和HttpClientHandler?

Fer*_*eia 315 c# idisposable using .net-4.5 dotnet-httpclient

.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();
}
Run Code Online (Sandbox Code Playgroud)

但是微软最明显的例子并没有Dispose()明确地或隐含地调用.例如:

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

检查完样本后,我发现您没有对HttpClient实例执行dispose操作.我用的HttpClient的所有实例使用我的应用程序发言,我认为这是正确的方式,因为HttpClient的实现IDisposable接口.我在正确的道路上吗?

他的回答是:

一般情况下这是正确的,尽管你必须小心"使用"和异步,因为它们并不真正混合.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. 如果他们需要,或者如果是推荐的做法,微软的例子是误导性的还是不安全的?

Dav*_*den 245

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

许多与其工作方式密切相关的人都说过这一点.

请参阅Darrel Miller的博文和相关的SO帖子:HttpClient爬行导致内存泄漏以供参考.

我还强烈建议您阅读使用ASP.NET设计Evolvable Web API中的HttpClient章节,了解有关内幕的内容,特别是此处引用的"生命周期"部分:

尽管HttpClient间接实现了IDisposable接口,但HttpClient的标准用法并不是在每次请求后都将其处理掉.只要您的应用程序需要发出HTTP请求,HttpClient对象就会存在.在多个请求之间存在一个对象,可以设置一个用于设置DefaultRequestHeaders的位置,并防止您必须在HttpWebRequest所需的每个请求上重新指定CredentialCache和CookieContainer之类的内容.

甚至打开DotPeek.

  • 为了澄清你的答案,说"你不需要处理HttpClient,如果你坚持以便重新使用它",这是否正确?例如,如果一个方法被重复调用并创建一个新的HttpClient实例(即使在大多数情况下它不是推荐的模式),那么说这个方法不应该处理该实例(不会被重用)仍然是正确的吗?它可能会导致成千上万的不受干扰的情况.换句话说,你应该尝试重用实例,但是如果不重用,你最好处理它们(释放连接)? (55认同)
  • @FernandoCorreia是的.如果由于某种原因你反复创建和销毁HttpClient实例然后是,你应该处理它.我不是建议忽略IDisposable接口,只是试图鼓励人们重用实例. (23认同)
  • 为了进一步证实这个答案,我今天与HttpClient团队进行了交谈,他们确认HttpClient并非设计为按请求使用.当客户端应用程序继续与特定主机交互时,HttpClient的实例应保持活动状态. (18认同)
  • @DavidPeden将HttpClient注册为单身对我来说听起来很危险,因为它是可变的.例如,分配给`Timeout`属性的每个人都不会互相踩踏? (18认同)
  • 我认为可以理解的令人沮丧但正确的答案取决于它.如果我必须被提供给大多数(我从未说过所有)案例的一般建议,我建议您使用IoC容器并将HttpClient的实例注册为单例.然后,实例的生命周期将限定为容器生命周期的生命周期.这可以在应用程序级别定义,也可以在Web应用程序中按请求定义. (7认同)
  • 另外值得注意的是HttpClient的单个实例不尊重DNS更改.有关详细信息,请参阅http://byterot.blogspot.se/2016/07/singleton-httpclient-dns.html(对于dotnetcore中的DNS问题,请访问https://github.com/dotnet/corefx/issues/11224) (6认同)
  • 除了@Jon-Eric 和@GreenMoose 指出的问题之外,客户端证书和自定义服务器证书验证回调等也是`HttpClient` 状态的一部分(通过其`HttpMessageHandler` 构造函数参数)。 (2认同)

Oha*_*der 41

目前的答案有点令人困惑和误导,他们错过了一些重要的DNS影响.我会试着总结清楚的事情.

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

  • 如果出于某种原因确实需要处理 HttpClient,则应保留 HttpMessageHandler 的静态实例,因为处理该实例实际上是由于处理 HttpClient 引起的问题的原因。HttpClient 有一个构造函数重载,允许您指定不应处理提供的处理程序,在这种情况下,您可以将 HttpMessageHandler 与其他 HttpClient 实例重用。 (2认同)
  • 你应该坚持你的 HttpClient,但你可以使用类似 System.Net.ServicePointManager.DnsRefreshTimeout = 3000; 的东西。这很有用,例如,如果您使用的是随时可以在 wifi 和 4G 之间切换的移动设备。 (2认同)

svi*_*gen 17

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

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

但是,一般来说,当一个类实现时IDisposable,理解是你应该Dispose()在你完全准备好并且能够完成它的实例.我认为这种情况尤其如此,HttpClient其中没有明确记录资源或连接是否被保持/打开.在其中连接将被再次使用的情况下[快],你要放弃Dipose()它的ING -你不是在这种情况下,"完全准备好".

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

  • 就好像有人带了一根香蕉到你家里吃它,然后站在果皮上.他们应该如何处理果皮?......如果他们带着它出门,就让他们走吧.如果它们粘在一起,让它们把它扔进垃圾桶里,这样它就不会让这个地方发臭. (7认同)

Tim*_*lez 8

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

HttpClientHandler.cs - 处理

ServicePointManager.CloseConnectionGroups(this.connectionGroupName);
Run Code Online (Sandbox Code Playgroud)

如果您不调用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();
}
Run Code Online (Sandbox Code Playgroud)

如果你没有将空闲时间设置为无限,那么看起来安全不要调用dispose并让空闲连接计时器启动并为你关闭连接,尽管你最好在using语句中调用dispose.你知道你已经完成了一个HttpClient实例,并且可以更快地释放资源.


Dav*_*vre 5

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

public void DoServiceCall() {
  var client = new HttpClient();
  await client.PostAsync();
}
Run Code Online (Sandbox Code Playgroud)

在Azure辅助角色中,在重复调用此方法(不丢弃HttpClient)之后,它最终会失败SocketException(连接尝试失败).

我让HttpClient成为一个实例变量(在类级别处理它),问题就消失了.所以我会说,是的,处理HttpClient,假设它是安全的(你没有出色的异步调用)来这样做.

  • "HttpClient实例应该在整个应用程序生命周期中重用",这对于许多应用程序来说并不是一个好主意.我在想使用HttpClient的Web应用程序.HttpClient保持状态(例如它将使用的请求头),因此一个Web请求线程可以轻易地践踏另一个正在做的事情.在大型Web应用程序中,我也看到HttpClient是主要连接问题的问题.如果有疑问,我会说Dispose. (7认同)

Ray*_*Luo 5

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

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

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

而且他们彼此之间不必冲突。只是如何组织代码以重复使用HttpClientAND并正确处置它。

我的另一个答案引用了一个更长的答案

某些博客文章中看到人们指责HttpClient' IDisposable接口如何使他们倾向于使用该using (var client = new HttpClient()) {...}模式,然后导致疲惫的套接字处理程序问题,这并不是巧合。

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

但是,当我们以这种方式编写代码时,虽然看起来确实是一时的事:

using (var foo = new SomeDisposableObject())
{
    ...
}
Run Code Online (Sandbox Code Playgroud)

有关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();
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

通过这种新的理解,现在我们重新访问该博客文章,我们可以清楚地注意到“修复”只初始化了HttpClient一次,但从未对其进行处理,这就是为什么我们从其netstat输出中可以看到,连接保持在ESTABLISHED状态,这意味着它已经没有正确关闭。如果关闭,则其状态将改为TIME_WAIT。实际上,在整个程序结束后只泄漏一个打开的连接并没什么大不了的,并且在修正后,博客发布者仍然可以看到性能的提高;但是,将IDisposable归咎于并选择不处置它在概念上是不正确的。


pcd*_*dev 5

由于这里似乎还没有人提到它,因此在.Net Core 2.1中管理HttpClient和HttpClientHandler的最佳新方法是使用HttpClientFactory

它以干净且易于使用的方式解决了上述大多数问题和陷阱。摘自史蒂夫·戈登的精彩博客文章

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

Microsoft.AspNetCore.All
Microsoft.Extensions.Http
Run Code Online (Sandbox Code Playgroud)

将此添加到Startup.cs:

services.AddHttpClient();
Run Code Online (Sandbox Code Playgroud)

注入和使用:

[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);
    }
}
Run Code Online (Sandbox Code Playgroud)

探索Steve博客中的一系列帖子,以获取更多功能。