文章目錄
原文地址
Microsoft .NET 遠端:技術概述
Piet Obermeyer 和 Jonathan Hawkins
Microsoft Corporation
更新日期:2001 年 7 月
摘要:本文提供 Microsoft .NET 遠端架構的技術概述。它包括使用 TCP 通道或 HTTP 通道的樣本。(共 15 頁列印頁)
注意 本文包括已更新的 beta 2 代碼。
內容
介紹
遠程對象
代理對象
通道
啟用
使用租用的物件存留期
結束語
附錄 A:使用 TCP 通道的遠端樣本
介紹
Microsoft .NET 遠端提供一個允許對象在應用程式定義域間互相互動的架構。此架構提供若干服務,包括啟用和生存期支援,以及負責在本地和遠程應用程式之間傳輸訊息的通訊信 道。格式化程式用於在通過通道傳輸訊息之前編碼和解碼資訊。應用程式可以在效能因素比較關鍵的地方使用二進位編碼,或者在必須具有與其他遠端架構的互 操作性的地方使用 XML 編碼。所有 XML 編碼都使用 SOAP 協議從一個應用程式定義域向另一個應用程式定義域傳輸訊息。遠端的設計考慮了安全性,並提供許多掛鈎以使通道接收器在流通過通道傳輸之前可以訪問訊息和序列化 流。
在沒有基礎架構支援的情況下管理遠程對象的生存期常常會很麻煩。.NET 遠端提供了許多啟用模型以供選擇。這些模型分為兩類:
用戶端啟用的對象由基於租約的生存期管理器管理,該管理器確保在對象的租約到期時對該對象進行記憶體回收。對於伺服器啟用的對象,開發人員可以選擇“single call”或“singleton”模型。Singleton 的生存期也由基於租約的生存期控制。
遠程對象
任何遠端架構的主要目標之一是提供必要的結構,該結構隱藏調用遠程對象上的方法和返回結果的複雜性。調用方應用程式定義域外部的任何對象都應認為是 遠端,即使這些對象是在同一台電腦上執行。在應用程式定義域內部,所有對象都通過引用來傳遞,而基中繼資料類型通過值來傳遞。由於局部對象引用只在建立它們 的應用程式定義域內部有效,所以無法以這種形式將它們傳遞到遠程方法調用或從遠程方法調用返回。所有必須跨越應用程式定義域邊界的局部對象都必須通過值來傳遞,並 且應使用 [serializable] 自訂屬性來標記,否則它們必須實現 ISerializable 介面。當對象作為參數傳遞時,架構會序列化對象並將它傳輸到目標應用程式域,對象將在那裡被重新構造。無法序列化的局部對象不能被傳遞到不同的應用程式定義域,因此不能將它們用於遠程。
任何對象都可以通過將其從 MarshalByRefObject 派生而變成遠程對象。當用戶端啟用遠程對象時,它將收到遠程對象的代理。所有對此代理進行的操作都被正確地間接定址,以便使遠端結構能夠正確地截獲和 轉寄調用。這種間接定址確實對效能有一些影響,但是已經對 JIT 編譯器和執行引擎 (EE) 進行了最佳化,以便在代理和遠程對象駐留在同一應用程式定義域時避免不必要的效能損失。在代理和遠程對象處於不同應用程式定義域的情況下,堆棧上的所有方法調用參數 都被轉換為訊息並傳輸到遠程應用程式定義域,然後這些訊息在該域中重新變為堆疊框架並調用方法調用。同一過程也用於從方法調用返回結果。
代理對象
代理對象在用戶端啟用遠程對象時建立。代理對象充當遠程對象的代表,並確保將對代理進行的所有調用都轉寄到正確的遠程對象執行個體。為了確切地理解代理對象如何工作,我們需要更為詳細地研究它們。當用戶端啟用遠程對象時,架構將建立 TransparentProxy 類的一個本地執行個體,該執行個體包含所有類的列表以及該遠程對象的介面方法。由於 TransparentProxy 類在建立時註冊到 CLR,因此所有對代理進行的方法調用都會被運行庫截獲。在這裡將檢查調用以確定它是否是遠程對象的有效方法,以及遠程對象的執行個體是否與代理駐留在同一應 用程式域中。如果是,則將一個簡單的方法調用路由到實際對象。如果該對象處於不同的應用程式定義域中,則將堆棧的調用參數打包到 IMessage 對象中並通過調用該對象的 Invoke 方法轉寄到 RealProxy 類。該類(或其內部實現)負責將訊息轉寄到遠程對象。TransparentProxy 和 RealProxy 類都在啟用遠程對象時隨即建立,但只有 TransparentProxy 被返回到用戶端。
為了更好地理解這些代理對象,我們需要轉而簡要地論及 ObjRef。在啟用節中提供了有關 ObjRef 的詳細說明。下列方案簡要描述了 ObjRef 與這兩個代理類是如何發生關係的。注意這是對該過程的一個十分概括的描述是很重要的;存在一些不同的情況,具體取決於對象是用戶端啟用的還是伺服器啟用的,以及它們是 singleton 還是 single-call 對象。
- 遠程對象是在遠端電腦上的應用程式定義域中註冊的。對該對象進行封送以產生一個 ObjRef。ObjRef 包含從網路上的任何位置定位和訪問遠程對象所需要的所有資訊。該資訊包括類的強式名稱、類的階層(類的父級)、類所實現的所有介面的名稱、對象 URI 和所有登入的可用通道的詳細資料。當遠端架構接收到對該遠程對象的請求時,它使用對象 URI 檢索為該遠程對象建立的 ObjRef 執行個體。
- 用戶端通過調用 new 或 Activator 函數之一(如 CreateInstance)來啟用遠程對象。對於伺服器啟用的對象,遠程對象的 TransparentProxy 在用戶端應用程式域中產生並返回到用戶端,不需要進行任何遠程調用。遠程對象只在用戶端調用遠程對象上的方法時被啟用。由於用戶端期望架構在被要求時啟用 對象,因此此方案顯然不適用於用戶端啟用的對象。當用戶端調用一個啟用方法時,將在用戶端上建立啟用代理,並將 URL 和對象 URI 作為端點啟動對伺服器上的遠程啟用器的遠程調用。遠程啟用器啟用該對象,ObjRef 被傳輸到用戶端,並被解除封送處理以產生返回到用戶端的 TransparentProxy。
- 在解除封送處理期間,將分析 ObjRef 以提取遠程對象的方法資訊,並且建立 TransparentProxy 和 RealProxy 對象。在將 TransparentProxy 註冊到 CLR 之前,所分析的 ObjRef 的內容被添加到 TransparentProxy 的內部表中。
TransparentProxy 是一個不可被替換或擴充的內部類。另一方面,RealProxy 類和 ObjRef 類是公用的,並且可在必要時擴充和自訂它們。例如,由於 RealProxy 類處理遠程對象上的所有函數調用,因此它是執行Server Load Balancer的理想候選。當調用 Invoke 時,從 RealProxy 派生的類可以擷取有關網路上伺服器的載入資訊,並將該調用路由到適當的伺服器。只需為所需的 ObjectURI 從通道請求一個 MessageSink 並且調用 SyncProcessMessage 或 AsyncProcessMessage 以便將調用轉寄到所需的遠程對象。調用返回時,RealProxy 將自動處理返回參數。
以下代碼片斷顯示如何使用派生的 RealProxy 類。
MyRealProxy proxy = new MyRealProxy(typeof(Foo)); Foo obj = (Foo)proxy.GetTransparentProxy(); int result = obj.CallSomeMethod();
可以將上面獲得的 TransparentProxy 轉寄到另一個應用程式定義域。當第二個用戶端試圖調用代理上的方法時,遠端架構將試圖建立 MyRealProxy 的一個執行個體,並且如果程式集可用,則將通過此執行個體路由所有調用。如果程式集不可用,則將通過預設遠端 RealProxy 路由這些調用。
通過替換預設 ObjRef 屬性 TypeInfo、EnvoyInfo 和 ChannelInfo,可以容易地自訂 ObjRef。下列代碼顯示如何做到這一點。
public class ObjRef { public virtual IRemotingTypeInfo TypeInfo { get { return typeInfo;} set { typeInfo = value;} } public virtual IEnvoyInfo EnvoyInfo { get { return envoyInfo;} set { envoyInfo = value;} } public virtual IChannelInfo ChannelInfo { get { return channelInfo;} set { channelInfo = value;} } } 通道
通道用於在本地和遠程對象之間傳輸訊息。當用戶端調用遠程對象上的方法時,參數以及與調用有關的其他詳細資料都將通過通道傳輸到遠程對象。調用的任 何結果將以同樣的方式返回到用戶端。用戶端可以選擇在“伺服器”上註冊的任何通道與遠程對象通訊,從而允許開發人員自由地選擇最適合他們需要的通道。自定 義任何現有通道或產生使用不同通訊協議的新通道也是可能的。通道的選擇取決於下列規則:
- 在可以調用遠程對象之前,必須有至少一個通道註冊到遠端架構。在註冊對象之前,必須登入通道。
- 按應用程式定義域來註冊通道。在單個進程中可以存在多個應用程式定義域。如果進程死亡,則它註冊的所有通道都被自動銷毀。
- 多次註冊偵聽同一連接埠的相同通道是非法的。即使通道按應用程式定義域進行註冊,同一電腦上的不同應用程式定義域也不能註冊偵聽相同連接埠的同一通道。可以註冊偵聽兩個不同連接埠的同一通道。
- 用戶端可以使用任何登入通道與遠程對象通訊。當用戶端試圖串連到遠程對象時,遠程架構確保將遠程對象串連到正確的通道。用戶端負責在試圖與遠程對象通訊之前調用 ChannelService 類上的 RegisterChannel。
所有通道都從 IChannel 匯出,並根據通道的用途實現 IChannelReceiver 或 IchannelSender。大多數通道都同時實現接收器和發送器介面以使它們能夠在任一方向進行通訊。當用戶端調用代理上的方法時,該調用將被遠端架構截獲並更改為被轉寄到 RealProxy 類(或者是實現 RealProxy 的類的一個執行個體)的訊息。RealProxy 將訊息轉寄到通道接收器鏈以供處理。
鏈中的第一個接收器通常是一個格式化程式接收器,它將訊息序列化為位元組流。然後將訊息從一個通道接收器傳遞到下一個通道接收器,直到它到達位於鏈末 端的傳輸接收器。傳輸接收器負責建立與伺服器端的傳輸接收器的串連,並將位元組流發送給伺服器。然後,伺服器上的傳輸接收器通過伺服器端的接收器鏈轉寄該字 節流,直到它到達格式化程式接收器,在這裡將訊息從它的調度點還原序列化為遠程對象本身。
遠端架構的一個容易混淆的方面是遠程對象與通道之間的關係。例如,如果只在調用到達時啟用物件,則 SingleCall 遠程對象如何設法偵聽要串連的用戶端?
神奇之處部分在於遠程對象共用通道這一事實。遠程對象並不擁有通道。承載遠程對象的伺服器應用程式必須註冊其所需的通道以及希望用遠端架構公開的對象。當註冊通道後,該通道自動在指定連接埠啟動對用戶端請求的偵聽。當註冊遠程對象後,將為該對象建立 ObjRef,並將 ObjRef 存 儲在表中。當請求在通道中到達時,遠端架構將檢查訊息以確定目標對象並檢查對象參考資料表以在表中尋找引用。如果找到對象引用,則將從該表中檢索架構目標 對象或在必要時啟用架構目標對象,然後架構將調用轉寄到該對象。在同步調用的情況下,將在訊息調用期間維持來自用戶端的串連。由於每個用戶端串連都在自己 的線程中處理,所以單個通道可以同時為多個用戶端提供服務。
安全性在產生商務應用程式時是一個重要的考慮因素,開發人員必須能夠為遠程方法調用增加安全功能(如授權或加密),以便滿足業務需求。為了適應此需要,可以自訂通道以便為開發人員提供對往返於遠程對象的訊息的實際傳輸機制的控制。
HTTP 通道
HTTP 通道使用 SOAP 協議傳輸往返於遠程對象的訊息。所有訊息都經過 SOAP 格式化程式傳遞,該格式化程式將訊息更改為 XML 並序列化,然後為流添加所需的 SOAP 頭。還可以配置 HTTP 通道以使用二進位格式化程式。然後使用 HTTP 協議將得到的資料流傳輸到目標 URI。
TCP 通道
TCP 通道使用二進位格式化程式將所有訊息序列化為二進位流,並使用 TCP 協議將該流傳輸到目標 URI。還可以將 TCP 通道配置為 SOAP 格式化程式。
啟用
遠端架構支援遠程對象的伺服器和用戶端啟用。當不需要遠程對象維護方法調用間的任何狀態時,通常使用伺服器啟用。在多個用戶端調用同一對象執行個體 上的方法以及對象維護函數調用之間的狀態的情況下,也使用伺服器啟用。另一方面,從用戶端執行個體化用戶端啟用的對象,並且用戶端通過使用為該用途提供的基於 租約的系統來管理遠程對象的生存期。
所有遠程對象都必須在用戶端可以訪問它們之前註冊到遠端架構。通常由宿主應用程式執行對象註冊,該應用程式啟動後,將一個或多個通道註冊到 ChannelServices,將一個或多個遠程對象註冊到 RemotingConfiguration,然後等待直到它被終止。注意到以下一點很重要,即所註冊的通道和對象只在註冊它們的進程處於活動狀態時可用。當進程退出時,該進程註冊的所有通道和對象都將自動從註冊它們的遠端服務中移除。將遠程對象註冊到架構時,需要下列四條資訊:
- 包含類的程式集名稱。
- 遠程對象的類型名稱。
- 用戶端將用來尋找對象的對象 URI。
- 伺服器啟用所需的對象模式。這可以是 SingleCall 或 Singleton。
可以通過以下方式註冊遠程對象:調用 RegisterWellKnownType,將上面的資訊作為參數傳遞;或者將上面的資訊儲存在設定檔中,然後調用 Configure,從而將設定檔名作為參數傳遞。這兩個函數都可用於註冊遠程對象,因為它們執行完全相同的功能。由於可以不重新編譯宿主應用程式而改變更配置置檔案的內容,所以後者更便於使用。下列代碼片斷顯示如何將 HelloService 類註冊為 SingleCall 遠程對象。
RemotingConfiguration.RegisterWellKnownServiceType( Type.GetType("RemotingSamples.HelloServer,object"), "SayHello", WellKnownObjectMode.SingleCall);
其中 RemotingSamples 是命名空間,HelloServer 是類的名稱,Object.dll 是程式集的名稱。SayHello 是將在該處公開我們的服務的對象 URI。對象 URI 可以是直接宿主的任何文本字串,但是當服務寄宿在 IIS 中時它需要 .rem 或 .soap 副檔名。因此,建議所有的遠端終結點(URI 的)都使用這些副檔名。
當註冊對象時,架構為此遠程對象建立對象引用,然後從程式集提取所需的關於對象的中繼資料。然後將此資訊以及 URI 和程式集名稱儲存在對象引用中,該對象引用被儲存在用來跟蹤登入的遠程對象的遠端架構表中。注意到遠程對象本身不是由註冊進程執行個體化這一點很重要。 這僅在用戶端試圖調用對象上的方法或從用戶端啟用物件時發生。
任何知道此對象的 URI 的用戶端現在都可以通過用 ChannelServices 註冊其首選的通道來獲得此對象的代理,並通過調用 new、GetObject 或 CreateInstance 來啟用物件。下列代碼片斷顯示如何進行此操作的樣本。
"" ChannelServices.RegisterChannel(new TcpChannel()); HelloServer obj = (HelloServer)Activator.GetObject( typeof(RemotingSamples.HelloServer), "tcp://localhost:8085/SayHello");
這裡,"tcp://localhost:8085/SayHello" 指定想要使用連接埠 8085 上的 TCP 串連到 SayHello 端點上的遠程對象。當編譯此用戶端代碼時,編譯器顯然需要有關 HelloServer 類的類型資訊。可以使用下列任一方式提供此資訊:
- 提供對儲存 HelloService 類的程式集的引用。
- 將遠程對象拆分為實現和介面類,並在編譯用戶端時將介面用作引用。
- 使用 SOAPSUDS 工具直接從端點提取所需的中繼資料。此工具串連到所提供的端點、提取中繼資料並產生可用來編譯用戶端的程式集或原始碼。
GetObject 或 new 可用於伺服器啟用。注意到當進行其中任何一個調用時並沒有執行個體化遠程對象這一點很重要。事實上根本沒有產生任何網路調用。架構從中繼資料擷取足夠的資訊以創 建代理,而根本不必串連到遠程對象。網路連接只在用戶端調用代理上的方法時建立。當調用到達伺服器時,架構將從訊息中提取 URI,檢查遠端架構表以尋找匹配該 URI 的對象的引用,然後在必要時執行個體化該對象並將方法調用轉寄到該對象。如果對象註冊為 SingleCall,則它將在方法調用完成後被銷毀。為所調用的每個方法建立對象的新執行個體。GetObject 與 new 之間的唯一差異是,前者允許將 URL 指定為參數,而後者是從配置中擷取 URL。
CreateInstance 或 new 可用於用戶端啟用的對象。兩者都可使用帶有參數的建構函式執行個體化對象。當用戶端試圖啟用用戶端啟用的對象時,啟用請求被發送到伺服器。可以通過遠端架構提供的租用服務來控制用戶端啟用的對象的生存期。對象租用將在以下章節中描述。
使用租用的物件存留期
每個應用程式定義域都包含負責管理該域中租約的租約管理器。系統會定期查看所有租約以確定到期租約次數。如果租約已到期,則將調用該租約的一個或多個主 辦方,並為它們提供續訂租約的機會。如果這些主辦方都決定不續訂租約,則租約管理器將租約移除並對該對象進行記憶體回收。租約管理器維護租約列表,並將租約 按剩餘租用時間排序。具有最短剩餘時間的租約儲存在列表的頂部。
租約實現 ILease 介面並儲存用於確定要更新的策略和方法的屬性集合。可以在調用時續訂租約。每當在遠程對象上調用方法時,租用時間都被設定為當前 LeaseTime 的最大值與 RenewOnCallTime 之和。當 LeaseTime 到期後,主辦方將被要求續訂租約。因為必須經常處理不可靠的網路,所以可能會出現租約主辦方停用情況,並且為了確保不在伺服器上留下處於僵停狀態的對象,每個租約都具有 SponsorshipTimeout。此值指定在終止租約之前等待主辦方回覆的時間。如果 SponsershipTimeout 為空白,則將使用 CurrentLeaseTime 確定租約何時到期。如果 CurrentLeaseTime 的值為 0,則租約將不會到期。可使用配置或 API 來重寫 InitialLeaseTime、SponsorshipTimeout 和 RenewOnCallTime 的預設值。
租約管理器維護按主辦時間遞減的順序儲存的主辦方(它們實現 ISponsor 介面)列表。當需要主辦方續訂租用時間時,將要求一個或多個位於列表頂部的主辦方續訂時間。列表頂部表示先前要求最大租約續訂時間的主辦方。如果主辦方在 SponsorshipTimeOut 時間範圍內沒有響應,則將其從列表中移除。可以通過調用 GetLifetimeService 來獲得對象租約,將需要租約的對象作為參數傳遞。此調用是 RemotingServices 類的靜態方法。如果對象對於應用程式定義域是本地的,則此調用的參數是對象的本地引用,並且返回的租約是租約的本地引用。如果對象是遠端,則將代理作為參數傳遞,並將租約的透明代理返回到調用方。
對象可以提供它們自己的租約,從而控制它們自己的生存期。它們通過重寫 MarshalByRefObject 上的 InitializeLifetimeService 方法做到這一點,如下所示:
public class Foo : MarshalByRefObject { public override Object InitializeLifetimeService() { ILease lease = (ILease)base.InitializeLifetimeService(); if (lease.CurrentState == LeaseState.Initial) { lease.InitialLeaseTime = TimeSpan.FromMinutes(1); lease.SponsorshipTimeout = TimeSpan.FromMinutes(2); lease.RenewOnCallTime = TimeSpan.FromSeconds(2); } return lease; } }
只可以在租約處於初始狀態時更改租約屬性。InitializeLifetimeService 的實現通常調用基類的相應方法來檢索遠程對象的現有租約。如果以前從未封送該對象,則返回的租約將處於其初始狀態並且可以設定租約屬性。對象一旦被封送,租約將從初始狀態變為活動狀態,初始化租約屬性的任何嘗試都將被忽略(引發異常)。在啟用遠程對象時調用 InitializeLifetimeService。可以用啟用調用提供租約的主辦方列表,並且可以在租約處於活動狀態的任何時候添加附加的主辦方。
可以延長租用時間,如下所示:
- 用戶端可以調用 Lease 類上的 Renew 方法。
- 租約可以從主辦方請求 Renewal。
- 當用戶端調用對象上的方法時,租約由 RenewOnCall 值自動續訂。
當租約到期時,它的內部狀態將從“活動”變為“到期”,不再進一步調用主辦方,並且將對對象進行記憶體回收。因為如果在 Web 上或在防火牆後面部署主辦方,則遠程對象執行主辦方上的回調操作經常是困難的,所以主辦方不必與用戶端位於同一位置。它可以位於遠程對象可以訪問的任一部 分網路。
使用租約來管理遠程對象的生存期是引用計數的替換方法,引用計數對於不可靠的網路連接通常是複雜和低效的。儘管有人會爭論說可以將遠程對象的生存期延長得比所需的長一些,但是,用於引用計數和 Ping 用戶端的網路通訊量的減少仍使租用成為一個十分有吸引力的解決方案。
結束語
提供滿足大多數商務應用程式需求的完美的遠端架構就算能夠實現,想必也是項艱巨的任務。通過提供可以根據需要擴充和自訂的架構,Microsoft 已經朝正確的方向邁出了關鍵的一步。
附錄 A:使用 TCP 通道的遠端樣本
本附錄顯示如何編寫簡單的“Hello World”遠程應用程式。用戶端將一個 String 傳遞到遠程對象,遠程對象將單詞“Hi There”追加到該字串的末尾,並且將結果返回到用戶端。要修改此樣本以使用 HTTP 而不是 TCP,僅需將源檔案中的 TCP 替換為 HTTP。
將這段代碼儲存為 server.cs:
using System; using System.Runtime.Remoting; using System.Runtime.Remoting.Channels; using System.Runtime.Remoting.Channels.Tcp; namespace RemotingSamples { public class Sample { public static int Main(string [] args) { TcpChannel chan = new TcpChannel(8085); ChannelServices.RegisterChannel(chan); RemotingConfiguration.RegisterWellKnownServiceType (Type.GetType("RemotingSamples.HelloServer,object"), "SayHello", WellKnownObjectMode.SingleCall); System.Console.WriteLine("Hit <enter> to exit..."); System.Console.ReadLine(); return 0; } } }
將這段代碼儲存為 client.cs:
using System; using System.Runtime.Remoting; using System.Runtime.Remoting.Channels; using System.Runtime.Remoting.Channels.Tcp; namespace RemotingSamples { public class Client { public static int Main(string [] args) { TcpChannel chan = new TcpChannel(); ChannelServices.RegisterChannel(chan); HelloServer obj = (HelloServer)Activator.GetObject(typeof(RemotingSamples.HelloServer) , "tcp://localhost:8085/SayHello"); if (obj == null) System.Console.WriteLine("Could not locate server"); else Console.WriteLine(obj.HelloMethod("Caveman")); return 0; } } }
將這段代碼儲存為 object.cs:
using System; using System.Runtime.Remoting; using System.Runtime.Remoting.Channels; using System.Runtime.Remoting.Channels.Tcp; namespace RemotingSamples { public class HelloServer : MarshalByRefObject { public HelloServer() { Console.WriteLine("HelloServer activated"); } public String HelloMethod(String name) { Console.WriteLine("Hello.HelloMethod : {0}", name); return "Hi there " + name; } } }
此為組建檔案:
all: object.dll server.exe client.exe object.dll: share.cs csc /debug+ /target:library /out:object.dll object.cs server.exe: server.cs csc /debug+ /r:object.dll /r:System.Runtime.Remoting.dll server.cs client.exe: client.cs server.exe csc /debug+ /r:object.dll /r:server.exe /r:System.Runtime.Remoting.dll client.cs