寫在前面
ASP.NET Core 的 Web 服務器默認采用Kestrel,這是一個跨平台、輕量級的Web服務器。
在開始之前,先回顧一下.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():完成讀取和寫入操作,並取消基礎流的讀取
另外還包括四個公共屬性,如下所示:
1: public RawStream TransportStream { get; }
2:
3: public Pipe Input { get; }
4:
5: public Pipe Output { get; }
6:
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不同的是,里面增加操作過程的日志記錄,主要用於記錄在連接適配過程中的信息,不過需要啟用日志才能把日志信息記錄下來,以下是其對外的使用方式:
1: public static class ListenOptionsConnectionLoggingExtensions
2: {
3: /// <summary>
4: /// Emits verbose logs for bytes read from and written to the connection.
5: /// </summary>
6: /// <returns>
7: /// The <see cref="ListenOptions"/>.
8: /// </returns>
9: public static ListenOptions UseConnectionLogging(this ListenOptions listenOptions)
10: {
11: return listenOptions.UseConnectionLogging(loggerName: null);
12: }
13:
14: /// <summary>
15: /// Emits verbose logs for bytes read from and written to the connection.
16: /// </summary>
17: /// <returns>
18: /// The <see cref="ListenOptions"/>.
19: /// </returns>
20: public static ListenOptions UseConnectionLogging(this ListenOptions listenOptions, string loggerName)
21: {
22: var loggerFactory = listenOptions.KestrelServerOptions.ApplicationServices.GetRequiredService<ILoggerFactory>();
23: var logger = loggerName == null ? loggerFactory.CreateLogger<LoggingConnectionAdapter>() : loggerFactory.CreateLogger(loggerName);
24: listenOptions.ConnectionAdapters.Add(new LoggingConnectionAdapter(logger));
25: return listenOptions;
26: }
27: }
Kestrel特性抽象
該模塊下的 Kestrel特性,比較重要的有連接超時設置(包括設置超時時間、重置超時時間以及取消超時限制。這個特性使得我們的連接變得更加可控,比如,在某些特殊場景下,特殊條件下,我們需要取消超時限制或者動態重置超時時間),TLS應用程序協議功能,基於Http2.0的StreamId記錄功能,用於停止連接計數的功能。
以下是連接超時接口的源代碼:
1: /// <summary>
2: /// Feature for efficiently handling connection timeouts.
3: /// </summary>
4: public interface IConnectionTimeoutFeature
5: {
6: /// <summary>
7: /// Close the connection after the specified positive finite <see cref="TimeSpan"/>
8: /// unless the timeout is canceled or reset. This will fail if there is an ongoing timeout.
9: /// </summary>
10: void SetTimeout(TimeSpan timeSpan);
11:
12: /// <summary>
13: /// Close the connection after the specified positive finite <see cref="TimeSpan"/>
14: /// unless the timeout is canceled or reset. This will cancel any ongoing timeouts.
15: /// </summary>
16: void ResetTimeout(TimeSpan timeSpan);
17:
18: /// <summary>
19: /// Prevent the connection from closing after a timeout specified by <see cref="SetTimeout(TimeSpan)"/>
20: /// or <see cref="ResetTimeout(TimeSpan)"/>.
21: /// </summary>
22: void CancelTimeout();
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對象,源碼如下所示:
1: public IConnectionBuilder Use(Func<ConnectionDelegate, ConnectionDelegate> middleware)
2: {
3: _middleware.Add(middleware);
4: return this;
5: }
6:
7: public ConnectionDelegate Build()
8: {
9: ConnectionDelegate app = context =>
10: {
11: return Task.CompletedTask;
12: };
13:
14: for (int i = _middleware.Count - 1; i >= 0; i--)
15: {
16: var component = _middleware[i];
17: app = component(app);
18: }
19:
20: return app;
21: }
需要注意的是ListenOptions在該類庫內部還有兩個子類,AnyIPListenOptions和LocalhostListenOptions,以用於特定場景的監聽使用。
2、Kestrel服務器選項是在KestrelServerOptions中實現的,該類用於提供Kestrel特定功能的編程級別配置,該類內部會維護ListenOptions的列表對象,該類將ListenOptions的功能進一步展開,並加入了HTTPS、證書的默認配置與應用,這個類比較大,本文就不貼出源碼了,有興趣的同學可以自己去翻閱。
3、HTTPS連接適配選項在HttpsConnectionAdapterOptions實現,這個類用於設置Kestrel如何處理HTTPS連接,這里引入和證書功能、SSL協議、HTTP協議、超時功能,同時這里還可以自定義HTTPS連接的時候的證書處理模式(AllowCertificate、RequireCertificate等),以下是HttpsConnectionAdapterOptions的構造函數:
1: public HttpsConnectionAdapterOptions()
2: {
3: ClientCertificateMode = ClientCertificateMode.NoCertificate;
4: SslProtocols = SslProtocols.Tls12 | SslProtocols.Tls11;
5: HandshakeTimeout = TimeSpan.FromSeconds(10);
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)
代碼如下所示:
1: .ConfigureKestrel((context, options) =>
2: {
3: options.Limits.MaxConcurrentConnections = 100;
4: options.Limits.MaxConcurrentUpgradedConnections = 100;
5: options.Limits.MaxRequestBodySize = 10 * 1024;
6: options.Limits.MinRequestBodyDataRate =
7: new MinDataRate(bytesPerSecond: 100, gracePeriod: TimeSpan.FromSeconds(10));
8: options.Limits.MinResponseDataRate =
9: new MinDataRate(bytesPerSecond: 100, gracePeriod: TimeSpan.FromSeconds(10));
10: options.Listen(IPAddress.Loopback, 5000);
11: options.Listen(IPAddress.Loopback, 5001, listenOptions =>
12: {
13: listenOptions.UseHttps("testCert.pfx", "testPassword");
14: });
15: options.Limits.KeepAliveTimeout = TimeSpan.FromMinutes(2);
16: options.Limits.RequestHeadersTimeout = TimeSpan.FromMinutes(1);
17: options.Limits.Http2.MaxStreamsPerConnection = 100;
18: options.Limits.Http2.HeaderTableSize = 4096;
19: options.Limits.Http2.MaxFrameSize = 16384;
20: options.Limits.Http2.MaxRequestHeaderFieldSize = 8192;
21: options.Limits.Http2.InitialConnectionWindowSize = 131072;
22: options.Limits.Http2.InitialStreamWindowSize = 98304;
23: });
1: // Matches the non-configurable default response buffer size for Kestrel in 1.0.0
2: private long? _maxResponseBufferSize = 64 * 1024;
3:
4: // Matches the default client_max_body_size in nginx.
5: // Also large enough that most requests should be under the limit.
6: private long? _maxRequestBufferSize = 1024 * 1024;
7:
8: // Matches the default large_client_header_buffers in nginx.
9: private int _maxRequestLineSize = 8 * 1024;
10:
11: // Matches the default large_client_header_buffers in nginx.
12: private int _maxRequestHeadersTotalSize = 32 * 1024;
13:
14: // Matches the default maxAllowedContentLength in IIS (~28.6 MB)
15: // https://www.iis.net/configreference/system.webserver/security/requestfiltering/requestlimits#005
16: private long? _maxRequestBodySize = 30000000;
17:
18: // Matches the default LimitRequestFields in Apache httpd.
19: private int _maxRequestHeaderCount = 100;
20:
21: // Matches the default http.sys connectionTimeout.
22: private TimeSpan _keepAliveTimeout = TimeSpan.FromMinutes(2);
23:
24: private TimeSpan _requestHeadersTimeout = TimeSpan.FromSeconds(30);
25:
26: // Unlimited connections are allowed by default.
27: private long? _maxConcurrentConnections = null;
28: private long? _maxConcurrentUpgradedConnections = null;