ASP.NET/MVC/Core的HTTP请求流程

ASP.NET HTTP管道(Pipeline)模型

1. 先讲一点,再深刻思考

一般我们都在写业务代码,优化页面,优化逻辑之间内徘徊。也许我们懂得HTTP,HTTPS的GET,POST,但是我们大部分人是不知道ASP是如何去解析HTTP,或者IIS是如何去处理页面请求。我们只知道WebForm拉控件,MVC写Controller,Action,却不知道IIS,NetFrameWork帮我们做了很多事情。那接下我们就是要去了解IIS帮我们做了些啥事情。

2. 那啥叫管道(Pipeline)模型

  • 初理解Pipeline

    • 从一个现象说起,有一家咖啡吧生意特别好,每天来的客人络绎不绝,客人A来到柜台,客人B紧随其后,客人C排在客人B后面,客人D排在客人C后面,客人E排在客人D后面,一直排到店面门外。老板和三个员工首先为客人A准备食物:员工甲拿了一个干净的盘子,然后员工乙在盘子里装上薯条,员工丙再在盘子里放上豌豆,老板最后配上一杯饮料,完成对客人A的服务,送走客人A,下一位客人B开始被服务。然后员工甲又拿了一个干净的盘子,员工乙又装薯条,员工丙又放豌豆,老板又配上了一杯饮料,送走客人B,客人C开始被服务。一直重复下去。

    • 从效率方面观察这个现象,当服务客人A时,在员工甲拿了一个盘子后,员工甲一直处于空闲状态,直到送走客人A,客人B被服务。老板自然而然的就会想到如果每个人都不停的干活,就可以服务更多的客人,赚到更多的钱。老板通过不停的尝试想出了一个办法。以客户A,B为例阐述这个方法:员工甲为客户A准备好了盘子后,在员工乙开始为客户A装薯条的同时,员工甲开始为客户B准备托盘。这样员工甲就可以不停的进行生产。整个过程如下图,客户们围着咖啡吧台排队,因为有四个生产者,一个老板加三个员工,所以可以同时服务四个客户。我们将目光转向老板,单位时间从他那里出去的客户数提高了将近四倍,也就是说效率提高将近四倍。

    这样子,我们就很好理解了Pipeline了,那其实就是每个人络绎不绝的做自己的事情,但个人事情又是整个流程的一部分,有点像工厂的小妹,只做包鞋底,但是这又是制造鞋的中间流程。一条流水,一条管道,一直处理下去。那每个HTTP请求,到了IIS也是这样子的。

  • 针对Pipeline模型带来的启示,好处

    • 工作流的参考模型
      其实就是上面所说一样,pipeline模型与工作流模型,都是链式的,就像一条生产线,各个组件的前后协同。
    • 服务framework的参考构建模型
      Pipeline模型的一个特点是,在其内部是以组建的模式来实现组合的(虽然这些组建有先后顺序之分),它假如你把侧重点放在它的链式组合上,而不是将侧重点放在上面的工作流上(工作流的关注点是流程的先后顺序),那么完全可以用这种方式来搭建一个复杂的服务,当然这样做的前提是,单个组件的粒度必须尽可能小并且耦合性极低。
      那其实就很像现在的微服务了,只不过微服务更大,服务对象是一个应用程序。

    Pipeline模型带来的:流程式(有序)+可拆卸(配置),比普通的封装机动性更好。

  • Pipeline模型的缺点
    每次它对于一个输入(或者一次请求)都必须从链头开始遍历(參考Http Server处理请求就能明确),这确实存在一定的性能损耗。

3. 讲完Pipeline,我们讲他在IIS上的实现

