Dotnet Core IHttpClientFactory深度研究

微信扫一扫,分享到朋友圈

Dotnet Core IHttpClientFactory深度研究

今天,我们深度研究一下IHttpClientFactory。

一、前言

最早,我们是在Dotnet Framework中接触到 HttpClient

HttpClient 给我们提供了与 HTTP 交互的基本方式。但这个 HttpClient 在大量频繁使用时,也会给我们抛出两个大坑:一方面,如果我们频繁创建和释放 HttpClient 实例,会导致 Socket 套接字资源耗尽,原因是因为 Socket 关闭后的 TIME_WAIT 时间。这个问题不展开说,如果需要可以去查 TCP 的生命周期。而另一方面,如果我们创建一个 HttpClient 单例,那当被访问的 HTTPDNS 记录发生改变时,会抛出异常,因为 HttpClient 并不会允许这种改变。

现在,对于这个内容,有了更优的解决方案。

从Dotnet Core 2.1开始,框架提供了一个新的内容: IHttpClientFactory

IHttpClientFactory 用来创建 HTTP 交互的 HttpClient 实例。它通过将 HttpClient 的管理和用于发送内容的 HttpMessageHandler 链分离出来,来解决上面提到的两个问题。这里面,重要的是管理管道终端 HttpClientHandler 的生命周期,而这个就是实际连接的处理程序。

除此之外, IHttpClientFactory 还可以使用 IHttpClientBuilder 方便地来定制 HttpClient 和内容处理管道,通过前置配置创建出的 HttpClient ,实现诸如设置基地址或添加 HTTP 头等操作。

     为防止非授权转发,这儿给出本文的原文链接: https://www.cnblogs.com/tiger-wang/p/13752297.html

先来看一个简单的例子:

public void ConfigureServices(IServiceCollection services)
{
services.AddHttpClient("WangPlus", c =>
{
c.BaseAddress = new Uri("https://github.com/humornif");
})
.ConfigureHttpClient(c =>
{
c.DefaultRequestHeaders.Add("Accept", "application/vnd.github.v3+json");
c.DefaultRequestHeaders.Add("User-Agent", "HttpClientFactory-Sample");
});
}

在这个例子中,当调用 ConfigureHttpClient()AddHttpMessageHandler() 来配置 HttpClient 时,实际上是在向 IOptions 的实例 HttpClientFactoryOptions 添加配置。这个方法提供了非常多的配置选项,具体可以去看微软的文档,这儿不多说。

在类中使用 IHttpClientFactory 时,也是同样的方式:创建一个 IHttpClientFactory 的单例实例,然后调用 CreateClient(name) 创建一个具有名称 WangPlusHttpClient

看下面的例子:

public class MyService
{
private readonly IHttpClientFactory _factory;
public MyService(IHttpClientFactory factory)
{
_factory = factory;
}
public async Task DoSomething()
{
HttpClient client = _factory.CreateClient("WangPlus");
}
}

用法很简单。

下面,我们会针对 CreateClient() 进行剖析,来深入理解 IHttpClientFactory 背后的内容。

二、HttpClient & HttpMessageHandler的创建过程

CreateClient() 方法是与 IHttpClientFactory 交互的主要方法。

看一下 CreateClient() 的代码实现:

private readonly IOptionsMonitor<HttpClientFactoryOptions> _optionsMonitor
public HttpClient CreateClient(string name)
{
HttpMessageHandler handler = CreateHandler(name);
var client = new HttpClient(handler, disposeHandler: false);
HttpClientFactoryOptions options = _optionsMonitor.Get(name);
for (int i = 0; i < options.HttpClientActions.Count; i++)
{
options.HttpClientActions[i](client);
}
return client;
}

代码看上去很简单。首先通过 CreateHandler() 创建了一个 HttpMessageHandler 的处理管道,并传入要创建的 HttpClient 的名称。

有了这个处理管道,就可以创建 HttpClient 并传递给处理管道。这儿需要注意的是 disposeHandler:false ,这个参数用来保证当我们释放 HttpClient 的时候,处理管理不会被释放掉,因为 IHttpClientFactory 会自己完成这个管道的处理。

然后,从 IOptionsMonitor 的实例中获取已命名的客户机的 HttpClientFactoryOptions 。它来自 Startup.ConfigureServices() 中添加的 HttpClient 配置函数,并设置了 BaseAddressHeader 等内容。

最后,将 HttpClient 返回给调用者。

理解了这个内容,下面我们来看看 CreateHandler(name) 方法,研究一下 HttpMessageHandler 管道是如何创建的。

readonly ConcurrentDictionary<string, Lazy<ActiveHandlerTrackingEntry>> _activeHandlers;;
readonly Func<string, Lazy<ActiveHandlerTrackingEntry>> _entryFactory = (name) =>
{
return new Lazy<ActiveHandlerTrackingEntry>(() =>
{
return CreateHandlerEntry(name);
}, LazyThreadSafetyMode.ExecutionAndPublication);
};
public HttpMessageHandler CreateHandler(string name)
{
ActiveHandlerTrackingEntry entry = _activeHandlers.GetOrAdd(name, _entryFactory).Value;
entry.StartExpiryTimer(_expiryCallback);
return entry.Handler;
}

