c#

位置:IT落伍者 >> c# >> 浏览文章

Microsoft .NET Remoting技术概述


发布日期:2023年02月18日
 
Microsoft .NET Remoting技术概述

简介

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 与这两个代理类的关联方式但请注意这只是关于该进程的一个极其概括的说明;根据对象是客户端激活对象还是服务器激活对象以及它们是单一元素对象还是单一调用对象该进程会有所不同

远程对象注册在远程计算机的应用程序域中远程对象被封送以生成 ObjRefObjRef 包含了从网络上的任意位置定位和访问远程对象所需的所有信息包括:类的增强名称类的层次结构(其父类)类实现的所有接口的名称对象 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)proxyGetTransparentProxy();

int result = objCallSomeMethod();

上例中获取的 TransparentProxy 可以被转发到另一个应用程序域中当第二个客户端试图调用代理上的某个方法时远程处理框架会尝试创建 MyRealProxy 类的实例并且如果程序集可用所有的调用都会路由至此实例如果程序集不可用调用会路由至默认的远程 RealProxy

通过为默认的 ObjRef 属性 TypeInfoEnvoyInfo 和 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 并将其存储在表中当通道上传来一个请求时远程处理框架会检查该消息以确定目标对象同时检查对象引用表以定位表中的引用如果找到了对象引用将从表中检索框架目标对象或在必要时将其激活然后框架将调用转发至该对象对于同步调用在消息调用期间会一直维持来自客户端的连接因为每个客户端连接都在自己的线程上处理所以一个通道可以同时服务于多个客户端

生成商务应用时安全性是一个重要问题要满足商务要求开发人员必须能给远程方法调用添加诸如授权或加密等安全特性为了实现这一目标开发人员可以自定义通道使其能够对与远程对象之间的实际消息传输机制进行控制在传输到远程应用程序之前所有的消息都必须流过 SecuritySinkTransportSink 和 FormatterSink且这些消息传递到远程应用程序后会以相反次序流过同样的接收器

HTTP 通道

HTTP 通道使用 SOAP 协议与远程对象传输消息所有的消息流过 SOAP 格式化程序时都被转换为 XML 格式且被序列化所需的 SOAP 头也会被添加到该流中您也可以指定能够生成二进制数据流的二进制格式化程序然后数据流会使用 HTTP 协议传输到目标 URI

TCP 通道

TCP 通道使用二进制格式化程序将所有的消息序列化为二进制流并使用 TCP 协议将其传输到目标 URI

激活

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

在可以接受客户端的访问之前所有的远程对象都必须用远程处理框架注册对象注册一般由宿主应用程序来完成宿主应用程序将启动使用 ChannelServices 注册一个或多个通道使用 RemotingServices 注册一个或多个远程对象然后等待被终止请注意已注册的通道和对象只有在用来注册它们的进程活动时才可以使用如果退出了该进程则会自动从远程处理服务中删除它注册的所有通道和对象在框架中注册远程对象时需要以下四项信息:

包含类的程序集名称

远程对象的类型名称

客户端定位对象时将使用的对象 URI

服务器激活所需的对象模式该模式可以是 SingleCall也可以是 Singleton

远程对象可以通过下列两种方式注册:调用 RegisterWellKnownType将上述信息作为参数传递;或将上述信息存储在配置文件中然后调用 ConfigureRemoting 并将该配置文件的名称作为参数传递以上两种方法执行的功能相同因此您可以使用它们中的任意一种来注册远程对象当然后一种方法更方便些因为无需重新编译宿主应用程序即可改变配置文件的内容以下代码片断显示了如何将 HelloService 类注册为 SingleCall 远程对象

RemotingServicesRegisterWellKnownType(

server

SamplesHelloServer

SayHello

WellKnownObjectModeSingleCall);

其中server是程序集的名称HelloServer 是类的名称SayHello 是对象 URI

注册了远程对象后框架将为该对象创建一个对象引用然后从程序集中提取与该对象相关的必要元数据随后这一信息将与 URI 和程序集名称一起存储在对象引用中(该对象引用将被写入一个用于跟蹤已注册远程对象的远程处理框架表中)请注意除了在客户端试图调用对象上的某个方法或从客户端激活对象时以外注册进程不会实例化远程对象自身

现在任何知道该对象 URI 的客户端都可以使用 ChannelServices 注册通道并调用 newGetObject 或 CreateInstance 激活对象从而获得该对象的一个代理以下代码片断显示了该操作的示例:

ChannelServicesRegisterChannel(new TCPChannel);

