学习ASP.NET Core,怎能不了解请求处理管道[1]: 中间件究竟是个什么东西?
Posted tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了学习ASP.NET Core,怎能不了解请求处理管道[1]: 中间件究竟是个什么东西?相关的知识,希望对你有一定的参考价值。
ASP.NET Core管道虽然在结构组成上显得非常简单,但是在具体实现上却涉及到太多的对象,所以我们在 “通过重建Hosting系统理解HTTP请求在ASP.NET Core管道中的处理流程”(上篇、中篇、下篇) 中围绕着一个经过极度简化的模拟管道讲述了真实管道构建的方式以及处理HTTP请求的流程。在本系列 中,我们会还原构建模拟管道时可以舍弃和改写的部分,向读者朋友们呈现一个真是的HTTP请求处理管道。 ASP.NET Core 的请求处理管道由一个服务器与一组有序排列的中间件构成,前者仅仅完成请求监听、接收和响应这些与底层网络相关的工作,至于请求接收之后和响应之前的所有工作都交给中间件来完成。ASP.NET Core的中间件通过一个类型Func<RequestDelegate, RequestDelegate>的委托对象来表示,而RequestDelegate也是一个委托,它代表一项请求处理任务。 [本文已经同步到《ASP.NET Core框架揭秘》之中]
目录
一、RequestDelegate
二、HttpContext
FeatureCollection
DefaultHttpContext
HttpContextFactory
三、ApplicationBuilder
ApplicationBuilderFactory
中间件类型
中间件类型的注册
一、RequestDelegate
服务器接受到抵达的HTTP请求之后会构建一个描述当前请求的原始上下文,服务器的类型决定了这个原始上下文的类型,比如在我们模拟管道默认采用的HttpListenerServer由于采用HttpListener来监听、接收并响应请求,所以它对应的原始上下文是一个HttpListenerContext对象。但是对于管道的后续部分,即由注册的中间件构建的链表,它们需要采用统一的方式来处理请求,所以服务器最终会根据原始的上下文来创建一个抽象的HTTP上下文,后者通过抽象类HttpContext来表示。
我们不仅可以利用这个HttpContext获取描述当前请求的上下文信息,同样可以利用它来实现对响应的控制。针对当前请求的任何处理操作总是在这么一个上下文中进行,所以一项请求处理任务完全可以抽象成一个类型Func<HttpContext,Task>的委托来表示,实际上具有如下定义的RequestDelegate委托具有类似的定义。
1: public delegate Task RequestDelegate(HttpContext context);
每个中间件都承载着独立的请求处理任务,它本质上也体现了在当前HttpContext下针对请求的处理操作,那么为什么中间件不直接通过一个RequestDelegate对象来表示,而是表示为一个类型为Func<RequestDelegate, RequestDelegate>的委托对象呢?原因很简单,中间件并不孤立地存在,所有注册的中间件最终会根据注册的先后顺序组成一个链表,每个中间件不仅仅需要完成各自的请求处理任务外,还需要驱动链表中的下一个中间件。
如上图所示,对于一个由多个Func<RequestDelegate, RequestDelegate>对象组成的中间链表来说,某个中间件会将后一个Func<RequestDelegate, RequestDelegate>对象的返回值作为输入,而自身的返回值则作为前一个中间件的输入。某个中间件执行之后返回的RequestDelegate对象不仅仅体现了自身对请求的处理操作,而是体现了包含自己和后续中间件一次对请求的处理。那么对于第一个中间件来说,它执行后返回的RequestDelegate对象实际上体现了整个应用对请求的处理逻辑。
二、 HttpContext
对当前上下文的抽象解除了管道对具体服务器类型的依赖, 这使我们可以为ASP.NET Core应用自由地选择承载(Hosting)方式,而不是像传统的ASP.NET应用一样只能寄宿在IIS之中。抽象HTTP上下文的目的是为了实现对请求处理流程的抽象,只有这样我们才能将针对请求的某项操作体现在一个标准的中间件上,有了这个这个标准化的中间件才有所谓的请求处理管道。
ASP.NET Core通过具有如下所示的HttpContext类来表示这么一个抽象的HTTP上下文。对于一个HttpContext对象来说,它的核心体现在用于描述请求和响应的Request和Response属性之上。除此之外,我们还可以通过它获取与当前请求相关的其他上下文信息,比如用来控制用户认证的AuthenticationManager对象和代表当前请求用户的ClaimsPrincipal对象,以及描述当前HTTP连接的ConnectionInfo对象和用于控制WebSocket的WebSocketManager。我们可以获取并控制当前会话,也可以获取或者设置调试追踪的ID。
1: public abstract class HttpContext
2: {
3:
4: public abstract HttpRequest Request { get; }
5: public abstract HttpResponse Response { get; }
6:
7: public abstract AuthenticationManager Authentication { get; }
8: public abstract ClaimsPrincipal User { get; set; }
9: public abstract ConnectionInfo Connection { get; }
10: public abstract WebSocketManager WebSockets { get; }
11: public abstract ISession Session { get; set; }
12: public abstract string TraceIdentifier { get; set; }
13: public abstract CancellationToken RequestAborted { get; set; }
14: public abstract IDictionary<object, object> Items { get; set; }
15:
16: public abstract IServiceProvider RequestServices { get; set; }
17: public abstract IFeatureCollection Features { get; }
18: }
当需要中指对请求的处理时,我们可以通过为RequestAborted属性设置一个CancellationToken对象从而将终止通知发送给管道。如果需要对整个管道共享一些与当前上下文相关的数据,我们可以将它保存在通过Items属性表示的字典中。我们一再提到依赖注入被广泛地应用ASP.NET Core管道中,HttpContext的RequestServices属性返回的根据在应用启动时注册的服务而创建的ServiceProvider。只要相应的服务被预先注册到指定的服务接口上,我们就可能利用这个ServiceProvider根据这个接口得到对应的服务对象。
1: public abstract class HttpRequest
2: {
3: public abstract HttpContext HttpContext { get; }
4: public abstract string Method { get; set; }
5: public abstract string Scheme { get; set; }
6: public abstract bool IsHttps { get; set; }
7: public abstract HostString Host { get; set; }
8: public abstract PathString PathBase { get; set; }
9: public abstract PathString Path { get; set; }
10: public abstract QueryString QueryString { get; set; }
11: public abstract IQueryCollection Query { get; set; }
12: public abstract string Protocol { get; set; }
13: public abstract IHeaderDictionary Headers { get; } >
14: public abstract IRequestCookieCollection Cookies { get; set; }
15: public abstract string ContentType { get; set; }
16: public abstract Stream Body { get; set; }
17: public abstract bool HasFormContentType { get; }
18: public abstract IFormCollection Form { get; set; }
19:
20: public abstract Task<IFormCollection> ReadFormAsync(CancellationToken cancellationToken);
21: }
如上所示的是抽象类HttpRequest是对HTTP请求的描述,它是HttpContext的只读属性Request的返回类型。我们可以利用这个对象获取到描述当前请求的各种相关信息,比如请求的协议(HTTP或者HTTPS)、HTTP方法、地址,也可以获取代表请求的HTTP消息的首部和主体。
在了解了表示请求的抽象类HttpRequest之后,我们再来认识一个与之相对的用于描述响应HttpResponse类型。如下面的代码片断所示,HttpResponse依然是一个抽象类,我们可以通过定义在它之上的属性和方法来控制对请求的响应。从原则上讲,我们对请求的所做的任意类型的响应都可以利用它来说实现。当我们通过表示当前上下文的HttpContext对象得到表示响应的HttpResponse之后,我们不仅仅可以将希望的内容写入响应消息的主体,还可以设置响应状态码以及添加相应的首部。
1: public abstract class HttpResponse
2: {
3: public abstract HttpContext HttpContext { get; }
4: public abstract int StatusCode { get; set; }
5: public abstract IHeaderDictionary Headers { get; }
6: public abstract Stream Body { get; set; }
7: public abstract long? ContentLength { get; set; }
8: public abstract IResponseCookies Cookies { get; }
9: public abstract bool HasStarted { get; }
10:
11: public abstract void OnStarting(Func<object, Task> callback, object state);
12: public virtual void OnStarting(Func<Task> callback);
13: public abstract void OnCompleted(Func<object, Task> callback, object state);
14: public virtual void RegisterForDispose(IDisposable disposable);
15: public virtual void OnCompleted(Func<Task> callback);
16: public virtual void Redirect(string location);
17: public abstract void Redirect(string location, bool permanent);
18: }
FeatureCollection
HttpContext的另一个只读属性Features返回一组“特性”对象。在ASP.NET Core管道式处理设计中,特性是一个非常重要的概念,特性是实现抽象化HttpContext的途径。具体来说,服务器在接收到请求之后会创建一个由自身类型决定的原始的上下文,管道不仅仅利用这个原始上下文来获取与请求相关的信息,它对请求的最终响应实际上也是通过这个原始上下文来完成的。所以对一个HttpContext对象来说,由它描述的上下文信息不仅仅来源于这个原始的上下文,我们针对HttpContext所做的任何响应操作最终都需要分发给这个原始上下文来完成, 否则是不会生效的。抽象的HttpContext和原始上下文之间的“双向绑定”究竟是如何实现的呢?
这个所谓的“双向绑定”即使其实很简单。当原始上下文被创建出来之后,服务器会将它封装成一系列标准的特性对象,HttpContext正是对这些特性对象的封装。一般来说,这些特性对象所对应的类型均实现了某个预定义的标准接口,接口中不仅仅定义相应的属性来读写原始上下文中描述的信息,还定义了相应的方法来操作原始上下文。HttpContext的属性Features返回的就是这组特性对象的集合,它的返回类型为IFeatureCollection,我们将实现了该接口的类型以及对应的对象统称为FeatureCollection。
1: public interface IFeatureCollection : IEnumerable<KeyValuePair<Type, object>>
2: {
3: TFeature Get<TFeature>();
4: void Set<TFeature>(TFeature instance);
5:
6: bool IsReadOnly { get; }
7: object this[Type key] { get; set; }
8: int Revision { get; }
9: }
一个FeatureCollection对象本质上就是一个Key和Value分别为Type和Object类型的字段,话句话说,特性对象通过对应的接口类型注册到HttpContext之上。我们通过调用Set方法将一个特性对象针对指定的类型(一般为特性接口)注册到这个字典对象上,并通过Get方法根据注册的类型获取它。特性对象的注册和获取也可以利用定义的索引来完成。如果IsReadOnly属性返回True,我们将不能注册新的特性或者修改已经注册的特性。 整数类型的之都属性Revision可以视为整个FeatureCollection对象的版本,不论是采用何种方式注册新的特性还是修改现有的特性,这个属性的值都将改变。
具有如下定义的FeatureCollection类实现了IFeatureCollection接口,我们默认使用的FeatureCollection就是这么一个类型的对象。FeatureCollection具有两个构造函数重载,默认无参构造函数帮助我们创建一个空的特性集合,另一个构造函数则需要指定一个FeatureCollection对象来提供默认特性。对于采用第二个构造函数重载创建的 FeatureCollection对象来说,当我们通过指定某个特性接口类型试图获取对应的特性对象时,如果对应的特性没有注册到当前FeatureCollection对象上,而是注册到提供默认特性的FeatureCollection对象上,后者将会提供最终的特性。
1: public class FeatureCollection : IFeatureCollection
2: {
3: //其他成员
4: public FeatureCollection();
5: public FeatureCollection(IFeatureCollection defaults);
6: }
对于FeatureCollection类型来说,它 的IsReadOnly总是返回False,所以它永远是可读可写的。对于调用默认无参构造函数创建的FeatureCollection对象来说,它 的Revision默认返回零。如果我们通过指定另一个FeatureCollection对象为参数调用第二个构造函数来创建一个FeatureCollection对象,前者的Revision属性值将成为后者同名属性的默认值。不论我们采用何种形式(调用Set方法或者索引)添加一个新的特性或者改变了一个已经注册的特性,FeatureCollection对象的Revision属性都将自动递增。上述的这些关于FeatureCollection的特性都体现在如下所示的代码片段中。
1: FeatureCollection defaults = new FeatureCollection();
2: Debug.Assert(defaults.Revision == 0);
3:
4: defaults.Set<IFoo>(new Foo());
5: Debug.Assert(defaults.Revision == 1);
6:
7: defaults[typeof(IBar)] = new Bar();
8: Debug.Assert(defaults.Revision == 2);
9:
10: FeatureCollection features = new FeatureCollection(defaults);
11: Debug.Assert(features.Revision == 2);
12: Debug.Assert(features.Get<IFoo>().GetType() == typeof(Foo));
13:
14: features.Set<IBaz>(new Baz());
15: Debug.Assert(features.Revision == 3);
DefaultHttpContext
ASP.NET Core默认使用的HttpContext类型为DefaultHttpContext,上面我们介绍的针对描述原始上下文“特性集合”来创建HttpContext的策略就体现在这个类型之上。DefaultHttpContext具有一个如下的构造函数,作为参数的FeatureCollection对象就是这么一个特性集合。
1: public class DefaultHttpContext : HttpContext
2: {
3: public DefaultHttpContext(IFeatureCollection features);
4: }
不论是组成管道的中间件还是建立在管道上的应用,在默认的情况下都是利用这个DefaultHttpContext对象来获取当前请求的相关信息,并利用这个对象来控制最终发送的响应。但是DefaultHttpContext对象这个这个过程中仅仅是一个“代理”,针对它的调用(属性或者方法)最终都需要转发给由具体服务器创建的那个原始上下文,在构造函数中指定的这个FeatureCollection对象所代表的特性集合成为了这两个上下文对象进行沟通的唯一渠道。对于定义在DefaultHttpContext中的所有属性,它们几乎都具有一个对应的特性,这些特性都对应着一个接口。表1列出了部分特性接口以及DefaultHttpContext对应的属性。
表1 描述原始HTTP上下文的特性接口
接口 |
属性 |
描述 |
IHttpRequestFeature |
Request |
获取描述请求的基本信息。 |
IHttpResponseFeature |
Response |
控制对请求的响应。 |
IHttpAuthenticationFeature |
AuthenticationManger/User |
提供完成用户认证的AuthenticationHandler对象和表示当前用户的ClaimsPrincipal对象 |
IHttpConnectionFeature |
Connection |
提供描述当前HTTP连接的基本信息。 |
IItemsFeature |
Items |
提供用户存放针对当前请求的对象容器。 |
IHttpRequestLifetimeFeature |
RequestAborted |
传递请求处理取消通知和中止当前请求处理。 |
IServiceProvidersFeature |
RequestServices |
提供根据服务注册创建的ServiceProvider。 |
ISessionFeature |
Session |
提供描述当前会话的Session对象。 |
IHttpRequestIdentifierFeature |
TraceIdentifier |
为追踪日志(Trace)提供针对当前请求的唯一标识。 |
IHttpWebSocketFeature |
WebSockets |
管理WebSocket |
对于上面列出的众多特性接口,我们在后续相关章节中都会涉及到,目前来说我们只需要了解一下两个最重要的特性接口,即表示请求和响应的IHttpRequestFeature和IHttpResponseFeature。从下面给出的代码片断我们不难看出,这两个接口的定义分别与抽象类HttpRequest和HttpResponse具有一致的定义。对于DefaultHttpContext类型来说,它的Request和Response属性分别返回的是一个DefaultHttpRequest和DefaultHttpResponse对象。DefaultHttpRequest和DefaultHttpResponse分别继承自HttpRequest和HttpResponse,它们分别利用这个两个特性实现了从基类继承下来的所有抽象成员。
1: public interface IHttpRequestFeature
2: {
3: Stream Body { get; set; }
4: IHeaderDictionary Headers { get; set; }
学习ASP.NET Core, 怎能不了解请求处理管道[4]: 应用的入口——Startup学习ASP.NET Core,怎能不了解请求处理管道[2]: 服务器在管道中的“龙头”地位
学习ASP.NET Core, 怎能不了解请求处理管道[5]: 中间件注册可以除了可以使用Startup之外,还可以选择StartupFilter