細說 HttpHandler 的映射過程


在ASP.NET編程模型中,一個來自客戶端的請求要經過一個稱為管線的處理過程。 在整個處理請求中,相對於其它對象來說,HttpHandler的處理算得上是整個過程的核心部分。 由於HttpHandler的重要地位,我前面已經有二篇博客對它過一些使用上的介紹。 【用Asp.net寫自己的服務框架】 中談到了它的一般使用方法。 【細說ASP.NET的各種異步操作】 又詳細地介紹了異步HttpHandler的使用方式。

今天的博客將着重介紹HttpHandler的配置,創建以及重用過程,還將涉及HttpHandlerFactory的內容。

回顧HttpHandler

HttpHandler其實是一類統稱:泛指實現了IHttpHandler接口的一些類型,這些類型有一個共同的功能,那就是可以用來處理HTTP請求。 IHttpHandler的接口定義如下:

// 定義 ASP.NET 為使用自定義 HTTP 處理程序同步處理 HTTP Web 請求而實現的協定。
public interface IHttpHandler
{
    // 獲取一個值,該值指示其他請求是否可以使用 System.Web.IHttpHandler 實例。
    //
    // 返回結果:
    //     如果 System.Web.IHttpHandler 實例可再次使用,則為 true;否則為 false。
    bool IsReusable { get; }

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

有關HttpHandler的各類用法,可參考我的博客【用Asp.net寫自己的服務框架】, 本文將不再重復說明了。它還有一個異步版本:

// 摘要:
//     定義 HTTP 異步處理程序對象必須實現的協定。
public interface IHttpAsyncHandler : IHttpHandler
{
    // 摘要:
    //     啟動對 HTTP 處理程序的異步調用。
    //
    // 參數:
    //   context:
    //     一個 System.Web.HttpContext 對象,該對象提供對用於向 HTTP 請求提供服務的內部服務器對象(如 Request、Response、Session
    //     和 Server)的引用。
    //
    //   extraData:
    //     處理該請求所需的所有額外數據。
    //
    //   cb:
    //     異步方法調用完成時要調用的 System.AsyncCallback。如果 cb 為 null,則不調用委托。
    //
    // 返回結果:
    //     包含有關進程狀態信息的 System.IAsyncResult。
    IAsyncResult BeginProcessRequest(HttpContext context, AsyncCallback cb, object extraData);
    //
    // 摘要:
    //     進程結束時提供異步處理 End 方法。
    //
    // 參數:
    //   result:
    //     包含有關進程狀態信息的 System.IAsyncResult。
    void EndProcessRequest(IAsyncResult result);
}

IHttpAsyncHandler接口的二個方法該如何使用,可參考我的博客【細說ASP.NET的各種異步操作】, 本文也將不再重復講解。

如果我們創建了一個自定義的HttpHandler,那么為了能讓它處理某些HTTP請求,我們還需將它注冊到web.config中,就像下面這樣:


<httpHandlers>
    <add path="*.fish" verb="*" validate="true" type="MySimpleServiceFramework.AjaxServiceHandler"/>
</httpHandlers>

雖然我以前的博客中也曾多次涉及到HttpHandler,但感覺還是沒有把它完整地說清楚,今天的博客將繼續以前的話題,因為我認為HttpHandler實在是太重要了。

HttpHandler的映射過程

在博客【用Asp.net寫自己的服務框架】中, 我從MSDN中摘選了一些ASP.NET管線事件,在這些事件中,第10個事件【根據所請求資源的文件擴展名(在應用程序的配置文件中映射),選擇實現 IHttpHandler 的類,對請求進行處理】 就是本文要介紹的重點事件。這個事件也是HttpHandler創建的地方。

由於IIS6,7在管線事件觸發機制上的有一定的差別,本文將以ASP.NET 2.0以及IIS6的運行方式來介紹這個過程, IIS7的集成模式下只是觸發機制不同,但事件的絕大部分是一樣的。 管線事件由HttpApplication控制,由MapHandlerExecutionStep負責封裝這個事件的執行過程:

Execute()最終調用了MapHttpHandler()方法,這個映射過程是由HttpApplication實現的。
MapHttpHandler方法的實現如下:注意代碼中我加入的注釋。

今天的代碼將着重分析這段代碼,以揭示HttpHandler的映射過程。

在這段代碼中,有3個主要的調用過程:
1. this.GetHandlerMapping(context, requestType, path, useAppConfig)
2. this.GetFactory(mapping)
3. factory.GetHandler(context, requestType, path.VirtualPathString, pathTranslated)
后面將着重分析這三個調用。

HttpContext.RemapHandler()

在MapHttpHandler()方法的開始處,有這么一段代碼,不知您注意沒有?


IHttpHandler handler = (context.ServerExecuteDepth == 0) ? context.RemapHandlerInstance : null;

if( handler != null )
    return handler;

這段代碼是什么意思呢?
為了能讓您較為簡單地理解這段代碼的意義,請看我准備的一個示例:
我創建了一個TestRemapHandler.ashx文件:

至於這個文件在運行時能輸出什么,我想我就不用截圖了。
接下來,我再創建一個Global.asax文件,並寫了一個事件處理方法:

MyTestHandler是我定義的一個自定義的HttpHandler,后面的示例也會用到它,所以,我現在就將它的實現代碼貼出來:

類型Counter是一個簡單的計數器,它的實現如下:

現在,您可以想像一下當我再次訪問TestRemapHandler.ashx時,會在瀏覽器中看到什么?

以下是我看到的結果:

從截圖中,我們可以看出,服務端的輸出並不是TestRemapHandler.ashx產生的,而是由MyTestHandler產生的。 這也說明我調用Context.RemapHandler()確實影響了后面的MapHttpHandler過程。 現在我們再回過頭來再看一下前面那段代碼:


IHttpHandler handler = (context.ServerExecuteDepth == 0) ? context.RemapHandlerInstance : null;

if( handler != null )
    return handler;

這段代碼也主要是為配合HttpContext.RemapHandler()一起工作的。 這里要補充的是:context.ServerExecuteDepth的判斷是說有沒有調用HttpServerUtility.Execute()

小結:如果我們在MapHttpHandler事件前調用HttpContext.RemapHandler(),將會直接影響后面的映射過程, 或者說,我們可以直接指定一個HttpHandler,而不是讓ASP.NET來替我們來決定。

HttpContext.RemapHandler()的另類用途

我在【用Asp.net寫自己的服務框架】 曾演示過ASP.NET URL路由的設計思路,雖然那個示例能較好地反映微軟的Routing組件的工作方式,但對於示例來說,有些代碼還是可以簡化的。 今天我將使用HttpContext.RemapHandler()來簡化那段代碼,簡化后的版本如下:

在MyServiceUrlRoutingModule2這個新版本中,基本上跳過了MapHttpHandler()中所有復雜的處理邏輯,因此會更快。

這種方法雖然更簡單,但它只能設置一個參數IHttpHandler,因此,如果有其它的參數需要一起傳遞,則要修改相關的HttpHandler類型, 以便容納需要傳遞的參數。 例如,在【我的服務框架】中, MyServiceHandler類型就專門定義了一個屬性ServiceInfo來保存要調用的方法信息。 MyServiceUrlRoutingModule.GetHandler()的實現代碼如下:

寫到這里,有個問題把我難住了:為什么ASP.NET 3.5 UrlRoutingModule不用RemapHandler()而是采用較復雜的方法呢? 難道是為了能保留二個參數嗎?但那個【context.Request.Path】沒有必要保存呢。 實在是沒有想通,最后想到ASP.NET 4.0 UrlRoutingModule會不會不一樣了? 於是,只好再次安裝 .net framework 4.0 了(沒辦法,老家的舊機器上還真沒有.net 4.0), 結果當我用Reflector.exe找到4.0版本的UrlRoutingModule時,發現它已被移到System.Web.dll中,而且, ASPL.NET 4.0也在使用RemapHandler() ! 看來微軟當初忘記了這個方法。

GetHandlerMapping()

在MapHttpHandler()方法中,有下面這個調用:


// 到 <httpHandlers> 配置中查找一個能與requestType以及path匹配的配置項
HttpHandlerAction mapping = this.GetHandlerMapping(context, requestType, path, useAppConfig);

接下來,我們再來看一下這個調用的實現代碼:

代碼中的重要階段,我加了一點注釋。我們尤其要注意的FindMapping(requestType, path)的那個調用。
但前面的那個RuntimeConfig.GetAppConfig().HttpHandlers 和RuntimeConfig.GetConfig(context).HttpHandlers) 又可以得到什么結果呢?
為了能讓您更容易地理解這個調用,我准備了一段代碼,可以直觀地顯示這個調用結果:

我的示例網站中的web.config中的<httpHandlers>配置節定義如下:

<httpHandlers>
    <add path="Ajax*.*.aspx,Ajax*/*.aspx" verb="*" validate="true" 
                type="MySimpleServiceFramework.AjaxServiceHandler, MySimpleServiceFramework" />
    <add path="*.test" verb="*" validate="true" type="MyTestHandler" />
</httpHandlers>

那么,前面的示例代碼的運行結果如下:

通過前面分析可知,代碼中的HttpHandlers實際就是在運行時所能訪問到的<httpHandlers>配置集合。 而且,我們在網站中所指定的配置會放在集合的前面,ASP.NET的默認配置會在靠后的位置。 接下來的FindMapping(requestType, path)的實現如下:

從代碼可以看出,其實所謂的【查找】過程,也就是【逐一匹配】每個在<httpHandlers>定義的配置項。

至於action.IsMatch()的匹配方式,我們可以從調用所傳遞的參數看出,其實只是判斷【verb, path】這二個參數而已。 這個匹配過程的實現代碼較長,我就不貼出它們的實現代碼了,而是打算通過示例來說明它是如何匹配的。

action.IsMatch()的匹配方式也可以算是GetHandlerMapping()的核心過程。而這里的action的類型其實是HttpHandlerAction, 它與<httpHandlers>配置項一一對應。我們可以先來看一下MSDN是如何解釋這個配置項的:

從MSDN的解釋中可以看出:verb,path都可以支持【通配符(*)】,並且可以使用逗號(,)來分隔多個預期項。 ASP.NET在實現通配符時,內部使用正則表達式的方式,具體過程可以自己去閱讀相關代碼。

下面,我將通過一個具體的配置項來分析這些配置參數:


<add path="Ajax*.*.aspx,Ajax*/*.aspx" verb="*" validate="true" 
        type="MySimpleServiceFramework.AjaxServiceHandler, MySimpleServiceFramework" />

1. path="Ajax*.*.aspx,Ajax*/*.aspx":表示可以接受【Ajax*.*.aspx】或者【Ajax*/*.aspx】這樣的URL格式。
2. verb="*": 表示可以接受所有的HTTP調用,如:GET,POST 等等。
3. type="MySimpleServiceFramework.AjaxServiceHandler, MySimpleServiceFramework": 表示當某個請求匹配path,verb時, 交給MySimpleServiceFramework程序集中的MySimpleServiceFramework.AjaxServiceHandler來處理請求。
4. validate="true":表示在第一次讀取<httpHandlers>時,驗證type的設置是否有效。如果此項設為false,則表示延遲驗證(創建時)。默認值:true

對於這4個屬性,有2個比較重要:
1. type: type中的字符串必須能在運行時找到一個可創建的類型,且該類型必須實現IHttpHandler或者IHttpHandlerFactory接口。
2. path: 在很多資料以及技術書籍中,一般都設置為某類文件擴展名,如:*.xxx ,事實上,我們完全可以設置為一個URL模式, 而且還可以設置多個URL模式。比如上面的配置示例中的path參數,具體在使用時,可以響應來自客戶端的以下請求:

注意:在path中直接使用某個擴展名的好處是不受配置項的順序影響,而且容易理解,因為可讀性較好。

這里要補充一點:MySimpleServiceFramework是我在博客【用Asp.net寫自己的服務框架】 中實現的那個框架。
在服務端,可以使用下面的代碼來處理前面客戶端發出的請求:

前面的示例配置中:path="Ajax*.*.aspx,Ajax*/*.aspx" ,我使用了aspx這個擴展名。
我想大家都知道aspx這個擴展名是ASP.NET可處理的擴展名,為什么我還可以使用呢?
其實這個問題的答案在我前面的截圖以及FindMapping的實現代碼中:由於我們指定的配置要優先於ASP.NET的默認配置,所以先有機會參與匹配,並能匹配成功。

