ASP.NET三剑客 HttpApplication HttpModule HttpHandler 解析

Posted CoderFocus

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了ASP.NET三剑客 HttpApplication HttpModule HttpHandler 解析相关的知识,希望对你有一定的参考价值。

我们都知道,ASP.Net运行时环境中处理请求是通过一系列对象来完成的,包含HttpApplication,HttpModule,HttpHandler。之所以将这三个对象称之为ASP.NET三剑客是因为它们简直不要太重要,完全是ASP.NET界的中流砥柱,责任担当啊。了解它们之前我们得先知道ASP.NET管道模型。

ASP.NET管道模型

这里以IIS6.0为例,它在工作进程w3wp.exe中会利用aspnet_isapi.dll加载.NET运行时。IIS6.0引入了应用程序池的概念,一个工作进程对应着一个应用程序池。一个应用程序池可以承载一个或多个Web应用。如果HTTP.SYS(HTTP监听器,是Windows TCP/IP网络子程序的一部分,用于持续监听HTTP请求)接收的请求是对该Web应用的第一次访问,在成功加载运行时后,IIS会通过AppDomainFactory为该Web应用创建一个应用程序域。也就是说一个应用程序池中会有多个应用程序域,它们共享一个工作进程资源,但是又不会互相牵连影响。

随后一个特殊的运行时IsapiRuntime被加载,会接管该HTTP请求。IsapiRuntime首先会创建一个IsapiWorkerRequest对象来封装当前的HTTP请求,随后将此对象传递给ASP.NET运行时HttpRunTime。从此时起,HTTP请求正式进入了ASP.NET管道。

HttpRunTime会根据IsapiWorkerRequest对象创建用于表示当前HTTP请求的上下文对象HttpContext。随着HttpContext对象的创建,HttpRunTime会利用HttpApplicationFactory创建或获取现有的HttpApplication对象。

HttpApplication负责处理当前的HTTP请求。在HttpApplication初始化过程中,ASP.NET会根据配置文件加载并初始化注册的HttpModule对象。对于HttpApplication来说,在它处理HTTP请求的不同阶段会触发不同的事件,而HttpModule的意义在于通过注册HttpApplication的相应事件,将所需的操作注入整个HTTP请求的处理流程。

最终完成对HTTP请求的处理在HttpHandler中,不同的资源类型对应着不同类型的HttpHandler。

整体处理流程如图所示:

抽象之后的处理流程如图所示:

ASP.NET三剑客 HttpApplication HttpModule HttpHandler 解析

HttpApplication

HttpApplication是整个ASP.NET基础架构的核心,它负责处理分发给它的HTTP请求。

提起HttpApplication就不得不说全局配置文件global.asax。global.asax文件为每个Web应用程序提供了一个从HttpApplication派生的Global类。该类包含事件处理程序,如Application_Start。

ASP.NET三剑客 HttpApplication HttpModule HttpHandler 解析

每个Web应用程序都会有一个Global实例,作为应用程序的唯一入口。我们知道ASP.NET应用程序启动时,ASP.NET运行时只调用一次Application_Start。这似乎意味着在我们的应用程序中只有一个Global对象实例,但是可不是只有一个HttpApplication对象实例。

ASP.NET运行时维护一个HttpApplication对象池。当第一个请求抵达时,ASP.NET会一次创建多个HttpApplication对象,并将其置于HttpApplication对象池中,然后选择其中一个对象来处理该请求。当后续请求到达时,运行时会从池中获取一个HttpApplication对象与请求进行配对。该对象与请求相关联,并且只有该请求,直到请求处理完成。当请求完成后,HttpApplication对象不会被回收,而是会返回到池中,以便稍后将其拉出为其他请求提供服务。通过使用HttpApplication对象来处理到的请求,HttpApplication对象每次只能处理一个请求,这样其成员才可以于储存针对每个请求的数据。下面我们来了解一下HttpApplication的成员。

前面我们讲到过,HttpApplication对象是由HttpRunTime根据当前HTTP请求的上下文对象HttpContext创建或从池子中获取的,并且在HttpApplication初始化过程中,ASP.NET会根据配置文件加载并初始化注册的HttpModule对象。HttpApplication中的Context属性(HttpContext(上下文)类的实例)和Modules属性(影响当前应用程序的HttpModule模块集合)就是用于存放它们的。在后面的HttpModule中还会讲到它们。

ASP.NET三剑客 HttpApplication HttpModule HttpHandler 解析

