HttpClient vs HttpClientFactory

早期HttpClient---> .NET 3.0 HttpClient/ IHttpCientFactory

 

1. 早期.NET HttpClient遇到的Socket滥用/DNS解析问题

使用早期 .NET的HttpClient 时,开发者可能会遇到套接字滥用和 DNS解析相关的问题, 而这两个问题是非此即彼的出现。

1>. Socket滥用

  “滥用” 指的是使用者不明白Httpclient底层实现和底层tcp连接的背景,而囫囵吞枣的编写

  using(var client = new HttpClient())    // httpclient 实现IDispose接口,开发者会自然而然地使用using语法糖

  {

  }   这样的代码导致的 Socket耗尽和端口耗尽问题。

 

早期版本的HttpClient有tcp连接池化技术, 但是并不提供对于池内连接的生命周期管理能力, 高并发频繁创建tcp实例, 最终会导致套接字耗尽。

② HttpClient实现了IDispose接口,频繁释放HttpClient实例,会关闭tcp连接; tcp连接在4次挥手后,TCP 端口不会在连接关闭后立即释放,(有关这一点的详细信息,请参阅 RFC 9293 中的 TCP TIME-WAIT), 这会导致主机的端口耗尽。

解决以上问题的思路: 重用 HttpClient 实例, 但是重用httpClient又带来了DNS解析问题。

2>. DNS 解析问题

HttpClient 仅在创建连接时解析 DNS 条目。 它不跟踪 DNS 服务器指定的任何生存时间 (TTL),这可能导致 DNS 解析结果不及时更新。

 

2. .NET Core2.1+ HttpClient 增强自身证明自己 

 意识到重用httpClient带上的dns解析副作用之后, ASP.NET团队和 .NET 团队 分别给出路线来尝试解决这个问题,

 前者推出了IHttpClientFactory 具备生命周期管理能力, 后者推出了新版的HttpClient。

这个版本的HttpClient 要解决早期HttpClient 非此即彼的Socket滥用和DNS解析问题,它的思路是哪里弱, 我就强化哪里。

.NET Core 2.1在HttpClient内部核心处理链中提供SocketsHttpHandler ,这个Handler为连接池中的连接提供了生命周期管理的能力。

 与连接生命周期相关的三个关键属性

var handler = new SocketsHttpHandler
{
    PooledConnectionLifetime = TimeSpan.FromMinutes(15), // 限制连接的生命周期,默认无限  Recreate every 15 minutes, 这个配置可用于缓解DNS解析问题  
    PooledConnectionIdleTimeout = TimeSpan.FromMinutes(2),   // 空闲连接在连接池中的存活时间, 默认2min  
    MaxConnectionsPerServer =  100,                  // 定义到每个目标服务节点能建立的最大连接数  默认是int.MaxValue

}; 
var sharedClient = new HttpClient(handler);

都聊到此了,必然推荐使用单例模式重用 HttpClient 实例,调整PooledConnectionLifetime 缓解DNS解析问题。

 

初始化对应的源码如下 :

/// <summary>Initializes a new instance of the <see cref="T:System.Net.Http.HttpClient" /> class with the specified handler. The handler is disposed when this instance is disposed.</summary>
    /// <param name="handler">The HTTP handler stack to use for sending requests.</param>
    /// <exception cref="T:System.ArgumentNullException">The <paramref name="handler" /> is <see langword="null" />.</exception>
    public HttpClient(HttpMessageHandler handler)
      : this(handler, true)
    {
    }

/// <summary>Initializes a new instance of the <see cref="T:System.Net.Http.HttpClient" /> class with the provided handler, and specifies whether that handler should be disposed when this instance is disposed.</summary>
/// <param name="handler">The <see cref="T:System.Net.Http.HttpMessageHandler" /> responsible for processing the HTTP response messages.</param>
/// <param name="disposeHandler">
/// <see langword="true" /> if the inner handler should be disposed of by HttpClient.Dispose; <see langword="false" /> if you intend to reuse the inner handler.</param>
/// <exception cref="T:System.ArgumentNullException">The <paramref name="handler" /> is <see langword="null" />.</exception>
public HttpClient(HttpMessageHandler handler, bool disposeHandler)
: base(handler, disposeHandler)
{
this._timeout = HttpClient.s_defaultTimeout;
this._maxResponseContentBufferSize = int.MaxValue;
this._pendingRequestsCts = new CancellationTokenSource();
}

 

 参数1: 用于发出请求的handler;

 参数2:传入的是true, 意味着HttpClient.Dispose时,释放内部的Handler, 因为是单例重用, 故给一次dispose的时机也是合适的。

