通过重建Hosting系统理解HTTP请求在ASP.NET Core管道中的处理流程[中]:管道如何处理请求


从上面的内容我们知道ASP.NET Core请求处理管道由一个服务器和一组中间件构成,所以从总体设计来讲是非常简单的。但是就具体的实现来说,由于其中涉及很多对象的交互,很少人能够地把它弄清楚。如果想非常深刻地认识ASP.NET Core的请求处理管道,我觉得可以分两个步骤来进行:首先,我们可以在忽略具体细节的前提下搞清楚管道处理HTTP请求的总体流程;在对总体流程有了大致了解之后,我们再来补充这些刻意忽略的细节。为了让读者朋友们能够更加容易地理解管道处理HTTP请求的总体流程,我们根据真实管道的实现原理再造了一个“迷你版的管道”。[本文已经同步到《ASP.NET Core框架揭秘》之中] [源代码从这里下载]

目录
一、建立在“模拟管道”上的应用
二、HttpApplication——一组中间件的有序集合
三、HttpContext——对当前HTTP上下文的抽象
四、服务器——实现对请求的监听、接收和响应

一、建立在“模拟管道”上的应用

再造的迷你管道不仅仅体现了真实管道中处理HTTP请求的流程,并且对于其中涉及的接口和类型,我们也基本上采用了相同的命名方式。但是为了避免“细枝末节”造成的干扰,我会进行最大限度的裁剪。对于大部分方法,我们只会保留最核心的逻辑。对于一些接口,我们会剔除那些与核心流程无关的成员。在通过这个模拟管道讲解HTTP请求的总体处理流程之前,我们先来看看如何在它基础上开发一个简单的应用。

