当需要向某特定URL地址发送HTTP请求并得到相应响应时,通常会用到HttpClient类。该类包含了众多有用的方法,可以满足绝大多数的需求。但是如果对其使用不当时,可能会出现意想不到的事情。

博客园官方团队就遇上过这样的问题,国外博主也记录过类似的情况,YOU'RE USING HTTPCLIENT WRONG AND IT IS DESTABILIZING YOUR SOFTWARE

究其缘由是一句看似正确的代码引起的:

using(var client = new HttpClient())

对象所占用资源应该确保及时被释放掉,但是,对于网络连接而言,这是错误的。

原因有二,网络连接是需要耗费一定时间的,频繁开启与关闭连接,性能会受影响;再者,开启网络连接时会占用底层socket资源,但在HttpClient调用其本身的Dispose方法时,并不能立刻释放该资源,这意味着你的程序可能会因为耗尽连接资源而产生预期之外的异常。

所以比较好的解决方法是延长HttpClient对象的使用寿命,比如对其建一个静态的对象:

private static HttpClient Client = new HttpClient();

但从程序员的角度来看,这样的代码或许不够优雅。

所以在.NET Core 2.1中引入了新的HttpClientFactory类。

它的用法很简单,首先是对其进行IoC的注册:

public void ConfigureServices(IServiceCollection services)
{
services.AddHttpClient();
services.AddMvc();
}

然后通过IHttpClientFactory创建一个HttpClient对象,之后的操作如旧,但不需要担心其内部资源的释放:

public class MyController : Controller
{
IHttpClientFactory _httpClientFactory; public MyController(IHttpClientFactory httpClientFactory)
{
_httpClientFactory = httpClientFactory;
} public IActionResult Index()
{
var client = _httpClientFactory.CreateClient();
var result = client.GetStringAsync("http://myurl/");
return View();
}
}

第一眼瞧去,可能不明白AddHttpClient方法与IHttpClientFactory有什么关系,但查到其源码后就能一目了然:

public static IServiceCollection AddHttpClient(this IServiceCollection services)
{
if (services == null)
{
throw new ArgumentNullException(nameof(services));
} services.AddLogging();
services.AddOptions(); //
// Core abstractions
//
services.TryAddTransient<HttpMessageHandlerBuilder, DefaultHttpMessageHandlerBuilder>();
services.TryAddSingleton<IHttpClientFactory, DefaultHttpClientFactory>(); //
// Typed Clients
//
services.TryAdd(ServiceDescriptor.Singleton(typeof(ITypedHttpClientFactory<>), typeof(DefaultTypedHttpClientFactory<>))); //
// Misc infrastructure
//
services.TryAddEnumerable(ServiceDescriptor.Singleton<IHttpMessageHandlerBuilderFilter, LoggingHttpMessageHandlerBuilderFilter>()); return services;
}

它的内部为IHttpClientFactory接口绑定了DefaultHttpClientFactory类。

再看IHttpClientFactory接口中关键的CreateClient方法:

public HttpClient CreateClient(string name)
{
if (name == null)
{
throw new ArgumentNullException(nameof(name));
} var entry = _activeHandlers.GetOrAdd(name, _entryFactory).Value;
var client = new HttpClient(entry.Handler, disposeHandler: false); StartHandlerEntryTimer(entry); var options = _optionsMonitor.Get(name);
for (var i = 0; i < options.HttpClientActions.Count; i++)
{
options.HttpClientActions[i](client);
} return client;
}

HttpClient的创建不再是简单的new HttpClient(),而是传入了两个参数:HttpMessageHandler handler与bool disposeHandler。disposeHandler参数为false值时表示要重用内部的handler对象。handler参数则从上一句的代码可以看出是以name为键值从一字典中取出,又因为DefaultHttpClientFactory类是通过TryAddSingleton方法注册的,也就意味着其为单例,那么这个内部字典便是唯一的,每个键值对应的ActiveHandlerTrackingEntry对象也是唯一,该对象内部中包含着handler。

下一句代码StartHandlerEntryTimer(entry); 开启了ActiveHandlerTrackingEntry对象的过期计时处理。默认过期时间是2分钟。

internal void ExpiryTimer_Tick(object state)
{
var active = (ActiveHandlerTrackingEntry)state; // The timer callback should be the only one removing from the active collection. If we can't find
// our entry in the collection, then this is a bug.
var removed = _activeHandlers.TryRemove(active.Name, out var found);
Debug.Assert(removed, "Entry not found. We should always be able to remove the entry");
Debug.Assert(object.ReferenceEquals(active, found.Value), "Different entry found. The entry should not have been replaced"); // At this point the handler is no longer 'active' and will not be handed out to any new clients.
// However we haven't dropped our strong reference to the handler, so we can't yet determine if
// there are still any other outstanding references (we know there is at least one).
//
// We use a different state object to track expired handlers. This allows any other thread that acquired
// the 'active' entry to use it without safety problems.
var expired = new ExpiredHandlerTrackingEntry(active);
_expiredHandlers.Enqueue(expired); Log.HandlerExpired(_logger, active.Name, active.Lifetime); StartCleanupTimer();
}

