跨平臺是ASP.NET Core一個顯著的特性,而KestrelServer是目前微軟推出了唯一一個能夠真正跨平臺的Server。KestrelServer利用一個名為KestrelEngine的網路引擎實現對請求的監聽、接收和響應。KetrelServer之所以具有跨平臺的特質,源於KestrelEngine是在一個名為libuv的跨平臺網路庫上開發的。
一、libuv
說起libuv,就不得不談談libev,後者是Unix系統上一個事件迴圈和事件模型的網路庫。libev因其具有的高效能成為了繼lievent和Event perl module之後一套最受歡迎的網路庫。由於Libev不支援Windows,有人在libev之上建立了一個抽象層以遮蔽平臺之間的差異,這個抽象層就是libuv。libuv在Windows平臺上是採用IOCP的形式實現的,右圖揭示了libuv針對Unix和Windows的跨平臺實現原理。到目前為止,libuv支援的平臺已經不限於Unix和Windows了,包括Linux(2.6)、MacOS和Solaris (121以及之後的版本)在內的平臺在libuv支援範圍之內。
二、KestrelServer
如下所示的程式碼片段體現了KestrelServer這個型別的定義。除了實現介面IServer定義的Features屬性之外,KestrelServer還具有一個型別為KestrelServerOptions的只讀屬性Options。這個屬性表示對KestrelServer所作的相關設定,我們在呼叫建構函式時通過輸入引數options所代表的IOptions物件對這個屬性進行初始化。建構函式還具有另兩個額外的引數,它們的型別分別是IApplicationLifetime和ILoggerFactory,後者用於建立記錄日誌的Logger,前者與應用的生命週期管理有關。
1 2 3 4 5 6 7 8 9 |
public class KestrelServer : IServer { public IFeatureCollection Features { get; } public KestrelServerOptions Options { get; } public KestrelServer(IOptions<KestrelServerOptions> options,IApplicationLifetime applicationLifetime, ILoggerFactory loggerFactory); public void Dispose(); public void Start<TContext>(IHttpApplication<TContext> application); } |
我們一般通過呼叫WebHostBuilder的擴充套件方法UseKestrel方法來完成對KestrelServer的註冊。如下面的程式碼片段所示,UseKestrel方法具有兩個過載,其中一個具有同一個型別為Action的引數,我們可以利用這個引數直接完成對KestrelServerOptions的設定。
1 2 3 4 5 |
public static class WebHostBuilderKestrelExtensions { public static IWebHostBuilder UseKestrel(this IWebHostBuilder hostBuilder); public static IWebHostBuilder UseKestrel(this IWebHostBuilder hostBuilder, Action<KestrelServerOptions> options); } |
三、KestrelServerOptions
由於Server負責請求的監聽、接收和響應,所以Server是影響整個Web應用響應能力和吞吐量最大的因素之一,為了更加有效地使用Server,我們往往針對具體的網路負載狀況對其作針對性的設定。對於KestrelServer來說,在建構函式中作為引數指定的KestrelServerOptions物件代表針對它所做的設定。我們針對KestrelServer所做的設定主要體現在KestrelServerOptions型別的如下5個屬性上。
1 2 3 4 5 6 7 8 9 |
public class KestrelServerOptions { //省略其他成員 public int MaxPooledHeaders { get; set; } public int MaxPooledStreams { get; set; } public bool NoDelay { get; set; } public TimeSpan ShutdownTimeout { get; set; } public int ThreadCount { get; set; } } |
KestrelServerOptions註冊的KetrelServer在管道中會以依賴注入的方式被建立,並採用構造器注入的方式提供其建構函式的引數options,由於這個引數型別為IOptions,所以我們利用Options模型以配置的方式來指定KestrelServerOptions物件承載的設定。比如我們可以將KestrelServer的相關配置定義在如下一個JSON檔案中。
1 2 3 4 5 |
{ "noDelay" : false, "shutdownTimeout" : "00:00:10", "threadCount" : 10 } |
為了讓應用載入這麼一個配置檔案(檔名假設為“KestrelServerOptions.json”),我們只需要在啟動型別(Startup)類的ConfigureServces方法中按照如下的方式利用ConfigurationBuilder載入這個配置檔案並生成相應的Configuration物件,最後按照Options模型的程式設計方式完成KestrelServerOptions型別和該物件的對映即可。
1 2 3 4 5 6 7 8 9 10 11 |
public class Startup { //其他成員 public void ConfigureServices(IServiceCollection services) { IConfiguration configuration = new ConfigurationBuilder() .AddJsonFile("KestrelServerOptions.json") .Build(); services.Configure<KestrelServerOptions>(configuration); } } |
四、ApplicationLifetime
我們將所有實現了IApplicationLifetime介面的所有型別及其對應物件統稱為ApplicationLifetime。從命名的角度來看,ApplicationLifetime貌似是對當前應用生命週期的描述,而實際上它存在的目的僅僅是在應用啟動和關閉(只要是關閉)時對相關元件傳送通知而已。
如下面的程式碼片段所示,IApplicationLifetime介面具有三個CancellationToken型別的屬性(ApplicationStarted、ApplicationStopping和ApplicationStopped),我們可以利用它們是否已經被取消(Cancel)確定當前應用的狀態(已經開啟、正在關閉和已經關閉)。如果試圖關閉應用,StopApplication方法應該被呼叫以發出應用正在被關閉的通知。對於KestrelServer來說,如果請求處理執行緒中發生未被處理異常,它會呼叫這個方法。
1 2 3 4 5 6 7 8 |
public interface IApplicationLifetime { CancellationToken ApplicationStarted { get; } CancellationToken ApplicationStopping { get; } CancellationToken ApplicationStopped { get; } void StopApplication(); } |
ASP.NET Core預設使用的ApplicationLifetime是具有如下定義的一個同名型別。可以看出它實現的三個屬性返回的CancellationToken物件是通過三個對應的CancellationTokenSource生成。除了實現IApplicationLifetime介面的StopApplication方法用於傳送“正在關閉”通知之外,這個型別還定義了額外兩個方法(NotifyStarted和NotifyStopped)用於傳送“已經開啟/關閉”的通知。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
public class ApplicationLifetime : IApplicationLifetime { private readonly CancellationTokenSource _startedSource = new CancellationTokenSource(); private readonly CancellationTokenSource _stoppedSource = new CancellationTokenSource(); private readonly CancellationTokenSource _stoppingSource = new CancellationTokenSource(); public CancellationToken ApplicationStarted { get { return this._startedSource.Token; } } public CancellationToken ApplicationStopped { get { return this._stoppedSource.Token; } } public CancellationToken ApplicationStopping { get { return this._stoppingSource.Token; } } public void NotifyStarted() { this._startedSource.Cancel(false); } public void NotifyStopped() { this._stoppedSource.Cancel(false); } public void StopApplication() { this._stoppingSource.Cancel(false); } } |
一個ASP.NET Core應用利用管道處理請求,所以管道的生命週期等同於應用自身的生命週期。當我們呼叫Run方法開啟WebHost時,請求處理管道被構建出來。如果管道在處理請求時發生未被處理的異常,管道的Sever會呼叫ApplicationLifeTime物件的StopApplication方法向WebHost傳送關閉應用的通知以便後者執行一些回收釋放工作。
五、設定監聽地址
在演示的例項中,我們實際上並不曾為註冊的KestrelServer指定一個監聽地址,從執行的效果我們不難看出,WebHost在這種情況下會指定“http://localhost:5000”為預設的監聽地址,Server的監聽地址自然可以顯式指定。在介紹如何通過程式設計的方式為Server指定監聽地址之前,我們有先來認識一個名為ServerAddressesFeature的特性。
我們知道表示Server的介面IServer中定義了一個型別為IFeatureCollection 的只讀屬性Features,它表示用於描述當前Server的特性集合,ServerAddressesFeature作為一個重要的特性,就包含在這個集合之中。我們所說的ServerAddressesFeature物件是對所有實現了IServerAddressesFeature介面的所有型別及其對應物件的統稱,該介面具有一個唯一的只讀屬性返回Server的監聽地址列表。ASP.NET Core預設使用的ServerAddressesFeature是具有如下定義的同名型別。
1 2 3 4 5 6 7 8 9 |
public interface IServerAddressesFeature { ICollection<string> Addresses { get; } } public class ServerAddressesFeature : IServerAddressesFeature { public ICollection<string> Addresses { get; } } |
對於WebHost在通過依賴注入的方式建立的Server,由它的Features屬性表示的特性集合中會預設包含這麼一個ServerAddressesFeature物件。如果沒有一個合法的監聽地址被新增到這個 ServerAddressesFeature物件的地址列表中,WebHost會將顯式指定的地址(一個或者多個)新增到該列表中。我們顯式指定的監聽地址實際上是作為WebHost的配置儲存在一個Configuration物件上,配置項對應的Key為“server.urls”,WebHostDefaults的靜態只讀屬性ServerUrlsKey返回的就是這麼一個Key。
1 2 3 4 5 6 |
new WebHostBuilder() .UseSetting(WebHostDefaults.ServerUrlsKey, "http://localhost:3721/") .UseMyKestrel() .UseStartup<Startup>() .Build() .Run(); |
WebHost的配置最初來源於建立它的WebHostBuilder,後者提供了一個UseSettings方法來設定某個配置項的值,所以我們可以採用如下的方式來指定監聽地址(“http://localhost:3721/”)。不過,針對監聽地址的顯式設定,最直接的程式設計方式還是呼叫WebHostBuilder的擴充套件方法UseUrls,如下面的程式碼片段所示,該方法的實現邏輯與上面完全一致。
1 2 3 4 5 |
public static class WebHostBuilderExtensions { public static IWebHostBuilder UseUrls(this IWebHostBuilder hostBuilder, params string[] urls) =>hostBuilder.UseSetting(WebHostDefaults.ServerUrlsKey, string.Join(ServerUrlsSeparator, urls)) ; } |