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”)。

作者:蒋金楠 
微信公众账号:大内老A
微博:www.weibo.com/artech

Server是如何完成针对请求的监听、接收与响应1的更多相关文章

  1. ASP.NET Core真实管道详解[2]:Server是如何完成针对请求的监听、接收与响应的【上】

    Server是ASP .NET Core管道的第一个节点,负责完整请求的监听和接收,最终对请求的响应同样也由它完成.Server是我们对所有实现了IServer接口的所有类型以及对应对象的统称,如下面 ...

  2. Self Host模式下的ASP. NET Web API是如何进行请求的监听与处理的?

    构成ASP.NET Web API核心框架的消息处理管道既不关心请求消息来源于何处,也不需要考虑响应消息归于何方.当我们采用Web Host模式将一个ASP.NET应用作为目标Web API的宿主时, ...

  3. 利用 chunked 类型响应实现后台请求的监听

    Koa 中实现 chunked 数据传输 中介绍了如何在 Koa 中实现 Transfer-Encoding:chunked 类型的响应分片传输.这里来看一个应用场景. 假如我们想监听后台的请求,并将 ...

  4. SpringBoot 对IBM MQ进行数据监听接收以及数据发送

    一.需求介绍 后端使用Spring Boot2.0框架,要实现IBM MQ的实时数据JMS监听接收处理,并形成回执通过MQ队列发送. 二.引入依赖jar包 <dependency> < ...

  5. andriod开发,简单的封装网络请求并监听返回.

    一.为什么封装 因为android 4.0 以后的发送网络请求必须要放到异步线程中,而异步线程必须跟handle合作才能更新主线程中的UI,所以建议用一个类继承handler来异步处理网络请求. 二. ...

  6. 权限管理demo-Http请求前后监听工具

    工具作用: 1. 输出每次请求的参数 2. 接口的请求时间 package com.mmall.common; import com.mmall.util.JsonMapper; import lom ...

  7. proxy写监听方法,实现响应式

    var data = { price: 5, quantity: 2 };var data_without_proxy = data; // 保存源对象data = new Proxy(data_wi ...

  8. React Native使用 DeviceEventEmitter发送通知emit和监听接收addListener的用法

    js 向 js 发送数据 DeviceEventEmitter.emit('自定义名称',发送数据);   例:边看边买退出登录之后,我的淘宝和详情页的钱包数据应该改变.这时,我们可以在退出登录请求返 ...

  9. js监听rem实现响应式

    原文链接:http://caibaojian.com/web-app-rem.html (function (doc, win) { var docEl = doc.documentElement, ...

随机推荐

  1. U3D navmesh寻路简单示范

    要求:放置一个BOSS,创建几个路标,自动循环这几个路标形成回路,变成自动巡逻,并配合animator系统的控制开关控制BOSS的动作 1.先设置好BOSS 中animator的控制开关,只看Run和 ...

  2. CodeForce 569A

    Time Limit:2000MS     Memory Limit:262144KB     64bit IO Format:%I64d & %I64u   Description Litt ...

  3. Quiz 6b Question 7————An Introduction to Interactive Programming in Python

     Question 7 Convert the following English description into code. Initialize n to be 1000. Initiali ...

  4. [Swust OJ 771]--奶牛农场(几何题,画图就好)

    题目链接:http://acm.swust.edu.cn/problem/771/    Description 将军有一个用栅栏围成的矩形农场和一只奶牛,在农场的一个角落放有一只矩形的箱子,有一天将 ...

  5. BZOJ 1880: [Sdoi2009]Elaxia的路线( 最短路 + dp )

    找出同时在他们最短路上的边(dijkstra + dfs), 组成新图, 新图DAG的最长路就是答案...因为两人走同一条路但是不同方向也可以, 所以要把一种一个的s,t换一下再更新一次答案 ---- ...

  6. BZOJ 1627: [Usaco2007 Dec]穿越泥地( BFS )

    BFS... --------------------------------------------------------------------------------------- #incl ...

  7. Eclipse之报错信息及其解决方案

    一.有很多人都喜欢开发js的时候用aptana,因此在eclipse中集成aptana插件是必须的,可是,在用link方式在eclipse中安装好aptana后,启动时会报如下错误 An intern ...

  8. jquery日历签到控件的实现

    calendar.js var calUtil = { //当前日历显示的年份 showYear:2015, //当前日历显示的月份 showMonth:1, //当前日历显示的天数 showDays ...

  9. plsql 的循环之 goto

    实例: /* 测试goto 的用法, */ procedure test_loop_go(pi_aab001 in number, po_fhz out varchar2, po_msg out va ...

  10. 网站压力测试之ApacheBench

    ApacheBench是 Apache 附带的一个小工具,专门用于 HTTP Server 的benchmark testing,可以同时模拟多个并发请求.使用yum安装apache,ab工具在/us ...