標籤:項目 建立 管道 階段 接收 檔案 檔案服務 資料 命名空間
我們都知道,ASP.Net運行時環境中處理請求是通過一系列對象來完成的,包含HttpApplication,HttpModule, HttpHandler。之所以將這三個對象稱之為ASP.NET三劍客是因為它們簡直不要太重要,完全是ASP.NET界的中流砥柱,責任擔當啊。瞭解它們之前我們得Crowdsourced Security Testing道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/
聲明:本文為博主學習感悟總結,水平有限,如果不當,歡迎指正。如果您認為還不錯,不妨點擊一下下方的【推薦】按鈕,謝謝支援。轉載與引用請註明出處。
公眾號:
ASP.NET三劍客 HttpApplication HttpModule HttpHandler 解析