看这段代码: CreateHandler() 做了两件事:

  1. 创建或获取 ActiveHandlerTrackingEntry
  2. 开始一个计时器。

_activeHandlers 是一个 ConcurrentDictionary<> ,里面保存的是 HttpClient 的名称(例如上面代码中的 WangPlus )。这里使用 Lazy<> 是一个使 GetOrAdd() 方法保持线程安全的技巧。实际创建处理管道的工作在 CreateHandlerEntry 中,它创建了一个 ActiveHandlerTrackingEntry

ActiveHandlerTrackingEntry 是一个不可变的对象,包含 HttpMessageHandlerIServiceScope 注入。此外,它还包含一个与 StartExpiryTimer() 一起使用的内部计时器,用于在计时器过期时调用回调函数。

看一下 ActiveHandlerTrackingEntry 的定义:

internal class ActiveHandlerTrackingEntry
{
public LifetimeTrackingHttpMessageHandler Handler { get; private set; }
public TimeSpan Lifetime { get; }
public string Name { get; }
public IServiceScope Scope { get; }
public void StartExpiryTimer(TimerCallback callback)
{
// Starts the internal timer
// Executes the callback after Lifetime has expired.
// If the timer has already started, is noop
}
}

因此 CreateHandler 方法要么创建一个新的 ActiveHandlerTrackingEntry ,要么从字典中检索条目,然后启动计时器。

下一节,我们来看看 CreateHandlerEntry() 方法如何创建 ActiveHandlerTrackingEntry 实例。

三、在CreateHandlerEntry中创建和跟踪HttpMessageHandler

CreateHandlerEntry 方法是创建 HttpClient 处理管道的地方。

这个部分代码有点复杂,我们简化一下,以研究过程为主:

private readonly IServiceProvider _services;
private readonly IHttpMessageHandlerBuilderFilter[] _filters;
private ActiveHandlerTrackingEntry CreateHandlerEntry(string name)
{
IServiceScope scope = _services.CreateScope();
IServiceProvider services = scope.ServiceProvider;
HttpClientFactoryOptions options = _optionsMonitor.Get(name);
HttpMessageHandlerBuilder builder = services.GetRequiredService<HttpMessageHandlerBuilder>();
builder.Name = name;
Action<HttpMessageHandlerBuilder> configure = Configure;
for (int i = _filters.Length - 1; i >= 0; i--)
{
configure = _filters[i].Configure(configure);
}
configure(builder);
var handler = new LifetimeTrackingHttpMessageHandler(builder.Build());
return new ActiveHandlerTrackingEntry(name, handler, scope, options.HandlerLifetime);
void Configure(HttpMessageHandlerBuilder b)
{
for (int i = 0; i < options.HttpMessageHandlerBuilderActions.Count; i++)
{
options.HttpMessageHandlerBuilderActions[i](b);
}
}
}

先用根DI容器创建一个 IServiceScope ,从关联的 IServiceProvider 中获取关联的服务,再从 HttpClientFactoryOptions 中找到对应名称的 HttpClient 和它的配置。

从容器中查找的下一项是 HttpMessageHandlerBuilder ,默认值是 DefaultHttpMessageHandlerBuilder ,这个值通过创建一个主处理程序(负责建立 Socket 套接字和发送请求的 HttpClientHandler )来构建处理管道。我们可以通过添加附加的委托来包装这个主处理程序,来为请求和响应创建自定义管理。

附加的委托 DelegatingHandlers 类似于Core的中间件管道:

  1. Configure() 根据 Startup.ConfigureServices() 提供的配置构建 DelegatingHandlers 管道;
  2. IHttpMessageHandlerBuilderFilter 是注入到 IHttpClientFactory 构造函数中的过滤器,用于在委托处理管道中添加额外的处理程序。

IHttpMessageHandlerBuilderFilter 类似于 IStartupFilters ,默认注册的是 LoggingHttpMessageHandlerBuilderFilter 。这个过滤器向委托管道添加了两个额外的处理程序:

  1. 管道开始位置的 LoggingScopeHttpMessageHandler ,会启动一个新的日志 Scope
  2. 管道末端的 LoggingHttpMessageHandler ,在请求被发送到主 HttpClientHandler 之前,记录有关请求和响应的日志;

最后,整个管道被包装在一个 LifetimeTrackingHttpMessageHandler 中。管道处理完成后,将与用于创建它的 IServiceScope 一起保存在一个新的 ActiveHandlerTrackingEntry 实例中,并给定 HttpClientFactoryOptions 中定义的生存期(默认为两分钟)。

该条目返回给调用者( CreateHandler() 方法),添加到处理程序的 ConcurrentDictionary<> 中,添加到新的 HttpClient 实例中(在 CreateClient() 方法中),并返回给原始调用者。

在接下来的生存期(两分钟)内,每当您调用 CreateClient() 时,您将获得一个新的 HttpClient 实例,但是它具有与最初创建时相同的处理程序管道。