先是将ActiveHandlerTrackingEntry对象传入新的ExpiredHandlerTrackingEntry对象。

public ExpiredHandlerTrackingEntry(ActiveHandlerTrackingEntry other)
{
Name = other.Name; _livenessTracker = new WeakReference(other.Handler);
InnerHandler = other.Handler.InnerHandler;
}

在其构造方法内部,handler对象通过弱引用方式关联着,不会影响其被GC释放。

然后新建的ExpiredHandlerTrackingEntry对象被放入专用的队列。

最后开始清理工作,定时器的时间间隔设定为每10秒一次。

internal void CleanupTimer_Tick(object state)
{
// Stop any pending timers, we'll restart the timer if there's anything left to process after cleanup.
//
// With the scheme we're using it's possible we could end up with some redundant cleanup operations.
// This is expected and fine.
//
// An alternative would be to take a lock during the whole cleanup process. This isn't ideal because it
// would result in threads executing ExpiryTimer_Tick as they would need to block on cleanup to figure out
// whether we need to start the timer.
StopCleanupTimer(); try
{
if (!Monitor.TryEnter(_cleanupActiveLock))
{
// We don't want to run a concurrent cleanup cycle. This can happen if the cleanup cycle takes
// a long time for some reason. Since we're running user code inside Dispose, it's definitely
// possible.
//
// If we end up in that position, just make sure the timer gets started again. It should be cheap
// to run a 'no-op' cleanup.
StartCleanupTimer();
return;
} var initialCount = _expiredHandlers.Count;
Log.CleanupCycleStart(_logger, initialCount); var stopwatch = ValueStopwatch.StartNew(); var disposedCount = 0;
for (var i = 0; i < initialCount; i++)
{
// Since we're the only one removing from _expired, TryDequeue must always succeed.
_expiredHandlers.TryDequeue(out var entry);
Debug.Assert(entry != null, "Entry was null, we should always get an entry back from TryDequeue"); if (entry.CanDispose)
{
try
{
entry.InnerHandler.Dispose();
disposedCount++;
}
catch (Exception ex)
{
Log.CleanupItemFailed(_logger, entry.Name, ex);
}
}
else
{
// If the entry is still live, put it back in the queue so we can process it
// during the next cleanup cycle.
_expiredHandlers.Enqueue(entry);
}
} Log.CleanupCycleEnd(_logger, stopwatch.GetElapsedTime(), disposedCount, _expiredHandlers.Count);
}
finally
{
Monitor.Exit(_cleanupActiveLock);
} // We didn't totally empty the cleanup queue, try again later.
if (_expiredHandlers.Count > 0)
{
StartCleanupTimer();
}
}

上述方法核心是判断是否handler对象已经被GC,如果是的话,则释放其内部资源,即网络连接。

回到最初创建HttpClient的代码,会发现并没有传入任何name参数值。这是得益于HttpClientFactoryExtensions类的扩展方法。

public static HttpClient CreateClient(this IHttpClientFactory factory)
{
if (factory == null)
{
throw new ArgumentNullException(nameof(factory));
} return factory.CreateClient(Options.DefaultName);
}

Options.DefaultName的值为string.Empty。

DefaultHttpClientFactory缺少无参数的构造方法,唯一的构造方法需要传入多个参数,这也意味着构建它时需要依赖其它一些类,所以目前只适用于在ASP.NET程序中使用,还无法应用到诸如控制台一类的程序,希望之后官方能够对其继续增强,使得应用范围变得更广。

public DefaultHttpClientFactory(
IServiceProvider services,
ILoggerFactory loggerFactory,
IOptionsMonitor<HttpClientFactoryOptions> optionsMonitor,
IEnumerable<IHttpMessageHandlerBuilderFilter> filters)