HelloServer obj = (HelloServer)ActivatorGetObject(

typeof(SamplesHelloServer) tcp://localhost:/SayHello);

其中tcp://localhost:/SayHello表示我们希望在端口 上使用 TCP 协议连接到位于 SayHello 终结点的远程对象在编译该客户端代码时编译器明显会要求关于 HelloServer 类的类型信息该信息可以通过以下方式之一来提供:

提供对 HelloService 类所在程序集的引用

将远程对象拆分为实现和接口类并在编译客户端时引用这些接口

使用 SOAPSUDS 工具直接从终结点提取所需的元数据此工具将连接至所提供的终结点提取元数据然后生成可用于编译客户端的程序集或源代码

GetObject 或 new 可用于服务器激活对象请注意使用这两个调用时不会实例化对象实际上不会生成任何网络调用框架从元数据获得了创建代理所需的足够信息但并未连接到远程对象上只有在客户端调用代理上的某个方法时才会建立网络连接当调用抵达服务器时框架将从消息中提取 URI检查远程处理框架表以便定位与 URI 匹配的对象引用然后在必要时将对象实例化并将方法调用转发至对象如果将对象注册为 SingleCall则完成方法调用后该对象会取消每次调用一个方法时都会创建一个新的实例GetObject 和 new 之间的唯一差别在于前者允许指定 URL 作为参数而后者从配置中获得 URL

CreateInstance 或 new 可用于客户端激活对象两者都允许使用带参数的构造函数来实例化对象客户端激活对象的生存期由远程处理框架提供的租用服务控制对象租用的内容在下一节中说明

对象的租用生存期

每个应用程序域都包含一个用于管理其租用情况的租用管理器所有的租用都会被定期检查以确定租用是否已过期如果租用过期则会调用该租用的一个或多个发起者使它们有机会更新租用如果所有的发起者都不准备更新租用则租用管理器会删除该租用并将该对象作为垃圾回收租用管理器按照剩余租用时间的顺序维护租用列表剩余时间最短的租用排在列表的顶端

租用可以实现 ILease 接口并存储一个属性集合用于确定更新的策略和方法您也可以使用调用来更新租用每次调用远程对象上的方法时租用时间都会设置为目前 LeaseTime 最大值加上 RenewOnCallTimeLeaseTime 即将过期时发起者会被要求更新租用因为我们有时会遇上网络不稳定所以可能会找不到租用发起者为了确保不在服务器上留下无效对象每个租用都带有一个 SponsorshipTimeout该值指定了租用终止之前等待租用发起者回复的时间长度如果 SponsershipTimeout 为零CurrentLeaseTime 会被用于确定租用的过期时间如果 CurrentLeaseTime 的值为零则租用不会过期配置或 API 可用于替代 InitialLeaseTimeSponsorshipTimeout 和 RenewOnCallTime 的默认值

租用管理器维护着一个按发起时间从大到小存储的发起者列表(它们实现 ISponsor 接口)需要调用发起者以更新租用时间时租用管理器会从列表的顶部开始向一个或多个发起者要求更新租用时间列表顶部的发起者表示其以前请求的租用更新时间最长如果发起者没有在 SponsorshipTimeOut 时间段内响应则它会被从列表中删除通过调用 GetLifetimeService 并将对象租用作为参数即可以获得该对象租用该调用是 RemotingServices 类的一个静态方法如果对象在应用程序域内部则该调用的参数是对象的本地引用且返回的租用也是该租用的本地引用如果对象是远程的则代理会作为一个参数传递且返回给调用方的是租用的透明代理

对象能够提供自己的租用并控制自己的生存期它们通过替代 MarshalByRefObject 上的 InitializeLifetimeService 方法来完成该操作如下所示:

public class Foo : MarshalByRefObject {

public override Object InitializeLifetimeService()

{

ILease lease = (ILease)baseInitializeLifetimeService();

if (leaseCurrentState == LeaseStateInitial) {

leaseInitialLeaseTime = TimeSpanFromMinutes();

leaseSponsorshipTimeout = TimeSpanFromMinutes();

leaseRenewOnCallTime = TimeSpanFromSeconds();

}

return lease;

}

}

只有当租用处于初始状态时才可以更改租用属性InitializeLifetimeService 的实现通常调用基类的相应方法来检索远程对象的现有租用如果在此之前从未对该对象封送过则返回的租用会处于其初始状态且可以设置租用属性一旦封送了对象则租用会从初始状态变为激活状态并忽略任何初始化租用属性的尝试(但有一种情况例外)激活远程对象时将调用 InitializeLifetimeService通过激活调用可以提供一个租用发起者的列表而且当租用处于激活状态时可以随时将其他发起者添加到列表中

可以下列方式延长租用时间:

客户端可以调用 Lease 类上的 Renew 方法

租用可以向某个发起者请求 Renewal

当客户端调用对象上的某个方法时RenewOnCall 值会自动更新租用

一旦租用过期其内部状态会由 Active 变为 Expired且不再对发起者进行任何调用对象也会被作为垃圾回收一般情况下如果发起者分散在 Web 上或位于某个防火墙的后面远程对象回叫发起者时会遇到困难因此发起者不必与客户端处于同一位置只要远程对象能够访问得到它可以为网络上的任意位置

通过租用来管理远程对象的生存期可以作为引用计数的一种替代方法因为当网络连接的性能不可靠时引用计数会显得复杂和低效尽管有人会坚持认为远程对象的生存期比所需的时间要长但与引用计数和连接客户相比租用降低了网络的繁忙程度将会成为一种非常受欢迎的解决方案

总结

要提供完美的能够满足大多数商务应用需求的远程处理框架即使能够做到也必然会非常困难Microsoft 提供了能够根据需要进行扩展和自定义的框架在正确的方向上迈出了关键的一步

               

上一篇:C#中的基元类型

下一篇:vc.net中实现启动画面淡入淡出