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

WebAPI 客户端的 HttpClient生命周期应该是多少?
对于多个调用,使用一个 HttpClient实例是否更好?

每个请求创建和处理一个 HttpClient的开销是多少,如下面的例子(取自 http://www.asp.net/web-api/overview/web-api-clients/calling-a-web-api-from-a-net-client) :

using (var client = new HttpClient())
{
client.BaseAddress = new Uri("http://localhost:9000/");
client.DefaultRequestHeaders.Accept.Clear();
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));


// New code:
HttpResponseMessage response = await client.GetAsync("api/products/1");
if (response.IsSuccessStatusCode)
{
Product product = await response.Content.ReadAsAsync<Product>();
Console.WriteLine("{0}\t${1}\t{2}", product.Name, product.Price, product.Category);
}
}
88948 次浏览

即使跨多个线程,HttpClient也是 设计用于重复使用多个呼叫HttpClientHandler具有用于跨调用重用的凭据和 Cookie。拥有一个新的 HttpClient实例需要重新设置所有这些东西。 此外,DefaultRequestHeaders属性包含用于多个调用的属性。必须在每个请求上重置这些值就失去了意义。

HttpClient的另一个主要优点是能够将 HttpMessageHandlers添加到请求/响应管道中,以应用横切关注点。它们可以用于日志记录、审计、节流、重定向处理、离线处理和捕获度量。各种各样的东西。如果在每个请求上创建一个新的 HttpClient,那么需要在每个请求上设置所有这些消息处理程序,并且还需要以某种方式提供这些处理程序的请求之间共享的任何应用程序级状态。

越多地使用 HttpClient的特性,就越能看到重用现有实例是有意义的。

然而,最大的问题是,在我看来,当一个 HttpClient类被释放时,它会释放 HttpClientHandler,然后它会强制关闭由 ServicePointManager管理的连接池中的 TCP/IP连接。这意味着每个带有新 HttpClient的请求都需要重新建立一个新的 TCP/IP连接。

在我的测试中,在局域网上使用普通 HTTP 对性能的影响可以忽略不计。我怀疑这是因为存在一个底层 TCP 保存区,即使在 HttpClientHandler试图关闭连接时,它也保持连接处于打开状态。

在互联网上的请求中,我看到了一个不同的故事。由于每次都必须重新打开请求,我已经看到了40% 的性能损失。

我怀疑对 HTTPS连接的攻击会更严重。

我的建议是,对于您连接到的每个不同的 API,应该使用 在应用程序的生存期内保留 HttpClient 的实例

如果您希望您的应用程序可伸缩,那么差异是巨大的!根据负载的不同,您将看到非常不同的性能数字。正如 Darrel Miller 提到的,HttpClient 被设计为跨请求重用。BCL 团队的编写人员证实了这一点。

我最近的一个项目是帮助一个非常大和著名的在线电脑零售商扩大黑色星期五/假日流量的一些新系统。我们在使用 HttpClient 时遇到了一些性能问题。因为它实现了 IDisposable,所以开发人员做了您通常会做的事情: 创建一个实例并将其放在 using()语句中。一旦我们开始负载测试的应用程序带来了它的膝盖服务器-是的,服务器不只是应用程序。原因是 HttpClient 的每个实例都在服务器上打开一个端口。由于 GC 的终止是不确定的,而且您正在处理跨多个 OSI 层的计算机资源,因此关闭网络端口可能需要一段时间。事实上,WindowsOS本身关闭一个端口最多需要20秒(每个微软)。我们打开端口的速度比关闭服务器端口的速度还要快——服务器端口的耗尽使得中央处理器达到了100% 。我的解决办法是将 HttpClient 更改为静态实例,从而解决了这个问题。是的,它是一种可任意使用的资源,但是性能上的差异远远超过了任何开销。我鼓励您做一些负载测试,看看您的应用程序是如何工作的。

您还可以在 WebAPI 指南页面上查看文档和示例,地址是 Https://www.asp.net/web-api/overview/advanced/calling-a-web-api-from-a-net-client

请特别注意以下呼吁:

HttpClient 的目的是实例化一次,并在应用程序的整个生命周期中重用。特别是在服务器应用程序中,为每个请求创建一个新的 HttpClient 实例将耗尽重负载下可用的套接字数量。这将导致 SocketException 错误。

如果您发现需要使用具有不同标头、基地址等的静态 HttpClient,则需要手动创建 HttpRequestMessage并在 HttpRequestMessage上设置这些值。然后,使用 HttpClient:SendAsync(HttpRequestMessage requestMessage, ...)

更新.NET 核心 : 你应该通过依赖注入使用 IHttpClientFactory来创建 HttpClient实例。它将为您管理生存期,您不需要显式地释放它。在 ASP.NET Core 中使用 IHttpClientFactory 发出 HTTP 请求

与大容量网站相关,但不直接与 HttpClient 相关。下面是我们所有服务中的代码片段。

        // number of milliseconds after which an active System.Net.ServicePoint connection is closed.
