当需要向某特定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. 可显示行号的log工具

    import android.util.Log; /** * (ExtendedLog=>ELog)可以记录行号,类名,方法名的Log工具 * * @author Fantouch */ pub ...

  2. [Android App]IFCTT,即:If Copy Then That,一个基于IFTTT的"This"实现

    IFCTT,即:If Copy Then That,是一个基于IFTTT(If This Then That)的"This"实现,它打通了"用户手机端操作"与& ...

  3. php验证码--图片

    这里我们介绍图片验证码的制作,有关字符验证码能够參考下面文章: 点击打开链接 图片验证码的制作分三步: 1.制作图片库 2.随机选取一张图片 3.输出图片内容 代码例如以下(这里为了方便我直接用的本地 ...

  4. C#-MVC开发微信应用(2)--微信消息的处理和应答

    微信应用使用场景和商机很多,所以这也是一个技术的方向,因此,有空研究下.学习下微信的相关开发,也就成为SNF完善的必要条件了.本系列文章希望从一个循序渐进的角度上,全面介绍微信的相关开发过程和相关经验 ...

  5. 第一部分:开发前的准备-第三章 Application 基本原理

    第3章 应用程序基本原理 首先我们需要强调一下Android 应用程序是用java写的.Android SDK工具编译代码并把资源文件和数据打包成一个文件.这个名字的扩展名是.APK.要在androi ...

  6. 第三部分:Android 应用程序接口指南---第二节:UI---第十二章 自定义组件

    第12章 自定义组件 Android平台提供了一套完备的.功能强大的组件化模型用于搭建用户界面,这套组件化模型以View和 ViewGroup这两个基础布局类为基础.平台本身已预先实现了多种用于构建界 ...

  7. Atitit 数据库 标准库  sdk 函数库 编程语言 mysql oracle  attilax总结

    Atitit 数据库 标准库  sdk 函数库 编程语言 mysql oracle  attilax总结 1.1. 常见的编程语言以及数据库 sql内部函数库标准化库一般有以下api1 1.2. 各个 ...

  8. golang slice

    golang 在for range一个slice时,会读出其cap长度.在for的过程中,即使动态append该slice,最终for也会在第一次读取的cap长度处停止. package main i ...

  9. [firefox] Scrapbook Plus的改进版Scrapbook X

    我在两年前的博文<Firefox上一些我用于知识管理的扩展> 里面提到过我在用Scrapbook Plus这个Firefox扩展, 用它来撷取网页构建自己的知识库(可以加标注.可以搜索.可 ...

  10. 推荐几个Windows工具软件: Stickies - 桌面贴

    主页: http://www.zhornsoftware.co.uk/stickies/index.html Stickies work like Post-it notes for your PC. ...