HttpApplication处理请求的整个生命周期是一个相对复杂的过程,为什么称之为复杂呢?因为HttpApplication类中存在大量的请求触发的事件,在请求处理的不同阶段会触发相应的事件。

ASP.NET三剑客 HttpApplication HttpModule HttpHandler 解析

我们可以通过HttpModule注册相应的事件,将处理逻辑注入到HttpApplication处理请求的某个阶段。这里需要注意的是,从BeginRequest开始的事件,并不是每个管道事件都会被触发。因为在整个处理过程中,随时可以调用Response.End()或者有未处理的异常发生而提前结束整个过程。所有事件中,只有EndRequest事件是肯定会触发的,(部分Module的)BeginRequest有可能也不会被触发。这个我们会在后面的HttpModule中提及。

HttpApplication类重要的Init方法和Dispose方法,这二个方法均可重载。它们的调用时机为:

Init方法在ApplicationStart之后调用,而Dispose在ApplicationEnd之前调用,另外ApplicationStart在整个ASP.NET应用的生命周期内只激发一次(比如IIS启动或网站启动时),类似的ApplicationEnd也只有当ASP.NET应用程序关闭时被调用(比如IIS停止或网站停止时)。

HttpModule

在前面我们讲解了ASP.NET管道模型和HttpApplication对象(其中的管道事件)。现在我们一起来了解一下HttpModule。

我们都知道ASP.NET高度可扩展,那么是什么成就了ASP.NET的高度扩展性呢?HttpModule功不可没。HttpModule在初始化的过程中,会将一些回调操作注册到HttpApplication相应的事件中,在HttpApplication请求处理生命周期的某一个阶段,相应的事件被触发,通过HttpModule注册的回调操作也会被执行。

所有的HttpModule都实现了IHttpModule接口,它和HttpApplication是直接打交道的。在其初始化方法Init()中接受了一个HttpApplication对象,这就让事件注册变得十分容易了。

ASP.NET三剑客 HttpApplication HttpModule HttpHandler 解析

我在了解了HttpModule之后,不禁发出一声惊叹,这不就是面向切面(AOP)嘛!!!我们可以把HttpModule理解为HTTP请求拦截器,拦截到HTTP请求后,它能修改正在被处理的Context上下文,完事儿之后,再把控制权交还给管道,如果还有其它模块,则依次继续处理,直到所有Modules集合(前面提到过,存在于HttpApplication)中的HttpModule都“爽”完为止(可怜的HTTP请求就这样给各个HttpModule轮X了)。也正是这种类似于拦截器模式的HttpModule,配合HttpApplication管道事件给ASP.NET带来了高度可扩展性。

与HttpHandler针对某一种请求文件不同,HttpModule则是针对所有的请求文件,映射给指定的处理程序对请求进行处理,而这些处理,可以发生在请求管线中的任何一个事件中。也就是说你订阅哪个事件,这些处理就发生于那个事件中,处理过后再执行,你订阅过的事件的下一个事件,当然你也可以终止所有事件直接运行最后一个事件,这就意味这他可以不给HttpHandler机会。

前面两段我们提到,HttpModule针对所有请求,处理可以发生在请求管线中的任何一个事件中。而且Modules集合中的所有HttpModule都要依次执行请求处理。这自然而然地让我们在使用强大的HttpModule时要十分注意性能问题,需要触发哪些事件处理,不需要触发哪些事件处理,要有严格的控制。要不会让程序负重,得不偿失。

ASP.NET中内置了很多HttpModule。我们打开C:\Windows\Microsoft.NET\Framework\v4.0.30319\Config文件夹下的webconfig文件,可以发现这样一段配置:

ASP.NET三剑客 HttpApplication HttpModule HttpHandler 解析

这些都是ASP.NET中内置的HttpModule配置。至于为什么要放在这里,原因也很简单。这里的配置都是.NET Framework的默认和基础的配置,如果要配置在每个项目的webconfig文件中,势必会让项目的配置变得十分复杂,所以统一都放到了这里进行配置。

至于上图中的 节点中的HttpModule配置的作用,我们上面也提到过。前面我们讲到过,在HttpApplication初始化过程中,ASP.NET会根据配置文件加载并初始化注册的HttpModule对象。注册的HttpModule对象初始化后,存放在了HttpApplication的Modules属性之中。具体初始化哪些HttpModule对象,当然就是和这些配置相关啦。

虽然ASP.NET中内置了很多HttpModule,但是我们可以实现自定义HttpModule给予扩展满足需要。下面我们自己来实现一下自定义HttpModule:

首先我们创建一个MVC5控制器DefaultController,然后在控制器中创建一个视图Index。在页面显示Hello World。

