必须在请求之间处理HttpClient和HttpClientHandler吗

本文关键字:HttpClient HttpClientHandler 处理 之间 请求 | 更新日期: 2023-09-27 18:06:24

.Net Framework 4.5中的System.Net.Http.HttpClient和System.Net.HHttpClientHandler实现IDisposable(通过System.Net.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的重新发布
  • HttpClient的实际MSDN文档
  • BingTranslateSample
  • 谷歌地图示例
  • WorldBankSample

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

在检查了你的样品后,我发现你没有进行处理HttpClient实例上的操作。我已经使用了HttpClient的所有实例在我的应用程序上使用声明,我认为这是正确的方式因为HttpClient实现了IDisposable接口。我在吗正确的路径?

他的回答是:

总的来说,这是正确的,尽管你必须小心"使用"和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. 如果他们需要,或者如果这是一种建议的做法,微软的例子是误导性的还是不安全的?

必须在请求之间处理HttpClient和HttpClientHandler吗

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

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

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

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

尽管HttpClient确实间接实现了IDisposable接口,HttpClient的标准用法是不处理它在每次请求之后。HttpClient对象旨在作为只要您的应用程序需要发出HTTP请求。有物体的存在于多个请求之间,可以设置一个位置DefaultRequestHeaders并防止您重新指定像CredentialCache和CookieContainer这样的东西是HttpWebRequest所必需的。

甚至打开DotPeek。

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

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

由于似乎还没有人在这里提到过它,在中管理HttpClient和HttpClientHandler的新的最佳方法。NET核心>2.1和。NET 5.0+正在使用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博客中的一系列帖子,了解更多功能。

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

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

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

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

简短回答:不,当前接受的回答中的声明不准确:"普遍的共识是,您不(不(需要处理HttpClient"。

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

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

而且他们之间没有必要发生冲突。这只是一个如何组织代码以重用HttpClient并正确处理它的问题。

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

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

我相信这可以归结为一个不言而喻的(错误的(概念:"IDisposable对象应该是短暂的"。

然而,当我们以这种风格编写代码时,它看起来确实是一件短暂的事情:

using (var foo = new SomeDisposableObject())
{
    ...
}

IDisposable的官方文件从不提及CCD_ 19对象必须是短暂的。根据定义,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();
        }
    }
}

有了这个新的认识,现在我们重温一下那篇博客文章,我们可以清楚地注意到,"fix"初始化CCD_ 20一次,这就是为什么我们可以从它的netstat输出中看到,连接保持在建立状态,这意味着它还没有被正确地关闭。如果它已关闭,则其状态将改为TIME_WAIT。在实践中,在整个程序结束后只泄漏一个打开的连接并不是什么大不了的事,博客发帖者在修复后仍然看到性能的提高;但是,指责IDisposable并选择不处理它在概念上是不正确的。

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

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

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

在Azure工作者角色中,在重复调用此方法(不处理HttpClient(后,它最终会以SocketException失败(连接尝试失败(。

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

在典型的用法(responses<2GB(中,没有必要处理HttpResponseMessages。

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

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

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

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

如果您想处理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(新的Uri("基本url"((。

  • HttpClient作为一个接口,不能调用Dispose((
  • Dispose((将由垃圾回收器以延迟的方式调用。或者当程序通过其析构函数清理对象时
  • 使用弱引用+延迟清理逻辑,因此只要它经常被重用,它就会一直使用
  • 它只为传递给它的每个基本URL分配一个新的HttpClient。原因由Ohad Schneider解释如下。更改基本url时出现错误行为
  • HttpClientHandle允许在测试中进行模拟

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

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

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

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

不,不要在每个请求上创建一个新请求(即使您处理了旧请求(。您将导致服务器本身(而不仅仅是应用程序(因操作系统上网络级别的端口耗尽而崩溃!

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

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

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

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;