写在前面

创建HttpClient实例的时候,在内部会创建HttpMessageHandler链,我们知道HttpMessageHandler是负责建立连接的抽象处理程序,所以HttpClient的维护实际上就是维护HttpMessageHandler的使用,释放HttpClient并不会及时释放连接,而通常情况下一般是创建全局使用的HttpClient实例,以减少重复连接的次数。当然这种方式所带来的的弊端也是显而易见的,因为当前的HttpClient实例所指向的服务器发生问题或者DNS发生变更,那么该实例是无法做到自动更新指向的。

以下为运行其流程图:

HttpClientFactory自.NET Core 2.1引入,可以认为它是一个配置和创建HttpClient的中心化,.NET Core通过引入HttpClientFactory用于自动化维护HttpMessageHandler池及其生命周期,避免在手动管理 HttpClient生存期时出现的常见 DNS 问题。在默认情况下MessageHandler的活跃状态是两分钟,也就是说,在两分钟后,就可以为HttpClient实例重新定位到正确的主机上。

本文的讨论思路将从我们能看到的代码开始一步步深入。

详细介绍

HttpClientFactory的功能主要位于Microsoft.Extensions.Http包中,它已经默认包含在Microsoft.AspNetCore.App元包中。针对HttpClientFactory的处理涉及到IHttpClientBuilder、IHttpClientFactory、IHttpMessageHandlerFactory、ITypedHttpClientFactory这几大接口,以下将分别做讨论。

services.AddHttpClient()

我们在创建或者配置HttpClient对象的时候,会在ConfigureServices方法中增加services.AddHttpClient(),即可注册IHttpClientFactory。

这段代码位于Microsoft.Extensions.DependencyInjection.HttpClientFactoryServiceCollectionExtensions中,它会初始化相关信息并注册到IServiceCollection中,这些信息包括日志、选项、核心抽象功能、类型客户端以及其他基础设施功能。

需要注意的是,在核心抽象功能中,DefaultHttpClientFactory是单例模式的,其所继承的接口对象的获取也是单例的,而HttpMessageHandlerBuilder注册方式确是每一次GetService的时候都会创建一个新的HttpMessageHandlerBuilder实例。

以下为services.AddHttpClient()的源代码,其中标红部分为核心抽象功能的注册:

   1:  public static IServiceCollection AddHttpClient(this IServiceCollection services)
   2:  {
   3:      if (services == null)
   4:      {
   5:          throw new ArgumentNullException(nameof(services));
   6:      }
   7:   
   8:      services.AddLogging();
   9:      services.AddOptions();
  10:     

11: services.TryAddTransient<HttpMessageHandlerBuilder, DefaultHttpMessageHandlerBuilder>(); 12: services.AddSingleton<DefaultHttpClientFactory>(); 13: services.TryAddSingleton<IHttpClientFactory>(serviceProvider => serviceProvider.GetRequiredService<DefaultHttpClientFactory>()); 14: services.TryAddSingleton<IHttpMessageHandlerFactory>(serviceProvider => serviceProvider.GetRequiredService<DefaultHttpClientFactory>());

  15:      
  16:      services.TryAdd(ServiceDescriptor.Transient(typeof(ITypedHttpClientFactory<>), typeof(DefaultTypedHttpClientFactory<>)));
  17:      services.TryAdd(ServiceDescriptor.Transient(typeof(DefaultTypedHttpClientFactory<>.Cache), typeof(DefaultTypedHttpClientFactory<>.Cache)));
  18:      
  19:      services.TryAddEnumerable(ServiceDescriptor.Singleton<IHttpMessageHandlerBuilderFilter, LoggingHttpMessageHandlerBuilderFilter>());
  20:   
  21:      return services;
  22:  }


DefaultHttpClientFactory

DefaultHttpClientFactory是一个用internal修饰的类,意味着该类只能在在其内部使用。它继承了IHttpClientFactory、IHttpMessageHandlerFactory这两个接口。由此可见,DefaultHttpClientFactory实例的创建被拆成了两种行为。

IHttpClientFactory的定位是一个抽象工厂,可以为指定名称的HttpClient实例创建自定义配置,它只有一个方法,HttpClient CreateClient(string name)。

IHttpMessageHandlerFactory的定位也是一个抽象工厂,它为指定名称的HttpMessageHandler实例创建自定义配置,它只有一个方法,HttpMessageHandler CreateHandler(string name)。

