1. 程式人生 > >學習ASP.NET Core,怎能不瞭解請求處理管道[2]: 伺服器在管道中的“龍頭”地位

學習ASP.NET Core,怎能不瞭解請求處理管道[2]: 伺服器在管道中的“龍頭”地位

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物件來處理,所以我們需要先來認識一下這個物件。

一、HttpApplication

對於ASP.NET Core管道來說,HttpApplication對會接管伺服器接收的請求,後續的請求完全由它來負責。如下圖所示,HttpApplication從伺服器獲得請求之後,會利用註冊的中介軟體註冊對請求進行處理,並最終將請求遞交給應用程式。HttpApplication針對請求的處理實際上會在一個執行上下文中完成,這個上下文為應用對單一請求的整個處理過程定義了一個邊界。單純描述HTTP請求的HttpContext是這個執行上下文中最為核心的部分,除此之外,我們還可以根據需要將其他相關的資訊定義其中,所以IHttpApplication<TContext>介面採用泛型引數的形式來表示定義這個上下文的型別。

image

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: }

二、KestrelServer

跨平臺是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支援範圍之內。

4

如下所示的程式碼片段體現了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: }

三、ServerAddressesFeature

在演示的例項中,我們實際上並不曾為註冊的KestrelServer指定一個監聽地址,從執行的效果我們不難看出,WebHost在這種情況下會指定“http://localhost:5000”為預設的監聽地址。伺服器的監聽地址自然可以顯式指定。在介紹如何通過程式設計的方式為伺服器指定監聽地址之前,我們有先來認識一個名為ServerAddressesFeature的特性。

我們知道表示伺服器的介面IServer中定義了一個型別為IFeatureCollection 的只讀屬性Features,它表