ASP.NET三剑客 HttpApplication HttpModule HttpHandler 解析

接下来我们创建一个自定义HttpModule(MyModule):

 
   
   
 
  1. namespace WebApplication

  2. {

  3.    public class MyModule : IHttpModule

  4.    {

  5.        public void Dispose()

  6.        {

  7.            throw new NotImplementedException();

  8.        }

  9.        public void Init(HttpApplication context)

  10.        {

  11.            context.BeginRequest += new EventHandler(BeginRequest);

  12.            context.EndRequest += new EventHandler(EndRequest);

  13.        }

  14.        void BeginRequest(object sender, EventArgs e)

  15.        {

  16.            ((HttpApplication)sender).Context.Response.Write("<h1>请求处理开始前进入我的Module</h1>");

  17.        }

  18.        void EndRequest(object sender, EventArgs e)

  19.        {

  20.            ((HttpApplication)sender).Context.Response.Write("<h1>请求处理结束后进入我的Module</h1>");

  21.        }

  22.    }

  23. }

我们在初始化方法Init中对HttpApplication的管道事件BeginRequest和EndRequest分别进行了注册。注册的事件会在响应中输出不同的文字。

最后不要忘记了在webconfig文件中进行配置,当然这个webconfig文件指的是自己项目的webconfig。我们需要告知ASP.NET我们有哪些需要处理的HttpModule,否则打死它他也不会知道我们的自定义HttpModule。

这里需要的注意的是,在IIS6和IIS7经典模式中,我们需要这样配置:

 
   
   
 
  1. <system.web>

  2.    <httpModules>

  3.      <add name="MyModule" type="WebApplication.MyModule,WebApplication"/>

  4.    </httpModules>      

  5. </system.web>

type="WebApplication.MyModule,WebApplication"中的 WebApplication.MyModule

指的是 WebApplication命名空间下的 MyModule类,后面的 WebApplication是所在程序集的名称。

而在IIS7集成模式中,需要这样进行配置:

 
   
   
 
  1. <system.webServer>

  2.    <modules>

  3.      <add name="MyModule" type="WebApplication.MyModule,WebApplication"/>    

  4.    </modules>

  5. </system.webServer>

否则会报下面的错误:

ASP.NET三剑客 HttpApplication HttpModule HttpHandler 解析

一切准备完毕。启动项目请求/Default/Index页面:

ASP.NET三剑客 HttpApplication HttpModule HttpHandler 解析

可以发现,我们的自定义HttpModule发挥作用了。前面我们提到过,Modules集合(前面提到过,存在于HttpApplication)中的HttpModule在执行到相应的管道事件时都会触发自己的注册事件。我们来试一下。

我们再建立一个自定义HttpModule(YourModule):

 
   
   
 
  1. namespace WebApplication

  2. {

  3.    public class YourModule : IHttpModule

  4.    {

  5.        public void Dispose()

  6.        {

  7.            throw new NotImplementedException();

  8.        }

  9.        public void Init(HttpApplication context)

  10.        {

  11.            context.BeginRequest += new EventHandler(BeginRequest);

  12.            context.EndRequest += new EventHandler(EndRequest);

  13.        }

  14.        void BeginRequest(object sender, EventArgs e)

  15.        {

  16.            ((HttpApplication)sender).Context.Response.Write("<h1>请求处理开始前进入你的Module</h1>");

  17.        }

  18.        void EndRequest(object sender, EventArgs e)

  19.         {

  20.             ((HttpApplication)sender).Context.Response.Write("<h1>请求处理结束后进入你的Module</h1>");

  21.         }    

  22.    }

  23. }

然后配置webconfig告诉ASP.NET我们又建立一个自定义HttpModule,你一定要帮我执行啊。

 
   
   
 
  1. <system.webServer>

  2.    <modules>

  3.      <add name="MyModule" type="WebApplication.MyModule,WebApplication"/>

  4.      <add name="YourModule" type="WebApplication.YourModule,WebApplication"/>    

  5.    </modules>

  6. </system.webServer>

最后启动项目请求/Default/Index页面:

ASP.NET三剑客 HttpApplication HttpModule HttpHandler 解析

结果恰恰说明了:HttpModule会对请求依次进行处理,直到所有Modules集合(前面提到过,存在于HttpApplication)中的HttpModule都处理完为止。

