程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> .NET網頁編程 >> 關於.NET >> 學習ASP.NET Core,怎能不了解請求處理管道[2]: 服務器在管道中的“龍頭”地位,asp.netcore

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

編輯:關於.NET

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


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>接口采用泛型參數的形式來表示定義這個上下文的類型。

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支持范圍之內。

如下所示的代碼片段體現了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,它表示用於描述當前服務器的特性集合,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: }

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved