关于c#:HttpClient和HttpClientHandler必须处理?

Do HttpClient and HttpClientHandler have to be disposed?

system.net.http.httpclient和system.net.http.httpclienthandler在.NET Framework 4.5(通过实现IDisposable system.net.http.httpmessageinvoker)。

声明说:"using文档

As a rule, when you use an IDisposable object, you should declare and
instantiate it in a using statement.

回答:这个模式使用本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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();
}

但最明显的例子是微软的Don’t Dispose()要么明确或implicitly呼叫。例如:

  • 博客文章的原announcing of HttpClient的释放。
  • 在MSDN文档的HttpClient的实际。
  • bingtranslatesample
  • googlemapssample
  • worldbanksample

在这里的评论,有人问:"微软的员工

After checking your samples, I saw that you didn't perform the dispose
action on HttpClient instance. I have used all instances of HttpClient
with using statement on my app and I thought that it is the right way
since HttpClient implements the IDisposable interface. Am I on the
right path?

他的回答是:

In general that is correct although you have to be careful with
"using" and async as they dont' really mix in .Net 4, In .Net 4.5 you
can use"await" inside a"using" statement.

Btw, you can reuse the same HttpClient as many times are [as] you like so
typically you won't create/dispose them all the time.

第二条本superfluous是需要担心的问题是,你可以使用多少次在HttpClient实例,但如果它是一个必要的信息后,你不再需要它。

