從更低的角度這篇文章在一個底層的角度來關注一個web請求怎樣到達asp.net框架,從web伺服器,通過ISAPI。看看這些後面發生了什麼,讓我們停止對asp.net的黑箱猜想。ASP.NET是一個非常強大用來創建web應用程序的平台,它為創建web應用程序提供了大量的靈活強大的支持。大多數人僅僅熟悉表層的WebForm和webservice,他們位於整個ASP.NET架構的最表層。在這篇文章里,我將會描述非常底層的ASP.NET並且解釋一個web請求是如何從web伺服器到達ASP.NET運行時,並且通過ASP.NET管道(pipeline)處理請求的。 對我來說理解一個平台的內部機制,能夠然我自己得到相應的滿意和舒適,同時也可以幫助我們寫出更好的應用程序。了解這些工具是怎樣作為整個框架的某一部分而互相配合,並且更加容易的找到問題的解決方案,更加重要的,在發生錯誤的時候,能夠幫助你定位以及調試這個錯誤。這篇文章的目標就是從系統的角度來看ASP.NET,並且幫助理解,請求是如何到達ASP.NET處理管道的。就這點而言,我們將會看到核心引擎,以及一個web請求是如何終結的。很多東西都是你在日常的工作中不需要知道的,但是它有利於理解ASP.NET架構怎樣路由到你經常編寫的應用程序的高層代碼的。大多數使用ASP.NET的人都是對Wenforms和WebService熟悉。這些高層的實現能夠簡化創建以web為基礎的應用程序,並且,ASP.NET是一個驅動引擎,提供了對web伺服器的底層介面,也為你在你的程序中用到的典型的高層前端服務的路由機制提供介面。WebForm和WebService僅僅是兩個在ASP.NET框架核心上構建的非常經久耐用的HttpHandlers。然而,ASP.NET在低層提供了更多的靈活性。HTTP Runtime和請求管道提供了所有的相同創建WebForm和WebService的能力,它們實際上也是由.NET託管代碼實現。而且,你如果決定自己定製、創建一個比WebForm稍低層的平台,所有的ASP.NET的低層的這些功能、機制,你也同樣可以使用。WebForm顯然是創建大多數web應用程序的最容易的方式,但是你在創建自定義內容的handlers或者對於進、出內容需要特殊的處理,或者你需要為另外一個應用程序創建一個定製的應用程序介面,使用低層的handlers或者modules能夠給你更好的性能以及對一個web請求的更好的控制。你也可以繞過WebForm和WebService的這些高層實現提供的這些功能、機制直接在底層進行操作。什麼是ASP.NET

