通过本篇文章,我将管道最核心的部分提取出来构建一个“迷你版”的ASP.NET Core框架。较之真正的ASP.NET Core框架,虽然重建的模拟框架要简单很多,但是它们采用完全一致的设计。为了能够在真实框架中找到对应物,在定义接口或者类型时会采用真实的名称,但是在API的定义上会做最大限度的简化。
HttpContext
一个HttpContext对象表示针对当前请求的上下文。要理解HttpContext上下文的本质,需要从请求处理管道的层面来讲。对于由一个服务器和多个中间件构成的管道来说,面向传输层的服务器负责请求的监听、接收和最终的响应,当它接收到客户端发送的请求后,需要将请求分发给后续中间件进行处理。对于某个中间件来说,完成自身的请求处理任务之后,在大部分情况下需要将请求分发给后续的中间件。请求在服务器与中间件之间,以及在中间件之间的分发是通过共享上下文的方式实现的。如下图所示,当服务器接收到请求之后,会创建一个通过HttpContext表示的上下文对象,所有中间件都在这个上下文中完成针对请求的处理工作。那么一个HttpContext对象究竟会携带什么样的上下文信息?一个HTTP事务(Transaction)具有非常清晰的界定,如果从服务器的角度来说就是始于请求的接收,而终于响应的回复,所以请求和响应是两个基本的要素,也是HttpContext承载的最核心的上下文信息。
我们可以将请求和响应理解为一个Web应用的输入与输出,既然HttpContext上下文是针对请求和响应的封装,那么应用程序就可以利用这个上下文对象得到当前请求所有的输入信息,也可以利用它完成我们所需的所有输出工作。所以,我们为ASP.NET Core模拟框架定义了如下这个极简版本的HttpContext类型。
public class HttpListenerFeature : IHttpRequestFeature, IHttpResponseFeature
{
private readonly HttpListenerContext _context;
public HttpListenerFeature(HttpListenerContext context)=> _context = context;
Uri IHttpRequestFeature.Url=> _context.Request.Url;
NameValueCollection IHttpRequestFeature.Headers=> _context.Request.Headers;
NameValueCollection IHttpResponseFeature.Headers=> _context.Response.Headers;
Stream IHttpRequestFeature.Body=> _context.Request.InputStream;
Stream IHttpResponseFeature.Body=> _context.Response.OutputStream;
int IHttpResponseFeature.StatusCode
{
get => _context.Response.StatusCode;
set => _context.Response.StatusCode = value;
}
}
如上面的代码片段所示,我们可以利用HttpRequest对象得到当前请求的地址、请求消息的报头集合和主体内容。利用HttpResponse对象,我们不仅可以设置响应的状态码,还可以添加任意的响应报头和写入任意的主体内容。
中间件
HttpContext对象承载了所有与当前请求相关的上下文信息,应用程序针对请求的响应也利用它来完成,所以可以利用一个Action类型的委托对象来表示针对请求的处理,我们姑且将它称为请求处理器(Handler)。但Action仅仅是请求处理器针对“同步”编程模式的表现形式,对于面向Task的异步编程模式,这个处理器应该表示成类型为Func的委托对象。
由于这个表示请求处理器的委托对象具有非常广泛的应用,所以我们为它专门定义了如下这个RequestDelegate委托类型,可以看出它就是对Func委托的表达。一个RequestDelegate对象表示的是请求处理器,那么中间件在模型中应如何表达?
public delegate Task RequestDelegate(HttpContext context);
作为请求处理管道核心组成部分的中间件可以表示成类型为Func的委托对象。换句话说,中间件的输入与输出都是一个RequestDelegate对象。我们可以这样来理解:对于管道中的某个中间件(下图所示的第一个中间件)来说,后续中间件组成的管道体现为一个RequestDelegate对象,由于当前中间件在完成了自身的请求处理任务之后,往往需要将请求分发给后续中间件进行处理,所以它需要将后续中间件构成的RequestDelegate对象作为输入。
当代表当前中间件的委托对象执行之后,如果将它自己“纳入”这个管道,那么代表新管道的RequestDelegate对象就成为该委托对象执行后的输出结果,所以中间件自然就表示成输入和输出类型均为RequestDelegate的Func对象。
中间件管道的构建
从事软件行业10多年来,笔者对架构设计越来越具有这样的认识:好的设计一定是“简单”的设计。所以在设计某个开发框架时笔者的目标是再简单点。上面介绍的请求处理管道的设计就具有“简单”的特质:Pipeline = Server + Middlewares。但是“再简单点”其实是可以的,我们可以将多个中间件组成一个单一的请求处理器。请求处理器可以通过RequestDelegate对象来表示,所以整个请求处理管道将具有更加简单的表达:Pipeline = Server + RequestDelegate。
表示中间件的Func对象向表示请求处理器的RequestDelegate对象之间的转换是通过IApplicationBuilder对象来完成的。从接口命名可以看出,IApplicationBuilder对象是用来构建“应用程序”(Applicatio