必须在请求之间处理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提供。(
所以我的问题是:
考虑到当前的实现(.NET Framework 4.5(,是否有必要在HttpClient和HttpClientHandler实例上调用Dispose((?澄清:我所说的"必要"是指如果不进行处理会产生任何负面后果,例如资源泄漏或数据损坏风险。
如果没有必要,那么既然他们实现了IDisposable,这是否是一种"良好的做法"?
如果有必要(或推荐(,上面提到的代码是否安全地实现了它(适用于.NET Framework 4.5(?
如果这些类不需要调用Dispose((,为什么它们被实现为IDisposable?
如果他们需要,或者如果这是一种建议的做法,微软的例子是误导性的还是不安全的?
普遍的共识是,您不需要(不应该(处理HttpClient。
许多密切参与其工作方式的人都说过这一点。
请参阅DarrelMiller的博客文章和相关的SO文章:HttpClient爬网导致内存泄漏,以供参考。
我还强烈建议您阅读使用ASP.NET设计可进化Web API中的HttpClient一章,了解引擎盖下发生的事情,特别是此处引用的"生命周期"部分:
尽管HttpClient确实间接实现了IDisposable接口,HttpClient的标准用法是不处理它在每次请求之后。HttpClient对象旨在作为只要您的应用程序需要发出HTTP请求。有物体的存在于多个请求之间,可以设置一个位置DefaultRequestHeaders并防止您重新指定像CredentialCache和CookieContainer这样的东西是HttpWebRequest所必需的。
甚至打开DotPeek。
当前的答案有点令人困惑和误导,并且它们缺少一些重要的DNS含义。我将努力总结清楚的情况。
- 一般来说,理想情况下,大多数
IDisposable
对象应该在处理完它们后进行处理,尤其是那些拥有命名/共享操作系统资源的对象。HttpClient
也不例外,因为正如Darrel-Miller所指出的,它分配取消令牌,并且请求/响应主体可以是非托管流 - 然而,HttpClient的最佳实践表明,您应该创建一个实例并尽可能多地重用它(在多线程场景中使用其线程安全成员(。因此,在大多数情况下,您永远不会简单地处理它,因为您将一直需要它
- "永远"重新使用同一个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"。
长答案:以下两种说法都是正确的,并且可以同时实现:
- 官方文档中引用了"HttpClient旨在实例化一次,并在应用程序的整个生命周期中重复使用">
- 假定/建议处置
IDisposable
对象
而且他们之间没有必要发生冲突。这只是一个如何组织代码以重用HttpClient
并正确处理它的问题。
一个甚至更长的答案引用自我的另一个答案:
见到人不是巧合在一些博客文章中指责HttpClient
的IDisposable
接口使他们倾向于使用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;