3. IHttpClientFactory: 曲线救国

 IHttpClientFactory从HttpClient上层解决了该问题,它的思路是: 哪里弱,我绕开他,从上层封装。 

IHttpClientFactory在HttpClient级别上提供HttpClientHandler 生命周期的跟踪和管理能力,它的管理方式更像是对于HttpClientHandler实例的缓存管理。

  •   默认的生命周期是2min, 跟踪handler生命周期并非池化技术, 故构建器只提供了  SetHandlerLifetime(TimeSpan.FromMinutes(5) )  这一个配置函数。
  •   上层用定时缓存的思路实现了统一管理HttpClientHandler生命周期, 也就意味着工厂产生的瞬态HttpClient 并不会被瞬态dispose
    new HttpClient(handler, disposeHandler: false)
    传入的是false, 意味着HttpClient.Dispose时,不释放内部的Handler
     public HttpClient CreateClient(string name)
        {
            ThrowHelper.ThrowIfNull(name);

            HttpMessageHandler handler = CreateHandler(name);
            var client = new HttpClient(handler, disposeHandler: false);  //dispose HttpClient时, 不释放内部的Handler

            HttpClientFactoryOptions options = _optionsMonitor.Get(name);
            for (int i = 0; i < options.HttpClientActions.Count; i++)
            {
                options.HttpClientActions[i](client);
            }

            return client;
        }

      public HttpMessageHandler CreateHandler(string name)
      {
         ThrowHelper.ThrowIfNull(name);

         ActiveHandlerTrackingEntry entry = _activeHandlers.GetOrAdd(name, _entryFactory).Value;

         StartHandlerEntryTimer(entry);

         return entry.Handler;

     }


最底层释放HttpClientHandler的源码在HttpMessageInvoker 类:

IHttpClientFactory 使用定时器来清除上层封装的Handler:

  •  上层封装的缓存HttpClientHandler:  LifetimeTrackingHttpMessageHandler
  •  定时器清除 过期缓存码 https://github.com/dotnet/runtime/blob/ab7e12334f07b72e67b897e9ae7a09d04fd1efb5/src/libraries/Microsoft.Extensions.Http/src/DefaultHttpClientFactory.cs#L278C49-L278C52

 

IHttpCLientFactory 工厂除了具备 “管理基础 HttpClientHandler 实例的缓存和生存期,可避免手动管理 HttpClient 生存期时出现的常见域名系统 (DNS) 问题”, 还具有

  • HttpClient实例的产生更符合.NET 框架的调性: DI、 以委托方式配置HttpClient中间件的惯例
  • 中心化配置、 命名或者类型化客户端
  • 提供基于 Polly 的中间件的扩展方法,以利用 HttpClient 中的委托处理程序。
  • (通过 ILogger)添加可配置的记录体验,以处理工厂创建的客户端发送的所有请求。

https://www.stevejgordon.co.uk/httpclient-connection-pooling-in-dotnet-core

 

4. HttpClient 已经默认启用http2, 上述问题还存在吗 ?

http2并不能自动解决 Socket滥用和DNS解析相关的问题,但http2 多路复用和头部压缩在一定程度上改善了问题。

- 重用 HttpClient 实例仍然是关键,以避免频繁创建和销毁实例导致的套接字耗尽。

- HTTP/2 本身并不解决 DNS 缓存和解析问题。依然需要通过适当的配置来管理 DNS 解析: 

PooledConnectionLifetime = TimeSpan.FromMinutes(15)

4.1  Http2协商升级

  1. h2c:

    • 客户端首先通过 HTTP/1.1 发起请求,并在请求头中包含 Upgrade: h2c 头部字段,表示希望升级到 HTTP/2。
    • 如果服务器支持 HTTP/2,并且同意升级,则会在响应中返回状态码 101 Switching Protocols,并随后切换到 HTTP/2 通信。
    • 如果服务器不支持 HTTP/2,则继续使用 HTTP/1.1 进行通信。
  2. ALPN(Application-Layer Protocol Negotiation):

    • ALPN 是在 TLS 握手过程中用于协商协议版本的扩展。
    • 当客户端和服务器都支持 HTTPS 时,客户端会在 TLS ClientHello 消息中通过 ALPN 扩展指明它支持的协议(例如 h2 表示 HTTP/2)。
    • 服务器在 TLS ServerHello 消息中选择一个客户端支持的协议。如果选择了 HTTP/2,接下来的通信就会使用 HTTP/2。
    • ALPN 是目前最常用的协商 HTTP/2 的方法,特别是在使用 HTTPS 的情况下,因为它是无缝且高效的。