那么HttpModule会对请求进行处理的顺序是怎么控制的呢?我们可以改变一下webconfig配置的顺序。

 
   
   
 
  1. <system.webServer>

  2.    <modules>

  3.      <add name="YourModule" type="WebApplication.YourModule,WebApplication"/>

  4.      <add name="MyModule" type="WebApplication.MyModule,WebApplication"/>

  5.    </modules>

  6. </system.webServer>

ASP.NET三剑客 HttpApplication HttpModule HttpHandler 解析

也就是说HttpModule的处理顺序,是根据配置的先后顺序来的,不存在什么优先级之说。

HttpHandler

与HttpModule针对所有的请求文件不同,HttpHandler是针对某一类型的文件,映射给指定的处理程序对请求进行出来。换一句话说就是,对请求真正的处理是在HttpHandler中进行的,前面的处理都是打辅助。但是并不是每一次请求HttpHandler都有机会接手的,辅助(HttpModule)也可以不给HttpHandler机会。

所有的HttpHandler都实现了IHttpHandler接口,其中的方法ProcessRequest提供了处理请求的实现。也就是说请求处理都是在这里面玩的,前提是辅助(HttpModule)得给机会,一会我们也写个例子玩一玩。

ASP.NET三剑客 HttpApplication HttpModule HttpHandler 解析

和HttpModule一样,HttpHandler类型建立与请求路径模式之间的映射关系,也需要通过配置文件。在C:\Windows\Microsoft.NET\Framework\v4.0.30319\Config文件夹下的webconfig文件中,也可以找到ASP.NET内置的HttpHandler配置。

ASP.NET三剑客 HttpApplication HttpModule HttpHandler 解析

ASP.NET中默认的HttpHandler映射操作发生在HttpApplication的PostMapRequestHandler事件之前触发,这种默认的映射就是通过配置。还有一种映射的方法,我们可以调用当前HttpContext的RemapHandler方法将一个HttpHandler对象映射到当前的HTTP请求。如果不曾调用RemapHandler方法或者传入的参数是null,则进行默认的HttpHandler映射操作。需要注意的是,通过RemapHandler方法进行映射的目的就是为了直接跳过默认的映射操作,而默认的映射操作是在HttpApplication的PostMapRequestHandler事件之前触发,所以在这之前调用RemapHandler方法才有意义。

 
   
   
 
  1. public sealed class HttpContext : IServiceProvider, IPrincipalContainer

  2. {

  3.   public void RemapHandler(IHttpHandler handler);  

  4. }

下面我们自己写以一个自定义HttpHandler玩一玩,我们有时候会有这么一个需求,自己的图片只希望在自己的站点被访问到,在其他站点或浏览器直接打开都不可以正常访问。那么HttpHandler就很适合这种场景的处理,我们以jpg格式的图片为例。

首先创建自定义HttpHandler(JPGHandler):

 
   
   
 
  1. namespace WebApplication

  2. {

  3.    public class JPGHandler : IHttpHandler

  4.    {

  5.        public bool IsReusable

  6.        {

  7.            get

  8.            {

  9.                return false;

  10.            }

  11.        }

  12.        public void ProcessRequest(HttpContext context)

  13.        {

  14.            context.Response.ContentType = "image/jpg";

  15.            // 如果UrlReferrer为空,则显示一张默认的404图片  

  16.            if (context.Request.UrlReferrer == null || context.Request.UrlReferrer.Host == null)

  17.            {

  18.                context.Response.WriteFile("/error.jpg");

  19.                return;

  20.            }

  21.            if(context.Request.UrlReferrer.Host.IndexOf("localhost") < 0)

  22.            {

  23.                context.Response.WriteFile("/error.jpg");

  24.                return;

  25.            }

  26.            // 获取文件服务器端物理路径  

  27.            string fileName = context.Server.MapPath(context.Request.FilePath);

  28.            context.Response.WriteFile(fileName);

  29.        }

  30.    }

  31. }

然后我们在站点下面添加两张图片做测试,当图片不可以正常显示时默认展示error图片:

ASP.NET三剑客 HttpApplication HttpModule HttpHandler 解析

测试搞起来,我们在浏览器中直接请求index.jpg资源。

ASP.NET三剑客 HttpApplication HttpModule HttpHandler 解析

效果不对啊,在浏览器中直接请求index.jpg资源应该是显示error图片啊。什么原因呢?不要忘了我们需要告诉ASP.NET我们自定义了HttpHandler,咱们没进行配置,ASP.NET当然不会知道。进行配置之后再来试试。

 
   
   
 
  1. <system.webServer>

  2.    <handlers>

  3.      <add name="jpg" path="*.jpg" verb="*" type="WebApplication.JPGHandler, WebApplication" />

  4.    </handlers>

  5. </system.webServer>

