.Net远程方法调用研究

  1. 简介

远程方法调用发展到现在,已经有以下几种框架实现: DCE/RPC , CORBA , DCOM , MTS/COM+ , Java RMI , Java EJB , Web Services/SOAP/XML-RPC , NET Remoting ,本文主要介绍了 .NET 远程方法调用的原理,实现以及与微软 COM/DCOM 实现的异同点。

  1. 框架

Microsoft .NET Remoting 提供了一种允许对象通过应用程序域与另一对象进行交互的框架。众所周知, Web 服务仅仅提供了一种简单的容易理解的方法来实现跨平台,跨语言的交互,而 DotNet Remoting 相对于 Web 服务就像 Asp 相对于 CGI 那样,实现了一种质的转变。 DotNet Remoting 提供了一个可扩展的框架,它可以选择不同的传输机制( HTTP 和 TCP 是内置的),不同的编码方式( SOAP 以及二进制代码),安全设置( IIS 或 SSL ),同时提供了多种服务,包括激活和生存期支持。

  1. 远程方法调用

对于远程方法调用来说,最直接的问题恐怕是:一个本地方法,推而广之,一个本地对象,如果放在网络环境中,如何传递这个方法的调用,返回,如何传递这个对象的请求。虽然对于应用开发人员来说这个并不是必不可少的事,但对于我们学习分布式操作系统来说,我想这是应该搞清楚的。我们知道, 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 上进行互操作的类型都可以使用。但是,这也确实使得某些数据类型不能使用。

  1. 代理

代理对象伪装成一个远程对象,并且向本地提供远程对象相同的接口。客户端应用程序与处理远程“真实”对象的应用(包括内存引用,指针,等等),都是通过代理对象实现的。代理对象当然并不自己完成方法调用,它将请求作为消息对象传给 .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");

  1. 消息

当一次函数调用指向远程对象的引用时, 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

1<soap-env:envelope soap-env:encodingstyle="http://schemas.xmlsoap.org/soap/encoding/" xmlns:i2="http://schemas.microsoft.com/clr/nsassem/ **General.BaseRemoteObject** / **General** " xmlns:soap-enc="http://schemas.xmlsoap.org/soap/encoding/" xmlns:soap-env="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
2<soap-env:body>
3<i2: **setvalue**="" id="ref-1">
4
5** <newval>42</newval> **
6
7
8</i2:></soap-env:body>
9</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 输出

 1<soap-env:envelope soap-env:encodingstyle="http://schemas.xmlsoap.org/soap/encoding/" xmlns:i2="http://schemas.microsoft.com/clr/nsassem/General.BaseRemoteObj *** 
 2
 3ect/General" xmlns:soap-enc="http://schemas.xmlsoap.org/soap/encoding/" xmlns:soap-env="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
 4<soap-env:body>
 5
 6** <i2:setvalue id="ref-1"> **
 7
 8** <newval>42</newval> **
 9
10** </i2:setvalue> **
11
12</soap-env:body>
13</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

}

}

}

  1. 小结

以上就是我几天来对 .NET Remoting 的学习所得。我的学习主要集中在 .NET Remoting 的远程方法传递这方面,因为我觉得这才是 RPC 最关键的技术。从几天的学习所得,可以看出 .NET FrameWork 的确是一种技术,思想都非常先进的框架。我唯一的怀疑在于微软所谓的平台无关性,这方面, JAVA 也许永远胜出一筹。

这篇文章 0.1 版放在我的 blog 上 http://blog.csdn.net/drizt/

参考文献:

1 . Advanced .NET Remoting (C# Edition) by Ingo Rammer Apress 2002

2 . COM 原理与应用 by 潘爱民 清华大学出版社

3 . .NET 体系架构评估 _ http://blog.sunmast.com/sunmast/articles/224.aspx _

4 . Inside .Net Remoting architecture _ http://www.cnblogs.com/rickie/archive/2004/10/22/55292.html _

5 . Microsoft .NET Remoting :技术概述 http://www.csdn.com.cn/program/4226.htm

Published At
Categories with Web编程
Tagged with
comments powered by Disqus