ASP.NET內幕 - ISAPI和應用程式域之間的橋樑

weixin_34219944發表於2009-01-05

介紹
在前一篇,也是這一系列的第一篇中,我介紹了web伺服器接收到web請求之後進行的第一步處理,以及如果被確定為ASP.NET資源請求時如何路由請求。你已經明白不同的IIS版本在處理ASP.NET相關請求時的差異,最終請求被分發到一個叫做aspnet_isapi.dll的非託管Win32元件,這個元件的作用就是web伺服器和託管ASP.NET架構之間的橋樑。
這篇文章中我將繼續討論上一篇中的遺留問題,開始研究處理環境的託管部分,在前一篇文章中它被稱作ASP.NET執行時環境,作為一個黑盒子提到過。
注意:ASP.NET執行時環境有時也叫做ASP.NET管道、Http執行時管道,或者是這些單詞的混合。
從aspnet_isapi.dll擴充套件到託管世界
前一篇文章中我解釋了IIS 5和IIS 6怎樣管理ASP.NET請求,忽略它們怎樣處理工作程式的建立、管理和回收,最後所有與請求相關的訊息歸結到了aspnet_isapi.dll擴充套件上。
這是非託管和託管世界的橋樑,也是所有ASP.NET架構中最缺乏文件的部分。
注意:在寫這篇文章的時候,IIS 7以一個上線許可與Longhorn Server Beta 3一起釋出了。IIS 7中許多事情會改變,儘管這些前期文章只是關注之前的IIS版本,以後的文章將會致力於IIS 7中引入的改進。
因為在這個主題的很多方面缺乏可用的文件,我將進行的解釋可能不完全正確,特別是涉及到非託管結構的部分。然而我關於未文件化部分主題的考量,是基於一些對理解框架內部工作原理很有幫助的工具進行的:

回到非託管和託管世界的橋樑上來,不管是IIS 6處理模式的情況下通過ISAPI擴充套件,還是IIS5處理模式的情況下通過工作程式,在CLR載入之後,黑暗魔法出現了。ASP.NETISAPI元件通過位於System.Web.Hosting名稱空間中的兩個託管類,呼叫一大把的非託管COM介面,通過COM介面方式暴露這些方法的類是AppManagerAppDomainFactory和ISAPIRuntime。
注意:公共語言執行時CLR掌管每一個.NET應用程式的執行環境,它為執行託管應用程式提供環境和服務。CLR必須執行在Win32程式中,ASP.NET是.NET框架為CLR提供的一個宿主之一,確切地說在ASP.NET中,ASP.NET工作程式(IIS 5中為aspnet_wp.exe,IIS6中為w3wp.exe)就是執行CLR的程式。
在研究這些類中發生的互動行為的技術細節之前,我們先大致看一下處理請求時究竟發生了什麼。我前面已經介紹過這兩個類,因為處理請求的入口點可以大致的分為兩個方面。
    1. 如果AppDomain還不存在則建立APPDomain,將AppDomain指派給與請求對應的應用程式,這通過AppManagerAppDomain類實現。
    2. 提交和處理請求,由ISAPIRuntime類實現。
這兩個方面都很重要,第一個包括了不需要開發者參與的一些互動動作,主要涉及應用程式的執行環境,而第二個則是結構上最可配置的部分,我將在這個系列的後續文章中完整地探討。
從另一個角度來看,第一步包含的操作在應用程式生存期裡只執行一次,就是說在啟動的時候,而第二步包含的互動在每一次定位請求的目標應用程式時都會出現。
建立AppDomain
在前面文章中瞭解到,一個ASP.NET應用程式被封裝成一個叫做應用程式域的實體,簡寫AppDomain,由ASP.NET架構中的一個類AppDomain表示。當特定應用程式的請求到達時,如果不存在則必須建立AppDomain物件,這通常發生在屬於特定應用程式的請求第一次到達時,或者由於某種原因相應的AppDomain被關閉掉了,幾個可能導致這個問題的原因我在後面會談到。注意一個ASP.NET應用程式只會有一個AppDomain存在,它跟IIS應用程式一對一對映,可能基於物理目錄或者虛擬目錄建立。那麼這個類的例項是怎樣建立的呢?
圖1:建立AppDomain例項時由JetBrains dotTrace Profiler產生的呼叫棧

