是否必须在请求之间处理HttpClient和HttpClientHandler?


334

.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操作。我已经在应用程序上使用了using语句来使用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. 如果他们有要求,或者这是推荐做法,那么Microsoft示例是否具有误导性或不安全性?


2
@Damien_The_Unbeliever,谢谢您的反馈。您对我如何澄清这个问题有任何建议吗?我想知道它是否会导致通常与不处理资源有关的问题,例如资源泄漏和数据损坏。
Fernando Correia

9
@Damien_The_Unbeliever:不正确。特别是,必须对流编写器进行处理以使其具有正确的行为。
Stephen Cleary 2013年

1
@StephenCleary-您在想什么方面?当然,您可以Flush在每次写操作之后调用一次,除了不便继续将基础资源保留超过必要的时间之外,“正确的行为”将不会发生什么?
Damien_The_Unbeliever 2013年

1
这是很明显的错误:“通常,当您使用IDisposable对象时,应在using语句中声明并实例化它”。在决定是否应该为它使用use之前,我总是会阅读有关实现IDisposable的类的文档。作为我实现IDisposable的库的作者,因为需要释放未管理的资源,如果创建的使用者每次都处置一个实例而不是重新使用一个现有实例,我会感到恐惧。这并不是说最终不要处置实例
。– markmnl

1
我已向Microsoft提交了一份PR,以更新其文档:github.com/dotnet/docs/pull/2470
markmnl

Answers:


259

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

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

请参阅Darrel Miller的博客文章和相关的SO文章:HttpClient爬网导致内存泄漏,以供参考。

我也强烈建议您阅读使用ASP.NET设计可演化的Web API》中的HttpClient一章,了解有关幕后情况的上下文,尤其是此处引用的“生命周期”部分:

尽管HttpClient确实间接实现了IDisposable接口,但是HttpClient的标准用法是在每次请求后都不要处理它。只要您的应用程序需要发出HTTP请求,HttpClient对象就可以存在。在多个请求中都存在一个对象,这为设置DefaultRequestHeaders提供了空间,并且使您不必在HttpWebRequest所必需的每个请求上重新指定诸如CredentialCache和CookieContainer之类的东西。

甚至开放DotPeek。


64
为了澄清您的答案,是否正确地说“如果您坚持使用实例稍后再使用它,就不需要处置HttpClient”是否正确?例如,如果一个方法被反复调用并创建一个新的HttpClient实例(即使在大多数情况下不是推荐的模式),那么说此方法不应该处置该实例(不会被重用)是否仍然正确?这可能导致成千上万个未处置的实例。换句话说,您应该尝试重用实例,但是如果您不重用,则最好处置它们(以释放连接)?
Fernando Correia

8
我认为可以理解的令人沮丧但正确的答案取决于它。如果我不得不提出在大多数情况下(我永远不会说全部)的通用建议,我建议您使用IoC容器并将HttpClient实例注册为单例。然后,实例的生存期将范围限定为容器的生存期。这可以在应用程序级别确定范围,也可以在Web应用程序中按请求确定范围。
David Peden 2013年

25
@FernandoCorreia是的。如果由于某种原因重复创建和销毁HttpClient实例,则可以,请处置它。我不建议忽略IDisposable接口,只是试图鼓励人们重用实例。
Darrel Miller

20
为了进一步增加对此答案的可信度,我今天与HttpClient团队进行了交谈,他们确认HttpClient并非设计用于每个请求。当客户端应用程序继续与特定主机交互时,HttpClient实例应保持活动状态。
Darrel Miller

19
@DavidPeden将HttpClient注册为单例对我来说很危险,因为它是可变的。例如,分配给该Timeout属性的每个人都不会互相踩踏吗?
乔恩·埃里克

47

当前的答案有点混乱和误导,并且它们缺少一些重要的DNS含义。我将尝试总结清楚的地方。

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

我一直都在处理它,因为我无法在实例上切换代理;)
ed22 '18

如果确实由于某种原因需要处置HttpClient,则应保留HttpMessageHandler的静态实例,因为处置实际上是导致处置HttpClient的问题的原因。HttpClient具有构造函数重载,允许您指定不应处理提供的处理程序,在这种情况下,您可以将HttpMessageHandler与其他HttpClient实例一起使用。
汤姆·林特

您应该坚持使用HttpClient,但是可以使用类似System.Net.ServicePointManager.DnsRefreshTimeout = 3000;的方法。这很有用,例如,如果您使用的移动设备随时可能在wifi和4G之间切换。
JohanFranzén19年

18

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

