了解asp.net底層架構
 
進入底層
這篇文章以非常底層的視角講述了web請求(request)在asp.net框架中是如何流轉的,從web服務器,通過isapi直到請求處理器(handler)和你的代碼.看看在幕后都發生了些什么,不要再把asp.net看成一個黑盒了.
asp.net是一個非常強大的構建web應用的平臺,它提供了極大的靈活性和能力以致于可以用它來構建所有類型的web應用.絕大多數的人只熟悉高層的框架如webforms和webservices-這些都在asp.net層次結構在最高層.在這篇文章中我將會討論asp.net的底層機制并解釋請求(request)是怎么從web服務器傳送到asp.net運行時然后如何通過asp.net管道來處理請求.
對我而言了解平臺的內幕通常會帶來滿足感和舒適感,深入了解也能幫助我寫出更好的應用.知道可以使用哪些工具以及他們是怎樣作為整個復雜框架的一部分來互相配合的可以更容易地找出最好的解決方案,更重要的是可以在出現問題時更好的解決它們.這篇文章的目標是從系統級別了解asp.net并幫助理解請求(request)是如何在asp.net的處理管道中流轉的.同樣,我們會了解核心引擎和web請求如何在那里結束.這些信息大部分并不是你在日常工作時必須了解的,但是它對于理解asp.net架構如何把請求路由到你的代碼(通常是非常高層的)中是非常有益的.
不管怎么樣,asp.net從更低的層次上提供了更多的靈活性.http運行時和請求管道在構建webforms和webservices上提供了同樣的能力-它們事實上都是建立在.net托管代碼上的.而且所有這些同樣的功能對你也是可用的,你可用決定你是否需要建立一個比webforms稍低一點層次的定制的平臺.
webforms顯然是最簡單的構建絕大多數web接口的方法,不過如果你是在建立自定義的內容處理器(handler),或者有在處理輸入輸出內容上有特殊的要求,或者你需要為另外的應用建立一個定制的應用程序服務接口,使用這些更低級的處理器(handler)或者模塊(module)能提供更好的性能并能對實際請求處理提供更多的控制.在webforms和webservices這些高層實現提供它們那些能力的同時,它們也對請求增加了一些額外負擔,這些都是在更底層可以避免的.
asp.net是什么
讓我們以一個簡單的定義開始:什么是asp.net?我喜歡這樣定義asp.net:
asp.net是一個復雜的使用托管代碼來從頭到尾處理web請求的引擎.
它并不只是webforms和webservies…
asp.net是一個請求處理引擎.它接收一個發送過來的請求,把它傳給內部的管道直到終點,作為一個開發人員的你可以在這里附加一些代碼來處理請求.這個引擎是和http/web服務器完全分隔的.事實上,http運行時是一個組件,使你可以擺脫iis或者任何其他的服務器程序,將你自己的程序寄宿在內.例如,你可以將asp.net運行時寄宿在一個windows form程序中(查看http://www.west-wind.com/presentations/aspnetruntime/aspnetruntime.asp可以得到更加詳細的信息)
運行時提供了一個復雜但同時非常優雅的在管道中路由請求的機制.其中有很多相關的對象,大多數都是可擴展的(通過繼承或者事件接口),在幾乎所有的處理流程上都是如此.所以這個框架具有高度可擴展性.通過這個機制,掛接到非常底層的接口(比如緩存,認證和授權)都變得可能了.你甚至可以在預處理或者處理后過濾內容,也可以簡單的將符合特殊標記的請求直接路由你的代碼或者另一個url上.存在著許多不同的方法來完成同一件事,但是所有這些方法都是可以簡單直接地實現的,同時還提供了靈活性,可以得到最好的性能和開發的簡單性.
整個asp.net引擎是完全建立在托管代碼上的,所有的擴展功能也是通過托管代碼擴展來提供的
整個asp.net引擎是完全建立在托管代碼上的,所有的擴展功能也是通過托管代碼擴展來提供的.這是對.net框架具有構建復雜而且高效的框架的能力的最好的證明.asp.net最令人印象深刻的地方是深思熟慮的設計,使得框架非常的容易使用,又能提供掛接到請求處理的幾乎所有部分的能力.
通過asp.net你可以從事從前屬于isapi擴展和iis過濾器領域的任務-有一些限制,但是比起asp來說是好多了.isapi是一個底層的win32風格的api,有著非常粗劣的接口而且難以用來開發復雜的程序.因為isapi非常底層,所以它非常的快,但是對于應用級的開發者來說是十分難以管理的.所以,isapi通常用來提供橋接的接口,來對其他應用或者平臺進行轉交.但是這并不意味者isapi將消亡.事實上,asp.net在微軟的平臺上就是通過isapi擴展來和iis進行交互的,這個擴展寄宿著.net運行時和asp.net運行時.isapi提供了核心的接口,asp.net使用非托管的isapi代碼通過這個接口來從web服務器獲取請求,并發送響應回客戶端.isapi提供的內容可以通過通用對象(例如httprequest和httpresponse)來獲取,這些對象通過一個定義良好并有很好訪問性的接口來暴露非托管數據.
從瀏覽器到asp.net
讓我們從一個典型的asp.net web請求的生命周期的起點開始.當用戶輸入一個url,點擊了一個超鏈接或者提交了一個html表單(form)(一個post請求,相對于前兩者在一般意義上都是get請求).或者一個客戶端程序可能調用了一個基于asp.net的webservice(同樣由asp.net來處理).在web服務器端,iis5或6,獲得這個請求.在最底層,asp.net和iis通過isapi擴展進行交互.在asp.net環境中這個請求通常被路由到一個擴展名為.aspx的頁面上,但是這個流程是怎么工作的完全依賴于處理特定擴展名的http handler是怎么實現的.在iis中.aspx通過’應用程序擴展’(又稱為腳本映射)被映射到asp.net的isapi擴展dll-aspnet_isapi.dll.每一個請求都需要通過一個被注冊到aspnet_isapi.dll的擴展名來觸發asp.net(來處理這個請求).
依賴于擴展名asp.net將請求路由到一個合適的處理器(handler)上,這個處理器負責獲取這個請求.例如,webservice的.asmx擴展名不會將請求路由到磁盤上的一個頁面,而是一個由特殊屬性(attribute)標記為webservice的類上.許多其他處理器和asp.net一起被安裝,當然你也可以自定義處理器.所有這些httphandler在iis中被配置為指向asp.net isapi擴展,并在web.config(譯著:asp.net中自帶的handler是在machine.config中配置的,當然可以在web.config中覆蓋配置)被配置來將請求路由到指定的http handler上.每個handler都是一個處理特殊擴展的.net類,可以從一個簡單的只包含幾行代碼的hello world類,到非常復雜的handler如asp.net的頁面或者webservice的handler.當前,只要了解asp.net的映射機制是使用擴展名來從isapi接收請求并將其路由到處理這個請求的handler上就可以了.
對在iis中自定義web請求處理來說,isapi是第一個也是最高效的入口
isapi連接
isapi是底層的非托管win32 api.isapi定義的接口非常簡單并且是為性能做了優化的.它們是非常底層的-處理指針和函數指針表來進行回調-但是它們提供了最底層和面向效率的接口,使開發者和工具提供商可以用它來掛接到iis上.因為isapi非常底層所以它并不適合來開發應用級的代碼,而且isapi傾向于主要被用于橋接接口,向上層工具提供應用服務器類型的功能.例如,asp和asp.net都是建立在isapi上的,cold fusion,運行在iis上的多數perl,php以及jsp實現,很多第三方解決方案(如我的wisual foxpro的web連接框架)都是如此.isapi是一個杰出的工具,可以為上層應用提供高效的管道接口,這樣上層應用可以抽象出isapi提供的信息.在asp和asp.net中,將isapi接口提供的信息抽象成了類型request和response這樣的對象,通過它們來讀取isapi請求中對應的信息.將isapi想像成管道.對asp.net來說,isapi dll是非常的”瘦”的,只是作為一個路由機制來將原始的請求轉發到asp.net運行時.所有那些沉重的負擔和處理,甚至請求線程的管理都發生在asp.net引擎內部和你的代碼中.
作為協議,isapi同時支持isapi擴展和isapi過濾器(filter).擴展是一個請求處理接口,提供了處理web服務器的輸入輸出的邏輯-它本質上是一個處理(事物?)接口.asp和asp.net都被實現為isapi擴展.isapi過濾器是掛接接口,提供了查看進入iis的每一個請求的能力,并能修改請求的內容或者改變功能型的行為,例如認證等.順便提一下,asp.net通過了兩種概念映射了類似isapi的功能:http handler類似擴展,http module類似過濾器.我們將在后面詳細討論它們.
isapi是開始一個asp.net請求的最初的入口.asp.net映射了好幾個擴展名到它的isapi擴展,此擴展位于.net框架的目錄下:
<.net frameworkdir>/aspnet_isapi.dll
你可以在iis服務管理界面上看到這些映射,如圖1.查看網站根目錄的屬性中的主目錄配置頁,然后查看配置|映射.
圖1:iis映射了多種擴展名如.aspx到asp.net的isapi擴展.通過這個機制請求會在web服務器這一層被路由到asp.net的處理管道.
由于.net需要它們中的一部分,你不應該設置手動這些擴展名.使用aspnet_regiis.exe這個工具來確保所有的映射都被正確的設置了:
cd <.netframeworkdirectory>
aspnet_regiis – i
這個命令將為整個web站點注冊特定版本的asp.net運行時,包括腳本 (擴展名) 映射和客戶端腳本庫(包括進行控件驗證的代碼等).注意它注冊的是<.netframeworkdirectory>中安裝的特定版本的clr(如1.1,2.0).aspnet_regiis的選項令您可以對不同的虛擬目錄進行配置.每個版本的.net框架都有自己不同版本的aspnet_regiis工具,你需要運行對應版本的aspnet_regiis來為web站點或者虛擬目錄來配置指定版本的.net框架.從asp.net2.0開始提供了asp.net配置頁面,可以通過這個頁面在iis管理控制臺來交互的配置.net版本.
iis6通配符應用程序映射
如果你有一個asp.net應用程序需要處理虛擬目錄的(或者是整個web站點,如果配置為根目錄的話)每一個請求,iis6引入了新的稱為通配符應用程序映射的概念.一個映射到通配符的isapi擴展在每個請求到來時都會被觸發,而不管擴增名是什么.這意味著每個頁面都會通過這個擴展來處理.這是一個強大的功能,你可以用這個機制來創建虛擬url和不使用文件名的unix風格的url.然而,使用這個設置的時候要注意,因為它會把所有的東西都傳給你的應用,包括靜態htm文件,圖片,樣式表等等.
iis 5 和6以不同的方式工作
當一個請求來到時,iis檢查腳本映射(擴展名映射)然后把請求路由到aspnet_isapi.dll.這個dll的操作和請求如何進入asp.net運行時在iis5和6中是不同的.圖2顯示了這個流程的一個粗略概覽.
在iis5中,aspnet_isapi.dll直接寄宿在inetinfo.exe進程中,如果你設置了web站點或虛擬目錄的隔離度為中或高,則會寄宿在iis單獨的(被隔離的)工作進程中.當第一個asp.net請求來到,dll(aspnet_isapi.dll)會開始另一個新進程aspnet_wp.exe并將請求路由到這個進程中來進行處理.這個進程依次加載并寄宿.net運行時.每個轉發到isapi dll的請求都會通過命名管道調用被路由到這個進程來.
圖2-從較高層次來看請求從iis到asp.net運行時,并通過請求處理管道的流程.iis5和iis6通過不同的方式與asp.net交互,但是一旦請求來到asp.net管道,整個處理流程就是一樣的了.
不同于以前版本的服務器,iis6為asp.net做了全面的優化
iis6-應用程序池萬歲
iis6對處理模型做了意義重大的改變,iis不再直接寄宿象isapi擴展這樣的外部可執行代碼.iis總是創建一個獨立的工作線程-一個應用程序池-所有的處理都發生在這個進程中,包括isapi dll的執行.應用程序池是iis6的一個很大的改進,因為它允許對指定線程中將會執行什么代碼進行非常細粒度的控制.應用程序池可以在每個虛擬路徑上或者整個web站點上進行配置,這樣你可以將每個web應用隔離到它們自己的進程中,這樣每個應用都將和其他運行在同一臺機器上的web應用完全隔離.如果一個進程崩潰了,不會影響到其他進程(至少在web處理的觀點上來看是如此).
不止如此,應用程序池還是高度可配置的.你可以通過設置池的執行扮演級別(execution impersonation level )來配置它們的運行安全環境,這使你可以定制賦予一個web應用的權限(同樣,粒度非常的細).對于asp.net的一個大的改進是,應用程序池覆蓋了在machine.config文件中大部分的processmodel節的設置.這一節的設置在iis5中非常的難以管理,因為這些設置是全局的而且不能在應用程序的web.config文件中被覆蓋.當運行iis6是,processmodel相關的設置大部分都被忽略了,取而代之的是從應用程序池中讀取.注意這里說的是大部分-有些設置,如線程池的大小還有io線程的設置還是從machine.config中讀取,因為它們在線程池的設置中沒有對應項.
因為應用程序池是外部的可執行程序,這些可執行程序可以很容易的被監控和管理.iis6提供了一系列的進行系統狀況檢查,重啟和超時的選項,可以很方便的用來檢查甚至在許多情況下可以修正程序的問題.最后iis6的應用程序池并不像iis5的隔離模式那樣依賴于com+,這樣做一來可以提高性能,二來提高了穩定性(特別對某些內部需要調用com組件的應用來說)
盡管iis6的應用程序池是單獨的exe,但是它們對http操作進行了高度的優化,它們直接和內核模式下的http.sys驅動程序進行通訊.收到的請求被直接路由給適當的應用程序池.inetinfo基本上只是一個管理程序和一個配置服務程序-大部分的交互實際上是直接在http.sys和應用程序池之間發生,所有這些使iis6成為了比iis5更加的穩定和高效的環境.特別對靜態內容和asp.net程序來說這是千真萬確的.
一個iis6應用程序池對于asp.net有著天生的認識,asp.net可以在底層的api上和它進行交互,這允許直接訪問http緩存api,這樣做可以將asp.net級別的緩存直接下發到web服務器.
在iis6中,isapi擴展在應用程序池的工作進程中運行. .net運行時也在同一個進程中運行,所以isapi擴展和.net運行時的通訊是發生在進程內的,這樣做相比iis5使用的命名管道有著天生的性能優勢.雖然iis的寄宿模型有著非常大的區別,進入托管代碼的接口卻異常的相似-只有路由消息的過程有一點區別.
isapiruntime.processrequest()函數是進入asp.net的第一站
進入.net運行時
進入.net運行時的真正的入口發生在一些沒有被文檔記載的類和接口中(譯著:當然,你可以用reflector來查看j).除了微軟,很少人知道這些接口,微軟的家伙們也并不熱衷于談論這些細節,他們認為這些實現細節對于使用asp.net開發應用的開發人員并沒有什么用處.
工作進程(iis5中是aspnet_wp.exe,iis6中是w3wp.exe)寄宿.net運行時和isapi dll,它(工作進程)通過調用com對象的一個小的非托管接口最終將調用發送到isapiruntime類的一個實例上(譯注:原文為an instance subclass of the isapiruntime class,但是isapiruntime類是一個sealed類,疑為作者筆誤,或者這里的subclass并不是子類的意思).進入運行時的第一個入口就是這個沒有被文檔記載的類,這個類實現了iisapiruntime接口(對于調用者說明來說,這個接口是一個com接口)這個基于iunknown的底層com接口是從isapi擴展到asp.net的一個預定的接口.圖3展示了iisapiruntime接口和它的調用簽名.(使用了lutz roeder出色的.net reflector 工具http://www.aisto.com/roeder/dotnet/).這是一個探索這個步步為營過程的很好的方法.
圖3-如果你想深入這個接口,打開reflector,指向system.web.hosting命名空間. isapi dll通過調用一個托管的com接口來打開進入asp.net的入口,asp.net接收一個指向isapi ecb的非托管指針.這個ecb包含訪問完整的isapi接口的能力,用來接收請求和發送響應回到iis.
iisapiruntime接口作為從isapi擴展來的非托管代碼和asp.net之間的接口點(iis6中直接相接,iis5中通過命名管道).如果你看一下這個類的內部,你會找到含有以下簽名的processrequest函數:
 [return: marshalas(unmanagedtype.i4)]
int processrequest([in] intptr ecb, 
                   [in, marshalas(unmanagedtype.i4)] int useprocessmodel);
其中的ecb參數就是isapi擴展控制塊(extention control block),被當作一個非托管資源傳遞給processrequest函數.這個函數接過ecb后就把它做為基本的輸入輸出接口,和request和response對象一起使用.isapi ecb包含有所有底層的請求信息,如服務器變量,用于表單(form)變量的輸入流和用于回寫數據到客戶端的輸出流.這一個ecb引用基本上提供了用來訪問isapi請求所能訪問的資源的全部功能,processrequest是這個資源(ecb)最初接觸到托管代碼的入口和出口.
isapi擴展異步地處理請求.在這個模式下isapi擴展馬上將調用返回到工作進程或者iis線程上,但是在當前請求的生命周期上ecb會保持可用.ecb含有使isapi知道請求已經被處理完的機制(通過ecb.serversupportfunction方法)(譯注:更多信息,可以參考開發isapi擴展的文章),這使得ecb被釋放.這個異步的處理方法可以馬上釋放isapi工作線程,并將處理傳遞到由asp.net管理的一個單獨的線程上.
asp.net接收到ecb引用并在內部使用它來接收當前請求的信息,如服務器變量,post的數據,同樣它也返回信息給服務器.ecb在請求完成前或超時時間到之前都保持可訪問(stay alive),這樣asp.net就可以繼續和它通訊直到請求處理完成.輸出被寫入isapi輸出流(使用ecb.writeclient())然后請求就完成了,isapi擴展得到請求處理完成的通知并釋放ecb.這個實現是非常高效的,因為.net類本質上只是對高效的、非托管的isapi ecb的一個非常”瘦”(thin)的包裝器.
裝載.net-有點神秘
讓我們從這兒往回退一步:我跳過了.net運行時是怎么被載入的.這是事情變得有一點模糊的地方.我沒有在這個過程中找到任何的文檔,而且因為我們在討論本機代碼,沒有很好的辦法來反編譯isapi dll并找出它(裝載.net運行時的代碼)來.
我能作出的最好的猜測是當isapi擴展接受到第一個映射到asp.net的擴展名的請求時,工作進程裝載了.net運行時.一旦運行時存在,非托管代碼就可以為指定的虛擬目錄請求一個isapiruntime的實例(如果這個實例還不存在的話).每個虛擬目錄擁有它自己的應用程序域(appdomain),當一個獨立的應用(指一個asp.net程序)開始的時候isapiruntime從啟動過程就一直在應用程序域中存在.實例化(譯注:應該是指isapiruntime的實例化)似乎是通過com來進行的,因為接口方法都被暴露為com可調用的方法.
當第一個針對某虛擬目錄的請求到來時,system.web.hosting.appdomainfactory.create()函數被調用來創建一個isapiruntime的實例.這就開始了這個應用的啟動進程.這個調用接收這個應用的類型,模塊名稱和虛擬目錄信息,這些信息被asp.net用來創建應用程序域并啟動此虛擬目錄的asp.net程序.這個httpruntime實例(譯注:原文為this httpruntime derived object,但httpruntime是一個sealed類,疑為原文錯誤)在一個新的應用程序域中被創建.每個虛擬目錄(即一個asp.net應用程序寄)宿在一個獨立的應用程序域中,而且他們也只有在特定的asp.net程序被請求到的時候才會被載入.isapi擴展管理這些httpruntime對象的實例,并根據請求的虛擬目錄將內部的請求路由到正確的那個httpruntime對象上.
圖4-isapi請求使用一些沒有文檔記載的類,接口并調用許多工廠方法傳送到asp.net的http管道的過程.每個web程序/虛擬目錄在它自己的應用程序域中運行,調用者(譯注:指isapi dll)保持一個iisapiruntime接口的引用來觸發asp.net的請求處理.
回到運行時
在這里我們有一個在isapi擴展中活動的,可調用的isapiruntime對象的實例.每次運行時是啟動的并運行著的時候(譯注:相對的,如果運行時并沒有啟動,就需要象上一章所說的那樣載入運行時),isapi的代碼調用isapiruntime.processrequest()方法,這個方法是真正的進入asp.net管道的入口.這個流程在圖4中顯示.
記住isapi是多線程的,所以請求也會通過appdomainfactory.create()(譯注:原文為applicationdomainfactory,疑有誤)函數中返回的引用在多線程環境中被處理.列表1顯示了isapiruntime.processrequest()方法中反編譯后的代碼,這個方法接收一個isapi ecb對象和服務類型(workerrequesttype)作為參數.這個方法是線程安全的,所以多個isapi線程可以同時在這一個被返回的對象實例上安全的調用這個方法.
列表1:processrequest方法接收一個isapi ecb并將其傳給工作線程
public int processrequest(intptr ecb, int iwrtype)
{
httpworkerrequest request1 = isapiworkerrequest.createworkerrequest(ecb, iwrtype);
string text1 = request1.getapppathtranslated();
string text2 = httpruntime.appdomainapppathinternal;
if (((text2 == null) || text1.equals(".")) ||
(string.compare(text1, text2, true, cultureinfo.invariantculture) == 0))
{
httpruntime.processrequest(request1);
return 0;
}
httpruntime.shutdownappdomain("physical application path changed from " +
text2 + " to " + text1);
return 1;
}
這里實際的代碼并不重要,記住這是從內部框架代碼中反編譯出來的,你不能直接處理它,它也有可能在將來發生改變.它只是用來揭示在幕后發生了什么.processrequest方法接收非托管的ecb引用并將它傳送給isapiworkerrequest對象,此對象負責為當前請求創建創建請求上下文.在列表2中顯示了這個過程.
system.web.hosting.isapiworkerrequest類是httpworkerrequest類的一個抽象子類(譯注:httpworkerrequest和isapiworkerrequest都是抽象類,并且isapiworkerrequest繼承自httpworkerrequest),它的工作是構建一個作為web應用輸入的輸入輸出的抽象視角.注意這里有另一個工廠方法:createworkerrequest,通過判斷接受到的第二個參數來創建對應的workerrequest對象.有三個不同的版本:isapiworkerrequestinproc,isapiworkerrequestinprocforiis6,isapiworkerrequestoutofproc.每次有請求進入,這個對象被創建并作為請求和響應對象的基礎,它會接收它們的數據和由workerrequest提供的數據流.
抽象的httpworkerrequest類在低層接口上提供一個高層的抽象,這樣就封裝了數據是從哪里來的,可以是一個cgi web服務器,web瀏覽器控件或者是一些你用來給http運行時”喂”數據的自定義的機制.關鍵是asp.net能用統一的方法來接收信息.
在使用iis的情況下,這個抽象是建立在isapi ecb塊周圍.在我們的請求處理過程中,isapiworkerrequest掛起isapi ecb并根據需要從它那里取出信息.列表2顯示了請求字符串值(query string value)是如何被取出來的.
列表2:使用非托管數據的isapiworkerrequest方法
// *** implemented in isapiworkerrequest
public override byte[] getquerystringrawbytes()
{
byte[] buffer1 = new byte[this._querystringlength];
if (this._querystringlength > 0)
{
int num1 = this.getquerystringrawbytescore(buffer1, this._querystringlength);
if (num1 != 1)
{
throw new httpexception( "cannot_get_query_string_bytes");
}
}
return buffer1;
}
// *** implemented in a specific implementation class isapiworkerrequestinprociis6
internal override int getquerystringcore(int encode, stringbuilder buffer, int size)
{
if (this._ecb == intptr.zero)
{
return 0;
}
return unsafenativemethods.ecbgetquerystring(this._ecb, encode, buffer, size);
}
isapiworkerrequest實現了一個高層次的包裝方法,它調用了低層的核心方法,負責真正的訪問非托管apis-或稱為”服務級別的實現”(service level implementation).這些核心方法在特殊的isapiworkerrequest子類中為它寄宿的環境提供特殊的實現.這實現了簡單的擴展的(pluggable)環境,這樣一來當以后新的web服務器接口或其他平臺成為了asp.net的目標時附加的實現類可以在被簡單的提供出來.這里還有一個協助類(helper class)system.web.unsafenativemethods.里面許多對isapi ecb結構的操作實現了對isapi擴展的非托管操作.
httpruntime,httpcontext和httpapplication
當一個請求到來時,它被路由到isapiruntime.processrequest()方法.這個方法調用httpruntime.processrequest方法,它作一些重要的事情(用reflector查看system.web.httpruntime.processrequestinternal方法):
· 為請求創建一個新的httpcontext實例
· 獲取一個httpapplication實例
· 調用httpapplication.init()方法來設置管道的事件
· init()方法觸發開始asp.net管道處理的httpapplication.resumeprocessing()方法
首先一個新的httpcontext對象被創建并用來傳遞isapiworkerrequest(isapi ecb的包裝器).這個上下文在整個請求的生命周期總都是可用的并總可以通過靜態屬性httpcontext.currect來訪問.正像名字所暗示的那樣,httpcontext對象代表了當前活動請求的上下文因為他包含了在請求生命周期中所有典型的你需要訪問的重要對象:request,response,application,server,cache.在請求處理的任何時候httpcontext.current給你訪問所有這些的能力.
httpcontext對象也包含一個非常有用的items集合,你可以用它來保存針對特定請求的數據.上下文對象在請求周期的開始時被創建,在請求結束時被釋放,所有在items集合中保存的數據只在這個特定的請求中可用.一個很好的使用的例子是請求日志機制,當你通過想通過在global.asax中掛接application_beginrequest和application_endrequest方法記錄請求的開始和結束時間(象在列表3中顯示的那樣).httpcontext對你就非常有用了-如果你在請求或頁面處理的不同部分需要數據,你自由的使用它.
列表3-使用httpcontext.items集合使你在不同的管道事件中保存數據
protected void application_beginrequest(object sender, eventargs e)
{
//*** request logging
if (app.configuration.logwebrequests)
context.items.add("weblog_starttime",datetime.now);
}
protected void application_endrequest(object sender, eventargs e)
{
// *** request logging
if (app.configuration.logwebrequests)
{
try
{
timespan span = datetime.now.subtract(
(datetime) context.items["weblog_starttime"] );
int milisecs = span.totalmilliseconds;
// do your logging
webrequestlog.log(app.configuration.connectionstring,
true,millisecs);
}
}
一旦上下文被設置好,asp.net需要通過httpapplication對象將收到的請求路由到適合的應用程序/虛擬目錄.每個asp.net應用程序必須被設置到一個虛擬目錄(或者web根目錄)而且每個”應用程序”是被單獨的處理的.
httpapplication類似儀式的主人-它是處理動作開始的地方
asp.net2.0中的變化
asp.net2.0并沒有對底層架構做很多改變.主要的新特性是httpapplication對象有了一系列新的事件-大部分是預處理和后處理事件鉤子-這使得應用程序事件管道變得更加的顆粒狀了.asp.net2.0也支持新的isapi功能- hse_req_exec_url-這允許在asp.net處理的內部重定向到另外的url上.這使得asp.net可以在iis中設置一個通配符擴展,并處理所有的請求,其中一部分被http處理器(handler)處理,另一部分被新的defaulthttphandler對象處理. defaulthttphandler會在內部調用isapi來定位到原始的url上.這允許asp.net可以在其他的頁面,如asp,被調用前處理認證和登錄等事情.
域的主人:httpapplication
每個請求都被路由到一個httpapplication對象上.httpapplicationfactory類根據應用程序的負載為你的asp.net應用創建一個httpapplication對象池并為每個請求分發httpapplication對象的引用.對象池的大小受machine.config文件中processmodel鍵中的maxworkerthreads設置限制,默認是20個(譯注:此處可能有誤,根據reflector反編譯的代碼,池的大小應該是100個,如果池大小小于100,httpapplicationfactory會創建滿100個,但是考慮到會有多個線程同時創建httpapplication的情況,實際情況下有可能會超過100個).
對象池以一個更小的數字開始;通常是一個然后增長到和同時發生的需要被處理的請求數量一樣.對象池被監視,這樣在大負載下它可能會增加到最大的實例數量,當負載降低時會變回一個更小的數字.
httpapplication是你的web程序的外部包裝器,而且它被映射到在global.asax里面定義的類上.它是進入httpruntime的第一個入口點.如果你查看global.asax(或者對應的代碼類)你會發現這個類直接繼承自httpapplication:
public class global : system.web.httpapplication
httpapplication的主要職責是作為http管道的事件控制器,所以它的接口主要包含的是事件.事件掛接是非常廣泛的,包括以下這些:
l beginrequest
l authenticaterequest
l authorizerequest
l resolverequestcache
l aquirerequeststate
l prerequesthandlerexecute
l …handler execution…
l postrequesthandlerexecute
l releaserequeststate
l updaterequestcache
l endrequest
每個事件在global.assx文件中以application_前綴開頭的空事件作為實現.例如, application_beginrequest(), application_authorizerequest()..這些處理器為了便于使用而提供因為它們是在程序中經常被使用的,這樣你就不用顯式的創建這些事件處理委托了.
理解每個asp.net虛擬目錄在它自己的應用程序域中運行,而且在應用程序域中有多個從asp.net管理的池中返回的httpapplication實例同時運行,是非常重要的.這是多個請求可以被同時處理而不互相妨礙的原因.
查看列表4來獲得應用程序域,線程和httpapplication之間的關系.
列表4-顯示應用程序域,線程和httpapplication實例之間的關系
private void page_load(object sender, system.eventargs e)
{
// put user code to initialize the page here
this.applicationid = ((howaspnetworks.global)
httpcontext.current.applicationinstance).applicationid ;
this.threadid = appdomain.getcurrentthreadid();
this.domainid = appdomain.currentdomain.friendlyname;
this.threadinfo = "threadpool thread: " +
system.threading.thread.currentthread.isthreadpoolthread.tostring() +
"<br>thread apartment: " +
system.threading.thread.currentthread.apartmentstate.tostring();
// *** simulate a slow request so we can see multiple
// requests side by side.
system.threading.thread.sleep(3000);
}
這是隨sample提供的demo的一部分,運行的結果在圖5中顯示.運行兩個瀏覽器,打開這個演示頁面可以看到不同的id.
圖5-你可以通過同時運行多個瀏覽器來簡單的查看應用程序域,應用程序池實例和請求線程是如何交互的.當多個請求同時發起,你可以看到線程id和應用程序id變化了,但是應用程序域還是同一個.
你可能注意到在大多數請求上,當線程和httpapplication id變化時應用程序域id卻保持不變,雖然它們也可能重復(指線程和httpapplication id).httpapplication是從一個集合中取出,在隨后到來的請求中可以被復用的,所以它的id有時是會重復的.注意application實例并不和特定的線程綁定-確切的說它們是被指定給當前請求的活動線程.
線程是由.net的線程池管理的,默認是多線程套間(mta)線程.你可以在asp.net的頁面上通過指定@page指令的屬性aspcompat=”true”來覆蓋套間屬性.aspcompat意味著為com組件提供一個安全的執行環境,指定了這個屬性,就會為這些請求使用特殊的單線程套間(sta).sta線程被存放在單獨的線程池中,因為它們需要特殊的處理.
這些httpapplication對象全部在同一個應用程序域中運行的事實是非常重要的.這是為什么asp.net可以保證對web.config文件或者單獨的asp.net頁面的修改可以在整個應用程序域中生效.改變web.config中的一個值導致應用程序域被關閉并重啟.這可以保證所有的httpapplication可以”看到”這個修改,因為當應用程序域重載入的時候,所做的修改(譯注:即被修改的文件)會在啟動的時候被重新讀入.所有的靜態引用也會被重載,所以如果程序通過app configuration settings讀取值,這些值也會被刷新.
為了在sample中看到這點,點擊applicationpoolsandthreads.aspx頁面并記下應用程序域id.然后打開并修改web.config(加入一個空格并保存).然后重新載入頁面.你會發現一個新的應用程序域已經被創建了.
本質上當上面的情況發生時,web應用/虛擬目錄是完整的”重啟”了.所有已經在管道中被處理得請求會繼續在現存的管道中被處理,當任何一個新的請求來到時,它會被路由到新的應用程序域中.為了處理”被掛起的請求”,asp.net在請求已超時而它(指請求)還在等待時強制關閉應用程序域.所有事實上是可能出現一個應用程序對應兩個應用程序域,此時舊的那個正在關閉而新的正在啟動.兩個應用程序域都繼續為它們的客戶服務,直到老的那個處理玩正在等待處理的請求并關閉,此時只有一個應用程序域在運行.
“流過”asp.net管道
httpapplication觸發事件來通知你的程序有事發生,以此來負責請求流轉.這作為httpapplication.init()函數的一部分發生(用reflector查看system.web.httpapplication.initinternal()方法和httpapplication.resumesteps()方法來了解更多詳情),連續設置并啟動一系列事件,包括執行所有的處理器(handler).這些事件處理器映射到global.asax中自動生成的哪些事件中,同時它們也映射到所有附加的httpmodule(它們本質上是httpapplication對外發布的額外的事件接收器(sink)).
httpmodule和httphandler兩者都是根據web.config中對應的配置被動態載入并附加到事件處理鏈中.httpmodule實際上是事件處理器,附加到特殊的httpapplication事件上,然而httphandler是用來處理”應用級請求處理”的終點.
httpmodule和httphandler兩者都是在httpapplication.init()函數調用的一部分中被載入并附加到調用鏈上.圖6顯示了不同的事件,它們是何時發生的以及它們影響管道的哪一部分.
 
圖6-事件在asp.net http管道中流轉的過程.httpapplication對象的事件驅動請求在管道中流轉.http module可以攔截這些事件并覆蓋或者擴展現有的功能.
httpcontext, httpmodules 和 httphandlers
httpapplication它本身對發送給應用程序的數據一無所知-它只是一個通過事件來通訊的消息對象.它觸發事件并通過httpcontext對象來向被調用函數傳遞消息.實際的當前請求的狀態數據由前面提到的httpcontext對象維護.它提供了所有請求專有的數據并從進入管道開始到結束一直跟隨請求.圖7顯示了asp.net管道中的流程.注意上下文對象(即httpcontext),這個從請求開始到結束一直都是你”朋友”的對象,可以在一個事件處理函數中保存信息并在以后的事件處理函數中取出.
一旦管道被啟動,httpapplication開始象圖六那樣一個個的觸發事件.每個事件處理器被觸發,如果事件被掛接,這些處理器將執行它們自己的任務.這個處理的主要任務是最終調用掛接到此特定請求的httphandler.處理器(handler)是asp.net請求的核心處理機制,通常也是所有應用程序級別的代碼被執行的地方.記住asp.net頁面和web服務框架都是作為httphandler實現,這里也是處理請求的的核心之處.模塊(module)趨向于成為一個傳遞給處理器(handler)的上下文的預處理或后處理器.asp.net中典型的默認處理器包括預處理的認證,緩存以及后處理中各種不同的編碼機制.
有很多關于httphandler和httpmodule的可用信息,所以為了保持這篇文章在一個合理的長度,我將提供一個關于處理器的概要介紹.
httpmodule
當請求在管道中傳遞時,httpapplicaion對象中一系列的事件被觸發.我們已經看到這些事件在global.asax中作為事件被發布.這種方法是特定于應用程序的,可能并不總是你想要的.如果你要建立一個通用的可用被插入任何web應用程序的httpapplication事件鉤子,你可用使用httpmodule,這是可復用的,不需要特定語應用程序代碼的,只需要web.config中的一個條目.
模塊本質上是過濾器(fliter)-功能上類似于isapi過濾器,但是它工作在asp.net請求級別上.模塊允許為每個通過httpapplication對象的請求掛接事件.這些模塊作為外部程序集中的類存貯.,在web.config文件中被配置,在應用程序啟動時被載入.通過實現特定的接口和方法,模塊被掛接到httpapplication事件鏈上.多個httpmodule可用被掛接在相同的事件上,事件處理的順序取決于它們在web.config中聲明的順序.下面是在web.config中處理器定義.
<configuration>
<system.web>
<httpmodules>
<add name= "basicauthmodule"
type="httphandlers.basicauth,webstore" />
</httpmodules>
</system.web>
</configuration>
注意你需要指定完整的類型名和不帶dll擴展名的程序集名.
模塊允許你查看每個收到的web請求并基于被觸發的事件執行一個動作.模塊在修改請求和響應數據方面做的非常優秀,可用為特定的程序提供自定義認證或者為發生在asp.net中的每個請求增加其他預處理/后處理功能.許多asp.net的功能,像認證和會話(session)引擎都是作為httpmodule來實現的.
雖然httpmodule看上去很像isapi過濾器,它們都檢查每個通過asp.net應用的請求,但是它們只檢查映射到單個特定的asp.net應用或虛擬目錄的請求,也就是只能檢查映射到asp.net的請求.這樣你可以檢查所有aspx頁面或者其他任何映射到asp.net的擴展名.你不能檢查標準的.htm或者圖片文件,除非你顯式的映射這些擴展名到asp.net isapi dll上,就像圖1中展示的那樣.一個常見的此類應用可能是使用模塊來過濾特定目錄中的jpg圖像內容并在最上層通過gdi+來繪制’樣品’字樣.
實現一個http模塊是非常簡單的:你必須實現之包含兩個函數(init()和dispose())的ihttpmodule接口.傳進來的事件參數中包含指向httpapplication對象的引用,這給了你訪問httpcontext對象的能力.在這些方法上你可以掛接到httpapplication事件上.例如,如果你想掛接authenticaterequest事件到一個模塊上,你只需像列表5中展示的那樣做
列表5:基礎的http模塊是非常容易實現的
列表5:基礎的http模塊是非常容易實現的
public class basicauthcustommodule : ihttpmodule
{
public void init(httpapplication application)
{
// *** hook up any httpapplication events
application.authenticaterequest +=
new eventhandler(this.onauthenticaterequest);
}
public void dispose() { }
public void onauthenticaterequest(object source, eventargs eventargs)
{
httpapplication app = (httpapplication) source;
httpcontext context = httpcontext.current;
… do what you have to do… }
}
記住你的模塊訪問了httpcontext對象,從這里可以訪問到其他asp.net管道中固有的對象,如請求(request)和響應(response),這樣你還可以接收用戶輸入的信息等等.但是記住有些東西可能是不能訪問的,它們只有在處理鏈的后段才能被訪問.
你可以在init()方法中掛接多個事件,這樣你可以在一個模塊中實現多個不同的功能.然而,將不同的邏輯分到單獨的類中可能會更清楚的將模塊進行模塊化(譯注:這里的模塊化和前面的模塊沒有什么關系)在很多情況下你實現的功能可能需要你掛接多個事件-例如一個日志過濾器可能在beginrequest事件中記錄請求開始時間,然后在endrequest事件中將請求結束寫入到日志中.
注意一個httomodule和httpapplication事件中的重點:response.end()或httpapplication.completerequest()會在httpapplication和module的事件鏈中”抄近道”.看”注意response.end()”來獲得更多信息.
注意response.end()
當創建httpmodule或者在global.asax中實現事件鉤子的時候,當你調用response.end或 appplication.completerequest的時候要特別注意.這兩個函數都結束當前請求并停止觸發在http管道中后續的事件,然后發生將控制返回到web服務器中.當你在處理鏈的后面有諸如記錄日志或對內容進行操作的行為時,因為他們沒有被觸發,有可能使你上當.例如,sample中logging的例子就會失敗,因為如果調用response.end()的話,endrequest事件并不會被觸發.
httphandlers
模塊是相當底層的,而且對每個來到asp.net應用程序的請求都會被觸發.http處理器更加的專注并處理映射到這個處理器上的請求.
http處理器需要實現的東西非常簡單,但是通過訪問httpcontext對象它可以變得非常強大.http處理器通過實現一個非常簡單的ihttphandler接口(或是它的異步版本,ihttpasynchandler),這個接口甚至只含有一個方法-processrequest()-和一個屬性isreusable.關鍵部分是processrequest(),這個函數獲取一個httpcontext對象的實例作為參數.這個函數負責從頭到尾處理web請求.
單獨的,簡單的函數?太簡單了,對吧?好的,簡單的接口,但并不弱小!記住webform和webservice都是作為http處理器實現的,所以在這個看上去簡單的接口中包裝了很強大的能力.關鍵是這樣一個事實,當一個請求來到http處理器時,所有的asp.net的內部對象都被準備和設置好來處理請求了.主要的是httpcontext對象,提供所有相關的請求功能來接收輸入并輸出回web服務器.
對一個http處理其來說所有的動作都在這個單獨的processrequest()函數的調用中發生.這像下面所展示的這樣簡單:
public void processrequest(httpcontext context)
{
context.response.write("hello world");
}
也可以像一個可以從html模板渲染出復雜表單的webform頁面引擎那么完整,復雜.通過這個簡單,但是強大的接口要做什么,完全取決于你的決定.
因為context對象對你是可用的,你可用訪問request,response,session和cache對象,所以你擁有所有asp.net請求的關鍵特性,可以獲得用戶提交的內容并返回你產生的內容給客戶端.記住httpcontext對象-它是你在整個asp.net請求的生命周期中的”朋友”.
處理器的關鍵操作應該是將輸出寫入response對象或者更具體一點,是response對象的outputstream.這個輸出是實際上被送回到客戶端的.在幕后,isapiworkerrequest管理著將輸出流返回到isapi ecb的過程.writeclient方法是實際產生iis輸出的方法.
 
圖7-asp.net請求管道通過一系列事件接口來轉發請求,提供了更大的靈活性.application當請求到來并通過管道時作為一個載入web應用并觸發事件的宿主容器.每個請求都沿著配置的http過濾器和模塊的路徑走(譯注:原文為http filters and modules,應該是指http module和http handler).過濾器可以檢查每個通過管道的請求,handler允許實現應用程序邏輯或者像web form和webservice這樣的應用層接口.為了向應用提供輸入輸出,context對象在這個處理過程中提供了特定于請求的的信息.
webform使用一系列在框架中非常高層的接口來實現一個http處理器,但是實際上webform的render()方法簡單的以使用一個htmltextwriter對象將它的最終結果輸出到context.response.outputstream告終.所以非常夢幻的,終究即使是向webform這樣高級的工具也只是在request和response對象之上進行了抽象而已.
到了這里你可能會疑惑在http handler中你到底需要處理什么.既然webform提供了簡單可用的http handler實現,那么為什么需要考慮更底層的東西而放棄這擴展性呢?
webform對于產生復雜的html頁面來說是非常強大的,業務層邏輯需要圖形布局工具和基于模塊的頁面.但是webform引擎做了一系列overhead intensive的任務.如果你想要做的是從系統中讀入一個文件并通過代碼將其返回的話,不通過webform框架直接返回文件會更有效率.如果你要做的是類似從數據庫中讀出圖片的工作,并不需要使用頁面框架-你不需要模板而且確定不需要web頁面并從中捕捉用戶事件.
沒有理由需要建立一個頁面對象和session并捕捉頁面級別的事件-所有這些需要執行對你的任務沒有幫助的額外的代碼.
所以自定義處理器更加有效率.處理器也可用來做webform做不到的事情,例如不需要在硬盤上有物理文件就可用處理請求的能力,也被稱為虛擬url.要做到這個,確認你在圖1中展示的應用擴展對話框中關掉了”檢查文件存在”選項.
這對于內容提供商來說非常常見,象動態圖片處理,xml服務,url重定向服務提供了vanity urls,下載管理以及其他,這些都不需要webform引擎.
異步http handler
在這篇文章中我大部分都在討論同步處理,但是asp.net運行時也可以通過異步http handler來支持異步操作.這些處理器自動的將處理”卸載”到獨立的線程池的線程中并釋放主asp.net線程,使asp.net線程可以處理其他的請求.不幸的是在1.x版的.net中,”卸載”后的處理還是在同一個線程池中,所以這個特性之增加了一點點的性能.為了創建真正的異步行為,你必須創建你自己的線程并在回調處理中自己管理他們.
當前版本的asp.net 2.0 beta 2在ihttphandlerasync(譯注:此處應該是指ihttpasynchandler,疑為作者筆誤)接口和page類兩方面做了一些對異步處理的改進,提供了更好的性能,但是在最終發布版本中這些是否會保留.
我說的這些對你來說夠底層了嗎?
唷-我們已經走完了整個請求處理過程了.這過程中有很多底層的信息,我對http模塊和http處理器是怎么工作的并沒有描述的非常詳細.挖掘這些信息相當的費時間,我希望在了解了asp.net底層機制后,你能獲得和我一樣的滿足感.
在結束之前讓我們快速的回顧一下我在本文中討論的從iis到處理器(handler)的過程中,事件發生的順序
iis獲得請求 
檢查腳本映射中,此請求是否映射到aspnet_isapi.dll 
啟動工作進程 (iis5中為aspnet_wp.exe,iis6中為w3wp.exe) 
.net運行時被載入 
isapiruntime.processrequest()被非托管代碼調用 
為每個請求創建一個isapiworkerrequest 
httpruntime.processrequest()被工作進程調用 
以isapiworkerrequest對象為參數創建httpcontext對象 
調用httpapplication.getapplicationinstance()來從池中取得一個對象實例 
調用httpapplication.init()來開始管道事件并掛接模塊和處理器 
httpapplicaton.processrequest被調用以開始處理. 
管道中的事件被依次觸發 
處理器被調,processrequest函數被觸發 
控制返回到管道中,后處理事件被依次觸發 
 
有了這個簡單的列表,記住這些東西并把他們拼在一起就變得容易多了.我時??纯此鼇砑由钣洃?所以現在,回去工作,做一些不那么抽象的事情…
雖然我都是基于asp.net1.1來進行討論的,不過在asp.net2.0中這些處理過程看上去并沒有發生太大的變化.
國內最大的酷站演示中心!
| 
 
 | 
新聞熱點
疑難解答
圖片精選