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
二、請求的處理與執行上下文的創建與釋放
三、日志記錄
請求處理開始與結束時記錄的日志
針對請求的日志上下文范圍
請求唯一標識的生成
一、HttpApplication
對于ASP.NET Core管道來說,HttpApplication被用來處理Server接收的請求,這個對象可以視為對注冊的所有中間件的封裝,它對請求的處理工作實際上最終會委托這些中間件來完成。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: loggerFactory.AddConsole();
6: }
7:
8: public void Configure(IApplicationBuilder app)
9: {
10: app.Run(context => context.Response.WriteAsync("Hello World!"));
11: }
12: }
我們啟動這個控制臺應用讓它開始利用KestrelServer在默認的端口(5000)進行請求監聽,然后利用瀏覽器向對應的地址(我們將目標地址設定為“http://localhost:5000/helloworld”)發送請求,控制臺上將會輸出管道在請求處理過程中寫入的日志消息。如下所示的兩條等級為Information的日志就是在開始和完成請求時分別被HostingApplication的CreateContext和DisposeContext方法寫入的。第一條日志包含不僅僅包含請求的目標地址,還包括請求采用的協議(HTTP/1.1)和HTTP方法(GET),第二條則反映了整個請求處理過程所花的時間。
上面演示的時候請求被正常處理的情況下管道自身記錄的日志,如果在處理過程中拋出異常,該異常會作為參數傳遞給HostingApplication的DisposeContext方法,后者會額外寫入一條等級為Error的日志記錄發生的錯誤。下面的代碼片段展現了出現異常情況下寫入的三條日志。
針對請求的日志上下文范圍
為了查看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(true);
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: }
程序啟動后我們采用瀏覽器向相同的目標地址(“http://localhost:5000/helloworld”)發送兩次請求。對于這兩次請求記錄的日志,它們分別是在不同的日志上下文中被寫入的,我們可以根據這個上下文范圍對記錄下來的日志消息進行有效地分組。針對這兩次請求,服務端一共有如下6條日志消息被記錄下來,針對同一請求的三條日志具有相同的上下文范圍信息,該體現不僅僅包含請求的路徑(“/helloworld”),還具有一個唯一標識請求的ID。
請求唯一標識的生成
日志上下文范圍攜帶的用于唯一標識當前請求的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”)。
文章列表