我们先看一下这两个方法的实现,会觉得很有意思

   1:  public HttpClient CreateClient(string name)
   2:  {
   3:      if (name == null)
   4:      {
   5:          throw new ArgumentNullException(nameof(name));
   6:      }
   7:   
   8:      var handler = CreateHandler(name);
   9:      var client = new HttpClient(handler, disposeHandler: false);
  10:   
  11:      var options = _optionsMonitor.Get(name);
  12:      for (var i = 0; i < options.HttpClientActions.Count; i++)
  13:      {
  14:          options.HttpClientActions[i](client);
  15:      }
  16:   
  17:      return client;
  18:  }
  19:   
  20:  public HttpMessageHandler CreateHandler(string name)
  21:  {
  22:      if (name == null)
  23:      {
  24:          throw new ArgumentNullException(nameof(name));
  25:      }
  26:   
  27:      var entry = _activeHandlers.GetOrAdd(name, _entryFactory).Value;
  28:   
  29:      StartHandlerEntryTimer(entry);
  30:   
  31:      return entry.Handler;
  32:  }

可以看到,我们通过名称查找HttpClient对象的时候,也会依照该名称以GetOrAdd方式去查找相应的HttpMessageHandler对象,也就说HttpClient对象和HttpMessageHandler对象可以通过名称关联起来。

需要注意的时候在调用CreateHandler方法的时候会调用StartHandlerEntryTimer方法,这个方法是干嘛的呢,他维护着定时器。该方法位于Microsoft.Extensions.Http.ActiveHandlerTrackingEntry中,我们将此类视为是一个不可变的(当然,其内部的定时器是变化的),为“到期”池创建一个可以显著简化线程需求的新对象。

除了这两个方法外,我们要需要注意DefaultHttpClientFactory对HttpMessageHandler的管理功能。DefaultHttpClientFactory内部维护者一个定时器和两个HttpMessageHandler对象集合,这两个集合分别是ActiveHandler和ExpiredHandler。内部定时器会定期从ExpiredHandler集合中扫描并清理无效的 HttpMessageHandler对象。

ActiveHandler集合的增加是在调用CreateHandler方法时增加的,其移除是在回调的时候移除,这个移除入口也只有这一处。

ExpiredHandler集合的增加也是在调用CreateHandler方法时,通过内部的一个回调机制增加的,其移除通过定时器定期扫描来实现的。这处需要注意的是,ExpiredHandlerTrackingEntry这个类中有一个属性,代码如下:

   1:  private readonly WeakReference _livenessTracker;

   1:  public bool CanDispose => !_livenessTracker.IsAlive;

通过WeakReference 类型的变量来标识该HttpMessageHandler对象是否应该被从集合中移除。

定时器一般是个比较消耗资源,而且一旦用不好,就会引发线程问题,DefaultHttpClientFactory在处理定时器的时候,首先通过停止所有挂起的计时器,在清除后如果还需要继续处理无效HttpMessageHandler对象,将会重新启动计时器,虽然看似多余了点,但是比通过锁定整个清理机制来确定是否阻塞清理任何并启动定时器要好多了。

   1:  internal void CleanupTimer_Tick()
   2:  {
   3:      StopCleanupTimer();
   4:   
   5:      if (!Monitor.TryEnter(_cleanupActiveLock))
   6:      {
   7:          StartCleanupTimer();
   8:          return;
   9:      }
  10:   
  11:      try
  12:      {
  13:          var initialCount = _expiredHandlers.Count;
  14:          Log.CleanupCycleStart(_logger, initialCount);
  17:   
  18:          var disposedCount = 0;
  19:          //开始清理
  20:   
  21:          Log.CleanupCycleEnd(_logger, stopwatch.GetElapsedTime(), disposedCount, _expiredHandlers.Count);
  22:      }
  23:      finally
  24:      {
  25:          Monitor.Exit(_cleanupActiveLock);
  26:      }
  27:   
  28:      if (_expiredHandlers.Count > 0)
  29:      {
  30:          StartCleanupTimer();
  31:      }
  32:  }

以下为这两个队列的处理示意图:

