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
二、请求的处理与执行上下文的创建与释放
三、日志记录
请求处理开始与结束时记录的日志
针对请求的日志上下文范围
请求唯一标识的生成
一、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”)。