COM 宏架构拓扑






4.97/5 (47投票s)
2001年7月25日
41分钟阅读

214299

2576
关于 COM 架构,以及 COM 客户端和 COM 服务器部署的文章。
概述
本文主要章节的快捷方式
目的
首先,我必须定义“COM 宏架构”的含义。
在“宏”这个词上,我不是指“预处理器”或“脚本命令”。不,我在这里所说的“宏”是指一种宏观的观察视角,这也类似于金融术语:“宏观经济”和“微观经济”分别指代整个经济体和国家经济,其中国家是界限。
因为“COM 架构”太过庞大,我不得不将其分为两部分。我将“部署单元”(从技术角度来看,我们称之为可执行文件或 DLL 文件)作为划分的界限。因此,我们有以下两组:
宏架构
:作为一个部署单元,我们将讨论客户端应用程序和 COM 服务器。微架构
:在一个部署单元内部,我们将讨论运行进程、COM apartment、线程并发和同步、内存共享。
这一系列文章只涉及“宏架构”。
您将学习如何制作 COM 服务器和客户端应用程序。您将看到如何在本地和远程计算机上部署和安装它们。
这一系列文章旨在做到**更具实践性**而非理论性。但是,您可以在参考文献部分找到许多深入细节的优秀书籍和文章。
本系列文章名为“COM 宏架构拓扑”,这是**主文章**。它将为您提供:
- 客户端应用程序和 COM 服务器之间关系的概述。
- 构建系统(客户端应用程序和 COM 服务器)所需的所有说明。
- 安装、配置和卸载系统所需的所有过程。
- COM 服务器,“COM 宏架构拓扑 (服务器)”。在这篇文章中,您将了解如何构建 COM 服务器。
- 客户端应用程序,“COM 宏架构拓扑 (客户端)”。在这篇文章中,您将学习如何构建客户端应用程序。
- COM ID 和注册表项一览。您将找到一些 COM 定义以及对最常见的 COM 注册表项的简要说明。
谁应该阅读本文?
我假设读者- 应该了解 C++ 语言,因为所有的代码示例都是用 C++ 编写的。
- 应该对 COM 有基本了解,例如客户端/服务器模型、COM 接口、接口封送(代理/存根)。
- 应该对 Windows NT/2000 安全方面有基本了解。
- 应该对 COM 架构而不是开发方面更感兴趣。
但是,初学者仍然可以阅读本系列文章,并将实践部分用作教程。根据我的经验,我通过实践开始学习 COM,然后书籍帮助我巩固并理解理论。
我提醒阅读本系列的 COM 开发人员,主要主题是**COM 架构**。我不会解释或比较用于开发 COM 客户端应用程序或服务器的各种 COM 辅助工具、C++ 库(例如 ATL)甚至 C++ 设计方面(例如智能指针或包装器类)。
要求
本示例中的所有代码均使用 C++ 编写,在 Windows 2000 下使用 Visual C++ 6.0 (sp4)。它们还使用了 Active-X Template Library (ATL)。
下载

所有可执行文件和 DLL 文件都输出在 `\Implementation\Bin` 目录中。
部署注意事项
部署本文所述的组件时请小心。请考虑以下几点:
- 要安装它们,您需要拥有管理员权限(客户端和服务器计算机)。
- 允许服务器使用“`交互式用户`”的身份或具有交互式权限的用户帐户(这样就可以显示对话框)。
- 远程调用服务器时,请确保与您的服务器关联的用户已在服务器计算机上作为交互式用户本地登录,以便对话框可以显示。
- 使用“终端服务客户端”无法获得交互式用户帐户,即使您的帐户具有交互式权限(有关更多信息,请参阅[Bi6])。
交互式用户是指当前登录到服务器的用户。
限制
虽然 COM 在许多操作系统中都可用,但我只考虑了 Windows 实现。这意味着 COM 的某些方面可能会根据其操作系统实现而具有不同的限制(例如,安全性、COM 信息存储等)。一些理论
COM 和基础设施
在进行实际操作并构建 COM 服务器之前,您必须了解您将提供给 COM 的信息将如何用于将客户端应用程序绑定到 COM 服务器。在这里,我只讨论 2 个 COM 基础设施点:
有关注册表和 COM 词汇的信息,请参阅文章“COM ID 和注册表项一览”。
激活机制
激活机制是 COM 用于定位、加载和启动 COM 服务器(DLL 或可执行文件)的过程。如果一切顺利,结果将是**实例化一个 COM 对象或 COM 类对象**。正如您所理解的,**激活是一个动态操作**,它发生在运行时。COM 提出了 3 种激活模型来将 COM 对象引入内存(参见[Bi1],第 5 章和[Bi11],第 3 章)。
- 客户端可以请求 COM 绑定到给定类的类对象或类工厂(使用 `CoGetClassObject()`)。
- 客户端可以请求 COM 基于 `CLSID` 创建类的新实例(使用 `CoCreateInstance/Ex()`)。
- 客户端可以请求 COM 基于对象的持久状态来使其“复活”(使用 `CoGetInstanceFromFile()`)。
所有这些激活模型都使用 COM 服务控制管理器 (SCM) 的服务。COM SCM 不同于 Windows NT 或 Windows 2000 SCM(用于启动独立于登录的进程,称为 Windows 服务)。每个支持 COM 的主机都有一个 COM SCM。它的作用是激活特定计算机上的 COM 对象(本地或远程)。
COM 服务器的静态/动态方面
在这一点上,您应该从**静态**和**动态**方面来看待您的 COM 服务器:- COM 服务器的**静态方面**是它的包:它可以被构建成**可执行文件**或**动态链接库 (DLL)** 文件。
- COM 服务器的**动态方面**与 COM 在运行时定义的服务器类别有关:**进程内**和**进程外**。

