正文
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文件中,势必会让项目的配置变得十分复杂,所以统一都放到了这里进行配置。
至于上图中的节点中的HttpModule配置的作用,我们上面也提到过。前面我们讲到过,在HttpApplication初始化过程中,ASP.NET会根据 配置文件 加载并初始化注册的HttpModule对象。注册的HttpModule对象初始化后,存放在了HttpApplication的Modules属性之中。具体初始化哪些HttpModule对象,当然就是和这些配置相关啦。
虽然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 框架揭秘》
作者:撸码那些事
来源:http://songwenjie.cnblogs.com/
声明:本文为博主学习感悟总结,水平有限,如果不当,欢迎指正。如果您认为还不错,不妨点击一下下方的【推荐】按钮,谢谢支持。转载与引用请注明出处。
微信公众号: