閱讀608 返回首頁    go 微軟 go Office


ASP.NET Core真實管道詳解[2]:Server是如何完成針對請求的監聽、接收與響應的【上】

Server是ASP .NET Core管道的第一個節點,負責完整請求的監聽和接收,最終對請求的響應同樣也由它完成。Server是我們對所有實現了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。當Server在接收到抵達的請求之後,實際上會直接交給這個HttpApplication對象來處理,所以我們需要先來認識一下這個對象。

目錄
一、HttpApplication
二、請求的處理與執行上下文的創建與釋放
三、日誌記錄
    請求處理開始與結束時記錄的日誌
    針對請求的日誌上下文範圍
    請求唯一標識的生成

對於ASP.NET Core管道來說,。HttpApplication針對請求的處理實際上會在一個執行上下文中完成,這個上下文實際上為應用對單一請求的整個處理過程定義了一個邊界。單純描述HTTP請求的HttpContext是這個執行上下文中最為核心的部分,除此之外,我們還可以根據需要將其他相關的信息定義其中,所以IHttpApplication<TContext>接口采用泛型參數的形式來表示定義這個上下文的類型。

HttpApplication不僅僅需要在這個執行上下文中處理Server轉發給它的請求,這個上下文對象的創建和回收釋放同樣需要由它來完成。如下麵的代碼片段所示,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,它內嵌於HostingApplication類之中。對於這個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: }


三、日誌記錄

由於管道處理其中總是在一個由HttpApplication創建的執行上下文中進行,所有上下文的創建和回收釋放可以視為 整個請求處理流程開始和結束的標識。對於HostingApplication來說,CreateContext和DisposeContext方法分別被調用的時候,它會利用初始化時指定的Logger對象作相應的日誌記錄。除此之外,作為開始處理請求標誌的CreateContext方法還是創建一個日誌上下文範圍,其目的是將針對同一請求的日誌時間關聯起來。這個上下文範圍對應著Context對象的Scope對象,通過上麵的代碼片段我們可以看出針對這個日誌上下文範圍的釋放同樣發生在DisposeContext方法中。

接下來我們通過實例演示的形式來看看究竟怎樣的日誌消息分別被它的CreateContext和DisposeContext方法記錄下來。在一個ASP.NET Core控製台應用中,為了將記錄的日誌消息直接打印到控製台上,我們需要為管道使用的LoggerFactory注冊一個ConsoleLoggerProvider。在添加相應NuGet包(“Microsoft.Extensions.Logging.Console”)之後,我們定義了如下一個Startup類型,它采用構造函數注入的方式得到這個LoggerFactory並調用擴展方法AddConsole實現了對ConsoleLoggerProvider的注冊。

   1: public class Startup
   2: {
   3:     public Startup(ILoggerFactory loggerFactory)
   4:     {
   5:         
   6:     }
   7:  
   8:     public void Configure(IApplicationBuilder app)
   9:     {
  10:         app.Run(context => context.Response.WriteAsync("Hello World!"));
  11:     }
  12: }