使用Reflector可以推斷AppDomain有一個丟擲NotSupportedException異常的私有構造器,因此很明顯這個方法行不通。實際上初始化AppDomain例項的入口點是AppManagerAppDomainFactory.Create方法,為了找出這一點,需要使用一個分析器跟蹤初始化物件時生成的呼叫棧。圖1顯示了JetBrains dotTracProfiler的截圖,它顯示了執行在IIS上的一個web應用程式在初始化AppDomain時生成的呼叫棧。有很多類參與了這個過程,它們也許是開發者永遠不需要用到的。
注意:AppManagerAppDomainFactory類在CLR初始化過程中只執行一次,就像它的名字暗示的,它作為建立AppDomain以及其他重要物件的入口點。
圖2顯示了一個跟前面圖1一樣的呼叫棧,這次是從微軟CLR Profiler呼叫樹的輸出中擷取的,它為AppDomain的初始化提供了更多的資訊。實際上高亮行表明主執行緒建立了兩個AppDomain類的例項。
圖2:微軟CLR Profiler呼叫樹檢視顯示的建立AppDomain例項時的呼叫棧

那麼是怎樣建立這個額外的例項呢,為什麼建立?很不幸這不是個容易的問題。前面圖中顯示的呼叫棧,指示了在建立與實際執行應用程式相關的AppDomain例項時採用的步驟,因此這個額外的例項,看起來像是用於某些難於理解的目的的輔助物件,盡我所能的猜測,它被用於容納所有不屬於任何應用程式的物件,例如AppManagerAppDomainFactory,因此被放置在一個隔離的AppDomain中。對於AppManagerAppDomainFactory類,這個輔助AppDomain只在CLR初始化時例項化一次,它在CLR初始化很早的時候進行,如圖3所示。
圖3:額外AppDomain例項的順序和分配方法

圖3展示了Red Gate ANTSProfiler的一個截圖,它顯示了額外AppDomain例項在CLR初始化很早的時候建立,很明顯它比AppManagerAppDomainFactory類更早建立,因為很有可能它是AppManagerAppDomainFactory的容器。實際上在前面分析會話圖示中,單例AppManagerAppDomainFactory例項的ID為52。
另一個檢視前面提到的兩個AppDomain例項初始化過程的方法,是微軟CLR Profiler提供的分配檢視,它建立一個圖形流程,展示處理中類的使用位置,像圖4顯示那樣。
圖4:微軟CLR Profiler分配檢視顯示的兩個AppDomain初始化過程

它展示了一個平面檢視,從根元素,即CLR,和執行緒類之間裁剪出所有的類,然而它清晰的顯示了建立兩個AppDomain時的呼叫順序。
從前面圖中可以得到另外一個關於AppDomain例項的資訊是,每個例項佔用100位元組記憶體。這不會帶來多少問題,但有人會認為AppDomain是一個很大的類,因為它必須容納整個應用程式。實際上它提供很多的服務,但並不儲存太多資料。
到現在輔助AppDomain例項已經被建立了,包括AppManagerAppDomainFactory類,它的Create方法就是圖1和2中顯示的呼叫棧的入口方法。
這個方法通過COM暴露出來,提供給呼叫者,因此是非託管程式碼。AppManagerAppDomainFactory類實現了 IAppManagerAppDomainFactory介面,它的結構顯示在圖5中。
圖5:IAppManagerAppDomainFactory介面結構

這個介面使用ComImport和InterfaceType屬性修飾,它們將型別繫結到非託管介面型別上。呼叫這個方法時,將產生圖1和2的呼叫棧圖形,最終觸發AppDomain類例項的建立,容納用於處理請求的目標應用程式。
AppDomain物件建立以及執行之後,剩餘工作就是處理請求,這比前面已經完成的工作內容更多,這也許是ASP.NET架構中最有意思的部分,但在這個有趣的部分開始之前我已經介紹了一些關鍵的主題。
總結
這篇文章中我介紹了ASP.NET基礎結構中一些很底層的主題,涉及那些由ASP.NETISAPI擴充套件展示的非託管世界,以及執行web應用程式的AppDomain所展示的託管世界之間的一些介面。我展示了初始化AppDomain的機制,以及哪些類參與了這個處理。下一篇文章中我會通過HTTP管道討論託管程式碼,在我的映像裡這應當是ASP.NET架構最吸引人的一章,實際上它使得ASP.NET不同於其他所有的web開發框架。我希望你像我寫這篇文章時一樣充滿興趣的閱讀,我的建議是開啟一些分析工具自己嘗試這些內容,這是充分理解它怎樣工作的最好方法。

相關文章