h2c 是 "HTTP/2 over cleartext" 的缩写。它指的是在没有加密的情况下,通过明文(cleartext)进行 HTTP/2 协议通信。这种方式通常用于内网环境或可信网络中,主要用于减少加密带来的性能开销。

ALPN  这是最常用的 HTTP/2 传输方式,通过加密的 TLS(传输层安全协议)进行传输。大多数现代浏览器和服务器都要求通过 TLS 使用 HTTP/2,以确保数据传输的安全性和隐私。

 

5. HttpClientFactory 使用方式

ASP.NET Core 在 2.1 之后推出了具有弹性 HTTP 请求能力的 HttpClient 工厂类 HttpClientFactory。

HttpClientFactory 以模块化、可命名、可配置、弹性方式重建了 HttpClient 的使用方式:

由 DI 框架注入 IHttpClientFactory 工厂;由工厂创建 HttpClient 并从内部的 Handler池分配请求 Handler (httpclient连接池、sqlclient连接池都是复用底层的tcp连接)。

HttpClient 可在 DI 框架中通过IHttpCLientBuilder对象配置 Policy 策略。

一个完整的 HttpClient 包括三部分:

  • 基础业务配置:BaseAddress、DefaultRequestHeaders、DefaultProxy、TimeOut.....

  • 核心 MessageHandler:负责核心的业务请求

  • [可选的]附加 HttpMessageHandler

附加的 HttpMessageHandler 需要与核心 HttpMessageHandler 形成链式 Pipeline 关系,最终端点指向核心 HttpMessageHandler,
链表数据结构是 DelegatingHandler 关键类(包含 InnerHandler 链表节点指针)

P1. 构建 HttpClient

在 Startup.cs 文件开始配置要用到的 HttpClient

services.AddHttpClient("bce-request", x =>
                   x.BaseAddress = new Uri(Configuration.GetSection("BCE").GetValue<string>("BaseUrl")))
                .ConfigurePrimaryHttpMessageHandler(_ => new BceAuthClientHandler()
               {
                   AccessKey = Configuration.GetSection("BCE").GetValue<string>("AccessKey"),
                   SerectAccessKey = Configuration.GetSection("BCE").GetValue<string>("SecretAccessKey"),
                   AllowAutoRedirect = true,
                   UseDefaultCredentials = true
               })
               .SetHandlerLifetime(TimeSpan.FromHours(12))
               .AddPolicyHandler(GetRetryPolicy(3));
 

 

配置过程充分体现了.NET Core 推崇的万物皆服务,配置前移的 DI 风格;
同对时 HttpClient 的基础、配置均通过配置即委托来完成

Q1. 如何记录以上配置?

微软使用一个HttpClientFactoryOptions对象来记录 HttpClient 配置,这个套路是不是很熟悉?

  • 通过 DI 框架的AddHttpClient扩展方法产生 HttpClientBuilder 对象

  • HttpClientBuilder 对象的ConfigurePrimaryHttpMessageHandler扩展方法会将核心 Handler 插到 Options 对象的 HttpMessageHandlerBuilderActions 数组,作为 Handlers 数组中的 PrimaryHandler

  • HttpClientBuilder 对象的AddPolicyHandler扩展方法也会将 PolicyHttpMessageHandler 插到 Options 对象的 HttpMessageHandlerBuilderActions 数组,作为 AdditionHandler

 //  An options class for configuring the default System.Net.Http.IHttpClientFactory
 public class HttpClientFactoryOptions
    {
        public HttpClientFactoryOptions();
        // 一组用于配置HttpMessageHandlerBuilder的操作委托
        public IList<Action<HttpMessageHandlerBuilder>> HttpMessageHandlerBuilderActions { get; }
        public IList<Action<HttpClient>> HttpClientActions { get; }
        public TimeSpan HandlerLifetime { get; set; }
        public bool SuppressHandlerScope { get; set; }
    }

显而易见,后期创建 HttpClient 实例时会通过 name 找到对应的 Options,从中加载配置和 Handlers。

P2. 初始化 HttpClient 实例

通过 IHttpClientFactory.CreateClient() 产生的 HttpClient 实例有一些内部行为:
标准的 HttpClient(不带 Policy 策略)除了 PrimaryHandler 之外,微软给你附加了两个 AdditionHandler:

  • LoggingScopeHttpMessageHandler:最外围 Logical 日志

  • LoggingHttpMessageHandler:核心 Http 请求日志

之后将排序后的 AdditionHanders 数组与 PrimaryHandler 通过 DelegatingHandler 数据结构转化为链表, 末节点是 PrimaryHandler

输出的日志如下:

Q2. 微软为啥要增加外围日志 Handler?