ASP.NET三剑客 HttpApplication HttpModule HttpHandler 解析

这次效果对了,是我们想要的。关于跨域图片访问我们就不做测试了,感兴趣的话可以自己试一试。

前面我们提到了HttpHandler默认的映射方式是通过配置,那么我们再来试一试非默认的方式,通过HttpContextd的RemapHandler方法。

这又到了辅助(HttpModule)来帮忙的时候了,因为需要在HttpModule注册管道事件。前文提到在PostMapRequestHandler事件之前调用RemapHandler方法才有意义。BeginRequest事件在PostMapRequestHandler事件之前,我们就在BeginRequest事件中调用RemapHandler方法。

 
   
   
 
  1. namespace WebApplication

  2. {

  3.    public class MyModule : IHttpModule

  4.    {

  5.        public void Dispose()

  6.        {

  7.            throw new NotImplementedException();

  8.        }

  9.        public void Init(HttpApplication context)

  10.        {

  11.            context.BeginRequest += new EventHandler(BeginRequest);            

  12.        }

  13.        void BeginRequest(object sender, EventArgs e)

  14.        {

  15.            ((HttpApplication)sender).Context.RemapHandler(new JPGHandler());

  16.        }

  17.    }

  18. }

然后我们需要在webconfig中配置MyModule,注释掉JPGHandler。

ASP.NET三剑客 HttpApplication HttpModule HttpHandler 解析

最后启动项目,访问index.jpg资源,结果果然不出意外,和默认方式通过配置一样,我们的自定义HttpHandler起到了效果。

ASP.NET三剑客 HttpApplication HttpModule HttpHandler 解析

我们再来试一下在PostMapRequestHandler事件之后调用RemapHandler方法,真的会没有意义吗?

我们将RemapHandler方法调用放到AcquireRequestState事件中,AcquireRequestState事件是PostMapRequestHandler事件后的第一个事件。

 
   
   
 
  1. namespace WebApplication

  2. {

  3.    public class MyModule : IHttpModule

  4.    {

  5.        public void Dispose()

  6.        {

  7.            throw new NotImplementedException();

  8.        }

  9.        public void Init(HttpApplication context)

  10.        {

  11.            context.AcquireRequestState += new EventHandler(AcquireRequestState);

  12.        }

  13.        void AcquireRequestState(object sender, EventArgs e)

  14.        {

  15.            ((HttpApplication)sender).Context.RemapHandler(new JPGHandler());

  16.        }

  17.    }

  18. }

然后启动项目,再访问index.jpg资源。

ASP.NET三剑客 HttpApplication HttpModule HttpHandler 解析

我们发现ASP.NET框架中已经给我们做了限定,并没有给我们任何犯错的机会!那么ASP.NET内部是怎么实现调用顺序限定的呢?我们可以通过ILSpy看一下源码。

ASP.NET三剑客 HttpApplication HttpModule HttpHandler 解析

圈红的部分,每当RemapHandler执行时,它会将当前方法所在事件(在ASP,NET管道模型中我们提到了随着HttpContext对象的创建,HttpRunTime会利用HttpApplicationFactory创建或获取现有的HttpApplication对象,HttpApplication对象包含着一个HttpContext属性,所以是能做到这一点的)和一个枚举(如下图,对管道事件按照顺序进行了枚举编码)进行比较,如果大于或等于这个枚举(PostMapRequestHandler事件),说明是在PostMapRequestHandler事件之后进行的映射,便会抛出异常。

ASP.NET三剑客 HttpApplication HttpModule HttpHandler 解析

总结

理解掌握了HttpApplication,HttpModule, HttpHandler这些并不能让我们变得牛逼,但是ASP.NET 的管道模型和高可扩展性的实现方式却对我们有着借鉴性的意义。再就是我们学习一定要自己动手体验一下,不要相信任何权威,要只相信自己的双手和自己的眼睛。希望大家看完这篇文章,脑子里能时刻记住这样一张图就OK了。

因为本人能力有限,所以文中错误难免,希望大家指正和提出宝贵建议。

参考:《ASP.NET MVC 5 框架揭秘》


-----END-----


以上是关于ASP.NET三剑客 HttpApplication HttpModule HttpHandler 解析的主要内容,如果未能解决你的问题,请参考以下文章

ASP NET 是啥?

ASP.NET_基础

ASP.NET Core与ASP.NET区别

ASP.NET和ASP的区别是啥?

ASP.NET 生命周期 – ASP.NET 应用生命周期

ado.net和asp.net区别?