小結:在GetHandlerMapping()過程中,會根據請求的URL地址以及HTTP調用動作(GET,POST),返回一個在<httpHandlers>定義的配置項(HttpHandlerAction類型)。

GetFactory()

在GetHandlerMapping()返回一個HttpHandlerAction的配置項后, HttpApplication會調用this.GetFactory(mapping);獲取一個IHttpHandlerFactory ,本小節將來分析這個過程。

首先,我們還是來看一下GetFactory的實現代碼:

代碼中做了哪些事情,我在注釋中有說明。這個方法的最后一定會返回一個IHttpHandlerFactory的實例。

看到這里,您覺得奇怪嗎?
可能我們僅僅只是實現了一個自定義的IHttpHanlder接口,並沒有實現IHttpHandlerFactory,那么ASP.NET是如何處理的呢?

請注意 new HandlerFactoryCache(mapping) 這行代碼,這可不是簡單地創建一個緩存對象,具體實現代碼如下:

代碼中,我加入的注釋已經回答了前面提出的問題:如果是一個IHttpHandler對象,ASP.NET會再創建一個HandlerFactoryWrapper對象來包裝IHttpHandler對象, 如果是IHttpHandlerFactory對象,則直接返回。
我們再來看一下HandlerFactoryWrapper的實現代碼:

小結:HttpApplication會根據web.config中的配置去查找一個匹配的IHttpHandlerFactory類型, 即使我們配置的是自定義的IHttpHandler類型,而不是IHttpHandlerFactory類型,調用過程依然如此。

GetHandler()

在ASP.NET中,定義了二個版本的HttpHandlerFactory接口,分別為:

public interface IHttpHandlerFactory
{
    IHttpHandler GetHandler(HttpContext context, 
                            string requestType, string url, string pathTranslated);
    void ReleaseHandler(IHttpHandler handler);
}

internal interface IHttpHandlerFactory2 : IHttpHandlerFactory
{
    IHttpHandler GetHandler(HttpContext context, 
                            string requestType, VirtualPath virtualPath, string physicalPath);
}

設計IHttpHandlerFactory接口的目的是為了在創建和重用IHttpHandler對象時,保留了足夠的擴展機會, 而IHttpHandlerFactory2則是一個僅供微軟使用的內部接口(因為VirtualPath類型的可見性也是internal)。

我們都知道aspx, ashx能直接處理HTTP請求,它們都實現了IHttpHandler接口。它們能處理HTTP請求也因為ASP.NET已經配置過它們。 以下是它們的默認配置:

<httpHandlers>
    <add path="*.aspx" verb="*" type="System.Web.UI.PageHandlerFactory" validate="true"/>
    <add path="*.ashx" verb="*" type="System.Web.UI.SimpleHandlerFactory" validate="true"/>
</httpHandlers>

有趣的是:PageHandlerFactory和SimpleHandlerFactory都實現了IHttpHandlerFactory2接口,因此,它們都可以根據要請求的路徑創建一個IHttpHandler實例。