这要结合 P1 给出的带 Policy 策略的 HttpClient,带 Policy 策略的 HttpClient 会在 AdditionHandlers 插入 PolicyHttpMessageHandler 来控制retryCircuit Breaker,那么就会构建这样的 Handler Pipeline:

所以微软会在 AdditionHandlers 数组最外围提供一个业务含义的日志 LogicalHandler,最内层固定 LoggingHttpHandler,这是不是很靠谱?

无图无真相,请查看带Policy策略的 HttpClient 请求堆栈:

Q3. 何处强插、强行固定这两个日志 Handler?
微软通过在 DI 环节注入默认的 LoggingHttpMessageHandlerBuilderFilter 来重排 Handler 的位置:

 // 截取自LoggingHttpMessageHandlerBuilderFilter文件

public Action<HttpMessageHandlerBuilder> Configure(Action<HttpMessageHandlerBuilder> next)
{
 return (builder) =>
 {
     next(builder);
     var loggerName = !string.IsNullOrEmpty(builder.Name) ? builder.Name : "Default";
     // We want all of our logging message to show up as-if they are coming from HttpClient,
     // but also to include the name of the client for more fine-grained control.
     var outerLogger = _loggerFactory.CreateLogger($"System.Net.Http.HttpClient.{loggerName}.LogicalHandler");
     var innerLogger = _loggerFactory.CreateLogger($"System.Net.Http.HttpClient.{loggerName}.ClientHandler");
     var options = _optionsMonitor.Get(builder.Name);
 
     // The 'scope' handler goes first so it can surround everything.
     builder.AdditionalHandlers.Insert(0, new LoggingScopeHttpMessageHandler(outerLogger, options));
 
     // We want this handler to be last so we can log details about the request after
     // service discovery and security happen.
     builder.AdditionalHandlers.Add(new LoggingHttpMessageHandler(innerLogger, options));
   };
}

 

Q4. 创建 HttpClient 时,如何将 AdditionHandlers 和 PrimaryHandler 形成链式 Pipeline 关系 ?

protected internal static HttpMessageHandler CreateHandlerPipeline(HttpMessageHandler primaryHandler, IEnumerable<DelegatingHandler> additionalHandlers)
{
   var additionalHandlersList = additionalHandlers as IReadOnlyList<DelegatingHandler> ?? additionalHandlers.ToArray();
   var next = primaryHandler;
   for (var i = additionalHandlersList.Count - 1; i >= 0; i--)
   {
      var handler = additionalHandlersList[i];
      if (handler == null)
      {
         var message = Resources.FormatHttpMessageHandlerBuilder_AdditionalHandlerIsNull(nameof(additionalHandlers));
         throw new InvalidOperationException(message);
      }
      handler.InnerHandler = next;
      next = handler;
   }
}

数组转链表IReadOnlyList<DelegatingHandler>的算法与 ASP.NET Core 框架的 Middleware 构建 Pipeline 如出一辙。

 

伪代码演示实例创建过程:

DefaultHttpClientFactory.CreateClient()
--->构造函数由 DI 注入默认的 LoggingHttpMessageHandlerBuilderFilter
--->通过 Options.HttpMessageHandlerBuilderActions 拿到所有的 Handlers
--->使用 LoggingHttpMessageHandlerBuilderFilter 强排 AdditionHandlers
--->创建 Handler 链式管道
--->用以上链式初始化 HttpClient 实例
--->从 Options.HttpClientActions 中提取对于 Httpclient 的基础配置
--->返回一个基础、HttpHandler 均正确配置的 HttpClient 实例

上述行为依赖于 ASP.NETCor 框架在 DI 阶段注入的几个服务:

  • DefaultHttpClientFactory

  • LoggingHttpMessageHandlerBuilderFilter:过滤并强排 AdditionHandlers

  • DefaultHttpMessageHandlerBuilder:Handler数组转链表

总结

本文从早期的HttpClient带来的尴尬问题(重用HttpClient带来的DNS解析问题), 聊到.NET团队尝试解决该问题的两个思路。

.NET Core 2.1 的思路是增强HttpClient库底层的连接池,提供了SocketsHttpHandler来控制连接的生命周期,

IHttpClientFactory的思路是绕过HttpClient本身的问题,在上层用缓存的思路来控制HttpClient 实例。

 https://learn.microsoft.com/en-us/dotnet/fundamentals/networking/http/httpclient-guidelines

 

现代的IHttpClientFactory 在框架层面提供了管理HttpClient实例的能力,是另外一个思路。

-  https://github.com/dotnet/runtime/tree/main/src/libraries/Microsoft.Extensions.Http/src

posted @ 2020-03-04 15:07  博客猿马甲哥  阅读(3222)  评论(5编辑  收藏  举报