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

2019-07-19 18:31:26 浏览数 (1)

写在前面

ASP.NET Core 的 Web 服务器默认采用Kestrel,这是一个跨平台、轻量级的Web服务器(需要指出的是从 ASP.NET Core 2.1 开始 Kestrel 就不基于 libuv 了 "Starting in ASP.NET Core 2.1.0, managed sockets will be replacing libuv as Kestrel's default transport." 详见 https://github.com/aspnet/Announcements/issues/296)。

在开始之前,先回顾一下.NET Core 3.0默认的main()方法模板中,我们会调用Host.CreateDefaultBuilder方法,该方法的主要功能是配置应用主机及设置主机的属性,设置Kestrel 服务器配置为 Web 服务器,另外还包括日志功能、应用配置加载等等,此处不做展开。

作为一个轻量级的Web Server,它并没有IIS、Apache那些大而全的功能,但它依然可以单独运行,也可以搭配IIS、Apache等反向代理服务器结合使用。

本文将从源码角度讨论ASP.NET Core应用在Kestrel的相关知识点。

Kestrel

Kestrel的存在意义

了解这个问题,首先需要强调的是.NET Core应用的目标就是跨平台,既然要跨平台那么就需要适用各个平台上的Web服务器,各个服务器的启动、配置等等都是不尽相同的,如果每个服务器提供一套实现出来,如果未来出现了一个新的Web Server,然后又要增加新的实现,这会导致.NET Core应用的适用性滞后,也会很消耗人力,无法很好的达到跨平台的目标。

我们可以把Kestrel视作一个中间件,一个适配的功能,它抽象了各个服务器的特性,使得各个应用只需要调用同样的接口,即可最大限度的在各个平台上运行。

运行方式

.NET Core 3.0下,Kestrel的集成已经相当成熟了,也提供了相应的自定义配置,以使得Kestrel的使用更加具有灵活性和可配性。它可以独立运行,也可以与反向代理服务器结合使用。

Kestrel本身是不支持多个应用共享同一个端口的,但是我们可以通过反向代理服务器来实现统一对外的相同的端口的共享。

以下是其单独运行示意图:

以下是其结合反向代理使用示意图:

Microsoft.AspNetCore.Server.Kestrel.Core

改类库是Kestrel的核心类库,里面包含了该功能的多个逻辑实现,以下简称改类库为Kestrel.Core。

Kestrel适配逻辑

如前文所说,Kestrel起到了抽象个服务器的功能,那么在适配其他服务器的过程中,必然涉及到的是,输入、输出、数据交互方式以及Trace功能。在Kestrel.Core中,该功能主要由AdaptedPipeline类来实现,该类继承自IDuplexPipe,并通过还通过构造函数获取到了Pipe对象。IDuplexPipe和Pipe均位于System.IO.Pipelines命名空间下,详细信息可以点击查看。

AdaptedPipeline有两个公共方法:

RunAsync():用于读取(读取后会有Flush操作)和写入数据,并分别装载到Task中

CompleteAsync():完成读取和写入操作,并取消基础流的读取

另外还包括四个公共属性,如下所示:

代码语言:javascript复制
   1:  public RawStream TransportStream { get; }
代码语言:javascript复制
   2:   
代码语言:javascript复制
   3:  public Pipe Input { get; }
代码语言:javascript复制
   4:   
代码语言:javascript复制
   5:  public Pipe Output { get; }
代码语言:javascript复制
   6:   
代码语言:javascript复制
   7:  public IKestrelTrace Log { get; }

它定义了提供可从中读取并写入数据的双工管道的对象。IDuplexPipe有两个属性,System.IO.Pipelines.PipeReader Input { get; }和System.IO.Pipelines.PipeReader Output { get; }。AdaptedPipeline还通过构造函数获取到了Pipe对象。

RawStream类继承自Stream,并重写了Stream的关键属性及方法,主要目标是提供适合于Kestrel读写数据方式的内部封装。

LoggingStream类也同样继承自Stream,和RawStream不同的是,里面增加操作过程的日志记录,主要用于记录在连接适配过程中的信息,不过需要启用日志才能把日志信息记录下来,以下是其对外的使用方式:

代码语言:javascript复制
   1:  public static class ListenOptionsConnectionLoggingExtensions
代码语言:javascript复制
   2:  {
代码语言:javascript复制
   3:      /// <summary>
代码语言:javascript复制
   4:      /// Emits verbose logs for bytes read from and written to the connection.
代码语言:javascript复制
   5:      /// </summary>
代码语言:javascript复制
   6:      /// <returns>
代码语言:javascript复制
   7:      /// The <see cref="ListenOptions"/>.
代码语言:javascript复制
   8:      /// </returns>
代码语言:javascript复制
   9:      public static ListenOptions UseConnectionLogging(this ListenOptions listenOptions)
代码语言:javascript复制
  10:      {
代码语言:javascript复制
  11:          return listenOptions.UseConnectionLogging(loggerName: null);
代码语言:javascript复制
  12:      }
代码语言:javascript复制
  13:   
代码语言:javascript复制
  14:      /// <summary>
代码语言:javascript复制
  15:      /// Emits verbose logs for bytes read from and written to the connection.
代码语言:javascript复制
  16:      /// </summary>
代码语言:javascript复制
  17:      /// <returns>
代码语言:javascript复制
  18:      /// The <see cref="ListenOptions"/>.
代码语言:javascript复制
  19:      /// </returns>
代码语言:javascript复制
  20:      public static ListenOptions UseConnectionLogging(this ListenOptions listenOptions, string loggerName)
代码语言:javascript复制
  21:      {
代码语言:javascript复制
  22:          var loggerFactory = listenOptions.KestrelServerOptions.ApplicationServices.GetRequiredService<ILoggerFactory>();
代码语言:javascript复制
  23:          var logger = loggerName == null ? loggerFactory.CreateLogger<LoggingConnectionAdapter>() : loggerFactory.CreateLogger(loggerName);
代码语言:javascript复制
  24:          listenOptions.ConnectionAdapters.Add(new LoggingConnectionAdapter(logger));
代码语言:javascript复制
  25:          return listenOptions;
代码语言:javascript复制
  26:      }
代码语言:javascript复制
  27:  }

Kestrel特性抽象

该模块下的 Kestrel特性,比较重要的有连接超时设置(包括设置超时时间、重置超时时间以及取消超时限制。这个特性使得我们的连接变得更加可控,比如,在某些特殊场景下,特性条件下,我们需要取消超时限制或者动态重置超时时间),TLS应用程序协议功能,基于Http2.0的StreamId记录功能,用于停止连接计数的功能。

以下是连接超时接口的源代码:

代码语言:javascript复制
   1:  /// <summary>
代码语言:javascript复制
   2:  /// Feature for efficiently handling connection timeouts.
代码语言:javascript复制
   3:  /// </summary>
代码语言:javascript复制
   4:  public interface IConnectionTimeoutFeature
代码语言:javascript复制
   5:  {
代码语言:javascript复制
   6:      /// <summary>
代码语言:javascript复制
   7:      /// Close the connection after the specified positive finite <see cref="TimeSpan"/>
代码语言:javascript复制
   8:      /// unless the timeout is canceled or reset. This will fail if there is an ongoing timeout.
代码语言:javascript复制
   9:      /// </summary>
代码语言:javascript复制
  10:      void SetTimeout(TimeSpan timeSpan);
代码语言:javascript复制
  11:   
代码语言:javascript复制
  12:      /// <summary>
代码语言:javascript复制
  13:      /// Close the connection after the specified positive finite <see cref="TimeSpan"/>
代码语言:javascript复制
  14:      /// unless the timeout is canceled or reset. This will cancel any ongoing timeouts.
代码语言:javascript复制
  15:      /// </summary>
代码语言:javascript复制
  16:      void ResetTimeout(TimeSpan timeSpan);
代码语言:javascript复制
  17:   
代码语言:javascript复制
  18:      /// <summary>
代码语言:javascript复制
  19:      /// Prevent the connection from closing after a timeout specified by <see cref="SetTimeout(TimeSpan)"/>
代码语言:javascript复制
  20:      /// or <see cref="ResetTimeout(TimeSpan)"/>.
代码语言:javascript复制
  21:      /// </summary>
代码语言:javascript复制
  22:      void CancelTimeout();
代码语言:javascript复制
  23:  }

Kestrel选项及限制功能

Kestrel的选项控制包括监听、Kestrel服务器、HTTPS连接适配。

1、监听选项功能在ListenOptions中实现,该类继承自IConnectionBuilder,ListenOptions的主要作用是描述Kestrel中已经打开的套接字,包括Unix域套接字路径、文件描述符、ipendpoint。ListenOptions内部会维护一个只读的List<Func<ConnectionDelegate, ConnectionDelegate>>()对象,并通过Use()方法加载新的Func<ConnectionDelegate, ConnectionDelegate>对象,然后通过Build方式返回最后加入的Func<ConnectionDelegate, ConnectionDelegate对象,源码如下所示:

代码语言:javascript复制
   1:  public IConnectionBuilder Use(Func<ConnectionDelegate, ConnectionDelegate> middleware)
代码语言:javascript复制
   2:  {
代码语言:javascript复制
   3:      _middleware.Add(middleware);
代码语言:javascript复制
   4:      return this;
代码语言:javascript复制
   5:  }
代码语言:javascript复制
   6:   
代码语言:javascript复制
   7:  public ConnectionDelegate Build()
代码语言:javascript复制
   8:  {
代码语言:javascript复制
   9:      ConnectionDelegate app = context =>
代码语言:javascript复制
  10:      {
代码语言:javascript复制
  11:          return Task.CompletedTask;
代码语言:javascript复制
  12:      };
代码语言:javascript复制
  13:   
代码语言:javascript复制
  14:      for (int i = _middleware.Count - 1; i >= 0; i--)
代码语言:javascript复制
  15:      {
代码语言:javascript复制
  16:          var component = _middleware[i];
代码语言:javascript复制
  17:          app = component(app);
代码语言:javascript复制
  18:      }
代码语言:javascript复制
  19:   
代码语言:javascript复制
  20:      return app;
代码语言:javascript复制
  21:  }

需要注意的是ListenOptions在该类库内部还有两个子类,AnyIPListenOptions和LocalhostListenOptions,以用于特定场景的监听使用。

2、Kestrel服务器选项是在KestrelServerOptions中实现的,该类用于提供Kestrel特定功能的编程级别配置,该类内部会维护ListenOptions的列表对象,该类将ListenOptions的功能进一步展开,并加入了HTTPS、证书的默认配置与应用,这个类比较大,这里就不贴出源码了,有兴趣的同学可以自己去翻阅。

3、HTTPS连接适配选项在HttpsConnectionAdapterOptions实现,这个类用于设置Kestrel如何处理HTTPS连接,这里引入和证书功能、SSL协议、HTTP协议、超时功能,同时这里还可以自定义HTTPS连接的时候的证书处理模式(AllowCertificate、RequireCertificate等),以下是HttpsConnectionAdapterOptions的构造函数:

代码语言:javascript复制
   1:  public HttpsConnectionAdapterOptions()
代码语言:javascript复制
   2:  {
代码语言:javascript复制
   3:      ClientCertificateMode = ClientCertificateMode.NoCertificate;
代码语言:javascript复制
   4:      SslProtocols = SslProtocols.Tls12 | SslProtocols.Tls11;
代码语言:javascript复制
   5:      HandshakeTimeout = TimeSpan.FromSeconds(10);
代码语言:javascript复制
   6:  }

可以看到,在默认情况下,是无证书模式,其SSL协议包括Tls12 和Tls11以及指定允许进行TLS/SSL握手的最大时间是十秒钟。

4、Kestrel的限制功能在KestrelServerLimits实现,主要包括:

  • 保持活动状态超时
  • 客户端最大连接数(默认情况下,最大连接数不受限制 (NULL))
  • 请求正文最大大小(默认的请求正文最大大小为 30,000,000 字节,大约 28.6 MB)
  • 请求正文最小数据速率(默认的最小速率为 240 字节/秒,包含 5 秒的宽限期)
  • 请求标头超时(默认值为 30 秒)
  • 每个连接的最大流(默认值为 100)
  • 标题表大小(默认值为 4096)
  • 最大帧大小(默认值为 2^14)
  • 最大请求标头大小(默认值为 8,192)
  • 初始连接窗口大小(默认值为 128 KB)
  • 初始流窗口大小(默认值为 96 KB)

代码如下所示:

代码语言:javascript复制
   1:  .ConfigureKestrel((context, options) =>
代码语言:javascript复制
   2:  {
代码语言:javascript复制
   3:      options.Limits.MaxConcurrentConnections = 100;
代码语言:javascript复制
   4:      options.Limits.MaxConcurrentUpgradedConnections = 100;
代码语言:javascript复制
   5:      options.Limits.MaxRequestBodySize = 10 * 1024;
代码语言:javascript复制
   6:      options.Limits.MinRequestBodyDataRate =
代码语言:javascript复制
   7:          new MinDataRate(bytesPerSecond: 100, gracePeriod: TimeSpan.FromSeconds(10));
代码语言:javascript复制
   8:      options.Limits.MinResponseDataRate =
代码语言:javascript复制
   9:          new MinDataRate(bytesPerSecond: 100, gracePeriod: TimeSpan.FromSeconds(10));
代码语言:javascript复制
  10:      options.Listen(IPAddress.Loopback, 5000);
代码语言:javascript复制
  11:      options.Listen(IPAddress.Loopback, 5001, listenOptions =>
代码语言:javascript复制
  12:      {
代码语言:javascript复制
  13:          listenOptions.UseHttps("testCert.pfx", "testPassword");
代码语言:javascript复制
  14:      });
代码语言:javascript复制
  15:      options.Limits.KeepAliveTimeout = TimeSpan.FromMinutes(2);
代码语言:javascript复制
  16:      options.Limits.RequestHeadersTimeout = TimeSpan.FromMinutes(1);
代码语言:javascript复制
  17:      options.Limits.Http2.MaxStreamsPerConnection = 100;
代码语言:javascript复制
  18:      options.Limits.Http2.HeaderTableSize = 4096;
代码语言:javascript复制
  19:      options.Limits.Http2.MaxFrameSize = 16384;
代码语言:javascript复制
  20:      options.Limits.Http2.MaxRequestHeaderFieldSize = 8192;
代码语言:javascript复制
  21:      options.Limits.Http2.InitialConnectionWindowSize = 131072;
代码语言:javascript复制
  22:      options.Limits.Http2.InitialStreamWindowSize = 98304;
代码语言:javascript复制
  23:  });

其部分源码如下:

代码语言:javascript复制
   1:  // Matches the non-configurable default response buffer size for Kestrel in 1.0.0
代码语言:javascript复制
   2:  private long? _maxResponseBufferSize = 64 * 1024;
代码语言:javascript复制
   3:   
代码语言:javascript复制
   4:  // Matches the default client_max_body_size in nginx.
代码语言:javascript复制
   5:  // Also large enough that most requests should be under the limit.
代码语言:javascript复制
   6:  private long? _maxRequestBufferSize = 1024 * 1024;
代码语言:javascript复制
   7:   
代码语言:javascript复制
   8:  // Matches the default large_client_header_buffers in nginx.
代码语言:javascript复制
   9:  private int _maxRequestLineSize = 8 * 1024;
代码语言:javascript复制
  10:   
代码语言:javascript复制
  11:  // Matches the default large_client_header_buffers in nginx.
代码语言:javascript复制
  12:  private int _maxRequestHeadersTotalSize = 32 * 1024;
代码语言:javascript复制
  13:   
代码语言:javascript复制
  14:  // Matches the default maxAllowedContentLength in IIS (~28.6 MB)
代码语言:javascript复制
  15:  // https://www.iis.net/configreference/system.webserver/security/requestfiltering/requestlimits#005
代码语言:javascript复制
  16:  private long? _maxRequestBodySize = 30000000;
代码语言:javascript复制
  17:   
代码语言:javascript复制
  18:  // Matches the default LimitRequestFields in Apache httpd.
代码语言:javascript复制
  19:  private int _maxRequestHeaderCount = 100;
代码语言:javascript复制
  20:   
代码语言:javascript复制
  21:  // Matches the default http.sys connectionTimeout.
代码语言:javascript复制
  22:  private TimeSpan _keepAliveTimeout = TimeSpan.FromMinutes(2);
代码语言:javascript复制
  23:   
代码语言:javascript复制
  24:  private TimeSpan _requestHeadersTimeout = TimeSpan.FromSeconds(30);
代码语言:javascript复制
  25:   
代码语言:javascript复制
  26:  // Unlimited connections are allowed by default.
代码语言:javascript复制
  27:  private long? _maxConcurrentConnections = null;
代码语言:javascript复制
  28:  private long? _maxConcurrentUpgradedConnections = null;

参考地址:https://docs.microsoft.com/zh-cn/aspnet/core/fundamentals/servers/kestrel?view=aspnetcore-3.0#maximum-streams-per-connection

0 人点赞