天天看點

了解ASP.NET底層架構   了解ASP.NET底層架構  

了解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中這些處理過程看上去并沒有發生太大的變化.