简介
远程方法调用发展到现在,已经有以下几种框架实现: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/general#
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/general#
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/general">
<soap-env:body>
<i2:setvalue id="ref-1">
<newval>42</newval>
</i2:setvalue>
</soap-env:body>
</soap-env:envelope>
看起来是多么美妙的形式啊,完全摆脱了分析rpc时的痛苦,不过我始终还是有疑问,微软如果始终坚持windows操作系统一统的路线,.net的真正跨平台性是否真正能实现。
一种常见的 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.baseremoteobj ***
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
}
}
}
小结
以上就是我几天来对.net remoting的学习所得。我的学习主要集中在.net remoting的远程方法传递这方面,因为我觉得这才是rpc最关键的技术。从几天的学习所得,可以看出.net framework的确是一种技术,思想都非常先进的框架。我唯一的怀疑在于微软所谓的平台无关性,这方面,java也许永远胜出一筹。
这篇文章0.1版放在我的blog上 http://blog.csdn.net/drizt/