讓我們在開始本節內容之前先了解以下幾個基本概念。
應用程序域
應用程序域(通常簡稱為AppDomain)可以視為一種輕量級進程。一個Windows進程內可以包含多個AppDomain。AppDomain這個概念的提出是為了實現在一個物理服務器中承載多個應用程序,並且這些應用能夠相互獨立。ASP.NET中利用AppDomain在同一個進程內承載了多組Web應用程序就是一個例子。實際上微軟曾進行過在單一進程內承載多達1000個簡單Web應用程序的壓力測試。
使用AppDomain所獲得的性能優勢主要體現在兩方面:
·創建AppDomain所需要的系統資源比創建一個Windows進程更少。
·同一個Windows進程內所承載的AppDomain之間可以互相共享資源,如CLR、基本.NET類型、地址空間以及線程。
而各個AppDomain之間的獨立性體現為以下這些特征:
·一個AppDomain可以獨立於其他的AppDomain而被卸載。
·一個AppDomain無法訪問其他AppDomain的程序集和對象。
·若沒有發生跨邊界的異常拋出,一個AppDomain擁有自己獨立的異常管理策略。這意味著一個AppDomain內出現問題不會影響到同一個進程內中的其他AppDomain。
·每個AppDomain可以分別定義獨自的程序集代碼訪問安全策略。
·每個AppDomain可以分別定義獨自的規則以便CLR在加載前定位程序集所在位置。
可以看出應用程序域是進程中的一個子單元,不過在.NET中還存在一個比應用程序域還要細粒度的單元——.NET上下文(Context)。
.NET Context
一個.NET 應用程序域能夠包含多個被稱為.NET上下文的實體。所有.NET對象都存在於上下文中,每個應用程序域中至少存在一個上下文。這個上下文稱為應用程序域的默認上下文,它在應用程序域創建的時候就創建了。下圖總結了它們之間的關系:
那麼MessageSink與上下文有什麼關系呢? 我們知道在通常情況下,如果訪問同一個AppDomain中對象的方法時,會采用基於棧的方式(詳見本系列上部)。在這種情況下,我們是無法攔截其中的消息的,因為此時根本不存在消息對象。只有當我們通過Transparent Proxy訪問另一個對象的方法時,才會采用基於消息的方式。而現在我們只知道當一個對象調用處在另一個AppDomain中的遠程對象(該對象為MarshalByRefObject子類)時,Remoting才會為調用方創建那個遠程對象的Transparent Proxy。在了解了.NET上下文的概念後,你會發現,即使處在同一個AppDomain中的兩個對象,如果它們所處的上下文不同,在訪問對方的方法時,也會借由Transparent Proxy實現,即采用基於消息的方法調用方式。此時,我們就可以在上下文中插入MessageSink了。那麼在上下文中是否存在類似IClientChannelSinkProvider的接口呢?很幸運,在經過一番探索後,我們發現確實存在類似的接口,而且還不止一個:IContributeEnvoySink、IContributeServerContextSink、IContributeObjectSink、IContributeClientContextSink這四個接口中各自包含了一個GetXXXSink的方法,它們都會返回一個實現了IMessageSink接口的對象。我們知道IClientChannelSinkProvider接口是配合配置文件最終實現向Pipeline中加入ChannelSink的,而以上這四個接口並沒有配合配置文件使用,不過與IClientChannelSinkProvider的使用方式倒也有異曲同工之效。讀者可以將下面這幅圖與本系列上部中的圖2比較一番。
在上圖中又出現了很多新的概念,下面將對它們一一做出解釋:
·ContextBoundObject
上下文可以看作應用程序域中一個包含對象和消息接收器的區域。對上下文裡的對象的調用會轉換成可以被MessageSink(消息接收器)攔截和處理的消息。我們知道要把調用轉換成消息,必須通過透明代理這個中介。而且,僅當對象是MarshalByRefObject的子類的實例並被其所在的應用程序域以外的實體調用時,CLR才會為它創建透明代理。這裡,我們希望對所有調用使用消息接收器機制,即使那些調用是來自同一個應用程序域中的實體。這個時候我們就需要用到System.ContextBoundObject類了。繼承自ContextBoundObject的類的實例同樣僅能由透明代理訪問。此時,即使在這個類的方法中使用的this引用也是透明代理而不是對這個對象的直接引用。我們會發現ContextBoundObject類繼承自MarshalByRefObject,這非常合理,因為它很好地強調了該類的特性——它告訴CLR這個類將會通過透明代理使用。
ContextBoundObject的子類的實例被視為上下文綁定的(context-bound)。沒有繼承自ContextBoundObject的類的實例則被視為上下文靈活的(context-agile)。上下文綁定的對象永遠在其上下文中執行。只要不是遠程對象,上下文靈活的對象總是在執行這個調用的上下文中執行。如下圖所示:
·ContextAttribute
上下文attribute是應用在上下文綁定的類上的.NET attribute。上下文attribute類實現了System.Runtime.Remoting.Contexts.IContextAttribute接口。上下文綁定的類可以應用多個上下文attribute。在這個類的對象創建期間,這個類的每個上下文attribute判斷這個對象的創建者所在的上下文是否適用。該操作通過以下方法完成:
public bool IContextAttribute.IsContextOK(Context clientCtx,IConstructionCallMessage ctorMsg)
只要其中一個上下文attribute返回false,CLR就必須創建一個新的上下文來容納這個新的對象。這樣,每個上下文attribute可以在這個新的上下文中注入一個或多個上下文屬性。這些注入通過以下方法完成:
public void IContextAttribute.GetPropertiesForNewContext(IConstructionCallMessage ctorMsg)
·IContextProperty
上下文屬性是實現System.Runtime.Remoting.Contexts.IContextProperty接口的類的實例。每個上下文可以包含多個屬性。上下文屬性在上下文創建的時候通過上下文attribute注入。一旦每個上下文attribute注入了它的屬性,就會為每個屬性調用下面的方法。此後就無法在這個上下文中注入另外的屬性了:
public void IContextProperty.Freeze( Context ctx )
然後,CLR通過調用下面的方法判斷新的上下文能否滿足每個屬性:
public bool IContextProperty.IsNewContextOK( Context ctx )
每個上下文屬性都有一個通過Name屬性定義的名稱:
public string IContextProperty.Name{ get }
上下文中承載的對象的方法可以通過調用下面的方法訪問上下文屬性:
IContextProperty Context.GetProperty( string sPropertyName )
這一點很有意思,上下文中的對象通過它們所在的上下文的屬性可以共享信息並訪問服務。不過,上下文屬性的主要作用並不在於此。上下文屬性的主要作用在於向相關上下文中的消息接收器區域注入消息接收器(MessageSink)。(消息接收器區域的概念將在後面介紹)
以上注入MessageSink的過程可以用下圖概括:
MessageSink Region
不知你是否記得之前提到的四個接口:IContributeEnvoySink、IContributeServerContextSink、IContributeObjectSink、IContributeClientContextSink。其實它們分別代表了四個不同的消息接收器區域:服務器(server)區域、對象(object)區域、信使(envoy)區域和客戶端(client)區域。要理解區域概念,你必須考慮上下文綁定的對象是否被位於另一個上下文的實體調用。這個實體可以是一個靜態方法或者另一個對象。在我們關於區域的討論中,我們把這個實體所在的上下文稱為調用方上下文(calling context),而把被調用對象所在的上下文稱為目標上下文(target context)。目標上下文中的每個屬性都可以在這些區域中注入消息接收器。
·注入服務器區域的消息接收器攔截所有從另一個上下文發往目標上下文中所有對象的調用消息。於是,每個目標上下文有一個服務器區域。
·注入對象區域的消息接收器攔截所有從另一個上下文發往目標對象中特定對象的調用消息。於是,上下文中每個對象會有一個對象區域。
·注入信使區域的消息接收器攔截所有從另一個上下文發往目標對象中特定對象的調用消息。信使區域和對象區域的不同點是信使區域位於調用方上下文而不是包含對象的目標上下文。我們使用信使區域把調用方上下文的信息傳遞給目標上下文的消息接收器。
·注入客戶端區域的消息接收器攔截所有從目標上下文發往位於其他上下文的對象的調用消息。於是,每個目標上下文有一個客戶端區域。你可能會對這個區域所處的位置有點困惑,似乎當它位於Calling context的信使區域下方時會顯得更加對稱。之所以會有這樣的誤解,是因為我們對Server、Client的理解有了偏差。你應該記住除了信使區域是位於Calling context外,另外三個區域都是處在Target Context。而所謂的Server,Client是針對處在Target Context中的對象在某不同時刻所扮演不同角色而言的。當然Calling context中也會有客戶端區域,不過其中的MessageSink不是通過Target context的屬性注入的,而應該依靠Calling context中的上下文屬性注入。
上圖說明了區域的概念。目標上下文包含名為OBJ1和OBJ2的兩個對象。我們選擇在目標上下文中放置兩個對象而不是一個是為了更好地說明對象區域和信使區域是在對象層面與消息的攔截關聯起來的,而服務器區域和客戶端區域則是在上下文層面與消息的攔截關聯起來的。
我們在每個區域中放置了兩個自定義消息接收器是為了更好地說明一個區域能包含零個、一個或多個消息接收器。具體地說,所有自定義消息接收器都通過目標上下文的屬性注入區域,即使這個區域不屬於目標上下文。因為你可以定義你自己的上下文屬性類,你可以選擇必須注入哪個消息接收器。
你可能注意到每個區域都包含一個用於通知CLR退出區域的系統終結器接收器(system terminator sink),它是由Remoting框架定義的,並且總是位於每個區域的末尾。
當調用方上下文和目標上下文處在同一個應用程序域中時,CLR會使用mscorlib.dll中CrossContextChannel內部類的實例作為信道。這個實例會使得當前線程的Context屬性發生切換。圖中也展示了這一實例。