(更新:在这样的事实,第二部分是关键的答案,下面是提供的dpeden red"。)

所以我的问题是:

  • 这是没有必要的,目前的执行(.NET Framework 4.5),调用Dispose()在与httpclienthandler HttpClient的实例?clarification:"必要的"如果有任何消极后果的均值为disposing资源困境,如泄漏或数据腐败的风险。

  • 如果是没有必要的,它是一个"良好做法",无论如何,因为他们实现IDisposable。

  • 如果是必要的(或没有),这是实施IT安全码以上(for .NET Framework 4.5)?

  • 如果论文类不需要调用Dispose(),为实现IDisposable为什么是他们?

  • 如果他们需要,或如果它是一个伟大的例子是微软的做法,对于成本或不安全?


  • 一般的共识是,您不需要(不应该)处理httpclient。

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

    请参阅darrel-miller的博客文章和相关的so文章:httpclient crawling results in memory leak以供参考。

    我还强烈建议您阅读httpclient一章,从使用ASP.NET设计可进化的Web API中了解引擎盖下正在发生的事情,特别是这里引用的"生命周期"部分:

    Although HttpClient does indirectly implement the IDisposable
    interface, the standard usage of HttpClient is not to dispose of it
    after every request. The HttpClient object is intended to live for as
    long as your application needs to make HTTP requests. Having an object
    exist across multiple requests enables a place for setting
    DefaultRequestHeaders and prevents you from having to re-specify
    things like CredentialCache and CookieContainer on every request as
    was necessary with HttpWebRequest.

    甚至打开dotpeek。


    目前的答案有点令人困惑和误导,它们缺少一些重要的DNS含义。我试着总结一下事情的发展方向。

  • 一般来说,大多数IDisposable对象在处理完后,尤其是那些拥有命名/共享操作系统资源的对象时,应该理想地进行处理。HttpClient也不例外,因为正如darrel-miller指出的那样,它分配取消令牌,请求/响应主体可以是非托管流。
  • 但是,httpclient的最佳实践表明,您应该创建一个实例,并尽可能多地重用它(在多线程场景中使用它的线程安全成员)。因此,在大多数情况下,你永远不会仅仅因为你一直需要它就把它处理掉。
  • 重新使用同一个httpclient"永远"的问题是,无论DNS发生了什么变化,底层的HTTP连接都可能针对最初的DNS解析的IP保持开放状态。在蓝/绿部署和基于DNS的故障转移等场景中,这可能是一个问题。处理这个问题的方法有很多种,其中最可靠的一种方法是在发生DNS更改后,服务器发送一个Connection:close头。另一种可能是在客户端循环使用HttpClient,可以是定期的,也可以通过一些了解DNS更改的机制。更多信息请参见https://github.com/dotnet/corefx/issues/11224(我建议在盲目使用链接博客帖子中建议的代码之前仔细阅读)。

  • 据我所知,只有在锁定稍后需要的资源(如特定连接)时,才需要调用Dispose()。它总是建议释放你不再使用的资源,即使你不再需要它们,仅仅是因为你一般不应该持有你不使用的资源(双关语是故意的)。

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

    但是,一般来说,当一个类实现IDisposable时,理解是您应该在完全准备好并能够使用它的实例后立即使用Dispose()。我认为这在像HttpClient这样的情况下尤其正确,因为没有明确记录资源或连接是否被保留/打开。在连接将再次被重用的情况下[很快],您将要放弃它的Dipose()--在这种情况下,您还没有"完全准备好"。

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


    dispose()调用下面的代码,关闭由httpclient实例打开的连接。代码是用dotpeek进行反编译创建的。

    httpclienthandler.cs-释放

    1
    ServicePointManager.CloseConnectionGroups(this.connectionGroupName);

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

    服务点管理器.cs

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    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。


    在我的例子中,我在一个实际执行服务调用的方法中创建了一个httpclient。类似:

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

    在一个Azure工作者角色中,在反复调用此方法(不处理httpclient)之后,它最终会在SocketException中失败(连接尝试失败)。

    我使httpclient成为一个实例变量(在类级别处理它),问题就消失了。所以我会说,是的,释放httpclient,假设它是安全的(您没有未完成的异步调用)。


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

    长话短说:以下两种说法都是正确的,并且可以同时实现:

  • "httpclient将被实例化一次,并在应用程序的整个生命周期中重复使用",引用自官方文档。
  • 应/建议处置IDisposable对象。
  • 它们不一定会相互冲突。这只是如何组织代码以重用HttpClient,并仍然正确地处理它的问题。

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

    见到人不是巧合在一些博客文章中指责HttpClientIDisposable接口使他们倾向于使用using (var client = new HttpClient()) {...}模式然后导致耗尽的套接字处理程序问题。

    我认为这归结为一个潜台词(mis?)概念:"IDisposable对象应该是短期的"。

    然而,当我们以这种方式编写代码时,它看起来确实是一个短暂的东西:

    1
    2
    3
    4
    using (var foo = new SomeDisposableObject())
    {
        ...
    }

    可转让证件的正式文件从来没有提到过IDisposable物体必须是短命的。根据定义,IDisposable只是一种允许您释放非托管资源的机制。再也没有了。从这个意义上说,你最终会触发处置,但这并不要求你以一种短暂的方式去做。

    因此,正确选择何时触发处置是您的工作,基于真实对象的生命周期需求。没有什么能阻止您长期使用IDisposable:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    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();
            }
        }
    }

    有了这个新的理解,现在我们重新访问那个博客帖子,我们可以清楚地注意到"fix"将HttpClient初始化一次,但决不处理它,这就是为什么我们可以从它的netstat输出中看到,连接保持在已建立状态,这意味着它没有正确关闭。如果它被关闭,它的状态将及时等待。实际上,在整个程序结束后只泄漏一个打开的连接并不重要,而博客海报在修复后仍能看到性能提升;但是,从概念上讲,指责IDisposable并选择不处理它是不正确的。


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

    如果httpclient方法的流内容未完全读取,则应释放其返回类型。否则,在这些流被垃圾收集之前,CLR无法知道它们是否可以被关闭。

    • 如果要将数据读取到byte[](例如getbytearrayasync)或字符串中,则会读取所有数据,因此无需进行释放。
    • 其他重载将默认为读取高达2GB的流(httpCompletionOption为ResponseContentRead,httpClient.MaxResponseContentBufferSize默认为2GB)

    如果将httpCompletionOption设置为ResponseHeadersRead或响应大于2GB,则应清除。这可以通过对httpResponseMessage调用Dispose或对从httpResponseMessage内容获取的流调用Dispose/Close或完全读取内容来实现。

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


    如果要释放httpclient,可以将其设置为资源池。在应用程序的末尾,您将处理您的资源池。

    代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    // 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允许在测试中模拟


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

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

    然后,我在需要访问该API的服务类中对自定义HTTP客户机采用构造函数依赖关系。这解决了生存期问题,并且在连接池方面具有优势。

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


    我认为应该使用singleton模式来避免创建httpclient的实例并一直关闭它。如果您使用.NET 4.0,可以使用下面的示例代码。有关singleton模式的更多信息,请查看此处。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    class HttpClientSingletonWrapper : HttpClient
    {
        private static readonly Lazy<HttpClientSingletonWrapper> Lazy= new Lazy<HttpClientSingletonWrapper>(()=>new HttpClientSingletonWrapper());

        public static HttpClientSingletonWrapper Instance {get { return Lazy.Value; }}

        private HttpClientSingletonWrapper()
        {
        }
    }

    使用下面的代码。

    1
    var client = HttpClientSingletonWrapper.Instance;