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

整体处理流程如图所示:

mark

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

mark

HttpApplication

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

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

mark

每个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中还会讲到它们。

mark

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

mark

我们可以通过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对象,这就让事件注册变得十分容易了。

mark

我在了解了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文件,可以发现这样一段配置:

mark

这些都是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。

mark

接下来我们创建一个自定义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>

否则会报下面的错误:

mark

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

mark

可以发现,我们的自定义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页面:

mark

结果恰恰说明了: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>

mark

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

HttpHandler

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

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

mark

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

mark

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图片:

mark

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

mark

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

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

mark

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

前面我们提到了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。

mark

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

mark

我们再来试一下在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资源。

mark

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

mark

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

mark

总结

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

mark

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

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

来源:http://songwenjie.cnblogs.com/
声明:本文为博主学习感悟总结,水平有限,如果不当,欢迎指正。如果您认为还不错,不妨点击一下下方的推荐按钮,谢谢支持。转载与引用请注明出处。
微信公众号:
posted @ 2018-05-21 08:06  CoderFocus  阅读(1163)  评论(8编辑  收藏  举报