天天看點

關于HttpHandler和HttpModule差別,作用 了解ASP.NET管線 了解HttpApplication 了解HttpHandler 了解HttpModule 三大對象的總結 案例示範 如何選擇?

閱讀目錄

  • 開始
  • 了解ASP.NET管線
  • 了解HttpApplication
  • 了解HttpHandler
  • 了解HttpModule
  • 三大對象的總結
  • 案例示範
  • 如何選擇?

最近收到幾個疑問:HttpHandler和HttpModule之間有什麼差别,我到底該選擇哪個?

之是以有這個疑問,是因為在這二類對象中都可以通路Request, Response對象,都能處理請求。

我原以為在部落格 用Asp.net寫自己的服務架構 中做了那麼多的示範應該把它們的使用方法說清楚了, 然而有些人看了我的那些示例,仍然不知道該如何選擇它們,為了實作同一個目标,我既用了HttpHandler,也有用HttpModule。 現在看來,我當時設計的那些示例并不是講清楚HttpHandler和HttpModule之間有什麼差别, 而是在示範如何利用HttpHandler和HttpModule設計一個服務架構。

很慶幸那篇部落格内容沒有走題,今天隻好再來寫一篇了。

本文約定:

1. HttpHandler泛指所有實作IHttpHandler接口的類型。

2. HttpModule泛指所有實作IHttpModule接口的類型。

是以,本文将不會特别區分這些類型與接口。

了解ASP.NET管線

HttpHandler和HttpModule,它們都與ASP.NET管線有關,是以我想了解這二類對象必須要了解ASP.NET管線的工作方式。

下圖反映了ASP.NET管線的處理流程:

關于HttpHandler和HttpModule差別,作用 了解ASP.NET管線 了解HttpApplication 了解HttpHandler 了解HttpModule 三大對象的總結 案例示範 如何選擇?

這是一張時序圖,我們應該從二個角度來了解它:

1. 有哪些調用動作。

2. 有哪些參與者。

每個調用動作,都反映了ASP.NET管線的處理階段,它們會引發相應的事件(除GetHandler,ProcessRequest之外), HttpModule則會訂閱這些事件,參與到管線處理過程。 這些階段中,有些階段還引發了二個事件,完整的管線事件可參考MSDN文檔: 

在處理該請求時将由 HttpApplication 類執行以下事件。 希望擴充 HttpApplication 類的開發人員尤其需要注意這些事件。

1. 對請求進行驗證,将檢查浏覽器發送的資訊,并确定其是否包含潛在惡意标記。 有關更多資訊,請參見 ValidateRequest 和腳本侵入概述。

2. 如果已在 Web.config 檔案的 UrlMappingsSection 節中配置了任何 URL,則執行 URL 映射。

3. 引發 BeginRequest 事件。

4. 引發 AuthenticateRequest 事件。

5. 引發 PostAuthenticateRequest 事件。

6. 引發 AuthorizeRequest 事件。

7. 引發 PostAuthorizeRequest 事件。

8. 引發 ResolveRequestCache 事件。

9. 引發 PostResolveRequestCache 事件。

10. 根據所請求資源的檔案擴充名(在應用程式的配置檔案中映射),選擇實作 IHttpHandler 的類,對請求進行處理。 如果該請求針對從 Page 類派生的對象(頁),并且需要對該頁進行編譯,則 ASP.NET 會在建立該頁的執行個體之前對其進行編譯。

11. 引發 PostMapRequestHandler 事件。

12. 引發 AcquireRequestState 事件。

13. 引發 PostAcquireRequestState 事件。

14. 引發 PreRequestHandlerExecute 事件。

15. 為該請求調用合适的 IHttpHandler 類的 ProcessRequest 方法(或異步版 IHttpAsyncHandler.BeginProcessRequest)。 例如,如果該請求針對某頁,則目前的頁執行個體将處理該請求。 

16. 引發 PostRequestHandlerExecute 事件。

17. 引發 ReleaseRequestState 事件。

18. 引發 PostReleaseRequestState 事件。

19. 如果定義了 Filter 屬性,則執行響應篩選。

20. 引發 UpdateRequestCache 事件。

21. 引發 PostUpdateRequestCache 事件。

22. 引發 EndRequest 事件。

23. 引發 PreSendRequestHeaders 事件。

24. 引發 PreSendRequestContent 事件。

圖檔中還反映了ASP.NET的三種主要的參與者:

1. HttpModule

2. HttpHandlerFactory

3. HttpHandler

有沒有有想過:這三種參與者中,每種有多少個參與對象呢?

為了清楚地回答這個問題,我準備了下面的表格:

管線參與者 每次請求中參與者數量
HttpModule >= 0
HttpHandlerFactory 1
HttpHandler 1