Microsoft的示例不一定是错误的。当应用程序退出时,将释放所有使用的资源。在该示例中,这几乎HttpClient是在使用完后立即发生的。在类似情况下,显式调用Dispose()有些多余。

但是,总的来说,当一个类实现时IDisposable,我们的理解是,Dispose()一旦您准备就绪并有能力,就应该立即查看其实例。我认为在HttpClient没有明确记录资源或连接是否处于打开状态的情况下尤其如此。如果连接将很快再次使用,您将要放弃Dipose()该连接–在这种情况下,您还没有“完全准备好”。

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


7
就像有人将香蕉带到您的房子,吃掉它并用果皮站着。他们应该怎么处理果皮?...如果他们正用它出门,那就放开他们。如果他们还在附近,请让他们将其扔进垃圾桶,以免弄脏地方。
svidgen

只是为了澄清这个答案,您是在说,“如果使用完程序后就立即结束,则不必进行处理”?如果期望程序继续执行其他操作,您应该处置吗?
Fernando Correia

@FernandoCorreia是的,除非我忘记了什么,否则我认为这是一个安全的原则。不过,在每种情况下都要考虑一下。例如,如果您使用的是连接,则不希望Dispose()过早地断开连接,如果现有连接可重用,则必须在几秒钟后重新连接。同样,您也不需要不必要Dispose()的图像或其他结构,而最终可能需要在一两分钟内进行重建。
svidgen

我明白。但是在这个问题所涉及的HttpClient和HttpClientHandler的特定情况下,它们是否持有诸如HTTP连接之类的开放资源?如果那是正在发生的事情,我可能不得不重新考虑使用它们的方式。
费尔南多·科雷亚2013年

1
@DPeden您的回答与我的根本不冲突。请注意,我说,你应该尽快你出售其实例()完全准备好,可以。如果您打算再次使用该实例,那么您还没有准备好
svidgen

9

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



8

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

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

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

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

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

某些博客文章中看到人们指责如何做到这一点并非巧合。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变量的生命周期完全一致,这大概是您可能已经在做的Programming-101事情。您甚至可能仍然可以使用using (...) {...}。例如,请参阅我的答案中的Hello World示例。
RayLuo

7

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

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

将以下软件包添加到您的.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

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

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

在Azure工作人员角色中,反复调用此方法(不处理HttpClient)后,最终将失败 SocketException(连接尝试失败)。

我将HttpClient设置为实例变量(将其放置在类级别),然后问题消失了。因此,我想说的是,假设HttpClient可以安全处理(您没有未完成的异步调用),则可以将其丢弃。


感谢您的反馈。这是一个有点复杂的问题。我建议阅读DPeden答案中链接的文章。简而言之,应该在整个应用程序生命周期中重用HttpClient实例。如果确实重复创建新实例,则可能需要处置它们。
Fernando Correia

6
“在整个应用程序生命周期中都应重用HttpClient实例”对于许多应用程序来说,这并不是一个好主意。我在考虑使用HttpClient的Web应用程序。HttpClient保持状态(例如它将使用的请求标头),因此一个Web请求线程可以轻松踩踏另一个正在执行的操作。在大型Web应用程序中,我还将HttpClient视为主要连接问题的问题。如有疑问,请说“处置”。
bytedev

@nashwan您不能在每个请求之前清除标题并添加新的标题吗?
Mandeep Janjua


@MandeepJanjua的示例似乎是作为控制台应用程序的客户端。我指的是一个Web应用程序作为客户端。
bytedev

3

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

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

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

如果将HttpCompletionOption设置为ResponseHeadersRead或响应大于2GB,则应清除。这可以通过在HttpResponseMessage上调用Dispose或在从HttpResonseMessage内容获得的流上调用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(“ base url”))。

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

完善。我看到您调用Dispose了在GC上注册的方法。在顶部应将其评级更高。
Jeson Martajaya

请注意,HttpClient按基本URL进行资源池化。因此,如果您在列表中访问了成千上万个不同的网站,则在不清理这些单独网站的情况下,您的性​​能将会下降。这提供了处置每个基本URL的能力。但是,如果您仅使用一个网站,则可能仅出于学术原因致电处置。
TamusJRoyce,

1

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

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

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

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



-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;

3
在执行此操作(和其他类似方案)时需要注意的事项:“ 不保证任何实例成员都是线程安全的。
tne 2014年

2
这个答案是否正确应该完全取决于您要使用HttpClient的应用程序。如果您有一个Web应用程序并创建一个单例HttpClient,然后所有您的Web请求都将共享该HttpClient,则可能会得到很多连接异常(取决于您的网站受欢迎程度!:-)。(请参见David Faivre的答案)
bytedev
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.