const int DefaultConnectionLeaseTimeout = 60000;


ServicePoint sp =
ServicePointManager.FindServicePoint(new Uri("http://<yourServiceUrlHere>"));
sp.ConnectionLeaseTimeout = DefaultConnectionLeaseTimeout;

来自 https://msdn.microsoft.com/query/dev14.query?appId=Dev14IDEF1&l=EN-US&k=k(System.Net.ServicePoint.ConnectionLeaseTimeout);k(TargetFrameworkMoniker-.NETFramework,Version%3Dv4.5.2);k(DevLang-csharp)&rd=true

”可以使用此属性确保 ServicePoint 对象的活动连接不会无限期地保持打开状态。此属性适用于应定期删除和重新建立连接的场景,如负载平衡场景。

默认情况下,当 KeepAlive 对于请求为 true 时,MaxIdleTime 属性设置由于不活动而关闭 ServicePoint 连接的超时值。如果 ServicePoint 具有活动连接,则 MaxIdleTime 没有效果,连接将无限期地保持打开状态。

当 ConnectionLeaseTimeout 属性设置为 -1以外的值,并且在指定的时间过去后,通过在请求中将 KeepAlive 设置为 false,在为请求提供服务之后关闭活动的 ServicePoint 连接。 设置此值会影响 ServicePoint 对象管理的所有连接。”

当您有一个 CDN 或其他端点后面的服务需要故障转移时,这个设置可以帮助调用者跟随您到达新的目的地。在此示例中,故障转移60秒后,所有调用方都应重新连接到新端点。它确实要求您了解您的依赖服务(您调用的那些服务)及其端点。

你也可以参考 Simon Timms 的这篇博文: https://aspnetmonsters.com/2016/08/2016-08-27-httpclientwrong/

HttpClient是不同的。虽然它实现了 IDisposable接口,但它实际上是一个共享对象。这意味着在封面下它是可重入的)和线程安全的。您应该在应用程序的整个生命周期中共享一个 HttpClient实例,而不是为每次执行创建一个新的 HttpClient实例。我们来看看为什么。

正如其他答案所述,HttpClient是用于重用的。但是,在多线程应用程序中重用单个 HttpClient实例意味着您不能更改其有状态属性(如 BaseAddressDefaultRequestHeaders)的值(因此您只能在整个应用程序中使用它们,如果它们是常量的话)。

绕过这个限制的一种方法是使用一个类来包装 HttpClient,该类复制所有您需要的 HttpClient方法(GetAsyncPostAsync等) ,并将它们委托给一个单例 HttpClient。然而,这是相当乏味的(您也需要包装 扩展方法) ,幸运的是,还有别的办法-继续创建新的 HttpClient实例,但是重用底层的 HttpClientHandler。只要确保你不会处理掉联络人:

HttpClientHandler _sharedHandler = new HttpClientHandler(); //never dispose this
HttpClient GetClient(string token)
{
//client code can dispose these HttpClient instances
return new HttpClient(_sharedHandler, disposeHandler: false)
{
DefaultRequestHeaders =
{
Authorization = new AuthenticationHeaderValue("Bearer", token)
}
};
}

需要指出的一点是,没有一个“不使用”的博客注意到,您需要考虑的不仅仅是 BaseAddress 和 DefaultHeader。一旦您使 HttpClient 成为静态的,就会有跨请求传输的内部状态。例如: 你用 HttpClient 对第三方进行身份验证以获得一个 FedAuth 令牌(忽略为什么不使用 OAuth/OWIN/etc) ,响应消息有一个针对 FedAuth 的 Set-Cookie 头,这被添加到你的 HttpClient 状态。下一个登录到 API 的用户将发送最后一个人的 FedAuth cookie,除非您在每个请求中管理这些 cookie。

作为第一个问题,虽然这个类是可抛弃的,但是与 using语句一起使用它并不是最佳选择,因为即使在释放 HttpClient对象时,底层套接字也不会立即释放,这可能导致一个名为‘ sockets 枯竭的严重问题。

但是,当您将 HttpClient用作单例或静态对象时,可能会遇到第二个问题。在这种情况下,单例或静态 HttpClient不尊重 DNS的更改。

. net core中,你可以对 HttpClientFactory做同样的事情,比如:

public interface IBuyService
{
Task<Buy> GetBuyItems();
}
public class BuyService: IBuyService
{
private readonly HttpClient _httpClient;


public BuyService(HttpClient httpClient)
{
_httpClient = httpClient;
}


public async Task<Buy> GetBuyItems()
{
var uri = "Uri";


var responseString = await _httpClient.GetStringAsync(uri);


var buy = JsonConvert.DeserializeObject<Buy>(responseString);
return buy;
}
}

配置服务

services.AddHttpClient<IBuyService, BuyService>(client =>
{
client.BaseAddress = new Uri(Configuration["BaseUrl"]);
});

文件和例子在 给你