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。
整体处理流程如图所示:
抽象之后的处理流程如图所示:
HttpApplication
HttpApplication是整个ASP.NET基础架构的核心,它负责处理分发给它的HTTP请求。
提起HttpApplication就不得不说全局配置文件global.asax。global.asax文件为每个Web应用程序提供了一个从HttpApplication派生的Global类。该类包含事件处理程序,如Application_Start。
每个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中还会讲到它们。
HttpApplication处理请求的整个生命周期是一个相对复杂的过程,为什么称之为复杂呢?因为HttpApplication类中存在大量的请求触发的事件,在请求处理的不同阶段会触发相应的事件。
我们可以通过HttpModule注册相应的事件,将处理逻辑注入到HttpApplication处理请求的某个阶段。这里需要注意的是,从BeginRequest开始的事件,并不是每个管道事件都会被触发。因为在整个处理过程中,随时可以调用Response.End()或者有未处理的异常发生而提前结束整个过程。所有事件中,只有EndRequest事件是肯定会触发的,(部分Module的)BeginRequest有可能也不会被触发。这个我们会在后面的HttpModule中提及。
HttpApplication类重要的Init方法和Dispose方法,这二个方法均可重载。它们的调用时机为:
Init方法在Application_Start之后调用,而Dispose在Application_End之前调用,另外Application_Start在整个ASP.NET应用的生命周期内只激发一次(比如IIS启动或网站启动时),类似的Application_End也只有当ASP.NET应用程序关闭时被调用(比如IIS停止或网站停止时)。
HttpModule
在前面我们讲解了ASP.NET管道模型和HttpApplication对象(其中的管道事件)。现在我们一起来了解一下HttpModule。
我们都知道ASP.NET高度可扩展,那么是什么成就了ASP.NET的高度扩展性呢?HttpModule功不可没。HttpModule在初始化的过程中,会将一些回调操作注册到HttpApplication相应的事件中,在HttpApplication请求处理生命周期的某一个阶段,相应的事件被触发,通过HttpModule注册的回调操作也会被执行。
所有的HttpModule都实现了IHttpModule接口,它和HttpApplication是直接打交道的。在其初始化方法Init()中接受了一个HttpApplication对象,这就让事件注册变得十分容易了。
我在了解了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中内置的HttpModule配置。至于为什么要放在这里,原因也很简单。这里的配置都是.NET Framework的默认和基础的配置,如果要配置在每个项目的webconfig文件中,势必会让项目的配置变得十分复杂,所以统一都放到了这里进行配置。
至于上图中的
虽然ASP.NET中内置了很多HttpModule,但是我们可以实现自定义HttpModule给予扩展满足需要。下面我们自己来实现一下自定义HttpModule:
首先我们创建一个MVC5控制器DefaultController,然后在控制器中创建一个视图Index。在页面显示Hello World。
接下来我们创建一个自定义HttpModule(MyModule):
namespace WebApplication
{
public class MyModule : IHttpModule
{
public void Dispose()
{
throw new NotImplementedException();
}
public void Init(HttpApplication context)
{
context.BeginRequest += new EventHandler(BeginRequest);
context.EndRequest += new EventHandler(EndRequest);
}
void BeginRequest(object sender, EventArgs e)
{
((HttpApplication)sender).Context.Response.Write("<h1>请求处理开始前进入我的Module</h1>");
}
void EndRequest(object sender, EventArgs e)
{
((HttpApplication)sender).Context.Response.Write("<h1>请求处理结束后进入我的Module</h1>");
}
}
}
我们在初始化方法Init中对HttpApplication的管道事件BeginRequest和EndRequest分别进行了注册。注册的事件会在响应中输出不同的文字。
最后不要忘记了在webconfig文件中进行配置,当然这个webconfig文件指的是自己项目的webconfig。我们需要告知ASP.NET我们有哪些需要处理的HttpModule,否则打死它他也不会知道我们的自定义HttpModule。
这里需要的注意的是,在IIS6和IIS7经典模式中,我们需要这样配置:
<system.web>
<httpModules>
<add name="MyModule" type="WebApplication.MyModule,WebApplication"/>
</httpModules>
</system.web>
type="WebApplication.MyModule,WebApplication"
中的WebApplication.MyModule
指的是WebApplication
命名空间下的MyModule
类,后面的WebApplication
是所在程序集的名称。
而在IIS7集成模式中,需要这样进行配置:
<system.webServer>
<modules>
<add name="MyModule" type="WebApplication.MyModule,WebApplication"/>
</modules>
</system.webServer>
否则会报下面的错误:
一切准备完毕。启动项目请求/Default/Index页面:
可以发现,我们的自定义HttpModule发挥作用了。前面我们提到过,Modules集合(前面提到过,存在于HttpApplication)中的HttpModule在执行到相应的管道事件时都会触发自己的注册事件。我们来试一下。
我们再建立一个自定义HttpModule(YourModule):
namespace WebApplication
{
public class YourModule : IHttpModule
{
public void Dispose()
{
throw new NotImplementedException();
}
public void Init(HttpApplication context)
{
context.BeginRequest += new EventHandler(BeginRequest);
context.EndRequest += new EventHandler(EndRequest);
}
void BeginRequest(object sender, EventArgs e)
{
((HttpApplication)sender).Context.Response.Write("<h1>请求处理开始前进入你的Module</h1>");
}
void EndRequest(object sender, EventArgs e)
{
((HttpApplication)sender).Context.Response.Write("<h1>请求处理结束后进入你的Module</h1>");
}
}
}
然后配置webconfig告诉ASP.NET我们又建立一个自定义HttpModule,你一定要帮我执行啊。
<system.webServer>
<modules>
<add name="MyModule" type="WebApplication.MyModule,WebApplication"/>
<add name="YourModule" type="WebApplication.YourModule,WebApplication"/>
</modules>
</system.webServer>
最后启动项目请求/Default/Index页面:
结果恰恰说明了:HttpModule会对请求依次进行处理,直到所有Modules集合(前面提到过,存在于HttpApplication)中的HttpModule都处理完为止。
那么HttpModule会对请求进行处理的顺序是怎么控制的呢?我们可以改变一下webconfig配置的顺序。
<system.webServer>
<modules>
<add name="YourModule" type="WebApplication.YourModule,WebApplication"/>
<add name="MyModule" type="WebApplication.MyModule,WebApplication"/>
</modules>
</system.webServer>
也就是说HttpModule的处理顺序,是根据配置的先后顺序来的,不存在什么优先级之说。
HttpHandler
与HttpModule针对所有的请求文件不同,HttpHandler是针对某一类型的文件,映射给指定的处理程序对请求进行出来。换一句话说就是,对请求真正的处理是在HttpHandler中进行的,前面的处理都是打辅助。但是并不是每一次请求HttpHandler都有机会接手的,辅助(HttpModule)也可以不给HttpHandler机会。
所有的HttpHandler都实现了IHttpHandler接口,其中的方法ProcessRequest提供了处理请求的实现。也就是说请求处理都是在这里面玩的,前提是辅助(HttpModule)得给机会,一会我们也写个例子玩一玩。
和HttpModule一样,HttpHandler类型建立与请求路径模式之间的映射关系,也需要通过配置文件。在C:\Windows\Microsoft.NET\Framework\v4.0.30319\Config文件夹下的webconfig文件中,也可以找到ASP.NET内置的HttpHandler配置。
ASP.NET中默认的HttpHandler映射操作发生在HttpApplication的PostMapRequestHandler事件之前触发,这种默认的映射就是通过配置。还有一种映射的方法,我们可以调用当前HttpContext的RemapHandler方法将一个HttpHandler对象映射到当前的HTTP请求。如果不曾调用RemapHandler方法或者传入的参数是null,则进行默认的HttpHandler映射操作。需要注意的是,通过RemapHandler方法进行映射的目的就是为了直接跳过默认的映射操作,而默认的映射操作是在HttpApplication的PostMapRequestHandler事件之前触发,所以在这之前调用RemapHandler方法才有意义。
public sealed class HttpContext : IServiceProvider, IPrincipalContainer
{
public void RemapHandler(IHttpHandler handler);
}
下面我们自己写以一个自定义HttpHandler玩一玩,我们有时候会有这么一个需求,自己的图片只希望在自己的站点被访问到,在其他站点或浏览器直接打开都不可以正常访问。那么HttpHandler就很适合这种场景的处理,我们以jpg格式的图片为例。
首先创建自定义HttpHandler(JPGHandler):
namespace WebApplication
{
public class JPGHandler : IHttpHandler
{
public bool IsReusable
{
get
{
return false;
}
}
public void ProcessRequest(HttpContext context)
{
context.Response.ContentType = "image/jpg";
// 如果UrlReferrer为空,则显示一张默认的404图片
if (context.Request.UrlReferrer == null || context.Request.UrlReferrer.Host == null)
{
context.Response.WriteFile("/error.jpg");
return;
}
if(context.Request.UrlReferrer.Host.IndexOf("localhost") < 0)
{
context.Response.WriteFile("/error.jpg");
return;
}
// 获取文件服务器端物理路径
string fileName = context.Server.MapPath(context.Request.FilePath);
context.Response.WriteFile(fileName);
}
}
}
然后我们在站点下面添加两张图片做测试,当图片不可以正常显示时默认展示error图片:
测试搞起来,我们在浏览器中直接请求index.jpg资源。
效果不对啊,在浏览器中直接请求index.jpg资源应该是显示error图片啊。什么原因呢?不要忘了我们需要告诉ASP.NET我们自定义了HttpHandler,咱们没进行配置,ASP.NET当然不会知道。进行配置之后再来试试。
<system.webServer>
<handlers>
<add name="jpg" path="*.jpg" verb="*" type="WebApplication.JPGHandler, WebApplication" />
</handlers>
</system.webServer>
这次效果对了,是我们想要的。关于跨域图片访问我们就不做测试了,感兴趣的话可以自己试一试。
前面我们提到了HttpHandler默认的映射方式是通过配置,那么我们再来试一试非默认的方式,通过HttpContextd的RemapHandler方法。
这又到了辅助(HttpModule)来帮忙的时候了,因为需要在HttpModule注册管道事件。前文提到在PostMapRequestHandler事件之前调用RemapHandler方法才有意义。BeginRequest事件在PostMapRequestHandler事件之前,我们就在BeginRequest事件中调用RemapHandler方法。
namespace WebApplication
{
public class MyModule : IHttpModule
{
public void Dispose()
{
throw new NotImplementedException();
}
public void Init(HttpApplication context)
{
context.BeginRequest += new EventHandler(BeginRequest);
}
void BeginRequest(object sender, EventArgs e)
{
((HttpApplication)sender).Context.RemapHandler(new JPGHandler());
}
}
}
然后我们需要在webconfig中配置MyModule,注释掉JPGHandler。
最后启动项目,访问index.jpg资源,结果果然不出意外,和默认方式通过配置一样,我们的自定义HttpHandler起到了效果。
我们再来试一下在PostMapRequestHandler事件之后调用RemapHandler方法,真的会没有意义吗?
我们将RemapHandler方法调用放到AcquireRequestState事件中,AcquireRequestState事件是PostMapRequestHandler事件后的第一个事件。
namespace WebApplication
{
public class MyModule : IHttpModule
{
public void Dispose()
{
throw new NotImplementedException();
}
public void Init(HttpApplication context)
{
context.AcquireRequestState += new EventHandler(AcquireRequestState);
}
void AcquireRequestState(object sender, EventArgs e)
{
((HttpApplication)sender).Context.RemapHandler(new JPGHandler());
}
}
}
然后启动项目,再访问index.jpg资源。
我们发现ASP.NET框架中已经给我们做了限定,并没有给我们任何犯错的机会!那么ASP.NET内部是怎么实现调用顺序限定的呢?我们可以通过ILSpy看一下源码。
圈红的部分,每当RemapHandler执行时,它会将当前方法所在事件(在ASP,NET管道模型中我们提到了随着HttpContext对象的创建,HttpRunTime会利用HttpApplicationFactory创建或获取现有的HttpApplication对象,HttpApplication对象包含着一个HttpContext属性,所以是能做到这一点的)和一个枚举(如下图,对管道事件按照顺序进行了枚举编码)进行比较,如果大于或等于这个枚举(PostMapRequestHandler事件),说明是在PostMapRequestHandler事件之后进行的映射,便会抛出异常。
总结
理解掌握了HttpApplication,HttpModule, HttpHandler这些并不能让我们变得牛逼,但是ASP.NET 的管道模型和高可扩展性的实现方式却对我们有着借鉴性的意义。再就是我们学习一定要自己动手体验一下,不要相信任何权威,要只相信自己的双手和自己的眼睛。希望大家看完这篇文章,脑子里能时刻记住这样一张图就OK了。
因为本人能力有限,所以文中错误难免,希望大家指正和提出宝贵建议。
参考:《ASP.NET MVC 5 框架揭秘》