.NET Core开发日志——HttpClientFactory的更多相关文章

  1. .NET Core开发日志——Entity Framework与PostgreSQL

    Entity Framework在.NET Core中被命名为Entity Framework Core.虽然一般会用于对SQL Server数据库进行数据操作,但其实它还支持其它数据库,这里就以Po ...

  2. .NET Core开发日志——RequestDelegate

    本文主要是对.NET Core开发日志--Middleware的补遗,但是会从看起来平平无奇的RequestDelegate开始叙述,所以以其作为标题,也是合情合理. RequestDelegate是 ...

  3. C#实现多级子目录Zip压缩解压实例 NET4.6下的UTC时间转换 [译]ASP.NET Core Web API 中使用Oracle数据库和Dapper看这篇就够了 asp.Net Core免费开源分布式异常日志收集框架Exceptionless安装配置以及简单使用图文教程 asp.net core异步进行新增操作并且需要判断某些字段是否重复的三种解决方案 .NET Core开发日志

    C#实现多级子目录Zip压缩解压实例 参考 https://blog.csdn.net/lki_suidongdong/article/details/20942977 重点: 实现多级子目录的压缩, ...

  4. .NET Core开发日志——从搭建开发环境开始

    .NET Core自2016年推出1.0版本开始,到目前已是2.1版本,在其roadmap计划里明年更会推出3.0版本,发展不可不谓之迅捷.不少公司在经过一个谨慎的观望期后,也逐步开始将系统升级至最新 ...

  5. .NET Core开发日志——结构化日志

    在.NET生态圈中,最早被广泛使用的日志库可能是派生自Java世界里的Apache log4net.而其后来者,莫过于NLog.Nlog与log4net相比,有一项较显著的优势,它支持结构化日志. 结 ...

  6. .NET Core开发日志——Edge.js

    最近在项目中遇到这样的需求:要将旧有系统的一部分业务逻辑集成到新的自动化流程工具中.这套正在开发的自动化工具使用的是C#语言,而旧有系统的业务逻辑则是使用AngularJS在前端构建而成.所以最初的考 ...

  7. .NET Core开发日志——Linux版本的SQL Server

    SQL Server 2017版本已经可以在Linux系统上安装,但我在尝试.NET Core跨平台开发的时候使用的是Mac系统,所以这里记录了在Mac上安装SQL Server的过程. 最新的SQL ...

  8. .NET Core开发日志——Model Binding

    ASP.NET Core MVC中所提供的Model Binding功能简单但实用,其主要目的是将请求中包含的数据映射到action的方法参数中.这样就避免了开发者像在Web Forms时代那样需要从 ...

  9. .NET Core开发日志——简述路由

    有过ASP.NET或其它现代Web框架开发经历的开发者对路由这一名字应该不陌生.如果要用一句话解释什么是路由,可以这样形容:通过对URL的解析,指定相应的处理程序. 回忆下在Web Forms应用程序 ...

随机推荐

  1. V-rep学习笔记:转动关节2

    Torque or force mode: in this mode, the joint is simulated by the dynamics module, if and only if it ...

  2. 解决nginx access日志中400 bad request 错误(转)

    在access.log中有大量400错误,并以每天几百M的速度增加,占用大量空间.tail -f /opt/nginx/logs/access.log 116.236.228.180 - - [15/ ...

  3. (原)CNN中的卷积、1x1卷积及在pytorch中的验证

    转载请注明处处: http://www.cnblogs.com/darkknightzh/p/9017854.html 参考网址: https://pytorch.org/docs/stable/nn ...

  4. google全球地址

    IP Addresses of Google Global Cache www.kookle.co.nr Bulgaria 93.123.23.1 93.123.23.2 93.123.23.3 93 ...

  5. 2.翻译系列:为EF Code-First设置开发环境(EF 6 Code-First系列)

    原文链接:http://www.entityframeworktutorial.net/code-first/setup-entity-framework-code-first-environment ...

  6. linux ssh

    SSH 是建立在应用层和传输层基础上的一种安全协议. SSH传输数据是加密的,可以有效防止传输过程被截取数据保障安全. SSH的数据是经过压缩的,所以可以加快传输的速度 1. 首先查看一下当前linu ...

  7. 审核流(1)SNF.WorkFlow审核流简介--SNF快速开发平台3.1

    本项目是的在Spring.Net.Framework 平台之上进行研发.SNF.WorkFlow审核流是一款完全自主知识产权研发的为软件项目. 审核流基本概念:什么是审核流? 审核流:两个或两个以上的 ...

  8. AJAX技术之DWR框架

    DWR(Direct Web Remoting)是一个用于改善web页面与Java类交互的远程服务器端Ajax开源框架,可以帮助开发人员开发包含AJAX技术的网站.它可以允许在浏览器里的代码使用运行在 ...

  9. 《网络编程》IPv4 与 IPv6 相互操作

    前言 因为互联网终端不断添加.IPv4 地址长度(32位)已不可以满足要求.所以出现了 IPv6地址(128位).可是现有应用程序大部分还是採用 IPv4 地址形式,所以必须解决 IPv4 与 IPv ...

  10. 入门:移动APP中的各种导航

    即使是移动应用界面的原型设计,导航的形式也可以多种多样.尽管尺寸小,又必须紧凑排列大量数据,它们似乎受到了紧密的约束,但依然有着形形色色的选择. 人们曾经一度只会考虑一种形式——流行且广泛使用的垂直导 ...