Microsoft .NET 远程处理:技术概述

 

皮特·奥伯迈尔和乔纳森·霍金斯
Microsoft Corporation

总结: 本文提供 Microsoft .NET 远程处理框架的技术概述。 它包括使用 TCP 通道或 HTTP 通道的示例。 ) (15 个打印页

注意 本文包含更新的 Beta 2 代码。

目录

简介 远程对象 代理对象 通道 使用租赁 的激活 对象生存期 附录 A:使用 TCP 通道的远程处理示例

简介

Microsoft® .NET 远程处理提供了一个框架,该框架允许对象跨应用程序域相互交互。 该框架提供了许多服务,包括激活和生存期支持,以及负责将消息传输到远程应用程序或从远程应用程序传输消息的信道。 格式化程序用于在通道传输消息之前对消息进行编码和解码。 应用程序可以使用性能至关重要的二进制编码,或使用 XML 编码,其中与其他远程处理框架的互操作性至关重要。 所有 XML 编码在将消息从一个应用程序域传输到另一个应用程序域时都使用 SOAP 协议。 远程处理的设计考虑到了安全性,并且提供了许多挂钩,允许通道接收器在通过通道传输流之前访问消息和序列化流。

在没有基础框架支持的情况下管理远程对象的生存期通常很麻烦。 .NET 远程处理提供了许多激活模型可供选择。 这些模型分为两类:

  • 客户端激活的对象
  • 服务器激活的对象

客户端激活的对象受基于租约的生存期管理器的控制,该管理器可确保对象在租约到期时进行垃圾回收。 对于服务器激活的对象,开发人员可以选择“单个调用”或“单一调用”模型。 单一实例的生存期也受基于租约的生存期控制。

远程对象

任何远程处理框架main目标之一是提供必要的基础结构,以隐藏对远程对象调用方法并返回结果的复杂性。 调用方的应用程序域之外的任何对象都应被视为远程对象,即使这些对象在同一台计算机上执行也是如此。 在应用程序域中,所有对象都通过引用传递,而基元数据类型则按值传递。 由于本地对象引用仅在创建它们的应用程序域中有效,因此不能以该形式将其传递给远程方法调用或从远程方法调用返回。 必须跨应用程序域边界的所有本地对象都必须按值传递,并且应使用 [serializable] 自定义属性进行标记,或者它们必须实现 ISerializable 接口。 当对象作为参数传递时,框架将序列化对象并将其传输到目标应用程序域,将在其中重新构造对象。 无法序列化的本地对象无法传递到其他应用程序域,因此不可远程处理。

可以通过从 MarshalByRefObject 派生任何对象将其更改为远程对象。 当客户端激活远程对象时,它将接收远程对象的代理。 此代理上的所有操作都是适当的间接操作,使远程处理基础结构能够相应地截获和转发调用。 这种间接性确实会对性能产生一些影响,但 JIT 编译器和执行引擎 (EE) 已经过优化,以防止代理和远程对象驻留在同一应用程序域中时造成不必要的性能损失。 如果代理和远程对象位于不同的应用程序域中,堆栈上的所有方法调用参数都会转换为消息并传输到远程应用程序域,其中消息将转换回堆栈帧并调用方法调用。 相同的过程用于从方法调用返回结果。

代理对象

代理对象是在客户端激活远程对象时创建的。 代理对象充当远程对象的代表,并确保对代理进行的所有调用都转发到正确的远程对象实例。 为了准确了解代理对象的工作原理,我们需要更详细地检查它们。 当客户端激活远程对象时,框架会创建 类 TransparentProxy 的本地实例,其中包含所有类的列表以及远程对象的接口方法。 由于 创建 TransparentProxy 类时向 CLR 注册,因此运行时会截获代理上的所有方法调用。 此处检查调用以确定它是否是远程对象的有效方法,以及远程对象的实例是否与代理位于同一应用程序域中。 如果为 true,则简单方法调用将路由到实际对象。 如果对象位于其他应用程序域中,则堆栈上的调用参数将打包到 IMessage 对象中,并通过调用其 Invoke 方法转发到 RealProxy 类。 此类 (或更确切地说,它的内部实现) 负责将消息转发到远程对象。 激活远程对象时, 将创建 TransparentProxyRealProxy 类,但仅 将 TransparentProxy 返回到客户端。

为了更好地了解这些代理对象,我们需要绕道而行,并简要提及 ObjRef。 “激活”部分提供了 ObjRef 的详细说明。 以下方案简要介绍了 ObjRef 和两个代理类之间的关系。 必须指出,这是对该过程的非常广泛的描述:存在一些变体,具体取决于对象是激活客户端还是服务器,以及它们是单一实例对象还是单一调用对象。

  • 远程对象在远程计算机上的应用程序域中注册。 该对象被封送以生成 ObjRefObjRef 包含从网络上的任意位置查找和访问远程对象所需的所有信息。 此信息包括类的强名称、类的层次结构 (其父) 、该类实现的所有接口的名称、对象 URI 以及已注册的所有可用通道的详细信息。 远程处理框架在收到针对该对象的请求时,使用对象 URI 检索为远程对象创建的 ObjRef 实例。
  • 客户端通过调用 new 或激活 函数之一(如 CreateInstance)来激活远程对象。 对于服务器激活的对象,远程对象的 TransparentProxy 在客户端应用程序域中生成并返回到客户端,根本不会进行远程调用。 仅当客户端对远程对象调用方法时,才会激活远程对象。 此方案显然不适用于客户端激活的对象,因为客户端要求框架在要求激活对象时激活对象。 当客户端调用激活方法之一时,会在客户端上创建一个激活代理,并使用 URL 和对象 URI 作为终结点对服务器上的远程激活器发起远程调用。 远程激活器激活对象, ObjRef 将流式传输到客户端,在该客户端进行解封以生成返回到客户端的 TransparentProxy
  • 在取消编组期间,将分析 ObjRef 以提取远程对象的方法信息,并创建 TransparentProxyRealProxy 对象。 在向 CLR 注册透明Proxy 之前,已分析的 ObjRef 的内容将添加到 TransparentProxy 的内部表中。

TransparentProxy 是不能替换或扩展的内部类。 另一方面, RealProxyObjRef 类是公共类,可以在必要时进行扩展和自定义。 例如, RealProxy 类是执行负载均衡的理想候选项,因为它处理远程对象上的所有函数调用。 调用 Invoke 时,派生自 RealProxy 的类可以获取有关网络上服务器的负载信息,并将调用路由到相应的服务器。 只需从 Channel 请求所需的 ObjectURI 的 MessageSink ,然后调用 SyncProcessMessageAsyncProcessMessage 将调用转发到所需的远程对象。 调用返回时, RealProxy 会自动处理返回参数。

以下代码片段演示如何使用派生的 RealProxy 类。

   MyRealProxy proxy = new MyRealProxy(typeof(Foo));
   Foo obj = (Foo)proxy.GetTransparentProxy();
   int result = obj.CallSomeMethod();

上面获取的 TransparentProxy 可以转发到另一个应用程序域。 当第二个客户端尝试在代理上调用方法时,远程处理框架将尝试创建 MyRealProxy 的实例,如果程序集可用,则所有调用都将通过此实例路由。 如果程序集不可用,将通过默认远程处理 RealProxy 路由调用。

通过提供默认 ObjRef 属性 TypeInfo、EnvoyInfoChannelInfo 的替换项,可以轻松自定义 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 ,并实现 IChannelReceiverIchannelSender,具体取决于通道的用途。 大多数通道都实现接收方和发送方接口,使它们能够双向通信。 当客户端在代理上调用方法时,该调用被远程处理框架截获,并更改为转发到 RealProxy 类的消息 (或者更确切地说,是实现 RealProxy) 的类的实例。 RealProxy 将消息转发到通道接收器链进行处理。

链中的第一个接收器通常是将消息序列化为字节流的格式化程序接收器。 然后,消息从一个通道接收器传递到下一个通道接收器,直到它到达链末尾的传输接收器。 传输接收器负责与服务器端的传输接收器建立连接,并将字节流发送到服务器。 然后,服务器上的传输接收器通过服务器端的接收器链转发字节流,直到到达格式化程序接收器,此时消息从其调度点反序列化到远程对象本身。

远程处理框架的一个令人困惑的方面是远程对象和通道之间的关系。 例如,如果仅在调用到达时激活该对象, SingleCall 远程对象如何设法侦听要连接到的客户端?

magic 的一部分依赖于远程对象共享通道这一事实。 远程对象不拥有通道。 承载远程对象的服务器应用程序必须注册它们所需的通道以及它们希望使用远程处理框架公开的对象。 信道经过注册后,会自动开始在指定的端口上侦听客户端请求。 注册远程对象时,会为该对象创建 ObjRef 并将其存储在表中。 当请求传入通道时,远程处理框架会检查消息以确定目标对象,并检查对象引用表以在表中查找引用。 如果找到对象引用,则会从表中检索框架目标对象或在必要时激活该对象,然后框架将调用转发到该对象。 在发生同步调用时,来自客户端的连接在消息调用的持续时间内将保留。 由于每个客户端连接都是在它自己的线程内处理的,因此一个信道可以同时为多个客户端服务。

安全性是生成业务应用程序时的重要考虑因素,开发人员必须能够向远程方法调用添加授权或加密等安全功能,以满足业务需求。 为了满足此需求,可以自定义通道,使开发人员能够控制与远程对象之间的消息的实际传输机制。

HTTP 信道

HTTP 通道使用 SOAP 协议将消息传输到远程对象或从远程对象传输消息。 所有消息都通过 SOAP 格式化程序传递,其中消息将更改为 XML 并序列化,并将所需的 SOAP 标头添加到流中。 还可以将 HTTP 通道配置为使用二进制格式化程序。 然后,使用 HTTP 协议将生成的数据流传输到目标 URI。

TCP 信道

TCP 通道使用二进制格式化程序将所有消息序列化为二进制流,并使用 TCP 协议将流传输到目标 URI。 还可以配置 SOAP 格式化程序 TCP 通道。

激活

远程处理框架支持远程对象的服务器和客户端激活。 当不需要远程对象在方法调用之间维护任何状态时,通常使用服务器激活。 当多个客户端在同一对象实例上调用方法,并且对象在函数调用之间维护状态时,也使用此方法。 另一方面,客户端激活的对象从客户端实例化,客户端通过使用为此目的提供的基于租约的系统来管理远程对象的生存期。

所有远程对象都必须注册到远程处理框架,客户端才能访问它们。 对象注册通常由托管应用程序完成,该应用程序启动,向 ChannelServices 注册一个或多个通道,使用 RemotingConfiguration 注册一个或多个远程对象,然后等待它终止。 请务必注意,注册的通道和对象仅在注册它们的进程处于活动状态时可用。 当进程退出时,此进程注册的所有通道和对象将自动从注册它们的远程处理服务中删除。 向框架注册远程对象时,需要以下四条信息:

  1. 包含类的程序集名称。
  2. 远程对象的类型名称。
  3. 客户端将用于查找对象的对象 URI。
  4. 服务器激活所需的对象模式。 这可以是 SingleCallSingleton

可以通过调用 RegisterWellKnownServiceType、将上述信息作为参数传递或将上述信息存储在配置文件中,然后调用 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 注册它喜欢的通道并通过调用 newGetObjectCreateInstance 来激活该对象的代理。 以下代码片段演示了如何执行此操作的示例。

""      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 工具直接从终结点中提取所需的元数据。 此工具连接到提供的终结点,提取元数据,并生成程序集或源代码,然后可用于编译客户端。

GetObjectnew 可用于服务器激活。 请务必注意,在进行上述任一调用时,不会实例化远程对象。 事实上,根本不会生成任何网络调用。 框架从元数据中获取足够的信息来创建代理,而无需连接到远程对象。 仅当客户端在代理上调用方法时,才会建立网络连接。 当调用到达服务器时,框架会从消息中提取 URI,检查远程处理框架表以查找与 URI 匹配的对象的引用,然后根据需要实例化对象,并将方法调用转发到 对象。 如果对象注册为 SingleCall,则它在方法调用完成后销毁。 将为每个调用的方法创建 对象的新实例。 GetObjectnew 的唯一区别在于,前者允许将 URL 指定为参数,后者从配置中获取 URL。

CreateInstancenew 可用于客户端激活的对象。 两者都允许使用带参数的构造函数实例化对象。 当客户端尝试激活客户端激活的对象时,会向服务器发送激活请求。 客户端激活对象的生存期由远程处理框架提供的租赁服务控制。 下一节将介绍对象租赁。

使用 Leasing 的对象生存期

每个应用程序域都包含一个租约管理器,该管理器负责管理其域中的租约。 会定期检查所有租约的过期租约时间。 如果租约已过期,则会调用租约的一个或多个发起人,他们有机会续订租约。 如果没有任何发起人决定续订租约,则租约管理器将删除租约,并且对象将被垃圾回收。 租约管理器维护一个租约列表,其中租约按剩余租约时间排序。 剩余时间最短的租约存储在列表顶部。

租约实现 ILease 接口并存储确定要续订的策略和方法的属性集合。 可以随叫随到续订租约。 每次对远程对象调用方法时,租用时间都设置为当前 LeaseTime 加上 RenewOnCallTime 的最大值。 当 LeaseTime 过后,将要求发起人续订租约。 由于我们必须不时处理不可靠的网络,因此可能会出现租约发起人不可用的情况,并确保我们不会在服务器上保留僵尸对象,每个租约都有 一个 SponsorshipTimeout。 此值指定在终止租约之前等待发起人答复的时间。 如果 SponsorshipTimeout 为 null,则 CurrentLeaseTime 将用于确定租约何时到期。 如果 CurrentLeaseTime 的值为零,则租约不会过期。 配置或 API 可用于替代 InitialLeaseTimeSponsorshipTimeoutRenewOnCallTime 的默认值。

租赁经理维护发起人的列表, (他们实现 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 方法。
  • 租约可以向发起人请求 续订
  • 当客户端对 对象调用方法时,租约将通过 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("Hit <enter> to 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