我們都知道,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 框架揭祕》
-----END-----
喜歡本文的朋友們,歡迎掃一掃下圖關注公眾號擼碼那些事,收看更多精彩內容