Microsoft .NET Remoting:技术概述
Piet Obermeyer 和 Jonathan Hawkins
Microsoft Corporation
摘要:本文提供了 Microsoft .NET Remoting 框架的技术概述,其中包括了使用 TCP 通道或 HTTP 通道的示例。
目录
简介
远程对象
代理对象
通道
激活
对象的租用生存期
总结
附录 A:使用 TCP 通道进行远程处理的示例
附录 B:使用 HTTP 通道进行远程处理的示例
简介
Microsoft® .NET Remoting 提供了一种允许对象通过应用程序域与另一对象进行交互的框架。这种框架提供了多种服务,包括激活和生存期支持,以及负责与远程应用程序进行消息传输的通讯通道。格式化程序用于在消息通过通道传输之前,对其进行编码和解码。应用程序可以在注重性能的场合使用二进制编码,在需要与其他远程处理框架进行交互的场合使用 XML 编码。在从一个应用程序域向另一个应用程序域传输消息时,所有的 XML 编码都使用 SOAP 协议。出于安全性方面的考虑,远程处理提供了大量挂钩,使得在消息流通过通道进行传输之前,安全接收器能够访问消息和序列化流。
通常,如果没有底层框架的支持,管理远程对象的生存期会非常麻烦。.NET Remoting 提供了许多可供选择的生存期模型,这些模型分为两个类别:
- 客户端激活对象
- 服务器激活对象
客户端激活对象受基于租用的生存期管理器的控制,这种管理器确保了租用期满时对象可被回收。而对于服务器激活对象,开发人员则可以选择“单一调用”模式或“单一元素”模式。
远程对象
任何远程处理框架的主要目的之一就是要提供必要的基础结构,以便隐藏远程对象调用方法和返回结果的复杂性。任何位于调用方应用程序域之外的对象,即使在同一台计算机上执行,也会被认为是远程对象。在应用程序域内部,原始数据类型按数值传递,而所有的对象按引用传递。因为本地对象引用仅在创建对象的应用程序域内有效,所以它们不能以这种方式传递到远程方法调用或从远程方法调用返回。所有必须跨越应用程序域的本地对象都必须按数值来传递,并且应该用 [serializable] 自定义属性作标记,否则它们必须实现 ISerializable 接口。对象作为参数传递时,框架将该对象序列化并传输到目标应用程序域,对象将在该目标应用程序域中被重新构造。无法序列化的本地对象将不能传递到其他应用程序域中,因而也不能远程处理。
通过从 MarshalByRefObject 导出对象,您可以使任一对象变为远程对象。当某个客户端激活一个远程对象时,它将接收到该远程对象的代理。对该代理的所有操作都被适当地重新定向,使远程处理基础结构能够正确截取和转发调用。尽管这种重新定向对性能有一些影响,但 JIT 编译器和执行引擎 (EE) 已经优化,可以在代理和远程对象驻留在同一个应用程序域中时,防止不必要的性能损失。如果代理和远程对象不在同一个应用程序域中,则堆栈中的所有方法调用参数会被转换为消息并被传输到远程应用程序域,这些消息将在该远程应用程序域中被转换为原来的堆栈帧,同时该方法调用也会被调用。从方法调用中返回结果时也使用同一过程。
代理对象
代理对象是在客户端激活远程对象时创建的。作为远程对象的代表,代理对象确保对代理进行的所有调用都能够转发到正确的远程对象实例。为了准确理解代理对象的工作方式,我们需要更深入地研究它们。当某个客户端激活一个远程对象时,框架将创建 TransparentProxy 类的一个本地实例(该类中包含所有类的列表与远程对象的接口方法)。因为 TransparentProxy 类在创建时用 CLR 注册,所以代理上的所有方法调用都被运行时截取。这时系统将检查调用,以确定其是否为远程对象的有效调用,以及远程对象的实例是否与代理位于同一应用程序域中。如果对象在同一个应用程序域中,则简单方法调用将被路由到实际对象;如果对象位于不同的应用程序域中,将通过调用堆栈中的调用参数的 Invoke 方法将其打包到 IMessage 对象并转发到 RealProxy 类中。此类(或其内部实现)负责向远程对象转发消息。TransparentProxy 类和 RealProxy 类都是在远程对象被激活后在后台创建的,但只有 TransparentProxy 返回到客户端。
要更好地理解这些代理对象,我们需要简要介绍一下 ObjRef。激活一节中有关于 ObjRef 的详细说明。以下方案简要说明了 ObjRef 与这两个代理类的关联方式。但请注意,这只是关于该进程的一个极其概括的说明;根据对象是客户端激活对象还是服务器激活对象,以及它们是单一元素对象还是单一调用对象,该进程会有所不同。
- 远程对象注册在远程计算机的应用程序域中。远程对象被封送以生成 ObjRef。ObjRef 包含了从网络上的任意位置定位和访问远程对象所需的所有信息,包括:类的增强名称、类的层次结构(其父类)、类实现的所有接口的名称、对象 URI 和所有已注册的可用通道的详细信息。在接收到对某个远程对象的请求时,远程处理框架使用对象 URI 来检索为该对象创建的 ObjRef 实例。
- 客户端通过调用 new 或某个 Activator 函数(例如 CreateInstance)来激活远程对象。对于服务器激活对象,远程对象的 TransparentProxy 将在客户端应用程序域中生成并返回到客户端,这时不执行任何远程调用。只有在客户端调用远程对象的某个方法时,该远程对象才会被激活。此方案明显不适合客户端激活对象,因为客户端希望框架只在得到请求时才激活对象。当客户端调用某个激活方法时,客户端上会创建一个激活代理,并且将使用 URL 和对象 URI 作为终结点在服务器的远程激活器上初始化一个远程调用。远程激活器激活该对象,然后 ObjRef 流向客户端,并被取消封送以生成一个返回给客户端的 TransparentProxy。
- 取消封送的过程中会分析 ObjRef 以提取远程对象的方法信息,同时还会创建 TransparentProxy 和 RealProxy 对象。在用 CLR 注册 TransparentProxy 之前,分析后的 ObjRef 内容会被添加到 TransparentProxy 的内部表中。
TransparentProxy 是一种无法替代和扩展的内部类,而 RealProxy 和 ObjRef 类则属于公共类,可以在必要时进行扩展和自定义。因为 RealProxy 类能够处理远程对象的所有函数调用,所以它是执行负载平衡等操作的理想方法。调用 Invoke 时,从 RealProxy 导出的类可以获得网络中服务器的负载信息,并将该调用路由到适当的服务器。简单地为所需的 ObjectURI 从通道请求一个 MessageSink,并调用 SyncProcessMessage 或 AsyncProcessMessage 以将该调用转发至所需的远程对象。当调用返回时,通过调用 RemotingServices 类的 PropagateMessageToProxy 将返回参数推回到堆栈中。
下面的代码片断显示了如何使用导出的 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 将消息转发到消息接收器以进行处理。消息接收器负责与远程对象注册的通道之间建立连接,并通过通道(在不同的应用程序域)将消息从调度位置传输到远程对象本身。激活了一个远程对象后,客户端会通过调用选定通道上的 CreateMessageSink 来选择通道,并从其上检索能够与远程对象通讯的消息接收器。
远程处理框架的一个容易混淆的方面是远程对象和通道之间的关系。例如,如果 SingleCall 远程对象只在被调用时才激活,那么该对象如何侦听要连接的客户端?
部分答案在于这样一个事实:远程对象并不拥有自己的通道,而是共享通道。作为远程对象宿主的服务器应用程序必须注册要通过远程处理框架公开的对象以及所需的通道。注册后的通道会自动开始在指定的端口侦听客户请求。注册远程对象后,会为该对象创建一个 ObjRef 并将其存储在表中。当通道上传来一个请求时,远程处理框架会检查该消息以确定目标对象,同时检查对象引用表以定位表中的引用。如果找到了对象引用,将从表中检索框架目标对象或在必要时将其激活,然后框架将调用转发至该对象。对于同步调用,在消息调用期间会一直维持来自客户端的连接。因为每个客户端连接都在自己的线程上处理,所以一个通道可以同时服务于多个客户端。
生成商务应用时,安全性是一个重要问题。要满足商务要求,开发人员必须能给远程方法调用添加诸如授权或加密等安全特性。为了实现这一目标,开发人员可以自定义通道,使其能够对与远程对象之间的实际消息传输机制进行控制。在传输到远程应用程序之前,所有的消息都必须流过 SecuritySink、TransportSink 和 FormatterSink,且这些消息传递到远程应用程序后会以相反次序流过同样的接收器。
HTTP 通道
HTTP 通道使用 SOAP 协议与远程对象传输消息。所有的消息流过 SOAP 格式化程序时都被转换为 XML 格式且被序列化,所需的 SOAP 头也会被添加到该流中。您也可以指定能够生成二进制数据流的二进制格式化程序。然后,数据流会使用 HTTP 协议传输到目标 URI。
TCP 通道
TCP 通道使用二进制格式化程序将所有的消息序列化为二进制流,并使用 TCP 协议将其传输到目标 URI。
激活
远程处理框架支持远程对象的服务器激活和客户端激活。不需要远程对象在方法调用之间维护任何状态时,一般使用服务器激活。服务器激活也适用于多个客户端调用方法位于同一对象实例上、且对象在函数调用之间维持状态的情况。另一方面,客户端激活对象从客户端实例化,并且客户端通过使用基于租用的专用系统来管理远程对象的生存期。
在可以接受客户端的访问之前,所有的远程对象都必须用远程处理框架注册。对象注册一般由宿主应用程序来完成。宿主应用程序将启动,使用 ChannelServices 注册一个或多个通道,使用 RemotingServices 注册一个或多个远程对象,然后等待被终止。请注意,已注册的通道和对象只有在用来注册它们的进程活动时才可以使用。如果退出了该进程,则会自动从远程处理服务中删除它注册的所有通道和对象。在框架中注册远程对象时,需要以下四项信息:
- 包含类的程序集名称。
- 远程对象的类型名称。
- 客户端定位对象时将使用的对象 URI。
- 服务器激活所需的对象模式。该模式可以是 SingleCall,也可以是 Singleton。
远程对象可以通过下列两种方式注册:调用 RegisterWellKnownType,将上述信息作为参数传递;或将上述信息存储在配置文件中,然后调用 ConfigureRemoting 并将该配置文件的名称作为参数传递。以上两种方法执行的功能相同,因此您可以使用它们中的任意一种来注册远程对象。当然,后一种方法更方便些,因为无需重新编译宿主应用程序即可改变配置文件的内容。以下代码片断显示了如何将 HelloService 类注册为 SingleCall 远程对象。
RemotingServices.RegisterWellKnownType(
"server",
"Samples.HelloServer",
"SayHello",
WellKnownObjectMode.SingleCall);
其中,“server”
是程序集的名称,HelloServer 是类的名称,SayHello 是对象 URI。
注册了远程对象后,框架将为该对象创建一个对象引用,然后从程序集中提取与该对象相关的必要元数据。随后,这一信息将与 URI 和程序集名称一起存储在对象引用中(该对象引用将被写入一个用于跟踪已注册远程对象的远程处理框架表中)。请注意,除了在客户端试图调用对象上的某个方法或从客户端激活对象时以外,注册进程不会实例化远程对象自身。
现在,任何知道该对象 URI 的客户端都可以使用 ChannelServices 注册通道,并调用 new、GetObject 或 CreateInstance 激活对象,从而获得该对象的一个代理。以下代码片断显示了该操作的示例:
ChannelServices.RegisterChannel(new TCPChannel);
HelloServer obj = (HelloServer)Activator.GetObject(
typeof(Samples.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 的值为零,则租用不会过期。配置或 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 值会自动更新租用。
一旦租用过期,其内部状态会由 Active 变为 Expired,且不再对发起者进行任何调用,对象也会被作为垃圾回收。一般情况下,如果发起者分散在 Web 上或位于某个防火墙的后面,远程对象回叫发起者时会遇到困难。因此,发起者不必与客户端处于同一位置,只要远程对象能够访问得到,它可以为网络上的任意位置。
通过租用来管理远程对象的生存期可以作为引用计数的一种替代方法,因为当网络连接的性能不可靠时,引用计数会显得复杂和低效。尽管有人会坚持认为远程对象的生存期比所需的时间要长,但与引用计数和连接客户相比,租用降低了网络的繁忙程度,将会成为一种非常受欢迎的解决方案。
总结
要提供完美的、能够满足大多数商务应用需求的远程处理框架,即使能够做到,也必然会非常困难。Microsoft 提供了能够根据需要进行扩展和自定义的框架,在正确的方向上迈出了关键的一步。
附录 A:使用 TCP 通道进行远程处理的示例
此附录显示了如何编写简单的“Hello World”远程应用程序。客户端将一个字符串传递到远程对象上,该远程对象将单词“Hi There”附加到字符串上,并将结果返回到客户端。
将此文件保存为 server.cs。此处为服务器的代码:
using System;
using System.Runtime.Remoting;
using System.Runtime.Remoting.Channels.TCP;
namespace RemotingSamples {
public class HelloServer : IHello {
public static int Main(string [] args) {
TCPChannel chan = new TCPChannel(8085);
ChannelServices.RegisterChannel(chan);
RemotingServices.RegisterWellKnownType(
"server", "RemotingSamples.HelloServer", "SayHello",
WellKnownObjectMode.SingleCall);
System.Console.WriteLine("请按 <enter> 键退出...");
System.Console.ReadLine();
return 0;
}
public HelloServer()
{
Console.WriteLine("HelloServer 已激活");
}
~HelloServer()
{
Console.WriteLine("对象已清除");
}
public ForwardMe HelloMethod(ForwardMe obj)
{
Console.WriteLine("Hello.HelloMethod : {0}", name);
return "Hi there " + name;
}
}
}
将此代码保存为 client.cs:
using System;
using System.Runtime.Remoting;
using System.Runtime.Remoting.Channels.TCP;
namespace RemotingSamples {
public class Client
{
public static int Main(string [] args)
{
TCPChannel chan = new TCPChannel();
ChannelServices.RegisterChannel(chan);
ForwardMe param = new ForwardMe();
HelloServer obj = (HelloServer)Activator.GetObject(
typeof(RemotingSamples.HelloServer), "tcp://localhost:8085/SayHello");
if (obj == null) System.Console.WriteLine("无法定位服务器");
else {
Console.WriteLine("值为 " + param.getValue());
ForwardMe after = obj.HelloMethod(param);
Console.WriteLine("呼叫后的值为 " + after.getValue());
}
return 0;
}
}
}
下面是 makefile:
all: server.exe client.exe share.dll
share.dll: share.cs
csc /debug+ /target:library /out:share.dll share.cs
server.exe: server.cs
csc /debug+ /r:share.dll /r:System.Runtime.Remoting.dll server.cs
client.exe: client.cs server.exe
csc /debug+ /r:share.dll /r:server.exe /r:System.Runtime.Remoting.dll client.cs
clean:
@del server.exe client.exe *.pdb *~ *.*~
附录 B:使用 HTTP 通道进行远程处理的示例
将此文件保存为 server.cs。此处为服务器的代码:
using System;
using System.Runtime.Remoting;
using System.Runtime.Remoting.Channels.HTTP;
namespace RemotingSamples {
public class HelloServer : IHello {
public static int Main(string [] args) {
HTTPChannel chan = new HTTPChannel(8085);
ChannelServices.RegisterChannel(chan);
RemotingServices.RegisterWellKnownType(
"server", "RemotingSamples.HelloServer", "SayHello",
WellKnownObjectMode.SingleCall);
System.Console.WriteLine("请按 <enter> 键退出...");
System.Console.ReadLine();
return 0;
}
public HelloServer()
{
Console.WriteLine("HelloServer 已激活");
}
~HelloServer()
{
Console.WriteLine("对象已清除");
}
public ForwardMe HelloMethod(ForwardMe obj)
{
Console.WriteLine("Hello.HelloMethod : {0}", name);
return "Hi there " + name;
}
}
}
将此代码保存为 client.cs:
using System;
using System.Runtime.Remoting;
using System.Runtime.Remoting.Channels.HTTP;
namespace RemotingSamples {
public class Client
{
public static int Main(string [] args)
{
HTTPChannel chan = new HTTPChannel();
ChannelServices.RegisterChannel(chan);
ForwardMe param = new ForwardMe();
HelloServer obj = (HelloServer)Activator.GetObject(
typeof(RemotingSamples.HelloServer), "http://localhost:8085/SayHello");
if (obj == null) System.Console.WriteLine("无法定位服务器");
else {
Console.WriteLine("值为 " + param.getValue());
ForwardMe after = obj.HelloMethod(param);
Console.WriteLine("呼叫后的值为 " + after.getValue());
}
return 0;
}
}
}
下面是 makefile:
all: server.exe client.exe share.dll
share.dll: share.cs
csc /debug+ /target:library /out:share.dll share.cs
server.exe: server.cs
csc /debug+ /r:share.dll /r:System.Runtime.Remoting.dll server.cs
client.exe: client.cs server.exe
csc /debug+ /r:share.dll /r:server.exe /r:System.Runtime.Remoting.dll client.cs
clean:
@del server.exe