摘要:本文适用于要将 .NET Remoting 用于分布式多层应用程序设计的人员。文章从开发人员的角度介绍了该技术的功能。开发人员曾得益于这项技术所提供的方便的 RPC 机制,也曾感受过其不足之处带来的不便。本文假设读者熟悉 .NET Remoting,即使没有实际使用过,至少对其概念也有所了解。
产品特性一节对使用 Remoting 进行设计的人员很有用,最佳方法一节适用于使用 Remoting 进行构建的人员,Remoting 和 Web 服务一节试图消除有关“在何时选用何种技术”的困惑,摘要是对内容的精炼。
目录
概述
.NET Remoting 被誉为管理应用程序域之间的 RPC 的首选技术。应用程序域是公共语言运行库的隔离单元,它们是在进程内创建并运行的。这与 CLR 和非 CLR 托管的进程之间的进程间通信(互操作)不同。后一种类型的 RPC 通信(特别是 Web 上的)一般被认为是 Web 服务领域的问题。遗憾的是,这种看似清楚的区分,却由于可以在 IIS 下集成 .Net Remoting 服务器而变得模糊,正如 Microsoft .NET Remoting 框架简介一文中所述:
“通过在 IIS 中集成 .NET Remoting 对象,可以将其作为一种 Web 服务提供……”
一些 Microsoft 客户可能对 .NET Remoting 或多或少有些疑惑。我经常听到有人问“应该在什么时候使用 Remoting?”、“Remoting 何时会支持 NTLM?”、“如何保证远程会话的安全?”、“COM+ 怎么样?”以及“Remoting 如何管理事务?”
除了回答这些问题,本文还将介绍一些使用 .NET Remoting 的最佳方法,并概要介绍当前可以获得的功能。摘要预测了该技术的未来发展方向,特别是有关 Web 服务和新兴的全局 XML Web Service 体系结构 (GXA) 规范的问题。
产品特性一节的信息大部分来自 TechED N.Z. 2002,这次演示重点介绍了在分布式解决方案中使用 Remoting 的不同方法,阐明了 Remoting 的优点,也提到了一些不足之处。
最佳方法一节源于在多层 .NET 应用程序中使用 Remoting 的个人经验,其中介绍了很多在开发过程中用到的简单易行的最佳方法。
某些节包括了根据非正式谈话整理得到的资料,谈话的对象是对该技术及其发展方向都有深刻理解的 Microsoft 内部人员,但这里提供的信息决不代表未来的产品发布计划或安排。
产品特性
本节介绍 .NET Remoting 的功能和产品特性。
客户端/服务器通信
.NET Remoting 提供了一种很有用的方法,用于管理跨应用程序域的同步和异步 RPC 会话。远程对象代码可以运行在服务器上(如服务器激活的对象和客户端激活的对象),也可以运行在客户端上(其上的远程对象已经通过客户端/服务器的连接进行了序列化)。在任何一种情况下,只要完成初始化和配置(这并不困难),即可使用非常简单的编程语言,只需要少量的代码。远程对象(在按引用封送时是代理的对象)的使用对程序员是透明的。例如,早期的 Windows RPC 机制要求熟悉的类型和使用 IDL 工具的封送处理知识,并向开发人员公开 RPC 客户端和服务器存根的管理。Remoting 在为 .NET 提供 RPC 时要容易得多,而且由于使用简单易懂的 .NET 数据类型,从而消除了早期 RPC 机制中存在的类型不匹配的情况(这是一个非常大的威胁)。
默认情况下,可以将 Remoting 配置为使用 HTTP 或 TCP 协议,并使用 XML 编码的 SOAP 或本机二进制消息格式进行通信。开发人员可以构建自定义的协议(通道)或消息格式(格式化程序),并在需要时由 Remoting 框架使用。服务器和客户端组件都可以选择端口,就象可以选择通信协议一样。由此带来的一个好处是,很容易建立并运行基本的通信。
但是,在选择通信类型时还要考虑状态管理。本节接下来将介绍 Remoting 提供的各种通信选项及其相关的设计含义。
服务器激活的对象
“服务器激活的对象”是由服务器控制生存期的对象。它们只在客户端调用对象的第一个方法时,根据需要由服务器创建。服务器激活的对象只支持默认的构造函数。要对远程对象使用参数化的构造函数,可以使用“客户端激活”或“动态发布”(参见下文)。服务器激活的对象也被称为众所周知的对象类型,因为其位置 (URL) 是预先发布和已知的。服务器激活的对象有两种激活模式:Singleton 和 SingleCall,下面将介绍这两种模式。要创建服务器激活类型的实例,可以通过编程的方法配置应用程序,也可以进行静态配置。服务器激活的配置相当简单,例如,以下代码片段
<service> <wellknown mode="SingleCall" type="Hello.HelloService, Hello" objectUri="HelloService.soap" /></service>
描述了一个服务器激活的 (wellknown) 类型,其激活方式设置为 SingleCall。有关配置服务器激活的 Remoting 的详细信息,请参阅 MSDN 上 .Net Framework Developer's Guide 中的“Server-Side Registration”。
Singleton
这些对象遵循传统的 Singleton 设计模式,在这种模式中,任何时候内存中都只有一个实例,所有客户端都接受该实例提供的服务。但要注意,这些类型都有与之相关的默认生存期(请参阅下文的对象生存期管理一节)。这意味着对于可进行远程处理的类,客户端不必总是接收对这个类的同一实例的引用。后一种情况对状态管理很有意义,也是这种 Remoting 模式与传统的 Singleton 模式(要求对象标识相同)的不同之处。如果您的设计需要使用传统的 Singleton 状态管理模式,有两种方法可以解决此问题。一种方法是忽略默认的对象租用行为,以便“在主机应用程序域运行时始终”将对象保存在内存中。以下代码片段说明了如何做到这一点:
public class MyClass : MarshalByRefObject{ public override Object InitializeLifetimeService() { return null; }}
如上所述,这种机制将对象锁定到内存中,防止对象被回收,但只能在主机应用程序运行期间做到这样。对于 IIS 集成,如果集成 Remoting 会话的 IIS 或 IIS 进程被回收(很多原因可以导致这种现象),那么对象将被破坏。
要完全依赖 Remoting 的线程安全的 Singleton 状态数据,我们需要做三件事:
- 忽略租用机制,使租用成为无限期的,如上所述。
- 将远程服务器集成在我们自己设计的进程中,例如,可以完全控制其生存期的系统服务。虽然此进程也可以被回收,但与回收 IIS 辅助进程相比,其操作更明显,更易察觉。有关此机制的详细信息,请参阅下文的产品特性一节。
- 将远程服务器开发为线程安全的服务器,因为这样可以使用多个线程来完成客户端的并发请求。这意味着,管理并发将写入共享资源并通常关注对静态内存的共享访问。
SingleCall
SingleCall 远程服务器类型总是为每个客户端请求设置一个实例。下一个方法调用将改由其他实例进行服务。从设计角度看,SingleCall 类型提供的功能非常简单。这种机制不提供状态管理,如果您需要状态管理,这将是一个不利之处;如果您不需要,这种机制将非常理想。也许您只关心负载平衡和可伸缩性而不关心状态,那么在这种情况下,这种模式将是您理想的选择,因为对于每个请求都只有一个实例。如果愿意,开发人员可以向 SingleCall 对象提供自己的状态管理,但这种状态数据不会驻留在对象中,因为每次调用新的方法时都将实例化一个新的对象标识。
动态发布
还需要考虑服务器激活方法的最后一个类型,即动态发布。这是一种服务器激活的类型,通过提供程序化的发布机制,可以对对象结构进行更多的控制。它允许在特定的 URL 发布特定的对象,并可以选择使用参数化的构造函数。从结构上讲,这种类型可以看作是服务器激活的 Singleton 类型的一个微小变形。有关动态发布的信息,请参阅 .NET Framework Developer's Guide。
客户端激活的对象
“客户端激活的对象”是当客户端调用 new 或 Activator.CreateInstance() 时在服务器上创建的。客户端本身使用生存期租用系统,可以参与到这些实例的生存期中。这种激活机制能够提供最广泛的设计灵活性。如果使用客户端激活,当客户端试图激活对象时,激活请求将发送到服务器。这种机制允许使用参数化的构造函数和针对每个客户端的连接状态管理。使用客户端激活,每个客户端接受其特定的服务器实例提供的服务,从而简化了多个调用时对象状态的保存过程。但使用这些对象时一定要谨慎,因为很容易忘记会话是分布式的,对象实际上不仅在进程之外,而且在多层应用程序的情况下,还有可能在计算机之外(在 Internet 上设置一个属性并不过分)。实用而不花哨的接口应该成为这里的准则:为了提高性能,我们可能需要在高度结合与松散耦合之间进行权衡。要创建客户端激活类型的实例,可以通过编程的方法配置应用程序,也可以进行静态配置。在服务器上进行客户端激活的配置相当简单,例如,以下代码片段
<service> <activated type="Hello.HelloService, Hello" objectUri="HelloService.soap" /></service>
描述了一个客户端激活的类型。请注意,我们不再需要 URL,因为对于客户端激活的类型,类型本身就足以激活了。另外,wellknown 标记已被 activated 标记替代。有关配置客户端激活的 Remoting 的详细信息,请参阅 MSDN 上 .Net Framework Developer's Guide 中的“Server-Side Registration”。
扩展性
在处理远程方法调用的过程中,.NET Remoting 将格式化的“消息”沿 Remoting 的“通道”从客户端发送到服务器。消息格式和通道本身都是完全可扩展和可自定义的。默认的通道或格式化程序都可以由自定义构建的组件所替代。消息在传输过程中可以在多个“接收点”被截取和更改,允许对消息进行自定义的处理(例如消息加密)。.NET Framework Developer's Guide (Sinks and Sink Chains) 中介绍了自定义机制,而且 Internet 上已经出现了一些自定义的通道和格式化程序(例如,Named Pipe 通道的实现)。大多数人对这种扩展性并不感兴趣,因为该技术提供的默认格式化程序和通道已经可以在最广的范围内使用(即 TCP 和 HTTP,尤其是与 SOAP 消息格式化程序一起使用)。但是在最初的设计阶段,需要考虑各种解决方案选项,记住这种功能还是有必要的。
异常传播
.NET Remoting 完全支持跨 Remoting 边界的异常传播,这是对使用错误代码,如 DCOM 的重大改进。
使用 Remoting 异常,最好将异常类标记为可序列化的并实现 ISerializable 接口。这样,可以跨 Remoting 边界对异常进行正确地序列化,也可以在构造过程中将自定义的数据添加到异常中。对于需要远程处理以及在使用中要保持一致的异常,最好定义您自己的异常类。确保使用此方法能捕获所有异常并进行正确传播,而且不允许未处理的异常跨过 Remoting 边界。
对象生存期管理
.NET Remoting 为管理远程对象的生存期提供了功能强大的机制。如果我们的服务器对象不保留任何状态(如 SingleCall 对象),那么不必关注此进程,只需让 Remoting 基础结构完成要完成的工作即可,需要时,对象将作为垃圾被回收。如果我们保留状态,无论是服务器激活的 Singleton 还是客户端激活的对象,我们可能都要参与生存期管理进程:对象租用。我们已经看到很小程度的参与,使用了一种简单(且有用)的方法,就是忽略 InitializeLifetimeService 方法,如以上对 Singleton 的介绍中所述。这就使我们能够在集成对象的进程运行期间始终保留对象。那么,这个对象生存期进程如何工作呢?
Remoting 提供的对象管理机制基于租用原则:您永远不会拥有一个对象,只是借用它,只要持续支付就可以一直使用它。此过程将在下文中进一步介绍。但是,首先要简单介绍一下在 COM 领域中是如何处理对象清理的。DCOM 综合使用 ping 和引用计数两种方法来确定对象是否仍在运行,这样做不仅容易出错,而且对网络带宽的要求很高。使用引用计数时,最坏的情况是从来不会被完全理解,最好的情况也是很脆弱。过去(现在仍是)要对引用计数应用一些简单的规则才能使其发挥作用。COM 对象的 IUnknown 接口包括了 AddRef 和 Release 方法,需要由开发人员在适当的时候调用。有时程序员弄错了,结果造成对象没被删除,还导致相关的内存泄露。
相反,Remoting 基于租用的生存期管理系统综合利用了租用、负责人和租用管理器。每个应用程序域都包含一个租用管理器,它将每个 Singleton 或客户端激活的对象的租用对象引用保存在其域中。每个租用可以有零个或多个相关的负责人,负责人能够在租用管理器确定租用过期时重新租用。这种租用功能是由 Remoting 基础结构通过 ILease 接口提供的,通过调用 InitializeLifetimeService 获得,如上文所述。ILease 接口定义了很多用于管理对象生存期的属性:
- InitialLeaseTime。确定租用最初的有效期。
- RenewOnCallTime。在每个方法调用后,更新此时间单元的租用。
- SponsorshipTimeout。负责人通知租用过期后,Remoting 要等待的时间。
- CurrentLeaseTime。距租用到期的时间(只读)。
租用过期后,租用管理器将通知所有租用负责人,询问他们是否要更新租用。如果不更新,将释放相关的对象引用。
负责人是可以为远程对象更新租用的对象。要成为负责人,您的类必须从 MarshalByRefObject 中导出并实现 ISponsor 接口。一个租用可以有多个负责人,一个负责人也可以参与多个租用。
有关使用这些接口进行编程的租用管理机制,请参阅 Lifetime Leases(英文)上的 .NET Framework Developer's Guide 文档,这里就不重复介绍了。但值得注意的是,这种功能强大的机制只是对管理有状态的远程对象的生存期有意义。如上所述,您或者完全忽略它,利用它在其进程容器运行时将对象保存在内存中,或者完全参与到租用机制中。
远程服务器集成
有很多方法可以集成 .NET 远程服务器,主要分为两大类,如下所述。
ASP.NET 下的 IIS 集成
在 IIS 下集成远程服务器端对象的能力是作为标准功能提供的。它有很多优势,包括支持安全性和可伸缩性。
要在 IIS 下集成对象:
- 开发远程类并从 MarshalByRefObject 中继承(或将类声明为可序列化)。
- 使用 IIS 管理器创建一个虚拟的 Web 应用程序。
- 将包含您的类的程序集放到虚拟 Web 应用程序的 bin 子文件夹中。
- 创建一个 web.config 文件以保存 Remoting 服务器的配置定义,并将它放置到 Web 应用程序的虚拟根目录中。
就这么简单。但是,您应该了解一些限制:
- 不能为 IIS 集成指定应用程序名称,因为它是虚拟应用程序名称。
- 必须使用 HHTP 通道。
- 如果 Remoting 客户端也是一个 Web 应用程序,则启动时必须调用 RemotingConfiguration.Configure,它通常在 Global.asax 文件的 Application_Start 方法中。不能使用 <client> 标记来自动配置客户端 Web 应用程序。
- 不要指定端口,因为 IIS 会进行端口分配。如果需要,您仍可以使用 IIS 管理来为虚拟应用程序指定端口。
Remoting 应用程序域将集成在 Aspnet_wp.exe 辅助进程中,默认情况下,它将采用该进程的标识。
注意:目前 ASP.NET 中有一个错误,要求将 Aspnet_wp.exe 辅助进程的进程标识设置为“system”或本地计算机帐户,默认设置中,machine.config 中的“machine”配置不正确,导致在域控制器的 IIS 下集成时,ASP.NET 应用程序出现错误 500“内部服务器错误”。可以论证的是,该错误是由于缺乏说明如何适当地配置计算机帐户的文档所造成的。
在 IIS 下集成有很多功能上的优势:默认情况下,可以提供伸缩性、线程、审核、身份验证、授权和安全通信等功能。ASP.NET 辅助进程一直在运行,并且可以使用 machine.config 中的 <processModel> 元素进行线程和错误管理方面的微调。简而言之,IIS 的优势和功能都可用于远程服务器。
但它也有一些缺点:您必须使用比 TCP 速度慢的 HTTP。另外,IIS 可能循环执行 ASP.NET 辅助进程,这将破坏所有 Singleton 的状态。对您来说,这可能是问题也可能不是问题,要取决于您的设计需要,因为客户端的下一个调用将重新启动 Singleton。您可以将 IIS 配置为不循环执行辅助进程,但这种能力很有限,特别是在 IIS 5 中,而且可能造成更进一步的影响。这里最根本的意思是,如果要求远程服务器的安全性,那么无疑要使用 IIS 集成。至于性能,只有在系统测试/使用过程中实际察觉到问题时,才需要考虑,而且总能在硬件上找到解决问题的办法。
IIS 下要考虑的身份验证问题
身份验证选项
.NET Remoting 没有自己的安全模式:身份验证和授权是由通道和主机进程执行的,在这种情况下则由 IIS 执行。Windows 身份验证可用于 Remoting,配置方法是在 web.config 中设置 <authentication mode="Windows"/>。不能使用表单或 Passport 身份验证,因为 Remoting 客户端不能访问 Cookie,也不能重新定向到登录页面(因为远程服务器是为非交互使用设计的)。
将凭据传递到远程对象
如果远程对象是 IIS 集成的(在 ASP.NET 辅助进程中)并配置为使用 Windows 身份验证,则必须使用通道的凭据属性指定要使用的凭据,否则将导致不传递任何凭据就进行远程调用。这种疏忽是 HTTP 访问拒绝响应的常见原因。要使用集成远程对象代理的进程(Remoting 客户端进程)的凭据,请将通道的凭据属性设置为由进程凭据缓存维护的 DefaultCredentials。这可以使用通道元素(用于 Web 客户端),即 <channel ref="http" useDefaultCredentials="true"/> 公开地完成,也可以使用以下代码通过编程方式完成:
IDictionary channelProperties;channelProperties = ChannelServices.GetChannelSinkProperties(proxy);channelProperties["credentials"] = CredentialCache.DefaultCredentials;
要随远程对象调用一起传递“特定的”凭据,请禁用默认凭据,即设置 <channel ref="http" useDefaultCredentials="false"/> 并使用以下代码:
IDictionary channelProperties =ChannelServices.GetChannelSinkProperties(proxy);NetworkCredential credentials;credentials = new NetworkCredential("username", "password", "domain");ObjRef objectReference = RemotingServices.Marshal(proxy);Uri objectUri = new Uri(objectReference.URI);CredentialCache credCache = new CredentialCache();// 用 Negotiate、Basic、Digest、// Kerberos 或 NTLM 替换 authenticationTypecredCache.Add(objectUri, "authenticationType", credentials);channelProperties["credentials"] = credCache;channelProperties["preauthenticate"] = true;
注意:将 preauthenticate 属性设置为真(如上所述)将使 WWW 身份验证标头随初始请求传递。这将停止 Web 服务器拒绝对原始请求的访问,并对随后的请求执行身份验证。
在 IIS 之外集成
在 IIS 之外进行远程集成的方法有很多,如下所示。
在控制台应用程序中集成
开发人员可以编写一个启动 Remoting 基础结构的控制台应用程序,然后把它“留在附近”。把它留在附近的唯一原因,是因为它包含集成了远程调用的应用程序域。编写一个这样的程序非常简单:只需调用 RemotingConfiguration.Configure 方法,把您的远程主机配置文件传递给它,然后只需等待由某个事件,比如按键或收到特定的消息等来终止进程。
这种方法的优势是不要求使用中间层上的 IIS,但不可以随时生成,因此适用于演示、开发和测试。这并不是说它一无是处,只是用途有限而已。
在 GUI 应用程序中集成
开发人员还可以编写一个启动 Remoting 基础结构的 Windows GUI 应用程序,然后把它“留在附近”。同样,需要继续执行的唯一原因是它包含集成了远程调用的应用程序域。它的开发方法与控制台应用程序的方法相同:Remoting 主机可以直接启动,也可以根据用户的交互操作启动。同样,这种方法也具有不需要中间层上的 IIS 的优势,并可用于演示和测试。对该程序做一些变化可以得到对等网络(逻辑)winforms 应用程序,例如,聊天类型的应用程序。同样,该程序的使用范围也很有限。
在系统服务中集成
这种可能性非常有意思,因为 Remoting 基础结构提供的功能竟没有系统服务概念本身所提供的功能多。系统服务可以配置为在计算机启动时启动,并保留在周围直到您让它们离开,这对于远程集成是非常理想的。请注意,通过为虚拟应用程序设置“高隔离模式”,也可以将 IIS 应用程序配置为具有类似行为。关于这个问题还有很多内容值得探讨,本文就不讨论了。客户询问了许多关于这种机制的难题,包括它的用途。首先,介绍一些它的优点:我们已经介绍了服务本身的好处;另外,我们可以完全控制主机进程的激活,例如,可以选择是使用动态发布还是使用客户端激活;不需要 IIS,因为我们可以加载用户配置文件,并可以使用 TCP 上的二进制编码消息获得良好的性能。
但它的缺点也很多。首先,如果需要,您要构建自己的身份验证和授权机制。.NET Remoting Security Solution, Part 1:Microsoft.Samples.Security.SSPI Assembly(英文)一文完整详细地介绍了 .NET Remoting 的安全性解决方案:“……实现了 SSPI 的托管包装,提供了验证客户端和服务器以及签名和加密在二者之间发送的消息所需的核心功能。”这无疑是一笔宝贵的资源,它提供了一种添加此功能的机制,这非常有用。但问题是它并不是一个受支持的产品,而是一个提供补充功能的“非正式”方法。而且对开发人员还有一点威胁,因为该解决方案要依赖格式化程序和通道的可扩展性。所有这些都需要回避,要获得功能,必须向 Remoting 配置添加条目以说明使用 Windows NT Challenge/Response (NTLM)。但此类安全机制很有可能要加入到 .NET Remoting 的未来版本中。
系统服务也需要具有可伸缩性,并可作为 Remoting 服务器重新使用,因为多层的分布式应用程序将需要这些功能。例如,如果没有 IIS,集成服务将不得不管理自己的审核和授权,而这二者都是 IIS 在标准情况下附带的。
由于这些原因,系统服务集成机制的用途很有限,也许要在一个受约束的环境下使用,这种环境中的消息要排队进行单独交换,而安全性不是问题,或者还可以使用 TCP 上的 IPSec。
企业服务管理
为了使远程组件参与到 COM+ 环境中(并在 COM+ 的上下文中运行),需要从 ServicedComponent 中继承。ServicedComponent 和 System.EnterpriseServices 命名空间中提供的其他功能都允许 CLR 组件指定多个 COM+ 属性,如表示事务要求和服务器进程执行的属性等。再加上严格命名机制和使用 regsvcs 命令,远程组件可以成为整个 COM+ 环境中的一部分。
假设远程组件需要从 MarshalByRefObject 中继承,COM+ 组件需要从 ServicedComponent 中继承(而且在 .NET 托管代码中没有多重继承功能),如何实现这一点呢?幸运的是,ServicedComponent 是从 ContextBoundObject 派生的,而后者又是从我们需要的 MarshalByRefObject 派生的。在 Remoting 上直接构建 COM+ 集成是完全可以的,而且确实能够获得由企业服务提供的显而易见的优势,例如对象池、分布式的事务支持和基于角色的安全性等。但是,如何做到这一点以及这样的方法对未来验证的体系结构会产生什么样的影响,还是不得而知的。
我们有理由期待,随着时间的推移,COM+ 的上下文基础结构和 Remoting 的上下文基础结构将越来越接近。但在现阶段,如何做到这一点以及何时做到这一点还不很清楚。
使用 Remoting 的最佳方法
一直以来,开发和测试分布式组件不仅项目开销大,而且很令开发人员头疼。以下指导原则是在实践中摸索得到的,来之不易。
入门
Basic Remoting Task List(英文)一文提供了良好的开端,可以对照此文章检查在首次设置 Remoting 时要执行哪些任务,最好在整个过程中都将此文章作为参考资料。下面简单介绍一下要执行的步骤:
主机任务
- 设计服务,选择应用程序域、激活模式、通道、端口和发布。
- 实现 Remoting 主机应用程序域(例如 IIS/系统服务)。
- 配置主机激活、通道和协议设置。建议使用配置文件,可以通过调用 RemotingConfiguration.Configure 加载。
- 发布接口,供客户端使用(有关详细信息,请参阅下文中的“接口发布选择”)。
客户端任务
- 设计客户端,选择应用程序域和激活模式。
- 考虑是否需要注册通道和端口。
- 获取远程类型元数据。
- 实现客户端应用程序域。
- 配置客户端激活模式和其他类型的信息,如应用程序名称、通道和对象 URI 等。建议使用配置文件,可以通过调用 RemotingConfiguration.Configure 加载。
格式化选择
作为标准,Remoting 可以配置为在 HTTP 通道上使用 SOAP 或二进制格式化程序,或者在 TCP 通道上使用二进制格式化程序。一般情况下,在客户端配置文件中输入适当的条目和调用静态的 RemotingConfiguration.Configure 方法都可以实现这种配置。
例如,要将 Remoting 连接配置为使用 HTTP 上的二进制格式化程序,可以按以下方法完成配置条目:
<channel ref="http" useDefaultCredentials="true" port="0"> <clientProviders> <formatter ref="binary"/> </clientProviders></channel>
这里的“channel ref”指 HTTP 协议,“formatter ref”指要在通道上发送的消息格式,在此示例中为二进制。
遗憾的是,在开发过程中将二进制格式化程序用于 HTTP 通道,会产生屏蔽服务器端错误的副作用,例如,一般的服务器错误或访问冲突都会误报给客户端。这是因为使用二进制格式化程序时,客户端的 Remoting 组件需要以二进制格式返回消息,它无法正确解释纯文本的错误结果,并报告以下错误:
mscorlib.dll 中出现无法处理的异常类型 System.Runtime.Serialization. SerializationException。其他信息:BinaryFormatter 版本不兼容。需要使用 1.0 版。收到的版本为 1008738336.1684104552。
这种错误大部分“不是”因为版本不兼容,而是因为客户端无法分析文本格式的错误响应。虽然我们相信这种协议缺陷能够在产品的未来版本中得到解决,但还是强烈建议您在开发过程中使用 SOAP 格式化程序。证实之后,可以将此格式化程序切换为二进制以增强性能,但应该在性能优势充分且必要的情况下才这样做。
接口发布选择
设计并构建 Remoting 服务器之后,应将其提供的接口发布给客户端使用,以解析编译时的引用并允许动态地创建代理对象。有很多方法可以完成此操作,这里有必要重复一下。但首先有几点提示:
- 静态字段和方法永远都不能进行远程处理,.NET Remoting 始终处理某些形式的实例成员。
- 私有方法/类型不能进行远程处理。
- MarshalByRef 类型是通过引用进行远程处理的,可序列化的类型是在客户端进程中复制值并执行代码。
- 对象虚拟方法 Equals、GetHashCode 和 MemberwiseClone 等在本地执行。
了解了这些设计中应该注意的地方,就可以选择使用以下方法发布由 Remoting 服务器导出的接口:
- 向客户端提供服务器端的程序集,以在编译时使用。当只需要接口而不需要实现时,不建议也没必要使用这种方法。
- 对于 SOAP/HTTP 客户端(这里的 Remoting 服务器的功能是提供 Web 服务,尽管对这种服务还有些疑惑),Remoting 服务器可以提供说明服务器对象和方法的 Web 服务说明语言 (WSDL) 文件。.NET Framework SDK 附带的 SOAPSUDS 实用程序可用于生成这些 WSDL 文件,以作为元数据使用。实际上,这种方法更适合 Web 服务(从严格的 asmx 意义上讲)而不是 Remoting,因为 Remoting 接口的 WSDL 并不能与 Web 服务接口的 WSDL 完全兼容。Soapsuds Tool(英文)上的 .NET Framework Tools 文档详细介绍了 SOAPSUDS 实用程序。
- 在单独的库中声明一个接口并使用客户端部署该库。发布执行该接口的服务器类,客户端将可以使用它,方法是获取它执行的接口的代理。这是一种非常清楚的设计选择,因为它是人们特别感兴趣的接口。这种方法只能用于服务器激活的对象(请参阅产品特性一节),因为无法创建接口的实例。
- 使用 SOAPSUDS 为客户端构建替代类作为元数据使用。您可以对 Remoting 服务器程序集运行 SOAPSUDS,生成输出程序集(可以作为元数据直接使用)或源文件(可以直接包括在应用程序中)。 这种机制对于构建多层应用程序很有用,在这种应用程序中,一层中的对象要访问另一层中的远程对象。这种方法很有意思,上文的简介部分中引用的多层应用程序就使用了此方法。
假设我们在以下文件夹中打开一个命令窗口:
$FRAMEWORKSDK/Samples/Technologies/Remoting/Basic/RemotingHello/Service
我们可以编写:soapsuds -id:.-types:Hello.HelloService,Hello -oa:HelloInterface.dll
这将创建一个输出程序集 HelloInterface.dll,它包含在当前目录的 Hello 程序集中找到的只基于 Remoting 服务器 Hello.HelloService 的元数据。该程序集可由客户端直接使用。Remoting 服务器的位置是根据标准的 Remoting 配置,基于运行时提供的配置数据派生得到的。为客户端程序集生成的 MSIL
ldfld object [System.Runtime.Remoting]System.Runtime.Remoting.Services.RemotingClientProxy::_tp
清楚地显示出我们没有使用 Remoting 服务器实现,而是使用了由 SOAPSUDS 生成的元数据所构建的代理类。
不能确保/支持 SOAPSUDS 使用二进制进行格式化,因为它在输出程序集元数据中嵌入/映射了一些 SOAP 特有的内容。
建议您尽量保持 Remoting 接口的简单,使用“充实”而不“花哨”的接口,也就是说,要试着限制设计中远程调用的数量。在某些情况下,这可能需要传递冗余参数。将远程接口放在单独的类中,与实际实现的类相区分。这样可以获得一种表面类型模式:在需要时,可以轻松地使用另一种技术替换其中的 Remoting 层。
管理错误
本节介绍在开发(和使用)Remoting 解决方案的过程中可能会遇到的错误情况。在任何情况下,都应该记住要使用标准的设备使用和监视方法。事件记录仍是非常有价值的信息资源,就象网络监视器工具一样,网络监视器可以专门用于详细查看客户端/服务器的 Remoting 会话。中间层的 Remoting 服务器仍可以使用 Visual Studio .NET 提供的标准调试工具进行调试,例如,对于由 IIS 集成的 Remoting 服务器,可以通过向 ASP.NET 辅助进程附加调试会话(Visual Studio .Net | Debug [调试] | Processes [进程] | Attach [附加]) 来设置断点(如果资源可用)。但 Remoting 的错误很独特,下面列出了一些。请注意,所有错误都已使用 .NET Framework SDK 提供的 Basic Remoting Hello Sample 的各版本进行了复现,服务器和客户端也已在单机上运行。故障现象与在网络链接上的相同,只是由于 HTTP/TCP 的超时设置不同,需要相当长的时间才能出现错误。
丢失 MarshalByRef
由于 Remoting 要通过引用以用于给定的类,该类必须只做一件事,就是继承 MarshalByRefObject。假设开发人员忘记做这项工作,我们将得到一个 System.Runtime.Remoting.RemotingException 类型的异常,说明我们有一个“丢失的 MarshalByReference”.
是否能正确捕获和处理这个 RemotingException 将取决于程序员。(想想这个开发人员忘记了他应记住的唯一一件事。)
解决方法是:记住继承 MarshalByRefObject!
众所周知的服务器激活的错误服务器端点
对于服务器激活(请参阅产品特性一节),Remoting 服务器将其侦听处声明为端点。该端点一般包括一个对象 URI(远程对象的众所周知名称),一个协议和一个端口号。当然,所有这些都可能配置错误。
错误的 URI
由服务提供的 Basic Remoting Hello Sample 的 URI 是 HelloService.soap,如相关的 web.config 文件中所指定:
<configuration> <system.runtime.remoting> <application> <service> <wellknown mode="SingleCall" type="Hello.HelloService, Hello" objectUri="HelloService.soap" /> </service> </application> </system.runtime.remoting></configuration>
此服务是 IIS 集成的。IIS 集成要求 URI 带有后缀 .rem 或 .soap,我们在服务器上使用 .rope。在此实例中,我们将再次收到 RemotingException,这次显示的文本是“对象 </Hello.soap> 在服务器上已断开或不存在”。
请确保各个 URI 相互匹配!当 IIS 集成 Remoting 服务器时,还要确保 URI 以 .rem 或 .soap 结尾。
不匹配的协议/端口
为了进行此项测试,我们切换到控制台集成的服务器,以下是该服务器的配置文件:
<configuration> <system.runtime.remoting> <application name="RemotingHello"> <service> <wellknown mode="SingleCall" type="Hello.HelloService, Hello" objectUri="HelloService.soap" /> </service> <channels> <channel ref="http" port="8000" /> </channels> </application> </system.runtime.remoting></configuration>
假设我们要在服务器端将协议更改为 TCP,而使客户端保留 HTTP。
我们将再次收到 RemotingException,这次的文本是“底层连接已关闭:接收时出现意外错误”。
端口设置错误也会导致上述异常,唯一的不同是这种情况下,要用较长的时间才会出现错误。服务器和客户端之间的端口和协议必须匹配。
丢失 URI
另一种可能性是远程服务器没有运行,例如,服务器由 IIS 集成,而虚拟应用程序或相关的程序集丢失。再次使用 Basic Hello Remoting 服务器,我们需要虚拟应用程序 RemotingHello 能够运行。如果不能运行,我们将收到未处理的异常(取决于调用代码),但这次的异常将是:“无法加载类型 clr:Hello.HelloService, Hello”。
在这些情况下,请确保虚拟应用程序在运行,而且所需的程序集正确地放置在相关的 bin 子文件夹中。
总而言之,客户端必须正确地引用服务器定义的端点以便激活服务器,这意味着,端口、协议和 URI 的定义必须相互匹配。这太容易出错了。因此,如果服务器的位置定义为:
<service> <wellknown mode="SingleCall" type="Hello.HelloService, Hello" objectUri="HelloService.soap" /></service>
那么,客户的设置必须为:
<client url="http://localhost/RemotingHello"> <wellknown type="Hello.HelloService, Hello" url="http://localhost/RemotingHello/HelloService.soap" /></client>
其中,URL 表示集成 Remoting 服务的 IIS 虚拟应用程序,类型表示类和程序集名称。
Remoting 和 ASP.NET Web 服务
IT 设计中最好也是最坏的事情就是可以选择的体系结构组件太多了。Web 服务和 .NET Remoting 就属于这种情况,有时很难决定针对不同的目的应该选用哪种技术。当然,正确的答案是为要解决的问题选择最佳的技术。不要使用“始终使用 Web 服务”或“Web 服务是 Remoting 的子集,因此它就等于所有的 Remoting”等指令性的评述。本节将主要介绍这两种技术,说明在特定的情况下,为什么是选择这一种更有意义而不是另一种。
ASP.NET Web 服务和 .NET Remoting
让我们从 Web 服务的定义开始,定义说 Web 服务就是可以在 Web 上提供的服务。这个定义并不是很有用,我们不妨进一步把它提炼成“通过 SOAP 和 HTTP 访问的、可寻址的处理单元,这个处理单元是用 WSDL 描述的,可以通过 UDDI 发布。”这个定义就有用多了,因为它把 Web 服务和将 HTML 发送回浏览器的 Web 服务器区分开了。为了与 .NET Remoting 进行比较,我们特别强调了 Web 服务的定义,它与可在 Web 上提供的程序化的服务不同。例如,根据我们的定义,可以使用 WSDL 从客户端通过 HTTP 访问的远程主机就是 Web 服务。鉴于此(并强调 Microsoft ASP.NET Web 服务实现),对于分布式解决方案,在选择 ASP.NET Web 服务和 .NET Remoting 的“结合点”时,应该考虑哪些因素呢?
互操作性
一种常见的 Microsoft 理论是:如果需要在不同系统之间进行互操作,应该选择使用开放标准 (SOAP、XML、HTTP) 的 Web 服务方法,而使用 .NET Remoting 决不是一种交互的解决方案;如果各种系统中的所有组件都是 CLR 托管的,则 .NET Remoting“可能”是正确的选择。这一原则的适用范围很广,但有所区分还是非常有用的。.NET 远程对象的客户端应该是 .NET 客户端。如果您的功能必须在 Web(这里的 Web 即 Internet)上通过松散耦合的 SOAP 客户端(例如 Unix 进程)才能实现,则 Web 服务将是正确的选择。当然,Intranet 就不受这种限制:所有客户端都可以是 .NET 客户端,而且在这种配置中并不排除 .NET Remoting。同样,对于应用程序的中间层在防火墙之后并与 Web 层直接通信的环境,仍可选择 .NET Remoting。
强大的类型支持
.Net Remoting 支持所有托管的类型、类、接口、枚举、对象等,这通常被称为“多类型保真”。这里的关键在于,如果客户端和服务器组件都是在应用程序域中运行的 CLR 托管的对象,则数据类型的互操作是不成问题的。从根本上讲,我们拥有的是一个封闭的系统,会话的两端可以完全被理解,因此我们可以充分利用这一事实,处理好用于通信的数据类型和对象。
在各种系统并存的情况下,我们需要考虑系统之间的互操作性。对于可互操作的数据类型,我们要谨慎处理。例如,Web 服务数据类型的定义要基于 XML 架构定义 (XSD) 关于数据类型的说明。任何可以使用 XSD 进行描述并可以在 SOAP 上进行互操作的类型都可以使用。但是,这也确实使得某些数据类型不能使用。例如,对于无符号的字符类型或枚举,不存在相应的 W3C XSD 表示法。对于不同的 Web 服务实现,集合的处理不同,异常和数据集的处理也不同。另一个问题是,私有字段和属性不在 Web 服务调用之间传递,这对字段和属性本身来说并不是关键问题,但如果您的系统要求在不同的技术之间进行互操作,则在设计和测试系统时,这却是一个要考虑的因素,因为可以发送内容并不意味着可以接收到它。
再重复一遍,如果需要在不同的系统之间进行互操作,就不应该考虑使用 .NET Remoting 技术。如果是封闭的、CLR 托管的解决方案,则可以使用它。
状态管理
我们已经看到很多方法,使用基于激活方式(客户端激活或 Singleton)的 .Net Remoting 实现状态管理。对 .NET Remoting 和 Web 服务来说,通过 HTTP(带有不确定超时的无状态协议)来管理每个客户端的连接状态是件烦琐且不切实际的事情。但是,如果您需要维护状态,那么 Remoting 提供了一种基于每个对象的解决方案。Web 服务没有提供这种每个客户端的连接状态管理,但提供了对 ASP.NET 会话和应用程序对象的访问。
生存期管理
与状态管理有关的是生存期管理。正如我们所看到的,Remoting 为管理远程对象的生存期提供了功能强大的机制。Web 服务对象随 Web 服务的调用而存在和消失(从概念上讲,对同步和异步都是这样)。在这方面,Web 服务与 Remoting 相比,是一种单一调用类型。Remoting 对远程对象的激活和终止提供了更大程度上的控制。这对于您的设计可能有意义,也可能没意义。
按值调用和按引用调用
传递到 Web 服务调用的对象是经过序列化的,并按值进行传递。传递到 Remoting 的对象或被调用的对象本身可以按值或按引用进行传递。序列化的远程对象方法是在客户端进行处理的。在 Remoting 和 Web 服务之间进行选择时应该考虑这些不同。当然,这些考虑对您来说是否重要,也取决于要解决的问题的性质。
支持的协议
Web 服务调用仅限于 HTTP 上的 SOAP 编码的 XML。Remoting 可以使用 TCP 传输,或者扩展基础结构以支持自定义的协议。例如,在 www.gotdotnet.com 上的 jhawk 用户示例部分提供了一个使用 Named Pipe 的 Remoting 实现。
这里是 NamedPipe 自述文件的一个片段,阐明了 Remoting 的可扩展性:
通过实现 IChannel* 接口,可以使用可插入式通道结构将通道插入到 .NET Remoting 中。
Named Pipe 通道支持以下功能:
* 通过命名管道进行通信
* 同步消息
* 异步消息
* 单程消息
* 回调
* 通道接收
* 通道属性
* 自动生成管道名称
因此,如果您需要 Named Pipe,Remoting 可以提供解决方案。但是,与 SSPI NTLM 身份验证解决方案一样,Microsoft 目前也不支持这种解决方案,也许将来 Microsoft 会满足这种需要。
性能
如果性能对您的设计确实至关重要,那么通过 TCP 使用二进制消息格式的 Remoting 确实提供了一些显著的性能优势。对于本文所介绍的结果,如果要完整了解产生此结果的测试环境和测试,请参阅性能比较:.NET Remoting 与 ASP.NET Web 服务一文。
这里是从这篇文章中总结出的一些性能统计:
图例:ASMX - Web 服务,其他都是 Remoting 解决方案
WS 表示集成远程组件的 Windows 服务。
图 1:性能统计
文章接下来对性能图表进行了解释,如下所述:
“如上所示,对于 WS_TCP_Binary,其中的对象被配置为使用 TCP 通道和 Binary 格式化程序,而主机是 Windows 服务,其性能要优于其他的分布式技术。这是因为该方法通过原始 TCP 套接字传输二进制数据(比 HTTP 的效率高),且数据不需要编码/解码,因而降低了系统开销。可以看到,WS_TCP_Binary 和最慢的方法之间存在约 60% 的性能差距。
虽然 IIS_HTTP_Binary 与 WS_HTTP_Binary 产生的二进制负载相同,但其速度较慢,原因是从 IIS (Inetinfo.exe) 到 Aspnet_wp.exe 之间有额外的进程跃点。IIS_HTTP_SOAP 与 WS_HTTP_SOAP 的性能差别也是由此造成的。
WS_HTTP_Binary 和 WS_TCP_SOAP 的性能几乎相同。尽管前者有 HTTP 分析方面的额外系统开销,后者有 SOAP 分析方面的额外系统开销,但在本例中 HTTP 分析的系统开销与 SOAP 分析的系统开销几乎相同。
ASP.NET Web 服务的性能优于 IIS_HTTP_SOAP 和 WS_HTTP_SOAP,因为 ASP.NET XML 序列化比 .NET Remoting SOAP 序列化的效率高。从上述内容可以看出,ASP.NET Web 服务与 IIS_HTTP_Binary 的性能几乎相同。”
如果原始速度确实非常重要,那么这“60% 性能差距”就很有意义了。其缺点是要将服务器集成在 Windows 服务中,以便使用 TCP 协议(请参阅前面的远程集成一节)。它有效地权衡了性能的安全性,而且是一种“最好不要用于 Internet 或不安全的 Intranet”的方法。
小结
ASP.NET Web 服务基于 XML,用于要求使用 HTTP(假定它们集成在 IIS)的实际应用中,能够提供简单的编程模式和强大的跨平台支持,它通过使用 SoapExtensions 提供了一定程度的扩展性,例如加密数据流。Remoting 的编程模式更为复杂,但就协议和消息格式而言,它在类型保真、状态管理和扩展性方面具有明显的优势。Remoting 不能用于非 .NET 客户端,因此无法实现 Internet 客户端与远程主机的直接连接。当在 IIS 之外集成时,Remoting 不能提供安全性模型。当集成在 IIS 时,Remoting 可以提供与 ASP.NET 相同的安全性功能,包括使用 SSL 等安全协议。如果不需要考虑与其他平台的互操作性,而且客户端和服务器的配置完全在您的控制之下,则可以考虑使用 .NET Remoting。使用 Remoting 时,使用了 HTTP 通道的 IIS 集成要优于非 IIS 集成,这样,可以得益于相关的安全性和伸缩性基础结构。当然,这意味着您必须能够在解决方案中与 IIS 进行互操作。如果这无法实现,那么使用 Remoting“可能”就是件无法实现的艰巨任务了,这与要解决的问题的性质有关。由于 .NET Remoting 要求使用 .NET 客户端,因此有必要使用最快的可用格式化程序,这样一来,选择二进制而不选择 SOAP 将产生更好的性能。请记住上文的最佳方法一节的建议,在发布时使用此格式化程序,而不要在开发过程中使用。
摘要
.NET Remoting 是在某些分布式解决方案中使用的有效工具,它在所支持的协议和消息格式方面提供了可扩展的模型,并能在特定的情况下提供性能优势。它不应直接部署在 Internet 上,而且它的服务器对象应该集成在 IIS 之下,以充分利用 IIS 为在其控制下运行的进程提供的安全性和性能特性。
对于“封闭”的分布式解决方案,其中的客户端和服务器都是 CLR 托管的进程,应该考虑使用 Remoting。例如,Intranet 解决方案中使用安全 TCP 通道(如 IPSec)或 HTTP 的任意层中的组件,或者通过防火墙与 .NET Web 层组件会话的中间层应用程序组件。在这种情况下,当证实应用程序使用 SOAP 格式化程序后,应该选择二进制格式化程序和 HTTP 通道。
对于要与非 CLR 客户端进行互操作的系统,请使用 ASMX Web 服务,但要谨慎处理某些数据类型(请参阅强大的类型支持一节)。
请注意,使用 TCP 在 IIS 之外集成会带来性能优势,但需要自定义的安全性。
设计与实现
实现和配置 Remoting 是一个相当容易的过程。在此过程中,首先要选择 Remoting 主机、协议和激活模式。请尽量简化设计和实现过程,并认真考虑哪种接口发布机制对您的解决方案最有意义。建议的方法是,只把接口作为最易懂的概念模型来发布,但这样一来就不能使用客户端激活的对象。调试程序、事件日志和网络监视是开发过程中非常有用的工具,在开发远程组件时,它们也能助您一臂之力。
Remoting 的未来
象“何时使用 Remoting、何时使用 Web 服务”等问题都是很难回答的问题,更何况术语的定义也不是很清楚。例如,如果 Web 服务的定义不清楚,Remoting 就有可能配置为 Web 服务。
或许将来 Remoting 和 ASMX 技术能逐步融合。但在目前,我们至少可以比较合理地说明何时使用哪种技术,如上所述。
当前的开发重点是提供路由、安全性和事务支持的 GXA 实现。这种实现要基于使用 SOAP 标头,而目前的直接目标是扩展 Web 服务的功能。虽然如本文所述,从传统意义上讲,GXA 不支持 .NET Remoting,但它支持 Remoting 解决的很多问题,如状态和事务管理等。虽然现在的 GXA 实现可以解决 Web 服务所面对的许多问题,但它最根本的目的是尽量以不需要太高技术含量的方式解决这些问题。看到 GXA 的开发对 Web 服务和 .Net Remoting 的影响,将是一件充满乐趣的事情。