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/
聲明:本文為博主學習感悟總結,水平有限,如果不當,歡迎指正。如果您認為還不錯,不妨點擊一下下方的 推薦按鈕,謝謝支持。轉載與引用請注明出處。
微信公眾號:


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM