翻译|其它|编辑:郝浩|2004-02-04 11:20:00.000|阅读 1655 次
概述:
# 界面/图表报表/文档/IDE等千款热门软控件火热销售中 >>
Shannon Pahl
Microsoft Corporation
2002 年 4 月
摘要:本文提供了蕴含在 Microsoft .NET 与 COM+ 服务集成中的详细技术信息,并介绍了可用于托管代码的服务。
要掌握本文内容,需要了解一些 Microsoft?.NET 框架和 COM+ 服务的知识。了解企业服务会很有帮助,但这不是必要的。要了解这些主题,请参阅:
COM 提供了一种编写基于组件的应用程序的方法。众所周知,编写 COM 组件需要进行大量重复的琐碎工作。而 COM+ 并不完全是 COM 的新版本,实际上,COM+ 为组件提供了一个服务基础结构。组件在构建后安装到 COM+ 应用程序中,可以建立易于部署、吞吐量高、可缩放的服务器应用程序。(如果组件不需要使用任何服务,则不应放到 COM+ 应用程序中。)为了达到可缩放性和吞吐量目标,需要从一开始就使用事务、对象池和活动语义等服务来设计应用程序。
.NET 框架提供了另一种编写基于组件的应用程序的方法,与 COM 编程模型相比,它具有更好的工具支持、公共语言运行时 (CLR) 和更简单的编码语法等优势。COM+ 服务基础结构可以从托管和非托管代码进行访问。非托管代码中的服务称为 COM+ 服务。在 .NET 中,这些服务被称为企业服务。从 ServicedComponent 派生的类表明某个组件将需要服务。(如果组件不需要使用任何服务,则不应从 ServicedComponent 派生。)改进的工具支持使编程人员能够编写基于服务器的应用程序,而可缩放性和吞吐量问题仍需要通过良好的编程实践来实现。服务背后的基本理念是,从一开始就考虑吞吐量和可缩放性的设计,并利用企业服务在适当的位置轻松地实现那些设计模式。
有人可能会提出异议,服务基础结构设计实际上与 COM 甚至组件都没有多大关系:现在,COM+ 服务可以用于 COM 组件、.NET
组件、甚至其他不能称为组件的实体,如 ASP 页或任意代码块等(请参阅 Microsoft Windows?
XP
上的无组件服务 COM+ 功能)。
今天,所有可用的 COM+ 服务都可以用于 .NET 和 COM 对象。其中一些服务包括:事务、对象池和构造字符串、JIT、同步、基于角色的安全性、CRM 和 BYOT 等。有关 Microsoft Windows 2000 上的服务的完整列表,请参阅 Platform SDK 中的“COM+ 提供的服务”。Microsoft Windows XP 包括 COM+ 的一个新版本,称为 COM+ 1.5,它具有一些附加服务,也可以用于 .NET 组件。
为编写使用服务的托管应用程序,必须从 ServicedComponent 中派生需要服务的类,并使用各种自定义属性来指定所需的实际服务。本节介绍这些概念以及它们如何影响托管代码的编写。后面几节将进行详细说明。
假设编写了一个 Account 类(其实际代码将在后面列出)并放置在 BankComponent 程序集中。则可以按照以下方法使用该类:
using system; using BankComponent; namespace BankComponentClient { class Client { public static int Main() { Account act = new Account(); act.Post(5, 100); act.Dispose(); return 0; } } }
要建立该客户端,必须向 BankComponent 命名空间添加引用。此外,还必须为 System.EnterpriseServices 程序集添加引用 - 在 BankComponentClient 命名空间中,客户端将调用 Dispose() 和 ServicedComponent 构造函数,它们是在 System.EnterpriseServices 中定义的方法,而不是在包含 BankComponent 的程序集中定义的。当派生类没有重载所有基类方法时,.NET 通常要求使用这种处理方式。
BankComponent 服务器代码显示了在 .NET 中使用事务的 Account 类的实现。Account 类是从 System.EnterpriseServices.ServicedComponent 类中派生的。Transaction 属性将该类标记为需要一个事务。由于使用了 Transaction 属性,所以将自动配置同步和 JIT 服务。AutoComplete 属性用于指定:如果在方法执行过程中出现未处理的异常,运行时必须为该事务自动调用 SetAbort 函数,否则,将调用 SetComplete 函数。ApplicationName 属性将此程序集与为此应用程序存储服务配置数据的 COM+ 应用程序关联起来。该类所需的进一步修改已在代码中标明。
using System.EnterpriseServices
; [assembly:ApplicationName
("BankComponent")] [assembly:AssemblyKeyFileAttribute
("Demos.snk")] namespace BankComponentServer { [Transaction(TransactionOption.Required)
] public class Account :ServicedComponent
{ [AutoComplete
] public bool Post(int accountNum, double amount) { // 更新数据库,不必调用 SetComplete。 // 如果没有出现异常,则自动调用 SetComplete。 } } }
从 BankComponent 服务器命名空间中的代码可以看出,在 .NET 中使用 COM+ 服务是很容易的。下面简单列出了从编码到部署的整个过程:
sn –k Demos.snk
必须在 COM+ 目录中注册使用受服务组件的程序集。ServicedComponent 类和自定义属性是从托管代码访问 COM+ 服务的两个关键概念。服务的配置存储在 COM+ 目录中。对象在 CLR 中驻留和执行。图 1 显示了托管对象及其相关联的 COM+ 上下文,在下面的两节中会更清楚。
图 1:与托管组件相关联的服务
使用 COM+ 组件时需要手动配置目录,而使用受服务组件时,可以根据代码中的属性来更新目录。使用命令行工具 regsvcs.exe 或通过编写访问托管 API 的脚本可以显式注册程序集。后面的“部署”一节提供了详细的信息。在开发过程中,为方便起见,提供了 XCopy 部署,即简单地将程序集复制到应用程序目录中。每当客户端应用程序为从 ServicedComponent 中派生的类创建实例时,运行时都将检测是否已在 COM+ 应用程序中注册了该程序集。如果没有注册,则在本地目录中搜索程序集,如果找到了,该程序集中所有受服务组件都将在 COM+ 应用程序中注册,然后激活。这一过程称为迟缓注册,但并不适合所有情况。例如,标记为 COM+ 服务器应用程序的程序集需要显式注册(如下所示),迟缓注册不适合调用托管受服务组件的非托管客户端。迟缓注册在开发时很有用,因为如果没有它,就需要使用脚本、代码或 RegSvcs 来注册程序集。
自定义属性是从托管代码访问 COM+ 服务的两个关键概念之一。自定义属性用于指定所需的服务,如上述代码中的 Transaction 自定义属性。这些属性在程序集的元数据中存储了服务的配置选项。自定义属性的使用方式是:让某段代码加载程序集,然后使用反射来创建属性的实例并对其调用方法,从而提取存储在属性中的服务配置。然后,可以将该信息写入到 COM+ 目录中。执行这些步骤和其他步骤的代码包含在 EnterpriseServices.RegistrationHelper 中。为使注册过程更简单,所有注册窗体都使用了 EnterpriseServices.RegistrationHelper 组件。该组件可以作为托管类和 COM 对象来访问。
图 2:注册受服务组件
从概念上讲,RegistrationHelper 执行了以下步骤:
RegistrationHelper 将试图使用 RegistrationHelperTx 在事务中执行这些步骤。RegistrationHelperTx 是在安装 .NET 时创建的 COM+ 应用程序中的一个类。因此,如果注册失败,COM+ 目录和注册表将恢复到其原始状态。但目前,生成的类型库将仍保留在磁盘上(或者在 GAC 中,如果程序集在 GAC 中)。如果正在注册的程序集引用了同样使用 COM+ 服务的其他程序集,则相关图中的所有程序集将执行上述相同的步骤。
由于 RegistrationHelper 要访问 COM+ 目录,因而需要具有计算机上的非托管代码权限和管理权限。因此,对于 RegistrationHelper 的客户端也是一样,如迟缓注册、RegSvcs 或您的脚本/代码。这还意味着从 Internet 上下载的代码或存储在网络共享上的代码将不能进行注册。
可以编写不兼容的属性组合,例如,请求一个 Transaction 并将 Synchronization 设置为禁用。当前,这些组合是在注册时检测的(当把它们写入到 COM+ 目录中时),而不是在编译时检测的。某些属性具有与其他属性的相关性,例如,当只使用 Transaction 属性时,相当于使用 Transaction、JustInTimeActivation 和 Synchronization 属性。注册托管组件时,如果不使用属性覆盖“未配置的”默认值,将使用 COM+ 目录的默认值。例如,如果注册一个组件并且没有指定 Transaction 属性,则目录中事务设置的未配置默认值将设置为 TransactionOption.Disabled。这种方法使开发人员在组件不再需要某个属性时,可以从代码中删除它,然后,当再次注册程序集时,再适当地重新设置事务的目录条目。联机文档中给出了这些未配置默认值的详细列表。默认的配置值是属性参数中的默认值,例如,只使用属性 [Transaction] 表示 TransactionOption.Required。
由于托管类上的服务的配置数据存储在 COM+ 目录中,因此在注册程序集后,也可以通过管理的方式修改某些目录条目。但某些服务不能以这种方式修改。例如,在目录中禁用事务服务可能导致代码运行不正常。部署特有的设置(如对象构造字符串和安全性角色)可以在注册后进行处理。但如果在注册后设置,对于包含受服务组件的程序集的 XCopy 部署可能是不够的。COM+ 应用程序的导入和导出功能可以帮助分配应用程序的当前状态。有关导入和导出的进一步信息将在“远程组件”一节中介绍。
在某些情况下,配置数据并不参考目录,而只是从程序集元数据中提取。这些情况包括自动完成、JIT、对象池(尽管池的大小是从目录中提取的)和安全方法属性。有关此问题的详细信息将在各个服务的相关章节中讨论。
注册程序集的进程将自动生成 COM+ 所需的 GUID。如果程序集没有签名,则只根据类型和命名空间的名称生成 GUID。因此,如果程序集没有签名,则可能生成非唯一的 GUID。.NET 程序集可能遇到类似的情况,它甚至没有使用 COM+ 服务,但是需要唯一的类型名称。因此,必须对使用 COM+ 服务的程序集签名。如果程序集没有签名,注册将失败。注册还意味着使用 COM+ 服务的 .NET 类具有一个全局配置数据存储。尽管有可能将专用程序集复制到多个应用程序目录中,但最终所有这些应用程序都引用受服务组件的一个配置数据。因此,更改 COM+ 目录中的配置数据将影响使用该类的所有应用程序。这一点对于 Microsoft ASP.NET 配置中的多个 vroot 是很显然的,这些 vroot 都包括使用受服务组件的相同程序集的副本。使相同的 COM+ 应用程序具有多个配置的一种方法是在 Microsoft Windows .NET 上使用 COM+ 分区。要在 .NET 中使用 COM+ 分区服务,请不要使用 ApplicationID 属性 - 为在多个分区内安装相同的组件,COM+ 需要唯一的应用程序 ID。
通常,当客户端需要访问不在客户端应用程序目录中的程序集时,或者如果程序集被加载到不在客户端目录中的其他进程中时,请使用 GAC。从概念上讲,使用受服务组件的专用程序集实际上是共享程序集 - 它们使用共享的配置数据。如果 ApplicationActivationOption 被设置为库,则有可能在程序集中的类上使用事务,并且,如果所有程序集都加载自同一个目录,则可以在一个客户端使用该程序集。当使用 ApplicationActivationOption 的程序集被设置为服务器时,该程序集将由 dllhost.exe(通常不在客户端目录中)加载。使用 COM+ 服务器应用程序中的受服务组件的程序集应放置在 GAC 中。使用 COM+ 库应用程序中的受服务组件的程序集则不必放置在 GAC 中(除非它们位于不同的目录)。唯一的例外是组件保留在 ASP.NET 中时 - 程序集不应放置到 GAC 中,这样才能使阴影副本能够正常运行。
要删除使用受服务组件的 .NET 应用程序,应先从 GAC 中删除程序集(如果它是使用 GAC 注册的),使用 regsvcs.exe 从 COM+ 中取消程序集的注册,然后删除程序集及相关联的类型库。
使用 GUID 属性可以确定 COM+ 所需的 GUID。但建议使用版本控制,而不是明确地使用
GUID。当创建新的方法签名,或当类具有不同的服务属性时,应递增程序集的主版本号或次版本号。每个版本都应进行一次注册。注册程序集的新版本时,将为该版本生成新的
GUID,而且组件将使用相同的组件名称在相同的 COM+ 应用程序中注册。因此,组件会在 COM+ 应用程序中多次出现。但每个组件都有由 GUID 给定的唯一
ID。每个实例都引用该组件的一个特定版本。当使用 Microsoft Visual Studio?
.NET 建立 .NET
应用程序时,经常会遇到类似情况。环境将属性 [assembly:AssemblyVersion("1.0.*")]
添加到项目上。每次新建项目时都将生成新的内部版本号,因此,当重新注册程序集时,将生成新的
GUID。所以,最好在适当的时候手动递增内部版本号。客户端将使用 CLR 版本策略绑定到程序集上,从而能够使用 COM+
应用程序中的类的正确版本。编写使用受服务组件的程序集(托管服务器)时的一些类似情况包括:(下面使用了激活的某些方面,这些内容将在下一节中介绍)
版本控制应用于同一 COM+ 应用程序中的所有组件,也就是说,不能为应用程序自动添加版本。例如,不能使用版本策略为应用程序上的角色添加版本。要为应用程序添加版本,请使用应用程序名称属性。
企业服务基础结构是建立在上下文概念的基础之上的。上下文是具有类似执行要求的对象的环境。可以在激活和/或方法调用截取的过程中实施服务。尽管 COM+ 服务是用非托管代码编写的,但 COM+ 服务与 .NET 的集成要比仅在 .NET 中使用 COM 互操作技术强大得多。如果不从 ServicedComponent 中派生,注册进程将不会获得预期的效果。
受服务组件可以用各种组合方式予以激活和保留。如图 3 所示,这里将讨论三种情况:进程内(相同的应用程序域)、应用程序域间(相同的进程)和进程间的激活。这些情况的重点在于调用组件时所跨越的边界。进程内激活可能会跨越上下文边界,在应用程序域间的情况下会跨越上下文边界和应用程序域的边界,而在进程间情况下将处理跨计算机、跨进程和跨上下文的边界。
图 3:受服务组件的激活宿主
受服务组件的实现依赖于 .NET Remoting,它为插入以非托管或托管代码编写的服务提供了可扩展的机制。受服务组件是从 ContextBoundObject 中派生的,并能实现诸如 IDisposable 等各种接口。使用 ProxyAttribute 派生的自定义属性可以轻易地自定义 CLR 中的激活链。通过编写自定义的真正代理可以自定义截取。当需要新的受服务组件派生类时,可以自定义激活链以使激活调用真正地调用 CoCreateInstance 的托管 C++ 包装程序。这将使 COM+ 能够根据存储在以前注册的程序集的 COM+ 目录中的信息设置非托管的上下文和服务。这也是实现迟缓注册的阶段。在程序集的注册过程中,InprocServer32 键指向 mscoree.dll,因此最终将 COM+ CreateInstance 重新定向到运行时,以创建真正的托管对象。因此,在激活过程中,将创建一个自定义的真正代理对象。此代理的进程内版本被称作受服务组件代理或 SCP,如图 4 所示。
图 4:激活路径
从激活调用返回的路径将封送托管代码中的托管引用,通过非托管的 COM+,返回到托管代码中(图 4 中线路 1 的相反路径)。根据真正对象的创建位置,客户端将引用拆封到相关窗体中。在进程内激活的情况下,引用被拆封为对透明代理 (TP) 的直接引用,如图 5 所示。应用程序域间的引用被拆封为 .NET Remoting 代理。进程间或计算机间的引用(图 6)需要更多的拆封处理:COM 互操作调用由 ServicedComponent 在激活和拆封过程中实现的 IManagedObject。远程受服务组件代理 (RSCP) 在激活过程中调用 IServicedComponentInfo 以获取服务器对象的 URI,这意味着在激活过程中进行了两次远程调用。当方法级上需要 COM+ 基于角色的安全性时,这些接口需要与一个角色相关联,以便在基础结构调用这些接口时成功地进行拆封处理。“安全性”一节将讨论进程间激活和拆封处理对配置基于角色的安全性的影响。
图 5:进程内调用的基础结构
图 6:进程外调用的基础结构
因此,激活链已被自定义,以创建自定义的真正代理(用于截取)和创建非托管的上下文,只给 COM+ 留下了执行截取服务的语义所需的上下文基础结构。现在,COM+ 上下文与托管对象相关联,而不是与 COM 对象相关联。
图 7 显示了进程内方法调用的基础结构。自定义代理 (SCP) 使得能够截取托管调用。在激活过程中,COM+ 上下文 ID 存储在 SCP 中。当一个托管对象调用受服务组件时,存储在目标 SCP 中的上下文 ID 将与当前的上下文 ID 进行比较。如果上下文 ID 相同,则直接在真正的对象上执行调用。如果上下文 ID 不同,SCP 将调用 COM+ 以切换上下文并呈现输入方法调用的服务。对于进程内的调用,与 AppDomain.DoCallBack 类似,只是 AppDomain 为 COM+。DoCallBack 函数首先进入 COM+(图 7 中的步骤 2),它将切换上下文并呈现服务,然后回调 SCP 上的函数调用。SCP 进行数据封送并在真正对象上调用方法。当方法退出时,返回路径允许 COM+ 呈现离开方法调用的语义(图 7 中的步骤 5)。COM+ 仅用于呈现服务。数据封送和方法调用是在 .NET 运行时中进行的,这样在调用方法时就不必进行类型转换(如从 String 转换到 BSTR)。如果进程内调用使用了 COM 互操作,则需要进行数据封送。因此,以非托管代码呈现服务的调用不是进程内调用的 COM 互操作调用。
图 7:进程内调用的基础结构
对静态方法的调用不转发到透明和真正的代理。因此,静态方法不能使用截取服务,它们是在客户端的上下文中被调用的。内部方法将在正确的上下文中被调用,这意味着,在为新事务配置的对象上调用内部方法的客户端将参与到新事务中。但是,由于方法级服务需要 COM+ 目录中的一个接口(在下一节和“安全性”一节中将详细介绍该主题),因此不能为方法级服务配置内部方法。服务可以应用到属性上,但方法级属性(如 AutoComplete)必须分别放在获得者和设置者方法上。
AutoComplete 属性是使用事务的一种方便方法,无需编写任何代码以访问该服务。此外,还可以使用 ContextUtil.SetAbort 或 ContextUtil.SetComplete。该服务可以在 COM+ 资源管理器中通过设置方法属性的复选框进行配置。但托管对象不需要实现接口。受服务组件也是这样。如果接口上没有声明方法,方法级服务的配置将不能写入到注册的目录中,配置只能存储在元数据中。如果方法没有接口,上下文切换将从 SCP 中进行,使用存储在 IRemoteDispatch.RemoteDispatchAutoDone 上的配置信息(如果存在 AutoComplete 属性)。如果不存在 AutoComplete,则使用 IRemoteDispatch.RemoteDispatchNotAutoDone。IRemoteDispatch 是由 ServicedComponent 实现的一个接口。非托管客户端只能使用 IDispatch(后期绑定)调用没有接口的受服务组件,因此,由于在那种情况下没有真正的代理,因而不能实施 AutoComplete 语义。即使在使用接口时,AutoComplete 的配置仍然由托管客户端的元数据进行驱动。只有在进程外的情况下,才在 RemoteDispatchAutoDone 上进行 DCOM 方法调用。进程外组件不使用 DoCallBack 机制,而使用 DCOM 来发送调用和呈现服务。如果方法在接口上,则使用 DCOM 调用远程受服务组件上的接口方法,否则,调用将被调度到 ServicedComponent 上的 IRemoteDispatch 接口。这意味着即使象 Dispose() 这样的调用也是通过 DCOM 调用的,其含义将在以后讨论。
ContextUtil 类用于访问相关联的 COM+ 对象的上下文及其属性。它与由 CoGetObjectContext 以非托管代码方式返回的对象的功能类似。与受服务组件相关联的托管对象的上下文与相关联的非托管对象的上下文的用途不同。通过编写三个托管对象可以证明这一点,一个带有所需的事务(作为根),另外两个不从受服务组件中派生(作为上下文托管对象的子对象示例)。非受服务组件的行为将与带有事务支持的受服务组件一样,也就是说,它们可以调用资源管理器,而且必要时还可以使用 ContextUtil.SetAbort。创建根对象后,将创建相关联的非托管上下文并与当前的线程相关联。当调用子对象时,由于它们与非托管的上下文不相关,不需要进行 COM+ 上下文的更改,因而线程仍保持根的非托管上下文 ID。当子对象调用资源管理器时,资源管理器将从执行该子对象的线程中提取非托管上下文,即根对象的非托管上下文。依赖于这种方法是危险的,并且在以后的版本中,非托管上下文可能与托管上下文合并,因此,子对象将与潜在的、不同的托管上下文相关联,资源管理器将不再获得根对象的上下文。因此,升级到 .NET 的新版本可能会破坏依赖于这种行为的代码。
在这一节中,将比较托管客户端、托管服务器服务的组件解决方案与非托管客户端/服务器解决方案的性能。下表介绍了进程内的情况。为事务配置的 ServicedComponent 是用 C# 编写的,带有简单添加数字的单一方法。用相应的 C++ 实现进行比较。这种比较在不做任何实际工作的情况下,显示出托管和非托管的解决方案之间的不同。在托管解决方案中,进程内激活的速度大约要慢上 3.5 倍,而且当有上下文切换时,方法调用的时间大约要多 2 倍。但是,当比较需要上下文切换的受服务组件方法调用和不需要上下文切换的受服务组件方法调用时,它们大约相差 3 个数量级,这表明进程内受服务组件截取基础结构是非常成功的。对于进程外解决方案,激活的时间大约多出 2 倍,上下文间的方法调用大约多出 3 倍。
表 1 显示了使用托管和非托管解决方案时进程内激活和方法调用的时间比较。
表 1:进程内激活和方法调用
托管解决方案 | 非托管解决方案 | |
---|---|---|
激活 | 35 | 10 |
上下文间无操作方法调用 | 2 | 1 |
上下文间有操作方法调用 | 200 | 100 |
激活比无操作的方法调用的时间高一个数量级。加入一些工作以简单地获得一个 DTC 事务(但不对其做任何操作)可以使激活和方法调用的时间达到相同的数量级。当方法调用只简单地打开一个缓冲的数据库连接,其工作速度将比激活和无操作方法调用的结合高一个数量级,这就证明了当实验中加入实际工作时,受服务组件基础结构的系统开销只是理论值而已。
一般来讲,实时 (JIT) 服务不单独使用。它隐式地与事务服务一起使用,而且多数是与对象池一起使用。但是,此示例显示出一些有趣的主题。在以下代码中,.NET 类仅使用 JIT 服务编写。
using System; using System.EnterpriseServices; [assembly: AssemblyKeyFile("Demos.snk")] [assembly: ApplicationName("JITDemo")] namespace Demos { [JustInTimeActivation] public class TestJIT : ServicedComponent { public TestJIT() { // 首先获得调用 } [AutoComplete] public void DoWork () { // 用以下方法显示完成 ... // 1. autocomplete 属性或 // 2. ContextUtil.DeactivateOnReturn = true 或 // 3. ContextUtil.SetComplete(); } public override void Dispose(bool b) { // 有选择地替换此方法并使用您自己的 // 自定义 Dispose 逻辑。如果 b==true,则从客户端调用 Dispose(), // 如果等于 false,GC 将清除对象 } } }
该类从 ServicedComponent 中派生,并使用 JIT 属性指明所需的特定服务。为重载非托管代码中的 Activate 和 Deactivate 方法,要求类实现 IObjectControl 接口。而 ServicedComponent 类则有虚方法,可被重载以处理 Activate 和 Deactivate 事件。但是,ServicedComponent 及其真正代理 SCP 都不能实现 IObjectControl。相反,当 COM+ 请求 IObjectControl 接口时,SCP 将创建一个代理剥离程序。然后,在剥离程序上的 COM+ 的调用被发送到 ServicedComponent 的虚方法中。DeactivateOnReturn 位是通过使用方法上的 AutoComplete 属性、调用 ContextUtil.SetComplete()、ContextUtil.SetAbort() 或设置 ContextUtil.DeactivateOnReturn 来设置的。假定 DeactivateOnReturn 位是在每个方法调用的过程中设置的,则方法调用的顺序将为:类的构造函数、Activate、实际方法调用、Deactivate、Dispose(true),最后是类的终结器(如果存在)。进行另一个方法调用时,将重复相同的顺序。一个好的设计只需重载 Activate 和 Deactivate 方法即可知道对象何时被取出,何时又放回对象池。Activate 和 Deactivate 的其他逻辑应放置在类的构造函数和 Dispose(bool) 方法中。可以使用以下方法之一设置 DeactivateOnReturn 位:
所有受服务组件都有相关联的 COM+ 上下文,它作为引用存储在 SCP(在远程情况下,则为 RSCP)中。只有当发生 GC 或客户端调用 Dispose() 时,才释放该引用。最好不要依赖 GC 来清除上下文:COM+ 上下文束缚在一个 OS 句柄和一些内存上,从而可能会延迟这些句柄的释放,直至发生 GC。而且,尽管 ServicedComponent 没有终结器,但 SCP 实现了一个终结器,这意味着 COM+ 的上下文引用永远不能在第一次回收时被回收。实际上,当最终调用 SCP 上的终结器时,上下文还没有被终结器线程销毁,相反,销毁上下文的工作已经从终结器中删除,而被放到了内部队列中。这样做是因为人们发现,在某种压力环境中工作时(即受服务组件被快速创建、使用和放到范围之外),终结器线程会消耗大量资源。因而使用内部线程为队列提供服务,销毁旧的上下文。此外,任何创建新的 ServicedComponent 的应用程序线程都将首先试图从队列中取出一个项目并销毁旧的上下文。因此,从客户端调用 Dispose() 将使用客户端线程更快地剥离 COM+ 上下文,而且,它将释放上下文所消耗的句柄和内存资源。有时 Dispose() 可能会引发异常。一种情况是,如果对象驻留在已经终止的非根事务上下文中,则 Dispose() 调用可能会引发 CONTEXT_E_ABORTED 异常。另一种情况将在对象池中说明。
从性能角度来看,最好不要在 ServicedComponent 的派生类中实现终结器,而将此逻辑放到 Dispose(bool) 方法中。尽管 SCP 实现了终结器,但真正对象的终结器是使用反射调用的。
使用 JIT 的一个较好的设计方法为:
这里假定客户端是托管的,并且组件是进程内的。当组件在进程外时:(在“远程组件”一节中将详细介绍)
对象池的基本用途是对象的重用。对象池通常与 JIT 一起使用。缓冲的 COM 组件和 .NET 组件都是如此。
using System; using System.EnterpriseServices; [assembly: AssemblyKeyFile("Demos.snk")] [assembly: ApplicationName("OPDemo")] namespace Demos { [ObjectPooling(MinPoolSize=2, MaxPoolSize=50, CreationTimeOut=20)] [JustInTimeActivation] public class DbAccount : ServicedComponent { [AutoComplete] public bool Perform () { // 进行一些操作 } public override void Activate() { // .. 处理 Activate 消息 } public override void Deactivate() { // .. 处理 Deactivate 消息 } public override bool CanBePooled() { // .. 处理 CanBePooled 消息 // 基本实现返回 false return true; } } }
正如使用 JIT 的情况,可以通过两种方法使用对象池:
管理员可以在程序集部署和注册后修改池的大小和超时。池大小的变化将在重新启动进程时生效。在 Windows XP 或更好的版本中,池的大小应用到进程中的每个应用程序域中。在 Windows 2000 中,池的大小是在驻留在默认应用程序域中的缓冲对象的进程范围内有效的,这意味着,如果相同进程中的另一个应用程序域需要缓冲对象,客户端能够有效地跨过应用程序域以获取缓冲对象。要实现这一点,可以从 ASP.NET 应用程序中(其中每个 IIS vroot 都驻留在独立的应用程序域中)使用在 COM+ 库应用程序中定义的缓冲的 .NET 对象。
受服务组件不能使用参数化的构造函数。
.NET 框架的安全性促使代码仅在具有访问权限时才能访问资源。为了表示这一点,.NET 框架使用了权限的概念,它代表代码访问受保护资源的权力。代码请求所需的权限。.NET 框架提供代码访问权限类。也可以编写自定义的权限类。这些权限可用于向 .NET 框架指明代码需要获得的操作许可以及代码的调用者必须获得的操作授权。任何通过 System.EnterpriseServices 的代码路径都需要请求非托管的代码权限。
如果代码是从网上下载的,并且不能完全信任作者,那么 .NET 中的代码访问安全性在这样的应用程序中会非常有用。通常,使用受服务组件的应用程序是可以完全信任的,它需要安全性以便在多个进程间流动,并在部署时启用角色配置。这些是由 COM+ 基于角色的安全性提供的功能。
任何通过 System.EnterpriseServices 的代码路径都需要请求非托管的代码权限。这意味着:
此外,在 .NET 版本 1 中,切换线程时不复制安全性堆栈,因此不应在受服务组件中使用自定义的安全性权限。
System.EnterpriseServices 为 .NET 对象提供了采用 COM+ 安全性机制功能的安全性服务。当使用 COM+ 服务器应用程序保留组件时,RBS 功能要求使用 DCOM 传输协议从远程客户端激活组件。下一节将介绍有关远程的详细信息。因此,COM+ 中的安全性调用上下文和标识可以用于托管代码。此外,CoImpersonateClient、CoInitializeSecurity 和 CoRevertClient 是通常用在服务器端的常用调用,而 CoSetProxyBlanket 通常用在客户端。
某些安全性设置并不是使用属性存储在元数据中的,例如,向角色添加用户和设置进程安全性标识等。但是,程序集级的属性可用于配置 COM+ 服务器应用程序在 COM+ 资源管理器中的安全性选项卡中的内容:
要使用于 COM+ 库应用程序的任何访问权限检查有意义,请选择在进程级和组件级执行访问权限检查。
可以在程序集级、类级或方法级上应用 SecurityRole 属性。当应用到程序集级时,角色中的用户可以激活应用程序中的任何组件。当应用到类级时,角色中的用户还可以在组件上调用任何方法。应用程序级和类级的角色可以在元数据中配置,或通过访问 COM+ 目录以管理的方式进行配置。
使用元数据在程序集级上配置 RBS:
[assembly: ApplicationAccessControl(true, AccessCheckLevel=AccessChecksLevelOption.ApplicationComponent)] // 向此角色添加 NTAuthority\everyone [assembly:SecurityRole("TestRole1",true)] // 以管理的方式向角色添加用户 [assembly:SecurityRole("TestRole2")]
使用元数据在类级上配置 RBS:
[assembly: ApplicationAccessControl(true, AccessCheckLevel=AccessChecksLevelOption.ApplicationComponent)] ?[ComponentAccessControl()] [SecurityRole("TestRole2")] public class Foo : ServicedComponent { public void Method1() {} }
在程序集级或类级上的 RBS 可以通过管理的方式进行配置,因为在注册程序集后,那些实体将存在于 COM+ 目录中。但是,如前面所述,类方法并不出现在 COM+ 目录中。要在方法上配置 RBS,该类必须实现接口的方法,并且必须在类级上使用 SecureMethod 属性,或在方法级上使用 SecureMethod 或 SecurityRole 属性。此外,属性必须出现在类方法的实现中,而不是出现在接口定义的接口方法中。
[assembly: ApplicationAccessControl(true, AccessCheckLevel=AccessChecksLevelOption.ApplicationComponent)] Interface IFoo { void Method1(); void Method2(); } [ComponentAccessControl()] [SecureMethod] public class Foo : ServicedComponent, IFoo { // 以管理的方式向此方法添加角色 public void Method1() {} // “RoleX”被添加到此方法的目录中 SecurityRole("RoleX") public void Method2() {} }
在类级上使用 SecureMethod 可以使该类中所有接口上的所有方法都使用 COM+ 目录中的角色以管理的方式进行配置。如果该类实现了两个具有相同方法名称的接口,并且角色是以管理的方式配置的,那么必须在这两种方法出现在 COM+ 目录中时对它们进行角色配置(除非该类实现了特定的方法,如 IFoo.Method1)。但是,如果在类方法上使用了 SecurityRole 属性,则当注册程序集时,所有同名的方法都将自动使用该角色进行配置。
[assembly: ApplicationAccessControl(true, AccessCheckLevel=AccessChecksLevelOption.ApplicationComponent)] Interface IFoo { void Method1(); void Method2(); } [ComponentAccessControl()] public class Foo : ServicedComponent, IFoo { // 以管理的方式向此方法添加角色 [SecureMethod] // 或使用 SecurityRole(转换为 SecureMethod++) public void Method1() {} public void Method2() {} }
在此示例中,IFoo 和两种方法都出现在 COM+ 目录中,因此可以在每种方法上以管理的方式配置角色,但是,方法级 RBS 只能在 Method1 上实施。将 SecureMethod 或 SecurityRole 用于所有方法(将被要求参与方法级 RBS 安全性)上,或者如前面所述,将 SecureMethod 放置到类级上。
只要在方法级上配置 RBS,就需要 Marshaller 角色:当进行方法调用而在方法上没有配置 RBS 时,受服务组件基础结构将调用 IRemoteDispatch。当进行方法调用且在方法上配置了 RBS 时(当存在 SecureMethod 属性时),则使用 DCOM(使用与方法相关联的接口)进行方法调用。因此,DCOM 将保证在方法级上实施 RBS。但是,如“激活”和“截取”各节中所述,然后,COM 互操作和 RSCP 将调用 IManagedObject(以便让远程激活器将引用封送到其空间内)和 IServicedComponentInfo(以便查询远程对象)。这些接口与受服务组件相关联。由于组件被配置为进行方法级检查,因此,如果基础结构要成功进行这些调用,将需要一个角色与这些接口相关联。
因此,当注册程序集时,向应用程序添加了一个 Marshaller 角色,然后,必须以管理的方式向该角色添加用户。通常,应用程序的所有用户都添加到该角色中。这与非托管 COM+ 有些不同,在非托管 COM+ 中,在方法上配置 RBS 不需要此附加的配置步骤。在注册时自动向该角色添加“Everyone”是一个潜在的安全漏洞,因为现在每个人都可以激活(但不能调用)组件,而在此之前,他们可能无权激活它们。Marshaller 角色还将添加到 IDisposable 接口,以使客户端能够清除该对象。Marshaller 角色的一种替代方法是用户向所提到的三个接口中的每个接口添加相关角色。
ServicedComponent 类在其继承树中包含了 MarshalByRefObject,因此可以从远程客户端进行访问。有很多方法可以远程公开受服务组件。可以使用以下方法远程访问受服务组件:
为了使用 DCOM 远程访问受服务组件并将其保留在 Dllhost 中,首先要确保在 COM+ 服务器应用程序中注册程序集,并把它放在服务器计算机上的 GAC 中。然后,使用 COM+ 应用程序导出功能为应用程序代理创建一个 MSI 文件。在客户端上安装应用程序代理。托管程序集嵌入在应用程序代理中。安装程序也将注册该程序集并将它放到客户端计算机上的 GAC 中。因此:
从客户端的托管代码中激活服务器组件后,其基础结构如图 6 所示。
使用 DCOM 意味着 CLR 保留在 Dllhost 中,也就是说,应用程序配置文件 dllhost.exe.config 驻留在 system32 目录中。这也意味着配置文件将应用于计算机上的所有 Dllhost 进程。在 .NET 框架的下一个版本 (V1.1) 中,可以在 COM+ 应用程序中设置 COM+ 应用程序的根目录,该目录用于为应用程序查找配置文件和程序集。
对于客户端激活的对象,每当请求对象的 URI 时,即为该对象创建一个生存期租约。如前面“激活”一节中所述,URI 是由远程受服务组件代理请求的。当现有的进程内受服务组件被封送到远程进程时也会出现这种情况 - 每当 MBR 对象被 .NET 封送到应用程序域以外时,都要请求 URI。URI 用于确保 .NET 中的对象标识是唯一的,并防止出现代理链。因此,当托管客户端激活一个远程受服务组件时,将对服务器对象使用租约时间。请注意,非托管客户端在客户端没有远程受服务组件代理,因此不请求对象的 URI。它使用 DCOM 来确保对象的标识。因此,从非托管客户端激活受服务组件时不对其使用租约时间。
当受服务组件带有租约时间时,最好将 InitialLeaseTime 和 RenewOnCallTime 超时值设置为一个较小的值,甚至可以为 10 秒。可以使用 Dispose() 或让 GC 清除对象以销毁受服务组件。当调用 Dispose() 时,远程受服务组件代理将释放在 DCOM 代理上的引用,然后使自己可用于下一个 GC。服务器对象将处理 Dispose 调用(或创建一个新的服务器对象以便为远程调用提供 Dispose() 服务),销毁相关联的 COM+ 上下文,然后使自己可用于下一个 GC,但只有当超过租约时间时才可以使用。当客户端没有调用 Dispose() 时,服务器将首先等待客户端的 GC 释放对 DCOM 代理的引用,然后在超过租约时间时,才能使自己和 COM+ 上下文可用于下一个 GC。因此,要调用 Dispose(),并且还要降低默认的租约时间。即使客户端仍然存在,而租约时间已过期,对服务器对象的 DCOM 引用将使服务器对象继续生存。但是,不能总使用 DCOM 引用以维持受服务组件的生存状态。当客户端通过 CLR 远程通道或 COM+ SOAP 服务访问对象时,只有与租约有关的强大引用才能使受服务组件继续生存。
本文只讨论了可用于托管代码的一些服务。所有 COM+ 服务都可用于托管代码,如事务隔离级别、进程初始化、无组件的服务以及进程循环等。现在,.NET 框架以一致且具有逻辑性的方式提供了对所有 COM+ 服务的同等访问。而且,.NET 框架的许多创新部分,如 ASP.NET、Microsoft ADO.NET 和消息传递等,与 .NET 企业服务紧密集成在一起,从而可以利用诸如事务和对象池等服务。这种集成提供了一致的体系结构和编程模型。System.EnterpriseServices 命名空间提供了向托管类添加服务的编程模型。
本站文章除注明转载外,均为本站原创或翻译。欢迎任何形式的转载,但请务必注明出处、不得修改原文相关链接,如果存在内容上的异议请邮件反馈至chenjj@evget.com