3.1 Http请求带服务器的时候

  1. 当服务器接收到一个Http请求的时候,IIS是如何去决定,并处理该请求。答案就是文件的“后缀名”。

    服务器获取所请求的页面或文件的后缀名后,那服务器就回去寻找能出来这些后缀的应用程序。若是IIS找不到,并且文件没有受到IIS的保护(保护:App_Code文件夹的文件,不保护:平常的JS文件等),就会直接返回给客户端(浏览器,移动端等)

    那么能处理后缀名的程序叫什么呢? ISAPI 应用程序(Internet Server Application Programe Interface,互联网服务器应用程序接口)。它其实是一个接口,起到一个代理的作用,他的主要工作就是将请求的页面(文件)与处理该页面(文件)的后缀的处理程序进行一个映射。让其可以争取的去处理页面(文件)。

    那这个应用程序长什么样子呢?

    • 我们打开IIS(server2003),随意选中一个站点,鼠标右击属性,然后选择“主目录”选项,接着选择“配置”。然后就可以看到下面的画面。

    这边我们能看到后缀名与之可执行的文件路径。
    接着我们找到“.axpx”的后缀名,打开来看。

    我们可以发现".aspx"的后缀名是有aspnet_isapi.dll来处理的,所以IIS将".aspx"页面的请求交给这个dll,就不关心后面是如何处理了。所以我们现在知道了,ASP.NET只是服务器(IIS)的一个组成部分而已,它是一个ISAPI扩展。

  2. 上面是server2003的情况,现在08以上变成了不是在属性里面了, 他变成了IIS中的“ISAPI筛选器”和“处理程序映射”了。

    1. 我们点击“ISAPI筛选器”,可以出现发现,筛选器是分FrameWork与位数的,所以有四个。

    1. 我们进入处理程序映射,找出后缀为“.aspx”的,可以发现有很多个,对应了framework与位数,并且有个处理程序,

    3.双击进入,可以看到可以到处理模块的具体路径,以及请求限制。

注意,这边要是限制后,页面(文件)只能以某种特定方式访问。

3.2 宿主环境(Hosting)

从本质上讲,Asp.Net 主要是由一系列的类组成,这些类的主要目的就是将 Http 请求转变为对客户端的响应。HttpRuntime 类是 Asp.Net 的一个主要入口,它有一个称作 ProcessRequest的方法,这个方法以一个 HttpWorkerRequest 类作为参数。HttpRuntime 类几乎包含着关于单个 Http 请求的所有信息:所请求的文件、服务器端变量、QueryString、Http 头信息 等等。Asp.Net 使用这些信息来加载、运行正确的文件,并且将这个请求转换到输出流中,一般来说,也就是 HTML 页面。当然也可是是文件。

当页面(文件)发生变动时,为了能够卸载运行在同一进程中的应用程序,当然卸载也是为了重新新加载,Http请求被分放在相互隔离的应用程序域。也就是“AppDomain”。

那么对于IIS来说,IIS依赖一个叫HTTP.SYS的内置驱动程序来监听外部的HTTP请求,在操作系统启动的时候,IIS会在HTTP.SYS中注册自己的虚拟路径。(注册可以理解为,告诉HTTP.SYS哪些URL可以访问,哪些不可以访问,404就可以怎么来的,就是这么来的。)

如果是可以访问的URL,那么HTTP.SYS会将这个请求扔给IIS工作者进程。(就是我们所熟悉的W3WP.EXE,IIS5.0叫做ASPNET_WP.EXE)。
运行是每个进程都有一个身份标识,以及一系列的可选性能参数,其实就是应用程序池,唯一标识就是应用程序池名称。

基础的知识点都懂了,我们来看下一个大概的HTTP请求的过程

  1. HTTP.SYS接收Http请求信息。并将信息保存到HttpWorkRequest类中。
  2. 从相互隔离的AppDomain中加载HttPRuntime。
  3. 调用HttpRuntime的ProcessRequest方法。
  4. 程序猿创造世界
  5. IIS接收返回的数据流,重新返回给HTTP.SSY
  6. HTTP.SYS将数据返回给客户端(浏览器)

3.3 接下来是重点,理解Pipeline

前面讲了那么多,终于进入到重点,划重点。
首先我们先知道了什么是Pipeline,接着我们知道一个Http的请求过程。然后我们就知道了,原来Http请求到服务器,原来是走这样一条路。

但是我们忽略了,这个过程跟我们程序,跟我们代码怎么衔接起来的。
当Http请求进入 Asp.Net Runtime以后,它的管道由托管模块(Managed Modules)和处理程序(Handlers)组成,并且由管道来处理这个 Http请求。这个就是所谓的ASP的Http管道模型了。

接下来我们看下这个管道模型是怎么流动的。(WebForm)

  1. 首先进来肯定是HTTPRuntime,然后通过HttpApplicationFactory创建HttpApplication。

  2. HttpApplication会创建该次Http请求的HttpContext(上下文),那这个对象我们就很熟悉了,里面就包括HttpRequest、HttpResponse、HttpSessionState等。

  3. 那接下来请求会通过一系列的Module(可以理解为车间,可以做通过的物品做事情),Module可以做一些执行某个实际工作前的事情。因为它会Http请求有完全的控制权。

  4. 当Http请求完,它会被HttpHandler处理。在这一步,也就是我们实际做的事情了,他可以完成我们.aspx的所有业务。WebForm的每个页面都是继承“Page”,而“Page”继承了“IHttpHandler”接口

     	public class Page : TemplateControl, IHttpHandler{
    	// 代码省略
    	}
    
  5. HttpHandler处理完后,又会回到Module,这时可以做一些实际工作后的事情。
    我们很多事件,是不是有分前与后,就是这个道理。进行事件的前后拦截。

  6. 我们可以看下,在只有Module与Handler的情况,整个Http的流动走向,大概是这样子

3.4 那讲完WebForm的,我们来讲MVC的

在IIS7之前,如IIS6或IIS5,请求处理管道分为两个:IIS请求处理管道和ASP.NET管道,若客户端请求静态资源则只有IIS管道进行处理,而ASP.NET管道不会处理该请求。从IIS7开始两个管道合二为一,称为集成管道。其实就是我们IIS里会选择到应用程序池的托管模式。

再次了解

  • IIS 6以及IIS 7经典模式
    早期的IIS版本中,IIS接收到一个请求时先判断请求类型,如果是静态文件直接由IIS处理;如果是一个ASP.NET请求类型,IIS把请求发送给IIS的扩展接口ASP.NET ISAPI DLL。ISAPI相当于ASP.NET应用的容器,这也是我们之前讲的。

  • IIS 7 集成模式
    IIS 7和之前的版本区别比较大,IIS7直接把ASP.NET的运行管道流程集成到了IIS上。

    • 在IIS7中,ASP.NET请求处理管道Pipeline直接覆盖了IIS本身的管道Pipeline,IIS整个流程按照ASP.ENT管道流程执行,例如BeginRequest、AuthenticateRequest、…、EndRequest。而不是通过插件扩展(ISAPI)形式,不同的内容(jpg、html、php等)通过不同的插件来执行。
    • IIS7的优势体现在哪里?开发人员可以实现自定义的HttpModule或者HttpHandler,然后直接集成到IIS上,例如,我可以自定义一个JpgHttpHandler,然后通过IIS的Handler部署方式,把JpgHttpHandler部署到IIS上,处理所有的请求格式为*.jpg的请求。

知道这些我们继续了解MVC的管道模式

废话不多说,直接上图

废话也不多说,直接走下流程(以我的方式理解)

废话有点多

  1. 首先进入HttpRuntime,在一样通过HttpApplicationFactory创建HttpApplication
  2. HttpApplication会创建该次Http请求的HttpContext(上下文),那这个对象我们就很熟悉了,里面就包括HttpRequest、HttpResponse、HttpSessionState等。(这些都跟webform是一样的)
  3. HttpApplication继承IHttpHandler,接着我们开始走Module
  4. 然后我们进入到使用UrlRoutingModule(路由系统)UrlRoutingModule,从Http请求获取Controller和Action以及路由数据。
  5. 接着匹配Route规则,获取Route对象,解析对象。
  6. 请求IRouteHandler(MVCRouteHandler)
  7. 执行ProcessRequest方法,然后使用ControllerBulider获取ControllerFactory
  8. 接着就调用到Controller,同样的的方法到达Action,并执行
  9. 在执行Controller与Action可能会有各种认证,各种特性拦截
  10. 程序猿创造世界
  11. 返回ActionResult
  12. 响应Http
  13. 客户端接收响应