我们在这个模拟管道上开发一个简单的应用来发布图片。具体的应用场景是这样:我们将图片文件保存在服务器上的某个目录下,客户端可以通过发送HTTP请求并在请求地址上指定文件名的方式来获取目标图片。如下图所示,我们利用浏览器向针对某张图片的地址(“http://localhost:3721/images/hello.png”)发送请求后,获取到的目标图片(hello.png)会直接显示到浏览器上。除此之外,如果指定的图片地址没有包含扩展名(“.png”),我们的也会帮助我们自动匹配一个文件名(不包含扩展名)相同的图片。

由于我们模拟的管道采用与真实管道一致的应用编程接口,所以两种采用的编程模式也是一致的。这个用于发布图片的应用是通过如下几行简单的代码构建起来的。如下面的代码片断所示,我们在Main方法中创建了一个WebHostBuilder对象,在调用其Build方法创建应用宿主的WebHost之前,我们调用扩展方法UseHttpListener注册了一个类型为HttpListenerServer的服务器。这个HttpListenerServer是我们自己定义的服务器,它利用一个HttpListener对象实现了针对HTTP请求的监听、接收和最终的响应。监听地址(“http://localhost:3721/images”)是通过调用扩展方法UseUrls指定的。

   1: public class Program
   2: {
   3:     public static void Main()
   4:     {
   5:         new WebHostBuilder()
   6:             .UseHttpListener()
   7:             .UseUrls("http://localhost:3721/images")
   8:             .Configure(app => app.UseImages(@"c:\images"))
   9:             .Build()
  10:             .Start();
  11:  
  12:         Console.Read();
  13:     }
  14: }

应用针对图片获取请求的处理是通过我们自定义的中间件完成的。在调用WebHostBuilder的Configure方法定义管道过程中,我们调用IApplicationBuilder接口的扩展方法UseImages完成了针对这个中间件的定制。在调用这个扩展方法的时候,我们指定了存放图片的目录(“c:\images”),我们通过浏览器获取的这个图片(“hello.png”)就保存在这个目录下。

二、HttpApplication——一组中间件的有序集合

ASP.NET Core请求处理管道由一个服务器和一组有序排列的中间件组合而成。我们可以在这基础上作进一步个抽象,将后者抽象成一个HttpApplication对象,那么该管道就成了一个Server和HttpApplication的综合体(如下图所示)。Server会将接收到的HTTP请求转发给HttpApplication对象,后者会针对当前请求创建一个上下文,并在此上下文中处理请求,请求处理完成并完成响应之后HttpApplication会对此上下文实施回收释放处理。

我们通过具有如下定义的IHttpApplication类型来表示上述的这个HttpApplication,泛型参数TContext代表它针对每个请求而建立的上下文。一个HttpApplication对象在接收到Server转发的请求之后需要完成三项基本的操作,即创建上下文在上下文中处理请求以及请求处理完成之后释放上下文,这三个基本操作正好通过对应的三个方法来完成。

   1: public interface IHttpApplication
   2: {
   3:     TContext CreateContext(IFeatureCollection contextFeatures); 
   4:     Task ProcessRequestAsync(TContext context);
   5:     void DisposeContext(TContext context, Exception exception);
   6: }

用于创建上下文的CreateContext方法具有一个类型为IFeatureCollection接口的参数。顾名思义,这个接口用于描述某个对象所具有的一组特性,我们可以将它视为一个Dictionary对象,字典对象的Value代表特性对象,Key则表示该对象的注册类型(可以是特性描述对象的真实类型、真实类型的基类或者实现的接口)。我们可以调用Get方法根据指定的注册类型得到设置的特性对象,特性对象的注册则通过Set方法来完成。我们自定义的FeatureCollection类型采用最简单的方式实现了这个接口。

   1: public interface IFeatureCollection
   2: {
   3:     TFeature Get();
   4:     void Set(T instance);
   5: }
   6:  
   7: public class FeatureCollection : IFeatureCollection
   8: {
   9:     private ConcurrentDictionaryobject> features = new ConcurrentDictionaryobject>();
  10:  
  11:     public TFeature Get()
  12:     {
  13:         object feature;
  14:         return features.TryGetValue(typeof(T), out feature) 
  15:             ? (T)feature 
  16:             : default(T);
  17:     }
  18:  
  19:     public void Set(T instance)
  20:     {
  21:         features[typeof(T)] = instance;
  22:     }
  23: }

管道采用的HttpApplication是一个类型为 HostingApplication的对象。如下面的代码片段所示,这个类型实现了接口IHttpApplication,泛型参数Context是一个针对当前请求的上下文对象。一个Context对象是对一个HttpContext的封装,后者是真正描述当前HTTP请求的上下文,承载着最为核心的上下文信息。除此之外,我们还为Context定义了Scope和StartTimestamp两个属性,两者与日志记录和事件追踪有关,前者被用来将针对同一请求的多次日志记录关联到同一个上下文范围(即Logger的BeginScope方法的返回值);后者表示开始处理请求的时间戳,如果在完成请求处理的时候记录下当前的时间戳,我们就可以计算出整个请求处理所花费的时间。

   1: public class HostingApplication : IHttpApplication
   2: {
   3:     //省略成员定义
   4: }
   5:  
   6: public class Context
   7: {
   8:     public HttpContext     HttpContext { get; set; }
   9:     public IDisposable     Scope { get; set; }
  10:     public long            StartTimestamp { get; set; }
  11: }

下图所示的UML体现了与HttpApplication相关的核心接口/类型之间的关系。总得来说,通过泛型接口IHttpApplication表示HttpApplication是对注册的中间件的封装。HttpApplication在一个自行创建的上下文中完成对服务器接收请求的处理,而上下文根据表述原始HTTP上下文的特性集合来创建,这个特性集合通过接口IFeatureCollection来表示,FeatureCollection是该接口的默认实现者。ASP.NET Core 默认使用的HttpApplication是一个HostingApplication对象,它创建的上下文是一个Context对象,一个Context对象是对一个HttpContext和其他与日志相关上下文信息的封装。

三、HttpContext——对当前HTTP上下文的抽象

用来描述当前HTTP请求的上下文的HttpContext对于ASP .NET Core请求处理管道来说是一个非常重要的对象,我们不仅仅可以利用它获取当前请求的所有细节,还可以直接利用它完成对请求的响应。HttpContext是一个抽象类,很多用于描述当前HTTP请求的上下文信息的属性被定义在这个类型中。在这个这个模拟管道模型中,我们仅仅保留了如下两个核心的属性,即表示请求和响应的Requst和Response属性。

   1: public abstract class HttpContext
   2: {
   3:     public abstract HttpRequest     Request { get; }
   4:     public abstract HttpResponse    Response { get; }
   5: }

表示请求和响应的HttpRequest和HttpResponse同样是抽象类。简单起见,我们仅仅保留少数几个与演示实例相关的属性成员。如下面的代码片段所示,我们仅仅为HttpRequest保留了表示当前请求地址的Url属性和表示基地址的PathBase属性。对于HttpResponse来说,我们保留了三个分别表示输出流(OutputStream)、媒体类型(ContentType)和响应状态码(StatusCode)的属性。

   1: public abstract class HttpRequest
   2: {
   3:     public abstract Uri    Url { get; }
   4:     public abstract string PathBase { get; }
   5: }
   6:  
   7: public abstract class HttpResponse
   8: {
   9:     public abstract Stream     OutputStream { get; }
  10:     public abstract string     ContentType { get; set; }
  11:     public abstract int        StatusCode { get; set; }
  12: }

ASP.NET Core默认使用的HttpContext是一个类型为DefaultHttpContext对象,在介绍DefaultContext的实现原理之前,我们必须了解这样一个事实:对应这个管道来说,请求的接收者和最终响应者都是服务器,服务器接收到请求之后会创建自己的上下文来描述当前请求,针对请求的响应也通过这个原始上下文来完成。以我应用中注册的HttpListenerServer为例,由于它内部使用的是一个类型为HttpListener的监听器,所以它总是会创建一个HttpListenerContext对象来描述接收到的请求,针对请求的响应也是利用这个HttpListenerContext对象来完成的。

但是对于建立在管道上的应用来说,它们是不需要关注管道究竟采用了何种类型的服务器,更不会关注由这个服务器创建的这个原始上下文。实际上我们的应用不仅统一使用这个DefaultHttpContext对象来获取请求信息,同时还利用它来完成对请求的响应。很显然,应用这使用的这个DefaultHttpContext对象必然与服务器创建的原始上下文存在某个关联,这种关联是通过上面我们提到过的这个FeatureCollection对象来实现的。

如上图所示,不同类型的服务器在接收到请求的时候会创建一个原始的上下文,接下来它会将针对原始上下文的操作封装成一系列标准的特性对象(特性类型实现统一的接口)。这些特性对象最终服务器被组装成一个FeatureCollection对象,应用程序中使用的DefaultHttpContext就是根据它创建出来的。当我们调用DefaultHttpContext相应的属性和方法时,在它的内部实际上借助封装的特性对象去操作原始的上下文。

一旦了解DefaultHttpContext是如何操作原始HTTP上下文之后,对于DefaultHttpContext的定义就很好理解了。如下面的代码片断所示,DefaultHttpContext具有一个IFeatureCollection类型的属性HttpContextFeatures,它表示的正是由服务器创建的用于封装原始HTTP上下文相关特性的FeatureCollection对象。通过构造函数的定义我们知道对于一个DefaultHttpContext对象来说,表示请求和响应的分别是一个DefaultHttpRequest和DefaultHttpResponse对象。

   1: public class DefaultHttpContext : HttpContext
   2: { 
   3:     public IFeatureCollection HttpContextFeatures { get;}
   4:  
   5:     public DefaultHttpContext(IFeatureCollection httpContextFeatures)
   6:     {
   7:         this.HttpContextFeatures = httpContextFeatures;
   8:         this.Request      = new DefaultHttpRequest(this);
   9:         this.Response     = new DefaultHttpResponse(this);
  10:     }
  11:     public override HttpRequest      Request { get; }
  12:     public override HttpResponse     Response { get; }
  13: }

由不同类型的服务器创建的特性对象之所以能够统一被DefaultHttpContext所用,原因在于它们的类型都实现统一的接口,在模拟的管道模型中,我们定义了如下两个针对请求和响应的特性接口IHttpRequestFeature和IHttpResponseFeature,它们与HttpRequest和HttpResponse具有类似的成员定义。

   1: public interface IHttpRequestFeature
   2: {
   3:     Uri    Url { get; }
   4:     string PathBase { get; }
   5: }
   6:  
   7: public interface IHttpResponseFeature
   8: {
   9:     Stream     OutputStream { get; }
  10:     string     ContentType { get; set; }
  11:     int        StatusCode { get; set; }
  12: }

实际上DefaultHttpContext对象中表示请求和响应的DefaultHttpRequest和DefaultHttpResponse对象就是分别根据从提供的FeatureCollection中获取的HttpRequestFeature和HttpResponseFeature对象创建的,具体的实现体现在如下所示的代码片断中。

   1: public class DefaultHttpRequest : HttpRequest
   2: {
   3:     public IHttpRequestFeature RequestFeature { get; }
   4:     public DefaultHttpRequest(DefaultHttpContext context)
   5:     {
   6:         this.RequestFeature = context.HttpContextFeatures.Get();
   7:     }
   8:     public override Uri Url
   9:     {
  10:         get { return this.RequestFeature.Url; }
  11:     }
  12:  
  13:     public override string PathBase
  14:     {
  15:         get { return this.RequestFeature.PathBase; }
  16:     }
  17: }
  18: public class DefaultHttpResponse : HttpResponse
  19: {
  20:     public IHttpResponseFeature ResponseFeature { get; }
  21:  
  22:     public override Stream OutputStream
  23:     {
  24:         get { return this.ResponseFeature.OutputStream; }
  25:     }
  26:  
  27:     public override string ContentType
  28:     {
  29:         get { return this.ResponseFeature.ContentType; }
  30:         set { this.ResponseFeature.ContentType = value; }
  31:     }
  32:  
  33:     public override int StatusCode
  34:     {
  35:         get { return this.ResponseFeature.StatusCode; }
  36:         set { this.ResponseFeature.StatusCode = value; }
  37:     }
  38:  
  39:     public DefaultHttpResponse(DefaultHttpContext context)
  40:     {
  41:         this.ResponseFeature = context.HttpContextFeatures.Get();
  42:     }
  43: }

在了解了DefaultHttpContext的实现原理之后,我们在回头看看上面作为默认HttpApplication类型的HostingApplication的定义。由于对请求的处理总是在一个由HttpContext对象表示的上下文中进行,所以针对请求的处理最终可以通过具有如下定义的RequestDelegate委托对象来完成。一个HttpApplication对象可以视为对一组中间件的封装,它对请求的处理工作最终交给这些中间件来完成,所有中间件对请求的处理最终可以转换成一个RequestDelegate对象,HostingApplication的Application属性返回的就是这么一个RequestDelegate对象。

   1: public class HostingApplication : IHttpApplication
   2: {
   3:     public RequestDelegate Application { get; }
   4:  
   5:     public HostingApplication(RequestDelegate application)
   6:     {
   7:         this.Application = application;
   8:     }
   9:  
  10:     public Context CreateContext(IFeatureCollection contextFeatures)
  11:     {
  12:         HttpContext httpContext = new DefaultHttpContext(contextFeatures);
  13:         return new Context
  14:         {
  15:             HttpContext     = httpContext,
  16:             StartTimestamp  = Stopwatch.GetTimestamp()
  17:         };
  18:     }
  19:  
  20:     public void DisposeContext(Context context, Exception exception) => context.Scope?.Dispose();
  21:     public Task ProcessRequestAsync(Context context) => this.Application(context.HttpContext);
  22: }
  23:  
  24: public delegate Task RequestDelegate(HttpContext context);

当我们创建一个HostingApplication对象的时候,需要将所有注册的中间件转换成一个RequestDelegate类型的委托对象,并将其作为构造函数的参数,ProcessRequestAsync方法会直接利用这个委托对象来处理请求。当CreateContext方法被执行的时候,它会直接利用封装原始HTTP上下文的FeatureCollection对象创建一个DefaultHttpContext对象,进而一个Context对象。在简化的DisposeContext方法中,我们只是调用了Context对象的Scope属性的Dispose方法(如果Scope存在),实际上我们在创建Context的时候并没有Scope属性进行初始化。

我们依然通过一个UML对表示HTTP上下文相关的接口/类型及其相互关系进行总结。如下图8所示,针对当前请求的HTTP上下文通过抽象类HttpContext表示,请求和响应是HttpContext表述的两个最为核心的上下文请求,它们分别通过抽象类HttpRequest和HttpResponse表示。ASP.NET Core 默认采用的HttpContext类型为DefaultHttpContext,它描述的请求和响应分别是一个DefaultHttpRequst和DefaultHttpResponse对象。一个DefaultHttpContext对象由描述原始HTTP上下文的特性集合来创建,其中描述请求与相应的特性分别通过接口IHttpRequestFeature和IHttpResponseFeature表示,DefaultHttpRequst和DefaultHttpResponse正是分别根据它们创建的。

四、服务器——实现对请求的监听、接收和响应

管道中的服务器通过IServer接口表示,在模拟管道对应的应用编程接口中,我们只保留了两个核心成员,其中Features属性返回描述服务器的特性,而Start方法则负责启动服务器。Start方法被执行的时候,服务会马上开始实施监听工作。HTTP请求一旦抵达,该方法会利用作为参数的HttpApplication对象创建一个上下文,并在此上下文中完成对请求的所有处理操作。当完成了对请求的处理任务之后,HttpApplication对象会自行负责回收释放由它创建的上下文。

   1: public interface IServer
   2: {
   3:     IFeatureCollection Features { get; }
   4:     void Start(IHttpApplication application);    
   5: }

在我们演示的发布图片应用中使用的服务器是一个类型为HttpListenerServer的服务器。顾名思义,这个简单的服务器直接利用HttpListener来完成对请求的监听、接收和响应工作。这个HttpListener对象通过Listener这个只读属性表示,我们在构造函数中创建它。对于这个HttpListener,我们并没有直接为他指定监听地址,监听地址的获取是通过一个由IServerAddressesFeature接口表示的特性来提供的。如下面的代码片段所示,这个特性接口通过一个字符串集合类型的Addresses属性表示监听地址列表,ServerAddressesFeature是这个特性接口的默认实现类型。在构造函数中,我们在初始化Features属性之后,会添加一个ServerAddressesFeature对象到这个特性集合中。

   1: public class HttpListenerServer : IServer
   2: {
   3:     public HttpListener         Listener { get; }
   4:     public IFeatureCollection     Features { get; }
   5:  
   6:     public HttpListenerServer()
   7:     {
   8:         this.Listener = new HttpListener();
   9:         this.Features = new FeatureCollection()
  10:             .Set(new ServerAddressesFeature());
  11:     }
  12:     ...
  13: }
  14:  
  15: public interface IServerAddressesFeature
  16: {
  17:     ICollection<string> Addresses { get; }
  18: }
  19:  
  20: public class ServerAddressesFeature : IServerAddressesFeature
  21: {
  22:     public ICollection<string> Addresses { get; } = new Collection<string>();
  23: }

在Start方法中,我们从特性集合中提取出这个ServerAddressesFeature对象,并将设置的监听地址集合注册到HttpListener对象上,然后调用其Start方法开始监听来自网络的HTTP请求。HTTP请求一旦抵达,我们会调用HttpListener的GetContext方法得到表示原始HTTP上下文的HttpListenerContext对象,并根据它创建一个类型为HttpListenerContextFeature的特性对象,该对象分别采用类型IHttpRequestFeature和IHttpResponseFeature注册到创建的FeatureCollection对象上。作为参数的HttpApplication对象将它作为参数调用CreateContext方法创建出类型为TContext的上下文对象,我们最终将它作为参数调用HttpApplication对象的ProcessRequestAsync方法让注册的中间件来处理当前请求。当所有的请求处理工作结束之后,我们会调用HttpApplication对象的DisposeContext方法回收释放这个上下文。

   1: public class HttpListenerServer : IServer
   2: {
   3:     ...
   4:     public void Start(IHttpApplication application)
   5:     {
   6:         IServerAddressesFeature addressFeatures = this.Features.Get();
   7:         foreach (string address in addressFeatures.Addresses)
   8:         {
   9:             this.Listener.Prefixes.Add(address.TrimEnd('/') + "/");
  10:         }
  11:  
  12:         this.Listener.Start();
  13:         while (true)
  14:         {
  15:             HttpListenerContext httpListenerContext = this.Listener.GetContext();
  16:  
  17:             HttpListenerContextFeature feature = new HttpListenerContextFeature(httpListenerContext, this.Listener);
  18:             IFeatureCollection contextFeatures = new FeatureCollection()
  19:                 .Set(feature)
  20:                 .Set(feature);
  21:             TContext context = application.CreateContext(contextFeatures);
  22:  
  23:             application.ProcessRequestAsync(context)
  24:                 .ContinueWith(_ => httpListenerContext.Response.Close())
  25:                 .ContinueWith(_ => application.DisposeContext(context, _.Exception));
  26:         }
  27:     }
  28: }

由于HttpListenerServer采用一个HttpListener对象作为监听器,由它接收的请求将被封装成一个类型为HttpListenerContext的上下文对象。我们通过一个HttpListenerContextFeature类型来封装这个HttpListenerContext对象。如下面的代码片段所示,HttpListenerContextFeature实现了IHttpRequestFeature和IHttpResponseFeature接口,HttpApplication所代表的中间件不仅仅利用这个特性获取所有与请求相关的信息,而且针对请求的任何响应也都是利用这个特性来实现的。

   1: public class HttpListenerContextFeature : IHttpRequestFeature, IHttpResponseFeature
   2: {
   3:     private readonly HttpListenerContext context;    
   4:  
   5:     public string ContentType
   6:     {
   7:         get { return context.Response.ContentType; }
   8:         set { context.Response.ContentType = value; }
   9:     }
  10:  
  11:     public Stream OutputStream { get; }
  12:  
  13:     public int StatusCode
  14:     {
  15:         get { return context.Response.StatusCode; }
  16:         set { context.Response.StatusCode = value; }
  17:     }
  18:  
  19:     public Uri Url { get; }
  20:     public string PathBase { get; }
  21:  
  22:     public HttpListenerContextFeature(HttpListenerContext context, HttpListener listener)
  23:     {
  24:         this.context = context;
  25:         this.Url = context.Request.Url;
  26:         this.OutputStream = context.Response.OutputStream;
  27:         this.PathBase = (from it in listener.Prefixes
  28:           let pathBase = new Uri(it).LocalPath.TrimEnd('/')
  29:           where context.Request.Url.LocalPath.StartsWith(pathBase, StringComparison.OrdinalIgnoreCase)
  30:           select pathBase).First();
  31:     }
  32: }

下图所示的UML体现了与服务器相关的接口/类型之间的关系。通过接口IServer表示的服务器表示管道中完成请求监听、接收与相应的组件,我们自定义的HttpListenerServer利用一个HttpListener实现了这三项基本操作。当HttpListenerServer接收到抵达的HTTP请求之后,它会将表示原始HTTP上下文的特性封装成一个HttpListenerContextFeature对象,HttpListenerContextFeature实现了分别用于描述请求和响应特性的接口IHttpRequestFeature和IHttpResponseFeature,HostingApplication可以利用这个HttpListenerContextFeature对象来创建DefaultHttpContext对象。


通过重建Hosting系统理解HTTP请求在ASP.NET Core管道中的处理流程[上]:采用管道处理请求
通过重建Hosting系统理解HTTP请求在ASP.NET Core管道中的处理流程[中]:管道如何处理请求
通过重建Hosting系统理解HTTP请求在ASP.NET Core管道中的处理流程[下]:管道如何创建
源代码下载