ASP.NET Core管道由注冊的服務器和一系列中間件構成。我們在上一篇中深入剖析了中間件,現在我們來了解一下服務器。服務器是ASP .NET Core管道的第一個節點,它負責完整請求的監聽和接收,最終對請求的響應同樣也由它完成。[本文已經同步到《ASP.NET Core框架揭秘》之中]
服務器是我們對所有實現了IServer接口的所有類型以及對應對象的統稱。如下面的代碼片段所示,這個接口具有一個只讀屬性Features返回描述自身特性集合的FeatureCollection對象,另一個Start方法用於啟動服務器。
1: public interface IServer : IDisposable
2: {
3: IFeatureCollection Features { get; }
4: void Start<TContext>(IHttpApplication<TContext> application);
5: }
當我們Start方法啟動指定的Server的時候,必須指定一個類型為IHttpApplication<TContext>的參數,我們將實現才接口的所有類型及其對應對象統稱為HttpApplication。當服務器在接收到抵達的請求之後,它會直接交給這個HttpApplication對象來處理,所以我們需要先來認識一下這個對象。
對於ASP.NET Core管道來說,HttpApplication對會接管服務器接收的請求,後續的請求完全由它來負責。如下圖所示,HttpApplication從服務器獲得請求之後,會利用注冊的中間件注冊對請求進行處理,並最終將請求遞交給應用程序。HttpApplication針對請求的處理實際上會在一個執行上下文中完成,這個上下文為應用對單一請求的整個處理過程定義了一個邊界。單純描述HTTP請求的HttpContext是這個執行上下文中最為核心的部分,除此之外,我們還可以根據需要將其他相關的信息定義其中,所以IHttpApplication<TContext>接口采用泛型參數的形式來表示定義這個上下文的類型。
HttpApplication不僅僅需要在這個執行上下文中處理服務器轉發給它的請求,這個上下文對象的創建和回收釋放同樣需要由它來完成。如下面的代碼片段所示,IHttpApplication<TContext>接口的CreateContext和DisposeContext方法分別體現了針對執行上下文的創建和釋放,CreateContext方法的參數contextFeatures表示描述原始上下文的特性集合。在此上下文中針對請求的處理實現在另一個方法ProcessRequestAsync之中。
1: public interface IHttpApplication<TContext>
2: {
3: TContext CreateContext(IFeatureCollection contextFeatures);
4: void DisposeContext(TContext context, Exception exception);
5: Task ProcessRequestAsync(TContext context);
6: }
在默認情況下創建的HttpApplication是一個HostingApplication對象。對於HostingApplication來說,它創建的執行上下文的類型是一個具有如下定義的結構Context。對於這個Context對象表示的針對當前請求的執行上下文來說,描述當前HTTP請求的HttpContext是最為核心的部分。除了這個HttpContext屬性之外,Context還具有額外兩個屬性,其中Scope是為追蹤診斷而創建的日志上下文范圍,該范圍將針對同一個請求的多項日志記錄進行關聯,而另一個屬性StartTimestamp表示應用開始處理請求的時間戳。
1: public class HostingApplication : IHttpApplication<Context>
2: {
3: //省略成員
4: public struct Context
5: {
6: public HttpContext HttpContext { get; set; }
7: public IDisposable Scope { get; set; }
8: public long StartTimestamp { get; set; }
9: }
10: }
由於HostingApplication針對請求的處理是通過注冊的中間件來完成的,而這些中間件最終會利用上面介紹的ApplicationBuilder對象轉換成一個類型為RequestDelegate的委托對象,所有中間件對請求的處理通過執行這個委托對象來完成。我們在創建HostingApplication的時候需要提供這麼一個RequestDelegate對象。由HostingApplication創建的Context對象包含表示HTTP上下文的HttpContext對象,而後者是通過對應的工廠HttpContextFactory創建的,所以HttpContextFactory在創建時也是必須要提供的。如下面的代碼片段所示,HostingApplication類型的構造函數需要將這兩個對象作為輸入參數,至於另外兩個參數(logger和diagnosticSource),它們與日志記錄有關。
1: public class HostingApplication : IHttpApplication<HostingApplication.Context>
2: {
3: private readonly RequestDelegate _application;
4: private readonly DiagnosticSource _diagnosticSource;
5: private readonly IHttpContextFactory _httpContextFactory;
6: private readonly ILogger _logger;
7:
8: public HostingApplication(RequestDelegate application, ILogger logger, DiagnosticSource diagnosticSource, IHttpContextFactory httpContextFactory)
9: {
10: _application = application;
11: _logger = logger;
12: _diagnosticSource = diagnosticSource;
13: _httpContextFactory = httpContextFactory;
14: }
15: }
下面給出的代碼片段基本體現了HostingApplication創建和釋放Context對象,以及在此上下文中處理請求的邏輯。在CreateContext方法中,它直接利用初始化提供的HttpContextFactory創建一個HttpContext並將其作為Context對象的同名屬性,至於Context額外兩個屬性(Scope和StartTimestamp)該作何設置,我們會在本節後續部分對此作專門介紹。實現在ProcessRequestAsync方法中針對請求的處理最終體現在對構造時指定的這個RequestDelegate對象的執行。當DisposeContext方法被執行的時候,Context的Scope屬性會率先被釋放,在此之後HttpContextFactory的Dispose方法被調用以完成對Context對象自身的回收釋放。
1: public class HostingApplication : IHttpApplication<HostingApplication.Context>
2: {
3: public Context CreateContext(IFeatureCollection contextFeatures)
4: {
5: //省略其他實現代碼
6: return new Context
7: {
8: HttpContext = _httpContextFactory.Create(contextFeatures),
9: Scope = ...,
10: StartTimestamp = ...
11: };
12: }
13:
14: public Task ProcessRequestAsync(Context context)
15: {
16: Return _application(context.HttpContext);
17: }
18:
19: public void DisposeContext(Context context, Exception exception)
20: {
21: //省略其他實現代碼
22: context.Scope.Dispose();
23: _httpContextFactory.Dispose(context.HttpContext);
24: }
25: }
跨平台是ASP.NET Core一個顯著的特性,而KestrelServer是目前微軟推出了唯一一個能夠真正跨平台的服務器。KestrelServer利用一個名為KestrelEngine的網絡引擎實現對請求的監聽、接收和響應。KetrelServer之所以具有跨平台的特質,源於KestrelEngine是在一個名為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這個類型的定義。除了實現接口IServer定義的Features屬性之外,KestrelServer還具有一個類型為KestrelServerOptions的只讀屬性Options。這個屬性表示對KestrelServer所作的相關設置,我們在調用構造函數時通過輸入參數options所代表的IOptions<KestrelServerOptions>對象對這個屬性進行初始化。構造函數還具有另兩個額外的參數,它們的類型分別是IApplicationLifetime和ILoggerFactory,後者用於創建記錄日志的Logger,前者與應用的生命周期管理有關。
1: public class KestrelServer : IServer
2: {
3: public IFeatureCollection Features { get; }
4: public KestrelServerOptions Options { get; }
5:
6: public KestrelServer(IOptions<KestrelServerOptions> options, IApplicationLifetime applicationLifetime, ILoggerFactory loggerFactory);
7: public void Dispose();
8: public void Start<TContext>(IHttpApplication<TContext> application);
9: }
注冊的KetrelServer在管道中會以依賴注入的方式被創建,並采用構造器注入的方式提供其構造函數的參數options,由於這個參數類型為IOptions<KestrelServerOptions>,所以我們利用Options模型以配置的方式來指定KestrelServerOptions對象承載的設置。比如我們可以將KestrelServer的相關配置定義在如下一個JSON文件中。
1: {
2: "noDelay" : false,
3: "shutdownTimeout" : "00:00:10",
4: "threadCount" : 10
5: }
為了讓應用加載這麼一個配置文件(文件名假設為“KestrelServerOptions.json”),我們只需要按照如下的方式利用ConfigurationBuilder加載這個配置文件並生成相應的Configuration對象,最後按照Options模型的編程方式完成KestrelServerOptions類型和該對象的映射即可。針對KestrelServerOptions的服務注冊也可以定義在啟動類型的ConfigureServices方法中。
1: IConfiguration config = new ConfigurationBuilder()
2: .AddJsonFile("KestrelServerOptions.json")
3: .Build();
4:
5: new WebHostBuilder()
6: .UseKestrel()
7: .ConfigureServices(services=>services.Configure<KestrelServerOptions>(config))
8: .Configure(app => app.Run(async context => await context.Response.WriteAsync("Hello World")))
9: .Build()
10: .Run();
我們一般通過調用WebHostBuilder的擴展方法UseKestrel方法來完成對KestrelServer的注冊。如下面的代碼片段所示,UseKestrel方法具有兩個重載,其中一個具有同一個類型為Action<KestrelServerOptions>的參數,我們可以利用這個參數直接完成對KestrelServerOptions的設置。
1: public static class WebHostBuilderKestrelExtensions
2: {
3: public static IWebHostBuilder UseKestrel(this IWebHostBuilder hostBuilder);
4: public static IWebHostBuilder UseKestrel(this IWebHostBuilder hostBuilder, Action<KestrelServerOptions> options);
5: }
由於服務器負責請求的監聽、接收和響應,所以Server是影響整個Web應用響應能力和吞吐量最大的因素之一,為了更加有效地使用服務器,我們往往針對具體的網絡負載狀況對其作針對性的設置。對於KestrelServer來說,在構造函數中作為參數指定的KestrelServerOptions對象代表針對它所做的設置。我們針對KestrelServer所做的設置主要體現在KestrelServerOptions類型的如下5個屬性上。
1: public class KestrelServerOptions
2: {
3: //省略其他成員
4: public int MaxPooledHeaders { get; set; }
5: public int MaxPooledStreams { get; set; }
6: public bool NoDelay { get; set; }
7: public TimeSpan ShutdownTimeout { get; set; }
8: public int ThreadCount { get; set; }
9: }
在演示的實例中,我們實際上並不曾為注冊的KestrelServer指定一個監聽地址,從運行的效果我們不難看出,WebHost在這種情況下會指定“http://localhost:5000”為默認的監聽地址。服務器的監聽地址自然可以顯式指定。在介紹如何通過編程的方式為服務器指定監聽地址之前,我們有先來認識一個名為ServerAddressesFeature的特性。
我們知道表示服務器的接口IServer中定義了一個類型為IFeatureCollection 的只讀屬性Features,它表示用於描述當前服務器的特性集合,ServerAddressesFeature作為一個重要的特性,就包含在這個集合之中。我們所說的ServerAddressesFeature對象是對所有實現了IServerAddressesFeature接口的所有類型及其對應對象的統稱,該接口具有一個唯一的只讀屬性返回服務器的監聽地址列表。ASP.NET Core默認使用的ServerAddressesFeature是具有如下定義的同名類型。
1: public interface IServerAddressesFeature
2: {
3: ICollection<string> Addresses { get; }
4: }
5:
6: public class ServerAddressesFeature : IServerAddressesFeature
7: {
8: public ICollection<string> Addresses { get; }
9: }
對於WebHost在通過依賴注入的方式創建的服務器,由它的Features屬性表示的特性集合中會默認包含這麼一個ServerAddressesFeature對象。如果沒有一個合法的監聽地址被添加到這個 ServerAddressesFeature對象的地址列表中,WebHost會將顯式指定的地址(一個或者多個)添加到該列表中。我們顯式指定的監聽地址實際上是作為WebHost的配置保存在一個Configuration對象上,配置項對應的Key為“urls”,WebHostDefaults的靜態只讀屬性ServerUrlsKey返回的就是這麼一個Key。
1: new WebHostBuilder()
2: .UseSetting(WebHostDefaults.ServerUrlsKey, "http://localhost:3721/")
3: .UseMyKestrel()
4: .UseStartup<Startup>()
5: .Build()
6: .Run();
WebHost的配置最初來源於創建它的WebHostBuilder,後者提供了一個UseSettings方法來設置某個配置項的值,所以我們可以采用如上的方式來指定監聽地址(“http://localhost:3721/”)。不過,針對監聽地址的顯式設置,最直接的編程方式還是調用WebHostBuilder的擴展方法UseUrls,如下面的代碼片段所示,該方法的實現邏輯與上面完全一致。
1: public static class WebHostBuilderExtensions
2: {
3: public static IWebHostBuilder UseUrls(this IWebHostBuilder hostBuilder, params string[] urls)
4: =>hostBuilder.UseSetting(WebHostDefaults.ServerUrlsKey, string.Join(ServerUrlsSeparator, urls)) ;
5: }