我們啟動這個控製台應用讓它開始利用KestrelServer在默認的端口(5000)進行請求監聽,然後利用瀏覽器向對應的地址(我們將目標地址設定為“https://localhost:5000/helloworld”)發送請求,控製台上將會輸出管道在請求處理過程中寫入的日誌消息。如下所示的兩條等級為Information的日誌就是在開始和完成請求時分別被HostingApplication的CreateContext和DisposeContext方法寫入的。第一條日誌包含不僅僅包含請求的目標地址,還包括請求采用的協議(HTTP/1.1)和HTTP方法(GET),第二條則反映了整個請求處理過程所花的時間。

image

上麵演示的時候請求被正常處理的情況下管道自身記錄的日誌,如果在處理過程中拋出異常,該異常會作為參數傳遞給HostingApplication的DisposeContext方法,後者會額外寫入一條等級為Error的日誌記錄發生的錯誤。下麵的代碼片段展現了出現異常情況下寫入的三條日誌。

image

為了查看HostingApplication在CreateContext方法針對當前請求創建的日誌上下文範圍,我們在為LoggerFactory注冊ConsoleLoggerProvider的時候需要顯式開始針對日誌上下文範圍的支持,所以我們在調用AddConsole方法的時候將true作為額外的參數。除此之外,我們在Configure方法中利用注入的LoggerFactory創建相應的Logger,並利用它記錄一條等級為Information的日誌,日誌內容為“Write \"Hello World!\"”。

   1: public class Startup
   2: {
   3:     public Startup(ILoggerFactory loggerFactory)
   4:     {
   5:         loggerFactory.AddConsole();
   6:     }
   7:  
   8:     public void Configure(IApplicationBuilder app, ILoggerFactory loggerFactory)
   9:     {
  10:         app.Run(context =>
  11:         {
  12:             loggerFactory.CreateLogger("App").LogInformation("Write \"Hello World!\"");
  13:             return context.Response.WriteAsync("Hello World!");
  14:         });
  15:     }
  16: }

程序啟動後我們采用瀏覽器向相同的目標地址(“https://localhost:5000/helloworld”)發送兩次請求。對於這兩次請求記錄的日誌,它們分別是在不同的日誌上下文中被寫入的,我們可以根據這個上下文範圍對記錄下來的日誌消息進行有效地分組。針對這兩次請求,服務端一共有如下6條日誌消息被記錄下來,針對同一請求的三條日誌具有相同的上下文範圍信息,該體現不僅僅包含請求的路徑(“/helloworld”),還具有一個唯一標識請求的ID。

image

日誌上下文範圍攜帶的用於唯一標識當前請求的ID,同時也可以視為當前HttpContext的唯一標識,它對應著HttpContext的TranceIdentifier屬性。對於DefaultHttpContext來說,針對這個屬性的讀寫是借助一個名為HttpRequestIdentifierFeature的特性實現的,下麵的代碼提供了該對象對應的接口IHttpRequestIdentifierFeature和默認實現類HttpRequestIdentifierFeature的定義。

   1: public abstract class HttpContext
   2: {
   3:     //省略其他成員
   4:     public abstract string TraceIdentifier { get; set; }
   5: }
   6:  
   7: public interface IHttpRequestIdentifierFeature
   8: {
   9:     string TraceIdentifier { get; set; }
  10: }
  11:  
  12: public class HttpRequestIdentifierFeature : IHttpRequestIdentifierFeature
  13: {
  14:     private string _id;
  15:     private static long _requestId = DateTime.UtcNow.Ticks;
  16:     private static unsafe string GenerateRequestId(long id);
  17:     public string TraceIdentifier
  18:     {
  19:         get { return _id??(id= GenerateRequestId(Interlocked.Increment(ref _requestId)));}
  20:         set { this._id = value; }
  21:     }
  22: }

HttpRequestIdentifierFeature生成TraceIdentifier的邏輯很簡單。如上麵的代碼片斷所示,它具有一個靜態長整型字段_requestId,其初始值為當前時間戳。對於某個具體的HttpRequestIdentifierFeature對象來說,它的TraceIdentifier屬性的默認值返回的是這個字段_requestId加1之後轉換的字符串。具體的轉換邏輯定義在GenerateRequestId方法中,它會采用相應的算法 將指定的整數轉換一個長度為13的字符串(比如“0HKSDQNPC0424”)。


作者:蔣金楠
微信公眾賬號:大內老A
微博:www.weibo.com/artech
如果你想及時得到個人撰寫文章以及著作的消息推送,或者想看看個人推薦的技術資料,可以掃描左邊二維碼(或者長按識別二維碼)關注個人公眾號(原來公眾帳號蔣金楠的自媒體將會停用)。
本文版權歸作者和博客園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁麵明顯位置給出原文連接,否則保留追究法律責任的權利。
原文鏈接

最後更新:2017-10-25 13:33:33

  上一篇:go  ASP.NET Core真實管道詳解[1]:中間件是個什麼東西?
  下一篇:go  MaxCompute索引優化實踐