香农·帕尔
Microsoft Corporation
2002 年 4 月
摘要:提供 Microsoft .NET 和 COM+ 服务集成背后的技术详细信息,并描述可用于托管代码的服务。 ) (26 个打印页
目录
简介
事务
部署
服务组件
对象生存期
安全性
远程组件
结论
简介
本文需要熟悉 Microsoft®.NET Framework和 COM+ 服务。 熟悉企业服务不是必需的,但会有所帮助。 有关这些主题的良好背景信息,请参阅:
- Derek Beyer 的著作, C# COM+ Programming (Professional Mindware,2001)
- Tim Ewald 的文章, COM+ 集成:.NET 企业服务如何帮助你构建分布式应用程序
- Jeffrey Richter 的文章,第 2 部分:Microsoft .NET Framework为集成的Service-Oriented Web 提供平台
COM 提供了一种编写基于组件的应用程序的方法。 众所周知,编写 COM 组件所需的管道工作非常重要且重复。 COM+ 与其说是关于 COM 的新版本,不如说是新版 COM;COM+ 为组件提供服务基础结构。 组件生成并安装在 COM+ 应用程序中,以便生成可缩放的服务器应用程序,这些应用程序可在轻松部署的情况下实现高吞吐量。 (如果组件不需要使用任何服务,则不应将其放置在 COM+ 应用程序中) 。 可伸缩性和吞吐量是通过从一开始就设计应用程序来利用事务、对象池和活动语义等服务来实现的。
.NET Framework提供了编写基于组件的应用程序的另一种方法,并且比 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 上服务的完整列表,请参阅平台 SDK 中的 COM+ 提供的服务 。 Microsoft Windows XP 包括 COM+ 的新版本,即 COM+ 1.5,它具有其他服务,这些服务也可以与 .NET 组件一起使用。
事务
若要编写使用服务的托管应用程序,需要服务的类必须派生自 ServicedComponent,并使用各种自定义属性来指定所需的实际服务。 本部分介绍这些概念以及它们如何影响编写托管代码。 后面的部分提供了更详细的说明。
假设已编写一个类 Account (实际代码稍后) 列出,并且位于 BankComponent 程序集中。 此类的使用方式如下:
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+ 应用程序相关联。 代码中突出显示了此类所需的进一步修改。
BankComponent 服务器
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)
{
// Updates the database, no need to call SetComplete.
// Calls SetComplete automatically if no exception is thrown.
}
}
}
BankComponent 服务器命名空间中的代码显示了在 .NET 中使用 COM+ 服务是多么容易。 下面列出了从编码到部署的完整过程摘要:
写入服务器程序集。
生成程序集:
对程序集进行签名。 可为项目生成一次密钥文件。 无需为每个编译生成一个。 可以使用 Microsoft .NET 命令提示符创建密钥,sn.exe:
sn –k Demos.snk编译代码。 必须为 System.EnterpriseServices 添加引用。
部署应用程序。
使用服务组件的程序集必须注册到 COM+ 目录。 ServicedComponent 类和自定义属性是从托管代码访问 COM+ 服务的两个关键概念。 服务的配置存储在 COM+ 目录中。 对象驻留在 CLR 中并执行。 图 1 中描绘了托管对象及其关联的 COM+ 上下文,接下来的两节将更加清晰。
.gif)
图 1. 与托管组件关联的服务
使用 COM+ 组件时,需要手动配置目录,但使用服务组件时,可以根据代码中的属性更新目录。 可以使用命令行工具regsvcs.exe或通过编写访问托管 API 的脚本来显式注册程序集。 下面的部署详细信息部分提供了更多详细信息。 在开发过程中,只需将程序集复制到应用程序目录,即可方便地进行 XCopy 部署。 每当客户端应用程序创建派生自 ServicedComponent 的类的实例时,运行时会检测它是否已在 COM+ 应用程序中注册了程序集。 如果尚未注册,则会在本地目录中搜索程序集,如果找到,程序集中的所有服务组件都会在 COM+ 应用程序中注册,然后激活可以继续。 这称为延迟注册,但并非适用于所有方案。 例如,标记为 COM+ 服务器应用的程序集需要显式注册 (请参阅下面的) ,而延迟注册不适用于调用托管服务组件的非托管客户端。 延迟注册在开发期间非常有用,否则使用脚本、代码或 RegSvcs 注册程序集。
可能将程序集放在 GAC 中。 有关更多详细信息,请参阅部署部分。
运行客户端。
部署
自定义属性是从托管代码访问 COM+ 服务的两个关键概念之一。 自定义属性用于指定所需的服务,例如前面的代码列表中的 Transaction 自定义属性。 这些属性将服务的配置选项存储在程序集元数据中。 自定义特性的使用方式是:让一些代码加载程序集并使用反射、创建属性的实例,并调用属性上的方法来提取存储在 属性中的服务配置。 然后,可以将信息写入 COM+ 目录。 执行这些步骤和其他步骤的代码包含在 EnterpriseServices.RegistrationHelper 中。 为了简化注册过程,所有注册形式都使用 组件 EnterpriseServices.RegistrationHelper。 此组件可作为托管类和 COM 对象进行访问。
.gif)
图 2. 注册服务组件
从概念上讲,RegistrationHelper 执行以下步骤:
- 使用 RegistrationServices.RegisterAssembly 在注册表中注册程序集。 因此,类在注册表中显示为用托管代码编写的 COM 组件,并且具有指向mscoree.dll的 InprocServer32 键。 如果托管类未实现任何接口,则类的公共方法不会显示在 COM+ 目录中,除非使用 ClassInterfaceAttribute。 这意味着与方法级别关联的服务配置不能存储在目录中。 但是,某些 COM+ 服务可以在方法级别进行配置,并要求组件公开在 COM+ 目录中查看的接口。 例如,方法级别基于 COM+ 角色的安全性要求组件实现 接口才能配置服务。 有关此问题的详细信息,请参阅安全性部分。
- 使用 TypeLibConverter 从程序集生成 COM 类型库。 ConvertAssemblyToTypeLib。
- 注册类型库。 到目前为止,这与 RegAsm.exe /tlb 非常相同。
- 查找或创建 COM+ 应用程序。 该名称是从 ApplicationName 属性、程序集名称或提供的应用程序名称/GUID 中提取的。
- 使用类型库通过 COM+ 管理 API 配置 COM+ 应用程序。
- 遍历所有自定义属性,并使用 IConfigurationAttribute 将特定服务的配置数据写入 COM+ 目录。
RegistrationHelper 将尝试使用 RegistrationHelperTx(安装 .NET 时创建的 COM+ 应用程序中的类)在事务中执行这些步骤。 因此,如果注册失败,COM+ 目录和注册表将还原到其原始状态。 但是,目前,如果程序集位于 GAC) 中,则生成的类型库将保留在磁盘 (或 GAC 中。 如果要注册的程序集引用其他也使用 COM+ 服务的程序集,则依赖项关系图中的所有程序集都将执行上面列出的相同步骤。
由于 RegistrationHelper 访问 COM+ 目录,因此它需要非托管代码权限和计算机上的管理员权限。 因此,RegistrationHelper 客户端也是如此:延迟注册、RegSvcs 或脚本/代码。 这也意味着无法注册从 Internet 下载或存储在网络共享上的代码。
可以编写不兼容的属性组合,例如要求事务和将同步设置为禁用。 在将这些组合写入 COM+ 目录时,当前在注册期间检测到这些组合,而不是在编译期间检测到。 某些属性对其他属性具有隐式依赖项,例如,仅使用 Transaction 属性时,这等效于使用 Transaction、JustInTimeActivation 和 Synchronization 属性。 注册托管组件时,除非使用属性覆盖“未配置”默认值,否则将使用 COM+ 目录默认值。 例如,如果某个组件已注册且未指定 Transaction 属性,则目录中事务设置的未配置默认值将设置为 TransactionOption.Disabled。 此方法允许开发人员在组件不再需要某个属性时从代码中删除该属性,然后在再次注册程序集时,将适当重置事务的目录条目。 联机文档中指定了这些未配置的默认值的详细列表。 默认配置的值是属性参数中的默认值,例如,仅使用属性 [Transaction] 指示 TransactionOption.Required。
由于托管类上的服务的配置数据存储在 COM+ 目录中,因此某些目录条目也可能在注册程序集后进行管理修改。 不应以这种方式修改某些服务。 例如,禁用目录中的事务服务可能会导致代码运行错误。 可以在注册后操作特定于部署的设置,例如对象构造字符串和安全角色。 进行注册后设置时,对包含服务组件的程序集进行 XCopy 部署可能不够。 COM+ 应用程序导入和导出功能有助于分发应用程序的当前状态。 远程处理部分提供了有关导入和导出的详细信息。
在某些情况下,不会针对配置数据查阅目录,而只是从程序集元数据中提取的。 尽管池大小是从目录) 和安全方法属性中提取的,但这种情况适用于 AutoComplete、JIT、对象池 (。 有关此问题的更多详细信息,请参阅介绍这些服务的章节。
注册程序集的过程将自动生成 COM+ 所需的 GUID。 如果程序集未签名,则仅基于类型和命名空间名称生成 GUID。 因此,如果未对程序集进行签名,则可以生成非唯一 GUID。 对甚至不使用 COM+ 服务但需要唯一类型名称的 .NET 程序集施加了类似的命运。 因此,必须使用 COM+ 服务的签名。 如果未对程序集进行签名,注册将失败。 注册还意味着使用 COM+ 服务的 .NET 类具有一个全局配置数据存储。 尽管可以将专用程序集复制到多个应用程序目录,但所有此类应用程序最终都会引用服务组件的一个配置数据。 因此,更改 COM+ 目录中的配置数据会影响使用 类的所有应用程序。 Microsoft ASP.NET 配置中的多个 vroot 都构成使用服务组件的同一程序集的副本,这一点显而易见。 为同一 COM+ 应用程序设置多个配置的一种方法是在 Microsoft Windows .NET 上使用 COM+ 分区。 若要在 .NET 中使用 COM+ 分区服务,请不要使用 ApplicationID 属性, 为了在多个分区中安装相同的组件,COM+ 需要唯一的应用程序 ID。
通常,每当客户端需要访问与客户端应用程序目录不在同一目录中的程序集,或者程序集加载到另一个进程(该进程不在客户端的目录中)时,将使用 GAC。 从概念上讲,使用服务组件的专用程序集实际上是共享程序集-它们使用共享的配置数据。 如果将 ApplicationActivationOption 设置为 library,则有可能在程序集中的类上使用事务,并在一个客户端中使用该程序集(如果所有程序集都是从同一目录加载的)。 当使用 ApplicationActivationOption 的程序集设置为 server 时,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+ 应用程序中类的正确版本。 将程序集写入 (使用服务组件的托管服务器) 时的一些并行方案如下: (下面使用的激活的某些方面将在下一部分中介绍)
- 托管客户端、托管服务器,程序集中不使用固定 GUID。
- 客户端将加载版本策略指定的程序集。
- 托管客户端、托管服务器、使用的固定 GUID。
- 如果客户端激活类并使用版本策略访问旧版本的程序集,则代码中的固定 GUID 将在激活期间用于从目录中提取服务信息。 因此,使用此 GUID 的最后一个注册程序集中的信息将用于创建对象,该对象实际上可能是较新的版本,因此在尝试从实际创建的对象 (v2) 强制转换到代码 (v1) 中的引用时,会出现类型强制转换异常。
- 托管客户端、托管服务器、无固定 GUID,仅更改内部版本号。
- 尽管将生成新的 GUID,但类型库仍将具有相同的版本号,因为类型库只有两个版本的数字。 这可能仍然有效,但如果版本 2 安装在版本 1 上,然后卸载版本 1,则版本 2 的类型库将取消注册。 解决方案 1:下一个版本的 .NET Framework (V1.1) 通过启用独立于程序集的版本控制类型库来解决此问题。 这意味着在更改程序集版本号时,应更改类型库版本。 解决方案 2:仅使用主版本号和次要版本号。
- 非托管客户端、托管服务器,不使用固定 GUID。
- 客户端将使用 GUID 创建组件。 互操作会将 GUID 解析为名称,然后应用版本策略。 如果程序集的版本 1 和版本 2 位于计算机上,并且策略用于访问版本 2,则非托管客户端将获取版本 2。
- 安装版本 1,安装版本 2,卸载版本 1。 现在,客户端无法创建组件,除非存在要重定向到版本 2 的版本策略。 此外,对于版本 1 注册信息,必须存在注册表项。 为已卸载的版本 1 创建注册表信息的一种方法是在 Windows XP 上使用 COM+ 别名功能。
版本控制适用于同一 COM+ 应用程序中的所有组件,也就是说,没有自动对应用程序本身进行版本控制的方法。 例如,不能使用版本策略对应用程序上的角色进行版本控制。 使用应用程序名称属性执行应用程序版本控制。
服务组件
激活
企业服务基础结构基于上下文的概念。 上下文是具有类似执行要求的对象的环境。 可以在激活和/或方法调用拦截期间强制实施服务。 尽管 COM+ 服务是用非托管代码编写的,但 COM+ 服务与 .NET 的集成远不止是在 .NET 中使用 COM 互操作技术。 如果不从 ServicedComponent 派生,注册过程将不会产生所需的效果。
可以通过各种组合激活和托管服务组件。 如图 3 所示,本讨论将涉及三种情况:进程内 (相同的应用域) 、跨应用域 (同一进程) 和跨进程激活。 这些情况的重要性在于对组件进行调用时跨越的边界。 进程内激活会产生潜在的跨上下文边界,跨应用域案例具有跨上下文和跨应用域边界,而跨进程案例处理跨计算机/跨进程和跨上下文边界。
.gif)
图 3. 服务组件的激活主机
服务组件的实现依赖于 .NET 远程处理,后者提供了一种可扩展的机制来插入以非托管或托管代码编写的服务。 服务组件派生自 ContextBoundObject 并实现各种接口,例如 IDisposable。 可以使用 ProxyAttribute 派生的自定义属性轻松自定义 CLR 中的激活链。 可以通过编写自定义真实代理来自定义拦截。 当需要新的服务组件派生类时,会自定义激活链,以便激活调用实际调用 CoCreateInstance 的托管 C++ 包装器。 这允许 COM+ 根据以前注册的程序集中 COM+ 目录中存储的信息设置非托管上下文和服务。 这也是实现延迟注册的阶段。 在注册程序集期间,InprocServer32 键指向mscoree.dll,从而将 COM+ CreateInstance 最终重定向回运行时,以创建真正的托管对象。 因此,在激活期间,将创建自定义真实代理对象。 此代理的进程内版本称为服务组件代理或 SCP。 图 4 对此进行了描述。
.gif)
图 4。 激活路径
激活调用的返回路径通过非托管 COM+ 封送托管代码中的托管引用,并返回到托管代码 (图 4) 中第 1 行的反向路径。 根据实际对象的创建位置,客户端将引用取消封送为相关表单。 在进程内激活的情况下,图 5 指示该引用作为对透明代理的直接引用 (TP) 取消封送。 跨应用域引用作为 .NET 远程处理代理取消封送。 跨进程或跨计算机引用 (图 6) 需要更多非组合:COM 互操作在激活和取消组合期间对 ServicedComponent 实现的 IManagedObject 发出调用。 远程服务组件代理 (RSCP) 在激活期间对 IServicedComponentInfo 进行调用,以获取服务器对象的 URI,这意味着在激活期间进行了两次远程调用。 当方法级别需要 COM+ 基于角色的安全性时,这些接口需要与角色相关联,以便在基础结构对这些接口进行调用时,取消封送成功。 安全部分将讨论跨进程激活和封送处理对配置基于角色的安全性的影响。
.gif)
图 5。 进程内调用的基础结构
.gif)
图 6。 进程外调用的基础结构
因此,激活链已自定义,以创建自定义真实代理 (用于拦截) 并创建非托管上下文,使 COM+ 仅具有执行拦截服务语义所需的上下文基础结构。 COM+ 上下文现在与托管对象(而不是 COM 对象)关联。
Interception
图 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 运行时*,*中执行,因此调用方法时不需要将字符串等类型转换为 BSTR。 如果 COM 互操作用于进程内调用*,则需要数据封送处理。因此,在非托管代码中呈现服务的调用不是进程内调用的 COM 互操作调用。
.gif)
图 7。 进程内调用的基础结构
对静态方法的调用不会转发到透明代理和真实代理。 因此,静态方法无法使用拦截服务;而是在客户端的上下文中调用它们。 内部方法将在正确的上下文中被调用,这意味着对为新事务配置的对象调用内部方法的客户端将参与新事务。 但是,由于方法级服务需要 COM+ 目录中的接口 (本主题后面的更多内容,在安全) 中,无法为方法级服务配置内部方法。 服务可以应用于属性,但方法级属性 ((如 AutoComplete) )必须分别放在 getter 和 setter 方法上。
AutoComplete 属性是一种使用事务的便捷方式,无需编写任何代码来访问服务。 或者,可以使用 ContextUtil.SetAbort 或 ContextUtil.SetComplete。 通过在方法的属性上设置复选框,可以在 COM+ 资源管理器中配置此服务。 但是,托管对象不需要实现接口。 对于服务组件也是如此。 如果方法未在接口上声明,则方法级别服务的配置无法在注册时写入目录;配置只能存储在元数据中。 如果方法不存在接口,则当存在 AutoComplete 属性时,将使用 IRemoteDispatch.RemoteDispatchAutoDone 上存储的配置信息从 SCP 执行上下文切换。 如果 AutoComplete 不存在,则使用 IRemoteDispatch.RemoteDispatchNotAutoDone。 IRemoteDispatch 是由 ServicedComponent 实现的接口。 非托管客户端只能调用未使用 IDispatch (后期绑定) 的接口的服务组件,因此,由于在这种情况下没有真正的代理,因此无法强制实施自动完成语义。 即使使用接口,自动完成的配置仍由托管客户端的元数据驱动。 DCOM 方法调用仅在进程外的情况下对 RemoteDispatchAutoDone 进行。 进程外组件不使用 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 |
| Cross-context-do-nothing 方法调用 | 2 | 1 |
| 跨上下文 do-work 方法调用 | 200 | 100 |
激活的成本大约比对“无所事事”方法的调用高一个数量级。 添加一些工作,只是获取 DTC 事务 (但对它不执行任何操作,) 将激活和方法调用时间级别设置为相同的数量级。 当方法调用只是打开共用数据库连接时,方法调用工作比激活和“无所事事”方法调用的组合要大一个数量级,这证明在将实际工作添加到试验时,服务组件基础结构的开销是理论上的。
对象生存期
实时激活
实时 (JIT) 服务通常不会单独使用。 它与事务服务隐式使用,最常用于对象池。 但是,此示例有助于突出显示一些有趣的主题。 在下面的代码中,将编写仅使用 JIT 服务的 .NET 类。
using System;
using System.EnterpriseServices;
[assembly: AssemblyKeyFile("Demos.snk")]
[assembly: ApplicationName("JITDemo")]
namespace Demos
{
[JustInTimeActivation]
public class TestJIT : ServicedComponent
{
public TestJIT()
{ // First to get called
}
[AutoComplete]
public void DoWork ()
{ // Show doneness using ..
// 1. The autocomplete attribute or
// 2. ContextUtil.DeactivateOnReturn = true or
// 3. ContextUtil.SetComplete();
}
public override void Dispose(bool b)
{ // Optionally override this method and do your own
// custom Dispose logic. If b==true, Dispose() was called
// from the client, if false, the GC is cleaning up the object
}
}
}
类派生自 ServicedComponent 并使用 JIT 属性来指示所需的特定服务。 为了替代非托管代码中的 Activate 和 Deactivate 方法,需要 类来实现 IObjectControl 接口。 相反,ServicedComponent 类具有虚拟方法,可以重写这些方法来处理 Activate 和 Deactivate 事件。 但是,ServicedComponent 及其真实代理 SCP 都不实现 IObjectControl。 相反,当 COM+ 请求 IObjectControl 接口时,SCP 会创建代理拆解。 然后,COM+ 对拆解的调用将转发到 ServicedComponent 的虚拟方法。 使用 方法上的 AutoComplete 属性设置 DeactivateOnReturn 位,调用 ContextUtil.SetComplete () 、ContextUtil.SetAbort () 或设置 ContextUtil.DeactivateOnReturn。 假设在每个方法调用期间都设置了 DeactivateOnReturn 位,则方法调用的序列为:类的构造函数、Activate、实际方法调用、Deactivate、Dispose (true) ,最终是类的终结器(如果存在)。 进行另一个方法调用时,将重复相同的序列。 一种良好的设计做法是仅重写 Activate 和 Deactivate 方法,以了解对象何时被取出并放回对象池。 激活和停用的其余逻辑应放在类的构造函数和 Dispose (bool) 方法中。 可以使用以下方法之一设置 DeactivateOnReturn 位:
- 客户端仅将对象的状态用于单个方法调用。 在进入 方法时,将创建一个新的实际对象并将其附加到 SCP。 退出方法时,将停用真实对象,首先调用 Dispose (true) 后跟实际对象终结器(如果存在)。 但是,关联的 COM+ 上下文 SCP 和 TP 将保持活动状态。 客户端代码仍会保留对它认为是实际对象的引用, (透明代理) 。 客户端对同一引用进行的下一个方法调用将导致创建一个新的真实对象并将其附加到 SCP,以便为方法调用提供服务 (请参阅有关对象池的部分,以消除创建新对象) 的要求。 若要停用实际对象,实际对象需要在方法调用退出时指示完成性。 这可以通过以下方法实现:
- 类方法上的 AutoComplete 属性
- ContextUtil 类上的两个方法调用之一,即 DeactivateOnReturn 或 SetComplete
- 客户端在退出方法之前将 doneness 位设置为 false,对同一对象进行多个方法调用,而无需在每次方法调用后停用对象。 例如,限定在窗体级别使用 JIT 并具有两个窗体按钮的服务组件的范围,通过使方法显式将 doneness 位设置为 false 来调用同一对象实例上的方法。 在某些时候,“完成度”位应设置为 true。 此方法意味着客户端和对象之间存在协定。 这可由客户端隐式或显式完成:
- 客户端知道在对象完成时调用某个方法以停用对象。 方法实现使用选项 1 中的想法。 可以使用同一调用序列再次调用对象引用,这意味着将创建新的真实对象。
- 当客户端对对象调用 Dispose () 方法时,对象将被显式销毁。 Dispose () 是在 ServicedComponent 上定义的一种方法,并依次调用 Dispose (true) ,如果存在) 类的终结器,则 (然后拆解关联的 COM+ 上下文。 在这种情况下,不能对对象引用进行进一步的方法调用。 如果尝试此操作,将引发异常。 如果有多个客户端使用同一对象,则仅当最后一个客户端使用对象时,才应调用 Dispose () 。 但是,JIT 对象的无状态性质导致设计实践针对每个客户端模型的单个实例。
- 对象从不将其完成位设置为 true,客户端从不调用 Dispose () 。 发生垃圾回收时,实际对象、代理和上下文将被销毁。 GC 启动的方法调用顺序为 Deactivate、Dispose (false) 然后类终结器 ((如果存在) )。
所有服务组件都有一个关联的 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 的一个好设计做法是:
- 将自定义激活和最终化代码放在构造函数和 Dispose (bool) 方法中,而不是实现终结器,并通过使用方法上的 AutoComplete 属性指示完成性来使用单个调用模式。
- 当客户端使用 完 对象时,从客户端调用 Dispose () 。
讨论假定客户端是托管的,组件在处理中。 当组件进程外时: (远程处理部分概述了更多详细信息)
- 仅当客户端激活对象的 .NET 远程处理租约时间过期时,GC 才会清理对象。
- 如前所述,在进程外组件上调用方法时,DCOM 用于切换上下文并传递方法调用。 如果组件之前已由 JIT 停用,然后调用 Dispose () ,则会输入服务器上下文,并重新创建实际对象来为 DCOM 调用提供服务,最后再次停用。 对于进程内组件,如果已停用真实对象,则在处理 Dispose () 调用之前,不会尝试切换到正确的上下文, (这将重新激活组件) ,而是仅销毁上下文。
对象池
对象池的基本前提是对象重用。 对象池最常与 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 ()
{ // Do something
}
public override void Activate()
{ // .. handle the Activate message
}
public override void Deactivate()
{ // .. handle the Deactivate message
}
public override bool CanBePooled()
{ // .. handle the CanBe Pooled message
// The base implementation returns false
return true;
}
}
}
与使用 JIT 时一样,可以通过以下两种方式之一使用对象池:
- 单个调用模式。 在代码中,当客户端尝试进行方法调用时,将从池中检索对象,并在从单个方法调用退出时返回到池,假设 JIT 与对象池一起使用,并在方法调用期间将完成位设置为 true。 此处也适用于使用 JIT 的相同单次调用方法。 在创建对象并将其放置在池中时,仅调用一次构造函数。 使用 JIT 和共用对象时的方法调用顺序为:Activate、方法调用、Deactivate 和 CanBePooled。 如果 CanBePooled 返回 true,则对象将重新放入池 (尽管上下文保持活动状态,如前面) 所述。 在从池中提取任意对象后, (不) 再次调用构造函数的后续方法调用重复相同的方法调用顺序, (服务组件不能) 使用参数化构造函数。 最后,如果客户端在共用对象上调用 Dispose () ,则仅在进程内案例中销毁上下文。 如前所述,在进程外的情况下,调用 Dispose () 可以重新激活对象。 如果对象已入池,则必须从池中获取一个对象,这意味着 Dispose () 可能会引发具有CO_E_ACTIVATION_TIMEOUT的异常。
- 多呼叫模式。 使用 JIT 服务中突出显示的类似多个方法调用方法,只有在对对象进行多次方法调用后,才能将对象放回池中。 但是,如果客户端不调用 Dispose 且未使用 JIT,则无法确保当 GC 将对象放回池时,无法确保需要最终处理的池对象的子对象能够重新出现。 当对共用对象进行垃圾回收时,在 Deactivate 中也不能保证哪些成员仍然有效。 在.NET Framework (V1.1) 的下一个版本中,不会调用 canBePooled 和 Deactivate,也不会将对象放回池中。 使用此方法时,模型更加一致 - 在 Deactivate 子对象处于活动状态中,在 Dispose () 中,不保证子对象处于活动状态。 因此,对于不使用 JIT 的共用对象调用 Dispose () 至关重要,否则对象不会返回到池。
管理员可以在部署和注册程序集后修改池大小和超时。 重启进程时,池大小更改将生效。 在 Windows XP 或更高版本上,池大小适用于进程中的每个应用程序域。 在 Windows 2000 上,池大小为进程范围,共用对象驻留在默认应用程序域中,这意味着,如果同一进程中的另一个应用域需要共用对象,则客户端跨应用域有效地与共用对象通信。 实现此目标的一个实现是使用 COM+ 库应用程序中从 ASP.NET 应用程序中定义的共用 .NET 对象,其中每个 IIS vroot 都托管在单独的应用程序域中。
服务组件不能使用参数化构造函数。
安全性
代码访问安全性 (CAS)
仅当代码有权访问资源时,.NET Framework安全性才允许代码访问资源。 为了表达这一点,.NET Framework使用权限的概念,该概念表示代码访问受保护资源的权限。 代码请求所需的权限。 .NET Framework提供代码访问权限类。 或者,可以编写自定义权限类。 这些权限可用于向.NET Framework需要允许代码执行哪些操作,以及指示必须授权代码的调用方执行哪些操作。 通过 System.EnterpriseServices 的任何代码路径都请求非托管代码权限。
.NET 中的代码访问安全性在从 Web 下载代码且作者可能不完全信任的应用程序中最为有用。 通常,使用服务组件的应用程序是完全受信任的,需要安全性才能在多个进程之间流动,并在部署时启用角色配置。 这些是 COM+ 基于角色的安全性公开的功能。
通过 System.EnterpriseServices 的任何代码路径都需要非托管代码权限。 这意味着:
- 需要非托管代码权限才能在服务组件上激活和执行跨上下文调用。
- 如果将对服务组件的引用传递给不受信任的代码,则无法从不受信任的代码调用 ServicedComponent 上定义的方法。 但是,在某些情况下,在派生自 ServicedComponent 的类上定义的自定义方法可以从不受信任的代码调用:对不需要上下文切换、拦截服务的自定义方法进行调用,如果方法的实现不调用 System.EnterpriseServices 的成员,则可以调用不受信任的代码。
此外,在 .NET 版本 1 中,在进行线程切换时不会复制安全堆栈,因此不应在服务组件中使用自定义安全权限。
Role-Based Security (RBS)
System.EnterpriseServices 为 .NET 对象提供安全服务,这些对象镜像 COM+ 安全机制的功能。 使用 COM+ 服务器应用程序托管组件时,RBS 功能要求使用 DCOM 传输协议从远程客户端激活组件。 下一部分提供了有关远程处理的详细信息。 因此,COM+ 中的安全调用上下文和标识可用于托管代码。 此外,CoImpersonateClient、CoInitializeSecurity 和 CoRevertClient 是通常用于服务器端的常见调用,而 CoSetProxyBlanket 通常用于客户端。
某些安全设置不使用属性存储在元数据中,例如,将用户添加到角色和设置进程安全标识。 但是,程序集级别属性可用于配置 COM+ 服务器应用程序的 COM+ 资源管理器的“安全”选项卡中显示的内容:
为应用程序启用授权 (ApplicationAccessControlAttribute (bool) ) 。 若要支持 RBS,必须满足此要求。
安全级别 (ApplicationAccessControlAttribute (AccessChecksLevelOption) ) 。 如果设置为 AccessChecksLevelOption.Application,则分配给应用程序中角色的用户将添加到进程安全描述符,并在组件、方法和接口级别关闭精细角色检查。 因此,仅在应用程序级别执行安全检查,库应用程序依赖于主机进程实现进程级安全性。 如果属性设置为 AccessChecksLevelOption.ApplicationComponent,则分配给应用程序中角色的用户将添加到进程安全描述符,并在应用程序上执行基于角色的安全检查。 此外,还必须通过在 类上应用 ComponentAccessControl 属性,为需要 RBS 的每个组件启用访问检查。 在库应用程序中,基于角色的安全检查就像执行服务器应用程序一样。 安全属性包含在应用程序内所有对象的上下文中,并且安全调用上下文可用。 如果对象的配置与其创建者的上下文不兼容,则会在其自己的上下文中激活该对象。 编程式基于角色的安全性依赖于安全调用上下文的可用性。
对于适用于 COM+ 库应用程序的任何有意义的访问检查,请选择在进程和组件级别执行访问检查。
模拟和身份验证选择对应于 ApplicationAccessControl 属性的 ImpersonationLevel 和 Authentication 属性。
SecurityRole 属性可应用于程序集、类或方法级别。 在程序集级别应用时,该角色中的用户可以激活应用程序中的任何组件。 此外,当应用于类级别时,该角色中的用户可以对组件调用任何方法。 可以在元数据中配置应用程序和类级别角色,也可以通过访问 COM+ 目录进行管理配置。
使用元数据在程序集级别配置 RBS:
[assembly: ApplicationAccessControl(true, AccessCheckLevel=AccessChecksLevelOption.ApplicationComponent)] // adds NTAuthority\everyone to this role [assembly:SecurityRole("TestRole1",true)] // add users to roles administratively [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。 此外,特性必须出现在类方法实现上,而不是接口定义中的接口方法上。
对方法使用 RBS 的最简单方法是在类级别应用 SecureMethod 属性,然后通过管理方式或将 SecurityRole 属性放置在方法) 上来配置角色 (。
[assembly: ApplicationAccessControl(true, AccessCheckLevel=AccessChecksLevelOption.ApplicationComponent)] Interface IFoo { void Method1(); void Method2(); } [ComponentAccessControl()] [SecureMethod] public class Foo : ServicedComponent, IFoo { // Add roles to this method administratively public void Method1() {} // "RoleX" is added to the catalog for this method SecurityRole("RoleX") public void Method2() {} }在类级别使用 SecureMethod 允许类中所有接口上的所有方法以管理方式使用 COM+ 目录中的角色进行配置。 如果类实现两个接口,每个接口具有相同的方法名称,并且角色在管理上配置,则必须在 COM+ 目录 (这两种方法上配置角色,除非该类实现特定方法,例如 IFoo.Method1) 。 但是,如果在类方法上使用 SecurityRole 属性,则在注册程序集时,将自动为具有相同方法名称的所有方法配置该角色。
SecureMethod 属性也可以放置在方法级别。
[assembly: ApplicationAccessControl(true, AccessCheckLevel=AccessChecksLevelOption.ApplicationComponent)] Interface IFoo { void Method1(); void Method2(); } [ComponentAccessControl()] public class Foo : ServicedComponent, IFoo { // Add roles to this method administratively [SecureMethod] // Or use SecurityRole (translates to SecureMethod++) public void Method1() {} public void Method2() {} }在此示例中,IFoo 和这两种方法都显示在 COM+ 目录中,因此可以在任一方法上以管理方式配置角色,但是,方法级别 RBS 仅在 Method1 上强制执行。 对参与方法级别 RBS 安全性所需的所有方法使用 SecureMethod 或 SecurityRole,或将 SecureMethod 置于类级别,如前所述。
每当在方法级别配置 RBS 时,都需要封送处理程序角色:进行方法调用且未在方法上配置 RBS 时,服务组件基础结构会在 IRemoteDispatch 上调用。 进行方法调用并在方法上配置 RBS 时 (存在 SecureMethod 属性) 时,使用与方法关联的接口使用 DCOM 进行方法调用。 因此,DCOM 将保证在方法级别强制执行 RBS。 但是,如激活和拦截部分所述,COM 互操作和 RSCP 随后将对 IManagedObject (进行调用,以便远程激活器将引用封送到其空间) ,IServicedComponentInfo (查询远程对象) 。 这些接口与服务组件相关联。 由于 组件配置为执行方法级检查,因此,如果基础结构要成功进行这些调用,则需要将角色与这些接口相关联。
因此,在注册程序集时,会将封送程序角色添加到应用程序,然后必须以管理性将用户添加到此角色。 大多数情况下,应用程序的所有用户都添加到此角色。 这与非托管 COM+ 略有不同,在方法上配置 RBS 不需要此附加配置步骤。 在注册期间自动将“每个人”添加到此角色是一个潜在的安全漏洞,因为现在任何人都可以激活 (但不调用) 组件,而在此之前他们可能无权激活这些组件。 封送程序角色也添加到 IDisposable 接口,以允许客户端释放对象。 封送处理程序角色的替代方法是让用户将相关角色添加到上述三个接口中的每一个。
远程组件
类 ServicedComponent 在其继承树中包含 MarshalByRefObject,因此可以从远程客户端访问。 远程公开服务组件的方式有很多不同。 可以使用以下方法远程访问服务组件:
- HTTP 通道包含从 ASP.NET 调用或写入的服务组件,可提供良好的安全性和加密选项,以及已知的可伸缩性和性能。 与 SOAP 一起使用时,存在更多互操作性选项。 服务组件可以作为 COM+ 库应用程序托管在 IIS/ASP.NET 中。 如果使用 COM+ 服务器应用程序,IIS/ASP.NET 主机可以使用 DCOM 访问组件。
- COM+ Web Services:Check-Box 路由到 XML Web Services 中讨论了将服务组件公开为 SOAP 终结点的替代方法。
- 当服务组件托管在 Dllhost 中时为 DCOM。 此选项提供最佳性能和安全性,并能够跨计算机传递服务上下文。 选择远程处理技术时的主要设计问题应该是服务是否需要跨计算机流动。 例如,在服务器场中,在一台计算机上创建了事务,并且该事务需要在另一台计算机上继续运行,DCOM 是唯一可用于实现此目的的协议。 但是,如果客户端只需调用远程 ServicedComponent,则 HTTP 通道或 SOAP 终结点方法是很好的替代方法。
- 例如,.NET 远程处理通道 (TCP 或自定义通道) 。 若要使用 TCP 通道,需要在套接字上侦听进程。 通常,自定义进程用于侦听套接字,然后将服务组件作为 COM+ 库或服务器应用程序托管。 或者,Dllhost 可用作侦听器。 这两种方法都不太可能被使用,并且需要编写具有经过验证的性能、可伸缩性和安全性的自定义套接字侦听器。 因此,ASP.NET 或 DCOM 解决方案是大多数项目的最佳方法。
若要使用 DCOM 远程访问在 Dllhost 中托管的服务组件,首先请确保程序集已在 COM+ 服务器应用程序中注册并放置在服务器计算机上的 GAC 中。 然后,使用 COM+ 应用程序导出工具为应用程序代理创建 MSI 文件。 在客户端上安装应用程序代理。 嵌入在应用程序代理中的是托管程序集。 安装程序还将注册程序集并将其放置在客户端计算机上的 GAC 中。 因此:
- .NET Framework需要安装在客户端和服务器中。 这在客户端计算机上是必需的,即使只有非托管客户端将访问远程服务组件。 在 Windows 2000 平台上,还需要 Service Pack 3。
- 卸载代理后,还必须从 GAC 中删除程序集。
在客户端的托管代码中激活服务器组件之后的基础结构如图 6 所示。
使用 DCOM 意味着 CLR 托管在 Dllhost 中,这意味着应用程序配置文件dllhost.exe.config驻留在 system32 目录中。 这也意味着配置文件将应用于计算机上的所有 Dllhost 进程。 在下一个版本的 .NET Framework (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 Framework现在以一致且逻辑的方式提供对所有 COM+ 服务的平等访问。 此外,.NET Framework的一些创新部分(如 ASP.NET、Microsoft ADO.NET 和消息传送)与 .NET 企业服务深度集成,利用事务和对象池等服务。 此集成提供一致的体系结构和编程模型。 System.EnterpriseServices 命名空间提供编程模型,用于将服务添加到托管类。