接下来我们具体了解下细节

  • HttpApplication与HttpModule

    HTTP请求由ASP.NET运行时接管之后,HttpRuntime会利用HttpApplicationFactory创建或从HttpApplication对象池(.NET中类似的机制有线程池和字符串拘留池)中取出一个HttpApplication对象,同时ASP.NET会根据配置文件来初始化注册的HttpModule,HttpModule在初始化时会订阅HttpApplication中的事件来实现对HTTP请求的处理。

      public class HttpApplication : IComponent, IDisposable, IHttpAsyncHandler, IHttpHandler, IRequestCompletedNotifier, ISyncContext{ 	
    
       public HttpApplication();
       public ISite Site { get; set; }
       public IPrincipal User { get; }
    	 ...
      }
    

    很明显我们可以看到HttpApplication继承了IHttpHandler,是跟WebForm的Page是一样的

  • Route

    一个HTTP请求会经过至少一个HttpModule的处理。UrlRoutingModule是非常重要的模块,它是路由系统的核心。路由系统的职责是从请求URL中获取controller和action的名称以及其它请求数据。
    UrlRoutingModule根据当前请求的URL和RouteTable中已注册的路由模板进行匹配并返回第一个和当前请求相匹配的路有对象Route,然后根据路有对象获取路由数据对象RouteData(ASP.NET MVC中,路由数据必须包含controller和action的名称),再有RouteData获取IRouteHandler最终有IRouteHandler得到IHttpHandler

  • HttpHandler

    一个HTTP请求最终要进入HttpHanler中进行处理,一次HTTP请求只能被一个HttpHandler进行处理。

  • Controller
    IHttpHandler在ProcessRequest方法中对当前请求进行处理,在该方法中通过ControllerBuilder得到IControllerFactory然后通过反射的方式获取Controller的类型。

  • Action

    ASP.NET MVC中ControllerBase是所有Controller的基类,在该类型的Execute方法中通过IActionInvoker的InvokeAction方法来执行对Action的调用。在Action执行前会进行模型绑定和模型认证操作。

  • Filters

    在ASP.NET MVC5中有常用的过滤器有5个:IAuthenticationFilter、IAuthorizationFilter、IActionFilter、IResultFilter、IExceptionFilter。
    在ASP.NET MVC中所有的过滤器最终都会被封装为Filter对象,该对象中FilterScope类型的属性Scope和int类型属性Order用于决定过滤器执行的先后顺序,具体规则如下:
    Order和FilterScope的数值越小,过滤器的执行优先级越高;
    Order比FilterScope具有更高的优先级,在Order属性值相同时FilterScope才会被考虑

      //数值越小,执行优先级越高
      public enum FilterScope
      {
          Action= 30,
          Controller= 20,
          First= 0,
          Global= 10,
          Last= 100
      }
    
  • ActionResult

    Action执行完毕之后会返回ActionResult类型对象作为对此次请求进行处理的结果,对于不是ActionResult类型的返回值,ASP.NET MVC会将其转换为ActionResult类型。

总结:那这个就是MVC模式下,一个大概的请求走向,在MVC的模式下,它的管道是啥样子的。

3.5 看完MVC ,我们接着来看最牛的Core,谁叫它可以跨平台呢【傲娇】

既然Core是跨平台的,那么它不依托IIS,现在的IIS就是个摆设,它有独立的Core的SDK,Core的RunTime。我们来一探究竟!

Core是跨平台的,那他是怎么实现的呢,其实他自己本身有自己的Kestrel Server,可以直接对外部提供服务。但是还需要有个反向代理服务器将他保护起来,IIS就是其一,其他平台有其他的方式。

上图:

知识点:IIS 是通过 HTTP 的方式来调用我们的 ASP.NET Core 程序。而部署在IIS中时,并不需要我们手动来启动 ASP.NET Core 的控制台程序,这是因为IIS新增了一个 AspNetCoreModule 模块,它负责 ASP.NET Core 程序的启动与停止,并能监听 ASP.NET Core 程序的状态,在我们的应用程序意外崩溃时重新启动。

  • Hostting(宿主)

    IIS通过Http调用Core应用程序,所以我们肯定要创建一个Host来启动Core,WebHost,上面我们也讲到宿主,我们通过宿主在生成一系列的Http的上下文,环境等。整个http请求都在宿主内。

    • WebHost的创建

      public class Program{
      public static void Main(string[] args){
      CreateWebHostBuilder(args).Build().Run();
      }

        public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
            WebHost.CreateDefaultBuilder(args)
                .UseStartup<Startup>();
        }
      

      我们可以看到WeHost通过静态本身来调用创建出来的。“CreateDefaultBuilder”用来做一些简单的配置

      1. 注册 Kestrel 中间件,指定 WebHost 要使用的 Server(HTTP服务器)。
      2. 设置 Content 根目录,将当前项目的根目录作为 ContentRoot 的目录。
      3. 读取 appsettinggs.json 配置文件,开发环境下的 UserSecrets 以及环境变量和命令行参数。
      4. 读取配置文件中的 Logging 节点,对日志系统进行配置。
      5. 添加 IISIntegration 中间件。
      6. 设置开发环境下, ServiceProvider 的 ValidateScopes 为 true,避免直接在 Configure 方法中获取 Scope 实例。

      接着指定Startup最终使用Build创建WebHost。紧接着使用Run将程序跑起来。

    • WebHost的启动

      1. 初始化,构建 RequestDelegate
        RequestDelegate 是我们的应用程序处理请求,输出响应的整个过程,也就是我们的 ASP.NET Core 请求管道。
        1. 调用 Startup 中的 ConfigureServices 方法
        2. 初始化 Http Server
          Server 是一个HTTP服务器,负责HTTP的监听,接收一组 FeatureCollection 类型的原始请求,并将其包装成 HttpContext 以供我们的应用程序完成响应的处理。
        3. 创建 IApplicationBuilder
          IApplicationBuilder 用于构建应用程序的请求管道,也就是生成 RequestDelegate
        4. 配置 IApplicationBuilder
      2. 启动 Server,监听请求并响应
      3. 启动 HostedService
        那这边是简单的讲了下WebHost,具体的内容可以看下文章末尾的Core的链接。
  • Middleware(中间件),管道模型的构成

    • IApplicationBuilder
      首先,IApplicationBuilder 是用来构建请求管道的,而所谓请求管道,本质上就是对 HttpContext 的一系列操作,即通过对 Request 的处理,来生成 Reponse。因此,在 ASP.NET Core 中定义了一个 RequestDelegate 委托,来表示请求管道中的一个步骤,它有如下定义:
      public delegate Task RequestDelegate(HttpContext context);
      而对请求管道的注册是通过 Func<RequestDelegate, RequestDelegate> 类型的委托(也就是中间件)来实现的。它接收一个 RequestDelegate 类型的参数,并返回一个 RequestDelegate 类型,也就是说前一个中间件的输出会成为下一个中间件的输入,这样把他们串联起来,形成了一个完整的管道。
      它有一个内部的 Func<RequestDelegate, RequestDelegate> 类型的集合(用来保存我们注册的中间件)和三个核心方法:
      1. Use
      Use是我们非常熟悉的注册中间件的方法,其实现非常简单,就是将注册的中间件保存到其内部属性 _components 中。
      2. Build
      Hosting 的启动中,便是通过该 Build 方法创建一个 RequestDelegate 类型的委托,Http Server 通过该委托来完成整个请求的响应
      3. Run
      在我们注册的中间件中,是通过 Next 委托 来串连起来的,如果在某一个中间件中没有调用 Next 委托,则该中间件将做为管道的终点,因此,我们在最后一个中间件不应该再调用 Next 委托,而 Run 扩展方法,通常用来注册最后一个中间件
      4. New
      New 方法根据自身来“克隆”了一个新的 ApplicationBuilder 对象,而新的 ApplicationBuilder 可以访问到创建它的对象的 Properties 属性,但是对自身 Properties 属性的修改,却不到影响到它的创建者,这是通过 CopyOnWriteDictionary 来实现的

    所以 Core的管道其实就是Middleware来做的,每个都有前置,后置的处理步骤,中间可以调用其他Middleware。也可以并行走向。

ASP.NET Core 请求管道的构建过程,以及一些帮助我们更加方便的来配置请求管道的扩展方法。在 ASP.NET Core 中,至少要有一个中间件来响应请求,而我们的应用程序实际上只是中间件的集合,MVC 也只是其中的一个中间件而已。简单来说,中间件就是一个处理http请求和响应的组件,多个中间件构成了请求处理管道,每个中间件都可以选择处理结束,还是继续传递给管道中的下一个中间件,以此串联形成请求管道。通常,我们注册的每个中间件,每次请求和响应均会被调用,但也可以使用 Map , MapWhen ,UseWhen 等扩展方法对中间件进行过滤。

3.6总结

那我们分析了以往微软的3种形式底下的Web,分别是WebForm,MVC,Core,其中WebForm与MVC雷士,依托于IIS,不同的就是ISAPI的不同。前者的ISAPI以插件形式存在IIS,而后者将其整合成一套。Core跨平台,IIS只是他的一个反向代理服务器,HTTP请求带Core的程序,运行Core。
但是我觉得他们内容是基本不变的,HTTP请求,进来在彼此的AppDomain中创建HttpContext,然后就开始走Pipeline走流程,WebFrom走PageIHTTPHandler与其相关的IModule,而MVC是Global.asax继承HttpApplication,HttpApplication又继承IHTTPHandler,接下来WebFrom与MVC就各自走各自的Pipeline。Core却不是这样,它的Pipeline采用了Middleware(中间件)的形式。俄罗斯套娃,一步一步走Pipeline。最终都是响应客户端,一个HTTP请求,从开始到结束。

感谢张子阳的博客-WebForm
感谢雪飞鸿的博客-MVC
感谢雨夜朦胧-Core

posted @ 2018-11-30 11:23  HANS许  阅读(4418)  评论(4编辑  收藏  举报