MSDN Library 查看器帮助 |
Microsoft .NET 远程处理:技术概述
Piet Obermeyer 和 Jonathan Hawkins
Microsoft Corporation更新日期:2001 年 7 月
摘要:本文提供 Microsoft .NET 远程处理框架的技术概述。它包括使用 TCP 信道或 HTTP 信道的示例。(共 15 页打印页)
注意
本文包括已更新的
beta 2代码。
内容
介绍
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 类处理远程对象上的所有函数调用,因此它是执行负载平衡的理想候选。当调用 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("Hitto 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