為什麼要引入HttpHandlerFactory呢? 請看我的部落格 細說 HttpHandler 的映射過程,今天就不重複這塊内容了。

除開HttpHandlerFactory,我們可以發現:在ASP.NET管線中,HttpHandler應該隻有一個,而HttpModule是可選的。

進而,我們是不是可以這樣了解:HttpHandler才是處理請求的主角(不可缺少),HttpModule是配角(可以沒有)?

了解HttpApplication

前面我們一直在說ASP.NET管線,那麼,誰在控制管線過程?

答案是:HttpApplication對象。

1. HttpApplication細分它的處理過程,在不同階段引發不同的事件,使得HttpModule通過訂閱事件的方式加入到請求的處理過程中。

2. 在請求的處理過程中,HttpApplication對象主要扮演着控制處理流程的推進作用。

3. HttpApplication會在固定的階段擷取一個IHttpHandler執行個體,然後将請求的響應過程交給具體的IHttpHandler來實作。

HttpApplication如何産生,如何工作? 

1. HttpApplication對象會被重用,當HttpRuntime不能從HttpApplicationFactory擷取空閑的執行個體時,才會建立。

2. HttpRuntime會将每個請求交給一個HttpApplication對象來處理。

3. HttpApplication對象在初始化時負責加載全部的HttpModule。

4. 每個HttpApplication對象會控制屬于它的管線過程(前面已解釋)。

HttpApplication是個非常重要的類型,它的許多功能都屬于架構的基礎部分,不需要我們調用, 是以,我們平時不會用到它。

我不想讓部落格走題,下面來看看今天的主角吧。

了解HttpHandler

前面說到HttpRuntime會将請求交給HttpApplication來處理, 此時你有沒有想過這樣一個問題:為什麼HttpApplication不直接處理請求,而是要再交給一個HttpHandler對象來處理呢?

答案是:每個請求的内容可能并不相同,它們存在多樣性,是以ASP.NET采用了抽象工廠模式來處理這些請求。 ASP.NET在web.config的架構中,允許我們指定某些請求映射到一個HttpHandlerFactory,例如:

<!--适用于IIS6的配置-->
<system.web>
    <httpHandlers>
        <add path="*.cspx" verb="*" type="MyMVC.AjaxHandlerFactory, MyMVC" validate="true" />
        <add path="*.aspx" verb="*" type="MyMVC.MvcPageHandlerFactory, MyMVC" validate="true" />
        <add path="/mvc/*" verb="*" type="MyMVC.MvcPageHandlerFactory, MyMVC" validate="true" />
    </httpHandlers>
</system.web>

<!--适用于IIS7的配置(內建模式)-->
<system.webServer>
    <handlers>
        <add name="AjaxHandlerFactory" verb="*" path="*.cspx" type="MyMVC.AjaxHandlerFactory, MyMVC" preCondition="integratedMode" />
        <add name="MvcPageHandlerFactory" verb="*" path="*.aspx" type="MyMVC.MvcPageHandlerFactory, MyMVC" preCondition="integratedMode" />
        <add name="MvcPageHandlerFactory2" verb="*" path="/mvc/*" type="MyMVC.MvcPageHandlerFactory, MyMVC" preCondition="integratedMode" />
    </handlers>
</system.webServer>
      

當某個請求與一個規則比對後,ASP.NET會調用比對的HttpHandlerFactory的GetHandler方法來擷取一個HttpHandler執行個體, 最後由一個HttpHandler執行個體來處理目前請求。

HttpApplication是如何将請求交給HttpHandler執行個體來處理的呢?

為了了解這個過程,我們要來看一下IHttpHandler接口的定義:

// 這個接口用于同步調用
public interface IHttpHandler
{
    // 擷取一個值,該值訓示其他請求是否可以使用 IHttpHandler 執行個體。
    bool IsReusable { get; }

    // 通過實作 IHttpHandler 接口的自定義 HttpHandler 啟用 HTTP Web 請求的處理。
    void ProcessRequest(HttpContext context);
}
      

HttpApplication在将某個請求交給HttpHandler執行個體來處理時,是通過接口來調用的(ProcessRequest方法)。

與HttpHandler的相關話題:

1. 異步 HttpHandler:細說ASP.NET的各種異步操作。

2. 如何重用HttpHandler:細說 HttpHandler 的映射過程。

HttpHanlder的典型應用 

<%@ WebHandler Language="C#" Class="Login" %>

using System;
using System.Web;

public class Login : IHttpHandler {
    