從ASP.NET的默認配置,我們也可以看到:type參數是可以設置為一個實現IHttpHandlerFactory接口的類型,而不一定要求是實現IHttpHandler接口的類型。

小結:HttpApplication在處理請求時,並不會直接創建一個IHttpHandler的實例,而是先獲取一個IHttpHandlerFactory的對象, 再以接口的形式調用GetHandler()方法來獲取一個IHttpHandler實例。

IHttpHandler.IsReusable

IHttpHandler接口有個IsReusable屬性。MSDN對這個屬性的說明也非常簡單:

獲取一個值,該值指示其他請求是否可以使用 IHttpHandler 實例。

按照這個說明,當我們直接在創建一個實現IHttpHandler的類型,並在web.config中注冊到一個自定義的擴展名時,情況會如何呢?
我們再來看一下前面所提過的示例代碼(MyTestHandler的):

web.config中的配置為:

<httpHandlers>
    <add path="*.test" verb="*" validate="true" type="MyTestHandler" />
</httpHandlers>

當我連續5次訪問 http://localhost:51652/abc.test?id=1 時,會在瀏覽器中看到以下輸出結果:

從這個截圖來看,顯然:MyTestHandler的實例被重用了。

我想很多人都創建過ashx文件,IDE會為我們創建一個實現了IHttpHandler接口的類型,在實現IsReusable屬性時,一般都會這樣:

public bool IsReusable {
    get {
        return false;
    }
}

有些人看到這個,會想:如果返回true,就可以重用IHttpHandler實例,達到優化性能的目的。 但事實上,即使你在ashx中返回true也是無意義的,因為您可以試着這樣去實現這個屬性:

public bool IsReusable
{
    get { throw new Exception("這里不起作用。"); }
}

如果您訪問那個ashx,會發現:根本沒有異常出現!
因此,我們可以得出一個結論:默認情況下,IsReusable不能決定一個ashx的實例是否能重用。

這個結果太奇怪了。為什么會這樣呢?

前面我們看到*.ashx的請求交給SimpleHandlerFactory來創建相應的HttpHandler對象, 然而當ASP.NET調用SimpleHandlerFactory.GetHandler()方法時, 該方法會直接創建並返回我們實現的類型實例。 換句話說:SimpleHandlerFactory根本不使用IHttpHandler.IsReusable的屬性,因此,這種情況下,想重用ashx的實例是不可能的事, 所以,即使我在實現IsReusable屬性時,寫上拋異常的語句,根本也不會被調用。

同樣的事情還發在aspx頁面的實例上,所以,在默認情況下,我們不可能重用aspx, ashx的實例。

至於aspx的實例不能重用,除了和PageHandlerFactory有關外,還與Page在實現IHttpHandler.IsReusable有關,以下是Page的實現方式:

從代碼可以看到微軟在Page是否重用上的明確想法:就是不允許重用!

由於Page的IsReusable屬性我們平時看不到,我想沒人對它的重用性有產生過疑惑,但ashx就不同了, 它的IsReusable屬性的代碼是擺在我們面前的,任何人都可以看到它,試想一下:當有人發現把它設為true or false時都不起作用,會是個什么想法? 估計很多人會郁悶。

小結:IHttpHandler.IsReusable並不能決定是否重用HttpHanlder !

實現自己的HttpHandlerFactory

通過前面的示例,我們也看到了,雖然IHttpHandler定義了一個IsReusable屬性,但它並不能決定此類型的實例是否能得到重用。 重不重用,其實是由HttpHandlerFactory來決定的。ashx的實例不能重用就是一個典型的例子。

下面我就來演示如何實現自己的HttpHandlerFactory來重用ashx的實例。示例代碼如下(注意代碼中的注釋):

為了能讓HttpHandlerFactory能在ASP.NET中運行,還需要在web.config中注冊:

<httpHandlers>
    <add path="*.ashx" verb="*" validate="false" type="ReusableAshxHandlerFactory"/>
</httpHandlers>

有了這個配置后,我們可以創建一個Handler2.ashx來測試效果:

在多次訪問Handler2.ashx后,我們可以看到以下效果:

再來看看按照IDE默認生成的IsReusable會在運行時出現什么結果。示例代碼:

此時,無論我訪問Handler1.ashx多少次,瀏覽器始終顯示如下結果:

如果我啟用代碼行 throw new Exception("這里不起作用。"); 將會看到以下結果:

終於,我們期待的黃頁出現了。
此時,如果我在web.config中將ReusableAshxHandlerFactory的注冊配置注釋起來,發現Handler1.ashx還是可以訪問的。

回想一下前面我們看到的IHttpHandlerFactory接口,它還定義了一個ReleaseHandler方法,這個方法又是做什么的呢? 對於這個方法,MSDN也有一句簡單的說明:

使工廠可以重用現有的處理程序實例。

對於這個說明,我認為並不恰當。如果按照HandlerFactoryWrapper的實現方式,那么這個解釋是正確的。 但我前面的示例中,我在實現這個方法時,沒有任何代碼,但一樣可以達到重用HttpHandler的目的。 因此,我認為重用的方式取決於具體的實現方式。

小結:IHttpHandler.IsReusable並不能完全決定HttpHandler的實例是否能重用,它只起到一個指示作用。 HttpHandler如何重用,關鍵還是要由HttpHandlerFactory來實現。

是否需要IsReusable = true ?

經過前面文字講解以及示例演示,有些人可能會想:我在實現IHttpHandler的IsReusable屬性時, 要不要返回true呢?(千萬別模仿我的示例代碼拋異常哦。

如果返回true,則HttpHandler能得到重用,或許某些場合下,是可以達到性能優化的目的。
但是,它也可能會引發新的問題:HttpHandler實例的一些狀態會影響后續的請求。
也正是由於這個原因,aspx, ashx 的實例在默認情況下,都是不重用的。

有些人還可能會擔心:被重用的HttpHandler是否有線程安全問題?
理論上,在ASP.NET中,只要使用static的數據成員都會有這個問題。 不過,這里所說的被重用的單個HttpHandler實例在處理請求過程中,只會被一個線程所調用,因此,它的實例成員還是線程安全的。 但有一點需要注意:在HttpHandlerFactory中實現重用HttpHandler時,緩存HttpHandler的容器要保證是線程安全的。

如果您希望重用HttpHandler來提升程序性能,那么我建議應該考慮以下問題:
HttpHandler的所有數據成員都能在處理請求前初始化。(通常會在后期維護時遺忘,尤其是多人維護時)

小結:在通常情況下,當實現IsReusable時返回false,雖然性能上不是最優,但卻是最安全的做法。

HttpHandlerFactory的主要用途

前面示例演示了如何使用HttpHandlerFactory來重用HttpHandler,但設計HttpHandlerFactory並不是完全為了這個目的, 它的主要用途還是如何創建HttpHandler,而且定義IHttpHandlerFactory的主要目的是為了擴展性。

我想很多人也許使用過 Web Service ,它運行在ASP.NET平台上,自然也有對應的HttpHandler,我們來看看asmx這個擴展名是如何映射的。


<add path="*.asmx" verb="*"  validate="false"
     type="System.Web.Services.Protocols.WebServiceHandlerFactory, 
     System.Web.Services, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"/>

接着找WebServiceHandlerFactory,最后發現是這樣創建的HttpHandler :

這才是Factory嘛!

老實說,看到這幾句話,我是眼前一亮:用HttpHandlerFactory來動態處理【是否支持Session】實在是太合適了。

這里有必要補充一下:

internal class SyncSessionHandler : SyncSessionlessHandler, IRequiresSessionState
{
}

小結:HttpHandlerFactory用途並非是為了專門處理HttpHandler的重用,它只是一個Factory, WebServiceHandlerFactory從另一個角度向我們展示了HttpHandlerFactory在擴展性方面所體現的重要作用。

點擊此處下載示例代碼


免責聲明!

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



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