讓我們以一個簡單的定義開始:什麼事ASP.NET?我喜歡把ASP.NET定義如下:ASP.NET是一個使用託管代碼完成的,從前到後處理web請求的,久經考驗的框架。它並不僅僅是WebForm和WebService…ASP.NET是一個請求處理引擎,它通過它的內部的管道將一個請求傳送到一個開發者的代碼上。實際上這個引擎完全獨立於HTTP或者web伺服器。事實上,HTTP Runtime是一個在IIS或者其他任何伺服器之外的,您的應用程序的宿主環境。舉一個例子,您可以將ASP.NET runtime放到一個Windows窗口中(點擊獲得更多詳情http://www.west-wind.com/presentations/ASP.NETruntime/ASP.NETruntime.asp)運行時為請求通過這個管道提供了一個複雜而又優雅的機制。有一系列的相關對象,大多數都是可以在請求的每一個層次,通過實現其子類或者實現事件介面來進行擴展。通過這個機制能夠接觸到非常低層的介面,例如緩存,許可權驗證等。你甚至能在接受請求的前後過濾內容,或者將滿足特定要求的請求轉到你的代碼或者其他的URL地址。有很多不同的方法來完成相同的事情,而且所有的這些方法實現的都非常直接,這樣,就可以靈活的根據性能以及開發難度來選擇最好的方法.整個的ASP.NET引擎都是託管代碼完成的,並且,可以支持通過託管代碼進行拓展整個的ASP.NET引擎都是託管代碼完成的,並且,可以支持通過託管代碼進行拓展.這是一個對.NET框架是否能夠開發出久經考驗的、性能良好的框架的有力的證明。然而,給人印象最深刻的部分是ASP.NET的深思熟慮的架構,能夠使得這個結構非常易用,提供了處理請求的任何一個部分的能力。使用ASP.NET你能夠完成 以前是ISAPI擴展和ISAPI篩選器領域的工作,雖然帶有一些局限性,但是比ASP要好很多。ISAPI是一個非常底層的Win32形式的API,它僅有非常貧乏的介面,非常難創建經久耐用的應用程序。由於ISAPI非常的底層而且非常快速,它處在非託管開發層。這樣ISAPI有些時候主要用做連接其他的應用程序平台的橋。這並不意味著ISAPI已經死了。事實上ASP.NET在微軟的平台上,正是通過ISAPI的一個擴展和ASP.NET的運行時和IIS進行交互的。ISAPI提供了Web伺服器的核心介面,並且ASP.NET使用非託管的ISAPI代碼來向客戶端接收,發送數據。ISAPI提供的數據,是通過一些通用的對象暴露出去的,像HttpRequest和HtttpReponse,他們通過託管代碼對象,以一個非常好的,易接觸的介面形式,對外暴露非託管代碼的內容。從瀏覽器到ASP.NET讓我們從一個典型的ASP.NET Web Request的生命周期的最初開始。一個請求,在瀏覽器里,在一個用戶輸入一個URL地址或者點擊一個超鏈接,或者提交一個HTML表單(一個post類型的請求)。或者一個客戶端的程序會調用ASP.NET的WebService,這個WebService也使用過ASP.NET進行服務的。在伺服器端,IIS5或者6接收到請求。在最底層,ASP.NET通過一個ISAPI擴展和IIS進行交互。這樣的一個請求通常會被路由到一個以aspx為擴展名的頁面文件,但是如何處理這個請求,完全取決與HTTP handler的實現,這個handler為了處理指定的擴展名而創立起來。在IIS里,.aspx 被『應用程序擴展』(也可以成為腳本映射) 映射到ASP.NET ISAPI dll - ASP.NET_isapi.dll.每一個觸發ASP.NET的請求都是必須通過在ASP.NET_isapi.dll指明和註冊的擴展名。根據擴展名,ASP.NET將請求路由到相應的,負責響應請求的handler。舉一個例子,asmx這是一個WebService的擴展名,它不會被路由到磁碟上面的一個頁面文件,而是路由到一個WebService類裡面。其他很多的映射已經被ASP.NET安裝了,而且你也可以定義你自己的。所有的這些HttpHandlers都是在ASP.NET ISAPI裡面指出,從而在IIS裡面被映射,或者在web.config文件裡面設置,路由到指定的HTTP Handler的實現。每一個handler,是處理指定的擴展名的一個.NET類,這個類可以簡單到一個HelloWorld程序,也可以非常複雜,像一個ASP.NET page類或者 WebService 的實現。現在,理解擴展名是這種映射機制的基礎,這種機制是ASP.NET用來從IIS獲得一個用戶請求然後將其路由到指定的處理請求的handler。ISAPI是第一個也是性能最高的定製web請求處理的切入點。ISAPI 連接ISAPI是一個非常低層的非託管的win32API。這個介面根據ISAPI定義規範,非常的簡單,還有優化過的性能。他們非常的底層-處理指針,用函數指針來進行回調-它們為開發者和工具提供最底層的,最好性能的,來處理IIS的介面。由於ISAPI非常的底層,他並適合創建應用程序級別的代碼,而且,ISAPI趨向主要被用作 為高層工具提供應用程序伺服器功能的 橋介面。舉個例子,ASP和ASP.NET都是建立在ISAPI上面,還有Cold Fusion,運行在IIS上面的,大多數的Perl,PHP以及JSP的實現還有很多的第三方的解決方案,比如我的Web Connection framework for Visual FoxPro都是建立在ISAPI上面的。ISAPI是一個為高層應用程序提供介面的非常優秀的工具,這些介面抽象了ISAPI提供的信息。在ASP.NET和ASP中,這些引擎將ISAPI提供的信息抽象為像Request和Response這樣的對象,使他們讀取到ISAPI的Request信息。把ISAPI想像成鉛錘。對於ASP.NET來說,ISAPI dll非常瘦小,僅僅是作為一個路由機制,以管道形式傳送請求到ASP.NET運行時,所有的重型的處理甚至請求的線程管理,都在ASP.NET引擎和你的代碼中。依照協議,ISAPI支持ISAPI擴展和ISAPI篩選器。擴展是一個請求處理介面,並且提供處理web伺服器的傳入傳出的邏輯,它本質上是一個事務介面。ASP.NET和ASP都是做為ISAPI擴展被實現的。ISAPI過濾器是一組介面,他們能 查看每一個進入IIS的請求,修改內容,或者改變類似於驗證功能的行為。順便提一句,在ASP.NET中,通過兩個概念映射了類似ISAPI的功能:HTTPHandlers(擴展)和HttpModules(篩選器)。一會,我們看詳細的內容。 ISAPI是標記著ASP.NET的請求的初始代碼。ASP.NET映射了各種擴展名到ISAPI的擴展里,這些映射都在.NET Framework的目錄下:<.NET FrameworkDir>ASP.NET_isapi.dll你可以互動式的在IIS服務管理器裡面看到這些映射,如圖一.選擇你的網站,然後「主目錄」,「配置」,「映射」。

圖一: IIS 映射各種擴展名到ASP.NET ISAPI,像 .ASPX 。通過這個機制,請求在web伺服器層被路由到ASP.NET的處理管道。你不應該手動的設置它們,因為.NET需要他們。另外你也可以使用ASP.NET_regiis.exe 工具來使得各種腳本映射得到正確的註冊:cd <.NetFrameworkDirectory>ASP.NET_regiis - i這就將會為整個的站點註冊特定版本的ASP.NET運行時,創建各種客戶端腳本庫。注意,這裡是註冊在上面的文件夾安裝的特定版本的CLR 。ASP.NET_regiis命令的選項允許你可以單獨的設置一個虛擬目錄。每一個版本的.NET框架都有他自己版本的ASP.NET_regiis,你需要運行一個恰當版本的來註冊一個網站或者一個虛擬目錄。以ASP.NET2.0為例,你可以在IIS配置頁面裡面的ASP.NET選項選擇.NET的版本。 IIS5和IIS6工作方式不同當一個請求進入,IIS檢查腳本映射,然後將請求路由到ASP.NET_isapi.dll。這個DLL進行的操作在IIS6和IIS5中明顯不同,圖2大概的展示了這個流程。IIS5中直接宿主ASP.NET_isapi.dll在inetInfo.exe進程中,或者一個獨立的進程中。當第一個請求來到這個DLL文件的時候,將會產生另外的一個新的進程– ASP.NET_wp.exe –並且路由請求到這個新生成的進程中。這個進程一次的載入,宿主.NET運行時。每一個請求都是先來到ISAPI然後通過命名管道路由到工作進程

圖二– 從IIS到ASP.NET運行時的. IIS 5 and IIS 6以不同的方式處理 ASP.NET,但是總的來說,一旦到了ASP.NET的管道,就相同了。IIS6,不像以前的伺服器,它是完全為ASP.NET做過優化的 IIS 6 –應用程序池萬歲IIS6明顯的改變了處理模型,IIS不再像ISAPI的擴展一樣直接的處理任何不相關的可執行代碼。取而代之的是,IIS6總是創建一個獨立的工作進程—一個應用程序池—並且所有的請求都在這個進程裡面,包括ISAPI dll的執行。應用程序池是IIS6的一個重要改善,因為它們允許非常細粒度的控制指定進程執行的東西。可以為每一個虛擬目錄或者一個站點設置應用程序池,所以你能夠將每一個web應用程序分割到每一個進程中,這個進程和其他的應用程序的進程完全獨立。如果其中的一個進程死掉了,也不會影響到其他的進程。 另外,應用程序池是高度可設置化的。你可以通過設置它的執行模擬級別 來設置其執行的安全環境,你可以為每一個Web應用程序定製許可權。為ASP.NET的一個重要的改進就是應用程序池取代了大多數的在machine.config的進程模型。這在IIS裡面比較難於管理,因為這個設置是全局的,而且不能在應用程序web.config中被繼承。當IIS6運行的時候,進程模型設置大部分被忽略了忽略,取而代之的是從應用程序池中讀取。我這裡是說「大部分」,對於一些設置,像進程池的大小,IO線程仍舊在裡面(配置文件)設置,因為應用程序池裡面沒有對應的設置。 因為應用程序池是外部的可執行的,而且這些可以很容易的進行監視和管理。IIS6提供了一些健康檢測,重啟,超時的選項,這些可以檢測,大多數可以修正應用程序的錯誤。最後,IIS6的應用程序池並不依賴COM+,和IIS5的獨立進程一樣,它改進了性能和穩定性,尤其是內部使用了COM對象的應用程序。儘管IIS6的應用程序池區分於可執行文件,他們通過直接的接觸HTTP.SYS核心模塊,而進行了高度的HTTP操作優化。進入的請求,直接的路由到相應的應用程序池。InetInfo僅僅扮演了一個管理、設置服務-大多說交互實際發生在HTTP.SYS和應用程序之間,所有的這些構成了一個比IIS5更加穩定,更加高性能的環境。尤其是對於一些靜態內容和ASP.NET應用程序。一個IIS6的應用程序池也有ASP.NET內在的認識,而且一個ASP.NET能夠和新的底層的API進行交互,這使得ASP.NET直接接觸到HTTP Cache的API,也就使得能夠從ASP.NET層來控制Web伺服器的緩存。在IIS6,ISAPI擴展運行在一個應用程序池的工作進程中。.NET運行時也運行在這個進程,所以.NET運行時和ISAPI 擴展的交互是進程內的,這樣肯定是比一定要使用命名管道介面的IIS5效率更高。儘管IIS兩個版本的宿主模型是不同的,但是到達託管代碼之後都是相同的了,只有在做請求路由的時候有一些不同。ISAPIRuntime.ProcessRequest() 方法是進入ASP.NET的第一個入口進入到.NET運行時實際上進入.NET運行時,是通過一系列沒有給出文檔說明的類和介面。通過微軟,很少能夠了解這些介面和類,而且微軟並不想談論這些細節,因為他們認為這些實現的細節幾乎對創建一個ASP.NET應用程序沒有影響。 工作進程ASP.NET_WP.EXE (IIS5)和W3WP.EXE (IIS6)宿主.NET運行時,並且ISAPI DLL 通過底層的COM調用了一些少量的非託管介面,而最終調用到了一個ISAPIRuntime的子類。第一個入口就是這個沒有文檔說明的ISAPIRuntime類,它通過COM將IISAPIRuntime介面暴露給調用者。這些COM介面,底層的以不了解的介面,意味著從ISAPI擴展到ASP.NET的內部調用。圖3顯示了這個介面,在Lutz Roeder的優秀的.NET Reflector 工具(http://www.aisto.com/roeder/dotnet/)。反射程序集視圖和反編譯器,這使得我們能夠非常容易的看到反編譯的代碼(用IL,C#,VB),這是一個非常好的方法來談就這樣的過程。

圖3 –如果你想深入研究底層的介面,打開Reflector,並且打開 System.Web.Hosting 命名空間。進入ASP.NET的入口點通過COM介面而被ISAPI dll調用,這就獲得了一個非託管的指針,指向了ISAPI ECB.。ECB包含了訪問ISAPI的全部介面,能夠獲得請求的數據,也能夠將數據返回給IIS。IISAPIRuntime介面在ISAPI擴展的非託管代碼和託管代碼之間。如果你看一個這個類,你會發現有一個簽名如下的方法:[return: MarshalAs(UnmanagedType.I4)]int ProcessRequest([In] IntPtr ecb, [In, MarshalAs(UnmanagedType.I4)] int useProcessModel);ecb這個參數是ISAPI擴展控制模塊(Extension Control Block –ECB),這個方法將非託管的資源傳到ProcessRequest方法中。這個方法獲得ECB並且 通過Request和Response對象將它作為輸入輸出的基礎。一個ISAPI ECB包含了所有的底層請求信息,包括伺服器信息,一個來組織變數的輸入流同時也有一個用來寫回數據給客戶端的輸出流。這個單獨的ecb基本提供了所有的ISAPI請求的所有的功能,而ProcessRequest方法是這個資源和託管代碼交互的出口和入口。ISAPI擴展非同步的處理請求。這個模型中,ISAPI擴展立刻回報給工作進程或者IIS線程,而保持當前的請求的ECB存活。ECB包括一個機制允許ISAPI知道請求已經完成(通過ecb.ServerSupportFunction),然後釋放ECB。這個非同步處理立刻釋放ISAPI工作進程,並且卸載進程,而轉到由一個ASP.NET管理的獨立的線程。ASP.NET接收到這個ecb引用,用它來獲取一些關於當前請求的信息,比如伺服器變數,POST數據,也包括向伺服器返回輸出數據。ECB存活到請求完成,或者IIS超時並且,ASP.NET會繼續和其進行交互,直至請求完畢。輸出被寫入到ISAPI的輸出流(ecb.WriteClient()),並且,當請求完成的時候,ISAPI擴展被通知請求完成,ECB可以被釋放。這個實現非常的高效,因為.NET類完全扮演的是對高性能的非託管代碼簡單的封裝,載入.NET - 有一些神秘Loading .NET 讓我們回到這裡的一個步驟:我跳過了.NET運行時是怎麼載入的。這裡的事情有一點模糊,這個過程,我沒有獲得任何的文檔,並且我們在談本地代碼,而又沒有簡單的方法反編譯ISAPI DLL把它找出來。 我的最好的猜想是,當第一個ASP.NET映射擴展名被請求的時候,ISAPI擴展引導了.NET運行時。一旦這個運行時存在了之後,如果當前沒有,非託管代碼可以為給定的虛擬路徑請求一個ISAPIRuntime的實例對象。每一個虛擬目錄都會有它自己的AppDomain(應用程序域),在AppDomain里,ISAPIRuntime存在於一個獨立的應用程序引導的過程。實例化看起來像發生在COM裡面,因為介面方法是一個COM可調用的方法。為了創建ISAPIRuntime實例,System.Web.Hosting.AppDomainFactory.Create()方法被調用,當某一個虛擬目錄第一次被請求的時候。這開始了『應用程序』引導過程。這個調用獲得了參數的類型,模塊的名稱以及虛擬路徑的信息—對於應用程序來說,這個ASP.NET用來創建一個AppDomain以及運行指定虛擬目錄的ASP.NET應用程序。這個HttpRuntime派生對象在一個新的AppDomain裡面被創建。每一個虛擬目錄都被宿主在一個獨立的AppDomain中,僅僅載入指定的應用程序的請求。ISAPI擴展來管理HttpRuntime對象的實例,路由相應的請求到正確的請求的虛擬路徑上。

圖4 – 從ISAPI請求到ASP.NET的HTTP管道的傳輸過程,使用到了一些沒有文檔說明的類和介面,並且需要一些工廠方法調用。通過其調用者把一個引用放在IISAPIRuntime介面中(這個介面觸發了ASP.NET請求處理)每一個Web應用程序運行在其自身的AppDomain中.回到運行時在這時,我們已經有了一個被ISAPI擴展激活,可被其調用的ISAPIRuntime的實例。一旦運行時運行起來,ISAPI代碼將會調用到ISAPIRuntime.ProcessRequest()這個方法,這個方法是真正的進入ASP.NET管道的入口。這個流程已經顯示在圖4中。記住,ISAPI是一個多線程的,這樣請求將會以多線程的方式進如ASP.NET,通過ApplicationDomainFactory.Create()返回的引用。列表1顯示了IsapiRuntime.ProcessRequest反編譯的結果,這個方法接收一個ISAPI ecb對象,這個方法是線程安全的,所以,多線程的ISAPI可以同時的安全的調用這個單獨返回的對象實例。Listing 1: 處理請求的方法獲得了一個ISAPI Ecb並且將其傳入工作進程

CodepublicintProcessRequest(IntPtrecb,intiWRType){HttpWorkerRequestrequest1=ISAPIWorkerRequest.CreateWorkerRequest(ecb,iWRType);stringtext1=request1.GetAppPathTranslated();stringtext2=HttpRuntime.AppDomainAppPathInternal;if(((text2==null)||text1.Equals("."))||(string.Compare(text1,text2,true,CultureInfo.InvariantCulture)==0)){HttpRuntime.ProcessRequest(request1);return0;}HttpRuntime.ShutdownAppDomain("Physicalapplicationpathchangedfrom"+text2+"to"+text1);return1;}

這裡的代碼並不重要,並且記住,這是你將從來不會直接接觸到的,反編譯出來的框架內部的代碼,而且這些代碼可能在以後會改變。這意味著證實在後台發生了什麼。ProcessRequest接收到非託管的ECB引用,並且將它傳送到ISAPIWorkerRequest對象中,這個對象負責為當前請求創建請球上下文,就像在Listing2中顯示的一樣。 System.Web.Hosting.ISAPIWorkerRequest類是HttpWorkerRequest的一個抽象子類。它的職責是為輸入輸出創建一個抽象的視圖,這個作為整個web應用程序的輸入。注意另外一個工廠方法: CreateWorkerRequest,做為第二個參數,它接收到了要創建的worker request的類型,這裡有三種不用的版本:ISAPIWorkerRequestInProc, ISAPIWorkerRequestInProcForIIS6, ISAPIWorkerRequestOutOfProc。這個對象在每一個請求進來之後被創建,這個對象是Request和Response對象基礎,他們從WorkerRequest里接收他們的數據和流。這個HttpWorkerRequest抽象類用來為底層的介面提供一個高層的抽象,所以不管這個數據是來自一個CGI Web伺服器,一個瀏覽器,或者一些訂製的機制。關鍵的是ASP.NET能夠以同樣的方式來獲得信息。對於IIS的抽象,以一個ISAPI ECB模塊。在我們的請求過程中,ISAPIWorkRequst和IISAPI ECB交互,當需要的時候,通過它獲得數據。Listing2顯示展示了如何獲得query string 的值。Listing 2: 一個使用非託管的ISAPIWorkerRequest 的方法

Codepublicoverridebyte[]GetQueryStringRawBytes(){byte[]buffer1=newbyte[this._queryStringLength];if(this._queryStringLength>0){intnum1=this.GetQueryStringRawBytesCore(buffer1,this._queryStringLength);if(num1!=1){thrownewHttpException("Cannot_get_query_string_bytes");}}returnbuffer1;}//***ImplementedinaspecificimplementationclassISAPIWorkerRequestInProcIIS6internaloverrideintGetQueryStringCore(intencode,StringBuilderbuffer,intsize){if(this._ecb==IntPtr.Zero){return0;}returnUnsafeNativeMethods.EcbGetQueryString(this._ecb,encode,buffer,size);

ISAPIWorkerRequest實現了一個高層的封裝的方法,這個方法調用底層的用來和訪問底層非託管的API的Core方法。Core 方法在ISAPIWorkerRequest實例的子類中實現,這樣,為其宿主的環境提供實現方法。這樣構造了一個更加容易插拔的環境,方便添加一些新的伺服器或者服務ASP.NET的其他平台的介面的實現。也有一個輔助類System.Web.UnsafeNativeMethods。很多的這些方法,他們就是通過操作ISAPI ECB結構體來完成對ISAPI擴展的調用。 HttpRuntime, HttpContext, and HttpApplication 當一個請求到達,被路由到ISAPIRuntime.ProcessRequest()方法。這個方法繼而調用HttpRuntime.ProcessRequest,這個方法做了很多重要的事情(使用Reflector 查看System.Web.HttpRuntime.ProcessRequestInternal):?為請求創建一個新的HttpContext實例 ?得到一個HttpApplication 實例 ?調用 HttpApplication.Init() 來建立管道事件。 ?Init() 觸發 HttpApplication.ResumeProcessing(),這個方法開始ASP.NET管道處理 首先,一個新的HttpContext對象被封裝了ISAPI ECB的 ISAPIWorkerRequest創建,傳遞。這個Context在整個的請求生命周期中都可用,總是可以通過靜態的HttpContext.Current 屬性來獲得。就像名字提示的,HttpContext對象表示了當前激活的請求的上下文,因為它包含了在請求的生命周期中,所有的你要接觸到的重要的典型的對象: Request, Response, Application, Server, Cache.在處理請求的任何時候,HttpContext.Current讓你接觸到所有的這些對象。HttpContext對象同時包含了一個非常有用的集合,允許你用來存儲一些請求指定的數據。Context對象從請求周期被創建,當請求完成的時候釋放,所以存儲在這個集合的數據僅僅對應當前的請求。一個很好的應用的例子是一個時間請求記錄的機制,這裡你想記錄請求開始和請求結束的時間,通過在Listing3所示,在Global.asax 裡面,Application_BeginRequest 和Application_EndRequest 方法。HttpContext是你的朋友,你可以自由的使用,在不同的請求或者頁面處理的部分。

Listing 3 – 使用 HttpContext.Items 結合來讓你在管道事件之間保存數據

CodeprotectedvoidApplication_BeginRequest(Objectsender,EventArgse){//***RequestLoggingif(App.Configuration.LogWebRequests)Context.Items.Add("WebLog_StartTime",DateTime.Now);}protectedvoidApplication_EndRequest(Objectsender,EventArgse){//***RequestLoggingif(App.Configuration.LogWebRequests){try{TimeSpanSpan=DateTime.Now.Subtract((DateTime)Context.Items["WebLog_StartTime"]);intMiliSecs=Span.TotalMilliseconds;//doyourloggingWebRequestLog.Log(App.Configuration.ConnectionString,true,MilliSecs);}}

一旦Context對象被創立,ASP.NET需要路由進入的請求到相應的應用程序/虛擬目錄,通過一個HttpApplication對象。每一個ASP.NET應用程序被需建立一個虛擬目錄(或者根根目錄)每一個『application』獨立的處理。 HttpApplication像一個典禮的主人,處理動作在這裡開始

你的域的主人: HttpApplication每一個請求都被路由到一個HttpApplication對象。HttpApplicationFactory類根據你的ASP.NET應用程序的負載情況,為其創建一個HttpApplication對象池,並且為每一個請求處理這些引用。這個池的容量受限於設置在machine.config的ProcessModel鍵裡面的MaxWorkerThreads的值,默認值是20.然而這個池只啟動了少量對象,通常是一個然後同時進入的請求多了,池中對象將會增長。池是被監視的,在負載量大的時候,將會增長到它的容量的最大值,當負載下降的時候,池的容量又會降低。HttpApplication是你指定的Web應用程序的外部容器,並且它映射到定義在Global.asax的文件中。他是進入HttpRuntime的第一個點,如果你看Global.asax(或者其後置代碼)你會發現,他是派生自HttpApplication的一個類:public class Global : System.Web.HttpApplicationHttpApplication的主要目的是扮演了Http管道的事件控制者,所以它的介面主要由事件組成。事件是多方面的,包括:?BeginRequest ?AuthenticateRequest ?AuthorizeRequest ?ResolveRequestCache ?AquireRequestState ?PreRequestHandlerExecute ?Handler Execution ?PostRequestHandlerExecute ?ReleaseRequestState ?UpdateRequestCache ?EndRequest 這些事件都在Global.asax文件中通過以Application_為前綴的空方法實現。舉個例子,Application_BeginRequest(), Application_AuthorizeRequest().這樣的處理方法,非常的方便,因為在應用程序中,他們經常會被用到,這樣你就不用顯示的創建事件處理的委託。理解每一個ASP.NET虛擬用應程序運行在它自己的AppDomain中,然而在這個AppDomain中,有多個HttpApplication實例在同時運行,被一個ASP.NET的一個池來進行管理。這樣,多個請求就可以同時被處理,而且沒有互相的影響。 來看一看AppDomain,線程和HttpApplication的關係,看看Listing4的代碼。Listing 4 – 顯示了AppDomain,線程和HttpApplication實例的關係

CodeprivatevoidPage_Load(objectsender,System.EventArgse){//Putusercodetoinitializethepageherethis.ApplicationId=((HowASP.NETWorks.Global)HttpContext.Current.ApplicationInstance).ApplicationId;this.ThreadId=AppDomain.GetCurrentThreadId();this.DomainId=AppDomain.CurrentDomain.FriendlyName;this.ThreadInfo="ThreadPoolThread:"+System.Threading.Thread.CurrentThread.IsThreadPoolThread.ToString()+"<br>ThreadApartment:"+System.Threading.Thread.CurrentThread.ApartmentState.ToString();//***Simulateaslowrequestsowecanseemultiple//requestssidebyside.System.Threading.Thread.Sleep(3000);}

這部分代碼運行的結果在圖5中顯示,在兩個不同的瀏覽器中來訪問這個示例頁面,來看這不用的Id

圖 5 –通過同時用兩個瀏覽器訪問,你可以看到AppDomain,Application池還有請求進程怎樣互相影響,當多個請求到達的時候,你會發現線程,Application的Id都改變了,AppDomain卻保持不變。你會注意到,AppDomain 的ID保持不變,而HttpApplication的ID在大多數請求中都變化了,儘管他們有可能會重複。這些HttpApplication用過了,會在後面的請求中復用,所以Id是會重複出現的。注意,Application實例並不綁定到指定的線程,他們只是被分配到當前請求的線程中。線程來自 .NET的ThreadPool,而且,默認的是Multithreaded Apartment (MTA)這種形式的線程。你可以在ASP.NET頁面中重寫這部分,通過在@Page 指令設置ASPCOMPAT="true" 屬性,ASPCOMPAT意味著提供給COM組建一個安全的環境來運行,並且,ASPCOMPAT使用特殊的Single Threaded Apartment (STA)線程來服務這些請求。STA的線程被擱置、合併,因為他們需要特殊的處理。事實上,HttpApplication對象都在同一個AppDomain中非常重要。這正是ASP.NET怎樣保證修改了web.config或者獨立的頁面,可以被整個的AppDomain所識別。在web.config裡面修改值,可以引起AppDomain的關閉和重啟,這使得所有的HttpApplication都發現了這個變化,因為AppDomain重新載入的時候,他重新讀取了信息。所有的靜態成員都重新載入了,因為AppDomain重新載入,所以,應用程序從應用程序配置文件讀取設置的時候這些值都被更新了。來看這個例子。訪問ApplicationPoolsAndThreads.aspx頁面,並且注意AppDomain的Id。然後修改一下web.config(添加一個空格並且保存)。然後重新讀取這個頁面,你會發現AppDomain已經被重新創建了。本質上,當這發生後,web應用程序/虛擬目錄完全的『重啟』了。所有的已經在管道中的請求,將會繼續的通過現在已經存在的管道繼續運行,同時,所有的新的請求都會被路由到新的AppDomain。為了處理那些『掛起的請求』,在這些請求超時後或者甚至請求還在進行的時候,ASP.NET會強制關閉舊的AppDomain。這樣,實際上在某一特定時間點,可以存在兩個相同的AppDomain,為同一個HttpApplication服務,舊的AppDomain關閉,新的AppDomain裡面的Application對象將會急劇增加。兩個AppDomain都繼續服務,直到舊的一個將所有的請求都運行完畢,舊的將會被關閉,只留下新的AppDomain。ASP.NET管道的流程

HttpApplication通過觸髮指示你的應用程序狀態的事件,來負責請求的流程。這發生在HttpApplication.Init()方法中(用Reflector看System.Web.HttpApplication.InitInternal 和 HttpApplication.ResumeSteps() ),這個方法連續的創建並觸發了一系列的事件,包括了調用執行所有的handlers.事件處理自動的映射在global.asax裡面的事件,並且,他們也映射所有已經附加了的HTTPModule,本質上,HTTPModule是一個形象化了事件槽。HttpModules 和 HttpHandlers a都通過web.config裡面條目而被動態載入,並且將其綁定到事件鏈。HttpModules是實際的HttpApplication的事件處理者,而HttpHandlers是一個終點,用來處理『應用程序級的請求處理』的。HttpModules 實際是HttpApplication的事件處理器。Modules 和 Handlers的載入,附加到調用鏈都做為HttpApplication.Init()方法的一部分、圖6顯示了各種事件以及他們出發的時間和觸發影響的部分。

圖 6 – ASP.NET HTTP管道的事件流程。HttpApplication對象的事件驅動貫穿管道。 Http Modules能夠攔截這些事件,進而重寫或者增強已有的功能。 HttpContext, HttpModules 和 HttpHandlersHttpApplication自身並不知道傳送進來的數據,他僅僅是一個通信對象,通過事件來進行交互。他觸發事件,並且將信息通過HttpContext對象傳遞到被調用的方法中。當前請求的狀態數據存儲在我們前面提到的Httpcontext對象。它提供了所有請求的數據,並且在管道中,伴隨著每一個請求從開始到結束。圖7顯示了通過ASP.NET管道的流程,注意Context對象從開始到請求的結束,都可以用來存儲信息,在一個事件方法中存貯信息,在後面的事件方法中獲得這個數據。一旦管道開始,HttpApplication如圖6一樣,開始一個接著一個的觸發事件。每一個事件處理器被調用,如果事件被調用這些處理器執行他們的任務。這個過程的主要目的是最終調用HttpHandler處理一個請求。Handlers是處理ASP.NET請求的核心的機制,經常位於應用程序級代碼的執行。記住,ASP.NET 頁面和WebService框架,都是作為HTTPHandler的實現,在這裡請求的核心處理過程被執行。Modules往往是更核心的性質,用來準備或者發送處理交付於Handlers的Context。ASP.NET中,典型的默認的Handlers是Authentication, pre-processing的Caching 和 發送處理請求的編碼機制。有很多的信息在HttpHandlers 和 HttpModules中,但是為了保持這篇文章的一個合理的長度,我下面只是簡短介紹一下handlers。HttpModules隨著請求通過管道,一系列的事件在HttpApplication對象中被觸發。我們已經在Global.asax中看到了這些事件。這種方法是應用程序指定的,這樣,就不一定總是你想要的。如果你想創建一個廣義的HttpApplication事件處理,而且能夠可插拔的放到任何一個應用程序中,你可以使用HttpModules,他們是可以復用的,而且,不需要程序代碼指定,只需要在web.config裡面設置一下。Modules本質上像篩選器,就像一個在ASP.NET請求層的ISAPI Filter。Modules允許附加事件到每一個通過ASP.NET HttpApplication對象的請求。這些Module在外部的程序集的類裡面,在web.config裡面設置,並且當應用程序啟動的時候,隨著應用程序的啟動而載入。通過實現指定的介面和方法,將事件添加到HttpApplication事件鏈中。多個HttpModules能夠附加事件處理代碼到相同的事件上,這些附加的事件處理的順序根據在web.config裡面設置的一樣:<configuration><system.web><httpModules><addname="BasicAuthModule"type="HttpHandlers.BasicAuth,WebStore"/></httpModules></system.web></configuration>

注意,你需要指定一個類型全名,還有一個程序集的名字.Modules允許你看到每一個請求並且基於觸發事件形式的執行動作。Module非常適於修改request或者response的內容,用以提供定製的身份驗證或者為每一個請求執行預處理。很多的ASP.NET的特性,像身份驗證以及Sesion引擎都是通過HTTPModule來實現的。雖然,HTTPModule感覺上像ISAPI Filter,他們可以查看每一個通過ASP.NET應用程序的請求,但是他們只是能夠監視映射到ASP.NET應程序或者ASP.NET虛擬目錄的請求。這樣,你可以查看ASPX文件,或者其他的映射到ASP.NET的擴展名。但是你不能監視一個標準的.HTM或者圖片文件,除非你把他們的擴展名顯式的映射到ASP.NET ISAPI dll,就像圖1顯式的。一個Module的經常被用作,過濾指定文件夾下面的圖片,然後顯式一個『SAMPLE』覆蓋在每一個圖片上面,通過使用GDI+。(譯者:水印?)實現一個HTTPModule非常的容易:你必須實現IHttpModule介面,這個介面僅僅有兩個方法,Init()和Dispose().這個時間參數是一個HttpApplication對象,通過他你可以訪問Httpcontext對象,在這個方法中你可以接觸到HttpApplication的事件。舉個例子,如果你想附加AuthenticateRequest事件你可以像Listing5一樣。Listing 5: 一個基礎的HTTP Module非常好實現

CodepublicclassBasicAuthCustomModule:IHttpModule{publicvoidInit(HttpApplicationapplication){//***HookupanyHttpApplicationeventsapplication.AuthenticateRequest+=newEventHandler(this.OnAuthenticateRequest);}publicvoidDispose(){}publicvoidOnAuthenticateRequest(objectsource,EventArgseventArgs){HttpApplicationapp=(HttpApplication)source;HttpContextContext=HttpContext.Current;…dowhatyouhavetodo…}}

記住,你的Module訪問的是HttpContext對象,通過其就能獲得其他的管道對象,就如Response和Request,這樣你可以獲得輸入等等,但是記住,這些不一定在後面的事件鏈中還有效。 你可以附件多個事件在Init()方法中,這樣你就可以通過一個Module來管理不同的功能操作。然而,最好將不同的邏輯放在不同的類中使得Module真正的模塊化。在你要實現的很多功能中,你需要附加到多個事件-舉個例子,一個日誌filter需要早BeginRequest中記錄Request的開始時間,而在EndRequest中記錄請求的完成時間。注意HttpModules 和 HttpApplication 的事件events: Response.End() 或者 HttpApplication.CompleteRequest()將會切斷HttpApplication對象或者Module的事件鏈。See the sidebar 「Watch out for Response.End() 「 for more info.HttpHandlersModules相當的底層而且,對應的是對應ASP.NET應用程序的每一個請求。HTTP Handler則更加側重於對於一個指定的請求的操作,通常一個頁面都被映射到Handler.實現Http Handler要求非常基礎,但是通過訪問HttpContext對象可以獲得強大的功能。Http Handler通過實現一個非常簡單的介面IHttpHandler來實現(或者其非同步的版本的IHttpAsyncHandler),它僅僅包含了一的方法ProcessRequest()和一個IsReusagable屬性。關鍵的是,ProcessRequest()獲得了一個HttpContext對象的實例,這個方法負責處理Web請求,從開始到結束。一個簡單的方法?非常的簡單,對么?確實,一個簡單的介面,但是卻不是那麼的簡單!記得WebForm和WebService都是做為HTTPHandler的實現的,所以,很強大的功能封裝在這一個看似簡單的介面中。關鍵點是,接觸到HTTP Handler的時候,ASP.NET的內置對象已經為開始處理請求而創建和設置好了。關鍵就是HttpContext對象,他提供了所有的請求相關的功能獲得輸入信息,輸出信息到Web伺服器。 一個HTTPHandler所有的動作發生都是通過調用這個單獨的ProcessRequest().這個可以簡單的像:

publicvoidProcessRequest(HttpContextcontext){context.Response.Write("HelloWorld");}

也可以完全實現一個象WebForm Page引擎,可以輸出複雜格式HTML模板。這點完全取決與你的決定,你到底如何用這個簡單,卻有強大的介面!因為你可以使用Context對象,你可以獲得Request,Response,Session和Cache對象,這樣你有了所有的ASP.NET請求的特性, 你可以找到用戶提交的內容,也可以設置返回客戶端的內容。記住Context對象,他是你的朋友,在這個ASP.NET請求的生命周期中!Handler的關鍵性的操作應帶是最終的把output輸出結果到Response對象,或者更具體的說是Response對象的OutputStream。這個output返回客戶端的信息。在背後,ISAPIWorkerRequest負責將OutputStream返回到ISAPI ecb.WriteClient方法,執行了IIS輸出的過程。

圖 7 – ASP.NET請求管道流程通過一系列事件介面,提供了很大的靈活性。Application扮演了一個宿主容器的角色,它載入了Web應用程序,並且隨著請求的進入和在管道中的傳遞而觸發事件。每一個請求都是通過相同的路徑,通過HTTP Filters和設置了的Modules。Filters能夠檢測通過管道的每一個請求,而Handlers允許實現用用程序的邏輯和介面,就像WebForm和WebService。為了提供應用程序的輸入和輸出Context在整個過程提供了請求的相關信息。WebForm在這個基礎框架上面,通過實現一個HTTPHandler以及更高層的介面,然而最終,Wenforms的Render()方法簡單的使用一個HtmlTextWriter對象將其最終的輸出結果寫入到context.Response.OutputStream。這樣,最終,一個高層的工具,像WebForms僅僅是一個Request和Response對象的高層的抽象。你可能想知道,這點上,你是否需要處理HTTPHandler。畢竟,WebForm提供了簡單的可訪問的HTTPHandler實現,那麼我們為什麼要放棄這個靈活性而不厭其煩的做一些底層的事情呢?WebForm對於生成複雜的HTML頁面和需要圖形布局工具,模板化頁面的商業邏輯非常的好。但是,WebForm執行了很多的增加消耗的任務。如果你僅僅想在系統中讀取一個文件,並將其返回,那麼你可以跳過Web Form page框架,直接處理文件。如果你做的事情像從資料庫提供圖片,你也不需要Page框架—你不需要模板,而且沒有一個UI。沒有理由創建一個頁面對象和Seesion並且處理頁面級別的事件.所以handlers更加高效。Handler也可以完成WebForm不能完成的任務。例如,他能夠處理一個請求,不需要磁碟上有物理文件。 做這個,你需要在圖1中的應用程序擴展對話框中。關閉「檢查文件是否存在」選項。對於內容提供者是通用的,就像動態圖片處理,XML伺服器,Url重定向提供構造的Url,下載管理等等,這些都不是適合Wenform引擎。對你來說,我介紹的足夠了么?恩,我們這裡已經介紹了處理整個請求的過程。有很多的底層信息,我沒有仔細的講HTTPHandler和HTTPModule具體工作細節。挖掘這些信息需要一些時間,在理解ASP.NET怎樣工作上面,希望能給你和我自己一樣的滿意程度。 在結束之前,讓我們簡短的回顧一下從IIS到handler的事件序列:?IIS獲得請求 ?檢測腳本映射,映射到ASP.NET_isapi.dll ?觸發工作進程(ASP.NET_wp.exe 在 IIS5 或者 w3wp.exe 在 IIS6) ?.NET運行時載入 ?IsapiRuntime.ProcessRequest()通過非託管代碼調用 ?IsapiWorkerRequest created once per request ?IsapiWorkerRequest 每一次請求創建一次 ?HttpRuntime.ProcessRequest() called with Worker Request ?通過傳進Work Request, HttpContext對象被創建 ?HttpApplication.GetApplicationInstance() called with Context to retrieve instance from pool ?HttpApplication.Init() 調用,並且啟動管道事件序列,附加Modules和Handler ?被調用,開始處理進請求 ?管道事件觸發 ?Handlers被調用,並且ProcessRequest 方法執行 ?控制項返回管道並且發送請求事件觸發 通過這個簡單的列表,把這些是如何組合起來的記住會更容易。我不時的來看它來記憶。現在,我們回到工作上,繼續做一些不抽象的… 儘管,這裡我說的是基於ASP.NET1.1,但是ASP.NET2.0中,並沒有改變這些底層的處理過程。非常感謝微軟的Mike Volodarsky來審閱這篇文章,並且提了一些附件的提示並且 Michele Leroux Bustamante提供了ASP.NET管道請求基礎信息。
推薦閱讀:

查看原文 >>
相关文章