簡介
遠程方法調用發展到現在,已經有以下幾種框架實現:DCE/RPC,CORBA, DCOM,MTS/COM+,Java RMI,Java EJB,Web Services/SOAP/XML-RPC,NET Remoting,本文主要介紹了.NET遠程方法調用的原理,實現以及與微軟COM/DCOM 實現的異同點。
框架
Microsoft .NET Remoting 提供了一種允許對象通過應用程序域與另一對象 進行交互的框架。眾所周知,Web服務僅僅提供了一種簡單的容易理解的方法來 實現跨平台,跨語言的交互,而DotNet Remoting相對於Web服務就像Asp相對於 CGI那樣,實現了一種質的轉變。DotNet Remoting提供了一個可擴展的框架,它 可以選擇不同的傳輸機制(HTTP和TCP是內置的),不同的編碼方式(SOAP以及 二進制代碼),安全設置(IIS或SSL),同時提供了多種服務,包括激活和生存 期支持。
遠程方法調用
對於遠程方法調用來說,最直接的問題恐怕是:一個本地方法,推而廣之, 一個本地對象,如果放在網絡環境中,如何傳遞這個方法的調用,返回,如何傳 遞這個對象的請求。雖然對於應用開發人員來說這個並不是必不可少的事,但對 於我們學習分布式操作系統來說,我想這是應該搞清楚的。我們知道,DCOM協議 也被稱為對象RPC,它建立在DCE RPC協議基礎上,也就是說,在網絡傳輸這一層 ,它必須使用特殊的協議。另外Windows RPC 機制要求熟悉的類型和使用 IDL 工具的封送處理知識,並向開發人員公開 RPC 客戶端和服務器存根的管理。 Remoting 在為 .NET 提供 RPC 時要容易得多,而且由於使用簡單易懂的 .NET 數據類型,從而消除了早期 RPC 機制中存在的類型不匹配的情況(這是一個非 常大的威脅)。配置為使用 HTTP 或 TCP 協議,並使用 XML 編碼的 SOAP 或本 機二進制消息格式進行通信。開發人員可以構建自定義的協議(通道)或消息格 式(格式化程序),並在需要時由 Remoting 框架使用。服務器和客戶端組件都 可以選擇端口,就象可以選擇通信協議一樣。由此帶來的一個好處是,很容易建 立並運行基本的通信。
下面描述了.Net Remoting的五要素:
代理:在Client端偽裝為Remote Objects並轉發對Remote Objects的調用。
Message:消息對象包含了執行Remote Methods調用的必要數據參數。
Message Sink/Channel Sink:在Remote調用中,Message Sink允許定制消息 處理流程,這是.Net Remoting內置的可擴展特性。
Formatter:也是Message Sink,用來序列化消息,已適於網絡傳輸,如SOAP 。
Transport Channel:也是Message Sink,用來傳輸序列化的消息到遠程進程 ,如HTTP。
當訪問Remote Objects時,Client端application並不處理真實對象的引用, 而是僅僅調用Proxy對象的方法。Proxy對象提供與Remote Objects相同的接口, 偽裝成Remote Objects。Proxy對象自己並不執行任何方法,而是以消息對象 (Message Object)的形式轉發每一個方法調用給.Net Remoting Framework。
在類型支持方面,DCOM提供了一套復雜的列集和散集機制,他建立在RPC的基 礎上。由於RPC被定義為DCE標准的一部分,而DCE RPC定義了所有常用的數據類 型的數據表達方法,即網絡數據表示法。為了使存根(stub)代碼和代理對象能 夠正確地對參數和返回結果也進行列集和散集,它們應該使用一致的數據表示法 NDR,以便在不同的操作系統環境下也能夠遠程調用。
This figure is from the book named Advanced .Net Remoting.
反過來,我們再看看.NET Remoting 強大的類型操作,.Net Remoting 支持 所有托管的類型、類、接口、枚舉、對象等,這通常被稱為“多類型保真”。這 裡的關鍵在於,如果客戶端和服務器組件都是在應用程序域中運行的 CLR 托管 的對象,則數據類型的互操作是不成問題的。從根本上講,我們擁有的是一個封 閉的系統,會話的兩端可以完全被理解,因此我們可以充分利用這一事實,處理 好用於通信的數據類型和對象。
在各種系統並存的情況下,我們需要考慮系統之間的互操作性。對於可互操 作的數據類型,我們要謹慎處理。例如,Web 服務數據類型的定義要基於 XML 架構定義 (XSD) 關於數據類型的說明。任何可以使用 XSD 進行描述並可以在 SOAP 上進行互操作的類型都可以使用。但是,這也確實使得某些數據類型不能 使用。
代理
代理對象偽裝成一個遠程對象,並且向本地提供遠程對象相同的接口。客戶 端應用程序與處理遠程“真實”對象的應用(包括內存引用,指針,等等),都 是通過代理對象實現的。代理對象當然並不自己完成方法調用,它將請求作為消 息對象傳給.NET Framework。在這一方面,.Net Remoting 和DCOM思想上是一致 的。當某個客戶端激活一個遠程對象時,框架將創建 TransparentProxy 類的一 個本地實例(該類中包含所有類的列表與遠程對象的接口方法)。因為 TransparentProxy 類在創建時用 CLR 注冊,所以代理上的所有方法調用都被運 行時截取。這時系統將檢查調用,以確定其是否為遠程對象的有效調用,以及遠 程對象的實例是否與代理位於同一應用程序域中。如果對象在同一個應用程序域 中,則簡單方法調用將被路由到實際對象;如果對象位於不同的應用程序域中, 將通過調用堆棧中的調用參數的 Invoke 方法將其打包到 IMessage 對象並轉發 到 RealProxy 類中。此類(或其內部實現)負責向遠程對象轉發消息。 TransparentProxy 類和 RealProxy 類都是在遠程對象被激活後在後台創建的, 但只有 TransparentProxy 返回到客戶端。在代理對象創建時,需要引用Client 端的Messag Sink Chain,Sink Chain的第一個Sink對象的引用保存在RealProxy 對象的Identity屬性。所以,在下面這段代碼調用new或者GetObject獲得對象後 ,對象obj將會指向
TransparentProxy對象。
HttpChannel channel = new HttpChannel();
ChannelServices.RegisterChannel(channel);
SomeClass obj = (SomeClass) Activator.GetObject(
type of(SomeClass),
"http://localhost:1234/SomeSAO.soap");
消息
當一次函數調用指向遠程對象的引用時,TransparentProxy創建一個 MessageData對象並且將它傳給RealProxy對象的PrivateInvoke()調用, RealProxy相應地生成一個新的Message對象並且以MessageData對象為參數調用 其InitFields()方法,Message將會解析MessageData對象中地指針進行相應地函 數調用。當處理結束時,將會返回一個包含回應消息的IMessage對象, RealProxy將會調用它自己的HandleReturnMessage()方法,該方法檢查參數並且 調用PropagateOutParameters()方法。當處理結束後RealProxy將會從它的 PrivateInvoke()方法中返回,IMessage將會返回給TransparentProxy。CLR保 證函數返回值與其返回格式相符,所以對於應用程序來說,它僅僅認為這是一個 一般方法的調用,返回,這正是分布式的最終要求。
可以看到,.NET Remoting與DCOM的底層機制已經完全不同了,DCOM的一次方 法調用,需要經過列集(marshaling)和散集(unmarshaling)的處理,包括創建代 理對象和轉載存根代碼等。但是可以看到.NET做為DCOM的下一代,在基本思想上 和DCOM是一脈相承的。在我學習到現在的體會看來,.NET面向Web服務的RPC機制 的確是一大革新,讓我們看看IMessage怎麼進行傳遞的:
這是一個消息穿過Transport Channel(也就是Message Sink)的實例。
首先是SOAP遠程調用的HTTP請求:
POST /MyRemoteObject.soap HTTP/1.1
User-Agent: Mozilla/4.0+(compatible; MSIE 6.0; Windows 5.0.2195.0; MS .NET
Remoting;
MS .NET CLR 1.0.2914.16 )
SOAPAction:
"http://schemas.microsoft.com/clr/nsassem/General.BaseRemoteObject/Gen eral#
setValue"
Content-Type: text/xml; charset="utf-8"
Content-Length: 510
Expect: 100-continue
Connection: Keep-Alive
Host: localhost
然後是一個對於一個遠程對象setValue(int):
POST /MyRemoteObject.soap HTTP/1.1
User-Agent: Mozilla/4.0+(compatible; MSIE 6.0; Windows 5.0.2195.0; MS .NET
Remoting;
MS .NET CLR 1.0.2914.16 )
SOAPAction:
"http://schemas.microsoft.com/clr/nsassem/General.BaseRemoteObject/Gen eral#
setValue"
Content-Type: text/xml; charset="utf-8"
Content-Length: 510
Expect: 100-continue
Connection: Keep-Alive
Host: localhost
<SOAP-ENV:Envelope
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/"
xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"
SOAP- ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"
xmlns:i2=
"http://schemas.microsoft.com/clr/nsassem/General.BaseRemoteObject/Gen eral">
<SOAP-ENV:Body>
<i2:setValue id="ref-1">
<newval>42</newval>
</i2:setValue>
</SOAP-ENV:Body>
</SOAP-ENV:Envelope>
一種常見的 Microsoft 理論是:如果需要在不同系統之間進行互操作,應該 選擇使用開放標准 (SOAP、XML、HTTP) 的 Web 服務方法,而使用 .NET Remoting 決不是一種交互的解決方案;如果各種系統中的所有組件都是 CLR 托 管的,則 .NET Remoting“可能”是正確的選擇。
我想我還漏了一個重要的問題:串行化(Serialization)。這是實現跨進程 調用的關鍵技術。在COM中串行化使通過列集(marshaling)和散集 (unmarshaling)完成的,列集過程的復雜程度因參數類型而異,簡單的入WORD 或float只需按二進制序列填到數據包中,復雜的參數如指針需要考慮整個指針 層次,獲得所有的數據全部。散集是和列集向對應的過程,遠程機器的存根代碼 接受到列集數據後,重新建立堆棧的過程就是散集,每次遠程調用至少經過兩次 列集和兩次散集,因為返回值也需要列集回來再散集。COM的一個問題就是在於 其過於復雜的的參數傳遞問題這一套復雜的機制是很難搞明白,更何況對於代碼 的定制了。.NET Remoting對於串行化的處理是通過一個formatter來完成 的,.NET Remoting框架給你提供了兩個缺省的formatters,SoapFormatter和 BinaryFormatter,它們都能通過HTTP或TCP進行傳輸。在消息完成了 ImessageSink對象的預處理鏈後,它將通過SyncProcessMessage()到達 formatter。在客戶端,SoapClientFormatterSink將IMessage傳給 SerializeMessage()方法。這個函數設置TransportHeaders,請求它的NextSink (HttpClientTransportSink ),它將創建一個ChunkedMemoryStream並且傳給 channel sink.。真正的串行化由CoreChannel.SerializeSoapMessage()開始, 它建立一個SoapFormatter,並且調用其Serialize()方法,下面是一個對於 obj.setValue(42)方法的SOAP輸出
<SOAP-ENV:Envelope
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/"
xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"
SOAP- ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"
xmlns:i2="http://schemas.microsoft.com/clr/nsassem/General.BaseRemoteO bj ***
ect/General">
<SOAP-ENV:Body>
<i2:setValue id="ref-1">
<newval>42</newval>
</i2:setValue>
</SOAP-ENV:Body>
</SOAP-ENV:Envelope>
這種方式與COM比較更加清晰,易懂,並且也確實易於開發,例如我們可以實 現一個壓縮的Sink。下面是開發一個CompressionSink的骨架例程,從這來看, COM實在是望塵莫及。
using System;
using System.Runtime.Remoting.Channels;
using System.Runtime.Remoting.Messaging;
using System.IO;
namespace CompressionSink
{
public class CompressionClientSink: BaseChannelSinkWithProperties,
IClientChannelSink
{ private IClientChannelSink _nextSink;
public CompressionClientSink(IClientChannelSink next)
{ _nextSink = next;
}
public IClientChannelSink NextChannelSink
{ get {
return _nextSink;
}
}
public void AsyncProcessRequest(IClientChannelSinkStack sinkStack,
IMessage msg,
ITransportHeaders headers,
Stream stream)
{
// TODO: Implement the pre-processing
sinkStack.Push(this,null);
_nextSink.AsyncProcessRequest(sinkStack,msg,headers,stream);
}
public void AsyncProcessResponse(IClientResponseChannelSinkStack sinkStack,
object state,
ITransportHeaders headers,
Stream stream)
{
// TODO: Implement the post-processing
sinkStack.AsyncProcessResponse(headers,stream);
}
public Stream GetRequestStream(IMessage msg,
ITransportHeaders headers)
{ return _nextSink.GetRequestStream(msg, headers);
}
public void ProcessMessage(IMessage msg,
ITransportHeaders requestHeaders,
Stream requestStream,
out ITransportHeaders responseHeaders,
out Stream responseStream)
{
// TODO: Implement the pre-processing
_nextSink.ProcessMessage(msg,
requestHeaders,
requestStream,
out responseHeaders,
out responseStream);
// TODO: Implement the post-processing
}
}
}