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

. 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实例的操作。我已经使用了HttpClient的所有实例 在我的应用程序上使用声明,我认为这是正确的方式 因为HttpClient实现了IDisposable接口。我是在 正确的道路?< / p >

他的回答是:

一般来说,这是正确的,尽管你必须小心 "using"和async,因为它们在。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. 如果他们要求,或者这是一种推荐的做法,那么微软的例子是否具有误导性或不安全?

201511 次浏览

在我的理解中,调用Dispose()只有当它锁定你以后需要的资源(比如一个特定的连接)时才有必要。它总是推荐来释放你不再使用的资源,即使你不再需要它们,只是因为你不应该一般持有你不使用的资源(双关语)。

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

但是,一般来说,当一个类实现了IDisposable时,我们的理解是,只要你完全准备好并能够执行,你就应该Dispose()它的实例。我认为在像HttpClient这样的情况下尤其如此,其中没有明确地记录资源或连接是否被保持/打开。在连接将被再次重用的情况下,你会想要放弃Dipose()ing它——在这种情况下你还没有“完全准备好”。

< p >参见: IDisposable。处理方法何时调用Dispose

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

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

参考达雷尔·米勒的博客文章和相关SO帖子:HttpClient爬行导致内存泄漏

我还强烈建议你阅读用ASP设计可进化的Web api的HttpClient章节。净< / em >,以了解在引子下发生了什么,特别是这里引用的“生命周期”部分:

虽然HttpClient不间接实现IDisposable 接口,HttpClient的标准用法是不处理它 每次请求之后。HttpClient对象的存续期是as 只要您的应用程序需要发出HTTP请求。有一个对象 跨多个请求存在可以为设置提供一个位置 DefaultRequestHeaders,防止您重新指定 比如CredentialCache和CookieContainer

甚至可以打开DotPeek。

在我的例子中,我在一个实际执行服务调用的方法中创建了一个HttpClient。喜欢的东西:

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

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

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

我认为应该使用单例模式来避免创建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;

在典型使用(responses<2GB)中,没有必要Dispose HttpResponseMessages。

如果HttpClient方法的流内容没有被完全读取,那么它们的返回类型应该被丢弃。否则,CLR没有办法知道这些流可以被关闭,直到它们被垃圾回收。

  • 如果你正在将数据读入字节[](例如GetByteArrayAsync)或字符串,所有的数据都被读取,所以不需要处理。
  • 其他重载将默认读取流到2GB (HttpCompletionOption是ResponseContentRead, HttpClient。MaxResponseContentBufferSize默认为2GB)

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

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

目前的答案有点令人困惑和误导,它们遗漏了一些重要的DNS含义。我会试着把情况总结清楚。

  1. 一般来说,大多数IDisposable对象理想情况下,当你用完它们时,应该把它们处理掉吗,特别是那些own命名/共享的操作系统资源HttpClient也不例外,因为正如Darrel米勒指出的那样,它分配取消令牌,并且请求/响应体可以是非托管流。
  2. 然而,HttpClient的最佳实践说你应该创建一个实例并尽可能地重用它(在多线程场景中使用它的线程安全的成员)。因此,在大多数情况下你永远不会丢弃它,因为你会一直需要它
  3. “永远”重用同一个HttpClient的问题是底层HTTP连接可能会对最初的DNS解析IP保持打开状态,不管DNS是否更改. conf。在像蓝绿部署和dns故障切换这样的场景中,这可能是一个问题。处理这个问题有多种方法,最可靠的方法是在DNS发生更改后服务器发送Connection:close报头。另一种可能涉及到在客户端回收HttpClient,要么周期性地回收,要么通过某种机制来了解DNS的变化。参见https://github.com/dotnet/corefx/issues/11224获取更多信息(我建议在盲目使用链接博客文章中建议的代码之前仔细阅读它)。

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

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,你可以把它设置为一个资源池。在应用程序的末尾,分配资源池。

代码:

// 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("base url"))。

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

在构造函数中使用依赖注入使管理HttpClient的生命周期变得更容易——将生命周期管理置于需要它的代码之外,并使它在以后的日期中容易更改。

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

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

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

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

长回答:下面两个语句都是正确的,并且可以同时实现:

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

而且它们彼此之间并不必然冲突。这只是一个你如何组织你的代码来重用HttpClient并且仍然正确地处理它的问题。

引用自我的另一个答案的偶数再回答:

见到人不是巧合 在一些博客文章中责备HttpClientIDisposable接口 使它们倾向于使用using (var client = new HttpClient()) {...}模式 然后导致套接字处理程序耗尽的问题 我相信这可以归结为一个没有说出口的(错误的)概念: “一个IDisposable对象预期是短命的” . < / p >

然而,当我们以这种风格编写代码时,它确实看起来像一个短命的东西:

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();
}
}
}
有了这个新的理解,现在我们重新审视那篇博文, 我们可以清楚地注意到,"fix"只初始化HttpClient一次,但从不释放它, 这就是为什么我们可以从它的netstat输出中看到, 连接保持在ESTABLISHED状态,这意味着它还没有被正确关闭。 如果它是关闭的,它的状态将改为TIME_WAIT。 实际上,在整个程序结束后只泄漏一个打开的连接并不是什么大问题, 并且博客海报仍然看到修复后的性能增益; 但是,责怪IDisposable并选择不处理它在概念上是不正确的

由于这里似乎还没有人提到它,在. net Core >=2.1和. net 5.0+中管理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博客中的系列文章,了解更多功能。

请阅读下面我对一个非常类似的问题的回答。应该清楚的是,你应该将HttpClient实例视为单例,并在请求之间重用。

在WebAPI客户端中每次调用创建一个新的HttpClient的开销是什么?< / >