.NET Core 3.0之深入源码理解HttpClientFactory(一)的更多相关文章

  1. .NET Core 3.0之深入源码理解HttpClientFactory(二)

      写在前面 上一篇文章讨论了通过在ConfigureServices中调用services.AddHttpClient()方法,并基于此进一步探讨了DefaultHttpClientFactory是 ...

  2. .NET Core 3.0之深入源码理解Startup的注册及运行

    原文:.NET Core 3.0之深入源码理解Startup的注册及运行   写在前面 开发.NET Core应用,直接映入眼帘的就是Startup类和Program类,它们是.NET Core应用程 ...

  3. .NET Core 3.0之深入源码理解Configuration(一)

    Configuration总体介绍 微软在.NET Core里设计出了全新的配置体系,并以非常灵活.可扩展的方式实现.从其源码来看,其运行机制大致是,根据其Source,创建一个Builder实例,并 ...

  4. .NET Core 3.0之深入源码理解Kestrel的集成与应用(一)

      写在前面 ASP.NET Core 的 Web 服务器默认采用Kestrel,这是一个基于libuv(一个跨平台的基于Node.js异步I/O库)的跨平台.轻量级的Web服务器. 在开始之前,先回 ...

  5. .NET Core 3.0之深入源码理解Kestrel的集成与应用(二)

      前言 前一篇文章主要介绍了.NET Core继承Kestrel的目的.运行方式以及相关的使用,接下来将进一步从源码角度探讨.NET Core 3.0中关于Kestrel的其他内容,该部分内容,我们 ...

  6. .NET Core 3.0之深入源码理解Host(二)

      写在前面 停了近一个月的技术博客,随着正式脱离996的魔窟,接下来也正式恢复了.本文从源码角度进一步讨论.NET Core 3.0 中关于Host扩展的一些技术点,主要讨论Long Run Pro ...

  7. .NET Core 3.0之深入源码理解ObjectPool(一)

    写在前面 对象池是一种比较常用的提高系统性能的软件设计模式,它维护了一系列相关对象列表的容器对象,这些对象可以随时重复使用,对象池节省了频繁创建对象的开销. 它使用取用/归还的操作模式,并重复执行这些 ...

  8. .NET Core 3.0之深入源码理解HealthCheck(一)

    写在前面 我们的系统可能因为正在部署.服务异常终止或者其他问题导致系统处于非健康状态,这个时候我们需要知道系统的健康状况,而健康检查可以帮助我们快速确定系统是否处于正常状态.一般情况下,我们会提供公开 ...

  9. .NET Core 3.0之深入源码理解Host(一)

    写在前面 ASP .NET Core中的通用主机构建器是在v2.1中引入的,应用在启动时构建主机,主机作为一个对象用于封装应用资源以及应用程序启动和生存期管理.其主要功能包括配置初始化(包括加载配置以 ...

随机推荐

  1. MinGW 编译 libsndfile-1.0.25(只要有 MSYS,./configure make make install 就行了)

    最近做的一个项目需要读写 wav 文件.在网上找到 libsndfile 刚好满足我的需要.但是编译的时候遇到了点小麻烦,这里记录一下编译的过程,免得下次再编译时忘记了. 因为是在编译完成若干天后写的 ...

  2. QT 文件拖放事件dropEvent和dragEnterEvent

    重载以下两个函数,可以实现将文本文件拖放进文本编辑器 void MainWindow::dragEnterEvent(QDragEnterEvent *event)//拖进事件 { if(event- ...

  3. C# 对字段忽略模型校验

    1.在if (!ModelState.IsValid)之前给字段赋值,然后TryUpdateModel()2.使用如下方法 public ActionResult Create([Bind(Exclu ...

  4. .net core 2.0 dbfirst 报 dotnet ef 未找到与命令“dotnet-ef”匹配的可执行文件的解决办法

    问题描述: 最近研究了一段时间.netcore dbfirst,新建类库用来生成实体模型,执行命令总是提示 未找到与命令“dotnet-ef”匹配的可执行文件,根据网上的解决办法引用 Microsof ...

  5. NSCache缓存怎么来的

    什么是NSCache NSCache主要用来存储临时数据(键值对),当内存资源不够时,系统会自动释放部分数据.它有三个特点: • NSCache为了保持不占用过多的系统内存,它有多种自动回收内存策略: ...

  6. Unity Shader 菲涅尔环境反射

    菲涅尔反射描述了一种光学现象,当光照到物体表面时,一部分发生反射,另一部分则进入物体内部,发生折射或散射:相比直接的反射和折射计算,菲涅尔反射更接近真实情况. 可用下面的等式近似计算这种反射效果: F ...

  7. Django之分页器组件

    class Pagination(object): def __init__(self,current_page,all_count,per_page_num=2,pager_count=11): & ...

  8. Sqoop 的基本使用

    目录 一.Sqoop 基本命令 1. 查看所有命令 2. 查看某条命令的具体使用方法 二.Sqoop 与 MySQL 1. 查询MySQL所有数据库 2. 查询指定数据库中所有数据表 三.Sqoop ...

  9. java源码解析之String类(三)

    上一节我们主要讲了String类的一些不是很常用的方法,其中需要掌握的如下,我就不再赘述了 public int length() public boolean isEmpty() public by ...

  10. Spring 之Aop实现日志记录

    Aop实现见代码,简单demo实现 package com.idcos.automate.config; import com.idcos.automate.dal.auto.dao.dcos.Dco ...