从**位置**的角度来看,位于客户端计算机上的 COM 服务器称为**“本地服务器”**,而位于远程计算机上的 COM 服务器称为**“远程服务器”**。
此图指出了 3 个结果:
- 打包成**可执行文件**的 COM 服务器**永远不会**作为进程内服务器运行。它将始终作为进程外服务器运行(本地或远程)。
- 打包成**DLL**文件的 COM 服务器将**运行**为进程内或进程外服务器(本地或远程)。
请注意,DLL COM 服务器始终需要一个进程来运行。作为**进程内**服务器,它将被加载到**客户端进程空间**;作为**进程外**服务器,它应该被加载到**代理进程空间**。
- 只有用作**进程外服务器**的 COM 服务器才能在远程计算机上调用。
条条大路通 COM 服务器...
在部署和安装 COM 服务器时,您必须向 COM 提供一些信息,例如您的组件(物理上)将在哪里,您的组件提供哪些 COM 类对象等。所有这些信息都将允许 COM 库响应客户端关于启动和创建 COM 服务器对象的请求。
在 Windows NT 4.0 中,COM 将这些信息存储在其本地配置数据库中,也称为**注册表**。在 Windows 2000(或 v5.0)中,COM 使用分布式安全数据库,即**Active Directory**。
通常,所有 COM 服务器都应支持自注册。这意味着:
- **可执行文件** COM 服务器必须检查命令行中的开关:
/RegServer
或-RegServer
:将向注册表插入项,并通过调用 `CoRegisterClassObject()` 来注册其类工厂。/UnregServer
或-UnregServer
:将删除注册表中的项,并通过调用 `CoRevokeClassObject()` 来注销其类工厂。
所有这 4 个开关都区分大小写。
- **DLL** COM 服务器必须导出这两个函数:
DllRegisterServer()
:将向注册表插入项。DllUnregisterServer()
:将删除注册表中的项。
要注册 DLL COM 服务器,可以使用 Win32 SDK 中的实用工具 **REGSVR32.EXE**。此应用程序将调用这两个导出的函数来注册或注销 DLL COM 服务器。
ISampleClass *psc = NULL; HRESULT hr = CoGetClassObject(GUID1, CLSTX_ALL, 0, IID_IClassFactory, (void**)&psc);
- `CoGetClassObject()` 为我们提供了与指定类 ID **GUID1** 关联的类对象的**IClassFactory** 接口指针(即类工厂)。
- `CoGetClassObject()` 的第二个参数是一个位掩码值,它指定了客户端希望激活 COM 服务器的期望上下文:进程内、进程外、本地或远程。
**CLSTX_ALL** 是一个预处理器宏,定义如下:#define CLSCTX_ALL (CLSCTX_INPROC_SERVER| \ CLSCTX_INPROC_HANDLER| \ CLSCTX_LOCAL_SERVER| \ CLSCTX_REMOTE_SERVER)
这些值在**CLSCTX**的标准枚举中定义为:typedef enum tagCLSCTX { CLSCTX_INPROC_SERVER = 0x1, CLSCTX_INPROC_HANDLER = 0x2, CLSCTX_LOCAL_SERVER = 0x4, CLSCTX_REMOTE_SERVER = 0x10, ... }CLSCTX;
您还可以在 SDK 头文件(`objbase.h`)中找到 2 个其他快捷宏:#define CLSCTX_INPROC (CLSCTX_INPROC_SERVER|\ CLSCTX_INPROC_HANDLER) #define CLSCTX_SERVER (CLSCTX_INPROC_SERVER|\ CLSCTX_LOCAL_SERVER|\ CLSCTX_REMOTE_SERVER)
值得注意的是,像**Visual Basic**和**Visual Java**这样的环境总是使用 `CLSTX_ALL`,表示任何可用的实现都可以([Bi11],第 3 章)。
以下是 COM 用于定位、加载和启动 COM 服务器并返回接口指针给客户端的激活链(参见[Bi1],第 6 章,[Bi3],第 5 章和[Bi11],第 6 章)。

在上图中,黄色矩形表示 [
HKEY_CLASSES_ROOT\CLSID
] 下的注册表项或子项,绿色矩形表示 [HKEY_CLASSES_ROOT\AppID
] 下的注册表项或子项。为简单起见,我没有考虑安全性。所以,让我们看看会发生什么:
- 客户端调用 `CoGetClassObject()`,它将控制权交给 COM **服务控制管理器** (SCM)。由于 `CoGetClassObject()` 的第三个参数未设置主机名,SCM 将首先在本地注册表中查找以下项:
[HKCR\CLSID\{GUID1}]
- 请注意,在 Windows 2000 中,SCM 会先查找 [
HKEY_CURRENT_USER
] 键,然后是 [HKEY_CLASSES_ROOT
] 键(别名为 [HKCR
])。 - 如果本地不存在该项,SCM 将尝试在 Windows 2000 Active Directory 上查找信息并本地安装组件。
- 如果找不到该项,调用将以 `HRESULT` 代码 `REGDB_E_CLASSNOTREG` 失败:*CLSID 未正确注册*。
此错误代码也可能表示您在 `CoGetClassObject()` 的第二个参数中指定的值未在注册表中注册。
- 请注意,在 Windows 2000 中,SCM 会先查找 [
- 此时,类 ID `GUID1` 可用,SCM 将查找子项:
[HKCR\CLSID\{GUID1}\InprocServer32] @="C:\myserver.dll"
如果该项存在,它应该包含一个指向 DLL 文件的有效路径。COM 将 DLL 加载到客户端进程空间,调用 `DllGetClassObject()` 获取 `IClassFactory` 接口指针并将其返回给客户端。
- 如果此项不存在,则 SCM 将查找子项:
[HKCR\CLSID\{GUID1}\InprocHandler32] @="C:\myserver.dll"
同样,如果该项存在,它应该包含一个指向 DLL 文件的有效路径。COM 将 DLL 加载到客户端进程空间,调用 `DllGetClassObject()` 获取 `IClassFactory` 接口指针并将其返回给客户端。
COM SCM 正在本地查找进程外服务器:
- SCM 现在正在其缓存中查找请求的 COM 类对象(类工厂)当前是否已注册。如果是,SCM 将返回一个已封送的接口指针给客户端。
COM 服务器使用辅助函数 `CoRegisterClassObject()` 将其类工厂注册到 SCM。
如果服务器使用 `REGCLS_SINGLEUSE` 标志,则 SCM 不会将类工厂设为公共级别,因此它不会用于后续的激活请求。实际上,对于后续的激活调用,SCM 将使用步骤 #5 及之后的步骤启动新进程。
- 如果类工厂未在其缓存中注册(或已从 SCM 的公共视图中删除),则表示当前没有正在运行且提供此类工厂作为公共级别的进程外服务器。
在 Windows NT 4.0 或 2000 中,COM 支持 3 种创建服务器的进程模型:- NT 服务。
- 普通进程。
- 代理进程。它们用于托管 DLL 服务器。
- 本地:提供故障隔离和安全上下文。
- 远程:提供远程激活和分布式架构。
无论使用哪种进程模型创建服务器进程,服务器将有 120 秒(或 Windows NT Service Pack 2 或更早版本为 30 秒)来注册请求的类工厂。如果服务器进程在此时间内未能完成,SCM 将使客户端的激活请求失败([Bi11],第 6 章)。
创建服务器进程时,SCM 会首先查找**本地服务**。它将尝试获取一个 `AppID`:
- 通过查找命名值:
[HKCR\CLSID\{GUID1}] AppID={GUID2}
如果找到一个值,例如 `GUID2`,它将在 `AppID` 根目录下查找命名值:[HKCR\AppID\{GUID2}] LocalService="MyService"
- 如果它无法在 [HKCR\CLSID\{GUID1}] 键下找到 `AppID` 值 **GUID2**,它将使用类 ID 值本身 **GUID1** 作为 AppID,并在**AppID**根目录下查找:
[HKCR\AppID\{GUID1}] LocalService="MyService"
`GUID1` 代表 COM 类工厂的类 ID,而 `GUID2` 代表 COM 类工厂使用的 COM 应用程序 ID。
如果存在值,(COM) SCM 将请求 Windows SCM 启动服务 `MyService`。服务将使用辅助函数 `CoRegisterClassObject()` 注册其类工厂。现在,COM 可以查询类工厂 `GUID1` 并请求它返回 `IClassFactory` 接口的指针,该指针将被封送回客户端。
- 如果 SCM 找不到此命名值,那么它将尝试启动一个普通进程,因此它将查找子项:
[HKCR\CLSID\{GUID1}\LocalServer32] @="C:\myserver.exe"
如果存在值,SCM 将尝试使用 `CreateProcess()` 或 `CreateProcessAsUser()` 等 API 函数来启动本地服务器。
COM SCM 正在远程查找进程外服务器:
- 如果 SCM 找不到本地服务器,它将查找命名值:
[HKCR\AppID\{GUID2}] RemoteServerName="\\MyRemoteComputer"
如果此值可用,激活请求将转发到指定主机上的 SCM。值得注意的是,尽管客户端应用程序在查询激活时仅使用 `CLSCTX_LOCAL_SERVER` 标志,但如果未注册**本地服务器**,请求将被转发到远程计算机。
远程 SCM 将按照步骤 #4 至 #6(即步骤 #7.c 至 #7.e)尝试查找并启动支持请求的类工厂的远程服务器。如果成功,它将返回 `IClassFactory` 接口的指针,该指针将被封送回客户端计算机。
- 如果既没有启动**本地服务**也没有启动**本地服务器**,那么远程 SCM 将在远程计算机上检查命名值:
[HKCR\AppID\{GUID2}] DllSurrogate="C:\MySurrogate.exe"
如果此命名值存在,远程 SCM 将启动代理进程(例如 **MYSURROGATE.EXE**)并要求它加载在子项下指定的 DLL COM 服务器:[HKCR\CLSID\{GUID1}\InprocServer32] @="C:\myserver.dll"
然后它将要求它返回 `IClassFactory` 接口的指针,该指针将被封送回客户端计算机。
如果命名值 **DllSurrogate** 为空:[HKCR\AppID\{GUID2}] DllSurrogate=""
那么远程 SCM 将启动默认的代理进程(称为 **DLLHOST.EXE**)并要求它加载 DLL COM 服务器。
- 如果既没有启动**本地服务**也没有启动**本地服务器**,那么远程 SCM 将在远程计算机上检查命名值:
正如我之前写的,有三种进程模型。直到步骤 #6,SCM 才尝试创建前两种:NT 服务和普通进程。最后一种进程模型是代理进程,它可以在本地用于进程外加载和激活 DLL COM 服务器。尽管如此,要在本地激活这种进程,客户端应用程序必须通过仅使用 `CLSCTX_LOCAL_SERVER` 标志而不是 `CLSCTX_ALL` 标志明确要求 COM。
“*为什么(又来了)?*”
为了使用代理进程,SCM 仍然需要在子项 **[HKCR\CLSID\{GUID1}\InprocServer32]** 下拥有 DLL 文件名。这没问题,但当客户端指定 `CLSCTX_ALL` 时,它同时指定了**CLSCTX_INPROC_SERVER**,SCM 将首先查找子项 **[HKCR\CLSID\{GUID1}\InprocServer32]**,如果它存在,那么 SCM 将作为进程内服务器将 COM 服务器加载到客户端空间。在这种情况下,SCM 永远不会触及命名值 **DllSurrogate**。
一些说明:
- 如果在远程计算机上存在命名值 **RemoteServerName**,则远程 SCM 在执行远程激活时不会考虑它。这意味着您不能将远程激活重定向到另一台计算机,依此类推。
- `CoGetClassObject()` 的第三个参数是指向 `COSERVERINFO` 结构的指针。您可以使用此结构显式设置远程计算机名。
当此参数为 NULL 时,SCM 将在注册表中查找名为 **RemoteServerName** 的值。
安全性
一般来说,您使用安全基础设施来保护您的私有数据或任何资源免受不法之徒的侵害。最初,COM 没有特定的安全基础设施来保护客户端或服务器应用程序。Windows NT 4.0 的发布引入了进程和线程的安全级别,并允许远程访问服务器进程。在 Windows 平台实现(特别是 NT 4.0 和 2000)上,COM 安全基础设施是通过以下方式构建的:
- 操作系统功能,
- 以及 RPC 安全基础设施(参见[Bi11],第 6 章)。
Windows NT/2000 安全基础设施
Windows NT/2000 支持基于用户帐户、组和域的安全。Windows 95/98 的安全支持不如 Windows NT/2000。在本节中,我只讨论 Windows NT/2000 安全方面,有关 Windows 95/98 的具体实现,请参阅平台文档。在本节的其余部分,当没有必要区分时,“Windows”一词将指代 Windows NT/2000。
Windows 关注两个领域([Bi12],第 18 章):
- **身份验证**。身份验证试图回答“*您真的是您声称的那个人吗?*”身份验证在 Windows 中最常见的用途是用户登录时。
- **访问控制**。Windows 中的访问控制*允许*或*限制*特定用户访问特定资源或服务。
每当在用户打开的会话下启动进程时,Windows(默认情况下)会将用户的访问令牌与其关联。然后,当进程想要访问系统对象(例如线程、互斥体或文件)时,Windows 会将其附加的安全信息与进程的访问令牌进行比较。比较后,Windows 会授予或拒绝访问。
每个系统对象都可以包含一个安全描述符,该描述符由以下部分组成:
- **所有者 SID**。
- 用户所属的**主组 SID**。
- 用于审核消息的系统访问控制列表 (SACL)
- 用于访问控制的任意访问控制列表 (DACL)。
我认为是时候来张图了:

拒绝 ACE 条目始终放在最前面。
在 Windows 中,**身份验证**用户的任务由安全支持提供程序 (SSP) 完成。Win32 API 定义了一个名为安全支持提供程序接口 (SSPI) 的安全 API。SSP 是一个实现 **SSPI** 的 DLL,通常包含常见的身份验证和加密数据方案。
在 Windows NT 4.0 中,操作系统只提供了一个 SSP:NT LAN Manager 安全支持提供程序 (NTLM** SSP)。
在 Windows 2000 中,有更多:
- **NTLM** SSP,与 Windows NT 4.0 相同。
- **Kerberos** 是比 **NTLM** SSP 更复杂的身份验证协议。
- **Snego**(简单 GSS-API 协商机制)。事实上,**Snego** 本身并不是一个真正的 SSP。它是一种用于真实 SSP 之间协商的协议。如果双方(客户端和服务器)都支持 Kerberos,它将选择 Kerberos,否则选择 **NTLM**。
COM 安全基础设施
COM 区分安全性的 4 个基本方面([Bi4]):- **启动安全性**:哪些安全用户允许在一个新进程中创建一个新对象?
**=>** *保护服务器计算机*
- **访问安全性**:哪些安全用户允许调用一个对象?
**=>** *保护对象*
- **身份**:对象本身的安全用户是谁?
**=>** *控制对象*
- **连接策略**:完整性 - 消息是否会被篡改?隐私 - 消息是否会被他人拦截?身份验证 - 对象是否能找出甚至假定调用者的身份?
**=>** *保护数据*
- 全机器安全级别,
- 全应用程序安全级别(或按服务器),
- 按类划分,
- 按接口划分,
- 按方法调用划分。
注意:
当讨论最后 3 个级别(按类、按接口和按方法调用)时,它们通常称为**细粒度安全**。 您可以通过 2 种互补的方式设置安全信息:
- 通过**配置**。安全信息存储在**注册表**中。您可以使用 **DCOMCNFG.EXE** 来设置这些信息。许多书籍都解释了如何使用此工具,例如 [Bi3],第 5 章、[Bi12],第 18 章、[Bi10],第 10 章,以及 MSDN 文章 [Bi5]。
- 通过**编程**。您可以使用 COM 安全 API 来设置安全信息。
注册表上的设置也称为**默认设置**。
如果您在多个级别设置了安全信息,会发生什么?事实上,根据安全级别,您可以覆盖较低级别的设置,而使用更高级别的设置。这是一个显示安全级别顺序的图:

**按方法**的安全级别会覆盖**按接口**的安全级别,依此类推。
为了结束对 COM 安全模型的这一高级视图,其中一些安全信息只能在客户端设置,只能在服务器设置,或者在两端都设置。
所有这些参数和设置组合使得 COM 安全本身成为一个庞大的主题,但如果您正在开发分布式应用程序,则必须关注它们。
一些实践
COM 宏拓扑
完成 COM 组件后,我们需要部署、安装和配置它。部署组件可能有很多种情况。通过部署,我的意思是:
- 您将使用哪种类型的单元(可执行文件或 DLL)作为客户端或服务器?
- 您将客户端单元和服务器单元复制到哪里?
- 位置(`2`)
- **本地**表示客户端和服务器在同一台计算机上。但是可能不在同一个进程中。
- **远程**表示客户端和服务器不在同一台计算机上。
- 客户端(`2`):**可执行文件**或**DLL**。
- COM 服务器(`3`):可执行文件、DLL 或合并的 DLL。
- **可执行文件**。您需要了解 DCOM 应用程序安全以及代理/存根 DLL。
- **DLL**。这种情况对应于 2 个 DLL:一个用于 COM 服务器本身,另一个用于代理/存根 DLL。
- **合并代理/存根的 DLL**。只有一个 DLL。
因此,这 3 个标准共有 12 种情况(2 x 2 x 3)。
12 种情况
我们将逐个案例地介绍如何制作和安装我们的 COM 应用程序:客户端和服务器。
COM 宏架构 拓扑案例 |
本地: ![]() 远程: ![]() |
可执行文件: ![]() DLL: ![]() |
可执行文件: ![]() DLL: ![]() DLL(合并): ![]() |
案例 1 | ![]() |
![]() |
![]() |
案例 2 | ![]() |
![]() |
![]() |
案例 3 | ![]() |
![]() |
![]() |
案例 4 | ![]() |
![]() |
![]() |
案例 5 | ![]() |
![]() |
![]() |
案例 6 | ![]() |
![]() |
![]() |
案例 7 | ![]() |
![]() |
![]() |
案例 8 | ![]() |
![]() |
![]() |
案例 9 | ![]() |
![]() |
![]() |
案例 10 | ![]() |
![]() |
![]() |
案例 11 | ![]() |
![]() |
![]() |
案例 12 | ![]() |
![]() |
![]() |
图片图例 
以下是 12 种情况中使用的图片符号图例:
案例 1 
构建此案例的步骤如下:- 将 COM 服务器创建为可执行文件(参见“COM 宏架构拓扑 (服务器)”文章)。
- 完成步骤 1 到 6。
- 将客户端应用程序创建为可执行文件(参见“COM 宏架构拓扑 - 客户端”文章)。
- 将服务器文件(可执行文件和代理/存根 DLL)和客户端文件复制到目标计算机。
- 使用“本地注意事项”注册服务器。
- 步骤 7。
- 启动客户端应用程序并使用 COM 服务器。

图例
关于此部署架构的一些说明:
- 进程关系
- 客户端应用程序和 COM 服务器对象永远不会在同一个进程中运行。
- COM 服务器实例(进程)与客户端应用程序之间的关系基数可能是:
- 1 对 1:每个客户端应用程序都可以拥有自己的 COM 服务器。
- 1 对 n:一个 COM 服务器可以被许多客户端应用程序共享。
- n 对 1:一个客户端应用程序可以拥有许多 COM 服务器。
- COM 安全方面
- 您可以使用 **DCOMCNFG.EXE** 来设置不同的安全详细信息(访问、启动、用户等)。
- COM 服务器可执行文件可以开发为 Windows 服务。
- 通过简单地删除客户端可执行文件来移除客户端应用程序。
- 按照“COM 宏架构拓扑 (服务器)”文章中的说明卸载 COM 服务器。
- 步骤 9 使用“本地注意事项”。
- 通过简单地删除 COM 服务器可执行文件和代理/存根 DLL 文件来移除 COM 服务器文件。
案例 2 
对于此部署,您可能面临 2 种不同的配置:- COM DLL 服务器加载到客户端进程中。COM 将此服务器称为进程内服务器。
- COM DLL 服务器在客户端进程外部但在同一台计算机上运行。COM 将此服务器称为(本地)进程外服务器。
进程内服务器
构建此案例的步骤如下:- 将 COM 服务器创建为 2 个 DLL(参见“COM 宏架构拓扑 (服务器)”文章)。
- 完成步骤 1 到 6。
- 将客户端应用程序创建为可执行文件(参见“COM 宏架构拓扑 - 客户端”文章)。
- 将服务器文件(COM DLL 服务器和代理/存根 DLL)和客户端文件复制到目标计算机。
- 使用“本地进程内注意事项”注册服务器。
- 步骤 7。
- 启动客户端应用程序并使用 COM DLL 服务器。

图例
关于此部署架构的一些说明:
- 进程关系
- 客户端应用程序和 COM DLL 服务器将在同一进程中运行。
- COM DLL 服务器实例(进程)与客户端应用程序之间的关系基数是:
- 1 对 1:每个客户端应用程序都有自己的 COM DLL 服务器。实际上,DLL 的实例将在客户端进程之间共享。这意味着所有全局数据也将共享,因此它们应该是线程安全的。尽管如此,每个运行时实例(堆和栈)都将是客户端进程内存空间的一部分。
- COM 安全方面
- DLL 永远无法在没有进程的情况下运行。所有安全方面将由该进程管理,即客户端进程。
- 其他
- 如果 COM 对象位于不同的 COM apartment 中,客户端代码可以通过间接方式访问 COM 对象(链接 1.a、1.b 和 1.c),或者如果它们位于同一个 apartment 中,则可以通过直接方式访问(链接 2),但这超出了本系列文章的范围。
- 通过简单地删除客户端可执行文件来移除客户端应用程序。
- 按照“COM 宏架构拓扑 (服务器)”文章中的说明卸载 COM DLL 服务器。
- 步骤 9 使用“本地进程内注意事项”。
- 通过简单地删除 COM DLL 服务器文件和代理/存根 DLL 文件来移除 COM 服务器文件。
进程外服务器
构建此案例的步骤如下:- 将 COM 服务器创建为 2 个 DLL(参见“COM 宏架构拓扑 (服务器)”文章)。
- 完成步骤 1 到 6。
- 将客户端应用程序创建为可执行文件(参见“COM 宏架构拓扑 - 客户端”文章)。
- 将服务器文件(COM DLL 服务器和代理/存根 DLL)和客户端文件复制到目标计算机。
- 使用“本地代理注意事项”注册服务器。
- 步骤 7。
- 启动客户端应用程序并使用 COM DLL 服务器。

图例
关于此部署架构的一些说明:
- 进程关系
- 客户端应用程序和 COM DLL 服务器将在同一进程中运行。
- COM DLL 服务器实例(进程)与客户端应用程序之间的关系基数是:
- 1 对 1:每个客户端应用程序都可以拥有自己的 COM 服务器。
- 1 对 n:一个 COM 服务器可以被许多客户端应用程序共享。
- n 对 1:一个客户端应用程序可以拥有许多 COM 服务器。
- COM 安全方面
- 您可以使用 **DCOMCNFG.EXE** 来设置不同的安全详细信息(访问、启动、用户等)。
- 其他
- 如果在客户端代码中使用 **CLSCTX_INPROC_SERVER**、**CLSCTX_INPROC_HANDLER**、**CLSCTX_SERVER** 或 **CLSCTX_ALL** 等标志,COM 会首先启动进程内服务器。
- 通过简单地删除客户端可执行文件来移除客户端应用程序。
- 按照“COM 宏架构拓扑 (服务器)”文章中的说明卸载 COM DLL 服务器。
- 步骤 9 使用“本地代理注意事项”。
- 通过简单地删除 COM DLL 服务器文件和代理/存根 DLL 文件来移除 COM 服务器文件。
案例 3 
对于此部署,您可能面临 2 种不同的配置:- COM DLL 服务器加载到客户端进程中。COM 将此服务器称为进程内服务器。
- COM DLL 服务器在客户端进程外部但在同一台计算机上运行。COM 将此服务器称为(本地)进程外服务器。
进程内服务器
构建此案例的步骤如下:- 将 COM 服务器创建为 1 个 DLL(参见“COM 宏架构拓扑 (服务器)”文章)。
- 完成步骤 1 到 6。
- 将客户端应用程序创建为可执行文件(参见“COM 宏架构拓扑 - 客户端”文章)。
- 将服务器文件(这次 COM DLL 服务器与代理/存根代码合并)和客户端文件复制到目标计算机。
- 使用“本地进程内注意事项”注册服务器。
- 步骤 7。
- 启动客户端应用程序并使用 COM DLL 服务器。

图例
关于此部署架构的一些说明:
- 进程关系
- 客户端应用程序和 COM DLL 服务器将在同一进程中运行。
- COM DLL 服务器实例(进程)与客户端应用程序之间的关系基数是:
- 1 对 1:每个客户端应用程序都有自己的 COM DLL 服务器。实际上,DLL 的实例将在客户端进程之间共享。这意味着所有全局数据也将共享,因此它们应该是线程安全的。尽管如此,每个运行时实例(堆和栈)都将是客户端进程内存空间的一部分。
- COM 安全方面
- DLL 永远无法在没有进程的情况下运行。所有安全方面将由该进程管理,即客户端进程。
- 其他
- 如果 COM 对象位于不同的 COM apartment 中,客户端代码可以通过间接方式访问 COM 对象(链接 1.a、1.b 和 1.c),或者如果它们位于同一个 apartment 中,则可以通过直接方式访问(链接 2),但这超出了本系列文章的范围。
- 通过简单地删除客户端可执行文件来移除客户端应用程序。
- 按照“COM 宏架构拓扑 (服务器)”文章中的说明卸载 COM DLL 服务器。
- 步骤 9 使用“本地进程内注意事项”。
- 通过简单地删除 COM DLL 服务器文件来移除 COM DLL 服务器文件。
进程外服务器
构建此案例的步骤如下:- 将 COM 服务器创建为 1 个 DLL(参见“COM 宏架构拓扑 (服务器)”文章)。
- 完成步骤 1 到 6。
- 将客户端应用程序创建为可执行文件(参见“COM 宏架构拓扑 - 客户端”文章)。
- 将服务器文件(这次 COM DLL 服务器与代理/存根代码合并)和客户端文件复制到目标计算机。
- 使用“本地代理注意事项”注册服务器。
- 步骤 7。
- 启动客户端应用程序并使用 COM DLL 服务器。

图例
关于此部署架构的一些说明:
- 进程关系
- 客户端应用程序和 COM DLL 服务器将在同一进程中运行。
- COM DLL 服务器实例(进程)与客户端应用程序之间的关系基数是:
- 1 对 1:每个客户端应用程序都可以拥有自己的 COM 服务器。
- 1 对 n:一个 COM 服务器可以被许多客户端应用程序共享。
- n 对 1:一个客户端应用程序可以拥有许多 COM 服务器。
- COM 安全方面
- 您可以使用 **DCOMCNFG.EXE** 来设置不同的安全详细信息(访问、启动、用户等)。
- 其他
- 在此配置中,我们失去了只有一个 DLL 的优势。因为我们希望我们的组件在进程外运行,所以客户端应用程序仍然需要其进程空间中的封送代码(代理/存根代码)。因此,您必须构建并安装代理/存根 DLL。
- 如果在客户端代码中使用 **CLSCTX_INPROC_SERVER**、**CLSCTX_INPROC_HANDLER**、**CLSCTX_SERVER** 或 **CLSCTX_ALL** 等标志,COM 会首先启动进程内服务器。
- 通过简单地删除客户端可执行文件来移除客户端应用程序。
- 按照“COM 宏架构拓扑 (服务器)”文章中的说明卸载 COM DLL 服务器。
- 步骤 9 使用“本地代理注意事项”。
- 通过简单地删除 COM DLL 服务器文件来移除 COM DLL 服务器文件。
案例 4、案例 5、案例 6 
出于部署原因,我在客户端 DLL 和可执行文件之间进行了区分,但从功能角度来看,它们“没有区别”。我的意思是,两种情况下的客户端代码编写方式相同。但是,对于进程内 COM 服务器(DLL),您必须考虑 COM Apartment 的考虑,但这超出了本文的范围。
显然,如果您的客户端也是 COM 服务器,您必须对 COM 服务器应用与前面解释相同的规则。所以:
案例 7 
构建此案例的步骤如下:- 将 COM 服务器创建为可执行文件(参见“COM 宏架构拓扑 (服务器)”文章)。
- 完成步骤 1 到 6。
- 将客户端应用程序创建为可执行文件(参见“COM 宏架构拓扑 - 客户端”文章)。
- 将服务器文件(可执行文件和代理/存根 DLL)复制到服务器计算机(参见下方 HOST B)。
- 将服务器代理/存根 DLL 和客户端应用程序复制到客户端计算机(参见下方 HOST A)。
- 使用“远程注意事项”注册服务器。有两个注册:一个用于客户端计算机,一个用于服务器计算机。
- 步骤 7。
- 配置服务器的 DCOM 安全方面。
- 步骤 8。
- 启动客户端应用程序并使用 COM 服务器。

图例
关于此部署架构的一些说明:
- 进程关系
- 客户端应用程序和 COM 服务器对象将永远不会在同一台计算机上运行,因此也不在同一个进程中。
- COM 服务器实例(进程)与客户端应用程序之间的关系基数可能是:
- 1 对 1:每个客户端应用程序都可以拥有自己的 COM 服务器。
- 1 对 n:一个 COM 服务器可以被许多客户端应用程序共享。
- n 对 1:一个客户端应用程序可以拥有许多 COM 服务器。
- COM 安全方面
- 您可以使用 **DCOMCNFG.EXE** 来设置不同的安全详细信息(访问、启动、用户等)。
- DCOM 方面
- 您必须在客户端和服务器计算机上都启用 DCOM(在“`默认属性`”选项卡上)。
- 如果您有多台服务器计算机(多个 HOST B),您可以通过在注册表中更改(`AppID` 键下的)“`RemoteServerName`”值来配置客户端计算机使用不同的服务器计算机。
您也可以在客户端应用程序代码中使用 API 函数 `CoCreateInstanceEx()` 来确定对象将被实例化的计算机。
- COM 服务器可执行文件可以开发为 Windows 服务。
- 按照“COM 宏架构拓扑 (服务器)”文章中的说明卸载 COM 服务器。有两个卸载:一个用于客户端计算机,一个用于服务器计算机。
- 步骤 9 使用“远程注意事项”。
- 在客户端计算机上,通过简单地删除这些文件来移除客户端应用程序和服务器代理/存根 DLL。
- 在服务器计算机上,通过简单地删除 COM 服务器可执行文件和代理/存根 DLL 文件来移除 COM 服务器文件。
案例 8 
构建此案例的步骤如下:- 将 COM 服务器创建为 2 个 DLL(参见“COM 宏架构拓扑 (服务器)”文章)。
- 完成步骤 1 到 6。
- 将客户端应用程序创建为可执行文件(参见“COM 宏架构拓扑 - 客户端”文章)。
- 将服务器文件(COM DLL 服务器和代理/存根 DLL)复制到服务器计算机(参见下方 HOST B)。
- 将服务器代理/存根 DLL 和客户端应用程序复制到客户端计算机(参见下方 HOST A)。
- 使用“远程注意事项”注册服务器。有两个注册:一个用于客户端计算机,一个用于服务器计算机。
- 步骤 7。
- 配置服务器的 DCOM 安全方面。
- 步骤 8。
- 启动客户端应用程序并使用 COM 服务器。

图例
关于此部署架构的一些说明:
- 进程关系
- 客户端应用程序和 COM 服务器对象将永远不会在同一台计算机上运行,因此也不在同一个进程中。
- COM 服务器实例(进程)与客户端应用程序之间的关系基数可能是:
- 1 对 1:每个客户端应用程序都可以拥有自己的 COM 服务器。
- 1 对 n:一个 COM 服务器可以被许多客户端应用程序共享。
- n 对 1:一个客户端应用程序可以拥有许多 COM 服务器。
- COM 安全方面
- 您可以使用 **DCOMCNFG.EXE** 来设置不同的安全详细信息(访问、启动、用户等)。
- DCOM 方面
- 您必须在客户端和服务器计算机上都启用 DCOM(在“`默认属性`”选项卡上)。
- 如果您有多台服务器计算机(多个 HOST B),您可以通过在注册表中更改(`AppID` 键下的)“`RemoteServerName`”值来配置客户端计算机使用不同的服务器计算机。
您也可以在客户端应用程序代码中使用 API 函数 **CoCreateInstanceEx()** 来确定对象将被实例化的计算机。
- 按照“COM 宏架构拓扑 (服务器)”文章中的说明卸载 COM DLL 服务器。有两个卸载:一个用于客户端计算机,一个用于服务器计算机。
- 步骤 9 使用“远程注意事项”。
- 在客户端计算机上,通过简单地删除这些文件来移除客户端应用程序和服务器代理/存根 DLL。
- 在服务器计算机上,通过简单地删除 COM DLL 服务器文件和代理/存根 DLL 文件来移除 COM 服务器文件。
案例 9 
构建此案例的步骤如下:- 将 COM 服务器创建为 1 个 DLL(参见“COM 宏架构拓扑 (服务器)”文章)。
- 完成步骤 1 到 6。
- 将客户端应用程序创建为可执行文件(参见“COM 宏架构拓扑 - 客户端”文章)。
- 将服务器文件(这次 COM DLL 服务器与代理/存根代码合并)复制到服务器计算机(参见下方 HOST B)。
- 将服务器代理/存根 DLL 和客户端应用程序复制到客户端计算机(参见下方 HOST A)。
- 使用“远程注意事项”注册服务器。有两个注册:一个用于客户端计算机,一个用于服务器计算机。
- 步骤 7。
- 配置服务器的 DCOM 安全方面。
- 步骤 8。
- 启动客户端应用程序并使用 COM 服务器。

图例
关于此部署架构的一些说明:
- 进程关系
- 客户端应用程序和 COM 服务器对象将永远不会在同一台计算机上运行,因此也不在同一个进程中。
- COM 服务器实例(进程)与客户端应用程序之间的关系基数可能是:
- 1 对 1:每个客户端应用程序都可以拥有自己的 COM 服务器。
- 1 对 n:一个 COM 服务器可以被许多客户端应用程序共享。
- n 对 1:一个客户端应用程序可以拥有许多 COM 服务器。
- COM 安全方面
- 您可以使用 **DCOMCNFG.EXE** 来设置不同的安全详细信息(访问、启动、用户等)。
- DCOM 方面
- 您必须在客户端和服务器计算机上都启用 DCOM(在“`默认属性`”选项卡上)。
- 如果您有多台服务器计算机(多个 HOST B),您可以通过在注册表中更改(`AppID` 键下的)“`RemoteServerName`”值来配置客户端计算机使用不同的服务器计算机。
您也可以在客户端应用程序代码中使用 API 函数 **CoCreateInstanceEx()** 来确定对象将被实例化的计算机。
- 按照“COM 宏架构拓扑 (服务器)”文章中的说明卸载 COM DLL 服务器。有两个卸载:一个用于客户端计算机,一个用于服务器计算机。
- 步骤 9 使用“远程注意事项”。
- 在客户端计算机上,通过简单地删除这些文件来移除客户端应用程序和服务器代理/存根 DLL。
- 在服务器计算机上,通过简单地删除文件来移除 COM DLL 服务器文件。
案例 10、案例 11、案例 12 
与案例 4、案例 5和案例 6的解释相同。对应的案例是:
结论
一个最终的问题可能是:“*我应该构建进程内服务器还是进程外服务器?*”(参见[Bi10])。实际上,这将取决于您的需求和约束,但我认为您现在已经有足够的信息来自己回答这个问题。需要牢记的主要思想是:
- 作为可执行文件的 COM 服务器永远不会用作进程内服务器。
- 作为 DLL 的 COM 服务器既可以用作进程内服务器,也可以用作进程外服务器。
- 进程内与进程外
- 通过使用进程内服务器,您可以获得性能(COM 对象应该与客户端代码共享相同的 apartment)。
- 通过使用进程外服务器,您可以获得健壮性。
- 进程内服务器将受益于其父进程的安全特权。
- 进程内服务器的生命周期与其父进程的生命周期紧密相关。
当我开始这个系列文章时,我曾试图围绕一个二元性思想来组织我的工作,并选择了“宏/微”的二元性概念。现在,我可能意识到一个更好的二元性概念是“静态/动态”。
- 划分仍然是相同的:**部署单元**。
- 我们可以使用**静态**代替**宏**,因为一旦组件部署完成,它就不会从其物理位置(主机)和单元(可执行文件或 DLL 文件)移动。
- 我们可以使用**动态**代替**微**,因为在运行时,当我们实例化 COM 对象时,我们可以谈论应用程序和 COM 服务器内部发生的事情(如线程、内存、Apartment 等)以及这些实例之间的关系。
就我而言,我将尝试编写“COM 微架构”文章,或者也许是“COM 动态架构”。
谢谢
我感谢我的妻子 Minh 的支持;感谢 Dave Haste 和 Anne-Sophie Merot 花时间审阅我的文章。参考文献
- [Bi1]
- COM 规范,
版本 0.9,1995 年 10 月 24 日。
- [Bi2]
- 分布式 COM 深入解析,Guy Eddon 和 Henry Eddon,1998 年,
Microsoft Press。
- [Bi3]
- 学习 DCOM,Thuan L. Thai,1999 年 4 月,
O'Reilly 出版社。
- [Bi4]
- DCOM 架构,Markus Horstmann 和 Mary Kirtland,
1997 年 7 月 23 日,MSDN 库。
- [Bi5]
- COM 安全实战,Rajiv Dulepet,
MSDN 库。
- [Bi6]
- 在多种桌面平台上运行您的应用程序:终端服务器,Frank Kim,
Microsoft Interactive Developer,1998 年 12 月。
- [Bi7]
- ATL 深入解析,George Shepherd 和 Brad King,1999 年,
Microsoft Press。
- [Bi8]
- 组件对象模型规则,Charlie Kindel,1995 年 10 月 20 日,
MSDN 库。
- [Bi9]
- 设计 COM 接口,Charlie Kindel,1995 年 10 月 20 日,
MSDN 库。
- [Bi10]
- ActiveX/COM 问答,Don Box,1997 年,
问:“我正在构建一套几乎没有用户界面的 COM 对象,这些对象将通过网络从各种客户端语言调用。我应该构建进程内服务器还是进程外服务器?” ,
MSDN 库 / 转载自 Microsoft Systems Journal。
- [Bi11]
- COM 核心,Don Box,1998 年,
Addison Wesley。
- [Bi12]
- COM+ Base Services 深入解析,Guy Eddon 和 Henry Eddon,1999 年,
Microsoft Press。
- [Bi13]
- COM 和 COM+ 编程入门,Alan Gordon,2000 年,
Prentice Hall PTR。
- [Bi14]
- COM 安全入门,第一部分和第二部分,Jeff Prosise,2000 年 11 月,
Codeguru 网站