通过重建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
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
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 ConcurrentDictionary object>();
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
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
三、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管道中的处理流程[下]:管道如何创建
源代码下载