每个命名或类型化的 HttpClient 都有自己的消息处理程序管道。例如,名称为 WangPlus 的两个 HttpClient 实例将拥有相同的处理程序链,但名为 apiHttpClient 将拥有不同的处理程序链。

下一节,我们研究下计时器过期后的清理处理。

三、过期清理

以默认时间来说,两分钟后,存储在 ActiveHandlerTrackingEntry 中的计时器将过期,并触发 StartExpiryTimer() 的回调方法 ExpiryTimer_Tick()

ExpiryTimer_Tick 负责从 ConcurrentDictionary<> 池中删除处理程序记录,并将其添加到过期处理程序队列中:

readonly ConcurrentQueue<ExpiredHandlerTrackingEntry> _expiredHandlers;
internal void ExpiryTimer_Tick(object state)
{
var active = (ActiveHandlerTrackingEntry)state;
_activeHandlers.TryRemove(active.Name, out Lazy<ActiveHandlerTrackingEntry> found);
var expired = new ExpiredHandlerTrackingEntry(active);
_expiredHandlers.Enqueue(expired);
StartCleanupTimer();
}

当一个处理程序从 _activeHandlers 集合中删除后,当调用 CreateClient() 时,它将不再与新的 HttpClient 一起分发,但会保持在内存存,直到引用此处理程序的所有 HttpClient 实例全部被清除后, IHttpClientFactory 才会最终释放这个处理程序管道。

IHttpClientFactory 使用 LifetimeTrackingHttpMessageHandlerExpiredHandlerTrackingEntry 来跟踪处理程序是否不再被引用。

看下面的代码:

internal class ExpiredHandlerTrackingEntry
{
private readonly WeakReference _livenessTracker;
public ExpiredHandlerTrackingEntry(ActiveHandlerTrackingEntry other)
{
Name = other.Name;
Scope = other.Scope;
_livenessTracker = new WeakReference(other.Handler);
InnerHandler = other.Handler.InnerHandler;
}
public bool CanDispose => !_livenessTracker.IsAlive;
public HttpMessageHandler InnerHandler { get; }
public string Name { get; }
public IServiceScope Scope { get; }
}

根据这段代码, ExpiredHandlerTrackingEntry 创建了对 LifetimeTrackingHttpMessageHandler 的弱引用。根据上一节所写的, LifetimeTrackingHttpMessageHandler 是管道中的“最外层”处理程序,因此它是 HttpClient 直接引用的处理程序。

LifetimeTrackingHttpMessageHandler 使用 WeakReference 意味着对管道中最外层处理程序的直接引用只有在 HttpClient 中。一旦垃圾收集器收集了所有这些 HttpClientLifetimeTrackingHttpMessageHandler 将没有引用,因此也将被释放。 ExpiredHandlerTrackingEntry 可以通过 WeakReference.IsAlive 检测到。

在将一个记录添加到 _expiredHandlers 队列之后, StartCleanupTimer() 将启动一个计时器,该计时器将在10秒后触发。触发后调用 CleanupTimer_Tick() 方法,检查是否对处理程序的所有引用都已过期。如果是,处理程序和 IServiceScope 将被释放。如果没有,它们被添加回队列,清理计时器再次启动:

internal void CleanupTimer_Tick()
{
StopCleanupTimer();
int initialCount = _expiredHandlers.Count;
for (int i = 0; i < initialCount; i++)
{
_expiredHandlers.TryDequeue(out ExpiredHandlerTrackingEntry entry);
if (entry.CanDispose)
{
try
{
entry.InnerHandler.Dispose();
entry.Scope?.Dispose();
}
catch (Exception ex)
{
}
}
else
{
_expiredHandlers.Enqueue(entry);
}
}
if (_expiredHandlers.Count > 0)
{
StartCleanupTimer();
}
}

为了看清代码的流程,这个代码我简单了。原始的代码中还有日志记录和线程锁相关的内容。

这个方法比较简单:遍历 ExpiredHandlerTrackingEntry 记录,并检查是否删除了对 LifetimeTrackingHttpMessageHandler 处理程序的所有引用。如果有,处理程序和 IServiceScope 就会被释放。

如果仍然有对任何 LifetimeTrackingHttpMessageHandler 处理程序的活动引用,则将条目放回队列,并再次启动清理计时器。

四、总结

如果你看到了这儿,那说明你还是很有耐心的。

这篇文章是一个对源代码的研究,能够帮我们理解 IHttpClientFactory 的运行方式,以及它是以什么样的方式填补了旧的 HttpClient 的坑。

有些时候,看看源代码,还是很有益处的。

微信公众号:老王Plus

扫描二维码,关注个人公众号,可以第一时间得到最新的个人文章和内容推送

本文版权归作者所有,转载请保留此声明和原文链接

Prometheus服务端启动参数

上一篇

服务注册与发现 — 选择 CP 还是 AP?

下一篇

你也可能喜欢

Dotnet Core IHttpClientFactory深度研究

长按储存图像,分享给朋友