    public void ProcessRequest (HttpContext context) {
        context.Response.ContentType = "text/plain";

        string username = context.Request.Form["name"];
        string password = context.Request.Form["password"];

        if( password == "aaaa" ) {
            System.Web.Security.FormsAuthentication.SetAuthCookie(username, false);
            context.Response.Write("OK");
        }
        else {
            context.Response.Write("使用者名或密碼不正确。");
        }
    }      

通常我去這樣建立一個ashx檔案(HttpHanlder)響應某種特殊的請求。

是以,我們應該這樣了解HttpHanlder:一個HttpHanlder用于響應一類特定的請求。

我們經常用到的HttpHanlder有哪些? 

1. aspx頁面。

2. asmx服務檔案。

3. ashx檔案(一般處理程式)。

4. 實作IHttpHandler接口的自定義類型。

我們通常使用HttpHanlder做什麼? 

HttpHanlder類型 實作目标
aspx頁面 響應請求,輸出HTML結果
asmx服務檔案 響應服務調用
ashx檔案(一般處理程式) 實作簡單的AJAX響應
實作IHttpHandler接口的自定義類 響應什麼擴充名的請求??

了解HttpModule

設計HttpHanlder的目的很明确:生成響應結果。

那麼,設計HttpModule又是為什麼呢?

前面說過,一個HttpHanlder用于處理一類特定的請求,每個aspx, ashx都可以認為是一類請求。 有時候我們發現所有頁面可能都需要某些相同的檢查功能(如身份檢查), 假如隻能使用HttpHanlder,那我們就要讓所有頁面都去調用那些相同的檢查功能。 誰願意做這些重複的事情? 或許有些人會回答,可以自己實作一個基類,把檢查功能放在基類中去調用。 然而,這種做法隻能解決重複調用問題,它會讓代碼失去靈活性(擴充性), 試想一下:如果需要再加入新的檢查功能,或者用新的檢查方法替換原有的檢查邏輯時怎麼辦? 隻能是修改基類了吧?

設計HttpModule的目的正是為了提供一個靈活的方法解決這種功能重用問題。 它采用事件(觀察者)的設計模式,将某些HttpHanlder都需要的功能抽取出來, 形成不同的觀察者類型,這些觀察者類型可以編譯成類庫形式,供多個網站項目共用。 為了讓ASP.NET管線更靈活,ASP.NET允許我們在web.config中自由配置需要的HttpModule,例如:

<!--适用于IIS6的配置-->
<system.web>
    <httpModules>
        <add name="SetOutputCacheModule" type="MyMVC.SetOutputCacheModule, MyMVC"/>
    </httpModules>
</system.web>

<!--适用于IIS7的配置(內建模式)-->
<system.webServer>
    <modules>
        <add name="SetOutputCacheModule" type="MyMVC.SetOutputCacheModule, MyMVC" preCondition="integratedMode" />
    </modules>
</system.webServer>
      

配置隻是告訴ASP.NET:這些HttpModule需要運作起來。 有沒有想過這些HttpModule到底是如何進入管線運作起來的呢? 前面我隻是說了HttpModule會訂閱這些事件,那麼事件又是在哪裡訂閱的呢? 還是來看一下IHttpModule接口的定義吧:

// 這個接口用于同步調用
public interface IHttpModule
{
    //  初始化子產品,并使其為處理請求做好準備。
    void Init(HttpApplication app);

    void Dispose();
}
      

注意這個關鍵的Init方法,它傳入一個HttpApplication類型的參數,有了HttpApplication對象,HttpModule就可以訂閱HttpApplication的所有事件了。 請看下面的示例代碼: 

public class TestModule : IHttpModule
{
    public void Dispose() {}

    public void Init(HttpApplication app)
    {
        app.PostAcquireRequestState += new EventHandler(app_PostAcquireRequestState);
        app.PreRequestHandlerExecute += new EventHandler(app_PreRequestHandlerExecute);
    }

    void app_PreRequestHandlerExecute(object sender, EventArgs e)
    {
        throw new NotImplementedException();
    }

    void app_PostAcquireRequestState(object sender, EventArgs e)
    {
        throw new NotImplementedException();
    }
}      

HttpModule的典型應用 

public class SetOutputCacheModule : IHttpModule
{
    public void Init(HttpApplication app)
    {
        app.PreRequestHandlerExecute += new EventHandler(app_PreRequestHandlerExecute);
    }

