客户端/服务器通信
.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 状态数据,我们需要做三件事:
1. 忽略租用机制,使租用成为无限期的,如上所述。
2. 将远程服务器集成在我们自己设计的进程中,例如,可以完全控制其生存期的系统服务。虽然此进程也可以被回收,但与回收 IIS 辅助进程相比,其操作更明显,更易察觉。
3. 将远程服务器开发为线程安全的服务器,因为这样可以使用多个线程来完成客户端的并发请求。这意味着,管理并发将写入共享资源并通常关注对静态内存的共享访问。
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 下集成对象:
1. 开发远程类并从 MarshalByRefObject 中继承(或将类声明为可序列化)。
2. 使用 IIS 管理器创建一个虚拟的 Web 应用程序。
3. 将包含您的类的程序集放到虚拟 Web 应用程序的 bin 子文件夹中。
4. 创建一个 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 替换 authenticationType
credCache.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 的上下文基础结构将越来越接近。但在现阶段,如何做到这一点以及何时做到这一点还不很清楚。