管道是如何处理HTTP请求的?
我们知道ASP.NET Core请求处理管道由一个服务器和一组有序的中间件组成,所以从总体设计来讲是非常简单的,但是就具体的实现来说,由于其中涉及很多对象的交互,我想很少人能够地把它弄清楚。为了让读者朋友们能够更加容易地理解管道处理HTTP请求的总体流程,我们根据真实管道的实现原理再造了一个“模拟管道”并在此管道上开发了一个发布图片的应用,这篇文章旨在为你讲述管道是如何处理HTTP请求的
目录
一、HttpApplication
FeatureCollection
HostingApplication
二、HttpContext
DefaultHttpContext
HostingApplication
小结
三、服务器
HttpListenerServer
ServerFactory
小结
一、HttpApplication
ASP.NET Core请求处理管道由一个服务器和一组有序排列的中间件组合而成。我们可以在这基础上作进一步个抽象,将后者抽象成一个HttpApplication对象,那么该管道就成了一个Server和HttpApplication的综合体(如图5所示)。Server会将接收到的HTTP请求转发给HttpApplication对象,后者会针对当前请求创建一个上下文,并在此上下文中处理请求,请求处理完成并完成响应之后HttpApplication会对此上下文实施回收释放处理。
我们通过具有如下定义的IHttpApplication<TContext>来表示上述的这个HttpApplication,泛型参数TContext代表这个泛化的上下文类型。一个HttpApplication对象在接收到Server转发的请求之后需要完成三项基本的操作,即创建上下文、在上下文中处理请求以及请求处理完成之后释放上下文,这三个基本操作搞好通过对应的三个方法来完成。
1: public interface IHttpApplication<TContext>
2: {
3: TContext CreateContext(IFeatureCollection contextFeatures);
4: Task ProcessRequestAsync(TContext context);
5: void DisposeContext(TContext context, Exception exception);
6: }
FeatureCollection
用于创建上下文的CreateContext方法具有一个类型为IFeatureCollection接口的参数。顾名思义,这个接口用于描述某个对象所具有的一组特性,我们可以将它视为一个字典,它Vaue代表特性描述对象,Key则表示该对象的注册类型(可以是特性描述对象的真实类型或者真实类型的基类或者实现的接口)。我们可以调用Get方法根据类型得到设置的特性描述对象,后者的设置则通过Set方法来完成。FeatureCollection类型采用最简单的方式实现了这个接口。
1: public interface IFeatureCollection
2: {
3: TFeature Get<TFeature>();
4: void Set<TFeature>(TFeature instance);
5: }
6:
7: public class FeatureCollection : IFeatureCollection
8: {
9: private ConcurrentDictionary<Type, object> features = new ConcurrentDictionary<Type, object>();
10:
11: public TFeature Get<TFeature>()
12: {
13: object feature;
14: return features.TryGetValue(typeof(TFeature), out feature)
15: ? (TFeature)feature
16: : default(TFeature);
17: }
18:
19: public void Set<TFeature>(TFeature instance)
20: {
21: features[typeof(TFeature)] = instance;
22: }
23: }
HostingApplication
管道模式采用的HttpApplication是一个类型为 HostingApplication的对象。如下面的代码片段所示,这个类型实现了接口IHttpApplication<Context>,泛型参数Context是一个针对当前请求的上下文对象。一个Context是对一个HttpContext的封装,后者是真正描述当前HTTP请求的上下文。除此之外,Context还具有Scope和StartTimestamp两个属性,两者与日志记录和事件追踪有关,前者被用来将针对同一请求的多次日志记录关联到同一个上下文范围(即Logger的BeginScope方法的返回值),后者表示开始处理请求的时间戳,如果在完成请求处理的时候记录下当前的时间戳,我们就可以计算出整个请求处理所花费的时间。
1: public class HostingApplication : IHttpApplication<Context>
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<TContext>表示HttpApplication是对注册的中间件的封装。HttpApplication在一个自行创建的上下文中完成对服务器接收请求的处理,而上下文根据表述原始HTTP上下文的特性集合来创建,这个特性集合通过接口IFeatureCollection来表示,FeatureCollection是该接口的默认实现者。ASP.NET Core 默认使用的HttpApplication是一个HostingApplication对象,它创建的上下文类型为Context,一个Context对象是对一个HttpContext和其他与日志相关上下文信息的封装。
二、HttpContext
用来描述当前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属性。对于HttpResponse来说,我们保留了三个分别表示输出流(OutputStream)、媒体类型(ContentType)和响应状态码(StatusCode)。
1: public abstract class HttpRequest
2: {
3: public abstract Uri Url { get; }
4: }
5:
6: public abstract class HttpResponse
7: {
8: public abstract Stream OutputStream { get; }
9: public abstract string ContentType { get; set; }
10: public abstract int StatusCode { get; set; }
11:
12: public void WriteFile(string fileName, string contentType)
13: {
14: if (File.Exists(fileName))
15: {
16: byte[] content = File.ReadAllBytes(fileName);
17: this.ContentType = contentType;
18: this.OutputStream.Write(content, 0, content.Length);
19: }
20: this.StatusCode = 404;
21: }
22: }
DefaultHttpContext
ASP.NET Core默认使用的HttpContext是一个类型为DefaultHttpContext对象,在介绍DefaultContext的实现原理之前,我们必须了解这个一个事实:请求的接收者和最终响应者是服务器,一般来说服务器接收到请求之后会创建自己的上下文来描述当前请求,针对请求的相应也通过这个原始上下文来完成。在应用中不仅统一使用这个DefaultHttpContext对象来获取请求信息,同时还利用它来完成对请求的响应,所以它必然与服务器创建的原始上下文存在某个关联,这种关联是通过上面我们提到过的这个FeatureCollection对象来实现的。
如右图所示,不同类型的服务器在接收到请求的时候会创建一个原始的上下文,然后它会将操作原始上下文的操作封装成一系列标准的特性对象(特性类型实现统一的接口)。这些特性对象最终服务器被组装成一个FeatureCollection对象,应用程序中使用的DefaultHttpContext就是根据它创建的。当我们调用DefaultHttpContext相应的属性和方法时,在它的内部实际上借助封装的特性对象去操作原始的上下文。
一旦了解DefaultHttpContext是如何操作原始HTTP上下文之后,对于DefaultHttpContext的定义就很好理解了。如下面的代码片断所示,DefaultHttpContext具有一个IFeatureCollection类型的属性HttpContextFeatures,它表示的正是由服务器创建的用于封装原始HTTP上下文相关特性的FeatureCollection对象。通过构造函数的定义我们知道对于一个DefaultHttpContext对象来说,表示请求和响应的分别是一个DefaultHttpRequst和HttpResponse对象。
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: }
封装各种原始HTTP上下文的特性能够统一被DefaultHttpContext所用,它们的类型需要实现统一的接口,在这里我们定义了如下两个针对请求和响应的特性接口IHttpRequestFeature和IHttpResponseFeature,它们与HttpRequest和HttpResponse具有类似的成员定义。
1: public interface IHttpRequestFeature
2: {
3: Uri Url { get; }
4: }
5:
6: public interface IHttpResponseFeature
7: {
8: Stream OutputStream { get; }
9: string ContentType { get; set; }
10: int StatusCode { get; set; }
11: }
实际上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<IHttpRequestFeature>();
7: }
8: public override Uri Url
9: {
10: get { return this.RequestFeature.Url; }
11: }
12: }
13:
14: public class DefaultHttpResponse : HttpResponse
15: {
16: public IHttpResponseFeature ResponseFeature { get; }
17:
18: public override Stream OutputStream
19: {
20: get { return this.ResponseFeature.OutputStream; }
21: }
22:
23: public override string ContentType
24: {
25: get { return this.ResponseFeature.ContentType; }
26: set { this.ResponseFeature.ContentType = value; }
27: }
28:
29: public override int StatusCode
30: {
31: get { return this.ResponseFeature.StatusCode; }
32: set { this.ResponseFeature.StatusCode = value; }
33: }
34:
35: public DefaultHttpResponse(DefaultHttpContext context)
36: {
37: this.ResponseFeature = context.HttpContextFeatures.Get<IHttpResponseFeature>();
38: }
39: }
HostingApplication
在了解了DefaultHttpContext的实现原理之后,我们在回头看看上面作为默认HttpApplication类型的HostingApplication的定义。由于对请求的处理总是在一个由HttpContext对象表示的上下文中进行,所以针对请求的处理最终可以通过具有如下定义的RequestDelegate委托对象来完成。一个HttpApplication对象可以视为对一组中间件的封装,它对请求的处理工作最终交给这些中间件来完成,所有中间件对请求的处理最终可以转换成通过属性Application表示的RequestDelegate对象。
1: public class HostingApplication : IHttpApplication<Context>
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)
21: => context.Scope?.Dispose();
22:
23: public Task ProcessRequestAsync(Context context)
24: => this.Application(context.HttpContext);
25: }
当我们创建一个HostingApplication对象的时候,需要将所有注册的中间件转换成一个RequestDelegate类型的委托对象,并将其作为构造函数的参数,ProcessRequestAsync方法会直接利用这个委托对象来处理请求。当CreateContext方法被执行的时候,它会直接利用封装原始HTTP上下文的FeatureCollection对象创建一个DefaultHttpContext对象,进而创建返回的Context对象。在简化的DisposeContext方法中,我们只是调用了Context对象的Scope属性的Dispose方法(如果Scope存在),实际上我们在创建Context的时候并没有Scope属性进行初始化。
小结
我们依然通过一个UML对表示HTTP上下文相关的接口/类型及其相互关系进行总结。如右图所示,针对当前请求的HTTP上下文通过抽象类HttpContext表示,请求和响应是HttpContext表述的两个最为核心的上下文请求,它们分别通过抽象类HttpRequest和HttpResponse表示。ASP.NET Core 默认采用的HttpContext类型为DefaultHttpContext,它描述的请求和响应分别是一个DefaultHttpRequst和DefaultHttpResponse对象。一个DefaultHttpContext对象由描述原始HTTP上下文的特性集合来创建,其中描述请求与相应的特性分别通过接口IHttpRequestFeature和IHttpResponseFeature表示,DefaultHttpRequst和DefaultHttpResponse正是分别根据它们创建的。
三、服务器
管道中的服务器通过接口IServer表示,在模拟管道对应的应用编程接口中,我们只保留其核心的方法Start。顾名思义,Start方法被执行的时候,服务会马上开始实施监听工作。HTTP请求一旦抵达,该方法会利用作为参数的HttpApplication对象创建一个上下文,并在此上下文中完成对请求的所有处理操作。当完成了对请求的处理任务之后,HttpApplication对象会自行负责回收释放由它创建的上下文。
1: public interface IServer
2: {
3: void Start<TContext>(IHttpApplication<TContext> application);
4: }
HttpListenerServer
在我们演示的发布图片应用中使用的服务器是一个类型为HttpListenerServer的服务器。顾名思义,这个简单的服务器直接利用HttpListener来完成对请求的监听、接收和响应工作。如下面的代码片断所示,我们创建一个HttpListenerServer对象时需要为HttpListener指定一个监听地址前缀,如果没有指定会自动使用默认的地址(“http://localhost:3721/”)。
1: public class HttpListenerServer : IServer
2: {
3: public HttpListener Listener { get; }
4:
5: public HttpListenerServer(string url)
6: {
7: this.Listener = new HttpListener();
8: this.Listener.Prefixes.Add(url ?? "http://localhost:3721/");
9: }
10:
11: public void Start<TContext>(IHttpApplication<TContext> application)
12: {
13: this.Listener.Start();
14: while (true)
15: {
16: HttpListenerContext httpListenerContext = this.Listener.GetContext();
17:
18: HttpListenerContextFeature feature = new HttpListenerContextFeature(httpListenerContext);
19: FeatureCollection contextFeatures = new FeatureCollection();
20: contextFeatures.Set<IHttpRequestFeature>(feature);
21: contextFeatures.Set<IHttpResponseFeature>(feature);
22: TContext context = application.CreateContext(contextFeatures);
23:
24: application.ProcessRequestAsync(context)
25: .ContinueWith(_ => httpListenerContext.Response.Close())
26: .ContinueWith(_ => application.DisposeContext(context, _.Exception));
27: }
28: }
29: }
在Start方法中,我们调用HttpListener的Start方法开始监听来自网络的HTTP请求。HTTP请求一旦抵达,表示原始上下文的HttpListenerContext对象通过调用HttpListener的GetContext方法返回。我们创建了一个表述这个原始上下文相关特性的HttpListenerContextFeature对象,并将它分别针对类型IHttpRequestFeature和IHttpResponseFeature添加到创建的FeatureCollection对象上。作为参数的HttpApplication对象将它作为参数调用CreateContext方法创建上下文,并调用ProcessRequestAsync方法在这个上下文中处理当前请求。当所有的请求处理工作结束之后,我们会调用HttpApplication对象的DisposeContext方法回收释放这个上下文。
ServerFactory
当WebHost在创建管道的时候并不会直接创建服务器对象,服务器对象是通过它的工厂ServerFactory创建的。ServerFactory是对所有实现了IServerFactory接口的所有类型及其对象的统称,我们在模拟管道中对这个对象作了如下的简化,除去了创建服务器的CreateServer方法的参数。作为HttpListenerServer的工厂类,HttpListenerServerFactory直接利用构造函数中指定的监听地址创建了在CreateServer方法中返回的HttpListenerServer对象。
1: public interface IServerFactory
2: {
3: IServer CreateServer();
4: }
5:
6: public class HttpListenerServerFactory : IServerFactory
7: {
8: private string listenUrl;
9:
10: public HttpListenerServerFactory(string listenUrl = null)
11: {
12: this.listenUrl = listenUrl?? "http://localhost:3721/";
13: }
14:
15: public IServer CreateServer()
16: {
17: return new HttpListenerServer(listenUrl);
18: }
19: }
小结
右图所示的UML体现了与服务器相关的接口/类型之间的关系。通过接口IServer表示的服务器表示管道中完成请求监听、接收与相应的组件,我们自定义的HttpListenerServer利用一个HttpListener实现了这三项基本操作。当HttpListenerServer接收到抵达的HTTP请求之后,它会将表示原始HTTP上下文的特性封装成一个HttpListenerContextFeature对象,HttpListenerContextFeature实现了分别用于描述请求和响应特性的接口IHttpRequestFeature和IHttpResponseFeature,HostingApplication可以利用这个HttpListenerContextFeature对象来创建DefaultHttpContext对象。
一、采用管道处理HTTP请求
二、创建一个“迷你版”的管道来模拟真实管道请求处理流程
三、管道如何处理HTTP请求的
四、管道是如何被创建出来的