    void app_PreRequestHandlerExecute(object sender, EventArgs e)
    {
        HttpApplication app = (HttpApplication)sender;

        Dictionary<string, OutputCacheSetting> settings = ConfigManager.Settings;
        if( settings == null )
            throw new ConfigurationErrorsException("SetOutputCacheModule加載配置檔案失敗。");

        // 實作方法:
        // 查找配置參數,如果找到比對的請求,就設定OutputCache
        OutputCacheSetting setting = null;
        if( settings.TryGetValue(app.Request.FilePath, out setting) ) {
            setting.SetResponseCache(app.Context);
        }
    }      

這個Module用于給一些在配置檔案中指出要緩存的請求設定輸出緩存,示例代碼已在上篇部落格 不修改代碼就能優化ASP.NET網站性能的一些方法 介紹過了。 其實設定輸出緩存的最根本手段還是調用Response.Cache的一些公開方法,修改輸出響應頭。

我們用HttpModule做什麼事情? 

1. 修改某些請求(例如前面的示例修改了響應頭)。

2. 檢查檢查請求(例如身份認證檢查)。

HttpModule能處理哪些請求呢? 

1. 預設是全部進入ASP.NET的請求。

2. 如果隻需要處理部分請求,那麼請自行判斷(參考前面的示例)。

三大對象的總結

前面我分别介紹了HttpApplication,HttpHanlder和HttpModule,這裡再把三者的關系重新梳理一遍。

在請求的處理過程中,HttpApplication對象主要扮演着控制管線處理流程的作用,它負責推進整個處理流程, 除了在不同階段引發不同的事件外(供HttpModule使用),HttpApplication對象還會根據目前請求尋找一個合适的IHttpApplicationFactory執行個體, 并最終得到一個IHttpHandler的執行個體用于處理請求。

設計這三種類型的目的在于:

1. HttpApplication控制處理流程,在不同階段引發不同的事件。

2. 由于請求的多樣性,每個請求會由一個HttpHandler對象來處理。

3. 對于一些通用性的功能,尤其是與響應内容無關的,設計成HttpModule是最合适的。

案例示範

Q:我有一些html檔案,需要做身份認證檢查(判斷Session),我該如何實作?

想好了就來看看我的解決方案: 

/// <summary>
/// 用于響應HTML檔案的處理器
/// </summary>
public class HtmlHttpHandler : IHttpHandler, IRequiresSessionState, IReadOnlySessionState 
{
    public void ProcessRequest(HttpContext context)
    {
        // 擷取要請求的檔案名
        string filePath = context.Server.MapPath(context.Request.FilePath);

        // 用Fiddler檢視響應頭,如果看到有這個頭,就表示是由這段代碼處理的。
        context.Response.AddHeader("SeesionExist", (context.Session != null ? "yes": "no"));

        // 在這裡,你可以通路context.Session

        // 設定響應内容标頭
        context.Response.ContentType = "text/html";

        // 輸出檔案内容
        context.Response.TransmitFile(filePath);
    }      

在這個案例中我為什麼要選擇HttpHandler呢?

1. 我需要  響應一類請求 (所有的HTML檔案)。

2. 請求要求支援  Session

是以我最終選擇了HttpHandler 。

Q:我需要壓縮所有的ASP.NET請求的響應結果,該怎麼實作?

想好了就來看看我的解決方案: 

/// <summary>
/// 為請求支援gzip壓縮輸出的DEMO
/// </summary>
public class GzipModule : IHttpModule
{
    public void Init(HttpApplication app)
    {
        app.BeginRequest += new EventHandler(app_BeginRequest);
    }

    void app_BeginRequest(object sender, EventArgs e)
    {
        HttpApplication app = (HttpApplication)sender;

        // 判斷浏覽器是否支援GZIP壓縮
        string flag = app.Request.Headers["Accept-Encoding"];
        if( string.IsNullOrEmpty(flag) == false && flag.ToLower().IndexOf("gzip") >= 0 ) {
            // 設定GZIP壓縮
            app.Response.Filter = new GZipStream(app.Response.Filter, CompressionMode.Compress);
            app.Response.AppendHeader("Content-Encoding", "gzip");
        }
    }      

在這個案例中我為什麼要選擇HttpModule呢?

原因隻有一個:我需要  設定所有請求 。

是以我最終選擇了HttpModule 。

如何選擇?

在結束這篇部落格之前,再問問各位讀者:現在知道何時選擇HttpHandler還是HttpModule了嗎?

如果還沒有看明白,那我就最後告訴你一個識别方法:

1. 如果要響應一類請求,那麼就選擇HttpHandler。

2. 如果要修改或者檢查所有請求(總之就是不生成響應結果),那就選擇HttpModule。

最後給各位留下一個題目,下面這些ASP.NET提供的功能,它們是采用了哪個方式實作的?

1. Session

2. 身份認證

3. URL授權檢查

3. 通過trace.axd檢視跟蹤資訊

4. OutputCache

5. 禁止下載下傳config檔案

6. 禁止檢視下載下傳源代碼檔案

注意:本文的主題是:選擇HttpHandler還是HttpModule,是以請不要扯遠了。