寫在前面
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;