ASP.NET三劍客 HttpApplication HttpModule HttpHandler 解析

咱們都知道,ASP.Net運行時環境中處理請求是經過一系列對象來完成的,包含HttpApplication,HttpModule, HttpHandler。之因此將這三個對象稱之爲ASP.NET三劍客是由於它們簡直不要過重要,徹底是ASP.NET界的中流砥柱,責任擔當啊。瞭解它們以前咱們得先知道ASP.NET管道模型。web

ASP.NET管道模型

這裏以IIS6.0爲例,它在工做進程w3wp.exe中會利用aspnet_isapi.dll加載.NET運行時。IIS6.0引入了應用程序池的概念,一個工做進程對應着一個應用程序池。一個應用程序池能夠承載一個或多個Web應用。若是HTTP.SYS(HTTP監聽器,是Windows TCP/IP網絡子程序的一部分,用於持續監聽HTTP請求)接收的請求是對該Web應用的第一次訪問,在成功加載運行時後,IIS會經過AppDomainFactory爲該Web應用建立一個應用程序域。也就是說一個應用程序池中會有多個應用程序域,它們共享一個工做進程資源,可是又不會互相牽連影響。api

隨後一個特殊的運行時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請求的處理流程。bash

最終完成對HTTP請求的處理在HttpHandler中,不一樣的資源類型對應着不一樣類型的HttpHandler服務器

總體處理流程如圖所示:網絡

mark

抽象以後的處理流程如圖所示:架構

mark

HttpApplication

HttpApplication是整個ASP.NET基礎架構的核心,它負責處理分發給它的HTTP請求。框架

提起HttpApplication就不得不說全局配置文件global.asax。global.asax文件爲每一個Web應用程序提供了一個從HttpApplication派生的Global類。該類包含事件處理程序,如Application_Start。ide

mark
ASP.NET MVC的程序入口

每一個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 框架揭祕》


                                                       -----END-----


喜歡本文的朋友們,歡迎掃一掃下圖關注公衆號擼碼那些事,收看更多精彩內容

                                      

相關文章
相關標籤/搜索