软 件 开 发 技 木 从 书 COM+技术大全 ( 美) Richard C.Leinecker 著 高智勇赵乾唐华平黄蔚玲等译 (7)机械工业出版社 \ n China Machine Press本书介绍COM+技术的主要特性和编程技巧。主要内容包括Windows DNA和COM+的基 本概念、髙级COM编程技术、组件的管理、事务以及异步组件编程等。本书揭示了COM+的 内幕,实例丰富,分析透彻。拥有本书,可以最大限度地发挥COM+的潜力,获得更好的编 程技能。本书适合于所有Windows程序幵发人员,尤其对具有编程经验的人更加具有参考价 值。随书附带的光盘包含了书中所有实例代码,以及微软最新的极有价值的信息。 Richard C. Leinecker: COM+ Unleashed. Authorized translation from the English language edition published by Sams, an imprint of Macmillan Computer Publishing U. S. A. Copyright © 2000 by Sams. All rights reserved. Chinese simplified language edition published by China Machine Press. Copyright © 2001 by China Machine Press. 本书中文简体字版由美国麦克米兰公司授权机械工业出版社独家出版。未经出版者书面 许可,不得以任何方式复制或抄袭本书内容。 版权所有,侵权必究。 本书版权登记 号 :图字:01-2000-2813 图 书在 版 编目(CIP) 数据 COM+技术大全/ ( 美 )莱耐 克 (Leinecker,R.C.) 著;高智勇等译.-北京:机械工业出 版社,2001.8 ( 软件开发技术丛书) 书名原文:COM+Unleashed ISBN 7-111-08951-0 • I.C… n.① 莱 …② 高 … m.软件接口,COM+-程 序 设 计 IV.TP3U.52 中国版本图书馆CIP数据核字(2001 ) 第038022号 机械工业出版社(北京市西城区百万庄大街22号邮政编码丨00037) 责任编辑:宋燕红张鸿斌 北京昌平第二印刷厂印刷•新华书店北京发行所发行 2001年8月第1版第1次印刷 787mm x 1092mm 1/16 • 35.5印张 印数:0 001-5 000册 定价:76.00元 (附光盘) 凡购本书,如有倒页、脱页、缺页 ,由本社发行部调换本书的基本内容包括: 1 )讲述COM+在实际应用和高级解决方案中的强大能力。 2 )提供读者可能永远也想不到的,但是用COM+却可以轻松解决的捷径。 3 ) 揭开Windows DNA的神秘面纱,讲述其强大的服务器方产品,例如C O M +目录的主框架。 4>指出在实际的COM+/DNA工程中存在的缺陷和细微的差別。 本书面向的对象 本书决不是COM+的参考书或者其理论的概述。它面向于希望进一步深化的中高级COM +程 序员。我们假定读者具有C++的使用经验,并且熟悉Windows环境。 本书所包含的内容 CO M +是 一 个 很 大 的 话 题 ,完 整地 讲 述它 需要 好几本 书 。本 书将 重 点 放在使用M icrosoft DNA 技术编写高级COM+程序上。由于C O M +的重要性和趋同性,我们花了很多的篇幅讨论类 似于MTS和MSMQ 的服务器方DNA 产 品。尽管DNA 的焦点集中于发布的n层 互联M 程 序 上 ,但 是我们讨论的编程技术却应用于COM +的编程和普通的互操作上。 由于太复杂或者没有详尽的 说 明 文 档 ,COM + i午多强大的特性往往被初级程序员所忽视。本书以尽最简单的捷径讲解更多 深 奥 而 又强 大 的 、多方面的COM+知 识 。尽管仅从表面上理解还不够,但是一 些特 性 ,例如标 记 、 自定义调度和永久存储,对于准备开发可升级的、可靠而又有效的应用程序的COM +开发 者来说却是必须的。尽管我无法从理论的角度完全覆盖COM +的 所 有 内 容 ,但是我也没有完全 分割各个主题之间的关系。在C O M +中有一些诱人的算法和协议,我 将在整本 书贯 穿讲 述 ,以 帮助读者理解C O M +技术的优点和缺陷。 本书不包含的内容 本书假定你对COM+已经有所理解并且能用它进行一些基本的工作。我们将不解释最基本的 组成 构件 。例 如 ,你应该 熟 悉IU nknow n接口并且明白它在CO M + 中所起的作用 。本书不比较 COM+与其他组件技术的异同,例如CORBA。 IIS是DNA 中的一部分,本书不讨论它是因为它处于Web技 术 的边 缘,而COM 4•潘常可以通 过脚本和其他高级语言1编程。如果你想学习Web应用程序,可以査看本书第1和第2章中提到的参 考书目。 与作者联系 我将大部分个人时间花费在开发Web站点和新闻组上以及编写本书的主要动机之一,是以帮IV 助 我 的 同 事 开 发 程 序 为 乐 趣 。非 常 感 谢 你 购 买 了 本 书 ,当 你 对 本 书 有 什 么 疑 问 或 者 在 开 发 DNA/W eb的过程中遇到什么困难的话,不要犹豫,赶快与我联系。我的Web站点是www.source DNA.con\0 下载例子 本书中的所有例子都包含在附带的光盘屮,安装该光盘后将可以很容易地找到需要的东西。 由于某些原因,尽管许多开发者能够使其上司知道在程序中存在bug是一件很普 遍的事情, 但是在本书所包含的软件中存在错误却总是会令这些程序员恼火。编辑花了很多时间来测试本 书 中 的 例子,怛是很 遗 憾,像其他任何软件一样,在我们的代码中肯定也会有错误。我将及时 在我的ftp站点ftp://mcp.com/product_support或http://www.sourceDNA.com修改代码,以提供最新 的版本。 软件要求 下面的几个部分讨论了COM+技术内幕的软件需求。本书用到了儿个不同的技术和编程工具。 COM COM 并没有指定任何开发语言或工具,它只需要可用的语言或工具以能够产生二进制编码 的可注册的组件。尽管可以使ffl— 些非常低级的语言来生成COM 组 件 ,例如用汇编语言,但是 这往往需要付出特别多的劳动和努力才能在开发组中实现。微软开发了许多工具以尽可能地帮 助程序员在程序设计和逻辑推理方面避免麻烦。 在本书中,我使用了微软开发的能够尽快帮助你完成工作并且尽量避免bug的软件。特别地 , 本书中的代码使用了Visual C++ 6.0和Service Pack 3及Active Template Library来编写,其中一些 例子使用了Visual Basic 6.0和VB脚本来实现,以示范COM 的多种解决方案。COM 库使用了标准 W indow sN T、95/98和2000的库。 COM+ 尽管Windows N T 、95/98支持COM, 但是只有Windows 2000支持COM+。为了开发、测试 和使用本书中的例子,你的计算机上应该装有Windows 2000。 MTS/MSMQ MTS和MSMQ可以从微软的站点上免费得到,它们是Option Pack 4.0的一部分。MSMQ至少 需要NT Server 4.0以作为基础企业控制器,MTS则可以运行在Windows N T、95/98上。 本书的结构 本书包含四部分,后一部分建立在前一部分的基础之上。我尽量使每一部分包含自己的风 格 ,但是这不会影响你的阅读。这四部分是:Windows DNA 和COM+ 本部分讲述了组件对象模型以及它与微软DNA结构的关系。主要的服务器方产品和开发工具 包含在DNA环境中。在本部分你还可以学习多层建筑的声咅设计概念(基于组件的编程思想)。 高级COM+编程技巧 假定读者已经是中高级的COM+程 序 员 ,• 在本部分中讲述了功能更加强大的、但是有时隐含 的COM/COM+特性。 比如组件的连续性、事 件 、标 记 、线程和其他高级问题。 组件管理和事务 本部分讨论了Microsoft事务服务和COM+服务。这些丰富的资源和组件管理服务比单纯的 事务要有用多。 异步组件编程 在当今移动和非连接的计算机世界,MSMQ 和COM +队列组件为之提供了异步通信。这些 技术为从应用到应用的通信提供了可容错和可升级的解决方案。 本书由高 智男、赵 崑 、唐 华 平 、黄蔚 玲 、王绣勤等翻译,卨 智 勇 作 , 最 后的 审 稿,参加本 书翻译工作的还有白桐、谭 心 、李 明 、郭 东 、刘 波 、刘 彩 伟、邓建 同、徐 丽 、唐林平等。另 外 , 姜春 林、周余平对本书的翻译提出T 不少好的建议。前言 第一部分 Windows DNA和COM+ 第 1章 COM+: Windows DNA 的 粘 合 剂 …… 1 1.1 Windows DNA .................................................1 1 . 2 谈谈因特网:HTML和XML 2 1.3 Windows DNA服 务 ........................................ J 1.3.1 Windows DNA服务:COM和COM+ -5 1.3.2 Windows DNA服务:DNA内 核 ...........6 1.3.3 Windows DNA服务:工 具 ................... 7 1.4 DNA:功能概述.............................................W 1 .4 .1 浏 览 器 .....................................................J0 1.4.2 I1S ............................................................. 10 1.4.3 ASP .........................................................11 1.4.4 MTS .........................................................12 1.4.5 MSMQ和SQL Server ............................12 1.4.6 Visual Studio 6.0 .................................... 12 1.4.7 Visual Basic .............................................14 1.4.8 Visual C++ .............................................14 1 . 5 小 结 ..................................................................15 第2 章多层组件结构........................................ 16 2.1 Ad-Hoc 设 计 .................................................16 2 . 2 基础应用程序边界:外 观、逻辑、数据 服 务 ................................................................. 口 2 . 3 三层设计.........................................................!8 2 . 4 保持层与层之间的均衡................................ 20 2 . 5 多层设计.................................... :...................20 2 . 6 本地或分布.....................................................22 2 . 7 几种不错的设计技巧.................................... 22 2 .7 .1 将应 用抽象 为 各 层................................ 23 2 .7 .2 确 定 组 件 .................................................23 2 .7 .3 创建接口 .................................................24 2 .7 .4 实 现 组 件 ........................................... 24 2 .7 .5 设 计 约 束 ........................................... 24 2 .7 .6 设 计 目 标 ........................................... 2(5 2 . 8 设计工具.................................................. 28 2 . 9 小结..........................................................29 第二部分高级COM编程技巧 第3章 COM+结构与管理................................ 3.1 COM的 发 展 ........................................... 32 3.2 MTS的缺点............................................... 3.3 COM+结 构 ...............................................33 3 .3 .1 创建COM+对 象 ................................ 34 3 .3 .2用参数表示的对象结构.....................34 3.3.3 标 记 .................................................. 35 3 .3 .4 中立线程单元....................................35 3 .3 .5 对 象 池 ...............................................35 3 .3 .6 对 象 池管 理 ....................................... 36 3 .3 .7 动态负荷均衡....................................3(5 3.4 COM+配置服务....................................... 37 3.5 COM+的资源管理....................................37 3 . 6 开发COM+应用程序................................38 3 . 7 队列组件...................................................38 3 .7 .1放入队列的事劣;............................... J9 3 .7 .2 管理队列组件....................................外 3 . 8 松散耦合事件...........................................39 3.9 COM+的数据访问....................................40 3 .9 .1 读取最优化的数据访问.....................40 3 .9 .2 事务中的共享属性管理器................. 41 3.10 COM+的 安 全 性 .................................... 41 3 .1 1 基本的COM特 性 ....................................41 3.11.1结构存储........................................... 41 3.11.2取消未完成的COM调 用 ................. 42VII 3.12 小 结 ...................................................... 42 第4 章 持 久 存 储................................................. W 4.1 〖Persist 接口.....................................................43 4.1.1 IPersistStorage ........................................44 4.1.2 丨PersistFile ............................................ 45 4.1.3 IPersistStreamlnit....................................45 4.2 IStream接口 ................................................ 46 4.2.1 IStream: :W rite ()...............................47 4.2.2 IStream::Read()........................................ 48 4.2.3 丨Stream::Seek()........................................ 49 4 . 3 创建实现IPersistStream丨nit的ATL对 象 …50 4 . 4 使用一个持久对象....................................54 4 . 5 简化持久对象的创建.................................... 56 4 . 6 简化持久对象的使用....................................55 4 . 7 小结................................................................. 61 第5章 标 记 ...........................................................62 5.1 C0M*»■对象和标记........................................ 62 5 . 2 探究标记类塑.................................................66 5.2.丨 文 件 标 记 .................................................66 5 .2 .2 运行 对象 表............................................ 69 5 .2 .3 项 目 标 记 ...........................................70 5 .2 .4 组 合 标 记 .................................................70 5 .2 .5 类 标 记 .....................................................7/ 5 .2 .6 指 针 标 记 ...........................................72 5 . 3 小结................................................................. 72 , 第6 章可连接的对象......................................73 6 . 1 连接点.............................................................73 6 . 2 连接点容器...............................................75 6 . 3 连接点举例.....................................................75 6 . 4 事件和VB ...............................................S5 6 .4 .1 重 写亊件源............................................ &5 6.4.2 ATL代 理 程 序 生 成 器............................狀 6 .4 .3 编写VB客户程序...................................90 6 . 5 各种工具实现事件时有何不同................. 91 6.5.丨 事件和VB................................................ 9/ 6.5.2 事件和C++Builder ................................93 6.6 小结..........:.............................................. 96 第7 章 C 0 M + 线程..................................................97 7.1 PC线 程 的 发 展 .............................................91 7.2 COM4■线程类型.............................................98 7 .2 .1 工 作 者 线 程 .............................................98 7 .2 .2 消 息队 列 线 程.........................................99 7.2.3 窗 U 线程.................................................100 7 .2 .4 单元线程.................................................!03 7 .2 .5 线程池..................................................... 104 7.3 COM+线 程 模 型 .........................................W5 7 .3 .1 单线程服务程序.....................................105 7 .3 .2 单元线程服务稈序...........•‘...................106 7 .3 .3 中立线程服务程序................................ 108 7 . 3 . 4 自由线程服务程序................................ 108 7 . 4 线 程 同 步 ..................................................... “! 7 .4 .1 线程局部存储.........................................111 7 .4 .2 消除并发问题.........................................111 7.5 小 结 ............................................................. 117 第8 章 COM 和注册表.........................................//9 8.1 注册表 API..................................................... 119 8.2 Regedit和Regedt32 .................................... 125 8.3 COM的注册表结构.....................................ITJ 8 .3 .1 文件扩展名............................................./27 8.3.2 ProglD..................................................... 128 • 8.3.3 A ppID ..................................................... 130 8.3.4 CLSID ..................................................... 133 8.3.5 接 口 ......................................................... 135 8.3.6 TypeLibs................................................. /36 8.4 HKEY_LOCAL_MACHINE\SOFTWARE\ MicrosoftXOle ............................................137 8 .4 .1 允许和禁止DCOM................................ ■/对 8 .4 .2 默认权限.................................................J38 8 .4 .3 传统的安全性.........................................“ 8 8 . 5 注册C0M+服 务 程 序 ................................ U 9 8.5.1 Regsvr32................................ ................139 8 . 5 . 2 自注册进程外服务程序........................140 8 .5 .3 框 架......................................................... 】41 8.6 小 结 ............................................................. 141VIII 第9 章 COM+的最忧化、继 承 及 集 合 … … 142 9.1 DCOM的 速 度 ........................................142 9 .1 .1 对象定位........................................... MJ 9 .1 .2 网络循环........................................... 9 .1 .3 混合线程摸型.........................................144 9 . 2 远 程 激 活 .....................................................148 9 . 3 远程引用计数........................................149 9.4 代 理 进 程 ......................................................!50 9.5 IC-lassFactory.................................................153 9.6 继 承 ............................................................. 155 9.7 小 结 ............................................................. J60 第10章 使 用 NT服 务 ....................................161 1 0 .1 剖析服务...............................................163 10.1.1 main()和 WinMain................................ 163 10.1.2 ServiceMainO .................................... 164 10.1.3 ServiceCtrlHandle() ............................J67 10.2 ATL和 服 务 .................................................J68 1 0 .3 为使用服务而提供的工具.....................179 10.3.1 Administrative Tools中的Services Applet .................................................179 10.3.2 Diagnostic实用工具............................180 10.3.3 Service Controller................................18J 10.3.4 Event Viewer........................................ 181 10.4 OpenSCManager() .................................... 181 1 0 .4 .1 服 务 的 句 柄 .............,..........................182 1 0 .4 .2 操 作 服 务 .............................................182 1 0 .5 经由注册表安装服务............................ M3 1 0 .6 使用亊件口志........................................!84 1 0 .6 .1 消 息 编 译 器 ........................................ 184 10.6.2 RegisterEventSource(), Deregister EventSource()和 ReportEventO .......186 10.6.3 亊件日志阅读器............................187 1 0 .7 调W你的服务.............................................J88 1 0 .7 .1 系 统 账 号 .............................................188 10.7.2任务管理器:调 试 .........................188 10.7.3使用AT命令启动调试器.................!88 10.8 小 结 .............................................................189 第11章调度 ...................................................... 190 1 1 .1 理解调度 】90 1 1 .2 类型库调度 190 1 1 .3 标准调度 !91 11.3.1 定义DLL入U 点 ................................ 792 11.3.2 类 定 义 .................................................193 11.3.3 定义UD' TypeUbGUID和CLSID …797 1 1 .3 .4 代理程序和存根程序的定义...........J98 1 1 .3 .5 注 册 表 文 件 .........................................20? 1 1 .3 .6 转换MIDL的输出文件........................203 1 1 . 4 自定义调度 205 1 1 .4 .1 声 明 对 象 的 类 .....................................206 1 1 .4 .2 定 义 对 象 的 类 .................................... 207 1 1 .4 .3 定 义 代 理 程 序 的 类 ............................277 1 1 .4 .4 客 户 程 序 .............................................214 11.5 小 结 ............................................................. 2/7 第12章 COM 的安全性..................................2/8 12.1 COM与DCOM的 安 全 性 对 比 ............... 2/8 12.2 Windows安 全 性 .........................................2/9 1 2 .2 .1 完善 域的安全 性................................ 219 1 2 .2 .2 安 全 性 描 述 符 .....................................219 12.2.3 验 证 .....................................................230 12.3 模 拟 ............................................................. 232 12.3.1 伪 装 ..................................................... 232 12.3.2 ColmpersonateClient()和 CoRevertToSelf() ................................ 233 12.3.3 伪 装 ..................................................... 235 1 2 .4 说明性安全性.............................................235 1 2 .5 程序的安全性.............................................235 1 2 .5 .1 安 全 外 壳 .............................................235 12.5.2 IClientSecurity .................................... 236 1 2 .5 .3 访 #和 运 行 的 安 全 性 ........................237 12.6 小 结 ............................................................. 238 第13章 配 置 和 错 误 处 理 ............................... 239 1 3 .1 使用DCOMCNFG配置COM+对 象 ……239 1 3 .1 .1 传统COM服 务 程 序 ............................240 1 3 .1 .2 创建 自 动 服务 程 序............................242IX 1 3 .1 .3 默 认 属 性 .............................................244 1 3 .1 .4 默 认安全 性.........................................245 1 3 .1 .5 配畀COM+服 务 程 序 ........................249 13.1.6服务程序的位置............................ 250 1 3 .1 .7 服务程序的安全性............................250 1 3 .1 .8 服务程序的身份................................ 252 13.2 使用OLE2View程 序 ................................ 25J 13.2.1 OLE2View 的缺点............................ 254 1 3 .2 .2 使用0LE2View配置COM+对象……254 1 3 .2 .3 指定远程进程内服务程序的代理-2 5 4 1 3 .3 错误处理..................................................... 257 1 3 .3 .1 错误处理策略.....................................258 13.3.2 通过丨SupportErrorlnfo传递信息.......259 13.4 小结............................................................. 264 第14章 COM 的互联网脤务.......................... 265 14.1 一个新的COM+传输协议.....................265 1 4 .2 隧道TCP协议概述................................ 266 1 4 .2 .1 配置隧道TCP协 议 .........................2(57 14.2.2 Windows 95和Windows 98中的客户 程 序 配 置 .............................................267 14.2.3 Windows NT 4.0 SP4和Windows 2000 中的客户程序配琶.........................26S 1 4 .2 .4 客户机代理服务器的配罝............... 2砧 14.2.5 Windows NT Server 4.0上的服务器 配 置 ...............................................269 14.2.6 在Windows 2000 Server上配置RPC ...................................................... 14.3 使能CIS ............................................. 1 4 .4 代理服务器的配置.............................. 1 4 .4 .1 配置微软代理服务器................... 1 4 .4 .2 防火墙的配置.............................. 1 4 .5 配罟技巧和已知的问题...................... 14.5.1 CIS客户端上不正确的代理服务器 设 置 ............................................... m 14.5.2 关于Multihomed CIS服务器的 问 题 ..................................................... 272 14.5.3 MTS对回调的使用............................272 1 4 .5 .4 有关HTTP高速缓存设备的问题……272 1 4 .5 .5 影响CIS的注册表键......................... 14.6 OBJREF标 记 ........................................273 1 4 .7 必要的编程改变....................................TM 14.8 小结...................................................... 275 第 15章 MTS ................................................ 27(5 1 5 .1 商业事务............................................... 276 1 5 .1 .1 协调事务过程................................ 277 1 5 .1 .2 事务过程与COM .............................277 1 5 .2 什么是MTS........................................... 21名 1 5 .3 使用MTS的好处....................................278 1 5 .3 .1 组件的代理进程.............................275 1 5 .3 .2 基于角色的安全性.........................278 1 5 .3 .3 准 时 激 活 ....................................... 279 15.3.4 MTS资源管理器............................ 279 1 5 .3 .5 亊 务 协 调 ....................................... 279 15.3.6 MTS与微软互联网信息服务器的 集 成 ...............................................279 15.3.7 MTS与微软消息队列服务的集成… TJ9 15.4 MTS的结构........................................... 279 15.4.1 程 序 包 ........................................... 280 15.4.2 活 动 ............................................... 281 15.4.3 角 色 ............................................... 281 15.5 配置MTS............................................... 281 15.6 MTSX寸象............................................... 283 15.6.1为MTS开 发 对象.............................283 1 5 .6 .2 向一个程序包中添加对象..............286 1 5 .6 .3 程序包的属性................................ 287 1 5 .6 .4 对 象 属 性 ........................................288 1 5 .6 .5 配置基于MTS的 对 象 ..................... 289 1 5 .6 .6 导出程序包................................... 2S9 1 5 .6 .7 导入稈序鱼....................................289 15.7 A 级MTS技巧........................................290 1 5 .7 .1 为程序包和组件提供安全性..........290 1 5 .7 .2 为程序包创建角色.........................291 1 5 .7 .3 给组件或接口分配角色..................29/ 1 5 .7 .4 通过编程影响安全性..................... 2911 5 .7 .5 直接调用者与原始调用者的对比一292 1 5 .7 .6 负 载 均 衡 .............................................29J 1 5 .8 创建基于MTS的应用程序........................293 1 5 .8 .1 使用MTS进 行 设 计 ............................ 295 1 5 .8 .2 使用MTS扩 展 应 用 程 序 ....................2奶 1 5 .8 .3 远 程 管 理 .............................................296 15.9 小结..............................................................296 第三部分组件管理与事务 第16章 作 为组件管理器的C O M +................299 16.1 COM+编程及其他基于组件的服务……300 16.2 COM+坷扩展性 特 性................................ 300 16.3 COM+和标准COM组件............................ 16.3.1 标准COM组 件 .................................... 301 1 6 .3 .2 将标准COM组件用于COM+ ........... 304 16.3.3 COM+对标准COM组 件 的 好 处 ……306 1 6 .4 通向COM+组 件之路................................ 307 1 6 .4 .1 软 件 复 用 .............................................J07 1 6 .4 .2 性能、可扩展性和稳定性............... JJ3 16.5 COM+和 状 态 .............................................314 1 6 .5 .1 状 态 的 类 型 ........................................ 314 1 6 .5 .2 状 态 存 储 .............................................m 16.6 COM+组件必备的条件............................ 1 6 .7 编写COM+组 件 .........................................3V7 1 6 .7 .1 环 境 对 象 .............................................317 1 6 .7 .2 对 象 控 制 .............................................3J8 1 6 .7 .3 使用ATL编写COM+组 件 ............... 319 ^ 1 6 .7 .4 共 亨 属性 管 理器 ................................ 32J 1 6 .7 .5 在COM+内 引 用 对 象 ........................324 1 6 .7 .6 在COM+ 内 创 建 对 象 ............... 324 16.8 小 结 ...................................••.........................325 第17章作为事务协调器的C O M +................. 1 7 .1 对事务的需求.............................................326 1 7 .1 .1 定 义 的 亊 务 ........................................ J27 17.1.2 ACID.....................................................327 17.2 MS DTC .....................................................328 17.3 一个简单的事务例子................................J29 1 7 .4 事务协议..................................................... 332 17.4.1 OLE亊 务 .............................................332 17.4.2 XA亊务................................................. 332 17.4.3 C1CS和IMS事 务 ................................ 332 17.5 COM+亊务编程模型................................ 3J2 1 7 .5 .1 创 建 事 务 ............................................. 1 7 .5 .2 完 成 事 务 处 理 .....................................337 1 7 .6 旅行社实例.................................................339 1 7 .7 监视亊务.....................................................344 1 7 .8 设计中的考虑因素.................................... 344 1 7 .8 .1 提 出细粒度 的 组 件............................345 1 7 .8 .2 定位靠近其数据源的组件............... 345 1 7 .8 .3 在冏一应用程序中将使用相同资源的 组件放在一•起.................................... 345 17.9 小 结 ..............................................................J45 第18章 COM +的安全性....................................346 18.1 COM+ 的安全概念.................................... 346 18.1.1 角 色 .....................................................347 1 8 .1 .2 安 全 性 的 职责 .................................... 348. 1 8 .2 安全支持供应商接口................................ 349 18.3 COM+声明安全性.................................... 349 1 8 .3 .1 创 建 角 色 .............................................350 1 8 .3 .2 将角色加人到组件和接口中........... 35f 1 8 .3 .3 启 用 安 全 性 .........................................35J 18.3.4 验 证 ..................................................... 352 1 8 .4 过程⑶■安全性.................................... 352 1 8 .4 .1 识 别 用 户 .............................................352 1 8 .4 .2 给 用 户 授 权 .........................................356 18.5 小结..............................................................359 第 19章 COM 事 务集 成 器............................... 360 19.1 COMTI 的 要 求 ................................•........360 19.2 大型机和Windows DNA ........................ 19.2.1 SNA Server ........................................362 19.2.2 在COMTI之 前 .................................... 363 19.2.3 COMTI .................................................363 19.2.4 COMTI警 告 ........................................ 364 19.3 CICS和CICS-LINK ................................365XI 19.4 COMTI组 件 创 建 器 ............................ J67 1 9 .4 .1 组件创建器COBOL向导................. 368 19.4.2 CICS TP .............................................369 19.4.3 CICS-LINK ........................................ 375 19.5 COMTI的 管 理 控 制 台 .........................375 19.6 C0MT1运 行 时 间 ................................ m 19.7 小结...................................................... 379 第20章 负载均衡组件.•.................................. 380 2 0 . 1 负载均衡组件的定义............................ 380 2 0 .2 负载均衡组件的必要性.........................38J 20.2A 可 扩 展 性 ....................................... 381 20.2.2 有 效 性 ........................................... 382 2 0 .2 .3 灵 活 性 ........................................... 对2 2 0 . 3 并行性和粒度大小................................ 382 2 0 . 4 动态负载均衡算法................................ 384 2 0 . 5 负载均衡组件设计................................ 385 2 0 .6 负载均衡组件客户机设计.....................385 2 0 .7 坏消息...................................................385 2 0 .8 不用中央并行处理器的负载均衡 组 件 ...................................................... 20.8.1 用SCMT.作 ....................................386 20.8.2 CoCreatelnstance 带来的问题..........388 20.8 .3创建一个包套................................ 390 20.8.4 算 法 ...............................................39J 20.8.5 时 间 方 法 ...........................•............392 2 0 .8 .6时间方法的算法............................ 2 0 .8 .7 负载均衡的实现............................ 395 2 0 .8 .8其他均衡和分类技术.....................J96 2 0 .8 .9 运行时编译执行技术活化............. 20.9 小结...................................................... 397 第21章 优化Windows D N A应 用 程 序........398 2 1 .1 估计你的需要....................................... 398 2 1 .2 最优化技巧........................................... 399 2 1 .2 .1 使用用户或系统DSN代替文件 DSN ..................................................... 399 21.2.2优化算法,特别是反复循环..........外9 2 1 .2 .3 避免注册表存取访问.....................400 2 1 .2 .4 在任何可能的时候都用运行时编译 执 行 即 时 激 活 .....................................401 2 1 .2 .5 修 复 资 源 漏 失 .................................... 402 2 1 .2 .6 面叫对象的负载均衡组件实用性 体 系 结 构 .............................................402 2 1 .2 .7 选 择 工 作 语 言 .................................... 4仍 2 1 .2 .8 避 免 中 间 层 状 态................................ 403 2 1 .2 .9 避免数 据访问中间层........................403 2 1 . 3 使用微软Windows DNA工 具 包 ...........404 2 1 . 4 观察测试的结果.........................................408 21.5 小 结 ............................................................. 408 第四部分异步组件程序设计 第22章 松 散 耦 合 程 序 设 计 .......................... 409 2 2 . 1 什么是消息传递.........................................409 2 2 . 2 消息传递的优点.........................................410 2 2 .2 .1 用消息传递加强大型应用程序的 开 发 ..................................................... 410 2 2 .2 .2 消息传递史好地利用通信资源……410 2 2 .2 .3 消息传递在不同系统中取得一致…川 2 2 . 3 消息传递的弱点.........................................411 22.3.丨 延 长 处 理 时 间 .....................................412 2 2 .3 .2 异 步 执 行 .............................................4!2 2 2 . 4 同步与异步程序设i f ................................ 412 2 2 . 5 可扩展性..................................................... 414 2 2 . 6 面向消息的中间设备................................ 414 22.6.1 MOM 程序接口.....................................415 22.6.2 MOM系统软件.....................................4J5 2 2 .6 .3 管 理 工 具 .............................................415 2 2 . 7 微软消息队列服务器.................................415 22.7.1 MSMQ连 接 器 .....................................416 22.7.2 MSMQ和别的API ............................416 22.7.3 MSMQ和Email.....................................416 22.8 小 结 ..............................................................416 第23章 MSMQ管理机构和体系结构........417 23.1 MSMQ对象和属性.................................... 417 23.2 消 息 ............................................................. 4i9XII 23.3 队 列..............................................................420 23.3.丨 队 列 类 型 .............................................420 2 3 .3 .2 消 息 队 列 .............................................42! 2 3 .3 .3 管 理 队 列 .............................................421 2 3 .3 .4 应 答 队 列 .............................................421 2 3 .3 .5 日 志 队 列 .............................................42i 2 3 .3 .6 死 信 队 列 .............................................421 2 3 .3 .7 报 告 队 列 .............................................422 2 3 . 4 消息队列信息服务.................................... 422 2 3 . 5 本地队列存储.............................................422 2 3 . 6 队列属性..................................................... 422 2 3 . 7 优先级......................................................... 423 2 3 . 8 事务队列..................................................... 423 2 3 . 9 标识队列..................................................... 423 23.9.1 路 径 名 .................................................423 2 3 .9 .2 格 式 名 称 .............................................423 2 3 .9 .3 示 例 标 识 符 .........................................425 23.9.4 标 志 .....................................................425 23.9.5 类 型 ..................................................... 425 2 3 .9 .6 私 有 队 列 .............................................425 23.10 机 器 ......................................................... 425 23.U MSMQ企 业 .............................................426 2 3 .1 1 .1 站点连接.............................................426 2 3 .1 1 .2 连接的网络.........................................426 23.11.3 MSMQ控制器.................................... 426 23.12 MSMQ客 户 机 .........................................426 23.13 MSMQ管 理 机 构 .................................... 427 23.14 小 结 ......................................................... 427 第24章 MSMQ程 序 设 计 ............................... 425 24.1 MSMQIf-API .............................................428 2 4 . 2 用MSMQ库AP丨创建一个应用程序……429 2 4 .2 .1 格 式 名 称 .............................................429 24.2.2 路 径 名 .................................................430 2 4 .2 .3 査 找 格 式 名 称 .................................... 430 2 4 .2 .4 用 属 性 工 作 .........................................430 2 4 .2 .5 创 建 队 列 .............................................431 2 4 .2 .6 解 敗 队 列 .............................................431 2 4 .2 .7 打 开 队 列 .............................................432 2 4 .2 .8 发送一条消息................................ 432 2 4 .2 .9 接收一条消息.................................... 434 2 4 .2 .1 0 关闭队列........................................435 24.3 MSMQ ActiveX控制API ........................441 2 4 . 4 用COM+接门创建MSMQ应用程序……442 24.4.1 定义接口和 GUID................................ 442 24.4.2 初始化COM ........................................ 443 2 4 .4 .3 创 建 队 列 .............................................443 24.4.4 变 体 型 .................................................444 24.4.5 BSTR.....................................................444 2 4 .4 .6 解 散 队 列 .............................................445 2 4 .4 .7 打 开 队 列 .............................................445 2 4 .4 .8 发 送 消 息 .............................................445 2 4 .4 .9 接 收 消 息 .............................................446 2 4 .4 .1 0 关闭队列.............................................448 2 4 . 5 用灵巧指针创建一个MSMQ应用程序…454 24.5.1 定义接口和 G U 1D ................................ 454 24.5.2 .tli文 件 .................................................457 24.5.3 ATL从 属 物 .........................................457 2 4 .5 .4 创 建 队 列 .............................................458 2 4 .5 .5 解 散 队 列 .............................................458 2 4 .5 .6 打 开 队 列 .............................................458 2 4 .5 .7 发 送 消 息 ..................... .......................459 2 4 .5 .8 接 收 消 息 .............................................459 2 4 .5 .9 关 闭 队 列 .............................................460 2 4 . 6 用VBScript创建一个MSMQ应用程序…463 24.7 小结..............................................................466 第25章 高级MSMQ程序 设 计 ................... 467 25.1 游标 ..............................................................468 25.1.1 MSMQAPI 游 标 ................................ 469 25.1.2 MSMQ ActiveX组 件 游 标 ................471 2 5 . 2 查找队列...............................................473 2 5 . 3 消息确认、应答和记录.........................475 2 5 .3 .1 行 政管理队列.................................... 475 2 5 .3 .2 应 答 队 列 .............................................480 25.3.3 消息ID .................................................480XIII 25.3.4 记 录 .....................................................481 2 5 . 4 事务处理.....................................................481 2 5 .4 .1 消 息 事 务 处 理 .................................... 482 25.4.2 ITransaction .........................................482 2 5 .4 .3 创 建 事 务 队 列 .................................... 483 2 5 .4 .4 事 务处 理 的类 型................................ 483 2 5 .4 .5 外 部 事 务 处 理 .................................... 487 25.5 MSMQ Email API .................................... 494 2 5 . 6 异步操作.....................................................495 2 5 .6 .1 自 动 亊 件 .............................................496 2 5 .6 .2 系 统 事 件 对 象 .................................... 496 2 5 .6 .3 回 调 函 数 .............................................500 25.6.4 完成端口 .............................................505 2 5 . 7 队列安全性.................................................510 25.8 小 结 ............................................................. 511 第26章 松 散 耦 合 事 件 ....................................512 26.1 一些基本术语.............................................5!2 2 6 .1 .1 设 计 模 式 .............................................5!3 26.1.2 发 布 人 .................................................513 26.1.3 订 阅 人 .................................................513 26.1.4 COM+事 件 服 务 ................................ 5 U 2 6 .2 发布-订阅选项的比较............................5 /J 26.2.1 轮 洵 .....................................................5t3 2 6 .2 .2 紧 密 耦合 事 件.................................... 5M 2 6 .2 .3 过 紧 密 耦 合 .........................................5f4 2 6 .2 .4 要求并行的组件生存期....................515 2 6 .2 .5 无 法 过 滤 噪 声 .................................... 515 .2 6 .2 .6 松 散 耦 合 事 件 .................................... 5J5 26.3 COM+事 件服务........................................ 5J6 2 6 . 4 事件服务的演示........................................ 5J8 2 6 . 5 高级COM+ 事件服务问题........................523 26.5.1 订单和 lEventSubscription 接 口 ……523 2 6 .5 .2 使用短期i f 阅 单 ................................ 524 2 6 .5 .3 注 册 短 期 订 阅 单................................ S24 2 6 .5 .4 取消注册短期订阅单........................526 2 6 . 6 事件过滤.....................................................527 2 6 .6 .1 生 成 过 滤 器 串 .................................... 527 2 6 .6 .2 利用程序生成过滤器串....................528 26.7 小 结 ............................................................. 529 第2 7 章 队 列 组 件 .............................................530 2 7 . 1 队列组件概述.............................................530 2 7 . 2 分布式计算及队列组件............................531 2 7 .2 .1 确 认所接收的数据............................532 2 7 .2 .2 服务器请求更多的数据................... 532 2 7 .2 .3 确 认所执行 的操 作............................5J2 2 7 .2 .4 需 要 杳 找 数 据 .................................... 2 7 .2 .5 确 定 是 否 排 队 .................................... 2 7 . 3 队列组件结构.............................................534 2 7 .3 .1 生成并定义一个队列组件............... 534 2 7 .3 .2 客 户方 的队 列组 件............................535 2 7 .3 .3 服务器方的队列组件........................ 2 7 . 4 编写应用队列组件的一个演示程序……5允 2 7 .4 .1 使用Visual C++和ATL编写一个队列 组 件 .....................................................536 2 7 .4 .2 安 装队 列组 件 .................................... 5J9 2 7 .4 .3 定义COM+应 用 程 序 ........................539 2 7 .4 .4 标记需排队的COM+应 用 程 ……541 2 7 .4 .5 向COM+应 用 程 序 中添加 组 件……542 2 7 .4 .6 标记COM接口为排队的....................542 2 7 .4 .7 用Visual C++编写客户端应用程序…5 # 2 7 . 5 测试组件和客户程序................................ 5妨 2 7 . 6 导出COM+应 用 程 序................................ 549 27.7 小 结 ............................................................. 550 光盘使用说明...................................................... 55/ISBN 7-111-08951 _ www.china-pub.com 1 _ • 北京市西城区百万庄南街1号 100037 购书热线:(010)68995265, 8006100280 (北* 地区> 舉 P © 發 ISBN 7-111 -08951 -0/TP • 1921 定 价 :7 6 .0 0 元 (附光 盘 ) 造用水平:中、问级 Windows 2000 IS8N7-111-07571 页 2 9 7 页 : DNS技术指南 TP - 1199 价 32 0 0 元 Visual C++ 6 学习指南 ISBN7-111-07280 4 TP- 1118 页 玆 3 5 2 页 定 价 48 0 0 元 © COM+ Unleashed COM+ 技术大全 COM+编程内幕 • 高级COM+编程技巧 • COM+完整参考手册 丰富实例.专家经验的展示 * 配套光盘包括本书实例代码:以及微软最新信息 《Windows 2000 编程技术内幕►铃 《Windows W DM 设备驱动程序开发指南^杉 « < M F C 开发人员指南> *-2 < O p e n G L 参考手册> (第3版) 《De lp h i 5 编程实例与技巧》 i D e lp h i 5 企业级解决方案及应用剖析 ♦ Pe rl 5 编程详解* « J a v a 2 核心技术卷h 基础知识》1。2 《Ja v a 2 核心技术卷丨丨:高级特性》柃 «J a v a 2 图形设计卷丨:AW T ♦ (<} U a v a 2 图形设计卷丨丨:SWING 丨14 « J a v a 2 类库> (增补飯) < J i n i核心技术》 USP 高级编程》 (CORBA 企业解决方案》 «C++Builder5 编程实例与技巧丨« ♦ SQL Server 7 编程技术内幕> 《S Q L - 3 参 考 大 全 H 《J a v a S c r i p t 技术大全》付 《P y t h o n 核心编程柃 « C O M + 技术大全》杉第一部分 Windows DNA和COM+ 这部分内容包括: •COM +: Windows DNA 的粘合剂 • 多层组件结构 第1章 COM+: Windows DNA的粘合剂 本章内容: • Windows DNA • 谈谈因特网:HTML和XML • Windows DNA服务 •DNA:功能概述 1.1 Windows DNA DNA,即分布式互联网应用程序结构,是目前很多软件开发人员常用的一个词。它是有关 编写运行于微软分布式环境下(或者运行于经由国际互联网或内部互联网进行浏览的浏览器) 的可扩展且强健的应用程序的。互联网的发展正呈现指数增长,还暂时看不到平缓的趋势。微 软预见到这一发展趋势,力图使它的IE浏览器成为市场的领导者,不仅在用户和客户端,而且 也要在开发端占据领导地位。因此,DNA出现了。 汇 集 互联网正在快速地成为大多数新软件首选的开发平台。但是现存的所有程序模型、 技术和软件代码该怎么办呢?利用互联网进行会聚的时代即将到来。通过将原有代码封 装在组件当中,同时使用DNA进行新的开发,则可能得到最好的客户机-服务器系统 ( 多层结构、分布式工作、事务处理、队列),再加上互联网要素(脚本、平台、可复 用组件),就可以生成一个坚固的框架以适应所有现存的技术。这使得一个企业可以逐 渐修改原有系统,并以可扩展、可复用、可靠的系统取而代之(参见第19章 “COM事 务集成器” ,详细介绍了传统的系统和DNA)。 DNA是一个抽象概念。对于编写DNA程序来说,不像COM/DCOM/COM+那样有统一的规 范 ,决不存在带DNA标志的需求或者任何要求DNA遵守的规则。 微软在发展DNA h 做出了不懈的努力,使之成为编写可扩展的多层互联网程序的稳定框架 ( 多层结构的设计问题将在第2章中进行讨论)。 对 于开发者来说,这一框架更像是微软的工具和产品的指南,指导你在何时使用它们进行第一部分 W k如 m DNA和CC^+ 多层设计。例 如 ,MSMQ和MTS这两个DNA服 务 ,极大地减轻了实现互联网现有应用程序的工 作量,并且提高了可靠性。更重要的是,大多数DNA结构提供的服务对于程序员来说是免费的, 尤其是对COM+的介 绍 。在 某 种 情 况 下 (如MTS ) , 只需要将组件拖放到服务的操作环境中,就 可 以 0 动地获得服务提供的所有好处。你可以通过改变组件管理器中的一项设置,轻松实现动 态的负荷均衡。 事实上 ,COM+已经成为独立于DNA的一个整体,它为程序员们提供了开发多层应用程序 所必需的底层结构,而无需编写底层结构代码,使他们可以专注于业务问题的解决。 提 示 无 论 你是 在 编 写简 单的 网 上零 售 商 品目录,还是一个完善的企业范围的、可以对 用户交互进行控制的、安全的内联网 ,DNA都可以显著地减少你的Web应用程序的开 发时间。 在信息技术管理者看来,DNA极 大 地减 少r 为拥有一个系统而付出的代价,并且可以均衡 用于传统技术和技能的投资。DNA提供了处理与可扩展互联网应用程序有关的、更复杂的低层 通信细节的服务。这一点会极大地减少开发系统所用的经费。 处在DNA前端的是HTML和XML。HTML是一种与平台无关的语言,事实上它允许所有个 人和商用电脑进行相互间的通信。XML是HTML的延 伸 ,它给了你 极大 的灵 活 性,包括创建 COM+对象的能力。 提 示 你 的 应 用 程 序 不 需 要 Web或互联网要素就可以得到本章讨论的DNA服务的全部利 益。MTS和MSMQ可以像用于分布式环境一样,很容易地用于独立的本地程序的开发, 事实上,由于COM的位置透明特性,你的DNA应用程序根本就不知道自己到底是运行 在独立环境中,还是分布环境中,而且它根本不关心这些问题。 1 . 2 谈 谈 因 特 网 :HTML和XML 普通的HTML ( 因特网语言)不提供控制流机制或变量。当一个HTML页面创建之后,该页 面是静 态的 ,不可能因为我们的输入而改变它的结果。幸好你可 以借 助动 态HTML、XML和 ASP来补充HTML,使人产生一种错觉,即你的网络环境是丰富的,并且具有各种状态。依靠这 样的程序,状 态 (保持一个程序的各个变量的能力)也许就不是必需的了。 无状态环境 具有所有装饰性多媒体的浏览器,其实是为那些以浏览为实际目的的人准备的。它们无状 态且非线性;也就是说,它们会随机地从一个页面跳到另一个页面,根本没有事先预定的路线。 没有DNA技 术 的 帮 助 (如IIS会话或DHTML变量或XML scriptlet) , 基于浏览器的应用程序 不可能从一个页面状态保持到另一个页面。这对于使用浏览器作为交互式开发平台的软件开发 人员来说,是个很大的挑战。 保持状态对于传统的事件驱动或有限状态机模式的Windows应用程序来说,是不成问题的。 为 此 ,HTML、XML和DNA改变了Windows的 编 程 模 式 (受控的GUI环 境 ),迫使开发人员考虑 无状态模式。对于复杂问题,DNA应用程序的用户(网上浏览者)的工作环境将更加无拘无束。第1章 COM^ : Windows DNA的粘合别 客户或者浏览者可以不经过预定的步骤或状态,随机地浏览一个网站。如果你对自动机理 论比较熟悉,浏览者就像一个非确定的有限自动机,它有一个初态和一个终态c 只要将数据嵌入URL 就 可 能实现 将信 息 从一页带 到 下 一页,但这对编程来说是很困难的 ( 需要做很多的分析丁-作 ),而 &不利于扩展。Cookie是一个很好的选择,因为它可以把服务器 端的信息保存在客户端,何对于较老的浏览器来说是不具有这项功能的,而且用户也可能因为 安全或隐私的原因关掉这项功能。 COM+可以作为解决这一问题的桥梁。它可以很容易地将信息保存在客户机上,吋以解决大 多数由保存状态而引发的问题。 另一方 面,服务器面临着这样的问题,当一个表面上是匿名的连接随机地从一贞跳到另一 页 时 ,如何追踪它。通过映射客户的IP地址并使用定吋器,可以创建会话线程,并将每个访问 服务器的浏览器的状态记录下来。这是微软的IIS为客户保存状态的一种方法。你可以从下面儿 部分更加详细地了解到这种方法和其他的儿种方法,但首先你必须了解Windows DNA服务c 1.3 Windows DNA服务 从右到左读图1-1,你可以从一个比较高的层次开始了解DNA。这张图突出了DNA框架中相 关的服务和技术。再说一遍,DNA 没有标准,这张图只代表作者眼中的DNA。不存在任何关键 组成部分使你的程序适应DNA。这张图提出的是一种典铟的DNA框架的安排。 提 示 如 果 你 对 三层 结 构 模 型 比 较 熟 悉 ,你会注意到下面的讨论中提到了它的模式。如 果你没注意到也没关系,第2章中将深入探讨。 DNA 核心 COM COM+ 工具 用户界面和导航 分布式操作环境 HTML/XML | /Script 编写 f e e 1 仆 盤 ! .“, — 一 岬 互操作性 j 队列组件 i 组件创立 位置透明性 负 载 均 衡 | I 快速应用 1 挖 [消 ! 侧 豐 安全性 内 存 数 据 库 | ....捏 E m — 集成存储 连网 亊件服务 组开发 MJL 歷 」 「Email \ \ ^ m ^ 苺础服务 图 1-1 Windows DNA 服务 1.3.1 Windows DNA服 务 :COM和COM+ 为了使DNA工 作 ,操作系统的内核必须予以支持。一个特权服务必须随时存在,以完成通 佶协议的解释和检查以及组件的协作。这就足为什么COM7DCOM/COM+(以及服务控制管理器)4 第一部分 DNA和COAf+ 会作为分布式操作环境出现在DNA服 务 的 图 中 (参见图1-1 )。 COM包含所 有出现在图1-1最右边一列 的服 务;COM+包含第二列 的 服 务 。操作系统中的 COM+库提供了组件之间的必要纽带或管道服务,使它们以标准的方式进行通信和操作。COM 库所提供的服务有互操作性、位置透明性、安 全性、网络互连以及低级服务。每一项都将在下 面详细讨论。COM增加了队列组件、负荷均衡、内存中的数据库以及事件服务。下面的两项组 成分布式操作环境: • 互操作性—— 为了使组件技术能够起作用,必须存在一个发现二进制权能的持续迸程。组 件应该可以杳询其他组件,并且可以通过- 个析构进程发现是否有请求存在。COM提供了 对这个进程的访问,所有的绀件都可以通过IUnknown接口的Query丨merface机制迸行这个 访问。 • 位置透明性—— 在像DNA这样的分布环境中,组件不应该知道它的物理位置。它也不应该 硬编码它想访问的其他组件的物理位置。当然,一 定 有 “人” 知道组件在哪儿,在COM屮 这 个 “人” 就是Windows注册表。注册表使得DNA应用程序可以像在自己本地的环境里执 行一样,在互联网的任何地方执行。 1. 安全性 使用DNA的时候不可忽视安全性问题。 当你将系统分布于内联网或互联网上时,真正的暴 露和威胁将随之产生。 安全性问题主要分为两个部分:客户和服务器。对于客户而 言,执行 下载的二进制 映象 、 组件或其他文件时,将存在危 险,很可能使自己的机器染上病毒,或造成数据的丢失。这些危 险可以通过使用杏毒软件、证书密钥技术和验证可信数据源的CA ( 证 书 权 限 )来避免。丐 然, 不存在100%的安全保证,因为即使证书被验证了,也 还 是 有 机会被 假冒的(+ 要把声称为可信 的探听实体与后面要讲到的MTS假 冒相混 洧)。 对于一个典型的企业范围的DNA系统,这可能不是一个主要问题,闵为这样的分布式环境 使用的是物理隔绝的、装有防火墙的内联网,任何闯人 行为都 只可 能 是 来自内 部 的 (对此没有 阻止的方法)。 . 对于服务器而言,安全缺陷将导致更加严重的结果。服务器级的缺陷会导致各种各样的问 题 ,包括网络范围的数据破坏和知识产权的被窃。Windows NT提供了政府认可的、内核和文件 系统级的C2级安全保障,来帮助保护服务器,但这通常还不够,还需要一个网络中间层。 在客户和服务器之间存在着网络安全协议,如 加 密 套 接 字 协 议 层 (SSL)、TCP/IP防 火 墙 、 DCOM或 者 服 务 器 端 的 产 品 特 定 安 全 协 议 (如MTS/MSMQ),协议的不同依赖于通信介质的 不同。 DCOM安全协议将在第三部分深入讨论,而TCP/IP防火墙超出了本1$的 范闱,但关于这方 面的文献还是很多的。 MSMQ和MTS也有强大的安全系统,如将在本书后面部分讨论的加密。 尽管客户和服务器、上有这些安全协议,DNA应用程序还是会受到安全缺陷的威胁。没有一 个电子系统是完美无缺的。最好始终为你的DNA系统保留一套后备方案,以防故意破坏或盗窃 等 悄况的发 生。每晚的条份工作足必需的,并且应该分散到各台机器,它们要提供容错功能。第 1 章 C0A/+: Windows DNA 的粘合M 也就是说,一台机器保存所有的代码,另一台为应用程序组件提供服务,第三台保存所有的网 络服务程序等等。 提 示 对 于 高度 机密 的电 子信 息,惟一安全的方法就是使计算机完全与其他计算机隔离: 锁在屋里并且不连网。即使这样也要始终牢记没有绝对的安全。如果有足够的时间和资 源 ,任何形式的安•全保护都可能被破坏、袭击、闯入或者变为毫无用处。 幸运的是,COM+提供了较坚固的安全模型。开发人员可以很容易地利用这种安全 模 型 ,而无需编写更多代码。 2. 网络互连 网络互连对于分布式系统来说是极为東要的。在DNA中 ,你可以使用几种高层协议来实现 应 用程序的网络互连:COM+、DCOM、MSMQ, HTTP等 。它们都将协议栈的低层封装起來 ( 如TCP/IP),这样做非常有好处,可以利用的封装越多,DNA系统就越易于维护和复用。 MSMQ虽然不是网络协议,但它的确能够强化应用程序之间通过网络的相互通信。MSMQ 可以被设定使用多种流行的协议,如TCP/IP,并以简单的发送/接受式API封装所有的协议通信 细节。在以后的章节将详细讨论M SM(^ 3 .低级服务 其他的一些为使组件正常工作所必需的服务,被归到这一类—— 线程脊理 、事 务 、同步支 持 、组件注册和调试等。 我们不能想当然地认为COM低级服务足由操作系统提供的。如罘所有的组件都要携带若代 码去理解不同线程模型,或者参与事务的1:.下文环境,那么这将导致组件极度膨胀且运行缓慢。 此 外 ,如果把类似安全服务这样的低级服务硬编码在组件里,那么它们将会因为采用了新的安 全平台,而很快的变得与其他组件不兼容。 因 此,低级服务是必不可少的,它们可以提供一个环境,使得组件可以更加自由,而不必 考虑网络互连或者操作系统的实现等问题。 例 如,COM的低级服务的实现在UNIX环境与在NT或Windows95环境下肯定截然不同。如 果一个组件与COM兼 容 ,那么它一定可以在不同种类的 环 境 中进 行互操作 ,因为是低级服务 处理一切兼容性问题。组件只是简单地通知低级服务的API, 然后就可以由操作系统来做其余 的事。 本书后面部分将聚焦这些低级服务,并讨论如何利用它们。 4 .队列组件 假设一个应用程序调用一个在远程服务器上COM对 象 ,如果这个服务器由于某种原因无法 连 接上 ,那么通信就会出现问题。客户一直要尝试到远程服务器上网为止吗?或者客户干脆完 全取消这个进程?这个问题曾经折磨着许多DCOM的开发人员。 这个问题的答案就在于COM+的一个新的服务,就是众所周知的队列组件。这项服务允许客 户对象调用远程对象,即使这个远程对象无法连接,调用也将被一个系统实用程序记录,通过 异步协议传送到服务器,并由另一个系统实用程序在服务器端可用时,向服务器端的COM对象 回放调用。6 第一部分 IV/n办vw DNA和COM+ 这解决了开发人员的一个大难题,他们再也不必担心远程对象的当前状态了 :即使远程对 象无法连接,任务最终也会完成的C 5. 事件月足务 假设你有一个这样的系统:内部的数据周期性地改变,并旦要在重要的数据改变时,更新 在远程对象上的数据。如果没有一个特殊的机制来通知数据的改变,那么在有数据改变时,你 必须依赖一个具有查询和恢复功能的系统。 COM+增加了一项名为事件服务的功能。它将程序分为两类:出版者和订阅者。 出版者提 供 信息更新,订阅者则以通知的形式接收信息的更新。COM+的事件服务为汀阅者提供了一种 从出版者那里接收通知的、简单的/T法 ,也为出版者提供了简便的查找和呼叫它们的订阅者的 方法《 6. 内存中的数据库 内 存 中 的 数 据库(IMDB),提高了那些以从数据库中读取只读信息为主的企业级应用程序 的性能。它所 做 的就 是 ,缓存那唑包含在中间层机器的后端表格中的数据库信息。这 样 ,不必 访问存储介质就可以将数据从内存中迅速地调出。 7. 负荷均衡 对 于单 独一 台 服 务 器 来 说 ,你的 企 业级应 用 程 序太大 了 的 时候会 发 生什么 ?这是个许多 DCOM开 发 人 员回 答不 了 的 问题。直到COM+增 加 了 负 荷 均 衡 服 务 ,这 个 问 题 才 得 到 解 决 。 COM+的负荷均衡服务提供了一种自动机制,用以在一个分配池中,向儿个服务器分ffi对象创 建请求,从而分散负荷。 1.3.2 Windows DNA服 务 :DNA内核 在图i- i的中间是三层服务。每项服务都和严格定义的、只引出公共行为的接n 捆绑在一起= 它们分别是用户界面和导航、业务进程、综合存储。你将在第2$ •中更进一步地了解这些层所代 表的是什么,这里只是在了解DNA的同时了解一下它们。 1. 用户界面和导航 像Visual Basic或Visual IntcrDev ( VI ) 这样立足于用户界面层次的开发工具,因为可以运月J 各 种 控 件 (组 合 框 、按 钮 、滚 动 条 等 )快速地生成图形界面环境而大受欢迎。更由于它们衬以 封装低层COM细节,而成为组件开发人员的扱爱。VB可以通过使用ActiveX DLL向导轻松创建 COM组 件 ,而VI则可以通过鼠标的几下单击将源代码变为与COM兼容的代码。 像ASP、VBScript、ECMA JavaScript这样的脚本技术也很流行,主要是因为它们简单的语 法和友好的用户界面。虽然它们运行起来有一些慢,并且不像它们的编译副本那样强大. 似因 为它们易学易用,对应用程序来说还是很有价值的。任 何 时候,要将一种语言编写的代码夹在 用另一种语言编写的代码中一起编译,通常都会在性能上有所损失。 C++虽然不如它的RAD副 本 (VB、VI, VJ++等 )那样快速而简单,但也可以用于用户界面 层次。然 而 ,C++可以使开发人员更好的控制图形环境和硬件。VC++也可以生成最小的而且很 高效的二进制映像。 2. 业务进程第1 章 CCW+ ; Windows DNA的粘合剂 这是DNA的心脏,这里有核心的服务器端产品,可以使开发人员更专注于解决问题而不是 实现细节。例 如 ,大多数用于管理事务的筲道是由MTS提供的。如果需要队列服务,MSMQ可 以只做很少的设置和开发工作,便可以导出一个简单的接口来实现它。最 后,为了将应用程序 连接到互联网,还需要一个像微软IIS那样的HTTP服务器。你将在后面的章节屮发现,I1S除了 解释HTTP外 ,还做很多事。 应用程序还必须解决一个问题,这样完成以上任务的逻辑和规则才存在于这一层。在使用 COM+日、丨,将 规则封装在组件中更好,但这不是必需的。COM+组件允许复用和平台间的互操 作 ,但 是 ,只要业务规则可以被激活,并11信息能在层与层之间相互传递,任何编程技术都可 以使用。 提 示 服 务 器 端 产 品 ,如MTS和MSMQ,可以用在本地独立的环境中(,DNA腋务有一 个清楚的设计目标,即可伸缩性,尤其是在网络流量上,但这不意味着它只能用于分布 式环境。 还有重要的一点需要注意,那就是MSMQ不只属于中间层。MSMQ可以用作存储 的一种形式,这时它属于最低层 :综合存储。下一部分中假定的例子说明了这一概 念 : 3 .综合存储 不是所有的存储都要与数据库有联系。许多形式的物理存储是可以被接受的,而且有时是 必要的。有了DNA技术以及它与互联网的紧密联系,电子邮件或多媒体数据流(如音 频或视频) 对于原来的数据资源或接收器来说,是个强大的竞争对手^ 当不需要相关数据的时候,文件系 统也可以作为存储方式来使用。例 如 ,日志文件就是在应用程序运行时,存放在文件系统中的 典型文本文件。 当数据必须与其他数据关联时,一个相关的数据库,如SQL Server 7.0,就可以用于微软的 通用数据访问机制。 由于OLE DB及它 的 扩 展 集(Active Data Object ) 的组件特性,使它们对于 DNA来说是最完美的。其他的存储方式,如协作流方式,也可以存在。 1.3.3 Windows DNA服 务 :工 具 为了使用前面提到的各种服务,必须有一些工具来辅助DNA应用程序的集成。这里讨论的 工具与在DNA内核部分讨论的部分工具有所重叠,但这一层是面向DNA的内核和分布式环境的 开发的。 1. HTML的编写 HTML是介于DNA和浏览器所不知的应用程序之间的一扇门。岀一个应用程序的前端要被 不同种类的应用程序访问时,HTML是惟一的解决办法。HTML不仅是一种语言,而且是一种普 遍存在的平台。有了HTML和服务器端或客户端脚本,开发人员便可以编写出内容丰富的应用程 序 ,而无需考虑硬件或平台的问题,也不必关注前端配置、操作系统以及驱动程序等问题,而 且 ,部署变得不成问题,应用程序在每一次被访问的时候都是全新的。 注意 HTML的理想和现实之间可能有所差距,而最近几年这个问题越发明显。是的, HTML的目的是跨越平台,并允许开发人员一次性地编写出精美的程序。但随着脚本和8 第一部分 Wim/ows DNA和 组件的出现,在某些情况下可以证明,这和编写本地代码应用程序完全不同u 虽然存在许多创建HTML的工具,但你会发现Notepad也是其中之一。记事本是操作系统附 带的一种简单的、不加任何渲染的文本编辑器,它流行的原因是其易用性。HTML是很直接的语 言 ,没有逻辑、状态或是控制流,仅仅是标出标签。茧然WYSIWYG的HTML编辑器很易于使用, 尤其是在涉及图片的时候,但HTML足够简笮了,可以快速地编写,并且通常采取反复试验的设 计 方 法 (不推荐读者使用)。 正如前面提到的,只有HTML,是不足以开发出具有复杂的用户界面、内容丰富叵强大的应 用程序的。服务器端脚本技术(如ASP ) 和 客 户 端 组 件 (如Java applet或COM ActiveX控 件 ), 可以被用于增强HTML简单的功能。即使浏览器以不同的方式解释HTML,还是可能使用HTTP 让客户和服务器进行通信,这与TCP/IP Socket相比确实向前迈进了一步。 2. 组件的创建 组件比HTML复杂和强大许多倍,二者不在同一个数量级上。Visual Basic 6.0 S u itd i创建 DNA组件的理想幵发环境,因为它与许多服务器端产品(如IIS、MSMQ、SNA Server和MTS ) 兼容。VB、VI、VJ++N VC++也可以用于创建组件,而 &在C0M+的强大功能下,它们之间可 以相互协调。 在一个组件完成编译并注册之后,它可以被用于DNA框架的任何一层。COM的位置透明特 性可以使各层彻底实现分布,而无需组件开发人员做任何编程工作。 3 .快速应用程序开发 快 速 应 用 程 序 开 发 (RAD)工 具 (如VB) 已经& 前面讨论DNA内核的外观和导航层中提到 过 ,但那不是这些工具惟一可以使用的地方c RAD可以在整个DNA领域内使用,而 & 有时在你 设计原型时,它是惟一的选择。 . 当DNA和COM开发人员试图得到最高效的DNA解决方案时,RAD对于他们来说是非常重要 的 ,无论要设计的是原型还是实际软件产品。 即使有-.个非常详细的应用程序设计方案,还是 很难符合DNA的要求。工具的组合以及可以用于各层的产品非常多而复杂,有时甚至过剩。你 需要从Visual Studio得到帮助,使你的工作迅速完成,并呈现在用户的面前,并在这一过程中发 现程序的局限和缺欠。 使用RAD工具迅速生成原型的一个极好的例子是V16.0附带的DTC ( 设 计 时 间 控 件 )库 。 HTML DTC大大节省了用于编写基于浏览器数据库的应用程序的时间,例 如 ,只需要儿下鼠标 的单击和儿项属性的设K ,一个网格控件的DTC便可以连接到数据库,定 位表的信息,并将行 和列数据填入网格。然后就可以用组件将这一原型扩充,并迅速变为最终的产品。 4. 团队开发 DNA是分布式的,因此它的开发环境也应该是分布式的。由一个人来完成DNA系统所必需 的一切是有可能的,但更高效的方法是将工作任务分割,由一个Ifl队来共同完成。 对于传统的Windows应用程序的开发来说,一组开沒人员要做到协调一致是很不容易的,因 为要涉及到许多的耦合问题。而使用DNA ( 和基于 组 件 的 软 件 )则可以轻松地实现松散耦合, 而不必把集成问题作为主要问题来考虑。 例 如,写 B 录的人可以独立于数据组件开发者而独立工作,因为它们在DNA框架中关系不第7章 COM+: 的姑合沏 9 大。有一个关于开发应用程序的普遍的观点,即团队可以独立地工作,并定期核对和检验工作 成果。 在这一层次使用的最重要的工具之一就是Visual SourceSafe, 它支持版本控制和一种组件管 理 形 式 (二 进 制 文 件 )。VSS使得开发者可以锁住文件以防其他人同时修改,这种情况通常发生 在多人同时使用相同文件的时候。VSS也可以作为应用程序的数据库,如设计和规范说明文档、 用户文档、演 示 、原型等等。因为VSS具有存储 文件历 史(或进展)的能力,所以当一个项目被 交给另一个开发人员开发时,VSS将有很大的帮助。 另一 个管理 组 件 生命周 期的 好 工具是Visual Studio 6.0企 业版 附带 的Visual Component Manager。这个工具不像VSS那样提供版本控制,但是可以导出一个很友好的用于存储、打 包 、 配置组件的模型,以及任何辅助文件,如在线帮助、范例等等。图1-2显示了正在运行中的组件 管 理 器 (Component Manager )0 J1 0MTH. I X %IofTJHTH VM0 ~ 3 rl ti) cy 0 M I yew Actw« Oocawti AomxCotmIi AdMiCmgwTi onnxwnl H m m t Type I KTWl^gtTbt rcuong #d A m totr mlptot wb p#»r nctj» wartt», •uct Um ttvM _«qn.x Ufrm. oour_.Ftv Henw: adWrwten.Nn, Htm. gt, i«npiil 0f. ^nc^2 y t*or : Co»por«hon• FuMNhe^ ; S/l«19W iJ i^mroe(論:! 4m Oocimml Jiaof) CMle ( fnjt IjQ 0 ^ H || 爿 VOUfVO 一 图i - 2 可视组件管理器简化r 开发组的管理和组件的开发 改进你的应用程序很可能你现在的应用程序还没有成为每层都描述得很清楚、并能全 面适应Web要求的DNA应用程序。一个典型的公司会首先对程序进行测试,衡 量风险, 同时进行逐步的改进。 即使财富杂志的500强企业也是首先以很稳健的方式接触网络,在它们的网站上最 初只有简单的产品目录。随着时间的流逝和竞争的加剧,它们的网站开始加入一些交互 和动态的成分:一个搜索选项、一个计数器或者一个电子邮件联系页面。随着网站流量 的增加,就必须对网站进行扩展,考虑安全问题,并规划一个新的策略。 不幸的是,这是一项昂贵的工作,很多公司会开始怀疑互联网将带来的利益,毕竞10 第一部分 W/Z如 而 DNA和COM+ 在这个领域的熟练的专业技木人员并不多(尤其是对于动态网站目录和组件编程),并 且 新 的 互 联 网 框 架 (如DNA) 每天都在产生 ,而一个公司又必须选择一些工具进行人 员培训、开发、编程并且部署网站方案,这使得公司将面临很大的风险,任何形式的不 兼容都可能在用一种技术取代另一种技术(如用VBScript取代JavaScript ) 时产生 ,而 主要原因在于浏览器对HTML的解释不是标准化的。. 等着瞧的态度是不管用的,因为来自顾客的压力是真实而持久的。最终,随着越来 越多的人期待着与网站进行交互,网站必须进行改进,并引入新技术以实现这种交互。 在接纳DNA 的同时,推出成功的Web应用程序的工作变得更加易于管理,并且更加 经济。在第2章 中 ,将详细介绍如何为网站扩展进行规划,以避免返工。 1.4 DNA:功能概述 在讨论了总体的DNA 框架和它所包含的某些微软推出的工具及产品之后,你便可以开始从 个例着手对它的细节进行研究了。在下面儿小节里,将讨论图1-3所示的服务器端产品和工具。 1 .4 .1 浏览器 我们是通过浏览器来进入一个DNA应用程序的,这在图1-3中被描述为一台计算机连接互联 网协议HTTP。在DNA 中,浏览器实际上就意味着IE (Internet Explorer )0 微软已经将IE装入许 多操作平台,如M acintosh、Windows、Solaris以及SunO S。由于IE 支 持多种平 台 ,又与C O M ( A c t iv e X )集 成 ,并且在浏览器大战中取得了统治地位,因此成为理想的目标浏览器。 1 MSMQ I SQL 、丨 丨 丨 丨 丨 ^ 匕服务」 图 1-3 — 个典型的DNA 应用程序 1.4.2 IIS 另一个重点就 是微软的互联网信 息服 务器(IIS),一个决定性的 、使得DNA能够得以实现 的服务器端产品。它的主要目标之一就是高效而可靠地为HTML网页提供服务,解释HTTP协议。 11S不是惟一存在的HTTP服务器产品,竞 争 者 (如Apache和Netscape ) 在服务器产品领域已经有第/章 CCM/+: 的淼合利 11 很长时间了,并 F1.也推出了值得一提的、廉 价 (或 免 费 )的HTTP服务器产品,侣它们的目标集 中于使静态的内容尽可能地快。然而 ,与一个优化的内容服务器相比,使用n s 可以使你得到的 更多 :会话 管理 、安全以及丨SAPI。对于一个COM 开 发 人 员 来 说 ,这里面和你关系最人的就是 1SAPI及它的组件扩展家族,如ASP, 它允许服务器端脚本和动态内容的生成。 新型的网站服务器即将出现最近一些刚刚创建的公司已经引进了强健且动态的Web服 务器。最著名的是采用Silver Stream和NetDynamics这些与丨丨S要在解释HTTP上竞争一 番的对手的产品,而且它们扬言要在可靠性和性能上打败I1S, 会话管理对于COM开发人员来说也是很重要的,冈为它使得组件可以保存与服务器之间的 某个特沄的连接状态,组件本身是没有状态的,但是有关当前执行环境的信息可以以应用层IIS 变量或者会话层HS变量的形式保存c IIS可以被直接集成到组件中,或者通过某种脚本语言(如ASP) 集成。 1.4.3 ASP ASP给HTML (― 种静 态的 、不能和用户交互的语言)加人了动态元素。过去 ,HTML依赖 的足外部服务器端CGI程序或Perl脚本来实现基于HTML的输入。虽然实现了交互的梦想,但这 是以用户的等待时间为代价的,因为这样做需要很多往返操作。 这大约是1995年时的情况。 Q 从那时以后,一大批新技术改变了开发人员编写基于HTML应 用 程序 的方式 ,现 在 ,在客户端执行基于用户输入的代码已经成为可能,而无需反复查询服务 器 。这大大提高了程序的可靠性和性能,怛是却给应用程序增加了耦合问题。 ASP将复杂的任务封装起来,而这任务要做的是在只有静态封装数据存在时,创建动态的、 用户驱动的应用。ASP使得Web应用程序可以创建用户自己控制的环境,这不是一个新的概念, CGI便可以提供有限形式的动态内容。如果你使用过CGI编 程 ,应该知道它复杂而笨拙的用户界 面。而ASP虽然也有一些它自身的不足之处(如VBScript的语法及其实时调试的困难),但它却 将CGI带人了一个新的阶段;并且在用户和服务器COM 组件之间架起了一道桥梁。 ASP使得互联网应用程序更加易于控制,由于它具有无缝例示COM 组 件 的 能 力 ,事实上 , 互联网戍用程序已经很难从它们的W in32或GUI副本以功能上予以区分。看看下面的ASP脚 本 , 只有两行,但是它创建了一个COM 组 件 ,定位了一个接口,调用了一个方法,还在一个变量里 保存了结果,仅仅两行: set myobject = Server.CreateObject(“Object.ImyInterface“) result^ myobject.MyMethod(argument 1, argument2) 提 示 IIS的配置和管理超出了 COM编程的范围,应该由一本它自己的书来介绍。幸好, I1S中的安装程序做得很好,给用户设置了一个默认的工作环境。 已经证明,对于对安 全和优化问题不太苛求的数据移动来说,IIS默认设置的开发环境是有效的。如果你正12 弟一部分 DNA和COM+ 在一个产品服务器上配置系统,我建议你回顾一下IIS 的在线文档中有关安全和配置方 面的内容。 1.4.4 MTS HS下面是微软事务服务器(MTS )。在这个例子中,客户永远也看不到ASP以上的东西,所 以MTS做的是严格的幕后工作,而且做得要比它的名字包含的内容多得多。它 的主要 工 作 是 , 在组件和数据库的内部及之间自动地进行协调处理,同时也负责管理资源的缓存和共享,以及 产生无副作用的可伸缩性。 MTS在DNA框架中通常被用作组件的容器,有关MTS的特性将在本书的第四部分详细讨论, 而为了适应第四部分的内容需要,我把MTS当作组件管理器或代理程序看待。组件不需要涉及 事 务 ,也不需要安全机制来确保它从MTS获 益 ,更不必被限制在MTS环 境 中 ,它们可以分布到 多台计算机上。 瞀告不是所有的COM组件都适用MTS。与MTS兼容的组件需要满足一些约束条件, 这些约束将在第四部分中讨论。一个适用于COM+的组件则有所不同,它肯定适用于 MTSo 1.4.5 MSMQ和SQL Server 如果你的应用不得不和另一个应用异步通信,或者通过一个很慢的网络存储数据,微软消 息 队 列 (MSMQ)将对你大有帮助。MSMQ可以进行复杂的、涉及发送、接 收 、同步或异步消 息获取的协调工作。因为MSMQ会使用COM,这样组件本身便可以是消息并能自动管理。开发 人员只需要为COM的〖Persist接口提供必要的实现,并由MSMQ以可靠且容错的方式接管就可 以了 o 提示 M S M Q 到底是一种数据存储形式还是一种通信设备呢? 应该说它是一种像SQL Server—样的存储介质。我们假设的DNA 应用可能需要异步数据,而这就是MSMQ所适 用的情兄。 对于分布式应用程序来说,如果它们必须远程进行相互间的通信,或者即使在M — 台计算 机上相互通信,MSMQ也可以减轻开发人员编写底层通信细节(如WinSock、子线程 、并发控制 等 )的负担。关于MSMQ的深人讨论将在第四部分中进行。 提 示 MSMQ和它的近亲MTS—样也管理组件的资源。 1.4.6 Visual Studio 6.0 Visual Studio有哪些方面适合于DNA 呢? 图 1-3给你展示丫在DNA 应用中Visual StudioTH- 典型的用途。再说一遍,图 1-3屮描述的模甩是一个典型的例子,但绝对不是标准。VS6提供了 一大堆工具,而图1-4中仅仅介绍了其中的三个(一个很小但很重要的子集0。 VI实际上是在IIS 下创建Web应用程序的工业标准,它提 供了 一套工具(设计时间 控件 )、对$ l t COM+ : Wim^v^yDAM 的澥合剂 13 COM 的兼容性、团队开发支持以及用于给一些特性命名的WYSIWYG 编辑器。 VB 和V C + + 同 V J + +— 样 ,都 是 很 好 的 创 建 组 件 的 工 具 。其 他 的 COM 开 发 工 具 (如 Microfocus COBOL ) 同样可以创建COM 组 件 (虽然不支持COM + )。 这些工具将在下面进行详细讨论。 图卜4 Visua丨 Studio和DNA 1. Visual InterDev VI6.0是微软针对互联网应用程序推出的最新的开发工具,尤其是在涉及到ASP脚本的时候。 图1-4说明了它在DNA中的位置。有许多不错的介绍VI的书,我不打算在这里讨论它的特性和功 能 。然而 ,如果你正在使用或正打算使用VI,这里介绍一些你可以遵循的原则,使你能够更高 效地进行COM和COM+编程。 2 .避免使用数据捆绑控件 为了突出数据捆绑DTC (Design Time CoiUmls ) 在原型设计方面的性能,它的使用在前面 已经提到过了。然而 ,有一点是必须注意的,就是通常情况下,任 何 跨 界 线 (如跨越GUI和数据 库 )的 耦 合 的 引 入 都 是不好的(多层结构的设计将在第2章 中 讨 论 )。VI提 供了 一套SQL现成 DTC控件 ,用来快速开发直接访问数据库的Web应用程序。但存在某些这类快速开发所必需的合 法条件。 例 如 ,你正在开发一个小型的内联网网站,而且必须尽可能又快又好地完成任务,那么数 据捆绑控件将是一个很好的选择。如果你选用了这一控件,而又不把逻辑交给外部组件的话, 你应该知道这将带来重新编程的风险(如果你的应用程序不得不被转移到另一种不支持V16控件 的编程环境中编写,如Win32)。 瞀 告 除非你绝对肯定你的应用程序不会被改进或升级,否则一定要避免使用直接将UI ( 用户接口)和特定的数据库捆绑在一起的控件。将数据捆绑在UI上不利于扩展,而且 制约了DNA 的主要目标:复用*隹。14 第一部分 DNA 和COM+ 3 .均衡客户/服务器的脚本编写和验证过程 将服务器的工作分一些给客户机来做是会带来很多好处的。如果客户机浏览器支持脚本编 写 ,就可以利用它在动态交互环境中,进行简 单的 、装饰性的验证工怍,这将极大地减少往返 操 作 的 次 数 (以及等待时间)c 客户机进行脚本编写的另一个好处是在客户端解释和操作UI控件的能力。服务器可以向客 户端发送自定义A ctiveX组件和控件,作为独立的应用或包含在浏览器中使用。事件可以像在 Win32应用中一样被发出和被捕获,而且因为完全是在本地发牛,所以根本就没有延迟,这是对 任何地方网fc冲浪者的解脱。 . 但是均衡是重点。将所有东西都发送到客户端是不明智的,因为这将使得客户机过于独立., 而且需要- 次性下载很长时间。 而将所有的东西全都放在服务器上也不是个好主意,因 为这 将造成客 户端长时间的等待。 例 如,如果用户将整个表格都填好了,而只差一项没填,那么整个的往返操作将白费,因为眼 务器一定会拒绝接受这份表格。我们的原则是将验证用户数据的过程放到客户端(例 如 ,数据 是否是字母数字混合编制的、数据是否为空、数据格式是否正确等等),而将其他的业务处理留 给服务器来完成。 1.4.7 Visual Basic 事实上,VB 已经迅速成为快速创建COM 组件的工业标准,它将许多复杂的COM 和COM +细 节 (将在本书中讨论到)连同ActiveX控件和对象一起封装起来,这使得开发人员可以只把时M 兩在解决手头上的主要问题上,而不用担心太多的实现问题。虽然VB 可以在短时间内使一个组 件正常工作,但它依赖于许多其他的DLL,而且数虽巨大。比起VB 组件,ATL( A ctiv e Template Library ) 可能是个更好的长期解决方案。 不幸的是,VB的确有一定的局限性。虽然它可以在本地编译,但它仍然依赖于一个不可预 知的无用信息收集器,这个收集器在任何时候都可能降低程序运行的速度。它的错误处理机制 是相当 原始的,得到的是像意大利面条一样的程序代码,而且在发生错误时的流控制也很难管 理 (例如重新开始和重新开始下•一个子句)。 由于VB不支持实现的继承(只支持接口的继承),所以往往在需要面向对象的解决方案的时 候,某些面向对象的纯化论者会把VB 看作劣等语言c 尽管有这些约束,VB在DNA框架中仍然是一个非常强大的工具,而且绝对会使你用它编写 的组件得到大多数人的认同。衡量一下你的需求,是想要快速开发的组件还是要高效高速运行 的组件,这将是你决定选用什么工具(VB或是ATL ) 的主要因素。 1.4.8 Visual C++ 有了ATL的存在,再加上MFC对COM的支持,使用C++编写COM和COM+变得越来越容易。 ATL可以生成很小巧的组件,其中只包含必要的成分。虽然要做的事情比VB要 多 得 多 ,但是所 得到的组件要比缺乏经验的你自己编写的COM和COM+好得多。 在DNA内 部 ,你一定希望用VB来编写大部分组件,而将关键的任务交给ATL。ATL涉及史第!章 COM+: Windows DNA的粘合剂 15 多的调试和测试,所以最好不要创建很大的组件(包含许多方法和接口)。开发大而复杂的组件 是达不到预期目标的,因为在要进行调试、扩展或维护时,会遇到很大的麻烦。 1 . 5 小结 在 这 一 章里,我们了解了DNA这种分布式应用程序背后的、优越而理想的技术,它为高效 地开发企业级应用程序提供了理想的框架。 HTML提供了介于大多数平台和本地应用程序之间的纽带,它是开发人员创建可管理、跨平 台代码的好方法。了解了这些之后,又提到了随着脚本和组件的出现,产生的一些分歧。 COM+随后进入了我们的视野,解决了一系列的开发问题。它可以解决山于HTML和脚本的 无 状 态 性导致的许多 问题,它 也 使 得 开 发 人 员 在 编 写 要 求 做 到 位置 透 明、并且能够跨越不同 WAN链路进行通信的应用程序时,更加轻松。第2章多层组件结构 本章内容包括: • Ad-Hoc 设计 • 基础应用程序边界:外观、逻 辑 、数据服务 • 三层设计 • 保持层与层之间的均衡 • 多层设计 • 本地还是分布 • 几种不错的设汁技巧 • 设计工具 虽然COM 和COM+将是改变今后十年软件开发方式的、崭新 的 技 术 ,但 它们不 是全能 的 , 仅仅依赖它们自己是不能保证结构的稳定和可靠的。好的设计原则对于任何软件系统來说都是 关键的,不管它基于怎样的模型或框架。花足够的时间分析要解决的问题(做 什 么 ),之后再花 更多的时间设计一个好的方案(怎样 做 ),是必不可少的步骤。 提 示软 件 工 程 的 需 求 分析阶 段虽 然关键,但与COM 和COM+的编程没有任何关系,所 以不在本书中进行讨论。然而,设计則与COM 和COM+组 件密 切相关 ,值得用一章的 篇幅来介绍。如果没有在软件开发的设计阶段打下坚实的基础,那么这个软件就不可能 成功,当需要对应用程序进行修改和补充的时候,设计的缺陷将变得显而易见,因而导 致重写应用程序的可能性是很大的。 本章主要介绍COM和COM+编程所应遵循的基本设计原理。 第 1章将Windows DNA作为地图进行了讨论,而我们的任务就是正确地遵循这张地图,创建 可 靠 、可维 护、可扩展的应用程序。本章要讨论的设计原则将增加COM 和COM +方 案的价值, 而且对于Windows DNA 应用程序来说也是必不可少的。 为了确保软件设计整体的成功,并使设计适应COM 和COM +方 案 ,多层设计是有重点可依 的。本章将顺序讨论传统设计方法、三层模型设计和多层模型设计。 2.1 Ad-Hoc 设计 在你开发软件时,反复试验或没有设计方案的编程方法通常是会失败的。而令人惊讶的是, 即使设计缺陷已经被证实会导致开发周期的延长和预算的超支,软件开发人员仍然忽视开发周 期中的这个重要的环节,他们似乎永远也没有足够的资源和时间来进行这个看来空洞而难以捉 摸的阶段。 对于COM 面向组件的内在特性,在应用程序进行改进时,设计的不完善将更加的明显。虽第2章 多层组件结构 然基于组件的软件是有很多益处的,可一旦组件被集成到单一而僵硬的结构中,优势将迅速消 失 < 除非设 计 得 很 好 ,否 则 很 难 (有时则不可能)在改进和扩展组件的时候不ffi写 应 用 程 序3 除此 之外,还必须认识到,COM不是一项很容易掌握的技术,而且不应抱有这样的错误希望, 即COM是进行完美的软件开发的最佳选择。这样只会增加培训COM编程人员的投入。一•项失败 的COM方案不会比传统方案得到更多的谅解。由于COM的复杂性,它也许将永远隐藏在迷雾之 后尤法起动,冉加上我们对不了解的事物感到恐惧,所 以 ,在因设计问题导致COM方案失败的 时候,我们通常会责怪COM技 术 。 即使这项技术被IF.确地 使 用 ,组件本身还是不能解决所有已知的软件问题。如果你曾经使 用过DCOM编 程 ,你大概会同意我的观点。COM需要花时间学习,在调整和配置阶段有相当大 的工作童,而且通常不与其他的程序范例或语言关联。然 而 ,虽然付出了很多代价,伹却带来 了无形的好处:复用性、可维护性、可扩展性。 警 告 在 Windows环境下使用COM+编程是一种挑战。这一挑战不仅来自于对COM+及 其复杂的API的理解,而且来自于对辅助性的系统管理技能及相关领域知识的掌据。 2 . 2 基 础 应 用 程 序 边 界 :外 观 、逻 辑 、数据服务 基于组件的软件设计与基于其他传统方法的设计有细微的不同之处。首 先 ,组件本质上是 独立的实体,它们之间的相互通信只经由已知的公共接U , —个进程可以可靠地检测到某个组 件能做什么+ 能做什么。 如 果 组 件 被 而 紧 密 地 耦 合 在 一 起 ,它们便失去了所有本质性的优势,并且会增加维护 的复杂性。我们需要做的就是,以层来区分不同种类的组件,它们虽有相同之处但却解决不同 的问题。 划分工作已不是新概念了,而最成功的模型要数用于客户/服务器共同体的三层模型,你应 该将它作为COM+编程应遵循的主要设计方针来学习。 松散耦合与紧密耦合 我所说的系统紧密耦合,指的是它的相互依赖性。 个耦合结构将导致偃化和不灵活,但是易于设ii•和实现。耦合结构与单一结构几乎同义, 因为它们通常可以看作是单一实体,少有或没有可以相互交换的对象或组件,因此很像一块花 4-LJ k A | 冈石o 紧密耦合的应用程序很难维护,当需要修改某个单一系统时,开发组成员可能会发现他们 被逼进了死角。对系统进行修改是要承担风险的。某个模块的某个函数的某行代码的一个小小 的改变,可能会导致多米诺骨牌效应,致使另一个模块的某个函数破坏,最终导致整个系统以 一种奇怪的不可预知的方式瘫痪。 耦合可以被分为许多种形式,而最重要的几种耦合是,结 构耦合、进 程 内耦合、进程间耦 合和模块间耦合,这里是以对系统的重要性为依据排序的。 . • 结 构 耦 合 将 外 观 (或GUI) 代码与应用程序的数据及逻辑准则捆綁在一起。在应用程序的18 第一部分 DNA和COM+ 外 观 代 码 (主管通过按钮、视 窗、事件等与用户交流的代码)得知并假定存在一个特定的 数 据库,并& 使用了那个数据库所拥有的API以后,如 果有 新 的外观技 术 (如 浏 览 器 )或 数据库出现,也无法改进应用程序了,因为那将是非常困难。 • 进程内耦合使得一个进程或函数内的控制流变得混乱,这种形式的耦合,是所有耦合形式 中最难扩展的一种。它是通过在一个进程中频繁地使用GOTO和GOSUB命令来实现耦合的, 同时迅速生成几乎无法维护的、大量的代码。这样做的结果就是,当一个新的开发人员要 对进程进行修改时,他不得+ 重写整个进程。 • 进程间耦合通过全局内存共享的方式,将函数或进程梱绑在一起。全局变量和常量是软件 开发的双刃剑,它们使编程变得简单而快速,但是面对变化将 导致错误出 并 且难于 定 位 。 因为全局变量是匿名的,程 序 中 的 仟 何 “人” 都可以对它进行改动,造成混乱、无组织的 运行局面。而且在这种耦合方式下,编写安全线程代码变得更加困难。 • 模块间耦合造成了模块之间的依赖性。大多数单一的应用程序只依赖于几个通用的库或模 块 ,这将造成很大的问题,因为当任何一个库文件有所改变时,就必须重新编辑整个应用 程序。在考虑应用程序的远稈配S 时 ,这是很重要的。而对于COM+来 说 ,则无需它的客 户知道服务器有任何的改变或者要求客户进行重新编译。正如本章后面部分以及本书后面 其他章中讨论的,COM+接口应该是一成不变的。 另一方 面,松散耦合结构是一种独立的、可互操作的组件或对象的组合,结构的变化每次 只影响一个组件,前面描述的多米诺效应发生的可能性几乎是零。然 而 ,松散耦合结构使得开 发人员不得不在软件性能和幵发时间之间做出权衡,这样的结构需要很大的工作量来进行设计, 组件还会引入相当大的通信开销,并且通常难于实现。 虽然如此,松散耦合系统的优势远比开销重要,你将在本章后面了解到这一点。 2 . 3 三层设计 在客户/服务器世界里出现得最成功的模型便是三层模型,每一层就是一系列独立的、同性 质 的 对 象 (每个对象只解决一个小问题)的集合,一起解决一个较大但共同的问题。 设想一个如今典型的PC应用程序。它很可能是这样的,能与用户交互、处理某 些数据、在 某处保持它的状态一 因此得到了三层结构。 通常公认的三层是,外 观 、事务逻辑、数据服务。图2-丨诠释了这个概念,并 a 将三层结构 与单一结构作了对比,后者把事务逻辑和用户界面这样的抽象概念都捆绑在了一起。 你很可能在不同的时期以不同的称呼见到过这一模型(如把数据服务称作综合存储,把外 观称作 导航),但在任何情况下概念都是相同的。每一层内是包含公共接口的组件,这些 接r】连 接的是做实际工作的内部函数或方法。 COM和COM+把僵硬的实现边界封装在众所周知的公共接口内,层和层之间是没有区别的3 接口对于层来说是通用的,并且在粒度上要比组件的接门大,但封装仍然是根本。 除了接口以外,每一层都应该对任何相邻层的事一无所知。从进程的角度来看,这种漠不 关心似乎是一种约束,但这确实是-•种解放机制。 这里蕴藏着这种模型的力量:一层中的改变几乎不会影响到其他层,这使得这种结构可以第2章 多层组件结构 19 得到轻松的扩展和自由的升级。 数据服务 当前 表 示 > ▲ 逻辑 T 公 共 接 n 数据 业 务 逻 辑 I 表示 丨 公 共 接 n 处理 数 据 服 务 | 三层的单块的 图2 - 1 单一结构与三层设计 在一个设计得很好的三层结构中,层与层之间的通信只通过公共接口。当层与层之间是松 散耦合时,可 以简 单地替 换组 件 (或整个 一 层 ),以适应变化了的需求,而无需重写整个应用或 对系统进行重新测试。出于这个原因,每一层对临近层的实现应该完全不知且不负责任,应该 只能看到临近层的公共接口。在图2-1中 ,层与层之间的箭头就代表这些接口。 这种髙效通用的模型已经出现了许多年,但令人惊讶的是,在客户/ 服务器世界之外这种模 型却很罕见。随着COM+ ( 接 口 驱 动 )和DNA的 出现 ,三层模型将显示它巨大的用途。COM+ 使开发人员确信,创建跨越层边界、实现透明通信的组件是件有把握的事。 下面是图2-1所示的三层模型中每一层的介绍。 • 外观层—— 外观层涉及与用户的所有交互。特定的GUI操作 ,如重绘一个视窗、捕捉鼠标 的抬起、文本的输入等,都位于这一层。三层模型不允许用户与其他层交5 。 这一层只做很少的数据验证工作,但这样可以更好地与它下面的事务层匹配。外观层不包 含任何数据存储技术,也不知道数据是从何而来的。这一层有〜组接口,使它可以和事务层通 信 ,而这是它惟一能做的事。 • 业务逻辑层—— 下一层就是中间的业务逻辑层,大多数的过程在这里实现。所有业务特定 的准则都被放在这一层。它是应用程序实际解决问题的一层,是介于用户和任何物理数据 存储之间的中间层。和其他层一样,这一层也不应该知道其他相邻层(下面的数据服务层 和上面的外观层)的细节。它只处理数据,不存储也不提交。 • 数据服务层—— 数据服务负责应用程序所需的任何物理存储。特定的数据服务机制,如低 级数据库访问或SQL,应该放在这一层。当需要对物理存储介质进行扩展或改变时,只有 这一层会受到影响,其他层不会受到破坏,也不需要重新测试。典型的客户/服务器三层模 型涉及到结构化数据库存储,但在现在的三层设计中,不存在这种情况。 一个应用程序不需要数据库也可以从三层模型获益。在 这 一 层 ,任 何 形 式 的 存 储 (如文件 系 统 、电子邮件、多媒体数据流等等)都可以使用,远离外观和逻辑层,增加系统的可维护性,20 第一部分 Windows DNA^COM^ 以利于将来系统的发展。 2 . 4 保持层与层之间的均衡 如图2-2所 示 ,一个均衡良好的三层结构立于一个精密的支点上,左边是所有的外观代码, 中间是f 务逻辑层,右边是数据服务。 虽然可能事务逻辑层可以承担系统的大部分“重景”, 但是对于将来的系统发展来说,任何一层负担过重都将带 来灾难性后果。任何一端过重都会使均衡倾斜,并导致系 统崩溃。 在外观层一端,这可能意味着增加了数据联络或数据 约束控件,用以与数据层直接通S ( 参见图2-3)。 图2_2 —个均衡良好的应用程序 反过 来 ,过分依靠数据库特定的逻辑机制(如将进程或触发器控制放在这一层),将得到相 反的结果,使天平向数据服务端倾斜(参见图2-4)。 图2-3在GUI上存在太多的响应 图2-4在服务器上存在太多的响应 DB 图2 - 5 稳定强健的设计 在图2-5中 ,是一个建立在固定的逻辑层基础之上的 稳定的三层设计,消除了支点和扰乱系统的一切机会。方 形代表稳定的基础,它不会被将来任何层的扩展或修改所 动摇。 稳固的基础来自于划清外观与数据逻辑的界限,只要 应用程序要解决的问题不在意数据从何而来,或如何提交, 应用程序仍然会保持强健性和可扩展性。 三层结构不仅仅适用于客户/服务器环境,事实上,所有应用程序都可以被分为至少三层。(然 而也有例外,如果驱动程序和其他的低级硬件操作程序采用三层设计,将不能达到预期的目的。) 无论你是在开发一个简单的W in32应 用 程 序 ,或是一个成熟的、支持网络的电子商务系统, 这些原则都同样适用。无论问题怎样复杂,软件总能够以某种形式处理数据(不然将无事可做), 并且可以以不同行为作为准则,将软件划分为各具特色的几层。 2 . 5 多层设计 随着应用程序的扩展,它会变得更加复杂,有时不得不将某一层分解为两层或更多层,这第2章 多层组件结构 21 导致了多层结构的出现。虽然与用户之间的交互可能相同 但有时很容易将三个基本层分为更多层。 对于由几十个人共同幵发的大甩应用程序来说, 多层结构比 三层 结构更容 易操作 ,因为工作可以被 分成更小块分别做。COM+为多层开发提供了援助, 因为开发者使用它便可以创建出实现跨边界的透明 通信的组件。 如图2-6所 示 ,这是一张我用于一个复杂网站工 程 设 计 的实际 结 构 图 ,在三层模型中的外观层被分 为了两个独立的层:Win32和HTML。 在看图2-6的时候,你会问自己,“ 为什么不像以 前 一样只保留一 层呢 ?可以用两个组件来分别处理 HTML和W in 3 2 ,并增加必要的接口和方法,为什么 不这样做呢? ” 答 案是 ,Win32很 复杂,它将需要一 大堆相同性质的Win32组 件 ,而且还要与HTML的一 堆组件共存,ffriWin32本质上是和HTML有所区别的 ( 一个是线性的、事件驱动的,而另一个则是非线性、 静 态 的 ),因此这么做不太合适。 「 你 可 能 还 记 得 ,我 们是 这 样 定义层 的 :它是独 夂— 立 的 、具有相同性质的 对象 的 集合,一起解决一个 共同 的 目 标 。在团队 工 作 环 境 下 ,通过分解各层并 图& 一个实际的八层结构 减少每层的组件数,可以极大地降低开发和测试应用程序的复杂性。毕竟,独立地开发和维护 五个组件要比二十个容易得多。 一层的组件越多,这一层就越接近于单一结构模型(有太多 的耦合),这就抹煞了多层结构 的意义。在选择了HTML和Win32作为外观的情况下,将它们分到两层是合理的选择。 注意不要把图2-6与流控制图相混淆,它只给出了应用程序所要解决的几个人为抽象 的主要问题。从这个图中,你无法推断或设想出通信接口,以及教据是如何在应用程序 中 传 递的 (单向、双向、并发、异步等 等)。 瞀告每一层中所包含的组件个数一定要有一个限制。而如果你把每个组件都作为一层 来处理,你将回到 开始阶 段(都是组件,没有按性质组织起来)。在编写应用程序的时 候 ,可以使用层来使你获益,但一定不要把层次分得太细。 从上至下 ,外观层被分为四层,而它们通常应该属于业务层。为了总体的功能目标将它们 分 开 ,使得理解应用程序在做什么变得容易:验 证数 据 、分 解数 据 、保 护 数 据 (加 密 、设置权 限 等 等 > 以及处理数据。每一个动作都被放到单独的一层,但这么做会使层数增加并使之难于 管理—— 违反了模型的本意。 图2-7给出了数据验证/加工层的洋细内容,可以看出它包含了许多不同的、但功能相似的组 ( 提 交数 据 、收集 数 据 、存 储 数 据 ),22 第一部分 DNA和COM+ 件 ,每一个组件都在应用程序数据验证范围内,解决不同的问题。虽然数据验证可以被看作业 务层的功能,何验证组件与加工数据却没有什么关系。 下一部分将讨论在部署配置中,各层应该放在什么地方。 2 . 6 本地或分布 不要被多层结构中不相连的层之间表面上的分布所愚弄。层的编程并不怠味若分布式关系 或数据库的存在。它只是一个任何程序设计都可以遵循的框架,而有些应用程序可能不需要外 观层 ,例 如 ,它们也许只是用来监听无需交互的请求的应用。M样,数据服务层也可以不存在, 如果应用程序不涉及任何存储功能(如在Windows附件中通常可以找到的计算器)。 2 . 7 几种不错的设计技巧 幸运 的 是 ,合理的设汁原则是很容易应用于基于组件的软件设计中的。我们不会更多地去 研 究涉及面向对象的方法,而 会把注 意力集中在可靠的、用于设计多层COM+结 构 的 技 术 上 。 按重要性排序,这些技术分别为: 1)将应用程序抽象为不同功能的各层。 2 )确定组件执行的是很细小的行为。 3)创建各层之间的接口。 4 )实现组件的接口及其方法。 数据验证7幣形 图2 - 7 数据验证层$ 2 t 多层组件结构 23 从这些原则可以很清楚地看到,我们遵循的是归纳法,从总体出发,向细节深入。 2 . 7 .1 将应用抽象为各层 所有能用的软件都是为了解决一个或多个问题而编写的。所冇问题通常都可以被分为一些 子问题。使 用 组 件 ,关键就是要把大而复杂的问题分解为小而易管理的问题。通过使用这种分 而治之的方法,我们可以确保整个问题的解决,并能创建一个强健而易于维护的结构。这样我 们可以更容易地修改和扩展一个包含子问题的单一的组件,而不是用通常的重新测试和调试的 方法面对整个问题。 2 . 7 .2 确定组件 这个阶段有一点儿挑战性,需 要 远 见 (和事后说明)。这可能是所有阶段屮最有趣和最具创 造性的一个阶段,因为这里问时需要过去的经验、现在的技术和分析问题解决问题的能力。 再 说 一次,虽然不是所有的应用程序都适合三层模型,但 在某 种程度上 ,它们总是可以被 合理地抽象为这种模型。 我们从外观层开始。应用程序是通过图形界面与用户交互的吗?它是肓目满足请求的服务 吗?创建直观友好的GUI不是一件小事。我们时常会见到被矛盾困扰的GUI,或者忙得很的GUU 当你使用浏览器时,缺乏丰富的控件和状态只会使问题复杂化。 我们不会去讨论GUI的设计问题,但值得注意的是,不应该小看它^ 设 计 。一定要记住,虽 然外观层只是冰山的一角,但它是用户和应用程序之间惟一的联系纽带。小小的外观层可能只 是坐在巨大而复杂的业务层的肩膀上,怛如果不能提供给用户一个友好的工作环境,用户可能 会毫不犹豫地弃用整个应用程序。 在决定了使用可视的或不可视的技术(Win32、浏 览 器、控 制 台 等)之 后 ,就吋以将外观层 分解为不同功能的几部分,一部分负责处理菜单选项,另一部分负责标签,还一部分负责工具 提示等等。或者在浏览器中,一个组件可以被用来跟踪从一页变到另一页的上下文菜单,而其 他的组件可以负责跟踪组合框和文本框。将外观层分解得越细,就越易于维护和扩展。 ' 从经验来说,外观层是要被改变最多的一层。要想满足所有用户对于交可.的要求是不太可 能 的 ,但是一个满足大多数人需要的折衷方案是可以找到的。 其他两层应该遵循相同的原则,业务层负责验证和处理,这足两个组件。验证组件负责在 数据被提交处理之前,将数据进行过滤。这样处理组件就不必担心会处理到坏的数据,因为数 据已经被验证过了。这就消除了同时进行两个操作所带来的负担,而且同时操作会导致错误的 发生。COM+在这里是有帮助的,它可以参与事务过程—— 这对本结构来说是很需要的。 如果你的应用程序需要对数据进行访问,第 三 层 (但 绝不 是 最后一 层 )可以负责所有的数 据访问任务。如果与应用程序通信的是一个典型的相关数据库,可以使用两个组件:查询组件 和更新组件。例 如 ,它们各自都有自己的一套接口,用以执行用户特定的SQL命 令 。一定要尽 可能避免使用存储过程。虽然有时可以提升一点儿性能,但它们将底层的物理数据库与应用程 序的其余部分耦合在一起,这样做是不合适的。如果用组件代替存储过程,那么处理过程就是 通用的,可以在将来应用于任何数据库技术。24 第一部分 W h办 似 DNA和COM + 存储过程可能会让你付出昂贵的代价: 真实故事:我曾经为一个客户编程,他要求将他的Sybase应用程序移植到SQL上 我们觉得PowerBuilder应 用 程 序 可 能太大 (约四十万行代码)并且会耗费很多人力和时 间。 于 是我 们使 用 了 一 种混 合层 (2.5层 )结构 ,但是有太多Sybase特定的存储过程 ( 与SQL Server不兼容),而在这些存储过程中有70%是有关血务规则的。 在进行了细致的分析并花掉几千美元的咨询费之后,这一移植被认为是不可行的。 500万美元的应用程序就这样被放弃了,代替它的将是一个新的应用程序。 故事的意思是:存储过程可能会让你付出高昂的代价。 2 . 7 . 3 创建接口 接口处于COM 的核心部分,它们非常抽象,使你可以将通知行为与内部实现分开。接口应 该只描述对象所提供的公共服务,而对象的内部状态不应该能通过公共接口看到。 COM+的接口是组件及其客户程序之间的捆绑合同,在运行时,位于核心的接口其实就是一 些语义相似的方法或函数的集合,可以通过一个vtable指针访问。 接 U 本身没有任何功能,它仅仅是指向实现。接 n 可以跨组件复用和升级,可以增加新的 方 法 ,但旧的部分应该一直保留。 在设计接口时,一 定 要记住,一个接口所包含的所有方法应该在某方面具有共同性。一个 接口可以支持的方法数量是没有限制的,但最 好保 持在10个 以 下 ,这 使 接I 丨不会变得难于管理 和单一。 2 . 7 . 4 实现组件 最 后 ,在你通过划分层和接U 创建了一个概念模型之后,该是具体实现的时候了。依据应 用程序的复杂程度,这将是进行得最快的一部分。有了接口作为蓝图,并且有了合理的设计方 案给予的自信,应用程序的需求就可以变成代码的实现了。 2 . 7 .5 设计约束 在设计应用程序的时候,不管是多层结构还是单一结构,多个需求将使最终的产 品 具有- 定 的约束 性,有时需求之间会产生冲突或需要折衷,如软件大小和速度之间的冲突,以前曾是 一个软件开发者们需要面对的主要的折衷问题。随着内存价格的持续走低,这个问题已经不像 以前那么重要了,但是仍然有许多的折衷问题出现。下面就逐一讨论。 在开发如今的多层结构时,有许多真正的可量测的强制作用影响了结构的整体形状,下面 就是其中最重要的几个: • 合理运用现存的技术和技能。 • 上市时间。 •平 台 配 置 。 1. 合理运用现存的技术和技能第2章 多层组件结构 25 千万不要假设新T.程将使用所有的最新高科技工具、编程语言和服务器端产品. 并可以将 已冇的技术投入抛在一边,因为这样是不切实际的。事实上,大景的资源已经被投入到软件的 开发 中:硬 件 、软 件 、培训以及飞速上涨的IS薪 水 ,这一切加在一起对任何公司来说都是一笔 巨大的花费 幸运 的 是 ,使用COM+编程不需要IS的检查,而且使开发廉价的基于COM+的应用程序成力 可 能 ,而无需将微软以前推出的工具扔掉。例 如 ,对于某些公司来说,如宋它们已经有了另一 种数据库,也许根本就不需要像SQL Server 7.0这样昂贵的数据库产品。这个道理同样适用于操 作 系统 ,虽然运行Windows NT workstations是最理想的选择,但C0M + 也可以运行在Windows95 或 Windows98 下。 现存的以前编写的代码也不是问题。在微软的通向大型机世界的桥梁S N A S e r v e r 中 ,装有 COM 事 务 处 理 集 成 器(COMTI)。COMTI是一种创建用于透明的访问大型机资源的COM 组件的 工 具,第 19章将详细讨论C O M T Ic 由于UNIX 的互操作性问题,有 些 厂 商 (如C h iliS o f t )已经宣布幵始使用UNIX 的COM 库和 VMS操作系统。在写这本书的时候,还不清楚它们是否要转向COM +。 2. 上市时间 软件工程通常是会被延迟和超支的,有许多原因会造成这样的结果,何是实现和集成问题 一直被看作是罪魁祸首。 虽然C++是编写COM 的理想选择,但它不总是最好的。如果应用程序需要保证实时性或者 有严格的性能要求,一种中问层次的语言(如C /C + + )将是惟一的选择。然 而 ,现在的大多数应 用程序对实时性要求不是很严格,并且可以通过使用RAD 工具得到很大的好处,正如第1章中所 讨论的,有许多杰出的RAD工具存在;最流行的几个是,V B 、Vr和VJ++。 由于这些工具都支持C O M + ,所以它们很适合于创建原型并将原型变为实际软件。而RA D 和C++的组合将是最好的选择,因为开发者既可以依靠RAD 的简便,又可以依靠C+ +的速度和 效 率 ,快速地创建一个UI (用 户 界 面 )。当 然 ,所有这些都要为C 0 M + 的可复用件和可维护性 服务。 3 .平台配置 即使PC在不断地流行开来,很多公司还是不准备放弃原有系统。它们现在的系统可以正确 地运转而无需修理,为什么要在现有系统运转良好的时候,投那么多钱太购买新设备和培训人 员呢? 这种想法是合乎逻辑的。开发软件一直是需要很大投入的方面,而生意人则想在用户可以 容忍的情况下,尽讀减少投入。 . 随着电子商务的出现,软件工业从最新的工具和程序范例中, 目击了高新科技开发的加速 度。无需惊讶的是,许多管理人员被众多的选择所淹没,不得不保留他们已有的陈旧系统。他 们以为再等上6个月就能等来降价和性能更好的工具。 幸运 的 是 ,有一种平台实际上已经很通用,并且可以适应将来的硬件和软件扩展,它就是 Web浏览器。 U前几乎所有的操作系统(甚 至 电 话 )都已经有了自己的HTML解释器。准备为互联网祝酒26 第一部分 DNA和CCW+ 的现实看起来已不再是可笑的事了,以HTML作 为 “平台” 将是很有意义的„ 把HTML作为你的 目标平台吧。 2 . 7 .6 设计目标 在今天这个分布计算和互联网应用的世界里,内存是很便宜的,所以我们注重的是应用程 序的可靠性而不是大小。但 对如今的用户来说,仅有可靠性是不够的,对于开发人员来说必须 注意一些关键性的问题,下面列出了最重要的几个: • 可维护性。 • 可靠性。 • 实用性。 • 可扩展性。 • 可移植性。 • 安全性。 • 可复用性。 • 局 域 性 (分布或本地,与性能紧密相关)。 1. 可维护性 如果一个软件系统长时间地不升级不改进,那它怎么会有用呢? 如果说我们从千年虫问题 上得到了一个教训,那就是计算机编程人员应该更有判断力地设想未来。设计和实现中的远见 和灵活性会在未来得到很大的回报,而一个目光短浅的方案必然会在新的需求产生时,被多次 重新编写。 我 们的 目 标 是,在解决手头上 特 定 问题的 时候,努力在更普遍的层次上使问题得以解决, 而不是个别问题个别解决。我们时常会回头看看以前编写的程序,总希望过去的我们能再多努 力一下,消除一些大的进程或函数之间的耦合。不得不花费宝贵的时间对原有的程序进行部分 或全部的重写,不是一件开玩笑的事。 有了COM,很容易通过接口继承来解决上述问题。创建通用的接口,然后实现特定的行为 来解决你的问题。从IPersist类型的一组接口中,可以看到这一技术的典型例子,这将在第4章中 讨论。 2 .可靠性 对PC惟一 的 、最常见的抱怨可能就是它的不稳定性。无论这是操作系统的错还是应用软件 的错,但得到一大堆麻烦并损失宝贵时间的却是用户。 PC对UNIX的可靠性框架战争已经在Usenet上进行了很长时间r , 而i i 看来一段时间之内还 结束不了。UNIX对PC的稳定性问题争执不会很快消除,但这对时间要求严格的系统来说则是生 死相关的事。想想内置软件的医疗设备,或是飞机上的导航软件,如果病人或乘客有权选择他 们信任哪一种操作系统,最普遍的答案是什么呢? (我肯定会选UMX。) 我在两种平台上已经工作了近18个 年 头 ,经历过UNIX的 “寒流” ,但更感受到它的命令提 示 符 的 “温暖”,还有五彩斑斓的PC窗口内脆弱的诱惑。 但 是 “COM”,正 如 Don Box曾 经 说 过 的 , “ 是 一 种 爱 ” 。 COM拥 抱 所 存 的 陌 生 人第2章 多层组件结构 27 (IUnknown),并且不区别对待任何平台(只要它们支持COM )。 所以我们不讨论哪一个操作系统更可靠。坦白地说,作为COM+的开发人员,我们可以对可 靠性进行控制。我们通常面对的情况是,平台的局限和没有选择在什么操作系统下工作的权利。 然而 ,我们可 以保证,我们的代码绝对符合微软关于性能良好的组件的方针,并且不仅仅 可以移植到UNIX系 统 ,还可以移植到VMS等COM+支持的系统。这些方针超越了你努力学得的 和实在的学识,如 “不使用全局变M” 或 “避免使用GOTO语句” 等 ,它们有更现实的含义,如 并 发 、线程模型或交互通信等。 创建可靠组件的技巧将在本书的后面部分进行讨论。 3 .实用性 在21世 纪 ,消费者都将是受过教會而小心谨慎的。“系统当机” 的提示再也不会出现在银行 出纳员的岗位上。如今的消费者要求的是支持网络的立即执行,如果一个网站的网页不能在几 秒钟内显示出来,几下不耐烦的单击就会把他带到能够做到这一点的网站。 在一个非分布式的PC环 境 下 ,实用性是很难达成的。然而也不必去创建实现高实用性的容 错系 统。Windows DNA提供的服务器端产品,可以在最苛刻的环境下给用户提供足够的时间。 第26、27章将深入探讨这个问题。 4 .可扩展性 为了解决一时的问题而编写出的应用程序,很可能忽视了扩展的问题。一个设计的很好的 应用程序可以被扩展或适应改变了的环境,甚至某些开发者没有想象到的情况也可以适应。 作为COM+的 编 程人员,我们能拥有MTS是 很幸运 的,它可以称作是扩展机器。现在有了 C O M + ,我们更不担心MTS了。我们将一个正常的、性能良好 的 组 件放在 这 台“机器” 的一端, 然后看着它在另一端变成可扩展而且强健的组件。 但是否真的那么简单呢? MTS和COM+确实给我们带来了许多的可扩展性,但 这 +能 成 为 编写滥用资源的组件的借口。 在使用资源时,要试图尽可能地节省,绝不应该在不必要的情况下长时间地霸占资源。 5 .可移植性 可移植性可分为两个层次,一个在实现的层次,而另一个在用户或最终产品的层次。在有 浏览器和HTML之 前 ,可移植的程序是用C或C++编 写 的 ,然后根据各自需要的平台进行编译。 这种技术不总是管用,因为C没有为丰富的用户界面提供标准的API。为了解决这个问题,外观 代码和事务逻辑代码被一起编写出來,耦合整个 应用 ,从而导致了单一性。移植在一个项目中 是很重要的,并且需要相当多的资源来实现。 如今已经有了浏览器语言,如HTML和新推出的XML。 只需要为一个平台编写应用程序, 然后依赖外观层,针对普遍的兼容性生成兼容的HTML。 然而,对于浏览器也有一个不利条件,它们缺乏丰富的UI控件。像Java Applet、动态HTML 和ActiveX这样的技术可以缓和问题,但是这将在浏览器之间引入不兼容性。 为了真正实现对所有浏览器的兼容,我们必须尽可能只使用HTML及其简单原始的性能。实 际 上 ,这并不是ActiveX和Java组件推销者试图让你相信的一种牺牲。如果对你的应用程序来说, 按钮和文本框还不够多,那你可以使用图像映射來创建复杂的GUI。28 第一部分 VV//1 办m DNA 和COM+ 再说一次,如果你还没有这样做,建议你使用HTML浏览器作为你的外观层。 6. 可复用性 软件乌托邦的最为广泛接受的定义楚,可以用已经做好的标准组件集成出想要的软件的地 方 ,这很像电路板或交通工具的生产方法。虽然我们离这个目标还很远,但我们却可以今天创 建COM+组 件 ,而在几个月之后再重新使用它们。 编写得很好的COM+对象的版本特性,可以保证一个已有的接口在这个对象的生命周期内, 始终起作用。所以即使一些新的特性被加到一个旧的组件中,它过i •和现在的客户程序都不会 受到影响,W 为这些客户程序只使用它们需要的东西。 COM+的规格说明中谈到了永恒不变的接口,怛在现实中,这变得更像是一个建议。COM+ “认为”,一旦组件的接口被完成,它应该永远不变。 但 是完 全可 以 改变 接口(或者彻底去掉它),破坏规矩。这将造成COM+对象与其客户程序 之间联系的中断,无知的客户程序将因为非法内存地址而“天真地死去” ,而不去检查是否有一 个接口查询获得成功。 COM+不会捕捉这种联系上的缺陷,也不会警告开发人员存在违规行为。这完全取决于开发 者本身。 VS中 的 某 些 工具 (如VB或VI) 确实会在删除接门时给出繁告信息,但是要维持这种联系, 仍然取决于开发者. 没有了这种联系,组件的寿命将是有限的,而且它的可复用性将受到威胁。 在一个接口被完成以后,一定要避免对它进行改变,并且要在和其他组件一起工作时,随 时进行严密的检测(通过Querylnterface )。 7. 局域性 应用程序有多大?是不是需要跨计算机操作以迎合性能的需求? 一个地址空间是否足够7 有了COM的位S 透明特性,就不用去预见设计时的分布式结构在安装的时候是不是能用的 问题了。 有了MTS的帮 助,组件可以很容易地实现在机器之间的分布,而无论是客户机还是服务器 都不会感到有任何区別。只有注册表知道组件的真实位置,以及是否使用了代AU 只要客户机 需 要 ,服务器上的组件总是在相同的地址空间。 2 . 8 设计工具 现在存在许多的计算机辅助软件工程工具,帮助软件设计人员将人的思想融入连贯的软件 设计当中。Rational Rose是最流行的建模工具之一,它作为Visual Modeler附带在VS中。虽然一 种 杰 出 的 统 建 模 语 言 (UML)是 首选 ,但VM有写得很好的在线文忾和指南来帮助指导UML 新手 D 不必惊讶的是,VM中建立新项目使用的默认模板是三层模型 < 参见图2-8)。喜欢面向对象 的 人 ,在使用VM的时候会有宾至如归的感觉,而且它丰富的特性会征服非面向对象的程序员。 幸运的是,COM+不需要面向对象的技巧,因为它本身不是面向对象的(至少在二进制上)。 COM毕竟是有关二进制互用性的技术。第2 章 多层组件结构 29 E»t »- «• Ovo0> 1« gM Bl 1S1£| - u!j lo^cSVmm Ua*r S*Mcet B«8iae«s Seivtees 1 Data Service* r: O Butmm &«v«cm X P«»*B*CK«v Cj X- O OapememVtm 3 Owkr^i^m* 111--------- 1 J ..................... il cproKh «apat,可以被用于为给定的任何COM+类的对象结构分配字符串参数。IObjectConstructor的 接口可以被新的对象用于获得由COM+管理员分配的结构字符串。这将使管理员能够为一组与 对象相关的客户定义一个消息队列路径。 目前,COM+l.O只允许类在同一时刻与一个应用程序 相 关 ,这使得程序管理员为每个类只能分配一个结构字符串。这将在第5章中讨论。 » 一 • 4 S ) ■ > ---------------- f 9 &CC»W^:CMUIW # A CCiH IMBM 龜令 ,,A ^ CjOMMuMrr«rMC2Via ft 务 SflrmaOtmO ■!l . ......1 *1 com ApvuBM fiMacf} ,,,, 令 C0M*9C« COH^QC CCtU \Smrn ^okvC#.. UtMm • Wyfmm 图3 - 1 组件服务管理器襄J 章 COM+结构与管理 35 3 . 3 .3 标记 标 (己其实是一种对象,它们可以通过一个简单的字符牢名字重绀某些类型的对象(参见第5 章 )。C O M + 提 供 的 “新 标记” 使你能够由 一 个简 单的字符串名字构造出 一些新 对 象 ,例 如 , “newtOrderEmry.Order.l”。当然,标记是基于接□ 的COM 对 象 ,任何人都可以创建,这比起严 格的API调 用 (如CoCreatelnstanceO)要灵活得多c 例 如,使用标C ,调用者可以传递初始化参 数用亍对象的创建,这对于标准COM 对象的创建函数来说,是不能实现的e 3 . 3 . 4 中立线程单元 COM+引入了一种新的线程模型,中立线程单元,这种新的单元模59是为了适应基于事务的 应 用 程 序 服 务 器 (如MTS和COM+) 的特定性能需求而产生的,中立单元也是首选的COM+组件 线程模型。 回忆一下COM线程 模 型 ,是为了控制COM组件内部的并发而设计的。一个单元就是一些对 象和定义的集合,跨单元的调用通常会涉及大量的调度服务,通过这种方式,COM可以控制羊 兀内的并发。向首选的MTS线程 模 型 是单 线程单元(STA),STA模型是指将由单一线程创建的 对象集合到这个线程的单元中,任何对STA对象的外部调用,都必须被调用线程放入队列,然后 由目标STA线程进行分配。这 -系统的优势在于,你可以在一个多线程应用程序环境中快速地创 建STA组 件 ,而无需考虑同步问题的复杂性。劣势是COM串行化了所有对本单元的方法调用, 而不管是否需要这样进行串行化。为了进一步扩大性能的瓶颈,所有的外部调用都必须被敗人 队 列 ,这很容易大幅增加系统的开销。COM线程将在第7章中详细讨论。 那么怎么解决这些问题呢?现在的COM 支持每个进程有多线程单元(M T A ), M T A可以配 备自由线程 队 列 程 序 (FTM),允许进程内的任何线程(甚至来自于STA的 线 程 )直接调用MTA 对 象 方 法 ,而 无 需被放 入队 列 。听 起 来 好 像 可 以 了 ,是 吗 ?其 实 不 然 。MTA 基本上扼杀了 MTS/COM提供的并发控制功能,而且重新引人了涉及在多层应用程序中管理同步和线程的一切 复杂性和系统开销,并导致应用程序特定的线程管理与MTS/COM式的线程管理之间的竞争。 一 般而言 , MTA组件不应该在MTS或COM4■中实现。在COM +中 ,需要一种新的线程解决方案来 提升性能。 中立线程单元正是这样的方案。中立线程组件总是在中立.单元中创 建的 ,中立单元包含对 象但不包含线程。另外,中立单元与MTA的相似之处是,允许许多线程对对象进行并发的执行。 优势是COM+仍然负责线程管理和并发任务,这保证了没有一个对象在同一时间收到多于一个 的调用。 3 . 3 .5 对象池 COM+是第一个真正实现MTS所提供的对象池系统的应用程序服务器。遵循MTS基本方针 的组件可以在COM+中被汇聚成池。系统管理员可以使用组件服务管理器为对象池配置可用的 组 件 。对象池使对象一旦被创建出来,便可以被不同的客户连续地复用。对象池消除了每次事 务都要构造和析构对象的系统开销。对象必须通过使用IObjectControl的接口,仔细地重新初始36 第二部分高级COM编程技巧 化它们所需的任何状态,而& 也必须通过IObjectControl报告所需的支持程序。 在MTS中支 持对象池的对象,如果没有被很 好地编写,可能会在COM+环 境 下丧失作用 。 MTS从不真正地将对象聚集在对象池中,但看起来好像是它做的。支持对象池的对象接收所有 合适的lObjectComml调用 ,但MTS在事务完成时会删除这些对象,而不是将它们放回池中。这 意味着还没有露面的初始化bug将在调用构造和析构函数时,在COM+中出现,在那里对象不是 真的被删除,而是被放冋池中。还需要注意的是,池中的对象不能是单线程或单元线程的。STA 中创建的对象,会一直呆在同一个STA中 ,并总是被与STA相关的线程调用,这将给你的应用程 序的性能带来严重的影响。COM+使你可以设置许多的对象池参数,如图3-2所示。 JJ 2S1 r D)«por«n( supports •vortt and aUb^ct 图 3-2 设 置 对 象 池 参 数 3 . 3 .6 对象池管理 对象池也有管理方面的问题。系统管理员若想为一个指定的类设* 对 象池,他必须首先确 认这个类支持对象池。之后,使 用 组 件 服 务管理 器 (功能与MTS资源管理 器相同 )使这个类的 对象池发挥作用。组件服务管理器还可以设置最大和最小的池的参数限制,例 如 ,当池中没有 对象可用时,对客户超时时间的设定。聪明的系统管理员能够细心地设置池参数,以提高整个 服务器的性能。 3 . 3 .7 动态负荷均衡 MTS使系统管珲员可以配置客户应用程序,在一组服务器中选择一个特定的服务器,这叫 做静态负荷均衡。它是一种均衡方案,因为它使系统管理员可以将执行客户请求的负担分散到弟3 章 COA/+结构与管理 37 不同的服务器,而说它是静态的,是因为它不适合普遍情况。如 果 每 个 服 务 器 1:.放十个 客 户 , 当有一个服务器上的十个客户都需要服务的同时,另一个服务器上的十个客户很可能都不需要 服务,这使得一个服务器几乎过载,而另一个服务器却在闲置。你的选择只有让客户轮流休息 或采用动态负荷均衡方案。微软的工具还没有强大到可以处理这个问题,所以它选择了COM+ 中的动态负荷均衡方案。 负荷均衡是一种宽松的服务器聚类,它允许应用程序的特定层被分散在几个系统中。COM+ 使系统管理员可以以一种特殊的服务器聚类方式配置服务器应用程序。在动态负荷均衡方案中, 所有系统的服务器负载信息都被上报,客户的对象创建请求便可以发往负荷最轻的系统。 这种跟踪服务器负载信息并改变客户请求方向的COM+服 务 ,被称作负荷均衡路由选择程序 ( 路 由 器 )。COM+客户被设罝为从路由器请求服务,然后在整个应用程序组中负荷最轻的系统 中 使 用 对 象 。 正 如 你 看 到 的 ,这 纯 粹 是 一 项 管 理功能,不需要开发人员的介 入 。任何好的 COM+组件在被以动态负荷均衡方案配置以后,都能够运转良好。 动态负荷均衡方案在提高性能和服务器利用率上是有很大优势的。负荷均衡路由器不会将 客户引向服务器组中已经失效的服务器,这为系统平台提供了容错功能。而在这种情况下路由 器本身成了惟一致命的弱点。幸运的是,负荷均衡路由器可以在Windows 2000的fail-over群集器 中进行配置,这为动态负荷均衡方案带来了高性能,并且消除了工作在独立系统中的负荷均衡 路由器的惟一缺点。 3.4 C0M + 配置服务 Windows 2000中的COM+解决了大型企业敁关心的问题之一,那就是应用程序的配置。在 MTS的世界里,MTS资源管理器和MTS包极大地简化了分布式应用程序的设置问题,而且组件 服务管理器以相似的方式支持COM+的设置。为了充分利用COM+的配置服务,组件应该完全是 自我描述的,这就是说组件应该提供一个完整的类型库,设筲好的属件和其他的组件中心信息, 被存放在定义了所有组件安装和注册要求的组件库中。不幸的 是 ,在写这本书的时候,还没有 一种可以创建组件库的工具。仍然需要DllRegisterServerO来支持非COM+环 境 。为了充分支持 使 用 微 软安装 器 (MSI)进行动态设H 和 配 置 ,COM+组件被禁止依赖不是绀件自己建立的注册 表数据 ,并应该在卸载时彻底清除所有设置状态。 COM+使系统管理员可以输出客户和服务器应用程序的结果。COM+服务器应用程序除了 是被调用的应用程序并以.APL为扩展名作为应用库输出这两点外,其他的地方很像MTS包。客 户端的输出形成一个MSI应 用 。客户安装器可以被分布在活动目录内,这 部分 将 在第16章详细 讨 论。 3.5 COM+ 的资源管理 开发髙度可升级应用程序的真正窍门是高效的资源管理。COM+支持许多新机制,以使开发 人员可以改进他们对于资源分配和保存的策略,如对象池和用参数表示的对象结构。事务中资 源的征募是另一项重要的MTS/COM+应用程序服务器的行为。对于DTC事 务 ,COM+支持与 MTS相同的结构,. 即资源分配器和资源管理器。开发一个COM+的资源管理器或资源分配器不38 第二部分高级COM编程技巧 是一件容易的事,而且许多时候,伴有相当大的系统幵销。 均衡资源管理器 为 了 使 开 发 人 员 可 以支 持不重要 的 上 下文环 境 中 的 事 务,COM +引入了均衡资源管理器 (CRM ). CRM由两个不同的COM+类构成,CRM工作器执行手头上的仟务,而CRM均衡器则将 在一个执行过程失效时,取消CRM工作器的操作。COM+可以实现支持执行记录工具的CRM独 有的接口,而这种记录工具可以在系统出现故障时,恢复开发人员创建的CRM。客户创建CRM 工作器对象来完成执行任务,(WCOM+创建CRM均衡器对象来监控工作器对象的执行行为:顺、 便 说 一 下 ,MTS编程人员习惯把CRM称 作 “crumb” ,下次遇见你的COM+伙 伴 时 ,一定要用 “cnimb”,否则他们很可能会嘲笑你。 3 . 6 开发COM+应用程序 开发COM+应用程序结构上很像开发高质量的MTS应卬程序,COM+引入了几个新工具,ffl 以增强MTS所提供的服务和特性,如扩展基本的COM/DCOM系统。在这一部分中,将讨论某些 设计问题和几个新的COM+特 性 ,以及某些新的基本COM特性的升级。 设计COM+组件 当考虑使用COM+作为应用程序服务器平台时,要记住的敁重要的事是,COM+的应用程序 服务器端只是MTS的简单升级。虽然这是一个重要的升级,但基本MTS范例还是贾穿始末。因 此开始COM+的艰苦跋涉的一个途径就是,编写高质量的MTS进程内组件。COM+通过将MTS 服务模型与基本的COM特性相结合,获得了很多实际的好处。 下一步要考虑的是组件要在什么样的环境下使用。除了Windows 2000以外,COM+目前不 支持任何平台。这就意味着,如果要设计的组件需要运行在NT 4上 ,那么你还是应该坚持MTS 2.0所提出的方针c 这样的组件仍然可以很好地运行在COM+环境下。另一方面,如果只打算在 Windows 2000上开发,那你就可以利用到几项新的而且强大的企业级特性。 3 . 7 队列组件 微 软 的消息队 列 服 务 器 (MSMQ)已经为COM开发人员打开了一个全新的世界,在这里他 们将无所不能。MSMQ使得应用程序可以断开它们与外部服务的联系,这样可以极大地提髙可 升级性,并且使得企业级应用程序的每一部分都可以独立地、尽可能快地运行。MSMQ还通过 断开的队列提供了另一个昆著的优势,大铟应用程序的一部分可以暂时被断开,然后 再连 接 , 因为MSMQ将一直存储队列信息,直到它们能够被提交。 那么MSMQ与COM+到底有什么关系呢?我很高兴你问了这个问题。MSMQ在本地有个 标准的函数调用接口,尽管这个接口与COM应用程序和MTS应用程序很好地集成在一起,但在 整个本地COM世界中,MSMQ仍然只代表了微不足道的一个步骤。COM应用程序需要将调用信 息与要传送的消息捆绑在= 起 ,而不只是进行接口方法的调用。COM+通过使W 队列组件改变 了这一切。COM+的队列组件系统将MSMQ作为传送系统使用,这使得客户可以以一种异步的第J 章 CCW+ 结构与管理 方式对服务器进行COM调用。 这意味着,一个运行在没有联网的便携式电脑上的客户应用程序,仍然可以对一•个不存在 的 、使用标准COM 技术的服务器进行多次调用。运行在客户端的队列组件记录器将COM 方法调 用放人队列,以用于将来的提交。当服务器网络又可以使用时,被放入队列的COM 调用将被提 交给目标队列。服务器网络上的队列组件接收器将从队列中接收COM 调用消息,并将其传递给 队列组件执行器。然后执行器将对COM 服务器进行调用。 任何使用异步技术的主要缺点是,组件不能给调用者返间数据,但同步操作却可以。例 如 , 如 果一 个 组 件调用f 队列组件的CreateOnler方 法 ,组 件 不 会 返 回 一 个 值 (来说 明成 功 或失败) 或 者 任 何其 他值(如排序确认号)。相反 ,组件必须遵循分布式异步系统的原则,使用回调机制, 给客户返回一个相关的数据。第27章中将详细讨论这一机制。 3 . 7 .1 放入队列的事务 队列组件使用基本的MSMQ 事务支持,来管理事务中的交互,即使是在客户连接不上的情 况下,当一个客户对任何组件接口(为队列 经 过 设置的 )进行调用 时 ,记录器将所有的调用收 集起来变成•一条消息,如果消息不能提交,事务将被取消。相似的执行行为也会发生在服务器 端。如果消息不能被成功地退出队列,事务将被取消,MSMQ将会把消息放进合适的死信队列c 3 . 7 . 2 管理队列组件 对于一个企业级规模的系统来说,系统管理员在队列组件应用程序的管理中扮演了很重要 的角色。系统管理员使用组件服务管理器设界并配置队列组件应用程序,使用COM 资源管理器 设置COM+应用程序和必不可少的类的接口,以便它们可以使用队列组件服务。COM 资源管理 器将为客户应用程序输出合适的队列信息。应当注意的是,接口级的队列组件设贯是最重要的, 这就是说,一个类可以拥有一些直接接口和一些队列接口。客户也可以选择是以正常方式使用 组 件 ,还是以队列方式使用组件。这使得组件既可以工作于正常的紧密耦合方式,也可以工作 于队列方式,这些全凭客户自己判断。通常使用队列标记来创建队列组件,如 queue:/ne w: OrderEntry.Order. 10 创建出队列组件以后,它的操作方法与其他的COM 对象基本相同。然而 ,还是有一些限制。 正如你可能已经注意到的,队列组件服务提供的是一条单行的通信通道,这意味着为队列而设 置的接口必须只支持[in]参数。在服务器与客户断开的情况下,是没有办法使前者对后者的调用 产生响应的。此 外 ,如果事务对客户端资源有所影响,开发人员必须增加一条通道来检测在服 务器端已经失效的事务,并返回任何必要的客户操作。恢复客户端的两个常用的途径是,把要 传给客户的消息放人队列,或者将结果记入一个合适的数据库。 3.8 松散耦合事件 COM 技术的缺点之一是,一直以来缺少广播式的服务。有兴趣从COM 服务器得到通知的客 户们 ,一直依赖于自定义的解决办法或者是COM 的连接点。在涉及COM 的地方,这两种方法都 需要服务器对每一个客户迸行单独的接口方法调用。这种做法背离最优行为太远,而且也造成40 第二部分高级COM编程技巧 了设计中实现方法的紊乱,而这种设计应该要求是广播语义的简单明丫。正如我们通过队列组 件看到的,COM+正在改进传统COM的紧密耦合、同步、双向的方式。 使用松散耦合事件 松 散 耦 合 事 件 (LCE)为事件的分布引人了一种出版订阅模型。激 发 事 件 (进行方 法调用 ) 的服务器通过COM+事 件 服 务 “出版” 这一信息,想接收特定出版事件的客户通过COM+事件服 务订阅这一事件,当服务器激活事件,COM+事件服务将把这个事件广播给所有感兴趣的客户。 松散耦合事件系统有几个重要的方面。LCE系统在本地COM级提供广播式机制,服务器不 需要知道哪一个客户在接收。事件可以更高效地在进程之间、网络之间分布,因为只有一个事件 消息从服务器传送出去。这与连接点结构形成鲜明的对比,连接点结构需要服务器轮流地呼叫每 个客户。现 在 ,COM+可以在响应返回之前,同时呼叫所有的客户。然 而,服务器可以通过队列 组 件 ,从LCE系统中派生出异步行为。这种结构也为将来更多地增强面向广播的性能铺就了一条 道路。LCE系统也允许出版者对事件进行过滤,对哪个客户可以接收哪个事件进行控制。订阅者 也可以通过ISubscriberComrol接口实现过滤器,这个接门使客户可以丢弃或重定向特定的事件。 这里以一个定货系统为例。设计时认为: 一收到 定单,可用信息就可以发送,但这些信息对于实时定货系统不是必需的。这种设计 可能是考虑了流量问题。松 散 耦 合 结 构 (其 没有 可利用的任何实时信 息)可以在时间上更有效 地使用应用服务器。下了定单后,任何问题都可以通知到下单人。 注 意 .LCE是COM+工具包中另一个非常强大的工具。像队列组件一样,LCE服务弥补 了以往COM实现中的一个重大缺陷。队列组件为COM提供了 一种减弱方法调用的途径, 而LCE提供了一种本地COM广播机制。COM+的队列组件和LCE服务所表现出来的特 性 ,解决了很多问题,使得许多开发人员开发出大量的好软件。 3.9 COM+的数据访问 COM+中的数据访问很像MTS。特别是OLE DB和ADO,都是可选的访问机制。OLE DB提 供 / 一个强大的、可扩展的结构,用以支持不同类型的底S 数据存储,包括任何ODBC源 。由于 OLE DB的灵活性和高性能,它必然很复杂。ATL的消费者和生产者模版,可以减轻使用C++进 行OLE DB开 发 时 的负担。然而到目前为丨t ,编写OLE DB的 最流行 的 机制是ActiveX Data Object ( ADO )0 ADO提 供 f 一个工作于OLE DB之 上 的 双 重 接 组 件 的 实 现 ,它可以很容易地 被编译过的应用程序和脚本使用。当然,如果你和大多数开发人员一样的话,你应该已经拥有 了一个大型的基于ODBC的系统,在这种环境下也可以全面支持。 3 . 9 .1 读取最优化的数据访问 IMDB是一种灵巧的内存中数据库,它为读操作M 优化地缓存了系统。IMDB允许一次性载 入 静 态数 据 ,并且被多次查询。这种类型 的内 存中 数 据支 持,可以得到的性能优势是巨大的。 通过1MDB满足的本地查询,既不需要网络访问,也不需要磁盘访问。IMDB也支持写操作,它第J 章 COM+结构与管理 41 把更新积累起来,直到它们被提交。在 这 一点上 ,改变被提交给了底层数据库。J i 然 ,真正的 性能优势在于缓存静态读数据的广阔舞台。 应用程序访问IMDB的方式,基本与它们访问其他现代数据库的方式相同。IMDB是OLEDB 的提供者,这确保了ADO对大多数的编程语言和脚本环境的内在支持。 3 . 9 . 2 事务中的共享属性管理器 MTS共享性 质管 理 器 (SPM),对于用JIT技术编写的早期版本的组件是一个很有用的东西, 它可以在一个特定的服务器进程中,提供稳定的共享数据存储。COM+在IMDB之上实现了共享 性质管理器接口。这种新构造被称作事务中的共享性质管理器。一 定 要记住,和你的COM+伙 伴在一起时,要把SPM叫 做 “spam”。 3.10 COM+的安全性 COM+提出的是与MTS相同的基本应用程序服务器安全特性。COM+应用程序可以使用标准 COM安 全 性,即公开的、程 序 上 的 、基于任务的安全性。COM+已经扩展了对于接口方法的基 于任务的安全性,使系统管理员可以在最基础的层次控制访问(回忆一下,MTS只在接口层次 上支持任务成员资格)。不像MTS,COM+在库程序包中也支持基于任务的安全性。COM+通过 ISecurityCa丨丨Context接 口 提 供 增 强 的 安 全 性 信 息 。ISecurityCallContext取代了 MTS中的 ISecurityProperty接 口 ,并且提供了关于基本客户返回链中所有调用者的详细信息。C0M+安 全 . 性将在第18章中详细讨论。 3 . 1 1 基本的COM特性 在微软,MTS和C0M/DC0M代表了由不同的开发工具支持的不同的开发途径。在Windows 2000中 ,这种情况义出现在C0M+ (MTS的接任 者 )和核心COM服务之间。在这一部分中,将 讨论某些新的基本COM服 务 ,这些服务在Windows 2000中和C0M+打包在一起。 3 .1 1 .1 结构存储 结构存储是一种强大的复合文件管理服务,支持一个文件中的文件系统。结构存储最伟大 的地方在于,它是一个受到Windows和许多应用程序产品直接支持的标准机制。Windows 2000 许诺要将结构存储集成到更多的文件系统和壳体程序特性中去。链接跟踪是一种新的特性,它 使得Windows 2000可以在目标对象被移动时,在结构存储文件中更新OLE链 接 。所有Win32的 MoveFileO例程将与链接跟踪服务相协调,包括新的MoveFileWithProgress()例程。链接跟踪特 性只能在NTFS系统下起作用,但是却给出了一个等待已久的解决普遍问题的方案。 结构存储系统也引人了一些新的例程,来创建和打开复合文件: • StgCreateStorageEx() • StgOpenStorageEx() StgCreateStorageEx()例程取代了StgCreateDocFile()例 程 ,并可以被所有的Windows 2000应 用程序使用。对结构存储系统的增强可以通过这两个新的例程表现出来。42 第二部分高级COM编程技巧 Windows 2000包含了升级版本的NTFS ( NTFS v5 ) ,它使许多新的基于磁盘的服务可行。 NTFS v5系统下的复合文档可以采用STGFMT_NATIVE存储格式。NTFS本 地 结 构 存 储 (NSS ) 使用NTFS的本地的、基于属性的存储特性,来提高复合文档的效率。 Windows 2000结构存储也支持新的IDirectWriterLock接口,这种接口可以使一个写程序获得 对以直接方式打开的根存储对象的独占访问权。一些读程序可以同时访问这个存储对象。 3 .1 1 .2 取消未完成的COM调用 同步COM调用可以节省因等待失败的服务器请求超时,而延长的操作时间。另 一 方 面 .异 步调用的特性可以满足某些应用程序的需要,取消在超时事件发生之前未完成的调用。COM提 供了一些调用控制接口,它们可以被用于管理异步方法调用。COM+通过在每个方法调用之前 和之后调用CoSetCancelObjectO,为所有的使用标准调度的调用创建和管理Cancel对 象。COM 的Cancel对象支持ICancelMethodCalls接 H , —个指定Cancel对象的ICancelMethodCalls接 L1,可 以 通过使 用lID_ICancelMethodCalls接口的1D调用CoGetCance丨ObjecU)组 件 库 例 程 获 得 。 CoGetCancelObject()的原塑如下: HRESULT CoGetCancelObject( ................... D W O R D dwThreadld/Thread with pending call ................... REFIID riid, ..........//ID of requested interface ................... vo i d ** p p U n k ----); //Pointer to the requested interface ICancelMethodCalls支持表3-1中所列的方法。 表 3-1 丨 cancelMethodCalls 方法 IcancclMethodCa 丨丨 s 方法 作 用 CanccI ( ) 要求方法被取消 TesiCancel ( ) 检奔调用足否l i 经被取消 SelCancelTimeout ( ) 设罟调用超时时自动取消 得到Cancel对象接口的过程和Cancd()方法的调用 ,在CoCancelCaU()例程中有等同的部分。 CoTestCancelO为TestCancdO方法的调用提供了一条相似的捷径。这些特性的综合,使得对挂起 方法的调用级控制变得更加容易。 • 3 . 1 2 小结 COM+是COM发展中最合理的下一步,它没有彻底地脱离COM,而是保留了COM结构中最 好的东西,并使COM得到增强。 最伟大的进步之一就是具有负荷均衡和对象池的能力,而MTS实际上根本不能实现这些特性。 COM+增加的许多东西是为了提髙MTS的性能和功能。MTS/COM+模型是来自于微软的要 求 ,你应该确定你在跟随这个指引。COM+使MTS和COM不可分割,并且以增加更多的性能优 势来诱惑你。 本书的剩余部分将讨论如何使用COM+幵发组件应用程序,并使它的效力最大化。第4章持久存储 本章内容: • IPersist 接 口 • IStream 接 口 • 创建实现丨PersistStreamlnit的ATL对象 • 使用一个持久对象 • 简化持久对象的创建 • 简化持久对象的使用 开发人员要面临的一个挑战是维持一个COM对象的状态。在运行环境中,当一个COM对象 被载入时,它可能很有必要记住并回复到原来的状态。在 这种 情 况下 ,这个对象应该使用持久 存储数据,使它可以恢复到前一个状态的操作。当一个COM对象被删除时,保存当前的状态可 能也是必不可少的,使得当前的应用程序,或其他应用程序可以得到一个与该对象被删除时一 样的状态。这种情况下,对象应该保存持久数据。 使用持久存储的一个好例子是VB对ActivcX控件的处理。在ActiveX控件属性表中,可以看 出VB把相同属性的数据从一个会话保持到另一个会话。换句话说,如果你在运行VB时 ,为控件 的属性设定了某些想要的值,当你再运行VB时 ,那个控件将保留上个会话过程中的属性设置。 这一章将讨论如何将持久数据载人到对象,以及如何从对象保存持久数据。我们的讨论将 围绕IPersistStreamlnit和IStream接口进行。然而 ,对于持久数据存储来说,这一章中所讨论的内 容不是你惟一可以选择的方法。 两个控件例子和两个使用这些控件的程序例子将说明这些方法。第一个控件很简单,使你 可以很容易地看到和理解其中的实质。载入第一个控件的客户应用程序也很简单,它将说明如 何使用持久存储方法,而不依赖于类的创建向导所创建的类。 第二个例子程序比第一个做得更多,它会响应用卢的鼠标动作。然 而 ,我还是经常使用类 创建向导,以使事情简单化。因此,第二个例子没有花费比第一个更多的时间来创建。 4.1 IPersist 接口 有几个标准的持久存储接口允许客户应用程序获得和保存一个对象的状态。一个对象可以 通过实现一个或多个这样的接口,来声明它有使状态连续的能力。持久存储接口也可以使客户 通过载人连续的 数 据 ,复制一个对象。我已经提到了v b 的属性 持久性的使用。微软消息队列 (MSMQ)使用持久存储接口,为消息的传输保存对象的状态。表4-1给出了六个标准的持久存 储接口。 所有的持久存储接口都是由IPersist接口派生而来的。这个接口可以使客户得到对象的类标44 第二部分高级COM编程技巧 识 符 (CLSID)。具有代表性的做法是,客户会将对象的类标识符与对象的状态一起存储起来。 通 过 这种 方式 ,客 户 应 用 程 序 可 以知 道,当它要重新例示这个类时,应 该去 激话哪一 个对 象 。 对 于无 状态对 象,IPersist接口就足够了,因为客户应用程序需要知道的仅仅是激活哪一个类。 然 而 ,大 多 数 对 象 除 了 它 们 的 CLSID外 ,还 要 存 储 一 些 数据。这样的对象可以在由基本 的 IPersist接口派生出的、几种类型的持久存储接U 中进行选择。 表4 - 1 标准持久接口 接 口 描 述 IStorage IStorage接口支持创建和管理结构化的存储对象 IStream IStream接口支持将数据读或写入流对象 IPersist IP m is i接口定义单一的方法GetClasslD,该方法被标记为提供对象的 CLSID,以永久存储在系统中 IPersistStorage 该 接 U 定 义 一 个容器 应 用 程 序 ,以传 输一 个 已存储对象到容器对象中, 并且加栽和保存该存储对象 IPcrsistStream 该 接 n 提供 保存 和加载 对 象 的 方法,该方法为存储提供简单的序列流 IPersistFile 该 接 n 提供方法允 许从 磁 盘 文件 中加 铽对 象 ,或者将对象保存到磁盘 文件中 4.1.1 IPersistStorage IPersistStorage接口提供了一些方法,使容器应用程序可以将一个存储对象传给容器所包含 的对象中的一个,并载人和保存这个存储对象。IPersistStorage接 n 通常被用于复合对象。使用 这个接口的一个普遍的应用程序就是剪贴板或拖放操作。容器使用接口初始化对象,使它处于 一种已被载人或正在运行的状态。下面是接口的声明部分: interface IPersistStorage : IPersist { HRESULT IsDirty ( void ) ; HRESULT InitNew ( (in, unique] IStorage *pStg ) ; HRESULT Load ( [in, unique] IStorage *pStg ); HRESULT Save [in, unique] IStorage *pStgSave, [in] BOOL fSameAsLoad第4 章 持久存糖 45 HRESULT SaveCompleted ( (in, unique] IStorage *pStgNew ); HRESULT HandsOffStorage ( void 4.1.2 IPersistFile IPersistFile接口提供了一些方法,使对象可以从一个磁盘文件载入,或者被保存到一个磁盘 文件当中。这一章中的例子使用一个流对象进行载人和保存。IPersistFile接口以下面的方式与我 们在例子中所做的形成对比:当你想要从一个单独的文件中读出或写入信息时,你会愿意实现 —个IPersistFile接口。接口声明如下: interface IPersistFile : IPersist { HRESULT IsDirty ( void ) ; HRESULT Load ( [in] LPCOLESTR pszFileName, (in] DWORD dwMode ); HRESULT Save ( (in, unique] LPCOLESTR pszFileName, (in] BOOL fRemember ) ; HRESULT SaveCompleted ( (in, unique] LPCOLESTR pszFileName ) ; HRESULT GetCurFile ( [out] LPOLESTR *ppszFileName ); } ; 4.1.3 IPersistStreamlnit IPersistStreamlnit接口实际上是为了取代IPersistStream接口而设计的,它只增加了一个方法 调用lnitNew()。lsDirtyO、Load()、Save()和GetSizeMax()都是从IPersistStream派生出来的。所46 第二部分高级COM编程技巧 有的IPersistStreamlnit方法可以在表4-2中看到。接口声明如下: interface IPersistStreamlnit : r • IPersist \ HRESULT IsDirty /I void ); HRESULT Load / V (in, unique] IStream 1 ); rpStm HRESULT Save i \ [in, unique] IStream •pStm, 【in 丨 BOOL fClearDirty ); HRESULT GetSizeMax / \ [outl ULARGE INTEGER ); ♦pebSize HRESULT InitNew / V void \ •)I }; 表4-2 丨S t r e a m 接口 J j 法 描 述 Querylntcrface 从I U n k n o w n屮继承 AddRef 从IUnknown中继承 Release 从IUnknown中继承 GetClassID 从[Pcrsis丨中继承 IsDirty 检杳从最后一次保存起的改变 Load 从先前被保存的流中初始化一个对象 Save 将对象保存到特定的流中,并标识对象的dirty足否被承新设S GetSizeMax 返回黹要保存到对象中的流字作 InitNew 将对象设贤为默认值 4.2 IStream接口 IStream接口支持读写数据到流对象。流对象包含结构存储对象中的数据。简单的数据可以直 接的写人一个流,但最经常发生的是,在存储对象中流是元素的嵌套。它们与标准文件很相似。 IStream接口定义了与MS-DOS文件功能很相似的方法。例 如 ,每个流对象都有自己的访问 权限和查找指针。流对象与DOS文件的主要不同之处在于,流不是用一个文件句柄打开的,而 是通过一个IStream接口指针打开。第4 章 持久存储 47 这个接口的方法提供给你的对象数据的是,一种你可以读或写的连续的宁节序列。也有其 他的方 法,比如,用于提交和回复在执行模式中打开的流的变化的方法,以及用于限制对流中 的某一范围数据的访问的方法。 流可以长时间地保持打开状态,而不会消耗文件系统资源。IStrean^Release方法与文件的 关闭功能相似。一旦流对象被释放,它将不再有效并且不能再被使用。 表4-3给出了 IStream的各种方法以及对每个方法的简短描述。 表4-3 IStream接口 接 n 描 述 Querylnterface 从I Unknown中继承 AddRef 从IUnknown中继承 Release 从IUnknown中继承 Read 从丨SequemialSlream中继承而來。从流对象中读一些特定的字节到内存 中,以查找当前的指针 Write 从ISequentia丨Stream中继承而来,将--些特定的字节写人流对象中,汗 始丧找1 前的指针 Clone 创建一个新的流对象,它拥有与原对象相冏的字节,但是具有独立的 杳找指针 Commit 确保流对象的所有改变都反映在父存储对象中 Copy To 从当前流的杳找指针中复制一些特定的字节到另一个流的杳找指针中 LockRegion 限制流屮特定的访问。支持这个选项是因为一些义件系统并不支持它 Revert 从最后的一次r.Commh调H!起 ,使所有的流修改无效 Seek 在流的开始处将指针定位到一个新的位S, 或者到1 前位莨 SctSi2c 改变流对象的大小 Stat 从流中恢复STATSTG结构 UnlockRegion 解除先前使用丨S trcam:: LockRegion限制的i ) i 问 下一部分将给出我在例子程序中使用的IStream接 U 方法的实例。 4.2.1 IStream::Write() 清单4-1中的代码说明了如何使用IStream::Write()向一个IStream对象写人数据。 * 清单4 - 1 写入IStream对象 ^include int m a i n ( ) { IStorage *pStg = NULL; IStream *pStream = NULL; LONG 1 = ::StgOpenStorage( L“C:WMyStorageFile“, NULL, STGM_READWRITE丨 STGM_DIRECT|STGM_SHARE 一EXCLUSIVE, NULL, 0, &pStg ); if( FAILED( 1 )) { cout « “StflOpenStorageO failed.\n';48 第二部分高级COM编程技巧 exit( 0 ) ; } HRESULT hRes = pStg->OpenStream{ L'MyStream“, NULL, STGM_READWRITE|STGM_DIRECT|STGM_SHAR E_E XCLUSIVE, 0, &pStream ); if( FAILED( hRes )) { pStg->Release(); cout « “OpenStream() failed.\n“; exit( 0 ); } U L O N G Cb; hRes = stream->Write( L'Hello“, 12, &cb ); if( FAILE0( hRes )) cout « “Write() failed\n“; pStream->Release(); pStg->Release(); return 0; 4.2.2 IStream::Read() 清单4-2中的代码说明了如何使用IStreamzReadO从一个IStream对象读取数据。 清单4 - 2 从IStream对象读取数据 #include 〈w i n d o w s . h > #include int m a i n ( ) { IStorage *pStg = NULL; IStream *pStream = NULL; LONG 1 = ::StgOpenStorage( L_C:\\MyStorageFile_, NULL, STGM_READWRITE|STGM_DIRECT|STGM_SHARE_EXCLUSIVE, NULL, 0, &pStg ); if( FAILED( 1 )) { cout « 'StgOpenStorageO failed.\n“; exit( 0 ); } HRESULT hRes = pStg->OpenStream( fMyStream“, NULL, STGM_READWRITE 丨 STGM_DIRECT|STGM_SHARE_EXCLUSIVE, 0, &pStream ); if( FAILED( hRes )) { pStg->Release(); cout « BOpenStream() failed.\n“; exit( 0 ); unsigned short olestr[256J; char s[256];第4 章 持久存储 49 ULONG Cb; hRes = pStream->Read( olestr, 255, &cb ); if( FAILED( hRes )) cout « “Read() failed.\n“; wcstombs( s, olestr, 255 ); std::cout « s « std::endl; pStream->Release(); pStg->Release{); return 0; 4.2.3 IStream::Seek() 清单4-3中的代码说明了如何使用IStreanSeekO将指针移人一个IStream对 象0 清单4 - 3 在IStream对象中删除指针 #include #include int m a i n ( ) { IStorage *pStg = NULL; IStream *pSt「eam = NULL; LONG 1 = ::StgOpenSto「age( L-C:\\MyStorageFileM, NULL, STGM_READWRITE|STGMJ)IRECT|STGM_SHARE_EXCLUSIVE, NULL7 0, &pStg ); if( FAILED( 1 )) { cout « “StgOpenStorage() failed.\n“; exit{ 0 ); HRESULT hRes = pStg->OpenStream( L_MyStream_, NULL, STGM_READWRITE\STGM_DIRECT 丨 STGM_SHARE_EXCLUSIVE, 0, ApStream ); if( FAILED( hRes )) { pStg->Release(); cout « “OpenStream() failed.\nB; exit( 0 ); unsigned short olestr(256]; char s(256]; UL O N G cb; LARGE_INTEGER li ; LISet32(li, 2 ); hRes = pStream->Seek( li, STREAM_SEEK_SET, NULL ); if( FAILED( hRes )) cout « *Seek() failed.\n“; hRes = pStream->Read( olestr, 255, &cb );50 第二部分高级COM编程技巧 if( FAILED( hRes )) cout « NRead() failed.\n“; wcstombs( s, olestr, 255 ); std::cout « s « std::endl; p S t r e a m •> R e l e a s e (); pStg->Release(); return 0; } 4.3 创 建 实 现IPersistStreamlnit的ATL对象 我曾经读过一些书,这些书的作者会用整个一章解释某些东西,而我则忠实地跟随着这整 章的话题。在大多数情况下,我觉得自己理解了实质性的东西。但当要实际运用这些东西的时 候 ,却不知道如何下手。因 此 ,我将冒险给你们看一些你们已经知道的东西,我将给你一组循 序渐进的指导,指导你创建一个实现IPersistStreamlnit的ATL项目。运行C—, 创建一个ATL项 目,为了简单起见,使用默认设置。 在插人菜单中选择新建ATL对象。然后 ,在这个ATL项目中插入一个完全控件。如果你需要 持久存储,那么使用完全控件的理由就是,这样的控件是从IPersistStreamlnit接口继承而来的。 这简化了你进人持久数据存储领域的第一步。IPersistStreamlnit实际上什么也不实现,因为它只 是个接口。为了得到这个接口的默认ATL实现 ,你必须使用IPersistStreamlnitlmpl。 IPersistStreamlnit已经实现了GetClassID()、LoadO、Save()、InitNew()、IsDirty( ) 和 GetSizeMaxO几种方法,但你还有机会向控件中加入一些这些方法之外的方法。对于完全控件来 说 ,你想加入的方法必须出现在类的定义中,如下所示: II IPersistStream HRESULT _ stdcall IsDirty( void ); HRESULT _ stdcall Load( IStream * ); HRESULT =stdcall Save( IStream *, BOOL ); HRESULT — stdcall GetSizeMax( ULARGE_INTEGER * ); II IPersistStreamlnit HRESULT _ stdcall InitNew( void ); II IPersist HRESULT __stdcall GetClassID( CLSID* pClassID ); Next, you must actually implement the methods. Open the .cpp file for the full control you added, and add the following (empty) methods: HRESULT CPersistClassl::InitNew( void ) HRESULT CPersistClassl::GetClassID( CLSID* pClassID )章 持久存储 5J HRESULT CPersistClassI::IsOirty( void ) { return S^OK; HRESULT CPersistClassI Load( IStream *pStm ) { return S_0K; HRESULT CPersistClassI::Save( IStream *pStm, BOOL bClearDirty ) { return S_0K; HRESULT CPersistClassI::GetSizeMax( ULARGE_INTEGER *pcbSize ) return S_0K; } _ 要注意的一件事情是,事实上你不必真的去实现IsDirtyO和GetClass丨D(),因为IPersistStream IniUmpl已经实现了它们。为了完整,它们也在上面被给出了。在空方法存在之后,你就可以使 用它们为所欲为了。在大多数情况下,Save()和LoadO方法是最重要的。 清单4-4给出了第一个例子控件的.h文 件 (位于AtlPersistControll子目录下的Chapter03目录 中 )。控件类名为PersistClassl。看看清单,会发现除了方法原型外,我还增加了一个数据结构, 这个数据结构仅作为一个数据例子,你可能 会想把它 作为持久存储实现 (我实际上确实在第二 个例子中使用了这个数据结构)。它的目的是为了保持一个在绘制控件窗口时显示的文本字符串。 可以看出它由构造函数初始化,和InitNewO方 法 一 样 (参见清单4-5)。 清单4-4 PersistClassI.h------IpersistStoragelnit 成员方法 p u b l i c : CPersistClassI() { II Clear the data structure. mefnset( &m_TextData, 0, sizeof ( TEXTDATA )); II Initialize with the clock tick count, wsprintf ( m_TextData.szText, HGetTickCount()=%(!•, GetTickCount()); II Set the color. m_TextData.Color = RGB( 255, 255 , 0 ); typedef struct{ char szText[400]; COLORREF Color; } T E X T D A T A ; TEXTDATA m_TextData ; II IPersistStream HRESULT — stdcall IsDirty( void );52 第二部分高级COM编程技巧 HRESULT _ stdcall Load( IStream * ); H R E S U L T 二 stdcall Save( IStream *, BOOL ); H R E S U L T 二 Stdcall GetSizeMax ( ULARGEJNTEGER * ); // IPersistStreamlnit H R E S U L T 一 stdcall InitNew( void ); II I P e r s i s t H R E S U L T 一 stdcall QetClassID( CLSID* pClassID ); 在清单4-5中给出了实现这些方法的源代码。正如前面所提到的,InitNewO方法与构造函数 执 行 相 同 的 初 始 化 。Load()和Save()方 法使 用 了 一 个[Stream对 象 来读写 将被持久化的 数 据 ( m_TextData结 构 )。 清单4-5 PersistClassI .cpp------IpersistStoragelnit 成员方法 HRESULT CPersistClassI::InitNew( void ) II Clear the data structure. memset( &m_TextData, 0, sizeof( TEXTDATA )); II Initialize with the clock tick count, wsprintf( m_TextData.szText, MGetTickCourvt()=%cP, GetTickCount()); II Set the color. m_TextData.Color = RGB( 255, 255, 0 ); r e t u r n S 一OK; HRESULT CPersistClassI::GetClassIO( CLSID* pClassID ) { // Return the CLSID *pClassID = CLSID_PersistClassl; return S OK: HRESULT CPersistClassI::IsDirty( void ) return S_0K; HRESULT CPersistClassI::Load( IStream *pstm ) { ULONG IRead = 0; // Read the data. pStm->Read( &m_TextData, sizeof( TEXTDATA ), &lRead ); return S_OK; HRESULT CPersistClassI::Save( IStream *pstm, BOOL bClearDirty )弟4 章 持久存储 { ULONG lWritten : 0; II Write the data. HRESULT hr 3 pStm->Write( &m_TextData, sizeof( TEXTDATA ), &lWritten ); return S_0K; HRESULT CPersistClassl::GetSizeMax( ULARGE_INTEGER *pcbSize ) ( II Return the size of the data. (*pcbSize).QuadPart = sizeof( TEXTDATA ); return S_0K; 创建简单的ATL控件 如果不想要一个完全控件怎么办?如果只想要一个很简单的控件怎么办?你知道:ATL的控 件种类之多是很著名的。放 轻 松,你可以实现自己的愿望。如果你创建一个ATL对 象 ,你可以插 人一个简单的对象,然后再对类的定义进行简单的修改就可以了。 特别地必 须要 做两件 事 :派生IPersistStream和IPersistStreamlnit,以及增加一个变量和 方 法。 派生IPersistStream和IPersistStreamlnit,如下 : public IPersistStreamInitImpl, public IPersistStorageImpl 变童和方法的加入如下: BOOL m_bRequiresSave; static ATL_PROPMAP_ENTRY *GetPropertyMap() { r e t u r n ( N U L L ); > 顺便说一下,如果你需要GetPropertyMapO方法返回一个有效的值,是不行的。如果真的出 现这种情况,无论如何也需要一个完全控件。而对于简单的控件只能童力而行了。 CMyClass类的include文件如下所示: Class ATL_NO_VTABLE CMyClaSS : public CComObjectRootEx, public CComCoClass, public IDispatchImpl, public IPersistStreamlnitImpl, public IPersistStorageImpl54 第二部分高级COM编程技巧 p u b l i c : C M y C l a s s ( ) { } BOOL m_bRequiresSave; static ATL_PROPMAP_ENTRY *GetPropertyMap() { return( NULL ); } DECLARE_REGISTRY_RESOURCEID(IDR_MYCLASS) DECLARE_PROTECT_FINAL_CONSTRUCT() BEGIN_COM_MAP(CMyClass) COM^INTERFACE_ENTRY(IMyClass) COM^INTERFACElENTRY(IDispatch) END一COM一MAP // IMyClass p u b l i c : } ; 4 . 4 使用一个持久对象 下 一 步 要 做 的 是 ,创 建 一 个 使 用PersistClassl控 件 的 客 户 应 用 程 序 ,CD上的示例名为 PersistClientl ( 位于PersistClientl子目录下的Chapter03 目录中 )。 PersistClientl应用程序是一个简单的MFC单文档程 序 ,增加的惟一代码可以在类的构造函 数和析构函数中找到。在.h文件中增加了几个成员变童,清单4-6中给出了这些变进。 清单4-6 PersistClientlViewl.h—— 持久存储的成员变II BOOL m_bCoInitialized; IUnknown *m_pUnknown; IPersistStreamlnit *m_pPersistStreamInit; IStream *m_pStream; 在运行时,这个程序以一系列可以预知的步骤进行初始化并载入控件。调用ColnitializeO、 CoCreatelnstance()和 Querylnterface()的步骤参见清单 4-7。 第一段需要注意的代码是对CreateStreamOnHGlobaK)的调用 ,这将创建一个使用内存进行 读写的IStream对象。然后这个IStream对象便可以用于控件的Load()和Save()方法了。 在进行保存之前,hitNewO方法被调用,使得控件被设置到它的默认属性。 析 构 函 数 中 的 代 码 简 单 地 清 除 切 ,IStream对 象 、IPersistStreamlnit对象和IUnknown对象 都将被释放。CoUninitializeO被调用进行最后的清理工作。下面的CLSID可以在.rgs文件中找 到。第4 章 持久存储 55 清单4-7 PersistClientlView.cpp------内存持久存储的结构 const CLSID CLSID_PersistClass1 = {0X33733D5B, 0xd350, 0x11(13,{ 0x8d, 0x44, • 0x0, 0x10, 0x5a, 0xa7, 0x21, 0xbe }}; CPersistClientlView::CPersistClientlView() { HRESULT hRes ; m_bCoInitialized = TRUE; n_pUnknown = NULL; m_pPersistStreamInit = NULL; m_pStream = NULL; II Initialize. hRes = CoInitialize( NULL ); II Return if initialization failed. if( FAILED( hRes ) ){ m_bCoInitialized = FALSE; r e t u r n ; II Create the object. hRes = CoCreateInstance( CLSI0_PersistClass1, NULL, CLSCTX_INPROC_SERVER, IID_IUnknown, (void **} &m— pUnknown ); II Return if object creation failed, if( FAILED! hRes ) ){ m_pUnknown = NULL; r e t u r n ; II Attempt to get the IPersistStreamlnit interface object. hRes = m_pUnknown->QueryInterface( IID_IPersistStreamInit, (void **) &m_pPersistStreamInit );. II If we didn't get the IPersistStreamlnit interface II object, return, if( FAILED( hRes ) ){ m_pPersistStreamInit = NULL; r e t u r n ; II Create a memory-based stream. hRes = CreateStreamOnHGlobal( NULL, TRUE, &m_pStream ); II If the creation of the memory-based stream failed, return, if( FAILED( hRes ) ){ m_pStream = NULL; r e t u r n ; II Initialize the object to its default state. hRes = m_pPersistStreamInit->InitNew(); // Return if the InitNew() method failed.56 第二部分高级COM编程技巧 if( FAILED( hRes )) r e t u r n ; II Save the object to the stream. hRes = m_pPersistStreamInit->Save( m_pstream, TRUE ); CPersistClientlView::-CPersistClient1View() II Release the IStream object, if( m_pStream 1= NULL ) m_pStream->Release(); II Release the IPersistStreamlnit object, if( m^pPersistStreamlnit 1= NULL ) m_pPersistStreamInit->Release(); // Release the object. if( m_pUnknown != NULL ) m__pUnknown->Release(); II Uninitiali 2e. if( m_bCoInitialized ) CoUninitializeO; 4 . 5 简化持久对象的创建 在你创建一个控件的时候,vc++确实为你做了某些工作。不是手工地向类中增加方法,而 是从ClassView窗口中添加方法,这不仅向.h文件加入了原型代码,而且提供了一个可以放自己 代码的空方法。 遵循在创 建使 用完 全 控件 的ATL项 目 时 的 步 骤 ,从ClassView窗口添加Load()、SaveO、 InitNewO和IsDirtyO方 法 (你也可以添加GetClslD()和GetSizeMax( ) , 但我不打算在第二个例子 中使用它们)。 剩下的就容易了。只要将你的代码加到Load()和SaveO中 ,控件将保存你想要的数据。 第二个例子对象叫做AtlPersistControl2 ( 位于AtlPersistControl2子目录下的Chapter03目录 中 )。被添加的完全控件类叫PersistClass2。看看清单4-8,会发现PersistClass2的.h文件中很突出 的一部分。构造函数不再像第一个例子中那样完成相同的初始化过程,这 一 次 ,我们将实际使 用那些数据,你也可以看到被添加的方法。最 后 ,注意OnDrawO方 法 ,它将使用m_TextData结 构绘制窗口。 清单4-8 PersistClass2.h— 数据结构初始化,增加方法和改变OnDraw ()方法 p u b l i c : CPersistClass2()第4 章 持久存储 57 II Clear the data structure. memset( &m_TextData, 0, sizeof( TEXTDATA )); // Initialize with the clock tick count, wsprintf( m_TextData.szText, •GetTickCount( 卜 , GetTickCount()); II Set the color. m^TextData.Color = RGB( 255, 255, 0 ); typedef struct{ char szText[400]; COLORREF Color; } T E X T D A T A ; TEXTDATA m TextData;— • 9 II Other stuff here-. II I P e 「sistClass2 p u b l i c : STDMETHOD(InitNew)(void); STDMETHOD(Save)(IStream *pStm, BOOL bClearDirty)j STOMETHOD(Load)(IStream *pStm); STDMETHOD(IsDirty)(void); HRESULT OnDraw(ATl__DRAWINFO& di) { RECT& rc = *(RECT*)di.prcBounds; Rectangle(di.hdcDraw, rc.left, rc.top, rc.right, rc.bottom); SetTextAlign(di.hdcDraw, TA_CENTER|TA_BASELINE); LPCTSTR pszText = _T(“ATL 3.0 : Persistdass2 “ ); TextOut(di.hdcDraw, (rc.left + rc.right) I 2, (rc.top + rc.bottom) I 2, mTextData.szText, lstrlen(mTextData.szText)); return S_0K; 为了这些方法的实现,惟一增加的东西就是在淸单4-9中给出的,对InitNew()和Load()方法 的添加。当控件被重新初始化或重新载人,m_TextData结构中的数据将有所改变。这 时 ,重绘 窗口是 很重要的。在对控件窗口的合法 性做 了简 单的核 实之后,我调用了InvalidateRectO和 UpdateWindow()o这 时 ,用户便可以看到窗口中新的字符串数据。 清单4-9 AtlPersistControl2.cpp------用在持久存储的ATL类中的方法 STDMETHODIMP CPersistClass2::IsDirty() return S_OK; }58 第二部分高级COM编程技巧 STDMETHODIMP CPersistCXass2::Load(IStream *pStm) { ULONG IRead = 0; II Read the data. pStm->Read( &m_TextData, sizeof( TEXTDATA ), &lRead ); // If the window is valid, cause if( ::IsWindow( m_hWnd ) ){ InvalidateRect( NULL, TRUE ) UpdateWindow(); a r e d r a w . r e t u r n S一OK; STDMETHODIMP CPersistClass2::Save(IStream *pStm, BOOL bClearDirty) ULONG lWritten = 0; II Write the data. HRESULT hr = pStm->Write( &m_TextData, sizeof( TEXTDATA ), &lWritten ); return S_0K; STDMETHODIMP CPersistClass2::InitNew() II Initialize the data. memset( &m_TextData, 0, sizeof( TEXTDATA )); wsprintf( m_TextData.szText, “GetTickCount()=%d“, GetTickCountO ); m_TextData.Color = RGB( 255, 255 , 0 ); II If the window is valid, cause a redraw, if( ::IsWindow( m_hWnd ) ){ InvalidateRect( NULL, TRUE ); UpdateWindow(); return S_0K; 4 . 6 简化持久对象的使用 我一直认为添加载入和卸载COM对象的代码是很困难的事情。别理解错了;我不介意写多 少代码,但为什么要写不需要的、多余的代码呢? 这 使我想 起一 个 很 可笑的故事 。那 时我在 参 加 一 个 微 软 内 部 的高层 会议,并 且与微软的 ADO开发人员共进午餐。项目 经 理 看着 我问道:“你们的C++程序员是怎样的?你们有什么比 RAD好的工具吗? ” (RAD指的是快速应用程序开发)当然,这位项目经理是在暗示,我们的第4 章 持久存储 59 C++程序员只是出于对编程的热爱,才乐于一行接一行地写程序。然 而 ,在镇定了一下并且想起 我是一个受到微软邀请的客人之后,我进行了比较礼貌的回答。 重 点是 :有 很 多 时候,我们做了比应该做的工作更多的工作。许多 时候,我们担心向RAD 工具泄 漏我 们开发的某个控件,或 者担心 给 我 们的 应 用 程 序 增 加 额外 的“肿块”。不 要 拘 心 ! VC++可以以最少的额外代码,将一个ActiveX控件封装到一个类中,这样你的工作就变得容易 多了。 有经验的人可以跳过这一段,我不想让你受到侮辱。对 于 新手,这里讨论的是如何为一个 ActiveX控件创建一个封装类:从Project菜单中选择Add To Project选 项 ,出现一个对话框;有 一个选项是Componets and Controls,选择这一项;然后你必须选择Registered ActiveX Controls, 将出现一个可用ActiveX控件 的列表。为了给控件创 建封 装类,双击列表中你想要 的控件 ,并 在提示是否要插入控件时回答ok。你可 以为这个类命名或 者干 脆使 用默认的名字(我通常是使 用 默认 名)。现在控件被封装在类中,不必担心CoInitialize()或CoCreatelnstanceO。很容 易,是 不是? 接下来的一步是 为AUPersistComrol2对 象创 建一 个类,并将它加入到PersistClient2程序 ( 位于PersistClient2子目录下的Chapter03目录中 )中。类名是CPersistClass2, 而我加到视类 的.h文件中的变量名为n^Persist。我还声明了一个IStream对 象 。清单4-10给出了我添加的两个 变 量。 清单4-10 PersistClient2View.cpp——用在持久存储中的成员变量 CPersistClass2 m_Persist; IStream *m_pStre!m; 使 用 类 创 建 向 导 ,我向 CPersistClient2View类 添 加 了 三 个 函 数 :OnInitialUpdate( )、 OnLButtonDown()和OnRButtonDown()。在OnInitialUpdate()函数中,调用了控件的Creatc()函数 , 使控件创 建它 的窗口,并 将其加入视的窗口。我还创 建了 用 于控件 的LoadO和Save()函数的 IStream对象。事实上,OnInitialUpdate()函数最后要做的就是将控件的数据存人IStream对象。 OnLButtonDownO函数就是为了使控件的InitNewO方法被调用。因为这个方法将随着时钟的 变化,引起控件的文本字符串的更新,在控件窗U 中显示的数字将会改变。(i己住,要用左键单 击的不是控件,而是视窗。) OnRButtonDownO函数将使被保存的数据重新载人控件。这使得控件在它要显示的数字第一 次变得可以看见时,显示这个数字。Load()方法使用IStream对 象并得到数据。重要 的 一点是 , 在调用Load()方法之前,我们必须回头查找到数据流的开始部分,因为数据是保存在幵始部分的, 那是我们必须读取的地方。如果我们+ 去找开始部分便试阁读取数据,那么什么也不会被载入 控 件 ,因为这时的流指针的位置已经超过了数据的最高位。这相当于试图从一个只有100字节的 文本文件中读取第101个字节,这是不管用的。 同时要记住,控件中的Load()方法也会导致重绘。这很重要,因为仅仅恢复数据不会引起控 件窗 t l的重绘。清单4-11给出了被添加到CPersistClient2View类的三个函数。60 第二部分高级COM编程技巧 清单4-11 PersistClient2Vies.cpp------在程序中持久存储的使用 void CPersistClient2View::OnInitialUpdate() { CView::OnInitialUpdate(); II Create the control. R E C T R e c t ; Rect.left = Rect.top = 0; Rect.right - 250; Rect.bottom = 100; m_Persist.Create( “Persist“, WS_VISIBLE, Rect, this, 0x11111 II Create a memory-based stream. HRESULT hRes = CreateStreamOnHGlobal( NULL, TRUE, &m p s t r e a m ) failed, return.II If the creation of the memory-based stream if( FAILED( hRes ) ){ m__pStreani = NULL; r e t u r n ; // Save the object to the stream. m_Persist.Save( m_pStream, TRUE ) void CPersistClient2View::OnLButtonOown(UINT nFlags, CPoint point) { II Reinitialize the control. m_Persist•InitNew(); CView::OnLButtonDown(nFlags, point); } void CPersistClient2View::OnRButtonDown(UINT nFlags, CPoint point) II Load the object from the stream, i f ( m p S t r e a m 1= N U L L ){ ll We need a LARGE_INTEGER and a ULARGE_INTEGER. LARGE^INTEGER GoTo ; ULARGE_INTEGER NewPos ; GoTo.QuadPart = 0; // Seek to the beginning. m_pStream->Seek( GoTo, STREAM_SEEK__SET, &NewPos ); fl Call the Load() method. m_Persist.Load( m_pstream ); CView::OnRButtonDown(nFlags, point); } 当你运行PerSiStCliem2程 序 时 ,将看到如图4-1中所示的控件窗口,文本是像这样的一些东第4 章 持久存姥 61 西 :GetTick:Coiint()=432984395。左 键 单 击 视 的 窗 口 (而不 是控件窗口),会发现数字改变了。 当你右键单击视的窗口时,这个数字会恢复为原来的那个数字。 相 Ut畴 wmmmmm GefTki^HrtO O 51» 7 图4-1 PersistClient2 程序 4 . 7 小 结 持久数据对于大多数COM对象都很重要。通过使用很多可用的接口,不难实现持久数据。 你已经学到了一些标准的接口,并知道了在哪儿使用它们。两个例子说明了如何使持久数据在 程序中起作用。 学习的最好方法就是将这章的两个例子扩展。我建议增加持久数据的数量,并且以不同的 方式使用。第5章标 记 本章内容包括: • COM+对象和标记 • 探究标记类型 5.1 COM4•对象和标记 标 〖己是一种为其他的COM+对象提供服务的对象,它们使你可以通过名字来识别COM+对 象 , 标记提供的所有服务都与为它们要识别的对象提供指针有关。识别的整个过程加上接下来的指 针访问被称为绑定。 标记是实现IMoniker接口的对象,它们通常和其他的COM+对象一样,是在DLL中实现的。 有两种观察标记用途的角度:作为标记的客户,它是使用标记得到另一个对象指针的组件;作 为标记的提供者,它是提供标记给标记客户用以识别它内部的对象的组件。 COM+使用标记连接并激活对象,不管它们是在同一台计算机上还是跨越了网络。一个很電 要的用途是为了网络连接。它们也被用于识别、连接并运行复合文档链接对象。在这种情况下, 链接源就是标记的提供者,而包含链接对象的容器就是标记的客户。 考虑一个标准的普通的文件名,这个文件名指向一个数据的集合,而这些数据存储在磁盘 的某个地方。我们可以把文件的内容叫做对象一一内容就是信息,而且可能某处有一些代码知 道如何为这些信息提供某些功能性。一个这样的对象应该允许它的客户通过接口指针来操作它 的内容。 文件名本身并不是对象,而仅仅是指向处于空闲状 态的 对 象 。如何 使 用 这 个名字的 问题, 与将这个对象一一文件—— 从空闲状态变到运行状态有关。但这个名字本身是没有智能的,所 有有关如何运行这个对象以及如何管理这个文件名的知识,必须被编入想要使用这个文件对象 的客户程序中。通常这不成问题,因为应用程序已经和文件对象一起工作了很长时间。 然 而,在一个组件软件环境中,有比那些只存在于文件中的数据更多类型的对象存在。有 的对象闲置在数据库里,有的在电子邮件消息里,而有的则存在于其他文件的特定位置。而其 他的 对 象 ,有的参与了某些运行过程,有的则根本不存在空闲状态。然而 ,客户需要维持符号 链接一一也就是说,客户为了运行对象以及重新得到它们的接口指针,需要维持它们所绑定的 名字。客 户 也 需 要名字来 描述 文件的特定部分(或者一部分 文 件 中 的 一 部 分 )、数 据 库 查 询 、 远程计算以及系统管理操作等等。字面上 ,在一个计算环境中的任何数据集合和仟何过程或函 数 ,都可以有一个名字,而且一个命名和绑定结构可以使客户以高效和强有力的方式使用那些 资源。 这 样 看 来 ,无意义的名字将会引发问题,事 实 上 ,这与组件软件的思想背道而驰。为了增 加一个新的命名工具,需要对所有想要使用新类型名字的客户程序进行修改,换 句话说 ,每个第5 章 标 记 63 客户程序为了能够使用特定资源的特定类型的名字,必须包含一段特定的代码。如果它们不知 道如何使用这些名字,那么与名字相关的资源对它们来说就变成不可用的了。这在一个组件软 件系统中是完全不切实际的,在这样的系统中我们应该可以独立地改造、修 改 、更新和重新配 置软件组件,并且可以通过使用QuerylmerfaceO增加新的接口和特性,而不会丧失与已有客户 的兼容性。 使用标记 假 设 你 拥 有 产 品 X 在 美 国 中 部 地 区 1999年 第 四 季 度 的 销 售 信 息 ,这 一信 息 可 能 存 储 在 Wsalesinfo服务器上的某个特定表格的某个特定单元范围内。一个应用程序需要对这个数据进行 某些典型的操作:“ 这个季度产品X 的销售情况与产品Y的销售情况相比怎么样? ” “ 产品X在今 年第一季度的销售情况与它在去年第一季度的销售情况相比怎么样? ” “将这 个数字增加10%, 并为明年的销售定额更新表格,此表位于www.sourcedna.com/sales/infox/budget.xyz。” 清单5-1说明了如何使用标记和C0M+对象封装最后一个示例,而这个COM+对象实现了一 些必不可少的商业逻辑。 清单5 - 1 使用名字压缩 HRESULT hRes = S_0K ; IBindCtx *pBC = NULL; hRes = CreateBindCtx( NULL, &pBC ); if( SUCCEEDED( hRes ) ){ DWORD dwValue; IMoniker *pMoniker = NULL; II Create the moniker object. HRes = MkParseDisplayName( pBC, 一 LMfile:\\\\salesinfo\\sales\\infox\\salesQ499.xy2!SumnaryM, &dwValue, &pMonike「 ); if( SUCCEEDED( hRes ) ){ II Connect to the actual business object, create and II initialize it if necessary. HRes = pMoniker->BindToObject( pBC, NULL, IID_lSaleslnfo, &pSales ); if( SUCCEEDED( hRes ) ){ II Perform the operation. pSales->Add( 1.1, • “http://www.sourcedna.com/sales/infox/biJdget.xyz_ ); pSales->Release(); pMoniker->Release(); pBC->Release();64 第二部分高级COM编程技巧 这段代码首先调用CreateBindCtxO创建了一个IBindCtx对 象 ,IBindCtx接口提供了对绑定上 下 文 的 访 问 ,而 这 里 的 绑 定 上 下 文 指 的 是 一 个 存 储 有 关 特 定 标 记 的 绑 定 操 作 信 息 的 对 象 。 IMoniker对象是通过调用MkParseDisplayNameO创建的。 如果这两个调用都成功了,将调用BindToObjectO创建实际的事务对象,这个对象将在调用 Add()函数时被使用。 最后 ,调用Release()清理一切。 1. IMoniker::BindToObject() lMoniken:BindToObject()方法创建一个与标记关联的COM对象的实现实例。清单5-2给出了 使用 IMoniker:: BindToObject()方法的一个例子。 ► 清单5-2 使用丨Moniker::3indToObject ( ) ^include IUnknown * CreateAndBind(WCHAR * szDisplayName) { IBindCtx *pbindctx; IMoniker *pmoniker; U L O N G u l ; IUnknown *punknown; HRESULT hresult = ::CreateBindCtx(0, &pbindctx); hresult = ::MkParseOisplayName(pbindctx, szDisplayName, &ul, &pmoniker); hresult = pmoniker->BindToObject(pbindctx, NULL, IID 一IUnknown, (void**)&punknown); pbindctx->Release(); p m o n i k e r 々Release(}; return punknown; } i n t m a i n () { ::CoInitialize(NULL); IUnknown * pu = CreateAndBind(L“e:Wold.doc'); pu->Release(); ::CoUninitialize(); return 0; 2. IMoniker::BindToStorage() IMonik;er::BiiidToStorageO方 法 为 标 记 重 新 得 到 一 个 存 储 接 口 实 例 (通常 是IStorage或 IStream接 口 )。这不会载人COM对象的实现实例。 3. IMoniker::ComposeWith() IMoniker::ComposeWith()方 法 用 “这个” 标记和另一个标记创建出一个组合标记,而 “ 这 个” 标记被称为组合标记中的左标记。 关于组合标记的描述,可 以 在 本 章后 面的“组合标记” 部分找到。第5 章 标 记 <55 4. IMoniker::Enum() IMoniker::Enum()方法为组合标记重新得到IEnumMoniker对象的实例,IEnumMoniker对象 可以用于列举组合标记的组成标记。 5. lMoniker::IsEqual()和lMoniker::Reduce<) lMoniker::IsEqual()方法用来确定两个标i己是否相等。在调用IMoniker::丨sEqual()方法之前调 用IMoniker::Reduce(),使标记还原到最具体的形式。 6. IMoniker::Hash() IMonikei^HashO方法返回一个DWORD值,这个值可以被其他的组件用于对标记进行分类。 7. IMoniker::GetTimeOfLastChange() IMonikemGetTinieOfLastCliangeO方法可以用于确定标记最后一次被改变的时间,如果你在 通过标记缓存数据,这将是非常有用的。 8. IMoniker::CommonPrefixWith()和IMoniker::RelativePathTo() lMoniker::CommonPrefixWith()方法创建一个用于确定两个标记间相似性的新标记。例 如 , 文件标记 c:\mydir\subdir\myfile.doc 和c:\mydir\otherdir\my file .d o c之间有一个枏同的前缀 c:\mydiro IMoniker::RelativePathTo()方法创建一个新的相对标记,用于确定从一个标i己到另一个标记 的相对路径。还是使用前面的例子,得到的相对标记应该为..\otherdir\myme.doc。 9. IMoniker::GetDisplayName() IMoniker::GetDisplayName()方法重新得到标记的显示名,这个功能将返回与你用于创建标 记的文件名相同的文件名。 10. IMoniker::IsSystemMoniker() IMoniker::IsSystemMoniker()方 法返间标记的类型 。清单5-3给出了 一 个使用IMoniker:: lsSystemMoniker()方法的例子。 清单5-3 使用IMoniker::lsSystemMoniker() ^ i n c l u d e 〈w i n d o w s . #include int m a i n ( ) IBindCtx *pbindctx; IMoniker *pmoniker; U L O N G ul; HRESULT hresult = ::CreateBindCtx(0, &pbindctx); hresult = ::MkParseDisplayName(pbindctx, L“e:Wold.doc“, &ul, '•&pmoniker); D W O R D dw; pmoniker->IsSystemMoniker(&dw); std::cout « dw « std::endl; pbindctx->Release(); pmoniker->Release(); return 0;66 第二部分高级COM编程技巧 5 . 2 探究标记类型 提供标记的组件应该使这个标记可以被其他对象访问。理解系统提供的不同标记类的区别 是很重要的,这样才能为给定的对象选取合适的标记。COM也提供创建标i己的函数,这些函数 使用COM提供的标记类。这部分中,将讨论文件标记、运行对象表、项目标id 、类标记以及指 针标记。 5 . 2 . 1 文件标记 最简单的标记类塑就是文件标记,它们可以用于识别被存储于文件中的任何对象。文件标记 可以看作是本地文件系统为文件分配的路径名的封装。对一个标记调用lMoniker::BindToObjectO, 会使这个标记所对应的对象被激活,并返凹一个该对象的接口指针。被标记命名的对象源必须提 供一个IPersistFile接门的实现,以支 持绑定文件 标记(详见第4章 ,本章后面将讨论这个接口的 声 明 )。文件标记既可以代表绝对路径,也可以代表相对路径。 例 如 ,一个被存为CAFINANCIAL\PROJECTIONS.XLS的电子表格对象的标记,将包括与这 条路径等同的信息,但 是 ,标记不必由与上面相同的字符串组成。这个字符串只是显示名,它 是 标 记 内 容 的 表 示 , 对 于 终 端 用 户 来 说 是 很 有 意 义 的 。 这 个 显 示 名 可 以 通 过 调 用 IMoniker::GetDisplayName()方 法得 到 ,它仅用于向终端用户显示标记。IMoniker::GetDisplay NameO方法可以得到任何标记类的显示名。而在内部,标记应该以一种更有利于执行标记操作 的 格 式 存 储 相 同 的 信 息 ,但 这 对 用 户 来 说 却 是 无 意 义 的 o 当 这 个 电 子 表 格 对 象 通 过 调 用 BindToObjectO方法被绑定时,它将被激活,而激活方式也许是向电子表格内载人文件。 COM+提供了标记提供者和APICreateFileMonikerO,后者可以为提供者创建一个文件标i己 对 象 ,并返回它的指针。 COM+支持三种激活原语:绑定到类的对象、绑定到新的类实例以及绑定到存储在文件中的 持久对象。这可以在COM+的AI>I函数CoGetlnstanceFromFileO中看到,正如淸单5-4所示。 清单5-4 CoGet丨nstanceFromFile声明 HRESULT CoGetInstanceFromFile( I in, unique 丨 COSERVERINFO *pcsi, II host/security info [in, unique] CLSID *pClsid, If explicit CLSID (opt) [in, unique) IUnknown *punkOuter,// for aggregation [in] DWORD dwClsCtx, // locality? [in] DWORD grfMode, // file open mode [in] OLECHAR *pwszName, II file name of object (in] DWORD cmqi, // how many interfaces? (out, size一i s ( c m q i ) 】 MULTI_QI *prgmq JI where to put itfs CoGetInstanceFromFile()例程以一个文件名作为输入参数,这个文件名指向一个对象的持久 状 态。使用CoGetlnstaiKeFromFileO方法可以保证这个对象处于运行状态。然后 ,它将返回波激 活 (或重新激活)对象的一个或多个接口指针。为了实现这些,CoGetInstanceFromFile()苜先需 要确定刈象的CLS1D。需要CLS1D有两个原因。第 一 ,如果对象没有处于运行状态, COM+需要第5 章 标 记 67 这个对象的CLSID创建一 个新的实 例,并由持久映像初始化这个实例。第 二 ,如果调用者没有 指定要取消激活调用的明确的主机名,COM+将使用CLSID来确定在哪一台机器上激活对象。 如果CLSID没有被明确地传递给调用者,CoGetlnstanceFromFileO函数将通过调用COM+的 API函数GetClassFileO,从文件本身派生出CLSID: HRESULT GetClassFile([in, string! OLECHAR •pwszFileName, [out] CLSID *pclsid); GetClassFileO使 用文件 中 的报头信 息(和使用注册信息一样)确定文件中包含的是哪一种 类型.的对象。 ‘ 一旦类和主机被确定,COM+将检查目标主机上的运行对象表(ROT),来确定对象是否已 经被激活。ROT是SCM的一部分,它主要用于映射在本地主机上运行的实例的任意标记。持久 对 象 应 该 在 被 载 人 的 时 候 在 本 地 的 ROT中 注 册 自 己 。 为 了 将 持 久 对 象 文 件 名 表 示 为 标 记 , COM+提供了一种标准的标记类型,名为文件标记,它将文件名封装在IMoniker接口的背后。文 件 标记既可 以 通 过 将文件名传给MkParseDisplayNameO来 创 建 ,也 可 以 通 过 调 用 AP丨函数 CreateFileMoniker()来创 建: CreateFileMoniker(): HRESULT CreateFileMoniker( [in, string] const OLECHAR *pszFileName, [out) IMoniker **ppmk); 如果持久对象已经在ROT中注册了文件标记,CoGetlnstanceFromFileO将简单地为已经在运 行 的 对 象 返 回 一 个 指 针 。 如 果 在 ROT 中 没 有 找 到 这 个 对 象 ,CO M +将 通 过 调 用 实 例 的 IPersistFilcLoadO方 法 ,创建一个新的这个文件类的实例,并且用持久映像将它初始化,如清 单5-5所示。 . 清单5-5 IpersistFile接口声明 [ Object, UUid(0000010b•0000-0000.C000-000000000046) 1 interface IPersistFile : IPersist { II called by CoGetlnstanceFromFile to initialize object HRESULT Load( (in, string] const OLECHAR * pszFileName, jin] DWORD grfMode ); II remaining methods deleted fo 「 c l a r i t y } 从文件中载人任何持久状态的同时,在本地的ROT中进行注册以确保在同一•时间每个文件 只有一个实例在运行,这是对象实现的责任,如淸单5-6所示。 清单5 - 6 加载持久状态 STDMETHODIMP Example::Load(const OLECHAR *pszFileName, DWORD grfMode) { II read in persisted object state HRESULT hr = this->MyReadStateFromFile(pszFile, grfMode);68 第二部分高级COM编程彳支巧 if (FAILEO(hr)) return hr; // get pointer to ROT from SCM IRunningObjectTable *prot = 0; hr = GetRunningObjectTable(0, &prot); if {SUCCEEDED(hr)} { // create a file moniker to register in ROT IMoniker *pmk = 0; hr = CreateFileMoniker(pszFileName, &pmk); if (SUCCEEDED(hr)) { // register self in ROT hr = prot->Register(0, this, pmk, &m_dwReg); pmk->Release(); } prot->Release(); } r e t u r n h r ; . 在CoGetlnstanceFromFileO的执行过程中,新创建的实例的IPersistFile::Load()方法将被SCM 调用 。前面的例子使用的是COM+的API函数GetRunningObjectTableO,来 得 到 进入SCM的 IRunningObjectTable接口的指针。然 后 ,它使用这个接口将标记注册在ROT中 ,这 样 ,接下来 在创建新对象时,使用相同文件名对CoGetlnstanceFromFileO进行的调用将不会失败,而是将返 回对这个对象的引用。 文件标记存在的理由有两个。一是允许对象在ROT中注册,以使CoGetlnstanceFromFileO可 以找到这些对象。二是将对CoGetlnstanceFromFileO的使用隐藏在IMoniker接口的背后,不让客 户程序知道。文件标记的BindToObject()实现过程只是简单地调用CoGetlnstanceFromFileO, 如 清单5-7所示。 清单5-7 Implementation ()的使用 // pseudo-code from 0LE32.DLL STDMETHODIMP FileMoniker::BindToObject(IBindCtx *pbc, IMoniker *pmkToLeft, REFIID riid, void **ppv) { II assume failure * p p v = 0; HRESULT hr = £_FAIL ; if (pmkToLeft == 0) { // no moniker to left MULTI_QI mqi = { &riid, 0, 0 }; COSERVERINFO *pcsi; DWORD grfMode; DWORD dwClsCtx ; II these three parameters are attributes of the BindCtx this *>MyGetFromBindCtx(pbc, &pcsiy &grfMode, &dwClsCtx); hr = CoGetInstanceFromFile(pcsi, 0, 0 , d w C l s C t x , grfMode, this->m_pS2F i l e N a m e , 1, &mqi); if (SUCCEEDED(hr)) *ppv = mqi.pltf; e l s e { II there's a moniker to the left II ask object to left for IClassActivator弟5 章 标 记 69 II or IClassFactory } r e t u r n hr; 如果给定文件标记的行为,清单5-8中是调用CoGednstanceFromFile()的函数: 清单5-8 CoGetlnstanceFromFile ( ) 函数 HRESULT GetInfo(IInf * &rplnf) { OLECHAR *pwszObject = OLESTR(“W\\server\\public\\information.expl“); MULTI_QI mqi * { &IID_IInf, 0, 0 }; HRESULT hr = CoGetlnstanceFromFile(0, 0, 0, CLSCTX_SERVER, STGM_READWRITE, pwszObject, 1, &mqi); if (SUCCEEDED(hr)) rplnf = mqi.pltf; e l s e rplnf = 0; r e t u r n hr; } could be simplified by calling CoGetObject instead: HRESULT GetCornelius(IInf * &rplnf) { OLECHAR *pwszObject = OLESTR(“\\\\server\\public\\ information.expl * ); return CoGetOb j ect(pwszOb j ect,0,1ID_IInf, (void**)&rpInf); 正如先前类标记被使用时的情况,CoGetObjectO提供的间接级别允许客户指定任意复杂的 激活策略,而无需改变一行代码。 这个API函数的替换版本CoGetlnstanceFromlStorageO, 以一个分昆存储介质的指针为输入 参 数 ,而不是文件名。 除了CLSID对主机进 行正常的重新路由(被CoGetClassObject()/CoCreateInstanceEx()使 用 ) 选择之外,CoGetlnstanceFromFileO可以使用文件的UNC主机名,重新对向文件所在主机提出请 求的激活进行路由选择。 技术上,ROT不是整台机器范围的表,而是Winstation范围的表,这意味着,在默认情况下, 不 是 所 有 的 登 陆 会 话 都 可 以 访 问 对 象 。 为 了 确 保 对 象 对 所 有 的 客 户 都 可 见 ,对 象 在调用 IRunningObjectTable::Register()时 ,应该对ROTFLAGS_ALLOWANYCLIENT标志进行设置。 5 . 2 . 2 运行对象表 运 行 对 象 表 (ROT)是在每台机器上可全局访问的表,它可以保持对处于运行状态的COM+ 对 象 的跟踪,而这种运行状态可以被标记识别。标记提供者将对象注册在表中,增加对象的引 用次数。在对象被删除之前,它的标记必须从表中释放。 IROTData接口是由标记实现的,以使ROT能够对标记进行比较。ROT使 用IROTData接 n 来确定两个标记是否相等,例 如 ,当ROT要确定是否一个指定的标记被注册过时,它就必须这 样 做 。70 第二部分高级COM编程技巧 如果你止在写自己的标i己类(也就是说,编写自己的对IMoniker•接 口 的 实 现 ),或者如果你 的标记打算被注册到ROT中,你必须实现IROTData接 U 。 你其实不必使用这个接口,它是用于系统对ROT的实现的。 5 . 2 .3 项目标记 另一种被实现的标记类是项目标记,它可以用于识别包含在另一个对象中的对象。有一种 被包含的对象是嵌人复合文档的OLE对 象 。一个釔合文裆可以通过给每一个它所包含的对象分 配一个任意的名字,来识别它们,如 “myobjl” 、“myobj2” 等等。另一种被包含的对象是文档 中的用户选择范围,如电子表格中一段范围内的单元或文本文档中一段范围内的字符。由这些 组成的对象被称为伪对象,因为直到用户标定了选择范围时,它才被当作明确的对象对待。电 子表格可 能 使 用 一 个如“2C:8H” 的名字来确定一个单元格,而字处理文档可能使用书签来确定 一段范围的宇符。 当项目标记与一个可以识别外壳对象的标记结合使用时,是很有用的。一个项目标记通常 在被创建之后,将与一个文件标记结合,用以创建与对象的绝对路径等同的标记。你可以将文 件 标 记 “C:\stuff\games.doc“(用于识别外壳对象〉与 项 目 标 记 “embedobjl” (用于识别外壳对 象 内 的 对 象 )结合,形 成 一 个 新 标 记 “C:\stuff\games.doc\myobjl” ,它将惟一的识别特定文件中 的特定对象。你也可以结合更多的项目标i己,来识别更深的嵌套对象。例 如 ,如 果 “myobjl” 是一个电子表格的名字,为了识别这个电子表格对象中一个指定范围的单元,可以添加另外一 个项目标记,来 创 建 一 个 与 “C:\stuff\games.doc\myobjl\” 等同的新标记。 1.项目标记和文件标记 当一个项目标记与一个文件标记结合时,将形成一个绝对路径。这 样 ,项目标记就扩展了 文件系统路径名的概念,定义了识别个别对象的路径名,而不仅仅是文件的路径名。 项目标记与文件标记之间有一个显著的区别,文件标记中所包含的路径,对任何一个了解文 件系统的人都是有意义的,而项目标记中所包含的部分路径,只对特定的容器对象有意义。谁都 知 道 “c:\sUiff\games.dcK” 代表的是什么,而只有特定的容器对象才知道“ 1A:7F” 指的是什么。 一个容器对象不会翻译由另一个应用程序创建的项H 标记,而知道一个项目标记所指向的是哪一 个对象的惟一的容器,是那个首先将这个项目标记分配给相应的对象的容器。因此,在一个文件 的上下文环境中,由文件标记和项H 标记的结合体命名的对象源,不能只实现用于绑定文件标记 的IPersistFile,也要实现用于解析进入合适对象的项目标记名的IOleltemContainer。 2 .创建项目标记 为 了创 建 项目 标记对 象 并 为 标记 提 供者返回它 的 指针,OLE提 供了API函数Createltem Moniker()o 5 . 2 . 4 组合标记 标记的一个最有用的特性就是可以将标记结合在一起,组合标记就是由其他标记组成的新 标 记 ,它可以确定两部分之间的关系。这使你可以将绝对路径与一个对象集成在一起,而这个 对象可能给出了两个或更多个与部分路径等同的标记。你可 以 结合相同类的 标记(如两个文件第5 章 标 记 71 标 记 ),或 者 也 可 以 结合不 同类的 标记(如一个文件标记和一个项目标记)。如果你打算写自己 的标记类,那么你也可以将你的标记与文件标记或项目标记结合。组合标记的基本优势是,它 给了你一段代码来实现每个可能的、由更简单的标记组成的标记。这极大地减少了对特别定义 的标记类的需求。 因为不同类的标记可以相互结合,这提供了进入多重名字空间的能力。文件系统为作为文 件存储的对象定义了一个普通的名字空间,因为所有的应用程序都慷文件系统的路径名。类似 地 ,容器对象也为它所包含的对象定义了专用的名字空间,因为没有一个容器可以解释山另一 个容器对象产生的名字。因为文件标记和项目标记可以结合使用,这使得上述这些名字空间可 以被进入。标记的客户可以搜索所有的使用单一机制的对象的名字空间,客户只需简单地对标 记调用IMoniker::BindToObject(), 标记的 代 码 处 理余下 的 事 。对 组合标记洞用IMoniker:: GctDisplayName(), 生成一个由单独标记的显示名组成的新® 示名。 另 外 ,因为可以编写自己的标记类,组合标记允许你为对象的名字空间添加自定义的扩展 部分。 有 时 ,两个指定类的标记可以以一种特殊的方式组合。例 如 ,一个代表不完全路径的文件 标记和一个代表相对路径的文件标记,可以组成一个代表绝对路径的单独的文件标记。例 如 , 文 件 标 记 c:\stuff\music可 以 和 相 对 文 件 标 记 ..\backup\my fi 1 e.doc组 成 等 价 的 文 件 标 记 c:\stuff\backup\myfile.doc。这是一个非通配组合的例子。 另一方 面,通配组合允许任何两个标记的连接,不管它们是什么类。例 如 ,可 以 将 项 H 标 记和文件标记组合,当然,也可以是其他的任何方式。 因为非通配组合依赖于标记的类,因此是由特定的标记类的实现来定义它的细节。如果编 写了新的标记类,可以定义新型的非通配组合。与之相比,通配组合是由OLE定义的 。由通配 组合创建的标记被称为通配组合标记。 这三种标记一一文件标记、项 目 标 记 、通配组合标记一一可以共同工作,时且它们是最常 用的标记类。 标记客户应该调用〖Moniker::ComposeWith()来创建组合标记。被调用的标记在内部决定是 通 配 组 合 还 是 非 通 配 组 合 。 如 果 标 记 的 实 现 认 为 通 配 组 合 可 用 ,OLE 提 供了 A P I函数 CreateGenericComposite()来做这些事。 5 . 2 . 5 类标记 虽 然 可 以 通 过 使 用 CLSID 来 直 接 识 别 类 , 如 调 用 API函 数 CoCreateInstance()或 CoGetClassObject(),但是现在也可以通过使用一种叫做类标记的标记来进行识别。类标记是绑 定在创建它们的类的类对象七的。 通过标记来识别类的能力支持了一些很有用但也很难处理的操作。例 如 ,文件标记传统上 只支持与它们所指向的文件的类相关的类的绑定-----个Excel文件的标记只能与Excel对象的实 例绑定 ,而一个G丨F图像的标记只能与当前注册的GIF句柄的实例綁定。类标记可以通过与文件 标记的组合,指明一个它想用于操作一个文件的类。一个3D制表类的标记与一个Excel文件的标 记组合,可以得到一个新标记,它绑定于3D制表对象的实例,并且用Excel文件的内容初始化这72 第二部分高级COM编程技巧 个对象。 因 此 ,类标记在和其他类型的标记组合的时候是最有用的,如文件标记和项H标记。 类 标 记 也 可 以 与 支 持 绑 定 到 ICUssActivator接 口 的 标 记 组 合 , 当 以 这 种 方 式 组 合 时 , IClassActivator接 U 只是通过丨ClassActivator::GetClassObject,简宇地给出对类的对象和类的实 例的访问。类标记可以通过丨Moniker::IsSystemMoniker被 识别,而这个函数将在pdwMkSys中返 回 MKSYS_CLASSMONIKER。 5 . 2 . 6 指针标记 指针标i己用于识别那些只能存在于运行状态的对象,这同其他的标记类不同,其他的标记 既可以识别活动的对象,也可以识别不活动的对象。 例 如 ,假设一个应用程序有一个这样的对象,它没有被持久地存储,通 常 ,如果这个应用 程序的客户程序需要访问这个对象,可以简单地将这个对象的指针传给客户程序。然 而 ,假设 这个客户程序想要的是一个标记,因为这个对象既没有存放在文件中,也没有包含在另一个对 象里,所以它是不可能通过文件标记或项目标记被识别的。 但应用程序可以创建一个指针标记,在它的内部包含一个指针,并可以把它传给客户程序。 客 户 程 序 可 以 把 这 种 标 记 当 成 其 他 任 何 一 种 标 记 对 待 。然 而 ,当 客 户 程 序 对 指针标记调用 IMoniker::BindToObjectQ4-,标记内的代码不会去查看ROT,也不会载入任何东西。取而代之的 是 ,标记中的代码只是对标记中的指针调用了 IUnknown::QueryInterface()。 指针标k!允许处于运行状态的对象参与标记的操作,并被标记客户使用。指针标记和其他 标记类的一个重要的区别是,它不能被持久存储。如果你想保存它,调用IMonike^SaveO方法 时会返回一个错误。这意味着指针标记只在特定的场合下有用。如果需要使用指针标记,可以 调用API函数CreatePointerMonikerO来创建一个指针标记。 5 . 3 小 结 标记提供了一种使用名字汸问COM对象的方法,这对开发人员来说是个真正的好处。标记 也为COM对象提供了一种使用初始化字符串进行初始化的方法,简化了代码和例化。 在 这一章中,我们讨论了不同的标记种类,以及如何使用它们,还讨论了关于ROT的所有 重要话题。 你有几次 实现 了 对容器 (它可以通过名字识别相同类的不同对象实例)的映射呢?我知道 我已经实现过至少一百次了。标记和ROT便是为COM对象提供的、COM映射容器的实现。我曾 经遇到过几个实例,其中的ROT很吸引人。不幸的 是 ,由于使用标记和ROT编程的复杂性,你 可能还没有真正用它们做过什么。我希望这一章可以促使你在应用程序中使用标记。第6章可连接的对象 本章内容包括: •连接点 • 连接点容器 • 连接点举例 •事 件 和 VB • 各种工具实现事件时有何不同 这一章将讨论如何在VC++和VB中创建和使用事件。COM4■本质上不支持事件模型或回调函 数。在许多对象模型中,类可以拥有三种类塑的成员:属性、方 法 、事件。VB 和D elphi是提供 这些模型的、面向对象的工具。 COM+对拥有属性和方法的COM+类提供直接支持,但是没有一种COM+结构与事件等同。 这使得实现COM+事件变得非常困难。 ActiveX建议的模型是提供一个连接点,使客户程序可以在连接点注册一个事件接收器,可 以把这个事件接收器看作一个回调类,也就是为事件通知提供方法调用的类。事 件 眼 务 器 (或 源 )将为零个或更多个事件接收器对象提供通知(事件 )。图6-1给出了实现过程。 事件源定义了一个事件接收器接口,并且实现了一个或更多个连接点类和一个连接点外壳 类 。客户程序实现由事件源定义的事件接收器接口,并注册这个事件接收器对象。当事件被触 发 时 ,连接点对象将调用事件接收器对象中的方法。这不是很容易理解或实现的过程。 6 . 1 连接点 连接点类是IConnectionPoint接口的一个实现。清单6-1给出了IConnectionPoint接口的定义。74 第二部分高级COM编程技巧 清单 6-1 IconnectionPoint 接口的定义 interface IconnectionPoint : IUnknown { HRESULT GetConnectionimerface( [out】IID * piid); HRESULT GetConnectionPointContainer( [out] IConnectionPointContainer ** ppCPC) HRESULT Advise([in 】 IUnknown * pUnkSink, [out] D W O R D * p d w C o o k i e ) ; HRESULT Unadvised in 】 DWORD dwGookie); H R E S U L T EnumConnections( [out] IEnumConnections ** p p E n u m ) ; IConnectionPoint::GetConnectionInterface()方法将返回事件接收器接口的接 U 标 识 符 (1ID )0 IConnectionPoint::GetConnectionPointContainer()方法将返回连接点容器对象。 当客户程序想要 引起事件注意时,它将创建由服务器定义的事件接收器对象,并以一个指向事件接收器对象的 IUnknown指针作为输入参数,调用丨ConnectionPoint::AdviseO方法。 这个结构使得一个连接点接收器可以由多个连接点服务,同样,多个连接点接收器可以由 一 个 连 接 点 服 务 。为 了 枚 举 出 所 有 的 由 一 个 连 接 点 服 务 的 事 件 接 收 器 ,客 户 程 序 可 以调用 IConnectionPoint::EnumConnections()方法。返回的IEnumConnections对象可以被用来枚举所有 的需要通知的事件接收器。清单6-2给出了IEnumConnections接口的定义。 清单6 - 2 丨EnumConnection接口的定义 interface IEnumConnections : IUnknown { HRESULT Next([in] ULONG cConnections, 【outI CONNECTDATA * rgcd, [out] ULONG * lpcFetched); HRESULT Skip(【inI ULONG celt); HRESULT Reset(); HRESULT Clone( [out] IEnumConnections * ppenum); 对IEnumConnections::Next()方法的调用将返回一个CONNECTDATA结 构 。清单6-3给出了 CONNECTDATA结构的定义。 清单6-3 CONNECTDATA结构的定义 typedef struct tagCONNECTDATA { I U n k n o w n * p u n k ; DWORD dwCookie; } CONNECTDATA; CONNECTDATA结构中的cookie是在调用IConnectionPoint:: AdviseO方 法时返回的值。 IUnknown接口指针可以被连接点用来査询事件接收器接口,以及对事件接收器对象进行方法调 用 。(这个对事件接收器的方法调用,是在客户程序中实际被触发的事件。)弟(5章 可连接的对象 75 6 . 2 连接点容器 服 务 器 可 以 实 现 IConnectionPointContainer类 来 分 配 连 接 点 对 象 。 清 单 6-4给出了 IConnectionPointContainer 接 口的定义。 清单6-4 丨ConnectionPointContainer接口的定义 interface IConnectionPointContainer : IUnknown { HRESULT EnumConnectionPoints( lout] IEnumConnectionPoints ** ppEnum); HRESULT FindConnectionPoint([in] REFIID riid, [ o u t 】 IGormectionPoint ** ppCP); IConnectionPointContainer::FindConnectionPoint()方法将返iHlIConnectionPoim对 象 ,它为特 定的接口标识符服务。 IEnumConnectionPoints对象属于标准的枚举类。清单6-5给出 f IEmimComiecticmPohits接 口 的定义。 清单6-5 IEnumConnectionPoints接口的定义 interface IEnumConnectionPoints : IUnknown { HRESULT Next([in] ULONG cConnections, (outl IConnectionPoint ** rgcd, (out] ULONG * pcFetched); HRESULT Skip([in 】 ULONG celt); HRESULT Reset(); HRESULT Clone([out] IEnumConnectionPoints * ppenum); } 6 . 3 连接点举例 为了说明如何使用连接点,我将使用ATL的IConnectionPointContainerImp丨类。这个实现过 程从服务器端的角度,给出了使用连接点所必需的大多数功能性要求。 这个示例由三部分组成:触发 事 件 的 事 件 服 务 器 (源 )、捕获事件的事件接收器以及一个控 制程 序。事件接收器和控制程序将存在于客户进程,而事件源将存在于服务器进程。这很好地 模拟了在 大多 数程 序 中具有 代表 性的 事件接收器一 事件源模 型。这 个示例可以 在CD-ROM的 ComEvents子目录下的Chapter06目录中找到0 我们从分析事件源的IDL开 始 (参见清单6-6)。 清单6 - 6 事件源代码的IDL // EventServer.idl : IDL source for EventServer.dll II II This file will be processed by the MIDL tool to // produce the type library (EventServer.tlb) and marshaling76 第二部分高级COM编程41巧 // c o d e . import “oaidl.idl“; import 'ocidl.idl'; [ uuid(1579753F-41A7-11D2-BEA1•00C04F8B72E7), d u a l , helpstring(“IMyEventTrigger Interface*), pointer_default(unique) ) interface IMyEventTrigger : IDispatch [helpstring(“method Fire“)J HRESULT Fire(); ( o b j e c t , uuid(15797541-41A7-11D2-BEA1-00C04F8B72E7), d u a l , helpstring(“IMyEventSink Interface“), p o i nter一default(unique} ) interface IMyEventSink : IDispatch { (helpstring(“method Hello')] HRESULT Hello(); uuid(15797532-41A7-1102-BEA1 •00C04F8B72E7), version(1.0), helpstring(“EventServer 1.0 Type Library“) 1 library EVENTSERVERLib { importlib(“stdole32.tlb“); importlib(“stdole2.tlb“); UUid(15797540-41A7•11D2-BEA1-00C04F8B72E7), helpstring(“MyEventTrigger Class*) ] c o c l a s s MyEventTrigger { [ d e f a u l t 】 interface IMyEventT rigger; (default, source) interface IMyEventSink; 我创建了两个接U ,一个用于触发事件,另一 个用 于捕获事 件(事件接收器 )。每个接口都 有一个方法,一个是触发方法,另一个是接收方法。当事件源中的触发方法被调用时,事件接 收器中的接收方法也将被调用。 在coclass的定义中,包括了事件接口的属性[default,source】。这意味着这个接口是事件的默$ 6 t 可连接的对象 77 认接口。添加Uource】属性标记接口对于事件接收器接口来说是很車:要的。像Delphi和VB这样的 语言就是使用[source】属 性,把事件嗣方法和属性区分开来。 下 面 ,看一下清单6-7中触发器类的声明。 清单6 - 7 触发器的类声明 // MyEventTrigger.h : Declaration of the CMyEventTrigger #ifndef _MYEVENTTRIGGER_H_ #define ~MYEVENTTRIGGER“Hl ^include 'resource.h“ II main symbols iiiiiiiiiiiiiiiiiiiiiiiinuiiiii/iiiiiiinniiiinniiiiiniiiiiiiiiniiiiii // CMyEventTrigger class ATL_NO_VTABLE CMyEventTrigger : public CComObjectRootEx, public CComCoClass, public IConnectionPointContainerImpl, public IDispatchImpl, public IconnectionPointImpl { public: CMyEventTrigger() DECLARE_REGISTRY_RESOURCEID(IDR_MYEVENTTRIGGER) BEGIN_COM_MAP(CMyEventTrigger) COMllNTERFACE_ENTRY(IMyEventTrigger) COMjNTERFACE~ENTRY (IDispatch) COMllNTERFACElENTRY_IMPL(IConnectionPointContainer) END_COM_MAP() BEGIN_CONNECTION_POINT_MAP(CMyEventTrigger) CONNECTION_POINT_ENTRY(IIO_IMyEventSink) END_CONNECTION_POINT_MAP() II IMyEventTrigger p u b l i c : STOMETHOO(Fire ) ( ); #endif / /_MYEVENTTRIGGER_H_ 注意,在类的声明中我已经实现了IMyEventTrigger接 口。为了触发事件,这个接口将在以 后 被 调 用 。 为 了 使 用 ATL中 实 现 的 所 有 默 认 模 板 代 码 ,继 承 了 IConnectionPointlmpl和 IConnectionPointContainerlmpl。入口CONNECTION_POINT—ENTRY()可以告诉IConnection PointContainerlmpl连接点处理的是哪一个事件接收器接口。 触发器类的定义很简单,因为只需要实现一个方法(参见淸单6-8)。78 第二部分高级COM编程技巧 清单6 - 8 触发器的类定义 // MyEventTrigger.cpp : Implementation of CMyEventTrigget ^include “stdafx.h“ #include 'EventServer.h“ #include “MyEventTrigger.h“ iiiiuiiniiiiiiniiniuimiiii/unniiiiiniiiiiimniiiiiiiiunii II CMyEventTrigger STDMETHODIWP CMyEventTrigger::Fire() { // T000: Add your implementation code here IUnknown ** ppunknown * m_vec.begin(); if (*ppunknown != NULL) { IMyEventSink * pmyeventsink = (IMyEventSink * 广 ppunknown; pmyeventsink->Hello(); } return S_0K; > “ 包含在连接点中的IUnknown指 针 ,被保存在一个名为m__vec的CComDynamicUnkArray容器 成员变量中。可以通过使用与清单6-9中相似的代码操作这个数组。 清单6-9 导航CComDynamicUnkArray容器类 IUnknown** ppunknown = m_vec.begin{); while (ppunknown < m_vec.end()) { if (*ppunknown != NULL) { IMyEventSink * pmyeventsink = (IMyEventSink *)*ppunknown; (♦pmyeventsink)->Hello(); } ppunknown++; _____}_________________________________________________________________________________ __________________________________ 这就是整个事件服务器。没有很多的代码,因为IConnectionPointlmpl和IConnectionPoint Containerlmpl模板为你做了所有的事情。 事件接收器 事件接收器对象是一个进程内服务器。这个服务器没有新的接口,只有一个COM+类 (参见 清单 6-10)。 . 清单6 - 1 0 事件接收器对象IDL II EventClient.idl : IDL source for EventClient.dllII II This file will be processed by the MIDL tool to第6章 可连接的对象 79 // produce the type library (EventClient.tlb) and marshaling // code. import “oaidl.idl“; import ■ocidl.idl*; UUid(7BBCFB61-41AB-11D2-BEA1-00C04F8B72E7), version(1.0), helpstring(“EventClient 1.0 Type Library“) 1 library EVENTCLIENTLib { importlib(*stdole32.tlbM); i m p o 「tlib(_stdole2.tlir); importlib( ■ •. WEventServerWEventServer.tlb*); uuid(7BBCFB6F-41AB-11D2-BEA1-C0C04F8B72E7), helpstring(“MyEventSink Class“) ] coclass MyEventSink { [default] interface IMyEventSink; 在 IDL 中 需 要 注 意 的 是 ,我 在 事 件 接 收 器 类 中 ,输 入 了 事 件 源 的 类 型 库 ,并且实现了 IMyEventSink 接 口 0 事件接收器对象的类的声明(参见清单6-11 ) 和 定 义 (参见清单6-12)都很简单。 清单6 - 1 1 事件接收器对象类声明 // MyEventSink.h: Definition of the MyEventSink classIIiiinimuiiiuiiiiiui/iiiun/iiiiimiiiiiiiiiiiiniiuiii #if 丨defined(AFX_MYEVENTSINK_H_IMCLUDED— ) #define AFX MYEVENTSINK H INCLUDED^ • — — a m #if _MSC^VER >= 1000 #pragma once #endif // _MSC_VER >= 1000 #include “resource.h' // main symbols #include “ • .\\EventServer\\EN/entSe「ver.h“ ///////“///////////////////////////////////////////////////// II MyEventSink class MyEventSink :80 第二部分高级COM编程技巧 public CComObjectRoot, public CComCoClass , public IOispatchImpl { p u b l i c : MyEventSink() {} BEGIN_COM_MAP(MyEventSink) COM_INTERFACE_ENTRY(IDispatch) COM_INTERFACE=ENTRY(IMyEventSink) END_COM_MAP() //DECLARE_NOT^AGGREGATABLE(MyEventSink) II Remove the comment from the line above if you don't want II your object to support aggregation. DECLARE_REGISTRY_RESOURCEID(IDR_MyEventSink) // IMyEventSink p u b l i c : STDMETHOD(Hello ) ( ); # e n d i f II 丨defined(AFX_MYEVENTSINK_H_INCLUDED_} 清单6 - 1 2 事件接收器对象类定义 // MyEventSink.cpp : Implementation of CEventClientApp and // DLL registration. ^include “stdafx.hw ^include “EventClient.h' //include “MyEventSink.h“ #include ^include _.. \\EventServer\\EventServer__i. c“ iimiiiiiin/iiiiniin/uuiiifniiiiiiiinmiimumuiiii STDMETHODIMP MyEventSink::HellO() { II TODO: Add your implementation code here std::cout « 'The event was fired. Hurrayr « std::endl; r e t u r n S 一OK; 定义和声明都是标准的双重接口的实现。它们继承了IDispatchhnpl ATL类模板,并且实现 了 接 口 (IMyEventSink)的所有方法。在这种情况下,当事件被激活时,一个方法将向标准输 出发送一条消息。在声明和定义中惟一不寻常的部分是,事件服务器的头文件 (EvemServerh) 出现在类的声明之前,而事件服务器类型库的源文件(EventServerjx ) 出现在类的定义之前。 这些文件包含了事件接收器接口的C++定 义 (在头 文件 中 ),以及事件接收器接口的GUID (在 源 文 件 中 )。$ 6 t 可连接的对象 81 所有的动作都位于控制程序(参见淸单6-13)。 清 单 6 - 1 3 事 件 控 制 程 序 #include ^include #include //include “EventServerWEventServer.h“ #include 'EventServerWEventServer.i.c“ char s(1024 ]; HRESULT GetUnknown(WCKAR * strProgID, IUnknown ** ppunknown) { CLSID clsid; HRESULT hresult = ::CLSIDFromProgID(strProgID, &clsid); if (FAILED(hresult)) { II CLSIDFromProglD failed std: :cout « “CLSIDFromProglD failed = “ « _ltoa(hresult, s, 16) « std: :e n d l ; ATLASSERT(FALSE); return hresult; hresult = ::CoCreateInstance(clsid, NULL, CLSCTX_SERVER, IIDIUnknown, (void **)ppunknown); if (FAlLED(hresult)) { II CoCreatelnstance failed std::cout « “CoCreatelnstance failed = “ « _ltoa(hresult, s, 16) « std::endl; ATLASSERT(FALSE); return hresult; return S_0K; HRESULT Getlnterface(IUnknown * punknown, REFIID riid, IUnknown ** ppunknown) { HRESULT hresult = punknown->QueryInterface(riid, (void ppunknown); if (FAILED(hresult)) { II Querylnterface failed std::cout « “Querylnterface failed = “ « _ltoa(hresult, s, 16) « std::endl; ATLASSERT(FALSE); return hresult; } return S OK:82 第二部分高级COM编程技巧 i n t m a i n ( ) { //«==============:=============*== II Start the test app //;==5s:=======:===:==:s=============== ::CoInitialize(NULL); std::cout « “Start“ « std::endl; 〃 一 := = =一 := 一 ==:= = =一 ::= =一 == II initialize all my interfaces IUnknown * punknownserver = 0; I U n k n o w n * punknownclient = 0; IMyEventSink * pmyeventsink = 0; IConnectionPointContainer * pmyconnectionpointcontainer IConnectionPoint * pmyconnectionpoint = 0; IMyEventTrigger * pmyeventtrigger = 0; “ ====… ========… ====================; II Get my server IUnknown HRESULT hresult = GetUnknown(L'MyEventTrigger.MyEventTrigger.1•, &punknownserver); if (FAILED(hresult)) { II GetUnknown failed std::cout « “GetUnknown failed = _ « _ltoa(hresult, s, 16) « std::endl; ATLASSERT(FALSE); return hresult; :==: =====: : =============== II Get my client IUnknown //====================================== hresult = GetUnknown(LBEventClient.MyEventSink.1', &punknownclient); if (FAILED(hresult)) { II GetUnknown failed std::cout « “GetUnknown failed = • « ltoa(hresult, s, 16) « std::endl; ATLASSERT(FALSE); return hresult; 〃 -:=:=:==:=:===::====:===:::=… =:== II Get my server interface 〃===========… … :=:==: =:===: : ===== hresult = GetInterface(punknownserver IID—IConnectionPointContainer, (IUnknown **)^pmyconnectionpointcontainer) if (FAILED(hresult)) { II Getlnterface failed std::cout « “Getlnterface failed = •弟6 章 可连接的对象 83 « _ltoa(hresult, s, 16) « std::endl; ATLASSERT(FALSE); return hresult; } //======«=======::==========5:-===* ====!=== II Get the container hresult = pmyconnectionpointcontainer->FindConnectionPoint( IID—IMyEventSink, &pmyconnectionpoint); if (FAILED(hresult)) { II FindConnectionPoint failed std::cout « ■FindConnectionPoint failed = “ « _ltoa(hresult, s, 16) « std::endl; ATLASSERT(FALSE); return hresult; 11= - - = = ^ = = ~ - II Add the advise notification 11-=-==--==-===-===^=^==============^=== DWORD dwcookie = 0; hresult = pmyconnectionpoint->Advise(punKnownclient, &dwcookie); if (FAILED(hresult)) { II Advise failed std::cout « “Advise failed = “ « _ltoa(hresult, s, 16) « std::endl; ATLASSERT(FALSE); return hresult; II Get my server interface I W MB MB MB mm mmm mm mm mm w m# # ^ ^ mm ^ ^ ^ mb aam w mm A ^ mm ^ mm mm mb mm ^ ^ as hresult = GetInterface(punknownserver, IID_IMyEventTrigger, (IUnknown **)&pmyeventtrigger); if (FAILEO(hresult)) { // GetInterface failed std::cout « “Getlnterface failed = H « _ltoa(hresult, s, 16) « std::endl; ATLASSERT(FALSE); return hresult; II Fire th8 event hresult = pmyeventtrigger->Fire(); if (FAILEO(hresult)) { II Fire failed std::cout « “Fire failed = “ « _ltoa(hresult, s, 16) « std::endl; ATLASSERT(FALSE);84 第二部分高级COM编程技巧 return hresult; } //===========.====================:====== II release all my interfaces //====;==;=======5=================s====== if (punknownserver != 0) { punknownserver->Release(); } if (punknownclient 1= 0) { punknownclient->Release(); } if (pmyeventsink !: 0) { pmyeventsink->Release(); } if (pmyconnectionpoint != 0) { pmyconnectionpoint->Release(); > if (pmyconnectionpointcontainer 1= fd){ pmyconnectionpointcontainer->Release(); } if (pmyeventtrigger 1= 0) { pmyeventtrigger->Release(); } //=========:=*====:=======:===:============= II E n d the t e s t a p p 〃:==-=m ====-=======: … 一 === :==: === std::cout « •End' « std::endl; ::CoUninitialize(); return 0; 我在控制程序中实现了两个功能:创建新的COM+对 象 (GetUnknownO),以及查询对象的 COM+接 口 ( GetlmerfaceO )。我不会详细描述这些函数,因为它们是标准地创建对象和查询接 U 的过程。 在mainO函数中,创建了事件服务器和事件接收器对象。需要注意的是,事件服务器对象是 在进程外创建的,而事件接收器对象是在进程内创建的。这不是COM+事件模型所必需的,怛 这是标准的实现过程。 事件源是拥有事件的典型的ActiveX控 件 ,事件接收器是OLE中需要事件通知的客户应用程 序。OLE控件可能是网格控件,客户程序可能是拥有嵌人客户区域的控件的对话。当网格控件 被双击时 ,对话框需要接收通知。对话框将实现由网格控件定义的事件接收器接口。 当对话框 调用IConnectionPoiiiLAdviseO方 法 时 ,OLE控件将得到一个指向对话框事件接收器的指针。当 网格被双击,网格控件调用对话事件接收器方法,这样就产生了通知。 为 f 调用IConnectionPoint::Advise()方 法 ,客户必须首先得到一个指向IConnection Point第6 章 可连接的对象 85 Container接口的接口指针。清单6-13中 ,对Gctlntcrface()函数 的 第 一次调用实现 了 这*点。 有 了IConnectionPointContainer接 口,我们便可以调用IConnectionPointContainer::Find Connection()方 法 ,重新得到IConnectionPoint对象。 现 在 ,有了IConnectionPoint接 口 ,便可以调用IConnectionPoint::Advise()来设置事件通知。 需要注意的是,我将一个指向DWORD值的指针传给了IConnectionPoinf/.AdviseO方 法 。这是一 个可以用于调用IConnectionPoint::Unadvise()方法来结束事件通知的cookie。 下一步是,重新得到IEventTrigger接口的接口指针。在清单6-13中 ,对GetlnterfaceO函数的 第二次调用实现了这一点。在已经有了IEventTrigger接口之后,便可以调用IEventTrigger::Fire() 方法激活事件。当事件被激活时,MyEvemSintHelloO中的代码将被调用。 运行这个应用程序,应该产生下面的命令行输出: S t a r t The event was fired. Hurray! End 不是很难。代码看起来很有意思也很复杂,但确实不需要写很多代码。惟一 的 问题是 ,这 种类型的事件接收器不能用于VB。 6 . 4 事件和VB 我在前一个例子中使用了vtable来激活事件。VB只为事件接收器自动生成代码,而不生成 任 何 vtable代 码 来 捕 获 事 件 通 知 。 这 意 味 着 ,为 了 在 VB 客 户 程 序 中 激 活 事 件 ,必须调用 IDispatch: :Invoke 方 法。 事 件 的 黑 洞 这 是 微 软 的 面 向 对 象 结 构 中 的 黑 洞 之 一 。程序员怎么才能知道用哪一个 接口激活事件呢?是调度接口还是vtable接口?如果不给模型增加约束,这显然是不可 能的。 你可以规定COM服务器必须两个接口都调用,但这将导致使用额外的客户程序代 码 ,来确保一个事件不会被接收两次。 惟一可用的约束就是,当调用事件接收器方法时,限制使用调度接口。这抹煞了所 有使用vtable的优势,并且为了使用事件模型,程序员还必须去理解自动生成的代码。 为了使用调度接口调用事件,清单6-14给出了调用IDispatd^Invoke方法的必要代码。 清单6 - 1 4 通过Idispatch接口调用事件处理器 VARIANTARG* pvars = new VARIANTARG[1] ; for (int i = 0; i < 1 ; i+ + ) { Variantlnit(&pvars[i]); IUnknown** pp = m_vec.begin(); while (pp < m_vec.end())86 第二部分高级COM编程技巧 This file will be processed by the MIDL tool to produce the type library (vbsink.tlb) and marshaling code. import -oaidl.idl“; import -ocidl.idl“; object, uuid(18C9E280•8D36-1102-9C2B•DC6F06C10000), dual, helpstring(•IMyEventSource Interface“), p o inter一default(unique} J interface IMyEventSource : IDispatch { [id(1)» helpstring(•method FireEvent')] HRESULT FireEvent(long lSeconds); >; [ uuid(18C9E280•8D36-1102-9C2B•DC6F06C10000), version(1.0), helpstring(“vbsink 1.0 Type Library“) 1 library VBSINKLib { importlib(“stdole32.tlb“); importlib(■stdole2.tlb“); [ uuid(A0281 EE1 • 8036• 11CI2-9C2B• DC6F06C10000), nonextensible, helpstring(_DIMyEventSink Dispatch Interface“) pvars(0].vt = VT_I4; pvars(0].lVal= lFlags; DISPPARAMS disp = { pvars, NULL, 1, 0 } ; IDispatch* pDispatch = reinterpret_cast(*pp); pDispatCh.>Invoke(0x1, IID_NULL, LOCALE_USER_DEFAULT, DISPATCH_METHOD, &dispt NUIlT NULL, NULL ); } P P + + I delete[] pvars; 6 . 4 . 1 重写事件源 为了服从这种事件模型,事件源中的一些代码必须重写。IMyEventSink接口必须被改写为 调度接口。清单6-15给出了新的IDL文件的内容。 清 单 6 - 1 5 新 事 件 源 代 码 的 IDL II vbsink.idl •• IDL source for vbsink.dllII IIIf //第<5章 可连接的对象 87 dispinterface OIMyEventSink { properties: m e t h o d s : [id{1)] void Timeout(); uuid(18C9E28E•8036-11D2-9C2B•DC6F06C10000), helpstring('MyEventSource Class“) I coclass MyEventSource { [ d e f a u l t 】 interface IMyEventSource; [default, source] dispinterface OIMyEventSink; 为了使事件通知过程更加显而易见,还要增加一些修改,对事件触发进行一定时间的延迟。 而 为了引人延迟,必须创建第二个线程,这样第一个线程就不会封锁客户应用程序了。清单6- 16给出了新的事件源类的实现。 清单6 - 1 6 在一个独立的线程中触发延迟事件 II MyEventSource.cpp : Implementation of CMyEventSource #include “stdafx.h“ #include “vbsink.h' #include “MyEventSource.h“ iiiiiiimiiiiiiiiiiiiiiiiuiiiiiiiiiinniiiiiniiniimiiu II CMyEventSource STDMETHODIMP CMyEventSource::FireEvent(long lSeconds) { m_lSeconds = lSeconds; D W O R D dw; if (::CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)&CMyEventSou rce::ThreadStart, this, 0, & d w ) ) { return S_FALSE; return S_0K; DWORD WINAPI CMyEventSource::ThreadStart(void * pCookie) { CMyEventSource * object = (CMyEventSource *)pCookie; Sleep(1000*object•>m_lSeconds); obj ect->Fire_T imeout(); r e t u r n 0;88 第二部分高级COM编程技巧 正如要在第7$•中讨论的,这种类型的线程被称作工作者线程。当任务在后台进行时,它使 得主线程可以继续正常地处理。在这种情况下,后台任务是CMyEventSource::ThreadStart静态方 法 ,它在激活超时事件之前,要等待一定的时间。 我本来可以通过编写像清单6-14中 那 样 的 〔\1>^6扣50111*(^::?丨代5丫6111()来调用事件接收器处 理例程,但是VC++提供了一个可替换的、更 安 全、更容易的方法来派生这些代码。使用这种方 法 ,我 可 以 将 事 件 通 知 代 码 减 少 至 一 行 ,那 就 是 CMyEventSource->Fire_Timeout(>。 而 CMyEventSource->Fire_Timeout()背后的代码是由VC++ 6.0的ATL代理程序生成器生成的。 6.4.2 ATL代理程序生成器 ATL代理程序生成器有一项功能,就是生成对所有被连接的事件接收器广播事件通知所必需 的源代码。这使得编程变得更加容易,因为有了ATL代理程序生成器后,事件源中的事件通知只 需要一行代码就可以实现。 注意 ATL代 理 程 序 生 成 器 在VC++ 6 .0中 不 再 可 用 ,它已经被集 成 在DevStudio的 ClassView窗格中。保存并编译IDL文件,这样就可以生成一个类型库:可以通过右键单 击DevStudio的FileView窗格中的IDL文件,并在弹出菜单中选择编译选项对它进行编译。 在类型库被编译之后,右键单击ClassView窗格内的事件源类,并选择弹出菜单中 的 实 现 连 接 点 (CImplement ConnectionPoint) 选项 ,出现一个实现连接点对话框,便 可以选择事件接口并单击0 K 。 _________ IDE会生成一个类似清单6-17的头文件,它将自 动生成代码,来继承新产生的代理程序类。 匕 一 为了生成事件源代理程序代码,保存IDL文件并把它 编澤成 必要 的 类型 库。在 这 种 情 况 下 ,类型库文件是 vbsink.tlb,现在,在IDE中选择项目屮的添加项目中的组 一 件和控件D 选择Developer Studio组件文件夹,然后是ATL 代理程序生成器项。选 择 插 入 并 在 弹 出 的 确 认 框 中 单 击 ―:… 一 ....\ ..-.g=.jll OK。随后将出现如图6-2的ATL代理程序生成器对话框。 _ 在对话框中,在类型库名称(Typelibrary Name ) 编 辑控件中添加带有完全路径的类型库文件名。可以使用畨略号按钮浏览类型痄。选择事件接收器 调度接U, 并单右->按 钮 ,将接口从Not Selected窗格移到Se丨ected窗格。在代理程序类型框中, 选 择 连 接 点 (ConnectionPoint),并 单 击 插 入 (Insert)按钮。代码生成器会要求一个文件名,可 以简单地接受代码生成器所提供的文件名,单 击 保 存 (Save ) 按钮。 清单6-17给出了从类型库中生成的代码。 清单6-17 IConnection Point代理句柄 ////////////////////////////////////////////////////////////// // CProxyDIMyEventSinK template class CProxyDIMyEventSink : 图6 - 2 代 理 程 序 生 成 器第6 章*可连接的对象 89 public IConnectionPointImplkArray> { p u b l i c : //methods: //DIMyEventSink : IDispatch p u b l i c : void Fire_Timeout{) { T* pT = (T*)this; pT->Lock(); IUnknown** pp = m_vec.begin(); while (pp < m_vec.end()) { if (*pp != NULL) { DISPPARAMS disp = { NULL, NULL, 0,0}; IDispatch* pDispatch = reinterpret_cast(*pp); pDispatch->Invoke{0x1, IIO_NUL l 7 LOCALE_USER_DEFAULT, DISPATCH_METHOD, &disp, NULL, NULL, NULL ); } p p + + ; } pT->Unlock(); 为了实现这个连接点模板类,直接从这个模板派生出文件源类,并将调度接口的1D添加到 连接点映射中。清单6-18给出了新事件源类的声明。 清单6-18事件源代码类 II MyEventSource.h : Declaration of the CMyEventSource #ifndef _MYEVENTSOURCE_H_ #define _MYEVENTSOURCE~H~ #include “resource.h* II main symbols #include “cpvbsink.h“ munnnimiiiiiiiiniiniiiiniuii/iiiiiimmiiitinii II CMyEventSource class ATL_NO_VTABLE CMyEventSource : public CComObiectRootEx, public CComCoClass, public IConnectionPointContainerImpl, public IDispatchImpl, public CProxyDIMyEventSink { p u b l i c : CMyEventSource()90 第二部分高级COM编程技巧 OECLARE_REGISTRY_RESOURCEID(IDR_MVEVENTSOURCE) BEGIN^COM_MAP(C M y EventSou rce) COM_INTERFACE_ENTRY(IMyEventSource) COM_INTERFACE_ENTRY(IDispatch) COM_INTERFACE_ENTRY__IMPL(IConnectionPointContainer) END_COM_MAP() BEGIN_CONNECTION_POINT^MAP(CMyEventSource) CONNECTION_POINT_ENTRY(DIID_DIMyEventSink) ENO_CONNECTION_POINT_MAP() II IMyEventSource public: STDMETH00(FireEvent)(long lSeconds); p r i v a t e : l o n g m 一l S e c o n d s ; static~DWORD WINAPI ThreadStart(void * pCookie); #endif //__MYEVENTSOURCE_H_ 我没有重写控制程序或事件接收器客户程序,而是使用VB编写客户程序,作为证明这类事 件的处理与VB兼容的证据。 6 . 4 . 3 编写VB客户程序 编写VB客户程序非常简单。创建由一个格式程序和一个类文件组成的、简单的VB可执行程 序。进入Project、References,激活包含要使用的COM对象的类型库的引用c 将清单6-19中的代 码添加到格式程序源代码中。 清单6 - 1 9 启动事件 Option Explicit Dim vbsinkcls As Class'! Private Sub Commandl_Click() Set vbsinkcls s New Classl D o E v e n t s vbsinkcls.Go End Sub 注意,Classl是类文件的名字。如果要改变类文件的名称,就也必须改变格式程序中的代码。 最后,必须创建COM对 象 ,调用FireEvent方 法 ,并且响应超时事件通知。清单6-20给出了 执行这些操作的类文件的代码。 清单6 - 2 0 调用事件触发器和处理事件通知的类 Option Explicit Dim WithEvents vbsink As VBSINKLib.MyEventSource第6 章 可连接的对象 9J Attribute vbsink.VB 一VarHelpID * -1 Private Sub Class 一Initialize() Set vbsink = New VBSINKLib.MyEventSource E n d S u b P r i v a t e Sub Class_Terminate() Set vbsink = Nothing E n d S u b Private Sub vbsink_Timeout() • MsgBox “Timeout* End Sub Sub Go() vbsink.FireEvent 10 E n d S u b vbsink对象的类型库名必须是IDL文件中定义的类型库名,而coclass类的类名也在IDL文件 中有定义。COM类在Class_InitializeO子例程中创建,而在Class_Terminate()子例程中被销毁。 Go()子例程调用COM对象的FireEvent方法,而输入的是一个代表超时事件被发送前的10秒延迟 的参数。 这个类文件最重要的部分是子例程vbsinlCTimemitO,它用于接收事件通知。当VB接收到事 件通知时,它将寻找一个与对象名、后接下划线和后接通知方法名相匹配的子例程名。 运行VB可执行程序并单击触发按钮,10秒钟后,事件通知将被送出,随后将出现一个标有 Timeout字样的消息框。就是这样。 这个最后试验的VB和ATL项目都可以在CD-ROM中找到 ,具体位置是vbsink子目录下的 Chapter06 0 录中。 6 . 5 各种工具实现事件时有何不同 下面的两小节也是试验,我认为这会对理解COM事件如何工作很有帮助。这些试验说明了 编译器在生成它们自己的事件接口上有何不同。 6 . 5 . 1 事件和VB 如果你创建了•一个VB ActiveX控件并把项目编译成OCX,你可以使用OLEView来阅读OCX 的类型库。启动OLEView,并从菜单栏选择File, View TypeLib0 选择你新建的OCX并单击 Open按 钮,ITypeLib阅读器右边窗格显示的是IDL格式类型库的内容。淸单6-21给出的是为一个 VB ActiveX控件生成的默认IDL文件。92 第二部分高级COM编程技巧 清单6-21 Visual Basic ActiveX控件的IDL II Generated .IDL file (by the OLE/COM Object Viewer) // II typelib filename: Projectl.ocx fI Forward declare all types defined in this typelib interface _UserControll; dispinterface — UserControll; I UUid(700BD9B9-8D5F-11D2-9C2B • DC 6 F 0 6 C 10 _ ), v e 「sion(l.0), h e l p s t 「ing(-Projectl■) 1 library Projectl { II TLib : OLE Automation : // {00020430 -0000•0000•C000•000000000C46} importlib(“STD0LE2.TLB“); odl, uuid(700BD9B6-8D5F-11D2-9C2B-DC6F06C10000), v e 「s i o n ( 1 . 0), h i d d e n , dual, nonextensible, oleautomation ) interface _UserControl1 : IDispatch { UUid(700BD9B7• 8D5F-11D2-9C2B• DC6F06C10_), version(1.0), noncreatable, c o n t r o l ] coclass UserControll { [default] interface _UserControl1; [default, source] dispinterface — UserControll; UUid(700BD9B8-8D5F -11D2-9C2B•DC6F06C1 0 0 0 0 ) , version(l.0), h i d d e n , nonextensible ] dispinterface — UserControll { properties: m e t h o d s : 注意,这个控件实现了两个接口,一个双重接口和一个调度接口。双重接口包含了 ActiveX第6 章 可连接的对象 93 对象的属性和方法,它的名字是在控件名字的基础上前面加一个下划线。 调度接口包含ActiveX对 象 的 事 件 ,而它的名字是在控件名字的基础上前面加两个下划线。 这是个很令人费解的约定,因为有时很难分辨出一条下划线和两条下划线的区别,但这个约定 在现在这种情况下是可以使用的,因为现在很容易区别哪一个接口丨4 于哪一个控件。 如果你对比一下清单6-22和清单6-15中的两个1 D L,会发现它们其实很相似。这是我们试图 达到的相似性,这使得VB的模块可以使用我们的事件。 6.5.2 事件和C++Builder 另一个有趣的实验是查看用Inprise的C++Builder或Delphi编制的ActiveForm的内容。清单6- 22给出了使用C++Builder创建ActiveForm时生成的默认IDL文件。 清单6-22 C++ Builder ActiveForm的丨DL // Generated .IDL file (by the OLE/COM Object Viewer)II II typelib filename: II Forward declare all types defined in this typelib interface IActiveFormX; dispinterface IActiveFormXEvents; UUid(18C9B680•8CA9-11D2-9C2B•DC6F06C10000), version(1.0), helpstring(“Act iveFormProj1 Library*) ] library ActiveFormProjl II TLib : OLE Automation : 11 {00C20430•0000•0000•C000•000000000046} importlib(_STD0LE2.TLB_); o d l , uuid(18C9B681 -8CA9-11D2-9C2B• DC6F06C1 ), version(1.0), helpstring(“Dispatch interface for ActiveForm Control“), d u a l , oleautomation 1 interface IActiveFormX : IDispatch { [id(0x00000001), propget] H R E S U L T 一stdcall Visible< (out, retval) VARIANT^BOOL* Value); I id(0x00000001), propput] HRESULT _stdcall VisibleUinl VARIANT_BOOL Value); [id(0x00000002), propget] HRESULT _stdcall AutoScroll( [out, retval] VARIANT_B00L* value); [id(0x00000002), propput] HRESULT _stdcall AutoScroll([in] VARIANT_B00L Value); p r o p g e t ] HRESULT stdcall AxBorderStyle(94 第二部分高级COM编程技巧 (out, retval] TxActiveFormBorderStyle* Value); I id(0x00000003), propput] HRESULT _stdcall AxBorderStyle( (in] TxActiveFormBorderStyle Value); [id(Oxfffffdfa), propget] HRESULT _stdcall Caption([out, retval] BSTR* Value); (id(0xfffffdfa), propput) HRESULT _stdcall Caption([in] BSTR Value); (id(0xfffffe0b), propget1 HRESULT _stdcall Color(Iout, retval) 0LE_C0L0R* Value); [id(0xfffffe0b), propput) HRESULT _stdcall Color([in 丨 OLE_COLOR Value); [id(0xfffffe00), propget] H R ESULT 一Stdcall Font( [out, retval] IFontDisp** Value); [id(0xfffffe00), propput] HRESULT _stdcall Font([in] IFontDisp* Value); [id(0xff?ffe00), propputrefJ HRESULT _stdcall Font((in] IFontDisp* Value); [id(0x00000004), propget] HRESULT _stdcall KeyPreview( [out, retval] VARIANTBOOL* Value); [id(0x00000004 ) t propput] HRESULT _stdcall KeyP「eview((in] VARIANT—BOOL Value); (id(0x00000005), propget 】 HRESULT 一stdcall PixelsPerlnch( [out, retval] long* Value); [id(0x0C000005), pro 叩 utJ HRESULT _stdcall PixelsPerInch([in] long Value); [id(0x00C00006), propget 】 HRESULT _stdcall PrintScale( (out, retval] TxPrintScale* Value); [id(0x00000006), propput 】 HRESULT _stdcall PrintScale([in] TxPrintScale Value); (id(0x00C00007), propget] HRESULT _stdcall Scaled( [out, retval] VARIANT_B00L* Value); [id(0x00000007), propput] HRESULT _stdcall Scaled(Iin) VARIANT_BOOL Value); [id(0x00000008), propget] HRESULT _stdcall Active( [out, retval] VARIANT_B00L* Value); [id(0x00000009), propget] HRESULT stdcall DropTarget( [out, retval] VARIANT_B00L* Value); (id(0x00000009), propput) HRESULT _stdcall DropTarget([in] VARIANT—BOOL Value); [id(0x0000000a), propget1 HRESULT _stdcall HelpFile([out, retval] BSTR* Value); I id {0 x 0 0 9 0 0 0 0 a ), p r o p p u t ] HRESULT _stdcall HelpFile([in] BSTR Value); (id(0x0000000b), propget1 HRESULT _stdcall WindowState( (out, retval] TxWindowState* Value); [id (0x000抑抑b ), propput ] HRESULT stdcall WindowState( [in] TxWindowState Value);可连接的对象 95 [id(0xfffffdfe), propget] HRESULT _stdcall Enabled( [out, retval] VARIANT_BOOL* Value); [id(0xfffffdfe), propput] H R E S U L T 一stdcall Enabled((in] VARIANT 一BOOL Value); (i d (0 x 0 0 0 0 0 0 0 c ), p r o p g e t ] HRESULT _stdcall Cursor( 【out, retval] short* Value) [id(0xd000000c), propput] HRESULT _stdcall Cursor([in) short Value); uu id (18C9B683 • 8CA9-11D2-9C2B • DC6F06C1 ), version(1.0), helpstring(“Events interface for ActiveFormX Control' 1 dispinterface IActiveFormXEvents { properties: m e t h o d s : [id(0x00000001)] void OnActivateO; [id(0x00000002)] void OnClick(); [id (0x0^00093)] void OnCreate(); [id(0x00000004)] void OnDblClick(); [id(0x00000005)] void OnDestroyO; [id<0x00000006)] v o i d O n D e a c t i v a t e ( ); (id(0x00000e0f)] void OnPaintO; uuid(18C9B685-8CA9-11D2-9C2B•DC6F06C10000), version(1.C), helpstring(*ActiveFormX Control “ ), c o n t r o l ) coclass ActiveFormX { (default] interface IActiveFormX; [default, source] dispinterface IActiveFormXEvents typedef [uuid(18C9B687•80A9-11D2-9C2B•DC6F06C10000 ), version(1.0)] e n u m { afbNone = 0, afDSingle = 1, afbSunken = 2, afbRaised = 3 TxActiveFormBorderStyle; typedef [uuid(18C9B688-8CA9-1102-9C2B• DC 6 F 0 6 C 1 ), version(1.0)} enu m {96 第二部分高级COM编程技巧 poNone = 0, poProportional = 1 , poPrintToFit = 2 } TxPrintScale; typedef (uuid(18C9B689•8CA9•11D2•9C2B•DC6F06C10000 ), version(1.0)] enuin { mbLeft = 0, mbRight = 1, mbMiddle = 2 } TxMouseButton; typedef [uuid(18C9B68A.8CA9-11D2-9C2B-DC6F06C10000), version(1.0)] e n u m { wsNormal = 0, wsMinimized = 1, wsMaximized = 2 } TxWindowState; 如果你直接跳过了C++Builder生成的默认属性和事件,会再次发现这个IDL文件与清单6-21 中VB的IDL文件以及清单6-15中的IDL文件几乎相同。我更喜欢C++Builder•中的命名约定,在命 名事件调度接口时,使用的是一个Events后 缀,而不是VB中下划线。 6 . 6 小结 我承认大多数开发人员对COM的连接点和事件存在一种恐惧。但我个人的经验是,可以花 一些时间,自己创建一些实现不同种类事件的COM控 件 ,这样恐惧通常会被消除。如果你还没 有适应事件,我建议花些时间创建你自己的实现事件的ATL COM项目c 同时,不要止步于创建 COM服务器,一定要创建一个实现COM服务器上事件的客户程序。第7章 COM+线程 本章内容包括: • pc线程的发展 •COM+线程类型 • COM+线程模型 •线程同步 在COM+的世界里,理解线程是个很重大的任务。因为有大约半打不同的线程种类,大量的 线程同步问题,以及不同的线程模型,你可能在读过一本关于线程的书之后,仍然只掌握了需 要理解的问题的皮毛。 线程这个主题实在是太大了,因为随着它在PC上的发展,它所创造出的东西是非常多的。 7.1 PC线程的发展 最初,PC是没有能力处理线程,甚至多任务的。你可以认为多任务就是,一个计算机系统 同时运行多于一个的任务或程序的能力。最 终 ,在PC被装入32位操作系统的时候,程序员们开 始把单个的任务或程序当作进程看待。这种说法是从UNIX世界翻译过来的,而在UNIX中 ,进 程已经存在了几十年。 Windows 95和Windows NT是在PC上成功运行多任务的平台。16位Windows也支持多任务, 但这种操作系统缺乏许多特性,这些特性是一个像UNIX这样的多任务系统应该向高端计算机提 供的特性。而这些特性中最重要的要数抢先任务切换和隔离任务失败。 如果没有抢先任务切换特性,一个任务就必须明确地调用服从操作系统控制的函数。而一 个任务可能蓄意或偶然地拒绝服从操作系统的控制,并锁住所有其他正在运行的任务。 隔离任务故障是另一个32位Windows多任务系统的重要特性。以前Windows的版本,很容易 因为在任何运行的任务中发生了故障而变得不稳定。在 16位Windows中 ,PC由于发生故障而需 重新启动的现象是很普遍的。而32位Windows中的故障虽然也会带来麻烦,但是通常不重后计 算机也是可以恢复工作的。 随着32位Windows的发展,我们也渐渐接受了一个概念,那就是在一个进程中执行多个线程。 我们不仅可以同时运行多个进程,而且还可以在每个进程内运行多个线程c 经过几年的变化,现 在 ,一个新版本的OLE也被移植到32位Windows中。这段时间里,微软 的对象计算策略开始为人所知,那就是COM,而单元线程的概念也随之产生。因为本书是关于 COM+的 ,而本章又是讨论线程的,所以主要的讨论内容是单元线程。 本章的第一段确定了三个需要着重了解的领域:线程类型、COM+线程模型和线程同步。我 们将依次讨论它们。98 第二部分高级COM编程4支巧 注 意 我 必 须 提 醒 你 的 是 ,本章是在你熟悉Win32线程的基本内容的假设下写的,如果 你不知道CreateThreadO函数 ,我建议你尽快去熟悉它,因为几乎在每段代码中我都要 使用它。 我没有将头文件包括在代码清单中,但 是 ,我将本章的所有代码都收入了CD-ROM的 Chapter07目录中。CD-ROM上的代码包括了适当的头文件。 7.2 COM+ 线程类型 线程的类型表明了线程所提供的支持的数量。这 里 ,支持的意思是线程所能实现的特性的 数量。特性数董上的差异只对程序员很重要,用户不一定真的关心,他们也不必关心,到底应 用程序中使用了什么线程类型。操作系统也完全不知道不同的线程类型。 本章我们将讨论线程池以及下列线程类型: ’ • 工作者线程。 • 消息队列线程。 • 窗口线程。 • 单元线程。 注 意 我 们 讨 论 的 每 种 线 程 类 型 ,都将包含前一种线程类型的特性,并有一个或更多额 外的特性。消息队列线程类型与工作者线程类型相似,只是还包含了消息队列。窗口线 程类型又与消息队列类型相似,只是另外包含了GUI窗口。单元线程类型与窗口线程类 型相似,只是初始化了COM库。 7 . 2 . 1 工作者线程 最基本的线程类型就是工作者线程,它通常用于模拟后台处理。你可以轻松地通过在分立 的线程中启动一系列代码,为程序提供后台处理能力。清单7-1说明了如何使用工作者线程启动 后台处理。 清单7 - 1 工作者线程 class WorkerThread { public: int OoWorK(); static DWORD WINAPI ThreadProc(void * p); W o r k e r T h r e a d (); virtual -WorkerThread(); } ; WorkerThread::WorkerThread () {} WorkerThread::-WorkerThread () {} DWORD WINAPI Worker ::ThreadProc(void *p) //////////////////////////////////////第7 章 C O A f+ 线程 99 II TODO II Add background processing code hereII std::cout « “Hello World!“ « std::endl; Sleep(1000); std::cout « “Goodbye World!“ << std::endl; return O; } int WorkerThread::OoWork() { DWORD d w ; HANDLE thread = ::CreateThread(NULL, 0, ThreadProc, NULL, 0, &dw); ::CloseHandle(thread); return 0; 注 意,DoWorkO方法在实际工作被完成之前返回调用者D 在编写PauseWork()和StopWorkO 方法时,这将带来一定的困难,线程在繁忙地工作时,一个要停止动作的简单的方法一定要给 执行中的线程发出通知,而这个线程也一定要时刻等待通知。 7 . 2 . 2 消息队列线程 线程的下一个功能性进步是消息队列线程,它的优势是你可以将Windows消息登记到线程中。 清单7-2给出了一个开始并登记消息到消息队列线程的例子。 清单7 - 2 消息队列线程 class MessageQueue { p u b l i c : static DWORD w i n a p i ThreadProc(void *p); int DoWork(); MessageQueue(); virtual -MessageQueue(); p r i v a t e : DWORD m_idThread; #define MYMESSAOE (V/W」3SER+1 抑W MessageQueue::MessageQueue() { HANDLE thread = ::CreateThread(NULL, 0, ThreadProc, NULL, 0, &m_idThread); ::CloseHandle(thread); . MessageQueue::-MessageQueue() { ::PostThreadMessage(m_idThread, WM^QUIT, C, 0); DWORD WINAPI MessageQueue::ThreadProc(void *p)100 第二部分高级COM编程彳t 巧 MSG msg; while (::GetMessage(&msg, NULL, 0, 0)) { switch (msg.message) { case MYMESSAGE: 111II111U11111111 III 11111111111111111 II T O D O II Add background processing code hereII std::cout « “Hello World I“ « std::endl; Sleep(1O00); std::cout « 'Goodbye Worldr « std::endl; d e f a u l t : b r e a k ; } ; } ; r e t u r n 0; > int MessageQueue::Dowork() { :iPostThreadMessagetm^idThread, MYMESSAGE, 0, 0); r e t u r n 0; 消息通过调用PostThreadMessageO函数被登记到消息队列线程。你可以在创建线程时得到 一个线程ID,之后就可以使用这个ID 向线程登记消息了。 消息队列线程使用GetMessageO函数从消息队列得到消息。很可能你以前已经使用过 GetMessageO函数,为一个GUI对象得到Windows消息。在这种情况下,你已经指定了NULL窗 口句柄。如果你指定的是非NULL窗口句柄,Windows将只为指定的窗口返回消息。 DoWork()的调用会自动与GetMessage()同步。如果你调用DoWork()三次,调用程序不会等 待任何DoWorkO调用。但是因为在收到MYMESSAGE时 ,实际工作已经完成,所以开关退出, GetMessage()被调用。如果你想让它不可见,可以从窗口过程和TranslateMessage()中去掉 W M—PAINT 句柄。 7 . 2 . 3 窗口线程 窗口线程类型只比消息队列多一个窗口,它也使用GetMessage()函数从它的消息队列中得到 消息,但是为了得到消息,线程指定了一个窗口句柄而不是线程ID 来得到消息。清单7-3给出了 一 个窗口线程的例子。注 意,窗口是可见的, 但在很多情况下,你也许想让它不可见,因为有 人可能会关闭窗口,从而使这个类变得无用。 清单7 - 3 窗口线程 class Window { p u b l i c : static LRESULT 0ALL8ACK WndProc(HWND hwnd, UINT idMessage,务7章 CCW + 钱程 JOJ WPARAM wParam, LPARAM IParam); static DWORD WINAPI ThreadProc(void * p); int DoWork(); W i n d o w ( ); virtual -Window(); p r i v a t e : DWORD m_idThread; HWND mjiwnd; } ; 一 #define MYMESSAGE (WM_USER+1M0) Window: :V/indow() { HANDLE thread = ::CreateThread(NULL, 0, ThreadProc, &m_hwnd, 0 &m_idThread); ::CloseHandle(thread); } W i n d o w : :- W i n d o w () { ::PostMessage(m__hwnd, WM_QUIT, 0, 0); LRESULT CALLBACK Window::WndProc(HWND hwnd, UINT idMessage, WPARAM wParam, LPARAM IParam) { HOC hDC; PAINTSTRUCT pS ; RECT rect; switch (idMessage) • { case WW_PAINT: hDC = BeginPaint (hwnd, &ps); GetClientRect (hwnd, &rect); DrawText (hDC, *Hello Thread“, DT_SINGLELINE 丨 DT_CENTER EndPaint (hwnd, &ps); b r e a k ; case WM^DESTROY: PostQuitMessage(0); b r e a k ; case MYMESSAGE : lumtniniiiiiiiiiiiiiiiiniiiinii II TODO II Add background processing code hereII std::cout « “Hello World!“ « std::endl; Sleep(1000); std::cout « “Goodbye World!' « std::endl; b r e a k ; d e f a u l t : return DefWindowProc {hwnd, idMessage, wParam, IParam) •1, & r e c t , D T V C E N T E R ); return («L);102 第二部分高级COM编程技巧 DWORD WINAPI Window::ThreadProc(void *p) { WNDCLASSEX wndclass ; static char szWndClassName 【丨 = “Threading Example“; wndclass.cbSize = sizeof (wndclass); wndclass.style=CS_HREDRAW 丨 CSJ/REDRAW; wndclass.lpfnWndProc = WndProc; wndclass.cbClsExtra = 0; wndclass.cbWndExtra = 0; wndclass.hlnstance = ::GetModuleHandle(NULL); wndclass.hlcon = ::Loadlcon(NULL,IDI_APPLICATION); wndclass.hCursor = ::LoadCursor(NULL, IDC_ARR0W); wndclass.hbrBackground = (HBRUSH) ::GetStockObject(WHITE_BRUSH); wndclass.lpszMenuName = NULL; wndclass.lpszClassName = szWndClassName; wndclass.hlconSm = ::LoadIcon(NULL,IDI_APPLICATION); RegisterClassEx (&wndclass); H W N D h w n d ; M S G m s g ; hwnd = CreateWindow ( szWndClassName, “The Hello Program“, WS_OVERLAPPEDWINOOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CWUSEDEFAULT, NULL, NULL, GetModuleHandle(NULL ), NULL); •(HWND *)p = hwnd; ShovWindow(hwnd , SW_SH0WN0RMAL); UpdateWindow(hwnd); while (GetMessage(&msg,hwnd,0,0)) { TranslateMessage(&msg); DispatchMessage(&msg); } return msg.wParam; int Window::DoWorK() { ::PostMessage(m_hwnd, MYMESSAGE, 0 , 0); r e t u r n 0; 可以看出这个例子与消息队列线程的不同之处在于,线程创建了一个窗口并为这个窗口得 到消息。第7章 CWV/+线程 W3 7 . 2 .4 单元线程 最后一个基本线程类型是单元线程。任何用于执行组件对象调用的线程都被配置为单元线 程。每个对象在其生存期,都 位 于 单 元 (线 程 )中。所有对这个对象的调用都在单元线程执行。 清单7-4给出了一个比较小的单元线程的例子。 清单7-4 — 个小的单元线程 class Apartment { public: static DWORD WINAPI ThreadProc(void * p); int DoWork(); A p a r t m e n t (); virtual - A p a r t m e n t (); Apartment::Apartment() {} Apartment::-Apartment() {} DWORD WINAPI Apartment::ThreadProc(void *p) { ::CoInitialize(NULL); iinitiiiiiiimuiiiiniiiiiminni II TO D O // Add background processing code hereII std::cout « “Hello World!“ « std::endl; Sleep(l000); std::cout « MGoodbye World!“ « std::endl; ::CoUninitialize(); return 0; } int Apartment::DoWork() { DWORD dw; HANDLE thread = ::CreateThread(NULL, 0, ThreadProc, NULL, 0, &dw); ::CloseHandle(thread); return 0; 注意,这种线程类型不出现消息循环或窗口。对CoInitializeO函数的调用A 动生成了它自己 的消息队列,而COM+子系统可以把对对象的调用放在这个队列中,线程在这里接收来自位于 其他线程的客户的COM+调用。这个队列在第一次需要它的时候被创建。 最初,为了与COM和COM+子系 统 通信,每个线程必须有自己的单元。这种单元被称作单 线 程 单 元 (STA)。当COM引人 多线程单元(MTA)时 ,单元线程变成了误称,也就是说,实际 上单元不再依赖于一个线程。 如果你调用CoInitializeEx(NULL,COINIT_MULTITHREADED)函数,你的线程将开始一个104 第二部分高级COM编程技巧 多线程单元= 在同一进程中创建的所有线程,可以自动获得对这个多线程单元的访问权。每个 线程仍然可以通过调用C0InitialiZe(NULL)函数 ,初始化它们自己的单线程单元,但是线程只能 存在于一个单元中(一个STA或一个MTA )。 在 一 个 进 程 中 ,一 个线程 在 可 以 使 用COM 对 象 之 前 ,必 须 先 逬 人 一 个 笮 元 (通过调用 CoInitialize(EX)() )。C0_IN1T_MULTITHREADED使线程进入—•个多线程单元,而CoInitialize() 使线程进入一个单线程单元。 选择多线程或单线程单元的利弊,将在本章后面的部分进行详细讨论。 7 . 2 . 5 线程池 线程池不 是 一种 线程类型 ,但 它却是 一种 实现方 法。换 句 话 说 ,COM使用线程池来实现 COM如何进行对单线程和多线程单元的调用。前面四种线程类型的任何一种都可以被扩展,用 以支持线程池。清单7-5给出了使用消息队列线程的线程池的例子。 清单7 - 5 工作者线程池 class Pool { p u b l i c : static DWORD WINAPI ThreadProc(void * p); int OoWork(); P o o l ( ); virtual -Pool(); private: std: :queue<0W0R0> m qidThread; #define MYMESSAGE (WM USER+1000) #define NUMTHREADS 10~ Pool::Pool() { for (int i=0;i<10;i++) { D W O R D dw; HANDLE thread = ::CreateThread(NULL, 0, ThreadProc, NULL, 0 , & d w ) ; ::CloseHandle(thread); m_qidThread.push(dw); Pool::-Pool() { while (!m_qidThread.empty()) { ::PostThreadMessage(m_qidThrea(J.front(), WM_QUIT, 0 , 0 ); m_qidThread.pop();第7 章 COAU 线程 J05 DWORD WINAPI Pool::ThreadProc(void *p) { M S G m s g ; while (::GetMessage(&msg, NULL, 0, 0)) { switch (msQ.message) { case MYMESSAGE : ////////////////////////////////////// II T O D O II Add background processing code hereII std::cout « “Hello Worldr « std::endl; std::cout « “From Thread Number: ■ « ::GetCurrentThreadId() « std::endl; d e f a u l t : b r e a k ; } ; } ; r e t u r n 0; } int Pool::DoWork() { DWORD dw = m_qidThread.front(); ::PostThreadMessage(dw, MYMESSAGE, 0, 0); m_qidThread.pop(); m_qidThread.push(dw); r e t u r n 0; 线程池的一个重要的实现就是单元线程池。在单元线程模型中,COM+分配了几个单元线程, 在那里,可以创建COM+对象。这种类型的模型可以支持多线程服务器,在这个服务器中,每个单 独的对象都是单线程的,因此,不需要考虑并发问题。我们将在下一部分详细讨论这种线程模翌。 7.3 COM+ 线程模型 下列是四种最常用的COM+线程模型的类型: • 单线程。 • 单元线程。 • 中立线程。 • 自由线程。 7 . 3 . 1 单线程服务程序 到目前为止,大多数COM+服务程序都是使用单线程服务程序这种线程模型开发的。在这种 模型中,对所有对象的所有方法调用都由相同的线程完成。单线程只有一个单元,在 那里,对 COM+服务程序的进程外调用都被串行化。每个进程外的COM+调用都被放在单元的消息队列中, 单元线程每次处理一个消息队列中的调用。这种线程模型完全消除了涉及每个对象的数据或共 享数据的任何并发问题。106 第二部分高级COM编程技巧 厂 FwT) 这种模型中的笮元通常被称为STA ( 申.线程申.元 )。意思很清楚:单线程单元中的对象的方 法只能被同一个线程调用,而且一次只能调用一个。 单线程服务程序在单元的消息队列中有一个主要的瓶颈,因为所有的调ffl都被在消息队列 中串行化;并且只有一个线程可以从消息队列得到调用,不难想象性能是如何被降低的c 另外, 消息队列这个间接过程对每个COM+方法调用都需要几毫秒的处理时间,用于放置和重新获得 队列中的消息。 为了使讨论具体化,我 们 要 花 些 时 间 来 看 -个 例 1111...................21^ 子。我们将使用a t l 来示范这个简单的单线程实例。 创 建一 个 使 用单 线程 模 型 的ATL服务程序是很简单 的。使用ATL的AppWizard创建一个ATL可执行应用 程 序 。从菜单栏中,选择Insert, New ATL Object。 r ... .“ 厂 S«»aiCwec%anPoni 在ATL的Object Wizard中 ,选择Simple Object,并单 ifeNext 按 钮 。 在 ATL的 Object Wizard^ 性 表 的 Attribute 属性页中,Threading Model一栏选择Single ( 参见图7-1 )。在ATL的Object Wizard属性表的Name 图7_丨选择线程模型 属性页中,键入为对象取的名字,并单击ok按 钮 ,即可。 可以有许多方法消除这种瓶颈。你可以创建其他的单元线程,使单独的对象可以同时运行 ( 单元线程 模型 ),或者你可以允许多个线程存在于一个单元内(Q 由线程模® ) c 7 . 3 . 2 单元线程服务程序 第二种类型的COM+服务程序,是被许多经验丰富的COM+程序员使用的单元线程服务程序。 这种服务程序类型与单线程服务程序的不同在于,COM+服务程序可以有一个或多个单元线程 ( STA )0 每个对象只存在于一个单元线程(STA)屮 ,外部的调用仍然在单元线程的消息队列中被串 行 化 。这意味着,对每个对象的数据来说,不存在并发问题。因为运行于分开的单元线程内的 对象可以共享全局数据,并且同时运行。现在只有全局数据存在并发问题。 同样的,为了把讨论与具体示例联系起来,我们将花一些时间使用ATL做一个示例。创建使 用单元线程模型的ATL服务程序,比创建一个单线程服务程序,稍微W难一些。其他的步骤与创 建单线程服务程序一样 ,除了在ATL的Object Wizard厲性表的Attribute属性 页中,Threading Model—栏选择的是Apartment ( 参见图7-1 ) , 而不是Single。另外还需要存」处改变, 必须修改 CExeModule类 ,使它继承CComAutoThreadModule模 板 类 (参见淸申7-6 )0 演单7-6 继承CComAutoThreadModule模板类 class CExeModule : public CComAutoThreadModule { p u b l i c : LONG Unlock(); DWORD dwThreadID;弟7章 COM+线程 107 HANDLE hEventShutdown; void MonitorShutdown(); bool StartMonitor(); bool bActivity; } ; 还必须修改CExeModule::Unlock()方 法 ,使它调用CComAutoThreadModule::Unlock()方法 ( 参见清单7-7 )。 清单7-7 调用CComAutoThreadModule::Unlock()方法 LONG CExeModule: *. Unlock () { LONG 1 = CComAutoThreadModule::Unlock(); if (1 == 0) { bActivity = true; SetEvent(hEventShutdown); 11 tell monitor that we transitioned to zero > r e t u r n 1; > 还必须为你的COM+类的定义添加DECLARE_CLASSFACTORY_AUTO_THREAD()宏 (参 见清单7-8 )。 • 清单 7-8 实现 AUTO_THREAD(> 类 Class ATL_NO_VTABLE CATObject : public CComObjectRootEx, public CCoraCoCIass, public IDispatchImpl public: C A T O b j e c t O OECLARE_REGISTRY_RESOURCEID(IDR>TOBJECT) DECLARE_PROTECT_FINAL_CONSTRUCT() DECLARE_CLASSFACTORY_AUTO_THREAD() BEGIN_COM_MAP(CATObject) COM_INTERFACE_ENTRY(IATObject) COMJNTERFACE^ENTRY (IDispatch) END_COM_MAP() II IATObject p u b l i c : STDMETHOD(get_Thread)(/*[out, retval]*/ long *pVal);108 第二部分高级COM编程技巧 因为有了单元线程服务程序的存在,以往的线程不安全的COM+对象仍然可以继续存在,而 不 会 在 这 些 对 象 (本来没有能力在多线程环境中维持完整性)中引起并发问题。如果你愿意花 时间去使你的COM+对象线程安全,我建议你使用自由线程模型来创建你的COM+服务程序。 7 . 3 . 3 中立线程服务程序 COM+包括了一种叫做中立线程的新线程模型,它支持在任何线程类塑上对象的执行。中立 单元线程的好处是,它不需要在中立线程中运行的对象的开发者考虑对象的线程安全问题。 虽然单元决定将哪一个线程分配给对象,但 当这些调用被分配时还要设置同步属性控制。 —个活动以相同的并发条件将一个或多个上下文环境集合起来。活动可以跨越超过一个的上下 文环 境 和 对 象 ,但尽管进程中的每个环境只属于一个活动,然而有些环境却不域于任何活动。 如果环境不属于某个活动,单元中的任何线程都可以在任何时间进入这个环境。如果单元是单 线程单元,那么就只能有一个线程可以进入。在中立单元或多线程单元的情况下,可以存在对 环境的同时访问。 7 . 3 . 4 自由线程服务程序 自由线程模型不使用单元消息队列对外部的COM+调用进行串行化。这种类型-的服务程序可 以使外部的COM+调用直接在RPC线程调用方法,这意味着,你的COM+对象可以跨越一个或多 个线程 ,这 样 ,对每个对象的数据和全局数据引入了并发问题。 创建一个自由线程COM+服务程序几乎与创建单线程COM+服务程序一样简单。其他的步骤 与 创 建 单 线 程 服 务 程 序 一 样 ,除 了 在 ATL的Object Wizard属 性 表 的 A ttrib u te厲 性 页 中 , Threading Model一栏选择的是Free ( 参见图7-1 ),而小是Single。另一 个惟一的改变是在你的预 编译头文件 中 ,你必须取消对常量_ATL_APARTMENT_THREADED的 定 义 ,并且定义常量 _ATL_FREE_THREADED ( 参见清单7-9 )。 清单7-9 定义_ATL一FREEJTHREADED II stdafx.h : include file for standard system include files, II or project specific include files that are used frequently, II but are changed infrequently #if !defined(AFX_STDAFX_H) ^define AFX_STDAFX_H #lf JUSCJ/ER > 1000 #pragma once #endif // _MSC_VER > 1 0 0 0 #define STRICT #ifndef _WIN32_WINNT #define :WIN32_WINNT 0x0400 # e n d i f //#define _ATL-APARTMENT_THREADED #define _ATL_FREE_THREADED #include //You may derive a class from CComModule and use it if you want to弟7章 OM/+残程 109 I/override something, but do not change the name of _Module class CExeModule : public COomModule { p u b l i c : LONG Unlock(); DWORD dwThreadIO; HANDLE hEventShutdown; void MonitorShutdown(); bool StartMonitor(); bool bActivity; >; extern CExeModule —Module; #include 11 { {AfrX_INSERT_LOCATION} } II Microsoft Visual C++ will insert additional declarations immediately II before the previous line. # e n d i f II !defined(AFX_STDAFX_H) 自由线程COM+服务程序,不会有前两种模型的瓶颈。如果你在意性能,而且愿意花额外的 时间来使你的对象线程安全,自由线程COM+服务程序是你的最佳选择。 CD-ROM的Chapter07目录中有三个COM+本地服务程序。单线程服务程序被包含在单线程 项目 中 ,单元线程服务程序在单元线程项目中,而自由线程服务程序则在自由线程项目中。每 个服务程序都有一个含有一个只读属性的对象,这个属性将返回正在执行的服务程序线程的线 程ID。还有一个项H 叫做Controller,也在CD-ROM上 ,它创建并调用每种类型服务程序的方法 ( 参见淸单7-10)。 清单7-10调用不同的线程服务程序 CRITICAL_SECTION CS; DWORD WINAPI ThreadProc(void *p) { • Automation * object = (Automation *) p; long 1 = object >Get(L“Thread').lVal; ::EnterCriticalSection(&cs); std::cout « “Thread ID = _ « 1 « *\t*; ::LeaveCriticalSection(&cs); r e t u r n 0; int main(int argc, char* argv[1) { ::CoInitializeEx(NULL, C0INIT_MULTITHREADE0 ); { std::vector v; i n t i=C; for (i=0;i<10;i++) { Automation object; object.CreateObject(L'Singlethreaded.STObject“); v .push_back(obj ect);110 第 二 部 分 高级COM 编程技巧 for (i=0;i<1O;i++) Automation object; obj ect.CreateObject(L“Apartmentthreaded.ATObject_) v.push back(object); } for (i=0;i<10;i++) { Automation object; obj ect.CreateObj ect(L“Freethreaded.FTObj ect“); v .push_back(obj ect); ::InitializeOriticalSection(&cs); std::vector::iterator j = v.begin(); for (;j!=v.end();j++) { D W O R D d w ; HANDLE ththread = ::CreateThread(NULL, 0, ThreadProc, (void *)(Automation *)j, 0, &dw); ::CloseHandle(thread); } j = v.begin(); for (;j!=v.end();j++) { D W O R D dw; HANDLE thread = ::CreateThread(NULL, 0, ThreadProc, (void *)(Automation *)j, 0, &dw); ::CloseHandle(thread); > Sleep(10000); ::DeleteCriticalSection(&cs); } ::CoUninitialize(); r e t u r n 0; } 如果运行ControUer应用程序,你将会注意到返回了大约20个左右的线程ID。在单线程服务 程序中的所有对象都返回相同的线程ID, 而在单元线程服务程序中的每个独立的对象也总是返 回相同的线程ID,但是不同的对象通常有不同的线程ID。在自由线程服务程序中的对象返回的 是随机的线程ID。 注意我不得不在自由线程服务程序的方法调用中添加sleep调用,因为方法返回得太快, 以至于在处理调用时总是用同一个线程。 可以进行一个有趣的实验,让创建对象的过程循环2到3次 ,这 样 做 ,你便可以对每种类型 的服务程序返回什么样的线程ID有一个认识了。 自由线程对象不能存在于单线程单元中,它 们 存 在 于 进 程 的 多 线 程 单 元 (MTA)中。在 MTA中 ,一个或多个线程共享一个公用的单元,但在任何进程中只可以有一个MTA。另外,一 个存在MTA的进程还可以包括一个或多个STA,这使得程序员可以在MTA中创建新的线程安全第7 章 COM +线程 111 的COM类 ,而在分丌的STA中使、用已有的线程不安全的COM类 。 因为自由线程对象位于MTA中 ,每个对对象的调用都可以使用不同的线程进行处理,也就 是说,两个或更多个线程可以同时处理对相同对象的多个调用。因此,在MTA中的对象通常存 在并发问题,这是必须解决的问题。在关于线程同步的部分中,将讨论一些有助十编程的内容, 这些内容可以帮助你解决并发问题。 7 . 4 线程同步 从服 务 程 序角度看 ,在你 确定 了 要 使 用 的线程 模 型 之后,必 须要考虑并发问题。事 实 上 , 如果你选择单线程模沏,就不必考虑任何的同步问题。如果你选择的是单元线程模型,就必须 为全局数据考虑并发问题。如果你选择自由线程模型,就必须既为每个对象的数据也为全局数 据考虑并发问题。 但什么是每个对象的数据,什么又是全局数据呢?每个 对 象 的 数 据 仅 对^前 对 象 是局部 而 言的,这类数据中最常见的就是非静态C++类的成员。所有被分配在堆栈中的局部数据也是这种 类型的数据。所有除了每个对象的数据之外的数据,都是全局数据。但这个定义+ 是 很 完 整 , 有几个例外情况。 例 如,不使用COM+就可以使非静态C++类的成员数据直接被外部线程使用。这种类型的数 据尽管是属于每个对象的数据,但还是必须当成全局数据看待,因为可能会有一些STA拥有对这 种数据的访问权。 • 7 . 4 . 1 线程局部存储 线程局部 存 储(TLS)是一种特殊的存储类型,它限制对一个当前线程的访问。如果只有一 个线程有对线程局部存储的访问权,是不可能发生并发问题的。有了TLS,便可能使每个线程与 一定数M 的内存关联。因 此 ,每个线程都有只能被它自己访问的存储空间。对于线程局部存储 的任意一个实现来说,它可能会也可能不会缓解并发问题。并发问题在有一个线程局部存储的 实现的时候是否存在,是很难确定的。有时最好假设所有的线程局部存储都是全局数据,并消 除所有可能的并发问题。这可能会给你的应用程序带来瓶颈。如果瓶颈确实存在,我建议你重 新设计这个应用程序来消除并发问题。 会提出的一个问题可能是,对自由线程对象总是有相同的线程环境的假设,这显然是一个 不正确的假设。 7 . 4 . 2 消除并发问题 Windows API有许多函数可以用于消除并发问题。这些结构中最基本的是临界区。其他的并 发结构包括:互斥体、信号和事件。 1.临界区 这是并发控制的最小限度的实现,淸单7-11给出了一个使用临界区对象的示例。112 第二部分高级COM编程技巧 请单7 - 1 1 使用临界区 class Pool { p u b l i c : static DWORD WINAPI ThreadProc(void * p ) ; int DoWork(); P o o l 。; v i r t u a l - P o o l ( ); p r i v a t e : static CRITICAL_SECTION sm_csToProtectCout; std::queue m_qidThread; ^define MYMESSAGE (WM_USER+1000) ^define NUMTHREADS 10 CRITICAL SECTION Pool::sm csToProtectCout: Pool::Pool() { ::InitializeCriticalSection(&sm_csToProtectCout); for (int i=0;i<10;i++) DWOF HANC )RD d w ; JDLE thread = ::CreateThread(NULL, 0, ThreadProc, NULL, € ::CloseHandle(thread); m_qidThread.push(dw); Pool::-Pool() { while (!m_qidThread.empty()) { ::PostThreadMessage(m_qidThread.front()f WM_QUIT, 0, 0); m qidThread.pop(); } ::DeleteCriticalSection(&sm_csToProtectCout); DWORD WINAPI Pool::ThreadProc(void *p) { M S G m s g ; while (::GetMessage(&msg, NULL, 0, 0)) { switch (msg.message) { case MYMESSAGE: IUIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII II T O D O II Add background processing code hereII ::EnterCrit icalSect ion(Asm^csToProtectCout); std::cout « “Hello Worldl from Thread Number &dw);第7章 COM+ 线程 113 « ::GetCurrentThreadId() « std::endl; ::LeaveCriticalSection(&sm_csToProtectCout); d e f a u l t : b r e a k ; r e t u r n 0; int Pool::DoWork() DWCRD dw = m_qidThread,front(); ::PostThreadMessage(dw, MYMESSAGE, 0 , 0); m_qidThread.pop(); m_qidThread.pusn(dw); r e t u r n 0; 清单7-11中给出的代码与前面清单7-6中给出的线程池相似。为了防止多个线程同时对com 进行写操作,将emit例程封闭在临界区中,来消除任何并发问题。 还有一个并发问题我没有提到,做为例子: void Function( void ) { P o o l p; p.DoWork(); p.DoWork(); p.DoWork(); } 很可能发生的事情是,如果DoWorkO的运行时间较长,那么p很可能在会话过程结束之后和 最后一个DoWorkO结束之前被销毁。开始和结束多线程代码总是很难完美地实现。 如果你在适当位置没有放临界区,便运行清单7-12中的代码,将在标准输出端产生某些可怕 的 输 出 结 果 (参见清单7-13)。这是因为在emu命令的中间,独立的线程正在被换出。之后,下 一个线程在前一个线程还没有完成之前,便幵始输出结果。 清单7 - 1 2 线程安全检査 int main(int argc, char* argv[]) { Pool thread; S l e e p ( l 0 抑 ); for (int i=0;i<20;i++) { thread.DoWork(); > Sleep(2000); r e t u r n 0; 清单7 - 1 3 异步输出 HelloH HHeHWHeeHleHoelHlelHlerllellelllololollold o 1 ol o!W WoW oW oWo oW oWfr orWroWrorlrLolrolrodldrdlrdlmtdllldlld I d Id ITf fit If hrfr rf rfroroforforetn114 第二部分高级COM编程故巧 如果你在适当位置放上临界区,再运行清单7 - 1 2 中的代码,输出结果将更易读一些(参见清 单 7 - 14)。 清单7 - 1 4 同步输出 Hello World! from T h r e a d N u m b e r : 4 9 2 Hello Worldl from Thread Number: 3 5 3 Hello World! from Thread Number: 4 6 5 Hello World! from T h r e a d N u m b e r : 5 1 8 H e l l o W o r l d ! f r o m Thread Number: 4 5 0 Hello World! from Thread Number: 5 1 0 H e l l o W o r l d ! f r o m T h r e a d N u m b e r : 4 3 5 H e l l o W o r l d ! f r o m Thread Number: 4 1 6 Hello World! from Thread Number: 6 9 Hello world! f r o m Thread Number: 4 5 2 H e l l o W o r l d ! f r o m Thread Number: 4 9 2 H e l l o W o r l d ! f r o m T h r e a d N u m b e r : 3 5 3 H e l l o W o r l d ! f r o m Thread Number: 4 6 5 H e l l o W o r l d ! f r o m Thread Number: 5 1 8 H e l l o W o r l d ! f r o m Thread Number: 4 5 0 H e l l o W o r l d ! f r o m Thread Number: 5 1 0 H e l l o W o r l d ! f r o m T h r e a d N u m b e r : 4 3 5 H e l l o W o r l d ! f r o m Thread Number: 4 1 6 H e l l o W o r l d ! f r o m Thread Number: 69 H e l l o W o r l d ! f r o m T h r e a d N u m b e r : 4 5 2 临界区对象可以看成一个进程或是线程局部对象,丙此 ,它将只限制对与它自己的上下文 环境相同的资源的汸问。如果临界区对象被当作非单一类的非静态成员,它将不能被用于限制 对全局对象的访问。如果临界区是全局对象,它就可以被用于限制对非单一类的非静态成员的 访 问 ,但是临界区很受限制,因为它将负责防范对数据成员的每一个实例的当前汸问。 注 意 “ 临界区很受限制” 是什么意思呢?创建一个临界区并使用它解决多于一个的并 发问题是很容易的,但是这可能在临界区中造成一个瓶颈。有时候,应该创建多个临界 区,每个临界区解决一个并发问题,这要比多个对全局资源的并发访问问题共用一个临 界区好得多。 omrmorrooa m o mo mdT TmT mT hTh d a da d「N NdN dN :uNu uN uN mumN r :r :3 5:3 :5 252 13 03902265567 Hello World! from Thread Number: Hello World! from Thread Number: Hello World! from Thread Number: Hello World I from Thread Number: Hello world! from Thread Number: Hello World! from Thread Number: Hello World! from Thread Number: Hello World! from Thread Number : Hello World! from Thread Number: Hello World 1 from Thread Number: 3 2 9 3 1 6 rhuereherhermaearaera( bm7ebemebmeb2rerbrebr( T N r h r T r h T Mmu4bmbubmi 8 5 3 8 2 t h T mul 2 5 ; 4 7 5 0 6 5 0 2 3 5 8 3 7 3 2 5 8 5 5 2弟7 章 C 0 M + 线程 1]5 如果你继承了CComCriticalScction或CComAutoCriticalSection类 ,ATL会在你的类中自动 实现一个临界区对象。你的所有ATL对象都使用关联来实现CComAutoCriticalSection对象。你 的ATL对象中有Lock()和Unlock()两个方法,它们被重新定向到由CComAutoCriticalSection类派 生的成员对象。清单7-15给出了CComObjectRootEx类的 定义,所有的ATL对象都是从它派生出 来 的。 清单7-15 CComObjectRootEx类的定义 template class CComObjectRootEx : public CComObjectRootBase { p u b l i c : typedef ThreadModel _ThreadModel; typedef _ThreadModel::AutoCrit icalSect ion _CritSec; typedef CComObj ectLockT<_ThreadModel> ObjectLock; ULONG InternalAddRef() { ATLASSERT(m_dwRef != -1L); return _ThreadModel::Increment(&m_dwRef); } ULONG InternalRelease() { ATLASSERT(m_dwRef > 0); return _ThreadModel::Decrement(&m_dwRef}; } “ void Lock() (m_critsec.Lock();} void Unlock() {m_critsec.Unlock();> p r i v a t e : CritSec m critsec;w mmm , _CritSec被定义 S_ThreadModel:: AutoCriticalSection, 基于JThreadModel 的类型定义,它被 定义为CComAutoCriticalSection或CComFakeCriticalSection。如果你的对象是自由线程的,它实 现CComAutoCriticalSection,否则 ,实现CComFakeCriticalSection。 瑞 士 军 刀 所有这些类型定义都很令人难于理解,因为微软试图创建一个能实现所有意 图的临界区,这要求在线程模型不需要它的时候,它什么也不做。这种设计通常会导致 令人费解的代码产生。 微软设计时考虑到的事情之一,就是使程序员可以在写他自己的类的时候不必关心 所使用的线程模型。为了有一天会使用这一特性的个别程序员,微软已经创建了多层次 的类型定义,使我们其余的人迷惑不解。 这种过分设计有时被称作瑞士军刀设计模式。微软使用瑞士军刀设计模式的另一个 例子是MFC的CString类。我认为CString类中有一个方法可以将一个字符串翻译为40种 不同的语言。我也可能错了。 现 在 ,你已经有了为自己的对象定义的临界区,它们可以被用于对每个对象的数据实现并116 第 二 部 分 高级COM编程技巧 发控制。不幸的是,这个临界区—— 不加思索地就被使用了—— 可能会导致另一个瓶颈,因为 每个线程都需要等待才能获得临界区。如果这种情况发生,你可以多创建几个临界区,来缓解 瓶颈。 2.互斥体 互斥体核心对象,可以使你创建和管理那些不能得到临界区保护的对资源的访问。跨越进 程边界的资源是不能被临界区保护的,但是可以被互斥体保护。清单7-16给出了一个使用互斥 体对象的例子。注意,互斥体在不同进程间,通过名字被共享,但临界区则不是这样。 清单7 - 1 6 使用互斥体 class Pool { p u b l i c : static DWORD WINAPI ThreadProc(void * p); int DoWork{); P o o l ( ) ; virtual -Pool(); p r i v a t e : static HANDLE sm_mutex; std::queue m_qidThread; #define MYMESSAGE (WM USER+10 抑 ) . #define NUMTHREADS 10~ HANDLE Pool::smmutex; Pool::Pool() { smjnutex = ::CreateMutex(NULL, FALSE, 'MYMUTEX“); for (int i= 0 ; i < 10 ;i++) { D W O R D dw; HANDLE thread = ::CreateThread(NULL, 0, ThreadProc, NULL, 0, &dw); ::CloseHandle(thread); m_qidThread•push(dw); } > P o o l : :- P o o l () { while (!m^qidThread•empty{)) { ::PostThreadMessage(m_qidThread.front(), WMQUIT, 0, 0); m_qidThread.pop(); > ::WaitForSingleObject(nj_qidThread.front(), 5 M 0 ); ::CloseHandle(sm mutex); DWORD WINAPI Pool::ThreadProc(void *p)第7 章 COM +线程 1V7 M S G m s g ; while (::GetMessage(&msg, NULL, 0, 0)) { switch (msg.message) { case MYMESSAGE: ///////////////////////“///////////// II T O D O II Add background processing code hereII ;-.WaitForSingleOb j ect (sm_mutex, INFINITE ); std::cout « “Hello World 1 from Thread Number: • « ::GetCurrentThreadId() « std::endl; ::ReleaseMutex{sm_mutex); d e f a u l t : b r e a k ; } ; } ; r e t u r n 0; } int Pool::DoWork() { DWORD dw = m_qidThread.front(); ::PostThreadWessage(dw, MYMESSAGE, 0, C); m_qidThread.pop(); m^qidThread.push(dw); r e t u r n 记住,临界区是进程局部的对象,因此不能限制跨进程的资源访问。 有了清单7-16中的互斥体,你已经有了一个或多个且每个都运行一个Pool类实例的进程,它 们将共享名为MYMUTEX的互斥体。而在清单7-11的临界区例子中,每个进程都有它自己独立 的临界区。 信号和事件 信号和事件是更复杂的核心对象,它们也是用于限制对资源的访问的。信号实际上是更通 用的实现过程,它使得程序员可以指定有多少线程可以同时对一个资源进行访问。信号有一个 计 数 器 ,其范围是0到一个由程序员指定的最大上限。当计数器为0时处于无信号状态,汽计数 器为非零时处于有信号状态。 事件对象也和互斥体一样,是两 状态对象,也就是说,它不是处于有信号状态就是处于无 信号状态。但事件和信号对象都可以被任何线程从无信号状态变为有信号状态。而只有最初将 互斥体对象从有信号状态变为无信号状态的线程,才能将这个互斥体再从无信号状态变为有信 号状态。哪一个同步对象可以成为你最好的选择,主要依赖于你的并发问题。有的并发问题适 合于使用临界区,其他的则适合于使用互斥体、信号或事件。 7 . 5 小结 在本章中,我们讨论了COM+是如何适应不同的Windows线程模型的要求的。你应该了解哪118 第二部分高级COM编程技巧 一种线程类型、线程模型及同步对象对你可用,以及哪一个对你的项目最合适。下一次你再写 COM+服务程序或实现并发控制的时候,你将用新的知识来调整你的设计。 我们用了很多时间来讨论并发问题,这将帮助你避免和解决编程中遇到的问题。这些并发 问题可能是在多线程应用程序中最难处理的问题。第8章 COM和注册表 本章内容包括•. • 注册表API • Regedit 和 Regedt32 • COM注册表的结构 • HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\01e • 注册COM +服务程序 因为注册表与COM+密切相关,所以本章讨论注册表。如果你曾经对一个问题很纳闷• . 在应 用程序被例示的时候,它是怎样找到COM+对象的?你会发现本章是很有启发性的。正如你将 要看到的,注册表是个有关COM+服务程序信息的贮藏室,这个信息被用于定位和了解应用程 序所使用的COM+服务程序。 8 . 1 注册表API 读写注册表是很困难的事,一个例子就是API函数RegCreateKeyExO,很明显它有太多的参 数。使用一个函数,你就可以创建和打开注册表的键,并可以为这个键设K 安全权限,这一切 可以用一次调用实现,如果程序的性能是由你使用的代码的多少来衡量的,那么这样很好C 在清单8-1中 ,我编写了一个很小的C++类 ,CRegistry,它去除了来ft注册表API的绝大多 数 复杂 性,它包括三个函数,执行三个基本的操作:在注册表中设置键值、从注册表中重新获 得键值以及删除注册表中 的键。类 的 构 造 函 数 Q 动打开 指 定 的 键 ,如 果 不 存 在 ,就创建这个 键 。 清单8 - 1 轻易使用注册表类 class CRegistry { p u b l i c : CRegistry(); CRegistry( HKEY, const char * -CRegistryO; BOOL Open( HKEY, const char * B O O L C l o s e ( v o i d ); BOOL ReadDWORD( const char *, BOOL ReadString( const char * DWORD *, DWORD *pdwLastError = NULL ); LPVOID, int, DWORD *pdwLastError = NULL BOOL WriteDWORD( const char * , DWORD, DWORD *pdwlastError = NULL ); BOOL WriteString( const char * , LPVOID, DWORD *pdwLastError = NULL );120 第二部分高级COM编程技巧 BOOL Write( const char *, LPVOID, DWORD, int ); void BuildList( const char * , C S t r i n g A r r a y & ); void StoreList( const char * , CStringArray& ); p r o t e c t e d •• HKEY m_hKey ; OWORO m_dwLastError; BOOL Read(. const char *, LPVOID ); DWORD m_dwSize ; } ; CRegistry::CRegistry() { II Member variable m_hKey set to NULL so II we know we don't have to close it when Close() II is called from the destructor. mJiKey = NULL; CRegistry::CRegistry( HKEY hKey, const char *lpszSubKey ) II Member variable m_hKey set to NULL so II we know we don't have to close it when 01ose() II is called from the destructor. m_hKey = NULL; II This constructor opens the registry key. Open( hKey, IpszSubKey ); CRegistry::-CRegistry() // Call the close function from the destructor II to make sure the registry key is close (if it II was opened). C l o s e ( ) ; BOOL CRegistry::0pen( HKEY hKey, const char *lpszSubKey ) { II First try to close the registry. (It may not be II opened, but we want to be sure.) C l o s e ( ) ; II Reset our error variable. m_dwLastError = 0;弟8 章 COM和注册表 121 II Open the registry key. We use KEY_ALL_ACCESS to avoid // any problems. if{ ::RegOpenKeyEx( hKey, IpszSubKey, 0, _ KEY_ALL_ACCESS, &tn_hKey ) 1= ERROR_SUCCESS ){ II Get the error and return. m_dwLastError = GetLastError(); return{ FALSE ); II Return success r e t u r n ( T R U E ); BOOL CRegistry::Close( void { BOOL bRet = TRUE ; II If m_hKey is NULL then the registry key II isn't opened. If this is the case, return // indicating that Close() wasn't successful, if( m_hKey =- NULL ) r e t u r n ( F A L S E ); II Close the registry key. bRet = ( ::RegCloseKey( m_hKey ) == ERROR_SUCCESS ); II Get the error (if any). m_dwLastError = GetLastError(); II Set the key handle to NULL so we don't accidentally II close it again. m_hKey = NULL; II Return success or failure r e t u r n ( b R e t ); BOOL CRegistry::Read( const char *lpszValueName, LPVOID IpReturnBuffer ) // Generic read function. II If the key is not opened, return with failure, if ( mJiKey == NULL ) return( FALSE ); II Set the local variable to our size. DWORD dwSize = m dwSize; II Clear the return buffer. memset( IpReturnBuffer, 0, dwSize ); // Read the registry value. BOOL bRet = ( ::RegQueryValueEx( m_hKey, IpszValueName, NULL, NULL,122 第二部分高级COM编程技巧 (unsigned char *) IpReturnBuffer, &dwSize ) == ERROR 一S U C C E S S ); II Get the error (if any). m_dwLastError = GetLastError(); II Return success or failure return( bRet ); BOOL CRegistry::ReadDWORD( const char •IpszValueName, DWORD *pdwOata, ^DWORD *pdwLastError ) II If the key is not opened, return with failure, if ( mJiKey == NULL ) return( FALSE ); II Set the m_dwSize value so that we can call the II generic Read function. m_dwSize = sizeof( DWORD ); II Call the read function. BOOL bRet = Read( IpszValueName, pdwData ); II If caller wants the error code, store the value, if( pdwLastError 1: NULL ) *pdwLastError = m_dwLastError; II Return success or failure return( bRet ); BOOL CRegistry::ReadString( const char ^IpszValueName, LPVOID IpReturnBuffer, •int nSize, DWORD *pdwLastError ) { II If the key is not opened, return with failure. if( mJiKey == NULL ) r e t u r n ( F A L S E ); II Set the m_dwSize value so that we can call the II generic Read function. m_dwSi7e = nSize; II Call the read function. BOOL bRet = Read( IpszValueName, IpReturnBuffer ); II If caller wants the error code, store the value, if( pdwLastError != NULL ) *pdwLastError = m_dwLastError; II Return success or failure r e t u r n { b R e t );第8 章 COM和注册表 123 BOOL CRegistry::WriteDWORD( const char *lpszValueName, DWORD dwOata, DWORD *pdwLastError ) { I/ If the key is not opened, return with failure, if( m_hKey == NULL ) r e t u r n ( F A L S E ); II Call the write function. BOOL bRet = Write( IpszValueName, &dwData, REG_DW0RD, sizeof( DWORD II If caller wants the error code, store the value, if( pdwLastErron != NULL ) *pdwLastError = m_dv/Last Error; II Return success or failure return( bRet ); BOOL CRegistry::WriteString( const char *lpszValueName, LPVOID lpData, _DWORD *pdwLastError ) II If the key is not opened, return with failure, if( m_hKey =: NULL ) return( FALSE ); // Call the write function. BOOL bRet = Write( IpszValueName, lpData, REG_SZ, (DWORD) strlen( (const char *) lpData ) •♦• 1 ) store the valueII If caller wants the error code, if( pdwLastError != NULL ) •pdwLastError = m_dwLastError; /I Return success or failure return( bRet ); BOOL CRegistry::Write( const char *lpszValueName, LPVOID lpData, DWORD dwType •int nSize ) { II Generic write function. II If the key is not opened, return with failure if( m—hKey == NULL ) return( FALSE ); II Call the RegSetValueEx() function. BOOL bRet = ( ::RegSetValueEx( m h K e y , IpszValueName, 0, dwType, (unsigned char *) lpData, nSize ) == ERROR_SUCCESS ); II Get the error (if any).124 第二部分高级COM编程彳支巧 m_dwLastError = GetLastError(); II Return success or failure r e t u r n ( b R e t ); void CRegistry::BuildList( const char *IpszValueName, CStringArray& List ) { CString strTemp; char szTemp[1000]; II This function builds a char ** list of values in a II single registry value. II For instance: This;That;TheOther II creates a list of three char buffers II containing This, That, and TheOther II Clear our local temp buffer memset( szTemp, 0, sizeof( szTemp )); II Read the Registry value. if( !ReadString( IpszValueName, szTemp, sizeof( szTemp ) ) ) r e t u r n ; II Clear the list. List .RemoveAllO; II Store in a CString for easier manipulation. strTemp = szTemp; II Look for that's the delimiter, int nlndex = strTemp.Find( ); while( nlndex 1= -1 ){ II Get the partial string. CString strNew = strTemp•Left( nlndex ); II Kill the ';' character. strTemp = strTemp.Right( strTemp.GetLength() • nlndex - 1 ); II If the string has any length, add it. if( strNew.GetLength() > 0 ) L i s t . A d d { s t r N e w ); II Look for the next string, nlndex = strTemp.Find( ); II If we have text that didn't end with II it'll need to be added also, if( strTemp.GetLength() > 0 ) List.Add( strTemp );第5 章 COM和注册表 125 void CRegistry::StoreList( const char *IpszValueName, CStringArray& List ) { CString strTemp; II Given a char ** list, store these values into II a single registry item separated by •; • // Loop through for each string. for( int i=0; i 0 ) strTemp += “;*; II Now add the string data. strTerap += List.GetAt( i ); II If we didn't actually get any data, just return, if( strTemp.GetLength() == « ) r e t u r n ; II Write the string to the registry. WriteString( IpszValueName, strTemp.GetBuffer( 0 ) }; } 8.2 Regedit和Regedt32 微 软 提 供 了 两 个 不 同 的 编 辑 器 ,用 以 浏 览 和 操 作 W indows的 系 统 注 册 表 :Regedit和 Regedt320 Regedit提供了一些浏览和改变系统注册表所必需的基本功能。用户界面是经典的资源管理 器 风 格 (目录树在左,内容列表在右 >,如图8-1。 ft Cl »MV_amcMT_us0i CJ Wf.usois S CJ hr CMM*. ±-----------------------------1 」 图 8-1 Regedit的 用 户 界 面126 第二部分高级COM编程技巧 Regedt32除了拥有Regedit的几乎所宥功能外,还有其他一些功能。用户界面是经典的MDI 风格,如图8-2。每个键都可以在MDI的子窗口中打开。但是你应该知道,在Windows 9x平台上 是找不到Regedt32的。 araraesiBEBBE 图 8-2 Regedt32 的 用 户 界 面 每 个 注 册 表 的 键 都 有 权 限 ,它 可 以 允 许 和 禁 止 不 同 的 用 户 和 用 户 群 进 行 读 写 访 问 Regedt32提供了浏览和修改这些注册表权限的能力,如图8-3。 ; jUjsjI 图8-3 Regedt32对话框 两个编辑器我都用。Regedit有一些Regedt32没有的b u g ,但Regeciit更容易从命令行输人,因 此我更愿意使用Regedit。 导入和导出注册表文件 Regedit注册表编辑器具有导入和导出注册表文件的功能,这些文件都具有.RGS扩展名。为第S 章 COM和注册表 127 了便于注册COM对 象 ,编码向导经常会生成注册表文件。 注册表文件很适合于将注册表中的设置从一台计算机复制到另一台计算机,如果你在一台 计算机上有完全正确的注册表设置,那么你就可以将这些设置导出到一个注册表文件,然后再 将这个文件导人另一台计算机。 Regedt32也具有这项功能,但它将注册表信息保存为一种不可读的格式,而Regedit则将键 值保存为一种可读的文本文件。 8.3 COM的注册表结构 在系统注册表中,COM使用HKEY_CLASSES_ROOT主键保存对象初始化参数。在这个键 中共包括六类信息: • 文件扩展名。 • ProglDo • AppID。 • CLSIDo • 接口。 • TypeLibo COM 还使用 HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\01e 主键保存安全性信息。 我们将在后面详细讨论以上两个主键。 8 . 3 . 1 文件扩展名 文件扩展名是HKEY_CLASSESJROOT主键的子键。文件扩展名这个键有如下结构,如图8-4 所示: HKEY_CLASSES_ROOT { .extension > = { prog-id > ■SBBCQ O dew 图8 - 4 文件扩展名键128 第二部分高级COM编程41巧 这个键值告诉COM应该到哪里去找COM对象类型的ProgID。在找到ProgID之后,COM便可 以确定对象的CLSID。有了CLSID,COM就可以运行一个对象的实例,并将文件绑定到对象上。 对于某些系统注册表的键,我将给出一个较短的函数,用于配置键值。清单8-2中的代码可 以用于配置文件扩展名键值。 清 单 8 - 2 配 置 文 件 扩 展 名 键 值 i n t BuildFileExtensionKey(LPSTR szExtension, / LPSTR szProgld) CRegistry key(HKEY_CLASSES_ROOT, szExtension); key.SetValue(NULL, szProgld); } r e t u r n 0; 8.3.2 ProgID ProgID也是HKEY_CLASSESJlOOT主键的子键。因为文件扩展名和Pmg丨D都是同一个根 主键的子键,这导致了一个很乱的主键。ProgID的 键 值 (参见图8-5 ) 结 构如下 : HKEY_CLASSES^ROOT { ModuleName.ObjectName } = Description CLSID = { class-id-as-guid } CurVer = { version-dependent•prog * id } { ModuleName.ObjectName.VersionNumber } = Description CLSID = { class-id-as-guid } Q D W a d R f T F S ®WofdT«r.p?«i*&WortJW?5fd8(DWo»tJV«?3rd8 Q3Wo«fl)ocum«n! WSrr»rM 图8-5 ProgID键结构 清单8-3中的代码可以用于配置ProgID系统注册表键值。这段代码使用了两个新的API函 数 , StringFromCLSID()和WideCharToMultiByteO。前者的输入参数是一个GUID结 构 ,它将这个结 构转换为一个可以理解的字符串;后者将双字节字符串转换成熟悉的单字节字符串(只要你不 为Unicode平 台 编 程 )。第§ 章 COM和注册表 129 清 单 8 - 3 配置P r o g I D 注册表键 int BuiLdProgIDKey(LPSTR szProglO, LPSTR szDescription, CLSID clsid, LPSTR szProglDVersionlndependent) { II version dependent description CRegistry Key(HKEY_CLASSES_ROOT, szProgID); key.SetValue(NULL, szDescription); II version dependent CLSID key LPSTR StrCLSID = -CLSID “ ; LPSTR str = new char[::lstrlen(szProgID) +::lstrlen(strCLSID)+2]; ::lstrcpy(str, szProgID); ::lstrcat(str, “W“); ::lstrcat(str, strCLSID); CRegistry key2(HKEY_CLASSES_ROOT, str); deleteU str; II version dependent CLSID value LPOLESTR olestr = NULL ; str = NULL; ::StringFromCLSID(clsid, &olestr); int i = ::WideCharToMultiByte(CP_ACP, 0, olestr, *1, str, 0, NULL, NULL ); str = new char(i+1]; : : W i d e C h a 「To_ltiByte(CP_ACP, 0, olestr, -1, str, i 厂 NULL, N U L L ); Key2.Setvalue(NULL, str); delete(] str; II version independent description CRegistry Key3(HKEY_CLASSES_ROOT, szProglDVersionlndependent); key3.SetValue(NULL, szDescription); II version independent CLSID key str = new char[::lstrlen(szProglDVersionlndependent) + ::lstrlen(strCLSI0)+2]; ::lstrcpy(str, szProglDVersionlndependent); ::lstrcat(str, _\\_); ::lstrcat{str, strCLSID); CRegistry key4(HKEY_CLASSES_ROOT, str); deleted str ; II version independent CLSID value str = NULL; i = ::WideCharToMultiByte(CP_ACP, 0, olestr, -1, str, 0 ,一N U L L , N U L L ); str = new char[i+1]; ::WideCharToMultiByte(CP_ACP, 0, olestr, -1, str, i,~NULL, NULL ); key4.SetValue(NULL, str); delete[J st 「; // current version LPSTR strCurVer = “CurVer“; str = new char(::lstrlen(szProgIDVersionIndependent)130 第二部分高级COM编程技巧 + : :lstrlen(strCurVer)+2]; ::lstrcpy(str, szProglDVersionlndependent); ::lstrcat(str, -\\H); ::lstrcat(str, strCurVer); CRegistry key5(HKEY_CLASSES_ROOT, str); delete[] str; key5.SetValue(NULL, szProgID); r e t u r n 0; 1. ProgID和CLSID 每个ProgID都有一个表示它的CLSID的子键,这个子键有一个键值,是GUID结 构 。COM生 成的GUID对于使用它的程序员来说太复杂,因此,为了使人们更容易地使用COM, COM提供 了ProgID这种更简单的机制来识别对象。CLSID的值是从简单的ProgID转换为复杂的GUID的 , COM提供了两个API函数进行ProgID和CLSID之间的转换: WINOLEAPI ProglDFromCLSID (REFCLSID clsid, LPOLESTR FAR* IpszProgID ); WINOLEAP I CLSIDFromProglD (LPCOLESTR IpszProgID, LPCLSID lpclsid ); 2 .与版本相关的ProgID ProgID有以下句法: ModuleName.ObjectName. VersionNumber 同一模块中的所有对象都有相同的ModuleName,但 是 ,ObjectName将惟一地确定模块中的 每个对象。VersionNumber是 一 个 整 数 (从 1幵 始 ),用于确定对象的版本。例 如 ,对 象 的 第 -个 实现的ProgID比方说是MyModuleName.MyObjectName.l,那么这个对象的第二个实现的ProgID 就应该是MyModuleName.MyObjectName.2。这种约定使你可以将多版本的COM类安装在同一 台 机 器 t ,而每个版本将用递增的整数后缀来标记。 3 .与版本不相关的ProgID 再有一个与版本不相关的ProgID会更方便。与版本不相关的PmgID有以下句法: MyModuleName•MyObjectName 这个ProgID使客户程序开发人员可以不必检查系统中对象的版本便使用对象。 8.3.3 AppID AppID 是主键 HKEY J :LASSES_ROOT\AppID 的子键。 AppID的 键 (参见图8-6 ) 有如下结构: HKEY_CLASSES_ROOT APPID { app-id-as-guid } RemoteServerName = { DNS or UNC of server } II client machine only DLLSurrogate = { path\surrogate.exe or NULL for DllHost.exe } II server machine only LaunchPermission = REG_BINARY { self-relative s e c u r i t y descriptor }第5 章 COM和注册表 131 AccessPermission = REG_BINARY { self-relative security descriptor } ActivateAtStorage = { Y or N } LocalService = ServiceParameters = RunAs = { none I “Interactive User“ | user account } AuthenticationLevel = lO. 图8-6 A p p I D 键结构 AppID提供了有关运行COM服务程序的信息。COM服务程序可以被配置为远程运行,并具 有安全性、权限和服务。 1.远程进程内服务程序 为了运行远程计算机上的进程内服务程序,必须使用代理。DLLSurrogate键值名中的键值 数据就是可以当作代理的可 执 行 数 据 。如 果 你 将 这 个 值 置 零 ,DCOM 将 使 用 默 认 的 代 理 (DLLHost.exe )Q 为了使进程内服务程序远程运行,必 须指 定 服 务 器 (远 程 )上的DLLSurrogate 键值名,以及客户机上的RemoteServerName键值。 2 .远程进程外服务程序 如果对象在CLSID键中有一个RemoteServerName键值名,那么这个对象就可以作为远程迸 程外服务程序被运行和访问,而键值就是服务程序被运行的位置c 3.权限和安全性 COM使你可 以 为 运 行 (LaunchPermission ) 和 访 问 (AccessPermission) 本地计算机上的对 象设置权限,这些权限可以作为自相关安全性描述符,保存在注册表中。 清单8-4说明了如何将安全性描述符保存在注册表中。相同的技巧可以用于将安全性描述符 保存在文件中,或是将安全性描述符传给另一个进程。 清单8 - 4 在注册表中保存安全性描述 II......................................... II RegSaveSecurityDescriptorII......................................... int RegSaveSecurityDescriptor(PSECURITY_DESCRIPTOR pSD, HKEY hkeyT LPSTR szValueName) { // convert the security descriptor to I/ self-relative security description PSECURITY_DESCRIPTOR pSRSD = NULL ; DWORD cbSD = 0; if (I::MakeSelfftelativeSD(pSO, pSRSD, &cbSD))132 第二部分高级COM编程4支巧 { DWORD dw = ::GetLastError(); if( dw 1= ERROR_INSUFFICIENT_BUFFER ) { std::cout « “Error ::MakeSelfRelativeSD “ « dw « endl; r e t u r n 0; } } pSRSD = (PSECURITY_DESCRIPTOR) ::L 0C a l A l l 0C(LPTR, CbSD); if (!::MakeSelfRelativeSD(pSD, pSRSD, &cbSD)) { std::cout « “Error ::MakeSelfRelativeSD M « ::GetLastE 「ror() « endl; r e t u r n 0; II save the self-relative security description II in the registry if (::RegSetValueEx(hkey, szValueName, 0, REG_BINARY, (BYTE *)pSRSD, cbSD) 丨=ERR5R—SUCCESS) { std::cout « “Error ::RegSetValueEx _ « ::GetLastError() « endl; g : :LocalFree((HLOCAL) pSRSD); r e t u r n 0: if(pSRSD J= NULL) { ::LocalFree((HLOCAL) pSRSO); } r e t u r n 0; 为了保存安全性描述符,必须调用MakeSelfRelatWeSDO函数,将一个纯粹的安全性描述符 转换成一个自相关的安全性描述符。 自相关安全性描述符是完全自我包含的平面结构,而纯粹 的安全性描述符包含对结构外对象的引用。 4 .验证 在这个键中,Windows 2000有补充的键值。它使你可以修改默认的验证级别。 5 .远程激活 通 常 ,当你调用IMoniker::BindToObject()时 ,对象都在本地被创建和载入。 当缺少键值 ActivateAtStorage或该值被设为N时 ,对象将在本地创建和载人;但 是 ,如果该值被设为Y ,COM 将试图在对象所在的计算机上载入对象,这样 ,如果你创建了一个文件IMoniker::BindToObject(), 对象将在远程计算机上被创建(参见图8-7 )。 6 .作为服务的对象 如果COM对象要作为NT服务安装自己,必须用这个服务名设置注册表人口LocalService。 另外,当服务被运行时,可以将参数传递给这个服务。通过设置注册表入口ServiceParameters的 键值,就可以向服务传递参数—— 设置的键值。第8 章 COM和注册表 ActivateAtStorage=N ActivateAtStoragc=Y 文 件 文 件 持 续 对 象 _______________ ___________________ ___________ 边 界 持 续 对 象 客 户 进 程 客 户 进 程 图 8-7 ActivateAtStorage 设 置 7. 身份 通过使用RunAs键 值 ,就可以在一个具有另一个用户的安全环境的进程中运行对象的模块。 除了运行模块的用户之外,你还可以给进程赋予交互用户的安全环境,或者某个指定用户的安 全环境。 8.3.4 CLSID CLS1D是主键HKEY_CLASSESJROOT\CLSID的子键,它 的 结 构 (参见图8-8 ) 如下 HKEY_CLASSES_ROOT CLSID { class-idasguid } = Description AppID - { app-id-as-guid } ProgID = { ModuleName.ObjectName.VersionNumber } VersionlndependentProglD = { ModuleName.Obj ectName } I n p r o c S e 「ve「32 = { mypath\mymodule.dll } ThreadingModel = { [no value) | Apartment | Free | Both } InprocHandler32 = { mypath\myhandler.dll } LocalServer32 = { mypath\mymodule.exe } Defaultlcon = { mypath\mymodule, resource id TypeLib = { type-library-id-as-guicJ > } 3 REQ.SZ CVProQfam FS»*\Co*Ttmoft Fi Thf»adingMod«l: REQ_S2 . Apertmm - a {0000010MXKKHW1M0C* -t*D {0000010W»0(HHai M00I -CD (0000010WWWHW1 ____ 1 服务器 客户机 图 8-8 C L I S D 键 结 构134 第二部分高级COM 编程技巧 清单8-5中的代码可以用于配置CLS1D的 键 值 。 清 单 8 - 5 配 置 CLISD 键值 int BuildCLSIOKey(CLSID clsid, LPSTR szOescription, LPSTR szProgID, LPSTR szProglDVersionlndependent) { I! build the CLSID LPOLESTR Olestr = NULL ; LPSTR Str - NULL ; ::StringFromCLSID(clsid, &olestr); int i = ::WideCharToMultiByte(CP_ACP, 0, olestr, -1, str, 0 「 N U U _ , N U L L ); str = new char[i+1); ::WideCharToMultiByte(CP_ACP, 0, olestr, .1, str, i厂NULL, NULL); // build the subkey LPSTR Str2 = NULL ; LPSTR StrCLSID = “CLSID“; st「2 = new char[::lstrlen(strCLSID)+::lstrlen(str)+2]; ::lstrcpy(str2, StrCLSID); ::lstrcat(str2, ::lstrcat(st 「2, s t r ) ; II set description CRegistry key(HKEY_CLASSES_ROOT, Str2 ); key.SetValue(NULL, szOescription); delete() str2; // build CLSID\{CLSID}\ProgID string LPSTR strProgID = “ProgID“; st「2 = new char[: :lstrlea(strCLSIO) + : -.lstrlen(str) + ::1strlen(strProgID)+31; ::lstrcpy(Str2, StrCLSID ); ::lstrcat(str2, “ W ); ::lstrcat(str2, str); ::lstrcat(str2, “ \ \“); ::lstrcat(str2, strProgID); // set progid CRegistry key2{HKEY_CLASSES_ROOT, str2); key2.SetValue(NULL, szProgID); deleted str2;- // build CLSID\{CLSID}WersionlndependentProgID string LPSTR strProglDV = “VersionlndependentProglD“; s t 「2 = new char[::lstrlen(strCLSID)+: :lstrlen(str) :lstrlen(strProgIDV)+3]; : : l s t 「c p y ( s t r 2 , StrCLSID); ::lstrcat(str2, '\\*); ::lstrcat{str2, str); ::lstrcat(str2, _\\M); ::lstrcat(str2, strProglDV); II set version independent progid CRegistry key3(HKEY_CLASSES_R00T, st 「2 ) ;第8 章 COM和注册表 135 图 8 - 9 接 U 键 结 构 key3.SetValue(NULL, szProglDVersionlndependent); d e l e t e U s t 「2 ; delete[] str; r e t u r n 0; CLSID键提供了关于如何运行COM类的信息,这里最重要的信息是location键值,也就是实 现COM类的模块的位置。 1.位置 如果对象在CLSID键中有InprocServer32这个键值名,那么这个对象就可以作为进程内服务 程序被运行和访问,它的键值是DLL ( 动态链接库)的位置。为线程模型提供位置透明是COM+ 的 任 务 ,我将在后面详细讨论这一点。可以想象,在一个进程内服务程序被自由线程型的多线 程客户访问时,将是不稳定的。为了告诉COM+我们使用的线程模型,直接在InproCServer32注 册表键中设置ThreadingModel的键值。 如果对象在CLSID键中有InprocHandler32这个键值名,这个对象将使用自定义的对象句柄 ( 自定义〖Marshall接 口 )。这个键值就是自定义句柄模块的文件位置。为了使用标准调度,把这 个键值设为OLE32.DLL。 如果对象在CLSID键中有LocalServer32这个键值名,那么这个对象就可以作为本地的进程 外服务程序被运行和访问,它的键值是exe ( 可执行 模块)的位罝。 进程内句柄用于注册自定义调度。 自定义 调 度句柄(DLL) 在客户和服务器进程中都被载 入到进程内,它的职责是进行客户进程和服务器进程之间的调度。 2. CLSID类型库 TypeLib是类型库的CLSID,这个CLSID可以在HKEY_CLASSES_ROOT\TypeLib主键中找到。 8.3.5 接口 接口是主键HKEY_CLASSES_ROOT\Interface的子键,它 的 结 构 (参见图8-9 ) 如下 : HKEY_CLASSES_ROOT I n t e r f a c e { interface-as-guid } = Description ProxyStubClsid32 = { class-id-as-guid > TypeLib = { type-library-id-as-guid }136 第二部分高级COM编程技巧 你的调度程序中的DLLRegisterServer和DLLUnregisterServer两个函数应该可以添加和删除 这个键和它的键值,对于每个可以被调度的接口都应该添加一个Interface子键。 接口子键很小。对于进行标准调度或自定义调度,我们对代理•占位程序的CLSID很感兴趣, 因 为 这 个 程 序 是 用 来 实 现 对 这 个 接 口 进 行 调 度 的 。 而 对 于 类 型 库 调 度 ,我 们会 对类型库的 CLSID感兴趣。 1.代理•占位 ProxyStubClsid32是执行接口调度的代理-占位对的CLSID。默认的代理-占位ID—— 也就是 给 IDispatch 用的 ID—— 是{00020420-0000-0000-0000-000000000046} 0 2 .类型库 TypeLib是注册在TypeLib键中的类型库CLS1D。 8.3.6 TypeLibs TypeLibs是主键HKEY_CLASSESJROOT\TypeLib的子键,它 的 结 构 (参见图8-10)如下: HKEY_CLASSES_R00T T y p e l i b { type-library-id-as-guid } = Description { major.minor } = Description { locale-id } { platform > = { module } F L A G S = 0 HELPDIR = { helppath } - t -c-c - f ICO'J !3 {tXXI00205^ 00(H)Oi D^OOi t i {0000030( « 000-001 M O O , !3 {0000060IH>00(H)010^ 00i ^}{000?043IXX»KHX)C&COO REG.S2 - t - G n o L C 3 H 6 L P 0 I R L C b t o 23 {OOOiOCHJOOO-OOKKtX.. - t 1 1 |{0002(M F X »0(H)000-C00 _________________ __________________ a M) • • 图8-10 TypeLib键结构 locale-id是一个1到4位的十六进制数,表示地区ID。零键值代表LANG_SYSTEM_DEFAULT(0> 的 设 置 (这用 于 国 际化)。Platform是一个字符串,可以是w inl6、Win32、mac等。而module可 以是TLB ( 类 型 库 )、DLL或EXE。 1. RegisterTypeLib 函数RegisterTypeLibO将自动创建TypeLib键 。为了调用这个函数,必须首先为类型库分配 一个指向ITypeLib对象的指针。清单8-6说明了如何使用RegisterTypeLibO函数。 清单8-6 使用RegisterTypeLib()函数 int MyRegisterTypeLib(LPOLESTR IpszModule, LPOLESTR IpszHelpDir)第8 章 COM和注册表 137 ITypeLib* pTypeLib; HRESULT hr = ::LoadTypeLib(lpszModule, ApTypeLib); if (SUCCEEDED(h「)} { ::RegisterTypeLib(pTypeLib, IpszModule, IpszHelpDir);} if (pTypeLib != NULL) { pTypeLib.>Release(); } r e t u r n 0; 2 .标记 在TypeLib结构 中,以下标记可用: •1 一受 限,你不能浏览或使用类型库。 •2 —控 件 ,包含控件的描述。 •4一隐藏,用户不能浏览这个类型库,但可以被控件使用。 3 .帮助目录 类型库可以提供环境敏感帮助,HELPD1R的值就是提供帮助的文件的路径。如果不用这个 键 ,你必须在编译时提供帮助文件的路径。 8.4 HKEY 一 LOCAL 一 MACHINE\SOFTWARE\Microsoft\Ole 这个键包含了所有COM需要的默认安全信息,它 的 结 构 (参见图8-11 ) 如下 : HKEY_LOCAL_MACHINE SOFTWARE M i c r o s o f t Ole EnableDCOM = { Y or N } DefaultAccessPermission = REG_8INARY { self relative security descriptor } DefaultLaunchPermission = RE6_BINARY { self-relative security descriptor } LegacyAuthentication = Legacylmpersonation = LegacySecureReferences = { Y or N } i M -QDM5M0 -DM S TT S -ClM SVSO G -G ) —由 NaCOE -CDNofHDovefSiQning SubsytWw lot N D«teuW_nunchP*rmi“ o n REGJBIKAi Enabl»DCOM REG_S2 Y 钃 - Gd CuOook Expr«s» -------------------------1 i T h i ------------------------1 」 图 8-11 Microsoft\01e 结构138 第二部分高级COM编程枝巧 卜_ 的儿个部分将描述OLE注册表子键屮的+ 同设置。 8 . 4 . 1 允许和禁止DCOM EnableDCOM键值使你可以允许和禁止远程对象c 将这个键值设为N ,会 禁 lh远程对象,似 不会禁止DCOM的安全性。 8 . 4 . 2 默认权限 当COM服务程序没有为它的AppID键中的LaunchPermission和AccessPermission提供键值的 时候,可以使用DefaultLaunchPermission和DefaultAccessPermission两个键值。如果在AppID键 中没有Launch Permission, 就 使 用 06£31]1比 311!1011?61'111丨88丨0 !1 ;如 果 在 A pp丨D 键中没有 AccessPermission, 就使用DefaultAccessPermission。 要亲f i 为你的计箅机上成& 上千的COM服务程序管理各自的权限,实际上是不大可能的c 因此,我的方法是使用默认权限,而不去亲自配置COM服务程序c 如果一个COM服务程序确实 需要额外的或更少的安全性, 那时也只有我才添加A 定义的权限。 • 8 . 4 .3 传统的安全性 COM+提供了一些键值来设置组件的默认安全性,因此这些组件是不实现特定的安全性的。 有以下的验证级别可用: • RPC_C_AUTHN_LEVEL_DEFAULT—— 使用验证服务的默认验证级别c • RPC_C_AUTHN—LEVEL_NONE—— 没有验证。 • RPC_C_AUTHN_LEVEL_CONNECT—— 只在连接时验证 0 • RPC_C_AUTHN_LEVEL_CALL—— 验证每个调用。 • RPC_C_AUTHN_LEVEL_PKT—— 验 i正每个包。 • RPC_C_AUTHN_LEVEL-PKT一INTEGRITY—— 与 RPdAUTHN_LEVEL_PKT 相 同 , 并且验证数据在传输过程中是否有所改变。 •RPC_C_AUTHN 一 LEVEL_PKT_PRIVACY 与 RPC_CLAUTHN,LEVEL_PKTJNTEGRITY 相同,并 a 给包加密。 通过模拟其他用户,C O M +为服务程序提供了采用其他用户的安全环境的能力 。 Legacylmpersonaticm键值使你能够限制服务程序可以使用的模拟的种类。有如卜•模拟级别吋用: • Anonymous--- 不允许模拟。 • Identify— 除了检査客户权限之外,其他时候不允许模拟。 • Impersonate--- 除了调用其他对象以外,其他时候允许模拟。 • Delegate—— 允许模拟,即使在调用其他对象的时候。 如果LegacySecureReferences的键值是Y , 那么没有调用过Colnitia丨izeSecuri〖y()的对象 , 对 AddRef()和ReleaseO的调用 是安 全的 。也 就 是 说 ,COM+会 将 安 全 性加 到 这 些调 用 上 。 当 LegacySecureReferences的键值娃Y时 , COM+会变慢 。COM和注册表 139 8 . 5 注册C〇M+ 服务程序 在这一章里,我已经解释了很多的注册表子键和键值,在我告诉你一个坏消息之前,我先 要 让 你 结 束 观 看 “辛 普 森 - 家”,而这个坏消息就是,注册这么多的设置都是你的COM+服务程 序的责任。下面几部分介绍了你的注册代码应该放在哪里。 8.5.1 Regsvr32 DLLUnregisterServer和DLLRegister两个函数都是从DLL导出的。Regsvr32应用程序执行的 是下列四项任务,用来注册或反注册进程内服务程序: • 调用LoadLibrary函数,载入模块。 • 调用GetProcAddress函数,得到指向DLLRegister或DIXUnregister的函数指针。 • 调用 DLLRegister 或 DLLUnregister函数。 • 调用 FreeLibrary 函数。 DLLRegisterServer^DLLUnregisterServer 进程内服务程序使用DLLRegisterServer和DLLUnregisterServer进行自注册。你可以通过运 行DLLRegisterServer函数,注册一个ft注册DLL。这些会在一个名为module.dll的模块中ft动完 成 ,只要■在命令行执行REGSVR32 module.dll就可以了。可以通过运行DLLUnregisterServer函数 删除系统注册表的入口,完成反注册,这些也会在modiile.dU模块中自动完成,只要在命令行执 行REGSVR32 /U module.dll就可以了。 注册DLL应该在它的VERS丨ONINFO资 源中 包 括字符串“OLESelfRegister'清单8-7给出 了 在 VERSU3N1NFO 资源中 OLESelfRegister 的位置。 清单8-7 VERSIONINFO源代码中的OLESelfRegister串 VS_VERSION_INFO VERSIONINFO BEGIN BLOCK “StringFilelnfo“ BEGIN BLOCK M040904E4“ // Lang=US English, CharSet=Windows Multilingual BEGIN VALUE “OLESelfRegister-, _\0_ END END END 你的DLLRegisterServer和DLLUnregisterServer函数必须从你的DLL中 导 出 ,而且它们一走 不能包含C++ name mangling。如果导出的函数中包含了C++ name mangling , 那么像 REGSVR32这样的客户程序将无法调用这两个函数。清单8-8中给出了实现DLLRegisterServer和 DLLUnregisterServe 的占位程序。140 第二部分高级COM编程技巧 清单8 - 8 实现DLLRegisterServer的占位程序 //............................... STDAPI DllUnregisterServer() { II use RegDeleteKey() to remove keys return NOERROR; 大 多 数 框 架 (如ATL和MFC)会为DLLRegisterServer和DLLUnregisterServe两个函数自动生 成必要的代码。 8 . 5 . 2 自注册进程外服务程序 进程外服务程序应该在命令行注册和反注册它们自己。也 就 是 说 ,它们应该校验命令行参 数/RegServer和/UnRegServer。为了处理注册参数,清单8-9说明了应该如何实现你的WinMain() 函数。 清单8 - 9 为自注册表检査命令行参数 II.................. II MatchOptionII.................. BOOL MatchOption(LPTSTR lpsz, LPTSTR IpszOption) { if (lpsz[0] == ••• || lpsz[0] == '/') { l p s z + + ; } if (lstrcmpi(lpsz, IpszOption) == 0) return TRUE; } return FALSE; } II........... // W i n M a i n //............ int WINAPI WinMain(HINSTANCE hlnstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) { LPTSTR szCmdLine = lpCmdLine; TCHAR szTokens[] = _T€XT(B •/“); LPTSTR szNextToken = _tcstok(szCmdLine, szTokens); while (NULL != szNextToken) { if (MatchOption(szNextToken, TEXT(“UnregServer“)) II Unregister the server II use RegDeleteKey()弟&章 COM和注册表 141 return FALSE; } else if (MatchOption(szNextToken, TEXT(“RegServer“))) { II Register the server // use RegCreateKey() and RegSetValueO • • • return FALSE; } else if (MatchOption(szNextToken, TEXT(“Embedding“))) { b r e a k ; } //Find the next token szNextToken = _tcstok(NULL, szTokens); } II Initialize the COM Library. HRESULT hr = CoInitialize(NULL); if (FAILED(hr)) return FALSE; « • • II Uninitialize the COM Library. CoUninitialize(); return (msg.wParam); } 8 . 5 . 3 框架 VB和VC++ (MFC和ATL)提供了内置的功能,用以自动注册进程内和进程外服务程序。 如 果 你看 一 下MFC的 源 代 码 ,你 将 注 意 到 两 个 函 数 :AfxOleRegisterServerClass()和 AfxOleUnregisterServerClassO, 它们提供了一些必要的功能,用于注册和反注册COM服务程序。 而在ATL中 ,有两个相似的函数,分别是 :AtlModuleRegisterServer()和AtlModuleUnregister Server()0 MFC有两个用于注册类型库的函数,Afx01eRegisterTypeLib()和Afx01eUnregisterTypeLib()。 ATL也有一个函数用于注册类型库, AtlModuleRegisterTypeLib()。 ATL还提供了几个 机制,使你可以把.REG文件 作 为资源 嵌 入 你 的 模 块 (DLL或EXE)中。 同时提供了一个调用这项功能的函数:AtlModuleUpdateRegistryFromResourceDO。 8 . 6 小结 本章讨论了有关注册表的主题。如果你打算开发组件应用程序,注册表作为COM+服务程序 信息的储存库,是个很重要的主题。 我们给出了一个特殊的类,使你更容易对注册表进行访问,而无需依赖某些更受限制的API 调用。我们同时讨论了这个类和它的用途。 我们还讨论了如何使用Regedit和RegEdt32来学习 、 理解和编辑注册表。第9章 COM+的最优化、继承及集合 本章内容包括•. • DCOM的速度 • 远程激活 •远程引用计数 • 代理进程 • IClassFactory •继承 COM+不像某些人认为的那样比等同的(:++慢2000倍 ,慢6倍 (或者每个对象的构造函数调 用大约要慢20奄 秒 )可能更准确一些。在本章,我将解释这个速度差异是如何得到的。 注意当我谈到速度差异时,我指的是在某一台计算机上进行编译的结果,其他计算机 可能会产生不同的结果。在我的装有Windows 2000的计算机上,COM+要比等同的C++ 慢11倍。 成偶然间谈到了等同的C++。C++不是一种支持调用进程外对象方法的语言,所以 当我使用“等同的C++” 这个术语时,我指的是等同的进程内C++。显然,比较进程内 C++和进程外DCOM是不公平的。然而,在我比较不同类型的DCOM调用的速度时,我 把等同的C m ■当作一条基线。 9 .1 D C O M 的 速 度 那 么 ,为什么DC0M要慢6倍或20毫秒呢?你可以将它归结于C0M+所支持的安全性、远程 控 制 、位 置 透 明、类工厂以及多重接口。当你调用COM+对 象 时 ,COM+将搜索注册表来确定 你是否有权访问这个对象、对象是否需要在远程计算机上创建、以及对象需要在进程内创建还 是在进程外创建。COM+还要花费宝贵的时间获得类工厂,用于创建客户程序所需的C0NU类 和接口。 然 而 ,这些时间 没有被浪费。安 全 性 、远 程 控 制 、位 置 透 明 、类工厂以及多重 接口都是 DCOM提供的很有价值的特性。而C++本身不提供这些特性中的任何一种。所 以 ,当你在决定是 否值得花费这20奄秒的时候,权衡一下这20毫秒和上述的特性。 20毫秒的延迟发 生 在 对 象被创 建(或 被 删 除 )的时候,而不是在对对象进行方法调用的时 候。这在你决定是否愿意引入COM+的系统开销时,是很重要的因素之一。也就是说,如果在 你的程序运行中对象的构造次数很多,那么这种系统开销可能会很明显;而如果在你的程序运 行中对象的构造次数很少,那么这种系统开销可以被忽略。 在这一点上,有些人会说,“你还没看见我的COM+应用程序有多慢我相信你。我筲经见第9 拿 COM+的最优化、继承及集合 143 到过一些应用程序运行得像乌龟一样慢,而这就是为什么我们要讨论到底发生了什么,使你的 应用程序这么慢。 9 . 1 . 1 对象定位 在设计DCOM对象 时,有几个问题是很重要的。关键的问题是,“ 在对象的服务程序和客户 程序之间,存在的最低限度的定位关系是什么? ” 对于客户进程来说,对象的服务程序必须是 远程的还是本地的(也就是说,是进程内的还是进程外的 )?对于客户机来说,对象的服务程 序必须是远程的还是本地的?记住 ,进程内服务程序比等同的C++大约慢6倍 ,而DCOM的进程 外服务程序要比等同的C++慢2000倍 。 慢2000侪就成问题了。你的目标应该足,尽可能地使COM+对象服务程序相对于客户程序来 说 ,是本地的。 ‘ 一年前,我的一个朋友为我们提供了一个不良设计的好例子,这个设计就是将COM+对象放 在进程外。他设计的是一个数据库连接池管理器。在这个应用程序中,为了提供对数据库连接 的单点访问,连接管理器对象不得不被放在进程外。不幸的是,这意味若中.独的数据库连接也 是逬程外的。这 样 ,每个对进程外服务程序的调用都造成2000倍的时间损失。最 终 ,该应用程 序可以同时处理(最多)三 项事 务 ,而每秒处理的要稍微多一些。因为只能同时使用三个数据 库连接,所以就根本不再需要数据库连接池了。 这种类型的不良结构就是某些人认为COM+很慢的原因。我不责怪他们。产生不良结构是正 常 的 ,因为大多数COM+程序员不能分辨出进程内和进程外服务程序的差别。 进程外服务程序很慢,然而远程服务程序更慢。我无法准确说出到底有多慢,因为这主要 依赖于两台 计算机 之间的 网络距 离。只要 可 能 ,就 要尽蜇减 少计算机间环路的 数量。环路很 重要 。 9 . 1 . 2 网络循环 在上面例子的项目设计上还存在着另一个典型失败。业务引擎是用来在客户端远程运行的, 而对业务对象的调用是建立在属性接M 性的基础上的。客户端可执行程序始于载入一小部分业 务模型,或大约1,000,000原子项。每个属性是通过分別进行PROPERTY GET调用获得的。 想象一下,在两台计算机之间来回传输1,000,000个包要用多少时间c —个包从客户端发送 和另一个包从服务器返回被称为一个循环,上一个例子中就有丨,000,00()个循环。使用这种设汁, 客户永远也不可能一次成功载人。正确的设计应该一次得到尽可能多的项。 另一次偶然的机会,我得到一个非常快的业务引擎,它 可 以 一次得 到完 整的 对 象 (数据库 记 录 )。客户端可执行程序仍然要载入业务模型的一小部分,大约1,000,000个原子项或100,000 行 。四十五分钟后,客户端载入完毕。通 过 按 行 (而不 是按原子)打 包 (调 度 )业务 模 型 ,可 以将启动时间从无穷大减少到四十五分钟。怛 是 四 i •五分钟仍然不能接受。 于 是我决 定 一次传输多条记 录。在我发 现 了 一次传输的记 录个 数 的 最优值之 应 ,我可以 在 不 到 一 分钟的 时间内载入客户程序。一个好的通用原则是学习 优化方 法,并在试验基础上 编 程 U144 第二部分高级COM编程技巧 9 . 1 . 3 混合线程模型 另一个降低COM+速度的 因素 是混 合线程 模 型 。旧的单线程COM+类不必是线程安 全的 , 因为所有的COM+方法调用都在Windows的消息队列中被串行化。由于这些原有的COM+类不需 要线程安全的使用条件,COM+必须继续串行这些对原有COM+类的调用。这对于进程外服务 程 序不 成 问题,因为COM+对象位于它们自己的单线程单元中,并且COM+方法调用被适当地 串行化。 而当COM+类是进程内服务程序一部分的时候,问题就出现了。因为进程内COM+对象位于 创建这个对象的线程单元中 , 这个COM+对象可能在单线程单元中创建,也可能在多线程单元 中创建。线程模型的选择取决于调用客户,而不是服务程序设计者。如果COM+对象在多线程 单元中创建,并且是线程不安全的,就有问题了。 当客户要求的线程模型与服务程序线程模型不符时,COM+通过创建另一个单元来解决这个 问题,进程内COM+对象将在新的单元中创建。 在第8章 中 ,我们讨论过使你可以指定进程内服务程序线程模型的注册表键,COM+使用这 个注册表键值来确定进程内服务程序的线程模型。如果进程内COM+服务程序的线程模型与客 户要求不符,COM+将在一个新的单元中创建对象,这样可以迫使COM+方法调用在单元之间进 行调度。当COM+方法调用在消息队列串行化时,上述方法会导致另一个性能问题。 在CD-ROM上 ,我放了一个名为Dcomvscpp的项目 ,位于Chapter09目录下的一个子目录中。 我用这个项目来确定相对于等同的C++的DCOM的速度。这个项目的核心是控制器可执行程序, 它可以在几毫秒内创 建、调用并删除不同类型的对象。清单9-1给出了这个可执行程序的所有 代码。 清单9 - 1 控制器源代码 II controller.cpp : Defines the entry point for the console application. // ^include “stdafx.h“ ^include ■••/cpp/cpp.hu ^include *../dcom/dcom.h“ •include '../dcom/dcom^i.c' ^include ■../dcomserver/dcomserver.h“ #include “../dcomserver/dcomserver^i.c“ CRITICAL SECTION cs ; DWORD WINAPI CppThread(void * p) { DWORD dw = ::GetTickCount(); for (int )=0;j<(int)((DWORD *)p)10] ;j + + ) { C C p p * i = n e w C C p p ( ); i->Hello(); d e l e t e i; } d w = : '.GetTi c k C o u n t () . dw; ::EnterCriticalSection(&cs);第9 章 COM+的最优化、继承及集合 J45 std::cout « _\nCppTh「ead ticks ■ « dw « std::endl; ::LeaveCriticalSection(&cs); r e t u r n 0; static IClassFactory * pclassfactory = NULL; HRESULT GetUnknown(CLSID clsid, REFIID riid, IUnknown ** ppunknown) { if (pclassfactory==NULL) { * ::CoGetClassObject(clsid, CLSCTX^ALL, NULL, IID_IClassFactory, (void **)&pclassfactory); if( pclassfactory == NULL ) r e t u r n ( -1 ); > pclassfactory->CreateInstance(NULL, riid, (void **)ppl)nknown); return S_0K; }; DWORD WINAPI DComThreadServer(void * p) { ::Colnitialize(NULL); DWORD dw = ::GetTickCount(); for (int j=0;j<(*((OWORD *)p));j++) { I T e s t S e r v e r * i; GetUnknown(CLSIO_TestServer, IID^ITestServer, (IUnknown **)&i); i->Hello(); i - > R e l e a s e ( ); } pclassfactory->Release(); pclassfactory=NULL; dw = ::6etTickCount() • dw; ::EnterCriticalSection(&cs); std::cout « n\nDComThread server ticks “ « dw « std::endl; ::LeaveCriticalSection(&cs); ::CoUninitiali 2e ( ); r e t u r n 0; DWORD WINAPI DComThread(void * p) { ::CoInitialize(NULL); DWORD dw = ::GetTickCountO; for (int j=0;j<(*((OWORD *)p)) ;j++) { I T e s t O b j e c t * i; GetUnknown(CLSID_TestObj ect, IID_ITestObject, (IUnknown **)&i); i->Hello(); i->Release(); } pclassfactory•>Release(); pdassfactory=NULL; dw = ::GetTickCount() - dw; •• :EnterCriticalSection (&cs); std::cout « “\nOComThread ticks “ « dw « std::endl; ::LeaveCriticalSection(Acs);146 第二部分高级COM编程技巧 ::CoUninitialize(); r e t u r n C; DWORD WINAPI DComThreadServer2(voicl * p) { ::CoInitialize(NULL); DWORD dw = ::GetTickCount(); for (int j=0;j<(*((DWORD *)p)) ;j++) { ITestServer * i; GetUnknown(CLSID_TestServer, IID__ITestServer, (IUnknown **)&i); i->Hello(); i->Release(); } pclassfactory->Release(); pclassfacto 「y = N U L L ; dw = ::GetTickCount() • dw; ::EnterCriticalSection(&cs); std::cout « “\n01d DComThread server ticks “ « dw « std::endl; ::LeaveCriticalSection(&cs); ::CoUninitialize(); r e t u r n 0; DWORD WINAPI DComThread2(void * p) { ::CoInitialize(NULL); DWORD dw = ::GetTickCount ( ); for (int j=0;j<(*((DWORD *)p)) ;j++) { ITestObject * i; GetUnknown(CLSID_TestObject, IID_ITestObject, (IUnknown i->Hello(); i->Release(); } pclassfactory->Release(); pclassfactory=NULL; d w = : : G e t T i c k C o u n t () • dw; ::EnterCriticalSection(&cs); std::cout « M\n01d DComThread ticks “ « dw « std::endl; ::LeaveCriticalSection(&cs); ::CoUninitialize(); r e t u r n 0; int main(int argc, char* argv[]) ::InitializeCriticalSection(&cs); DWORD dw=0; DWORD times=10000; HANDLE handle; handle = ::CreateTh 「ead(NULL, 0, DComThread2, ×, 0, &dw); ::WaitForSingleObj ect(handle, INFINITE); ::CloseHandle(handle);第9 章 COM+的最优4匕、继承及集合 147 handle = ::CreateThread(NULL, 0, DComThreadServer2, ×, 0, &dw); ::WaitForSingleObj ect(handle, INFINITE); ::CloseHandle(handle); handle = ::CreateThread(NULLf ::WaitForSingleObj ect(handle, ::C l o s e H a n d l e (h a n d l e ); handle = ::CreateThread(NULLf :tWaitForSingleObject(handle, ::CloseHandle(handle); handle = ::CreateThread(NULL, •• :WaitForSingleObj ect (handle, ::CloseHandle(handle); 0, DComThread, ×, 0f &dw); INFINITE); 0, DComThreadServer, ×, 0, &dw); INFINITE); 0, CppThread, ×, 0, &dw); INFINITE); DeleteCriticalSection(&cs); r e t u r n 0; 这个控制器创建了 £ 个不同的线程来测试五种不同类型的对象。这五个对象分别是C++对象 (CppThread),进 程 内单 线程 模 型(DComThread ),进 程外单 线程 模 型(DComThreadServer >, 进 程内混合线程模型(DComThread2 ) , 以及进程夕卜混合线程模型(DComThreadServer2 )。 这个可执行文件说明了,使用与客户线程模型不同线程模型的进程内COM+服务程序,几乎 和进程外服务程序一样慢,参见表9-1和图9-1。这是因为COM+在单元之间进行调用的调度。 表9-1 350MHz Pentium的时钟周期对比 线 程 周 期 老的 DComThread 73556 老的DcomThread服务器 78984 DcomThread 200 DcomThread服务器 76320 CppThread 10 图9 - 1 运行控制器程序的结果148 弟 二 部 分 高级COM编程技巧 这里的教训是,为了提高运行速度,进程内COM+服务程序的线程模型必须与调用客户的线 程模型相同。 不幸的是,进程内服务程序的线程模型不可能总是与所有调用客户的线程模嘲相 同c COM+提供了一种能力,使你可以指定进程内COM+服务程序既属于单线程模彻又域于多线 程模型。为了使你的进程内服务程序可以用于两种线程模型,必须保证这个服务程序中的 COM+类是线程安全的。 所 以 ,为了® 大地提高进程内服务程序的运行速度,必须在注册表中指定此服务程序可以 既属于中线程模型又属于多线程模型,而且它还必须是线程安全的。 我们已经讨论了一些优化方法,当正确地应用这些方法时,可以极大地提高应用程序的速 度 。但 是 ,还有很多的优化方法我们没有讨论。而帮助你进行优化的最好方法就是,弄清程序 中到底发生了什么,并使用这些信息找到适用于你己的应用程序的优化方法。 我 们将幵始 讨 论 “D” ( distributed ) 在DCOM中是如何工作的。COM+是如何支持远程激活、 引用计数、ping以及调度的呢? 9 . 2 远程激活 服务控制管理器(SCM)是负责运行本地和远程COM+服务程序的,它使用IRemoteActWation 接口运行远程COM+服务程序。淸单9-2给出了IRemoteActivation接口的定义。 清单9 - 2 旧emoteActivation 的接口定义 interface IRemoteActivation { HRESULT RemoteActivation( (in] handle_t hRpc, {in] ORPCTHIS *0RPCthis, [out] ORPCTHAT • O R P C t h a t , (in) G U I D * C l s i d , [in, string, unique] WCHAR •pwszObjectName, [in, unique] MlnterfacePointer *pObjectStorage, 【i n 丨 D W O R D ClientImpLevel, (in] D W O R D M o d e , (in] D W O R D Interfaces, [in,unique,size^is(Interfaces ) 丨 I I D ★ pIIDs, I i n 】 unsigned short cRequestedProtseqs, I in, size_is(cRequestedProtseqs)] unsigned short RequestedProtseqs[] [ o u t ] 0 X 1 0 * p O x i d , { o u t 丨 DUALSTRINGARRAY **ppdsaOxidBindings, [ out] I P I D ♦pipidRemUnknown, [ o u t 丨 D W O R D ♦pAuthnHint, lout] COMVERSION •pServerVersion, [out] HRESULT * phr, [out,size_is(Interfaces)1 MlnterfacePointer **pplnterface0ata, [out,size_is(Interfaces)J HRESULT * p R e s u l t s 客户机上的SCM通过使用RPC函数RemoteActivateO调用服务器上的SCM,服务器SCM读取 Clsid参数并使用这个CLS1D创建一个本地的COM对 象 ,这个对象将被所有与pHDs数组参数匹配第9 章 COM 十的最优化、继承及集合 149 的接口查洵。服务器SCM返回一个调度接丨丨数组(ppImerfaceData )c RPC函数RemoteActWatcO还返回一个接口指针标识符(IPID)。IPID与接口指针相似,只不 过IPID还可以识别这个接U 背后的活动对象所处的服务器。返回的pipidRemUiiknown参数就是 对象的IRemUnknown接 U 的IPID。IRemUnknown接口将在本章后面部分进行讨论。 MlnterfacePoimer结构包含了要返回给客户机的调度接口指针。现 在 ,客户机上的SCM就可 以使用返回的数据结构引用远程对象了。 9 . 3 远程引用计数 —个应用程序到底调用r 多少次IUnknown::AddRef()和IUnknown::ReIease()方法呢?北美有 多少妈蚁呢? 因为COM+使用IUnknown::AddRef()和IUnknown::Release()方法进行引用计数,所 以 ,如果你每次都要在有人调用引用计数函数时等待一次网络循环,就会造成系统性能的下降。 为了消除这个潜在的缺陷,DCOM实现了IRemUnknown接口。清单9-3给出了IRemUnknown接 口的IDL ( 接 U定 义 语 言 )。 清单9-3 IRemUnknown的接口定义 interface IRemUnknown : IUnknown { HRESULT RemQueryInterface( [in] REFIPID ripid, [in] unsigned long cRefs, Iin] unsigned short clids, [in, size_is(clids)] IID * i i d s , lout, size_is(clids) REMQIRESULT ** ppQIResults); HRESULT RemAddRef( [in) unsigned short clnterfaceRefs, (in, size_is(cInte 「faceRefs}】REMINTERFACEREF InterfaceRefs[], [out, size_is(cInterfaceRefs)] HRESULT * pResults); HRESULT RemRelease( I in] unsigned short clnterfaceRefs, [in, size_is(cInte 「f a c e R e f s } 丨 REMINTERFACEREF InterfaceRefs[]); 客户调用IRemUnknown::RemAddRef()和IRemUnknown::RemRelease()方 法 , 来增加和减少 01 D ( 对 象 标 识 符 )的 引用 计 数 。这 将 减 少 网 络 循 环 数 ,从一个循环进行一次对 IUnknown::AddRefO或〖Unknown::Release()方法的调用 ,变为一个循环进行多次对丨Unknown:: AddRef()或IUnknown::Release()方法的调用。对IUnknown::AddRef()和丨Unknown::Release()方法 的调用不能直接转变为对IRemUnknown: :RemAddRef()和IRemUnknown: :RemRelease()方法的调 用。只有在对所有对象的IPID的所有本地引用被释放以后, 标准代理程序的实现才会进行 IRemUnknown::RemRelease()方法的调用。 你无论在什么时候使用远程对象,记住 , 对lUnknown::AddRef(>和IUnknown::Release()方法 的调用都是优化的。 但 是 ,当一个调用者对象没有调用IRemUnknown::RemRelease()方法就异常终丨t 的时候,会 发生什么呢?150 第二部分高级COM编程技巧 Ping客户 敁终 ,COM+服务程序的对象将被释放。COM+尝试ping客 户 ,并在指定的尝试失败次数之 后 ,释放所有对COM+服务程序对象的引用。如果ping发生在远程对象上,可以证明,上述方法 的效率是很低的。Ping结合起来可以生成ping集合,而ping集合要尝试ping的是每台远程计算机, 而不是每个对象。 你可能还想知道COM+是如何实现远程进程内服务程序的。我们使用代理进程来实现远程进 程内服务程序。 9 . 4 代理进程 代理进程是在Service Pack 2.0中被引人Windows NT 4.0的 。最初的DCOM只支持对进程外服 务程序的远程访问,而不支持远程进程内服务程序。拥有远程进程内服务程序是很矛盾的事,因 为拥有跨处理器的进程内对象是不可能的。为了远程使用进程内服务程序,COM引入了代理进程 的概念。代理进程在远程计算机上启动,同时进程 内对象被载入代理的进程空间(参见图9-2)。 某些引人关注的优越性可以用于代理进程。该 进程可以拥有一个与调用进程不同的安全环境。由 恶意编程导致的、发生在进程内服务程序中的故障, 对于代理进程空间来说是有限的,并且可以通过限 制代理进程的安全环境来预防此类事情的发生。代 理进程空间中的故障和受约束的代理安全环境,都 不会影响调用进程。 最 大 的缺点是 速度问题(因为进程外调用比进程内调用慢 >,以及不能共享进程内存空间。 虽然进程内服务程序可以与调用者共享进程内存空间,但是在涉及到代理进程时这是不可能实 现 的 。可以通过让代理釆用特殊的方式获得和更新调用者的进程内存空间的方法,来模拟对进 程内存空间的共享,但 是 ,这将导致代理进程性能的下降。 清单9-4给出了一个调用进程内服务程序的例子,但是它将服务程序载入了本地的代理进程。 其他的代码在CD-ROM上可以找到,位于Chapter09目录下的Surrogate子目录中。 清单9 - 4 调用代理DLL #include #include #include ^include “inprocessWinprocess.h* ^include “inprocess\\inprocess_i.c_ char s[1024]; HRESULT GetUnknown(WCHAR * strProgID, IUnknown ** ppUnknown) { CLSID clsid; HRESULT hRes = ::CLSIDFromP 「o g I D ( s t r P 「ogID, &clsid);茗9章 COM+的最优化、继承及集合 151 if (FAILED(hRes)) { II CLSIDFromProglD failed std::cout « “CLSIDFromProglD failed = _ « _ltoa(hRes, s, 16) « std::endl; ATLASSERT(FALSE); return hRes; I C l a s s F a c t o r y * pCF; hRes = ::CoGetClassObj ect(clsid, CLSCTX_LOCAL_SERVER, NULL, IIDIClassFactory, (void **)&pCF); if (FAILED(hRes)) { *ppUnknown = 0; std::cout « “CoGetClassObject failed = • « _ltoa(hRes, s, 16) « std::endl; ATLASSERT( FALSE ); return hRes; } hRes = pCF->CreateInstance(NULL, IID_IUnknown, (void **)ppunknown); pCF->Release(); if (FAILED(hRes)) •ppunknown = 0; std::cout « “Createlnstance failed = _ « _ltoa(hRes, s, 16) « std::endl; ATLASSERT(FALSE); return hRes; } return S OK; } ; HRESULT Get Interface(IUnknown * pUnknown, REFIID riid, IUnknown ** ppunknown) { HRESULT hRes * pUnknown->QueryInterface(riid, (void **)ppunknown); if (FAILED(hRes)) { II Querylnterface failed std::cout « “Querylnterface failed = _ « _ltoa(hRes, s, 16) « std::endl; ATLASSERT(FALSE); return hRes; return S_OK; } ; int m a i n ( ) { CoInitialize(NULL); std::cout « 'Starf « std:tendl; I U n k n o w n * p U n k n o w n ; HRESULT hRes = GetUnknown(L_Inprocess.MyInterface.1•, &pUnknown);152 第二部分高级COM编程技巧 if (FAILEO(hRes)) { CoUninitialize(); II GetUnknown failed return hRes; IMylnterface * p; hRes = Getlnterface(punknown, IID_IMyInte 「face, (IUnknown **)&p); if (FAILED(hRes)) pUnknown->Release(); CoUninitialize(); II GetUnknown failed return hRes; } long 1=0; . p->get_processid(&l); std::cout « “Object Process = - « l « std::endl; P * >get_threadid(&1); std::cout « “Object Thread = • « l « std::endl; std::cout « 'Process = “ « ::GetCurrentProcessId() « std::endl; std::cout « “Thread = “ « ::GetCurrentThreadId() « std::endl; std::cout « “End“ « std::endl; p->Release(); punknown->Release(); C o U n i n i t i a l i z e ( ); r e t u r n 0; >_________________________________;_________________________________________________________________________________ 要肴重注意的是,我在调用CoGetC丨assObject()g数 时 ,使用了CLSCTX_LOCAL_SERVER 作为类环境参数。如果我指定CLSCTX_SERVER或CLSCTXJNPROC_SERVER作为类环境,逬 程内服务程序就不能使用代理进程。我手工 编 写 了清单9-5中 的 注 册 表 设 置 ,并且使用我的 DURegisterServer()函数将这些设置写人注册表。 在使用代理时,可以使用标准代理,或者自己创建一个自定义的代理。为了使进程内服务 程序可以使用代理,要将客户机上注册表AppID主键中的RemoteServerName键值设》为远程服 务器的地址或名字。为了使用标准代理,要将服务器上注册表AppID主键中的DUSuirogate键值 设置为空。为了使用自定义代理,要将DUSuirogate键值设置为你的自定义代理模块的路径。淸 单9-5给出了必要的注册表设置,使进程内服务程序可以使用代理。 清单9 - 5 代理活动注册表设置 [HKEY^CLASSES_ROOT \ AppID \ {E7523002-4382 -1 1D2-BEA2-00C04F8B72E7}) = s _i n p r o c e s s • [HKEY_CLASSES^ROOT \ AppID \ {E7523002-4382•1102-BHA2-00C04F8B72E7}J .OllSurrogate = s '' [HKEY_CLASSES_R00T \ CLSID \ {E7523002-4382•1102•BEA2•00C04F8B72E7}J AppID = {E7523C02•4382•11D2-BEA2-O0C04F8B72E7}第9章 COM♦的最优化^继承及集合 J53 自定义代理 对于大多数对象实现来说,,默认的代理程序就足够了。而当你的进程内服务程序需要访问 那些通常只在客户进程中可用的全局内存时,默认代理程序就不能胜任了。在写自定义代理程 序时,代理实现的是ISurrogate接口。清单9-6给出了ISurrogate接口的定义。 清单9 - 6 代理的接口定义 interface ISurrogate : IUnknown { HRESULT LoadDllServer((inl REFCLSID clsid); HRESULT FreeSurrogate(); 代 理必 须调用CoRegisterSurrogate()函数 为自 己进 行注册。COM+将 会调用ISurrogate:: LoadDUServer()方法载入服务程序,以及调用ISurrogate::FreeSurrogate()方法释放代理。 9.5 ICIassFactory 类工厂过去常常被认为是COM+所要表达的意思,而COM+中最重要的接U 就是类工厂。随 着时间的流逝,类工厂渐渐失去了它往日的声望,这是因为所有关于类工厂的话题可说的都已 经说了。大多数COM+框架使用藏在宏定义层背后的样板代码来实现一个类的ICIassFactory类。 ICIassFactory接 口 仍 然 是 C O M +— 切 内 容 的 中 心 。 当你调用CoCreat?Instance()或者 CoCreateInstanceEx()函数,创建COM+对象时,你本质上调用的是ICIassFactory:: Createlnstance() 方法。 你可能已经注意到COM+的DLL导出一个名为DUGetClassObjectO的函数,COM+调用这个 函数,从你的进程内服务程序得到类工厂。图9-3描述了COM是如何使用DUGetClassObjectO函 数和ICIassFactory类来创建一个新的COM对象的。注 意 ,对CoCreateInstance()函数的调用是怎 样创建或得到一个类工厂实例的。接下来,类工厂又是如何创建对象。 清单9-7是一个简化版本,描述了CoCreateInstance()函数是如何使用ICIassFactory对象的。 清单9-7 CoCreatelnstance()函数的代码 HRESULT CoCreatelnstance(REFCLSID clsid, IUnknown * pUnkOuter, DWORD grfContext, REFIID iid, void * ppvObj) { ICIassFactory * pCF; HRESULT hr = ::CoGetClassObject(clsid, grfContext, NULL, IIDIClassFactory, (void **)&pCF); if (FAILED(hr)) { r e t u r n hr; } hr=pCF >CreateInstance(pUnkOuter, iid, (void **)ppv); p C F - > R e l e a s e ( ); if (FAILED(hr))154 第二部分高级COM编程技巧 *ppv = 0; } return hr; CoCreatelnstance 通过 DllGetClassFactory 检索丨 ClassFactory 调用 ClassFactor::CreateInstance 创建新的COM 对象 图9 - 3 使用类库创建COM对象 CoCreateInstanceEx()函数有相似的实现过程(参见清单9-8 )。CoCreateInstanceEx()函数为了 得到多于一个的对象接口,使用了一个MULTI一 QI结构数组。清单9-8中的代码是好像实现了某些 优化的伪代码。CoCreatelnstanceExO函数也可以用于选择在哪一台服务器上得到对象例示。 清单9-8 CoCreatelnstanceExO函数的代码 struct tagMULTI_QI { REFIID riid; void * pvObj; HRESULT hr; } MULTI_QI; HRESULT CoCreatelnstanceEx(REFCLSID clsid, IUnknown * punkOuter, DWORD g「fContext, COMSERVERINFO * pServerlnfo, DWORD dwCount, MULTI_QI * rgMultiQI) { IClassFactory * pCF; HRESULT hr = ::CoGetClassObject(clsid, grfContext, pServerlnfo, IID_IClassFactory, (void * )pCF); if (FAILED(hr))第9 章 COM+的最优化、继承及集合 155 return hr; } hr=pCF->createlnstance(pUnkOuter, iid, (void *)ppv); pCF*>Release(); for (DWORD i=0; iQueryInterface(rgMultiQI[i].riid, &rgMultiOI[i].pvObj); } if (FAILED(hr)) { *ppv = 0; > return hr; > 有了类工厂对象,你就可以做一些在COM中被认为是不可能的事情:实现继承。 9 . 6 继承 c o m +使你既可以使用接口的继承也可以使用实现的继承。使用接u 继 承 ,一 个接n 可以从 另一 个接口继承方 法。接口继承的 一 个例子就 是IDispatch接 口 ,它从IUnknown接 U 继承了 QuerylnterfaceO、AddRef()和Release()三个方法c COM+也允许你使用实现的继承,这种类型的继承使一个COM+类可以从另一个COM+类继 承实现过程。在COM+中 ,实现继承没有完全被实现,也就是说,继承了另一个COM+类的实现 过程的COM+类 ,无权访问被继承类的实现数据。因为有这个缺点,所以有时会说COM不支持 实现的继承。 在COM+中 ,提供了两种类型的实现继承:包含继承和集合继承,通过使COM+类重新实现 被继承类的方法来实现包含继承,而这些实现只是简单地重新定向对所包含的COM+类的调用。 集合 集 合 继 承 使 客 户 可 以 直 接 调 用 被 继 承 的 方 法 。这 种 方 法 的 一 个 缺 点 是 ,被集合对象 的 IUnknown方法必须与集合者的IUnknown方法集成在一起。另一个缺点是,只有进程内服务程序 可以实现集合继承。 我们使用IClassFactory接口实现集合继承。每个COM+类 ,不管它是否能被集合,都必须实 现一个类工厂对象,用这个对象来实现IClassFactory接口。类工厂的责任就是创建COM+类 。清 单9-9给出了IClassFactory接口的定义。 清单9 - 9 丨ClassFactory接口的定义 interface IClassFactory •• IUnknown HRESULT Createlnstance(IUnknown * punk,156 第二部分高级COM编程技巧 REFIID iid, void * * ppvobj); HRESULT LockServer(BOOL fLock); 所有的COM+对象都是通 过调用ICIassFactory::Create丨nstance()方 法 时创 建 的 :如果对 ICIassFactory::CreateInstance()方法的调用的第一个参数是NULL,创建出的对象将+ 能被集合。 如果这个参数是一个指向IUnknown的 指针,创建出的对象将可以被集合,并且这个对象将把所 有对IUnknown方法的调用交给备用的IUnknown接口。 集合继承是COM+的一个很少被使用的强大特性。ATL使你可以使用集合特性,而无需编写 太多代码。在 集合继承中 ,集合者继承的是被集合对象的实现,你需要知道这一点,才能理解 下面的ATL代码淸单。 只要ATL类不定义DECLARE_NOT一AGGREGATABLE宏 ,它就可以作为集合体使用,这使 得编写集合体变得很容易,因为不需要在集合体中再添加额外的代码。清单9-10给出了一个3 作集合体使用的类。再说一次,在集合体中不需要额外的代码来支持集合继承。 清单9-10 Aggregate类的声明 II MyAggregate.h : Declaration of the CMyAggregate #ifndef _MYAGGREGATE^H_ #define _MYAGGREGATElH~ #include “resource.h“ // main symbols iiiiiiiiiiHiiiiuiuiinniiiin/iiiiiiiniiiiiiiiiniiiiiiiimmiiiii II CMyAggregate class ATL__N0_VTABLE CMyAggregate : public CComObjectRootEx, public CComCoClass, public IDispatchImpl { public: CMyAggregate() OECLARE_REGISTRY_RESOURCEID(IDR_MYAGGREGATE) BEGIN_C0M_MAP(CMyAggregate) COM_INTERFACE_ENTRY(IMyAggregate) COM^INTERFACElENTRY(IDiSpatCh) END_C0M_MAP() // IMyAggregate public: STDMETHOD(Hello) ( ); } ; #endif // MYAGGREGATE H ATL的集合者还要稍微复杂一些。有了前面的集合体,我就使用ATL对象向导生成了一个标第9 章 COM+的最优化、继承及集合 157 准的ATL类 ,并在其中添加了一个名为Hello的方法。淸单9-11给出了一个集合者类的声明。 清单9-11 Aggregator类的声明 II MyAggregator.h : Declaration of the CMyAggregator #ifndef ___MYAGGREGATOR_H_ #define ^MYAGGREGATOR.H“ #include “resource.h* II main symbols #include “.. WaggregateWaggregate.h“ iimiiiiiuiiiiiiiiiiiinuiiiiiiiiiiii/uniiiiimmniiiiiiinniui II CMyAggregator class ATL_NO_VTABLE CMyAggregator : puDlic CComObjectRootEx, public CComCoClass, public IDispatchIirpl { p u b l i c : CMyAggregator() void FinalRelease() { m_pAggregatel)nknown. Release (); OECLARE_REGISTRY_RESOURCEID(IDR_MYAGGREGATOR) BEGIN_COM_MAP(CMyAggregator) COM~INTERFACE_ENTRY(IMyAggregator) COM^INTERFACE 二ENTRY(IDispatch) COM_INTERFACE_ENTRY_AUTOA66REGATE_BLIND((n_pAggregateUnknown.p, CLSID_MyAggregate) END_COM_MAP() CComPtr m_pAggregateUnknown; DECLARE_GET_CONTROLLING_UNKNOWN() • // IMyAggregator public: STOMETHOD(MyHello) ( ); #endif //_MYAGGREGATOR_H_ 需要进行六处修改。我在集合者类的头文件的开始部分包括了aggregate.h头文件。我不得不 调 用 CComObjectRootEx::FinalRelease()方 法 来 释 放 集 合 的 IU nknow n对 象 。我添加广 COM_INTERFACE_ENTRY_AUTOAGGREGATE_BLIND宏 ,用以自动创建集合对象和接口指 针 。我为集合的IUnknown接口指针添加了一个成员变董。另外,还添加了 DECLARE_GET_CONTROLLING_UNKNOWN宏 。最后,我在实现文件中包括了aggregatej.c 源 文 件 (参见清单9-12)。158 第二部分高级COM编程技巧 清单9-12 Aggregator类的定义 II MyAggregator.cpp : Implementation of CMyAggregator #include “stdafx.h“ ^include “aggregator.h“ #include “MyAggregator.h“ #include #include “•.\\aggregate\\aggregate_i.c■ //////////////////////////////////////////////////////////“//////““/// II CMyAggregator STDMETHODIMP CMyAggregator: •• Hello() { II TODO: Add your implementation code here std::cout « “This is the aggregator being called“ « std::endl; return S_0K; } _ 就是这些。现在,我的集合者类已经继承了集合类的实现。清单9-13描述了如何调用集合者 类中的被集合方法。这个清单中的代码与具有两个接口的一个类完全相同。 清单9-13活动中的代理 #include ^include ^include 'aggregatorWaggregator.h“ # include _aggregator\\aggregator一i •c■ #include “aggregate、\aggregate•h■ #include •aggregate 、\aggregate_i.c“ Char s[1024]; HRESULT GetUnknown(WCHAR * strProgID, IUnknown ** ppunknown) { CLSID clsid; HRESULT hRes = ::CLSIDFromProgID(strProgID, &clsid); if (FAILED(hRes)) { // CLSIDFromProglD failed std::cout « “CLSIDFromProglD failed = “ « _ltoa(hRes, s, 16) « std::endl; ATLASSERT(FALSE); return hRes; } hRes = :'.CoCreatelnstance(clsid, NULL, CLSCTX_SERVER, IID 一IUnknown, (void **)ppllnknown); if (FAILED(hRes)) — { II CoCreatelnstance failed std::cout « 'CoCreatelnstance failed = _ « _ltoa(hRes, s, 16) « std::endl; ATLASSERT(FALSE);第9章 COM+的 最优化、继承及集合 return hRes; > return S_OK; HRESULT Getlnterface(IUnknown * pUnknown, REFIID riid, IUnknown ** ppUnknown) { HRESULT hRes = pUnknown->QueryInterface(riid, (void **)ppUnknown); if (FAILED(hRes)) { II Querylnterface failed std::cout « 'Querylnterface failed = “ « _ltoa(hRes, s, 16) « std::endl; ATLASSERT(FALSE); return hRes; return S_0K; int main() { CoInitialize(NULL); std::cout « “Start' « std::endl; IUnknown * pUnknown; HRESULT hRes a GetUnknown(L“MyAgor8Qator.MyAfloregator.1 *, &pUnknown); if (FAILED(hRes)) CoUninitializeO; II GetUnknown failed return hRes; } IMyAggregator * p_tor; hRes = GetInterface(pUnknown, IID_IMyAggregator, (IUnknown **)&p_tor); if (FAILED(hRes)) { pUnknown *>Release() ; CoUninitializeO; // Getlnterface failed return hftes; } p_tor->Hello(); IMyAggregate * p_te; hRes = GetInterface(pUnknown, IIO^IMyAggregate, (IUnknown **)&p_te); if (FAILED(hRes)) II Getlnterface failed return hRes;160 第二部分高级COM编程技巧 p_te->Hello(); std::cout « “End“ « std::endl; p->Release{); pUnknown->Release(); CoUninitialize(); return 0; 当你运行集合继承可执行程序时,控制程序在开始创建对象,并获得一个指向这个对象的 IMyAggregate接口的指针。这个接口没有被集合,而且在调用He丨lo()方法时也没什么特别之处。 接下 来 ,集合获得一个指向它的IMyAggregate对象的指针。这个接门将和其他所有没有被集合 的接口一 样地被获得。读一 下控 制程 序 ,看起来 像 是没什么 特别的 事 会 发 生 ,但 是 ,实际上 COM从被集合对象那里获得了接口指针。 当HelloO方法被调用时,它是在被继承的类中被调用的,这 样 ,你就通过集合完成了实现 的继承。其他的代码在CD-ROM上可以找到,位于Chapter09目录下的Aggregation子 H 录中。 9 . 7 小结 你可以在不知道本章任何内容的情况下使用COM进行编程,我没介绍什么非常重要的东西。 但 是 ,本章讨论了很多你应该收藏起来的技巧,在你认识到你的COM应用程序太慢,或者在你 确实愿意继承COM对象的实现时,拿出来运用。 按照本章前面讨论的基准测试运行你的COM+应用程序,你可能会为可以用来进行改进的空 间之大而感到惊讶。记 住 ,远程激活会使你有意外的收获,多次的试验揭示了一个你无法预知 的事实。 代理进程是另一个很重要的内容。 了解一下代理进程,看看你是否能够提高远程激活的性能。 你还应该对应用程序做另一个试验:以某个方法的调用作为衡量标准,看看对象的创建过 程是怎样花费系统开销的。这 很容 易,只要修改你的代码来度量创建过程,然后调用这个方法 100,000次。第10章使用NT服务 本章内容包括: • 剖析服务 • ATL和服务 • 为使用服务而提供的工具 • OpenSCManager() •经由注册表安装服务 •使用事件日志 • 调试你的服务 服务是一些现在才渐渐开始受到开发人员青睐的强大特性,而服务没有被广泛使用的一个 原 因 是 ,只有很有限的几本书描述了如何创建服务。另一个原因则是需要很专业的知识来使用 服务。 假设你希望你的Windows 2000在每次启动时都启动某个进程,有一个服务可能就可以帮忙, 特别是在这个进程不需要用户接U 的时候。例 如 ,如果你需要一个进程查询时间,并且每过一 定的时间间隔,就执行一个数据库更新函数,那么你可以创建一个服务,自动地完成以上任务。 有限数量的文献不代表编写这些服务很难。正如你将在本章第一部分中学到的,创建一个 服务是很容易的。与服务之间的通信是问题所在,但这是为什么呢?为了解释这个难题,最好 我们先讨论一下什么是服务,以及为什么你需要它。 注意本章 所讨 论的 内容不能 应用 于Windows 9x平台上。 当一个用户登录到一个工作站上(机器 环 境 ),他 立 即 会 被分配一 个 桌面(用户 接 口 ),并 且桌面被激活。当这个用户启动一个新的进程或线程时,这个进程或线程就被加入到桌面,这 就是为什么你会在桌面上看到所有的GUI窗口。 我们可以创建另外的桌面,并把线程分配到这些桌面上。你可以在每个线程中都创建窗U, 但 是 ,只有那些位于当前桌面的线程中的窗口才可见。如果你切换桌面,另外一组窗口将出现, 这些新出现的窗口属于新的当前桌面的线程。清单10-1给出的代码可以生成本段所描述的事件 (MSDN提供了一个完整的示例,源代码叫Switcher)。 清单10-1切换桌面 #include HDESK hDesktopOrig = NULL; HDESK hDesktop = NULL; DWORD WINAPI ThreadProc(void * p)162 第二部分高级COM编程技巧 ::MessageBox(NULL, “This is on the desktop“, “Hey“, MB_OK); return 0; > DWORD WINAPI AlternateDesKtopThreadProc(void * p) { ::SetThreadDesktop(hDesktop); ::MessageBox(NULL, “This is on an alternate desktop“, “Hey“, MB_0K); ::MessageBox(NULL, “To switch desktops“, “Click OK“, MB_0K); ::Swit chDesktop(hDesktopOrig); return 0; } i n t m a i n ( ) { hDesktopOrig = ::GetThreadDesktop{::GetCurrentThreadId()); D W O R D dw; HANDLE hThread = ::CreateThread(NULL, 0, ThreadProc, NULL, NULL, &dw); hDesktop = ::CreateDesktop(_MyDesktop* , NULL, NULL, 0, DESKTOP_CREATEMENU 丨 DESKTOP_CREATEWINDOW 丨 DESKTOP_SWITCHDESKTOP, NULL); HANDLE hAltThread = ::CreateThread(NULL, 0, AlternateDesktopThreadProc, NULL, NULL, &dw ); ::MessageBox(NULL, “To switch desktops*, “Click OK*, M B O K ) ; ::SwitchDesktop(hDesktop); ::MessageBox(NULL, “To shutdown this app“, “Click OK“, MB_0K); ::CloseHandle(hAltThread); ::CloseHandle(hThread); ::CloseDesktop (hDesktop); return 0; 上 述 代 码 使 用 f Win32 API。设置桌面的函数是SetThreadDesktopO和SwitchDesktopO。使 用CreateDesktopO创建桌面,而普通的线程兩数CreateThreadO和CloseHandleO用于创建线程c 在Win32 AP〖中 ,服务是一个可执行对象,有关它的信息被放在由服务控制管理器维护的注 册表数据库中。这个数据库里包含的信息,决定了每个被安装的服务是根据命令启动,还是在 系统启动时h 动启动。这个数据库也可以包含一些服务的登录和安全性信息,使它即使在没有 用户登录的时候也可以运行。系统管理员可以为一个特定的服务自定义安全条件。把服务器应 用程序当作服务来实现,会得到许多的好处,而这些好处来自于服务的结构。 使用一个用于远程管理和控制的惟一的服务账号,就可以把一个服务器应用程序当作一个 服务来运行。例如,如果数据库应用程序和连接主机应用程序,都运行在同一个服务器系统中, 这些应用程序可以在分开的服务账号下作为服务运行。这就保证了只有指定的主机连接管理员, 才可以管理主机连接服务器应用程序,而只有数据库管理员才可以管理数据库服务器应用程序。 这种配置的另外一个好处是,它使得管理员可以控制运行于公共用户账号下的一组服务。 如果把服务器应用程序当作服务运行,那么它在代表客户访问对象时就可以模拟客户身份。 这 种 能 力 保 证f 服务器应用程序可以代表客户进行各种动作,而不需要服务器应用程序以一种 不恰当的高特权级别运行。这也保证了服务器应用程序不会去执行那些将直接桁绝客户的动作。 无论是在计算机启动时,还是在服务被一个自动启动的相关服务启动时,服务都可以被配 置为自动启动。不管在哪一种情况下,服务都会在无人为干预时启动,也就是说,不需要用户 登录来启动服务。闪为是自动启动,便可以保证被作为服务实现的服务器应用程序在任何霜要第/0 章使用iVT服务 163 的时候都可用,只要计算机在运行。此 外 ,相关的服务可以很容妨地被启动,甚至在合适的地 方自动地被启动。 最 后 ,把服务器应用程序作为服务来实现,使我们可以使用标准的用户和Win32编程接口来 安装和控制应用程序。这样的服务既可以本地也可以远程启动和停止,这使得网络管理员可以 通过使用控制面板上的Services applet或者服务MMC, 以一种轻松而一致的方式跨越网络对服务 进行控制。 如果服务器应用程序被作为一组服务实现,那么应用程序应该在服务之间使用远程过程调 用 (或者相似的远程机制),以允许每个服务可以在不同的计算机上运行。这个分布功能提供给 用户很多好处,如更大的容量和更好的可扩展性。 回到开始的讨论上,为什么与服务进行通信很难?因为服务所处的是非活动桌面,它们通常 不与用户交互。惟一的与一个进程进行通信的方法是使用IPC ( 进程间通信)。创建一个IPC协议要 比创建一个GUI前端更加困难,这就是为什么与服务的通信比与大多数进程的通信更加困难的原因。 但 是 ,这种说法不再正确,因为ATL引入了几个新类,使得与服务的通信变得和使用COM对象一 样容易。本章后面部分将讨论,使用ATL创建DCOM服务以及访问ATL-DCOM服务有多么简单c 1 0 . 1 剖析服务 各种服务都是由服务控制管理器(SCM)启动和停止的。为了使一个模块可以像服务一样 被启动和停止,它必须在SCM中注册。 10.1.1 main( >和WinMain() 服务也是可执行的模块。我们构造服务模块,使它们既可以作为服务乂可以作为交互式进 程运行。交互式进程是指存在于某个桌面的环境中的进程。正因为服务也是可执行的模块,因 此它们必须有mainO或WinMainO人口点,就像其他所有的可执行模块一样。可执行模块入口点 的用途是初始化服务。清单10-2描述了如何初始化一个服务,也 就 是 ,如何在SCM中注册服务 的 ServiceMain()例程。 清单1 0 - 2 服务器的mainO入口点 void WINAPI ServiceMain(DWORD a r g c , LPTSTR * argv); int WINAPI main(int argc, char * argt]) { if ((argc==2) && (::strcmp(arg[1)+1, “Service*)==0)) { InstallAService(_T(“MyBigService“)); return 0; } SERVICE_TABLE_ENTRY servicetableentry[1 = { { _T(“MyBigService“), ServiceMain >, { NULL, NULL } } ; •• :StartServiceCtrlDispatcher(servicetableentry); return 0;164 第二部分高级COM编程技巧 StartServiceCtrlDispatcher()函数在SCM 中注册 了ServiceMain()例程 。SCM 将调用这个 ServiceMainO函数启动服务。清单10-3给出了SERVICE_TABLE_ENTRY结构的定义。 清单 10-3 SERVICE一TABLE_ENTRY typedef S t r u c t _SERVICE_TABLE_ENTRY { LPTSTR IpServiceName; LPSERVICE_MAIN_FUNCTION IpServiceProc; } SERVICE.TABLE^NTRY, *LPSERVICE_TABLE_ENTRY; 这 个 结 构 是 StartServiceCtrlDispatcher()函 数 的 输 入 参 数 。你 还 可 以 通 过 在 对 StartServiceCtrlDispatcher()函数的调用中指定多个SERVICE_TABLE_ENTRY结 构 ,从而在 main()入口函数中启动多个服务。清单10-4给出了一个在mainO入口函数中启动多个服务的示例c 清单1 0 - 4 服务器main()入口点 void WINAPI ServiceMain!(DWORD argc, LPTSTR * argv); void WINAPI ServiceMain2(DWORD argc, LPTSTR * argv); int WINAPI main() SERVICE_TABLE_ENTRY se「vicetat)leent「y []= { { _T(*My First Service*), ServiceMaim } , { _T(“My Second Service*), ServiceMain2 }, { NULL, NULL } } ; ::StartServiceCtrlDispatcher(servicetableentry); return 0; 假设你对StartServiceCtrlDispatcher()的调用成功, SCM就将调用你的ServiceMainO服务初始 化函数。调用StartServiceCtrlDispatcherO函数失败的原因有两个:指定的调度表中包含有格式不 正 确 的 入 口 ,以 及进 程已经调用 过 StartServiceCtrlDispatcherO 函 数 (每个 进 程只能调用 StartServiceCtrlDispatcher()函数 一次)0 10.1.2 ServiceMainO SCM 在收到来自StartServiceCtrlDispatcher()函数的一个调用后,将在一个新的线程调用 ServiceMainO函数。清单10-5描述了ServiceMainO函数是如何实现的。 清单 10-5 ServiceMainO的实现 SERVICE_STATUS servicestatus; SERVICE一STATUS_HANDLE servicestatushandle; void WINAPI ServiceCtrlHandler(0W0RD dwControl); void WINAPI ServiceMain(DWORD argc, LPTSTR * argv) { servicestatus.dwServiceType = SERVICE_WIN32; servicestatus.dwCurrentState = SERVICE_START_PENDING; servicestatus.dwControlsAccepted = SERVICE_ACCEPT_ST0P;第川章使用NT服务 /65 servicestatus.dwWin32ExitCode = 0; servicestatus.dwServiceSpecificExitCode = 0; servicestatus.dwCheckPoint = 0; servicestatus.dwWaitHint = 0; servicestatushandle = ::RegisterServiceCtrlHandler(_T(“MyBiflService ' ), ServiceCtrlHandler); if (servicestatushandle == (SERVICE_STATUS_HANOLE)0) { II If RegisterServiceCtrlHandler fails, the II handle does not have to be closed, return; BOOL blnitialized - false; II Initialize the serviceII ... // In this section, if initialization II is successful then blnitialized = true; servicestatus.dwCheckPoint = 0; servicestatus.dwWaitHint = 0; if (Iblnitialized) { servicestatus.dwCurrentState = SERVICE一S T O P P E D ; servicestatus.dwWin32ExitCode = ERROR_SERVICE_SPECIFIC_ERROR; servicestatus.dwServiceSpecificExitCode = 1; } e l s e { servicestatus.dwCurrentState = SERVICE RUNNING; } ::SetServiceStatus(servicestatushandle, &servicestatus); return; 服 务 在 它 的ServiceMainO函 数 中 首 先 要 做 的 是 初 始 化 服 务 状 态 结 构 变 量 ,并通过调用 RegisterServiceCtrlHandler()函数注册 ServiceCtrlHandler()函数。 在服务控制程序被注册以后,服务应该初始化那些与服务相关的全局结构变量。服务应该 偶尔地调用SetServiceStaUisO函数,通知SCM服务的初始化是否在正常地进行,初始化是否完成, 以及初始化是否失败。SetServiceStatusO函数有两个参数:SERVICE_STATUS结构和SERVICE— STATUSJHANDLE句柄。SERVICE_STATUS_HANDLE句柄是从RegisterServiceCtrlHandler()函 数返回的句柄。清单10-6给出了SERVICE_STATUS结构的定义。 当服务接收到一个控制请求,服务的Handler函数必须调用SetServiceStatusO,即使服务的 状态没有改变。服务还可以在任何时间,通过它的任何线程,使用这个函数通知服务控制管理 器服务状态的改变。这种主动提供状态更新的例子包括:发生在服务从一种状态变为另一种状 态 (也就是SERVICE_START一PENDING ) 时的检查点更新,和发生在服务因可恢复性错误而不 得不停止运行时的重大错误更新。服务只有在调用了RegisterServicearlHamilerO函数并得到一 个服务状态句柄之后,才可以调用这个函数。166 第二部分高级COM编程技巧 清单10-6 SERVICE_STATUS结构的定义 typedef struct _SERVICE_STATUS { DWORD dwServiceType; DWORD dwCurrentState; DWORD dwControlsAccepted; DWORD dwWin32ExitCode; DWORD dwServiceSpecificExitCode; DWORD dwCheckPoint; DWORD dwWaitHint; } SERVICE_STATUS, *LPSERVICE 一STATUS; ■ SERVICE_STATUS: :d wServiceType 成员可以具有以下值: • SERVICE_WIN32_OWN_PROCESS—— 进程中只有一个服务。 • SERVICE_WIN32_SHARE_PROCESS-进程中有多个服务。 • SERVICE^KERNEL.DRIVER—— 进程是设备驱动程序。 • SERVICE_FILE_SYSTEM_DRIVER—— 进程是文件系统驱动程序。 • SERVICEJNTERACTIVE_PROCESS—— 进程将与桌面交互。 SERVICE_STATUS::dwCurrentState 成员可以具有以下值: SERVICE.STOPPED—— 服务没有在运行。 SERVICE_START_PENDING—— IE 在初始化服务。 SERV丨CE_STOP_PENDING—— 正在关闭服务。 SERVICE.RUNNING—— 服务正在正常运行c SERVICE_CONTINUE_PENDING—— 服务被暂停并民正在恢复。 • SERVICE_PAUSE_PENDING—— 正在暂停服务。 • SERVICE.PAUSED—— 服务被暂停。 5£1<\^1€£_5 丁八丁115::(^(^01^015 八 < ^ 6 卩【6(1成 员 可 以 具 有 以 下 值 : • SERVICE_ACCEPT_STOP—— 可以停止服务。 • SERVICE_ACCEPT_FAUSE_CONTINUE—— 服务可以被暂停和恢复。 • SERVICE, ACCEPT_SHUTDOWN—— 可以关闭服务。 • SERVICE JJSER_DEF丨NED_CONTROL—— 服务接受用户定义的控制消息。 SERVICE_STATUS::dwWin32ExitCode 和 SERVICE_STATUS::dwServiceSpecificCode 两个成 员可以被用于向SCM报告错误。服务可以通过设置SERVICE_STATUS::dwWin32ExitCode成 员 , 来提交Win32错误代码。或 者 ,服务也可以通过把SERVICE_STATUS::dwWin32ExitCode设置为 ERRORJSERVICE_SPEClFIC_ERROR,并且把SERVICE_STATUS::dwServiceSpecificCode设置 为错误代码,来提交指定的错误代码。 SERVICE_STATUS::dwCheckPoint 和 SERVICE_STATUS::dwWaitHim 两个成员向 SCM 说明 服 务 的 初 始 化 或 关 闭 过 程 的 进 展 情 况 。SERVICE_STATUS::dwCheckPoirU应 该 在每 次对 SetServiceStatusO进 行 调 用 时 加 1 , 以 表 明 初 始 化 或 关 闭 过 程 的 进 展 情 况 。 成员 SERVICE_STATUS::dwWaitHint指示的是从当前SetServiceStatus()函数的调用到下一次调用之间第 章 使 用 ATT服务 167 应 该 经 过 的 最 大时间上限( 以毫秒为单位 )。如果在下一次调用SetServiceStatMm数之前巳经 超过最大时限,SCM将假定你的服务运行不正常,并把它挂起。 在你的服务通过调用RegistcrServiceCtdHandleif)函数被适3 地初始化并注册之后,SCM将 幵始向ServiceCtrlHandle()回调函数发送消息.:》 10.1.3 S erviceCtrlHandle() ServiceCtdHandleO函数可以接收五条标准消息,你也可以在128-256范围内添加额外的控制 消息。以下是五条标准消息: • SERVICE_CONTROL_PAUSE--- SCM 要求暂停服务。 • SERVICE_CONTROL_CONTINUE—— SCM 要求恢复服务。 • SERVICE_CONTROL_STOP—— SCM 要求关闭服务。 • SERVlCE_CONTROL_SHUTDOWN—— 系统正在关闭。 • SERVICE_CONTROLJNTERROGATE—— SCM 要求得到服务的状态 c 淸单10-7描述了 ServiceCtrlHandle()函数的基本结构。 清单 10-7 ServiceCtrHandler()的例子 SERVICE_STATUS servicestatus; SERVICE_STATUS_HANDLE servicestatushandle; void WINAPI ServiceCtrlHandler(DWORD dwControl) { switch (dwControl) { . case SERVICE_C0NTR0L_PAUSE : servicestatus.dwCurrentState = SERVICE_PAUSE_PENOING; ::SetServiceStatus(servicestatushandle, &servicestatus); II TODO: add code to pause the service II not called in this service // ... servicestatus.dwCurrentState = SERVICEPAUSED; b r e a k ; case SERVICE_C0NTR0L_C0NTINUE : servicestatus.dwCurrentState = SERVICE_CONTINUE_PENDING; ::SetServiceStatus(servicestatushandle, &servicestatus); // TODO: add code to unpause the service // not called in this service II ... servicestatus.dwCurrentState = SERVICE_RUNNING; b r e a k ; case SERVICE_CONTROL_STOP : servicestatus.dwCurrentState = SERVICE_ST0P_PENDING; ::SetServiceStatus(servicestatushandle, &servicestatus); 11 TODO: add code to stop the service // ... servicestatus.dwCurrentState = SERVICE_STOPPED; b r e a k ; case SERVICE CONTROL SHUTDOWN:168 第二部分高级COM编程技巧 II TODO: add code for system shutdownII ... b r e a k ; case SERVICE_CONTROL_INTERROGATE: II TODO: add code to set the service statusII servicestatus.dwCurrentState = SERVICE_RUNNING; b r e a k ; } ::SetServiceStatus(servicestatushandle, &servicestatus); 注 意 在 某 些 情 况 下 ,我直接将状态设为挂起。这应该在调用这个函数后的30秒内完成, 否则SCM会假定你的服务运行不正常,并把这个线程桂起。 月艮务至少必须响应SERVICE_CONTROL_INTERROGAT:E消 息 。而 当你在调用Register ServiceCtrlHandleO函数时,服务也必须响应那些在SERVICE_STATUS:: dwControhAccepted消 息中列举出的消息。 main()入口函数、ServiceMain()初始化函数和ServiceCtrlHandle()消息处理函数--- 就是使 一个服务能够运行起来所必需的、全部的代码。你可能还对启动、停 止 、暂停和恢复一个服务 感兴趣,可以使用OpenSCManagerO和相关的API实现这些功能。 在本书CD-ROM的ChapterlO目录下的ntservice子目录中,可以找到一个NT服务示例的源代 码 。你可 以 通过在命 令行 运行可执行 程序(在命令后面加上/Service),安装这个服务。在注册 了这个服务之后,便可以启动、停 止 、暂停和恢复这个服务,但是这个服务什么也不做。 这个服务惟一缺少的部分就是一个用于与这个服务进行对话的丨PC协 议,这个协议将使服务 可以执行它想进行的任何操作。但这一直以来是一件很困难的事情,因为IPC协议是很难开发和 使用的。不 过 ,ATL为你提供了轻松开发COM类的能力。 10.2 ATL和服务 ATL的COM AppWizard提供了一种非常容易的方法,来生成创建一个显示COM对象的服务 所必需的所有代码。从Developer Studio的菜单栏中选择File、N e w ,启动向导。选择Projects标 签并从列表框中选择ATL COM AppWizard ( 参见图10-1 )。 键入适当的项目名称和路径,你的新项目代码将生成在这个路径所指向的地方。单击OK, ATLCOM AppWizard 出 现 (参见图 10-2 )。 向导 可 以 使你很方便地 将服务程序类型 定为Service ( 服 务 )。选择了Service之 后 ,单击 Finish按 钮 ,然后单击OK。Developer Studio将生成你的服务必需的所有代码。这样就可以了。 这个短过程所生成的代码与本章前几小节所讨论的所有代码都很相似。 ATLCOM AppWizard会生成十几个文件,而其中大多数都是创建ATL项目所必需的、典型 的 样板代 码,如预编译头文件和资源脚本等等。其中一个生成文件包含了实现服务的代码。清 单10-8给出了一个由ATL COM AppWizard生成的实现的例子。使用NT服务 769 n« Pwna» | WortupM I OtlOaCM—WH ^Ckm^RMMTiptWMd Is CiMion AqiMmrS ie^MSicMdPmWMd ■ISAR VMd HB»#C AdMKCm>o»M^Pd MFCiMMMrdHfl fkHmmO^tumW^md n 叫 3]wiraiAc«fc«M gwViK Cm ^ Ai«*C4tior\ ^^wy320yMnc •myatlservice ^include “myatlservice_i #include 170 第二部分高级COM编程技巧 CServiceModule 一M o d u l e ; BEGIN_OBJECT_MAP(ObjectMap) END_OBJECT_MAP() LPCTSTR FindOneOf(LPCTSTR p1 , LPCTSTR p2) { while (p1 != NULL && *p1 1= NULL) { LPCTSTR p = p2; while (p != NULL && *p 1= NULL) { if (*p1 == *p) return CharNext(pi); p = CharNext(p); } pi = C h a r N e x t ( p l ); } return NULL; } II II Although some of these functions are big they are declared inline since they are only used once inline HRESULT CServiceModule::RegisterServer(BOOL bRegTypelib, BOOL bService) { HRESULT hr = CoInitialize(NULL); if (FAILED(hr)) r e t u r n hr; II Remove any previous service since it may point to II the incorrect file U n i n s t a l l ( ); // Add service entries UpdateRegistryFromResource(IDR_Myatlservice, TRUE); II Adjust the AppID for Local Server or Service CRegKey keyAppID; LONG IRes = keyAppID.Open(HKEY_CLASSES_ROOT, _T(-AppID , } , K E Y _ W R I T E ); if ( I R e s 丨= ERROR_SUCCESS) return IRes; CRegKey key; I R e s 工 key•Open(keyAppID, _T(B{B51 F6322•99A5-11D2-BEC8-0»Ce4F8B72E7}“), KEV^WRITE); if (IRes ERROR_SUCCESS) return IRes; key.DeleteValue(_T('LocalService“)); if (bService) { key.SetValue(_T(“myatlservice*), _T('LocalService')); Key.SetValue(_T(“-Service“) 丨 _T(■ ServiceParameters*));第JO章 使 用 NT服务 171 II hr II Create service I n s t a l l ( ) ; Add object entries = CComModule::RegisterServer(bRegTypeLib); C o U n i n i t i a l i z e ( ); r e t u r n hr; > inline HRESULT CServiceModule::UnregisterServer() { HRESULT hr = CoInitialize(NULL); if (FAILED(hr)) r e t u r n hr; II Remove service entries UpdateRegistryFromResource(IDR_Myatlservice, FALSE); II Remove service Uninstall(); II Remove object entries CComModule::UnregisterServer(TRUE); CoUninitializeO ; r e t u r n S 一OK; > inline void CServiceModule::Init(_ATL_OBJMAP_ENTRY* p, HINSTANCE h, UINT nServiceNamelD, const GUID* plibid) { CComModule::Init(p, h, plibid); m_bService = TRUE; LoadString(h, nServiceNamelD, m__szServiceName, sizeof(m_szServiceName) I s i z e o f ( T C H A R ) ); II set up the initial service status m hServiceStatus = NULL; (status. dwServiceType = SERVICE_WIN32___PR0CESS; m_status.dwCurrentState = SERVICE_STOPPED; m_status.dwControlsAccepted = SERVICE_ACCEPT_STOP; ra_status.d¥Win32ExitCode = 0; status.dwServiceSpecificExitCode = 0; mstatus.dwCheckPoint = 0; m s t a t u s •續 aitHint - 0; LONG CServiceModule::Unlock() { LONG 1 = CComModule::Unlock(); if (1 == 0 && liB_bSen/ice} PostThreadMessage(dwThreadID, WM_QUIT, 0); r e t u r n 1; BOOL CServiceModule::lslnstalled()172 第二部分高级COM编程技巧 { BOOL bResult - FALSE ; SC_HANDLE hSCM * ::OpenSCManager(NULL, NULL, SC_MANAGER_ALL__ACCESS ); if (hSCM 1= NULL) { SC_HANDLE hService = ::OpenService(hSCM, m^szServiceName, service!query_config ); if (hService != NULL) { bResult = TRUE; ::CloseServiceHandle(hService); > ::CloseServiceHandle(hSCM); > return bResult; inline BOOL CServiceModule::Install() { if (Islnstalled()) return TRUE; SC 一HANDLE hSCM = ::OpenSCManager(NULL, NULL, SC MANAGER ALL ACCESS ); if (hSCM == NULL) { MessageBox(NULL, _T(“Couldn't open service manager*), m_szServiceName, MB_OK); return FALSE; } II Get the executable file path TCHAR SZFilePath[_MAX_PATHl ; ::GetModuleFileName(NULL, szFilePath, _MAX_PATH); SC 一HANDLE hService = ::CreateService( hSCM, m^szServiceName, m_szServiceName, SERVICE 二Al_L_ACCESS, SERVICE_WIN32_0WN_PR0CESS , SERVICE—DEmSnD—START , SERVICE_ERROR_NORMAL, szFilePath, NULL, NULL, _T(_RpCSS\0-> , N U L L , N U L L ); if (hService == NULL) { ::CloseServiceHandle(hSCM); MessageBox(NULL, _T(“Couldn't create service“), m_szServiceNamey m b _ 0 K ) T return FALSE; :-.CloseServiceHandle(hService); ::CloseServiceHandle(hSCM); return TRUE; inline BOOL CServiceModule: :lininstall()弟 川 章 使用NT服务 173 XJ if (!Islnstalled()) return TRUE; SC^HANDLE hSCM = ::OpenSCManager(NULL, NULL, SC_MANAGER_ALL_ACCESS); if (hSCM == NULL) { MessageBox(NULL, _T(“Couldn't open service manager“), m _ s z S e r v i c e N a m e , M B _ 0 K ); return FALSE; } SCHANDLE hService = ::OpenService(hSCM, m_szServiceName, SERVICE_STOP 丨 DELETE); if (hService == NULL) { ::CloseServiceHandle(hSCM); MessageBox(NULL, _T(“Couldn't open service-), ra_szServiceName, MB_0K)T return FALSE; > SERVICE_STATUS status; ::ControlService(hService, SERVICE_CONTROL_STOP, &status); BOOL bDelete = ::DeleteService(hService); ::CloseServiceHandle(hService); ::CloseServiceHandle(hSCM); if (bDelete) return TRUE; MessageBox(NULL, T('Service could not be deleted“), mszServiceName, MB_0K)T return FALSE: ///////////“//////////////////////////////////////////////////////////// II Logging functions void CServiceModule::LogEvent(LPCTSTR pFormat, •••} { TCHAR chMsg[256] ; HANDLE hEventSource; LPTSTR lpS2Strings[1); va_list pArg; va_start(pArg, pFormat); _vstprintf(chMsg, pFormat, pArg); va_end{pArg); lpszStrings[0) = chMsg; if (m_bService) I* Get a handle to use with ReportEvent(). */ hEventSource = RegisterEventSource(MULLt m_szServiceName); if (hEventSource 1= NULL)174 第二部分高级COM编程技巧 /* Write to event log. */ ReportEvent(hEventSource, EVENTUOG_INFORMATION_TYPE, 0, 0, NULL, 1, 0, (LPCTSTR*)~41pszStrings[0]t NULL); DeregisterEventSource(hEventSource); } } e l s e II As we are not running as a service, just write the error to // the console. —putts(chMsg); iiiiniiiiiiiiiiiJiiiiumiiiiii/uiiiiiiiiiiinniniimiiiuiininn II Service startup and registration inline void CServiceModule::Start() { SERVICE_TABLE_ENTRY st[]= { { m_szServiceName, _ServiceMain }, { NULL, NULL } } ; if (m_bService && !::StartServiceCtrlDispatcher(st)) { m 一bService = FALSE; > inline { // (n’bService == FALSE) Run(); void CServiceModule::ServiceMain(DWORD, LPTSTR*) Register the control request handler m status.dwCurrentState = SERVICE START PENDING:OM • m_hServiceStatus = RegisterServiceCtrlHandler(m_szServiceName _ H a n d l e r ) ; if (m_hServiceStatus == NULL) { LogEvent(_T(•Handler not installed“ )); return; } SetServiceStatus(SERVICE_START_PENDING); mstatus.dwWin32ExitCode = S_0K; m_status.dwCheckPoint - 0; m_status.dwWaitHint = 0; II When the Run function returns, the service has stopped. Run(); SetServiceStatus{SEHVICE_STOPPED); LogEvent(_T(■Service stopped“)); inline void CServiceModule::Handler(DWORD dwOpcode)第 川 章 使用NT Mi务 175 switch (dwOpcode) { case SERVICE_CONTROL_STOP: SetServiceStatus7sERVICE_ST0P_PENDING); PostThreadMessage(dwThreadID, WM_QUIT, 0 , 0); break; case SERVICE_CONTROL_PAUSE: break; case SERVICE_CONTROL_CONTINUE: break; case SERVICE_CONTROL一INTERROGATE: break; case SERVICE_CONTROL_SHUTDOWN: break; default: LogEvent(_T(“Bad service request“)); void WINAPI { M o d u l e } void WINAPI { Module CServiceModule::_ServiceMain(DWORO dwArgc, LPTSTR* IpszArgv) .ServiceMain(dwArgc, IpszArgv); CServiceModule::_Handler(DWORD dwOpcode) .Handler( d w O p c o d e ); void CServiceModule: -.SetServiceStatus(DWORD dwState) { m^status.dwCurrentState = dwState; ::SetServiceStatus(m_hServiceStatus, &m_status); } void CServiceModule::Run() { Module.(JwThreadID = GetCurrentThreadId(); HRESULT hr = CoInitialize(NULL); II If you are running on NT 4.C or higher you can use the following call // instead to make the EXE free threaded. II This means that calls come in on a random RPC thread II HRESULT hr = CoInitializeEx(NULL, COINIT^MULTITHREADED); _ASSERTE(SUCCEEDED(h「)); II This provides a NULL DACL which will allow access to everyone. CSecurityDescriptor sd; sd.Initiali2 eFromThreadToken(); hr = ColnitializeSecurity(sd, -1, NULL, NULL, RPC_C_AUTHN_LEVEL_PKT, RPC_C_IMP_LEVEL_IMPERSONATE, NULL, EOAC_NONE, NULL); _ASSERTeTsUCCEEDED(hr)); hr = Module.RegisterClassObjects(CLSCTX_LOCAL_SERVER | c l s c t x ! r e m o t e _ s e r v e r , REGCLS_MULTIPLEUSE); ASSERTE(SUCCEEDED(hr));176 第二部分高级COM编程故巧 LogEvent(_T(“Service started“)); if (m_bService) SetServiceStatus(SERVICE_RUNNING); M S G m s g ; while (GetMessage(&msg, 0, 0, 0)) OispatchMessage(&msg); _Module.RevokeClassObjects(); CoUninitializeO; iiuiiiiiiiniiiuiiiiiiniuiiiiiiniiiiiniiun/miiu/iiiiiiiiniiii // . extern _CB int WINAPI _tWinMain(HINSTANCE hlnstance, HINSTANCE /*hPrevInstance*/, LPTSTR lpCmdLine, int /*nShowCmd*/) { lpCmdLine = GetCommandLine(); //this line necessary for _ATL_MIN_CRT _ M o d u l e • Init(ObjectMap , h l n s t a n c e , IDS—SERVICENAME, &LIBID_MYATLSERVICELib ); Module.m bService = TRUE; TCHAR szTokens[ 】 = _T(■•/“); LPCTSTR IpszToken = FindOneOf(lpCmdLine, szTokens); while (IpszToken !: NULL) { if (lstrcmpi(IpszToken, _T(“UnregServer“))==0) return •Module.UnregisterServer(); II Register as Local Server if (1strcmpi(IpszTokenf _T(*RegServer“))=*0) r e t u r n 〜Module.RegisterServer(TRUE, FALSE); I / Register as Service if (1strcmpi(IpszToken, _T(“Service“))==0) return Module.RegisterServer(TRUE, TRUE); IpszToken = FindOneOf(IpszToken, szTokens); } II Are we Service or Local Server CRegKey keyAppID; LONG IRes : KeyAppID.Open(HKEY_CLASSES_ROOT, _T(“AppID“), KEY_READ); if (IRes != ERROR_SUCCESS) return IRes; CRegKey key; I R e s = key•Open(keyAppID, _T(■{B51F6322•99A5-11D2-BEC8•00C04F8B72E7}“), KEY_READ); if (IRes != ERROR_SUCCESS) return IRes; TCHAR SZValue[_MAX_PATH ] ; DWORD dwLen = ~MAX~PATH;— — , IRes = key.QueryValue(szValue, _T(“LocalService“), &dwLen);第/0 章 使用NT服务 177 Module.mbService = FALSE; if (IRes == ERROR_SUCCESS) —Module.m_bSe「vice = TRUE; _Module.Start(); 11 When we get here, the service has been stopped return ^Module.m_status. d'MWin32ExitCode; 如果你看一下CServiceModule类的 成 员函数 ,会注意到它们与本章前面给出的代码相似。 作 为 例 子 , 我 在 ServiceMain()函 数 的 开 始 部 分 用 CServiceModule::Init()方 法 初 始化了 SERVICE一STATUS数 组 。 在 main()函 数 中 ,CServiceModule::Start( ) 方 法 调 用 了 StartServiceCtrlDispatcher()函数c CServiceModule::ServiceMain()方法如同ServiceMain()函数一 样 ,通过调用RegisterServiceCtrlHandler()函数,在SCM中注册了控制调度程序。 总的来说,你得到了一种编写服务的感觉。但 是 ,因为ATL COM AppWizard总是试图实现 很 多 特性,所以你可能会读不懂代码,这是一个普遍的设计缺陷,被称为瑞士军刀式的设计模 式 。也 就 是 说 ,设计者试图将很多功能合并到一个类中,使这个类可以做任何事,也因此使代 码变得难于理解。但是请相信我, 这些代码是能够运行的。 除了包含启动和停止你的服务所必需的代码以外, 这个模块类还无条件地实现了事件的日 志和记载和ATL类的注册。本 章 后 面 的 “使用事件日志” 部分将详细讨论有关事件记录的内容。 现在你会问,ATL是如何使你更轻松地与服务进行通信的呢?到目前为止,你只看到了创建 服务所必需的样板代码。下一节将描述如何使用DCOM与一个服务进行通信。 DCOM的丨PC 回到ATL服务的项目工作区。现 在 ,你已经有了一个基本的ATL服 务项目 ,可以使用ATL Object Wizard为项目添加ATL类 。和以往一样,你可以从菜单栏中选择Insert, New ATL Object, 创建ATL类。在ATL Object Wizard的弹出对话框中的Category列表框里选择O bjects,在Objects 列表框里选择Simple Object,然后单击Next按钮。在ATL Object Wizard的属性对话框中,为你 的对象键人一个简短的名字。我在开始时通常会为服务添加一个作为公共入口点的单元素对象。 单 兀 素 类 是 一种 COM 类 ,它 的 类 工 / 只 创 建 一 个 类 的 实 例 ,所 有 对 IClassFactory:: CreatelnstanceO的调用都返回这一个实例。这意味着,所有的客户将共享这个对象的同一个实例。 假设这个对象属于自由线程模型,并且是线程安全的,那么它就可以被用作服务的公共入口点。 为 了 使你的ATL类 属 于 单 元 素 类 ,必 须 为 你 的 类 声 明 添 加 DECLARE_CLASSFACTORY_ SINGLETON宏 (参见清单 10-9 )。 清单10-9单元素ATL类 // singleton.h : Declaration of the Csingleton #ifndef __SINGLET0N_H__ #define SINGLET0N_H~1 ■ #include “resource.h“ // main symbols178 第二部分高级COM编程技巧 /////////////////////////////////////////////////“////////////////////// // Csingleton class ATL_NO_VTABLE Csingleton : public CComObjectRootEx, public CComCoClass, public IDispatchlmpl p u b l i c : Csingleton() { } DECLARE_CLASSFACTORY_SINGLETON(Csingleton) DECLARE_REGISTRY_RESOURCEID(IDR_SINGLETON) DECLARE 一PROTECT—FINAL_CONSTRUCT() BEGIN_COM_MAP(Csingleton) COM_INTERFACE_ENTRY(Isingleton) COM_INTERFACElENTRY(IOispatch) END_COM_MAP() II Isingleton p u b l i c : #endif //— SINGLETOH 我在CD-ROM中收人了一个名为myatlservice的项目,可以在ChapterlO目录下的myatlservice 子目录中找到。这个服务有一个ATL单元素类。我曾经在使用ATL向导的时候遇到一些问题,这 些向导有时会忘记在必要的地方包括头文件。这个工作区还包含一个创建ATL类的automation客 户程序,清单10-10给出了这个automation客户程序的代码。 清单10-10 ATL服务客户雄 II controller.cpp : Defines the entry point for the console application.II #include 'stdafx.h“ #include 'automation.hB int main(int argc, char* argv[]) { ::CoInitialize(NULL); { Automation object; object.CreateObject(L“Myatlservice.singleton.1“); } ::CoUninitialize(); r e t u r n 0; 这 个 程 序很简 单,它创建了一个automation对 象 ,然后就退出了。这个包括在CD-ROM的第70章 使用NT服务 179 myatlservice项目中的Automation类仅仅是一个简单的automation封 装 ,它试图在C++中模拟VB 的 automation语法 0 为了把模块作为服务运行,必须首先在SCM中注册这个服务。你吋以直接运行可执行文件 Myatlservice.exe ( 在它后面要加上-service命 令 行 参 数 )来做 到 这 -点 。 在注册了服务之后,再次单步调试客户程序代码,ATL模块将作为服务运行。也许你很想知 道如何分辨一个模块是作为服务运行,还是作为可执行程序运行。下 •节将介绍几个工具—— 包括Administrative Tools Services applet--- 帮助你确定一个服务的运行状态。 1 0 . 3 为使用服务而提供的工具 有四个工具是在你编写和使用服务时必不可少的,它们分别是:Administrative Tools中的 Services applet N Diagnostic utility、Service Controller和Event Viewer0 10.3.1 Administrative Tools中的Services Applet Services applet使你可以启动、停 止 、暂 停 、恢复以及配S 所有安装在计算机上的服务。你 可以在Windows 2000的Administrative Tools中启动Services applet。图 10-3是Services applet的主 对话框。 • ■ ~~ * * » * - -■ ( Owotcor \ SUM i 1ioqC»>a# % .09:4ltMiMan«9«r Automatic loe«8yiWm ^LogK4iCMiMan^».. Manual u>c«tSy«tam 5andt««d... Started AUtomflbc loaBrnm lOOfWOM mccLogir*. CorAgurw... SU^d 图 10-3 Addministrative Tools中 的 Service applet 启 动 、停 止 、暂停和恢复 服 务 是很容 易的 ,不需要太多 的解 释。如果你选择了 •一个服务, 并单击了Configure按 钮 ,服务配置对话框将出现(如图10-4 )。 你可以通过在Startup Type组框中选择Automatic,使服务在计算机启动时自动运行。服务通 常是运行在特定的系统账号环境下的,但是你可以在服务配置对话框中进行配置,使服务可以 运行在任何用户的环境 屮 。180 第二部分高级COM编程故巧 _灣 ^Xlsl |lcigOn| R«co««r| D«pandanMt| 图 1 0 ~ 4 服 务配 置 10.3.2 Diagnostic实用工具 另一个主要工具是Diagnostic实 用 工 具 (WinMsd.exe)。可以在system32目录中找到这个工 具。它可以列出计算机中所有正在运行或已被停止的服务。图10-5是Diagnostic工具的Services选 项卡。 有了WinMsd,就可以知道服务的状态了。对于区域内任何一台计算机,你都会得到相同的 列表。这就是为什么它对系统管理员非常有用。 图 10-5 D iag n ostic工 具 WinMsd是一个很容易得到的工具,因为在大多数工作站上都安装有WinMsd。你可以通过第 川 章 使用NT服务 J8J ^Mormflbon VI4/2000 >Worm«)on 2/14/2000 >Worm«bon iDQfZOOO 图 10-6 Event Viewer 当你开始使用事件日志时,你将很快了解到这个工具的重要性。本章后面会讨论如何向事 件曰志发送事件。 10.4 OpenSCManager() OpenSCManagerO函数可以用于得到一个SCM句柄 。在你得到一个SCM句柄之后,便可以 通过内部API开始与服务进行通信了。 从 开 始 菜 单 中 选 择 R u n,键 入 WinMsd,回 车 ,启 动 这 个 工 具 。 与 其 他 的 工 具 (如 Service Controller) 相 比 ,这种方便的启动是WinMsd的主要优势。 10.3.3 Service Controller Service Controller工具,Sc.exe,提供了前面两个工具的所有功能,除此之外,它还允许你 在远程计算机上启动、停止、暂 停 、恢复以及配置服务。 不幸的 是,Service Controller■工具不是预先安装的,你必须从安装光盘的binH录 中 ,将这 个可执行文件复制给每台需要它的计算机。但 是 ,因为Service Controller有很多优点,这种复制 工作是值得的,至少为你自己的工作站复制一个。 10.3.4 Event Viewer Event Viewer位于Start菜单的Program/Administrative Tools文件夹中,以及System32目录中。 图10-6显示的是Event Viewer的主窗口。 f f s m f m m l l H i l E E E m i m i 777 TT? Nate 知 他 Nato Nta 他 Naito NB May SI 注意 SCM的API应该只用来启动、停止、暂停和恢复一个服务。如果你想向服务发送 其他的请求,那么服务应该提供一个IPC协议来支持客户的请求。这是一个原则,而不 是标准。182 第二部分高级COM编程技巧 1 0 .4 .1 服务的句柄 第一件必须做的事就是调用OpenServiceO函数,得到一个指定服务的句柄。在你得到了指 定服务的句柄之后,便可以调用StartServiceO函数来启动服务。你可以使用CloseServiceHandleO 函数关闭服务的句柄。清单10-11给出了一个使用StartServiceO函数启动服务的例子。 清单 10-11 StartService()例子 void StartAService(const char * szServiceName) { SC^HANOLE handle = ::OpenSCManager(NULL, NULL, SC_MANAGER_ALL_ACCESS ); SC HANDLE tiService = : :OpenService(handle, szServiceName, SERVICE_ALl_ACCESS ); ::StartService(hService, 0, NULL); ::CloseServiceHandle(hService); :-.CloseServiceHandle(handle); } 上面用到的AIM原型如下: SC_HANDLE OpenSCManager( LPCTSTR lpMachineName, II pointer to machine name string L P C T S T R lpDatabaseName, II pointer to database name string D W O R O dwDesiredAccess I / type of access ) ; SC_HANDLE OpenService( S C _ H A N D L E hSCManager, f / handle to service control manager database L P C T S T R IpServiceName, II pointer to name of service to start D W O R D dwDesiredAccess // type of access to service ); BOOL StartService( S C _ H A N D L E hService, II handle of service D W O R D dwNumServiceArgs, II number of arguments LPCTSTR *lpSer\ziceArgVectors I / array of argument strings ); 1 0 .4 .2 操作服务 在你拥有了服务的句柄并且启动了服务之后,便可以调用ComrolServiceO函数来暂停、恢 复和停止这个服务,还可以调用QueryServiceStatus()函数来査询服务的状态。清单10-12给出了 一个使用这两个函数停止服务的例子。 清单10-12停止服务的例子 void StopAService(const char • szServiceName) { SC_HANDLE handle = ::0penSCManager(NULL, NULL, SC_WANAGER_ALL_ACCESS); SC_HANDLE hService = ::0penService(handle, szServiceName, SERVICE_ALL 一ACCESS} ; SERVICE_STATUS servicestatus; ::QueryServiceStatus(hService, &$ervicestatus);第 川 章 使用NT服务 183 CreateService()函数通过在Windows系统注册表中添加登记项来安装服务。图10-7给出了一 个由CreateService()函数添加登记项的例子。 if (servicestatus.dwCurrentState == SERVICE—RUNNING) { if( !::ControlService(hService, SERVICE_C0NTR0L_ST0P, &servicestatus)) { // Handle situation where service did not stop. } > ::CloseServiceHandle(hService); ::CloseServiceHandle (handle); 还可以使用ControlService<)函数向ServiceCtrlHandlers()函数发送本章前面讨论过的专有消息。 1 0 . 5 经由注册表安装服务 为了在一 台计算机上安装服务,必须在这台计算机的注册表的HKEY_LOCAL_MACHINE\ Sy stem\CurrentControlSet\Services 键中添加几个键值。 虽然你可以通过直接操作注册表来安装服务,但是你可能还是需要使用CreateServiceO函数。 清单10-13给出了一个调用CreateServiceO函数来安装服务的例子。 清单 10-13 使用CreateServlce() void InstallAService(const char * szServiceName) SC_HANDLE handle = ::OpenSCManager(NULL, NULL, SC_MANAGER_ALL_ACCESS ); char szFilename 丨2 5 6 1 ; ::GetModuleFileName(NULL, szFilename, 255); SC_HANDLE hService = ::CreateService(handle, szServiceName, szServiceName, SERVICE_ALL_ACCESS, SERVICE_WIN32_0WN_PR0CESS, SERVICE_OElAAND_START, SERvICE_ERR0R_IGN0RE, szFilename, NULL, NULL, NULL, NULL, NULL ); ::CloseServiceHandle(hService); ::CloseServiceHandle (handle); QMouctav* Cj mmdJR* □ MU M 」WTC CJ WSvw J WCORV JH^CLOCr UHf____ i«k no * Cl 图 10-7 注册表和CreateServiceO184 第二部分高级COM编程技巧 在安装了服务乏后,便可以通过调用ChangeServiceCotifigO函数来修改服务的配置。如果要 卸掉服务,调用DeleteServiceO函数。注 意,删除服务实际上不能卸掉服务,它只是将服务标记 为删除。在所有的服务句柄都被关闭之后,这个服务就被彻底删除了。 1 0 . 6 使用事件日志 一个与服务相关的主题就是事件日志。经 常 地 ,当我们调试一个服务时,会在提供运行时 的反馈上遇到麻烦。服务不会以消息窗口或其他任何GUI形式提供直接的反馈,取而代之的是, 它们会把反馈记入事件日志。下面几小节将讨论如何向事件日志写事件,以及如何从事件日志 读取事件。 事件日志中提供的信息非常全面,所以通常你可以在那里得到足够的信息。在日志 中 ,你 会得到事件的类型、 口期、时 间 、源以及类别。你还可以知道事件到底是由应用程序、系统还 是服务产生的。 注 意 实际上,在任何工作站或服务器上,都至少有三个事件日志,而这三个主要的事 件日志分别是应用程序事件日志、系统事件日志以及安全事件日志。而在本章中,我指 的都是应用程序事件日志。 1 0 .6 .1 消息编译器 为了实现事件日志的记录,首先要熟 悉的 工具是消息编译器(MC)。给消息编译器输入一 个消息脚 本(.me ) 文件 ,它将输出三个文件:一个二进制文件(.bin )、一个资源脚 本文件(.rc ) 以 及 一 个 头 文 件 (.h )。要 运 行消息编译器 ,在命令行中键入me -c myatlservicemc , 这里的 myatlservicemc是你创建的消息脚本文件的名称。清单10-14给出了一个消息脚本文件的示例, 它创建了三个消息资源字符串。 清单10-14消息脚本文件示例 MessageID=10 SymbolicName^MRS^HELLO Language=English My service has started M e s s a g e I D = SymbolicNatne=MRS_ERROR Language*English E r r o r : M e s s a g e I D = SymbolicName=MRS__GOODBYE Language=English My service has stopped • 在编译了这个消息脚本文件之后,你便已经生成了三个必不可少的文件。资源脚本文件应 该被编译并链接到你的服务模块文件中。资源脚本文件将包含那个二进制文件,所以在你编译第 川 章 使用NT月I务 】85 C • is the Customer code flag R • is a reserved bit Facility • is the facility code Code • is the facility's status code Define the facility codes 资源脚本时,二进制文件必须在其路径中。为了使用消息资源字符串的符号名,头文件应该包 含事件日志源代码中。下面就是MC生成的资源脚本: LANGUAGE 0x9,0x1 1 11 MSG000O1.bin 清单10-15给出了消息编译器创建的资源脚本和头文件。 清单10-15由MC产生的头文件 II // // //II IIIIIIIIIIIIIIIIII II IIII Values are 32 bit values laid out as follows 3322222222221111111111 098765432 | S e v | C | R | F a c i l i t y C o d e w h e r e Sev • is the severity code 00 . S u c c e s s 01 - Informational 10 - Warning 11 • E r r o r // II Define the severity codesIIII II Messageld: MRS_HELL0 // H MessageText:IIII %sII #define MRS_HELL0 0x2000000AL II II M e s s a g e l d : M R S _ E R R 0 RII186 第二部分高级COM编程技巧 II MessageText:II II E r r o r : % sII #define MRS_ERR0R 0x20000 抑 BL 最 后 ,你必须运行清单10-16中的代码,注册消息字符串。我还在CD-ROM上收入了另外的 源代码,名为EventLog,可以在ChapterlO目录下的EventLog子目录中找到。EventLog包 括 T本 章中的全部事件记录源代码。这个示例通过添加完整的记录事件日志的功能,扩展了前面给出 的服务。 清单1 0 -1 6 注册事件服务器 USES_C0NVERSI0N; H K E Y h k e y ; ::RegCreateKey(HKEY_LOCAL_MACHINE, 'SYSTEMWCurrentControlSet\\ _ 'Services\\EventLog WApplication Wmyatlservice“ , & h k e y ) ; Char SZ(MAX_PATH]; ::GetModuleFileName(NULL, sz, MAX_PATH); ::RegSetValueEx(hkey, ■EventMessageFile“, 0, REG_EXPAND,SZ, (LPBYTE) sz, ::strlen(sz)+ 1); DWORD dwOata = EVENTL0G_ERR0R_TYPE | EVENTL0G_WARNING_TYPE 丨 EVENTLOG_INFORMATION_TYPE; ::RegSetValueEx(hkey, “TypesSupported“, 0, REG_DW0RD, (LPBYTE) &dw0ata, sizeof(DV/ORD)); ::RegCloseKey (hkey); 10.6.2 RegisterEventSource(), DeregisterEventSource()和 ReportEvent() 写事件日志也很容易。为了打开和关闭用于写注册表的句柄,调用RegisterEventSouixeO和 DeregisterEventSource()函数。调用ReportEvent()函数向事件日志写入一个入口点。淸单 10-17是 用于向事件日志写入人口点的通用方法。 清单1 0 -1 7 写事件日志 void WriteEventLogEntry(int idMessageString, const char * * pszString) { HANDLE hEventSource = ::RegisterEventSource(NULL, szServiceName)); if (hEventSource != NULL) { ::ReportEvent(hEventSource, EVENTLOG_INFORMATION_TYPE, 0, idMessageString, NULL, 1, 0, szString, NULLj; ::DeregisterEventSource(hEventSource); 你 会 发 现 这 个 函 数 只 向 事 件 日 志 写 入 信 息 类 型 消 息 ,这 是 因 为 我 使 用 了 EVENTLOG_ INFORMATIONJTYPE参数。实际上,有五个类似的常量可用: EVENTLOG ERROR TYPE EVENTLOG WARNING TYPE第 川 章 使用NT服务 187 EVENTLOG_INFORMATION_TYPE EVENTLOG_AUDIT_SUCCESS EVENTLOG, AUDIT_FAILURE 在所有的事件都写入了事件H志之后,便可以使用Event Viewer了。查看事件日志的另一个 选择就是创建自己的事件日志阅读器。下一小节将讨论如何创建这样的阅读器。 1 0 .6 .3 事件日志阅读器 读取事件日志比向里面写还要容易。你可以通过使用OpenEventLogO和CloseEventLogO函 数来打开和关闭事件日志的句柄,可以调用ReadEventLogO函数读取单独的事件日志记录器。但 是 ,事件日志记录器只包含你传给ReportEventO函数的字符串,而不包含消息字符串。使消息 格式化是阅读器的职责。清单 10-18给出了一个事件阅读器的源代码,它可以把事件日志转换为 标准输出。 清单1 0 - 1 8 导出事件日志 // viewer.cpp : Defines the entry point for the console application.fl ^include “stdafx.h“ ^include 1 windows.h“ ^include “iostream.h“ int main() HANDLE handle = ::OpenEventLog(NULL, “Application“); if (handle == NULL) { return 0; DWORD dwRec=0; static const int c = 65536; DWORD dwRead, dwNext; BYTE by(c); while (1) { if (!::ReadEventLog(handle, EVENTL0G_BACKWAR0S_READ + EVENTLOG_SEQUENTIAL__READ, 1, by, c, &dwRead, &dwNext)) { break; } DWORD dw=0; while (dwRead>dw) { EVENTLOGRECORD * p; p = (EVENTLOGRECORD *)(by+dw); char * sz = (char *)by+dw+56;188 第二部分高级COM编程技巧 cout « “Record: ■ « ++dwRec « '\n'; TCHAR * tchar = (TCHAR *)(by+dw+p->StringOffset); cout « tchar « •\n.; dw += p->Length; ::CloseEventLog(handle); return 0; 记 住 ,你使用事件日志的原因是为程序员和操作者提供运行时刻的反馈。你不能使用消息 窗口或其他的GUI元素提供反馈,因为服务是没有桌面的。为了使调试更加容易,你可能打算向 事件日志发送很多反馈,这么做是不合适的。只有重要的信息才应该记人事件日志。下面几节 将介绍几个技巧,帮助你对服务进行调试。 1 0 .7 调试你的服务 服务比其他的进程更难调试的原因有几个,一个是服务不像其他进程一样拥有桌面,而另 一个原因是服务通常使用一个被称为系统账号的特殊用户账号运行。 1 0 .7 .1 系统账号 系统账号是有特权的,它有资格访问文件系统和许多其他的安全资源c 然 而 ,系统账号又 很受 限 制,它不能访问任何网络资源。系统账号是一个空会话账号,即使是每个人都有权访问 网络资源,空会话账号也无权访问网络资源。 而 且 ,你无法登录一台使用系统账号的计算机,这使得调试变得非常的困难。尤其是在运 行服务的账号拥有一组与交互用户不同的访问权的时候,调试更加困难。 1 0 .7 .2 任务管理器:调试 你可以通过从任务管理器中启动调试程序来调试一个进程,即使这个进程运行于系统账号下。 在任务管理器的Processes选 项 卡 ,右键单击一个进程,弹出它的右键菜单,在这个菜单中选择 D e b u g ,这时你 注册的调 试程 序 将被运 行 ,并 且附加当前的运行进程。也可以启动Developer Studio,用开关加上/P和你要调试的服务的迸程ID。 1 0 .7 .3 使用AT命令启动调试器 调试的另一个选择就是,得到一个运行于系统账号的安全环境中的命令解释程序。在开始 菜单中选择R u n ,键人命令at 9:05 /interactive cmd.exe,这里9:05是将来的一个时刻--- 也就是 说 ,如果当前的时间是10:06,你应该键人at 10:07/interactive cmd.exe。当时钟走到10:07时 ,调 度程序将运行一个命令解释程序。因为调度程序是在系统账号的安全环境下运行的,所以命令 解释程序也将在这个环境下运行。在这个命令解释程序中,你可以启动Developer Studio或任何 其他进程,而这个被启动的进程也将运行于系统账号的安全环境中。第10章 使 用 NT服务 189 1 0 . 8 小结 在 本 章 中 ,我们讨论了许多有关服务的内容:如何编写服务、使用服务以及调试服务。•现 在 ,你已经学到了创建服务的基本内容,但这些对你来 说是不熟练的 ,你应该尝试着编写一些 自己的程序。 注册表是你学到的另一个知识。 因为绝大多数有关服务的信息都存储在注册表中,而且服 务要从注册表中载入它们的大部分设置,所以这个话题与服务很有关系。对与服务相关的注册 表的理解,有时可能会成全你,也很可能连累你。 我用事件日志和调试作为本章内容的结尾。我们当然不能低估调试服务时的困难,但是我 们讨论了如何对付这些困难,包括使用事件日志。第11章调 度 本章内容包括: •理解调度 •类型库调度 •标准调度 • 自定义调度 1 1 . 1 理解调度 调度的定义有时不是很好理解,即使是那些已经掌握了这个概念的人也会说不清楚。调度 这个词原先是用来描述战前集合和组织军队的过程。而 在计算机领域,调度的意思是把数据集 合和组织到一个数据包中的过程,这个包将会被传送到另一个进程或线程。而反调度则是打开 数据包并在本地保存数据的过程。 客户进程开始于陈述一个对服务器COM+对象的请求,该请求被调度进入可传输的数据包中, 并被传给服务器对象。服务器对象打开数据包,处理请求。然 后 ,响应将被调度打包,传回客 户端。客户打开数据包,解释响应数据。 , 客户使用代理对象进行调度和反调度,这个代理对象提供了与COM+服务程序相同的接口。 代理不会去实现COM+服务程序的方法,它只会将方法参数打包,并将数据包传给真正的COM+ 服务程序,然 后,在有结果从COM+服务程序的方法返回时,打开结果数据包。 服务程序通常会有一个存根线程,接收来自代理的请求,并打开数据包,对COM+对象进行 真 正 的 调 用 (参见图11-1)。 图11-1代理-存根调度 一共有三种类型的调度:类型库调度、标准调度和自定义调度。虽然你只需要其中之一来调 度接口,但是 ,下面几部分介绍了每一种类型的调度,使你可以选择最适合你需要的调度类型。 1 1 .2 类型库调度 类型库调度利用了这样一个事实,那就是每次安装过程都会安装一个自动调度程序。只要第11章 调 Jt 191 你愿意接受自动调度程序所支持的数据类型给你带来的约束,你就可以使用类型库调度。也就 是 说 ,不必在客户端和服务器端安装代理-存根模块,你就可以使用代理-存根对。你现在只需要 在客户端和服务器端安装类型库就可以了,而不必安装代理-存根对。由于类型库调度的简单性, 我几乎总是使用这种调度。 为了使用类型库调度,你应该在注册表中表明你在使用自动调度程序,以及在哪可以找到 你的类型库。清单11-1给出了一个使用类塱库调度的COM+接口的注册表文件。 清单1 1 - 1 类型库调度注册文件 [HKEY_CLASSES_ROOT \ Interface \ {my- interfaces-iid }] @=“ IMylnterface'* [HKEY_CLASSES_ROOT \ Interface \ {my- interfaces -iid } \ ProxyStubClsid32] 0=_{00020424•0000•0000•C000-000000000046}* [HKEY_CLASSES_ROOT \ Interface \ {my- interfaces -iid } \ TypeLib} @=-{my-typelibs-guid}* “Version“=“l.0B [HKEY_CLASSES_ROOT \ TypeLib \ {my-typelibs-guid> \ 1.0] 0=“My Type Library“ [HKEY_CLASSES_ROOT \ TypeLib \ {my-typelibs-guid> \ 1.0 \ Win32] @=“c: WmypathWmymodule. tlb“ [HKEY_CLASSES_ROOT \ TypeLib \ {my-typelibs-guid} \ 1.0 \ Flags! @=“0M [HKEY_CLASSES_ROOT \ TypeLib \ {my-typelibs-guid} \ 1.C \ HelpDir]§»=11 * 上面的注册表文件在注册表中注册了IMylnterface接口。相关的typelib, “My Type Library”, 也进行了注册。应该注意的是,.tlb文件也被注册了。 1 1 .3 标准调度 我们使用微软接口定义语言(MIDL)编译器来实现标准调度。假设你有一个名为 IMylnterface的接口,并将其保存在myinterface.idl文件 中,你可以使用MIDL编译器为这个接口 生成代理-存根代码。MIDL编译器的输出包括以下五个文件: • myinterface.h--- 定义接口的头文件。 • myinterface_i.c--- 内有IID常量定义的源文件。 • myinterface_p.c---内有代理-存根代码的源文件。 • dlldata.c—— 内有代理-存根DLL人口的源文件。 • myinterface.tlb— 用于类型库调度的能够自动控制的类型库。192 第二部分高级COM编程技巧 清单 11-2是一个简申•的接口定义语 言(IDL) 文件 ,•它可以在使用MIDL编译器生成文件时, 作为输入文件使用。 清单11-2 —个简单的IDL文件 // MyMidlExperiment.idl : IDL source for MyMidlExperiment.dll // II This file wi ll be processed by the MIDL tool to II produce the type library (MyMidlExperiment.tlb) and II marshaling code. import “oaidl.idl“; import “ocidl.idl“; U U i d (C E 0 0 5 6 0 E •40F0-11D2-BEA0-00C04F8B72E7), helpstring(“IMylnterface Interface“), p o i n t e r 一default(unique) 1 interface IMylnterface : IUnknown { [helpst「ing(“method Hello“)) HRESULT Hello(); } ; I uu id (C E 0 0 5 6 0 1 •40F0-11D2-BEA0- 00C04F8B72E7) , version(1.0), helpstring(“MyMidlExperiment: 1.0 Type Library“) ) library MYMIDLEXPERIMENTLib { importlib(“stdole32.tlb“); import lib Cstdole2. tlb“); I uu id(CE00560F-40F0-11D2-BEAO•00C04F8B72E7), helpstring(“Mylnterface Class“> ] coclass Mylnterface { [ d e f a u l t 】 interface IMylnterface; 如果在你执行MIDL编译器的时候使用了上面这个IDL文 件 ,那么输出将是类型库和另外四 个文件 ,它们分别在下一部分的清单11-3、清单11-4、淸单11-5和清单11-6中给出。 1 1 .3 .1 定义DLL入口点 清单11-3是dlldata.c源 文件 ,它为你的代理存根定义了所有的DLL入口点。宏DLLDATA_ ROUTINES创建了代理存根所需的导出例程,这些例程包括DUGetClassObjectO、DllCanUnload Now()、GetProxyDllInfo()、DllRegisterServer()和DllUnregisterServer()o第11章 调 度 193 •cpp文件缺少COM进程内服务程序函数DllGetClassObjectO和DllCanUnloadNowO, 我们不 需要这些函数的明确定义,因为编译x 文件可以为你生成默认的实现。然而 ,这些实现仍然必须 从这个DLL中导出。标准调度DLL需要DllGetClassObjectO、DllCanUnloadNow()及其导出结果。 你只需要导出这三个函数,而不必在标准调度DLL中实现它们。当然,如果你已经在DLL中定 义 f DllRegisterServer()和DllUnregisterServer()两个函数,那么你还必须导出这两个函数。 清单11-3 MIDL编译器(dlldata.c)的输出 OllData file •• generated by MIDL compiler DO NOT ALTER THIS FILE This file is regenerated by MIDL on every IDL file compile. To completely reconstruct this file, delete it and rerun MIDL on all the IDL files in this DLL, specifying this file for the /dlldata command line option , #include #ifdef — cplusplus extern 'C“ { # e n d i f EXTERN_PROXY_FILE( MyMidlExperiment ) PROXYFILE_LIST_START /* Start of list */ REFERENCEJ>ROXY_FILE( MyMidlExperiment ), /* End of list */ PROXYFILE LIST END DLLDATA_ROUTINES( aProxyFileList, GET_DLL—CLSID ) #ifdef __cplusplus } “ e x t e r n BC_ */ #endif /* end of generated dlldata file * I 1 1 .3 .2 类定义 清单11-4是mymidlexperiment.h头文件,它包含了类的定义。如果你打箅直接引用IMylnterface 对象,你应该包含这个文件。 清单11-4 MIDL编译器(mymid丨experiment.h)的输出 /* this ALWAYS GENERATED file contains the definitions for the interfaces */194 第二部分高级COM编程技巧 /* File created by MIDL compiler version 3.03.0110 */ r at Mon Aug 31 09:51:22 1998 */ /* Compiler settings for MyMidlExperiment.idl: Oicf (0ptLev=i2), W 1 , Zp8, env=Win32, ms_ext, c_ext error checks: none*1 I / DL_FILE_HEADING( ) /* verify that the version is high enough to compile this file*/ #ifndef __REQUIRED_RPCNOR_H_VERSION_ #define _REQUIRED~RPCNDR~H~VERSION~ 440 # e n d i f ^include “rpc.h“ ^include “rpcndr.h“ #ifndef _RPCNDR_H_VERSION_ 林 error this stub requires an updated version of # e n d i f II _RPCNDR_H_VERSION_ #ifndef COM_NO_WINDOWS 一H #include “windows.h' ^include Bole2.h“ #endif /*COM_NO_WINDOWS_H*/ #ifndef _ MyMidlExperiment_h— ^define _ MyMidlExperiment_h 一 #ifdef __cplusplus extern “C“{ # e n d i f I* Forward Declarations * I #ifndef __IMyInterface_FWD_DEFINED_ #define _IMyInterfacelFWD~DEFINED~ typedef interface IMylnterface IMylnterface; #endif /* _IMyInterface_FWD_DEFINED_ */ #ifndef _MyInterface_FV/D_DEFINEO_ #define — MyInte 「face:FWD:DEFINED 二 #ifdef __cplusplus typedef class Mylnterface Mylnterface; # e l s e typedef struct Mylnterface Mylnterface; # e n d i f I* __ c p l u s p l u s * I #endif /* _MyInterface_FWD_DEFINED__ */ I* header files for imported files */ #include “oaidl.h“弟“章调 f t 195 ^include “ocidl.h* void — RPC__FAR * — RPCJJSER MIDL_user_allocate(size^t); v o i d — R P C 一USER MIDL^user^free( void — RPC_FAR * ); #ifndef _IMyInterface_INTERFACE^DEFINED_ #define — IMyInte 「face:INTERFACE 二DEFINED 二 * Generated header for interface: IMylnterface * at Mon Aug 31 09:51:22 1998 * using MIDL 3.03.0110 I* [object][unique][helpstring][uuid] */ EXTERN^C const IID IID_IMyInterface; 摊 i f defined(— cplusplus) && 丨defined(CINTERFACE} MIDL_INTERFACE(_ CE00560E•40F0-11D2-BEA0-00C04F8B72E7*) IMylnterface •• public IUnknown { p u b l i c : v i r t u a l I* [helpstring) */ HRESULT STDMETHODCALLTVPE Hello(void) = 0; # e l s e /* C style interface */ typedef struct IMylnterfaceVtbl BEGIN INTERFACE ♦Querylnterface)(HRESULT (STDMETHOOCALLTYPE — RPC_FAR IMylnterface __RPC_FAR *~This, /•【in]*/ REFIID riid, / * [ i i d _ i s 】[out】*/ void _RPC_FAR ppvObject); R P C F A R ULONG ( STDMETHOOCALLTYPE _RPC_FAR *AddRef )( IMylnterface — RPC 一F A R • T h i s ) ; ULONG ( STDMETHOOCALLTYPE __RPC_FAR ^Release )( IMylnterface ^RPC.FAR“7 This); /* [helpstring] */ hresult ( 一 RP C _ F A R IMylnterface __RPC_FAR * END_INTERFACE } IMylnterfaceVtbl; interface IMylnterface STOMETHODCALLTYPE • H e l l o )( T h i s );196 第 二 部 分 高级COM编程技巧 CONST_VTBL struct IMylnterfaceVtbl 一 RPC_FAR *lpVtbl; #ifdef COBJMACROS #define IMylnterface_QueryInterface(This,riid,ppvObj ect) \ (This)*>lpvtbl •> QueryInterface(This,riid,ppvObject) #define IMyInterface_AddRef(This) \ (This)->lpVtbl -> AddRef(This) #define IMylnterface_Release(This) \ (This)->lpVtbl -> Release(This) #define IMyInterface_Hello(This) \ (This)->lpVtbl •> Hello(This) # e n d i f /* COBJMACROS */ #endif /* C style interface * I /* [helpstring】 */ HRESULT STDMETHODCALLTYPE IMyInterface_Hello_Proxy( IMylnterface — RPC_FAR * This); void — RPC_STUB IMylnterface_Hello_Stub( IRpcStubBuffer *This, IRpcChannelBuffer *__pRpcChannelBuffer, PRPC_MESSAGE jjRpcMessaoe , DWORD *_pdwStubPhase); #endif /* __IMyInterface_INTERFACE_DEFINED_ */ #ifndef _MYMIDLEXPERIMENTLib_LIBRARY_DEFINED_ ^define MYMIDLEXPERIMENTLib_LIBRARY_DEFINED— * Generated header for library: MYMIDLEXPERIMENTLib * at Mon Aug 31 09:51:22 1998 * using MIDL 3.03.0110 *......./ /* [helpstring](version][uuidj */ EXTERN_C const IID LIBID_MYMIDLEXPERIMENTLib ; EXTERN_C const CLSID CLSID_MyInterface; #ifdef _ cplusplus第// 章 调 Jt 197 Class DECLSPEC_UUID(_CE00560F•40F0-11D2-BEA0-O0C04F8B72E7“) Mylnterface; # e n d i f # e n d i f I* MYMIDLEXPERIMENTLib LIBRARY DEFINED */ /* Additional Prototypes for ALL interfaces */ /* end of Additional Prototypes */ 羚ifdef — cplusplus } # e n d i f # e n d i f 你应该注意,代理和存根代码被合并在一个DLL中 ,所以文件名以ps ( 代表代理存根)结尾。 IRpcProxyBuffei■接口是 客 户 端底层 (如代 理管理 程 序 )与它所管理的接口代理实例之间对 话的桥梁。一S 代理被创建,它就如同每个正常的创建过程(IPSFactoryBuffer::CreateProxy()中 的参数pUnkOuter是 非 空 的 )一 样 ,被放人某 些 较大 的 对 象 中 。然后对想要访问的接口调用 QueryIntertace()( 从接 口代 理)0 在服务器端,每个接口存根程序都要实现内部接口IRpcStubBuffer。作为存根管理程序的服 务器端代码会调用IRpcStubBuffecConnect,并且将对象的丨Unknown指针传给接U存根程序。 代理程序和存根程序是通过RPC ( 远程过程调用)通道进行通信的,而这个通道是系统为进 程间通信提供的RPC基础结构的一部分。这个RPC通道实现了IRpcChamielBuffer接 口 ,它是一 个代理程序和存根程序都持有其指针的内部接口。代理程序和存根程序调用这个接口,获得一 个调度数据包,将数据传送给对方,然后在任务完成时删除数据包。接口存根程序还持有一个 指向原始对象的指针。 11.3.3 定义丨丨D、TypeLib GLMD和 CLSID 清单 11-5是mymidlexperimentji.c源 文件 ,它包含了接口 ( I1D )、类 型 库 ( TypeLib GUID ) 以及类标识符(CLSID)的定义。 清单 11-5 MIDL编译器(mymidlexperiment_.c)的输出 I* this file contains the actual definitions of */ I* the IIDs and CLSIDs */ /* link this file in with the server and any clients * I /* File created by MIDL compiler version 3.03.0110 */ /* at Mon Aug 31 ®9:51:22 1998 */ I* Compiler settings for MyMidlExperiment.idl: Oicf (0ptLev=i2), W1 , Zp8, env=Win32, ms_ext, c_ext error checks: none*1 / /^IDL_FILE_HEAOXNG() #ifdef — cplusplus198 第二部分高级COM编程技巧 extern _C_{ #endif #ifndef — IIDJ5EFINED— #define H l IDlDEFINED~ typedef struct _IID { unsigned long x; unsigned short si; unsigned short s2; unsigned char c[8);( } HO; # e n d i f II __IID_DEFINED_ #ifndef CLSID_DEFINED #define CLSID_OEFINED typedef IID CLSID; #endif // CLSI0_DEFINED const IID IID—IMylnterface = {0xCE0056OE,Cx4OF0,Ox11D2, {0xBE,0xA0,0x00,0xC0,0 X 4 F ,0x8B,0x72,0xE7}}; const IID LIBID_MYMIDLEXPERIMENTLib = {0XCE005601,0X40F0,0x11D2, {0xBE,0 x A 0,0x00, 0xC0, 0X4F, 0x 8 B ,0 x 7 2 ,0xE7» ; const CLSID CLSID_MyInterface = {0XCE00560F,0X40F0,0x11D2, {0xBEl0xA0,0x00l0xC0,0x4F,ex8B,0x72,0xE7 }}; #ifdef __cplusplus } # e n d i f 1 1 .3 .4 代理程序和存根程序的定义 清单11-6是mymidlexperiment_p.c源文件,它包含了实际的代理程序和存根程序的定义和代码。 清单11- 6 从M丨DL编译器(mymidlexperiment_p.c)的输出 I* this ALWAYS GENERATED f i l e contains the proxy stub code *1 /• F i l e created by MIDL compiler version 3.03.0110 * I I* at Mon Aug 31 09:51:22 1998 I* Compiler settings for MyMidlExperiment.idl: Oicf (0ptLev=i2), W 1 , Zp8, env=Win32, ms_ext, c_ext error checks: none第7 /章 调 度 199 I /^MIOL_FILE_HEADING() #define USE STUBLESS PROXY I* verify that the version is high enough to compile this file*/ #ifndef _REDQ_RPCPROXY_H_VERSION_ #define REQUIRED RPCPROXY H VERSION 440__ # e n d i f #include “rpcproxy.h“ #ifndef _RPCPROXYJi_VERSION_ #error this stub requires an updated version of # e n d i f II RPCPROXY H VERSION ^include “MyMidlExperiment.h“ #define TYPE_FORMAT__STRING 一S I Z E 1 #define PROC_FORMAT_STRING_SIZE 19 typedef Struct _MIDL_TYPE_FORMAT_STRING { s h o r t Pad; unsigned char Format 丨 TYPE_FORMAT_STRING_SIZE ]; } MIDL_TYPE_FORMAT_STRING;— typedef Struct JAIDL_PROC_FORMAT_STRING { s h o r t Pad; unsigned char Format[ PROC_FORMAT_STRING_SIZE ]; } MIDL PROC FORMAT STRING;“ extern const MIDL_TYPE_FORMAT_STRING _MIDL_TypeForn\atString; extern const MIDL~PROclFORMAT~STRING _MIDL_ProcFormatString; /* Object interface: IUnknown, ver. 0.0, GUID={0x00000000,0x0009,0x0000, { 0 X C 0 , 0 X 0 0 ,0X00,0X00,0X 抑 ,0X00,0X00,0x46}} */ I* Object interface: IMylnterface, ver. 0.0, GUID={0xCE00560E,0X40F0,0x11D2, {0xBE,0xA0,0x00,0xC0,0x4F,0x8B,0x72,0xE7}} *1 extern const MIDL_STUB_DESC Object^StubDesc; extern const MIDL_SERVER 一INFO IMylnterface^Serverlnfo; #pragma code_seg('.orpc') static const MIDL_STUB_OESC Object_StubDesc =200 第 二 部 分 高级COM编程枝巧 NdrOleAllocate, NdrOleFree, — MIDL_TypeFonnatString•Format, 0, /* -error bounds_check flag */ 0 x 2 0 0 0 0 , I* Ndr library version */ 0, 0x303006e, /* MIDL Version 3.3.110 */ , 1* R e s e r v e d l *1 ,/* R e s e r v e d 2 *1 ,/* R e s e r v e d 3 */ , 1* R e s e r v e d 4 */ 1* R e s e r v e d 5 *1 static const unsigned short IMylnterface_FormatStringOffsetTable(] = static const MIDL_SERVER_INFO IMylnterface_ServerInfo * { &Object_StubDesc, 0, — MIDL_ProcFormatString.Format, &lMylnterface_FormatStringOffsetTable(-3], static const MIDL_STUBLESS_PROXY_INFO IMyInterface_ProxyInfo = { &Object__StubDesc, _ MIDL_ProcFormatString•Format, &IMylnterface__FormatStringOffsetTable[ -3], CINTERFACE_PROXY_VTABLE(4) JMylnterfaceProxyVtbl = [ &IMyInterface_ProxyInfo, &IIO_IMyInterface, IUnKnown_QueryInterface_Proxy, IUnknown_AddRef_Proxy, IUnknown_Release_Proxy ,第N 章调 度 201 (void *)-1 I* IMylnterface::Hello *1 }; const CInterfaceStubVtbl IMylnterfaceStubVtbl * { 4IID_IMyInterface, &IMyInterface_ServerInfo, 4, 0, /* pure interpreted */ CStdStubBuffer METHODS #pragma data_seg(-.rdata') #if I defined {—APC_WIN32 一 ) #error Invalid build platform for this stub. #endif #if 丨(TARGET_ISJfT40_OR_LATER> #e「ror You need a Windows NT 4.0 or later to run this stub because it uses these features: -Oif or -Oicf, more than 32 methods However, your C/C++ compilation flags #error 轉error #error #error #error terror #endif in the interface indicate you intend to run this app on earlier This app will di? there with the RPC X WRONG STUB VERSION error. systems static const MIDL PROC FORMAT STRING MIDL ProcFormatString = I* Procedure Hello */ 0x33, /* FC_AUTO_HANDLE */ 0x64, /* */ /* 2 */ NdrFcShort( 0x3 ), /* 3 */ #ifndef _ALPHA 一 /* 4 */ NdrFcShort( 0x8 )f I* x86, MIPS, PPC Stack size/offset = 8 */ #else MdrFcShort( 0x10 ), /* Alpha Stack size/offset = 16 */ #endif I* 6 */ NdrFcShort( 0x0 ), I* 0 */ I* 8 */ NdrFcShort( 0x8 ), I* 8 V /* 10 */ 0x4, /* 4 *1 0x1, /* 1 •/ I* Return value *1 I* 12 */ NdrFcShort( 0x70 ), I* 112 */ #ifndef _ALPHA_ I* 14 */ NdrFcShort( 0x4 ), /* x86, MIPS, PPC Stack size/offset = 4 */ #else202 第二部分高级COM编程技巧 NdrFcShort( 0x8 )y /* Alpha Stack size/offset = 8 */ #endif I* 16 */ 0x8, /* FC_L0NG */ 0x0, /* 0 */ static const MIDL_TYPE_FORMAT_STRING _MIDL_TypeForrnatString const CInterfaceProxyVtbl * _MyMidlExperiment_ProxyVtblList[] { ( CInterfaceProxyVtbl *) &_IMylnterfaceProxyVtbl, 0 const CInterfaceStubVtbl * _MyMidlExperiment_StubVtblList(] { ( CInterfaceStubVtbl *) &_IMyInterfaceStubVtbl, 0 PCInterfaceName const _MyMidlExperiment_InterfaceNamesList[] { “IMylnterface“, > ; #define _MyMidlExperiment_CHECK_IID(n) \ IID_GENERIC^CHECK_IID( ~MyMidlExperiment, pIIO, n) int { stdcall _MyMidlExperiment__IID_Lookup( const int * if(!_MyMidlExperiment_CHECK_IID(0)) IID * pIIO, plndex ) •plndex = return 1; return C; } const ExtendedProxyFilelnfo MyMidlExperiment_ProxyFileInfo { (PCInterfaceProxyVtblList *) & _MyMidlExperiment_ProxyVtblList, (PCInterfaceStubVtblList *) & _MyMidlExperi»ent_StubVtblList,第11章 调 Jt 203 (const PCInterfaceName * ) & _MyMidlExperiment__InterfaceNamesList, 0 , // no delegation & _MyMidlExperiment_IID_Lookup, 1 1 .3 .5 注册表文件 清单11-7给出了使用标准调度的COM+类的注册表文件。 清单1 1 - 7 标准调度的注册表文件 [HKEY_CLASSES_ROOT \ CLSID \ {my-proxy-stub-clsid}] §=•IMylnterface proxy/stub factory“ [HKEY一CLASSES—ROOT \ CLSID \ {my-proxy-stub-clsid} \ InprocServer32] @=M{my-proxy-stub-module-path}' [HKEY—CLASSES—ROOT \ CLSID \ {my-proxy-stub-clsid} \ InprocServer32] §=“Both“ or other (HKEY_CLASSES_ROOT \ Interface \ {my - interfaces -iid}] @=“IMyInterface“ [HKEY_CLASSES_ROOT \ Interface \ {my-interfaces-iid } \ ProxyStubClsid32) 0=“{my-proxy-stub*clsid>M IHKEY^CLASSES_ROOT \ Interface V {my-interfaces-iid } \ NumMethods) 1 1 .3 .6 转换MIDL的输出文件 现在,重要的事情是—— 如何将这些MIDL输出文件转换成代理存根程序并将其注册?清单 11-8给出了两个命令,可以使用这两个命令编译链接并注册代理存根程序。因为只有一个DLL包 含代理和存根两个程序,因此,这个DLL应该同时存在于客户端和服务器端。ATL也允许你把代 理/存根放在同一个DLL中,就像在跨单元情况下真正的COM对象一样。如果你想在另一台计算 机上注册代理程序,那么你必须将marshalserverps.dll文件复制到那台机器上,并使用RegSvr32 进行注册。 清单11-8编译一连接和注册代理存根 nmake If marshalserverps.mK regsvr32 marshalserverps.dll 清单11-9是一个有趣的控制程序,在代理存根程序没被安装和已经安装两种情况下,它可以 得出不同的结果。204 第二部分高级COM编程技巧 清单1 1 - 9 未注册的代理存根结果 #include #include #include “marshalserverWmarshalserver.h“ #include “marshalserver\\marshalserver_i.c' char s(1024]; HRESULT GetUnknown(WCHAR * strProgID, IUnknown ** ppunknown) { CLSID clsid; HRESULT hresult = : :CLSIDFromProflIO(strProgID, & clsid); if (FAILED(hresult)) { II CLSIDFromProgID failed std::cout « *CLSIDFromProgID failed = “ « _ltoa(hresult, s, 16) « std::endl; ::DebugBreak(); return hresult; hresult = ::CoCreateInstance(clsid, NULL, CLSCTX_SERVER, IID_IUnknown, (void **yppunknown) if (FAILED(hresult)) { II CoCreatelnstance failed std::cout « “CoCreatelnstance failed = “ « _ltoa(hresult, s, 16) « std::endl; ::DebugBreak(J; return hresult; return S一OK; HRESULT Getlnterface(IUnknown * punkrtown, REFIID riid, IUnknown ** ppunknown) { HRESULT hresult = punknown*>QueryInterface(riid, (void **)ppunknown); if (FAILED(hresult)) { II Querylnterface failed std::cout « “Querylnterface failed = “ « _ltoa(hresult, s, 16) « std::endl; ::DebugBreaK(7; return hresult; return S OK; }; — int main() CoInitialize(NULL); std::cout « “Start“ « std::endl;第I 】章调 度 205 IUnknown * punknown; HRESULT hresult = GetUnknown(L*MarshalClass.MarshalClass.l“, &punknown); if (FAILED(hresult)) { II GetUnknown failed return hresult; IMarshalClass * pmc; hresult s GetInterface(punknown, IIO_IMarshalClass, (IUnknown **)&pmc); if (FAILED(hresult)) { // GetUnknown failed return hresult; std::cout « “End' « std::endl; CoUninitialize(); return 0; 如果你在注册代理存根程序之前运行清单11-9中的代码,对IUnknown::QueryInterface()的调 用将返回0x80004002或者E_NOINTERFACE。因为接口确实存在,所以这有些令人误解,但是, 接口是不能在进程之间被调度的。如果你已经注册了代理存'根程序,而你打算注销这个代理存 根程序,只需要再次运行Regsvr32程序,并同时在其后加上/U参数就可以了。在你注册了代理 存根DLL之后,清单11-9中的程序将可以无错地运行。我还在CD-ROM上收入了其他的标准调度 代码,可以在Chapter 11目录下的marshaling子目录中找到。 1 1 . 4 自定义调度 通常,使用标准调度是可以接受的,但是,有时也会需要更高效的自定义调度程序。你可 能在某些代码是只读的或者要传送一个值的时候,需要使用自定义调度。在这些情况下,不再 需要调用真正的对象,因为代理程序已经储存了对象的状态。一个对象可以通过暴露IMarshal接 口来实现自定义调度。清单11-10给出了IMarshal接口的定义。 清单1 1 -1 0 丨Marshal接口的定义 interface IMarshal : IUnknown { HRESULT GetUnmarshalClass([in) REFIID riid, [in, unique] void * pv, {in] DWORD dwDestContext, [in, unique] void * pvDestContext, {in] DWORD mshlflags, [out) CLSID * pCid); HRESULT GetMarshalSizeMax((in] REFIID riid, [in, unique] void * pv, 【in] DWORD dwOestContext, [in, unique] void * pvDestContext, [in] DWORD mshlflags,206 第二部分高级COM编程技巧 [out】 DWORD * pSize); HRESULT Marshallnterface([in, unique] IStrean * pStm, [in] REFIID riid, (in, unique] void * pv, [in] DWORD dwDestContext, {in, unique] void * pvDestContext, [in] DWORD mshlflags); HRSEULT Unmarshallnterface([in, unique] IStream * pStm, [ini REFIID riid, [out] void ** ppv); HRESULT ReleaseMarshalData((in, unique] IStream * pStm); HRESULT DisconnectObject(【in] DWORD dwReserved); 自定义调度的优势是,你可以选择不同的进程 间或网络通信协议,包括:微软的RPC、命名管道、 TCP ( 流 )、U D P ( 数据报)以及HTTP。与之相比, 标准调度只能使用微软RPC。 下面一个示例使用文件I/O和同步事件,来进行 代理程序和服务器对象之间的调度(参见图11-2)。 在这个示例中,我创建了三个部分来示范自定义调 度:一个ATL对象服务程序、一个ATL代理程序以 及- 个Win32客户程序。 图11-2自定义代理 因为我写了许多不必要的代码,所以使ATL对象服务程序变得很复杂。例如,其实根本不必 实现IMylnterface接口,因为调度程序实际上实现了方法的调用。为了便于阅读,我实现了COM+ 对象。图11-2中的COM+对象被删去了,说明的是在这个服务程序中没有必要实现COM+对象。 1 1 .4 .1 声明对象的类 淸单11-11给出了对象类的声明。两个数据成员保存着进行调度所必需的信息。成员变量 mjzEventname中是将触发数据调度的事件的名称,而成员变童mjzFilename中是包含被调度数 据的文件的名字。 清单1 1 -1 1 对象类的声明 II myinterface.h: Definition of the Mylnterface class II ////////////////////////////////////////////////////////////// #if !defined(AFX_MYINTERFACE_H) #define AFX_MYINTERFACE_H #if _MSC_VER >= 1000 #pragma once #endif II _MSC_VER >= 1000 #include 'resource.h“ II main symbols 服务器 存根 事件和文件输入输出系统 客户机进程 服务器逬程 tiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiuiiinjiiiininiiiiii/nii // Mylnterface第11章 调 良 207 struct OBJECTMARSHALDATA char szFilename[256]; char szEventname[256]; class Mylnterface : public IMylnterface, public CComObjectRoot, public CComCoClass, public IMarshal { public: Mylnterface。 { m_recordnumber = 0; } BEGIN_COM_MAP(Mylnterface) COM_INTERFACE_ENTRY(IMylnterface) COM_INTERFACE_ENTRY(IMarshal) END_COM_MAP() 11DECLARE_NOT_AGGREGATABLE(Mylnterface) II Remove the comment from the line above if you don't want II your object to support aggregation. DECLARE_REGISTRY_RESOURCEID(IDR_MyInterface) II IMylnterface public: STDMETHOO(Next)(long *); STDMETHOD{get_propertythree)(/*[out, retval]1 STDMETHOD(get_propertytwo)(/*[out, retval]*/ STDMETHOD(get_propertyone)(/*[out, retval]*/ public: static long m一recordnumber; I long *pVal); long *pVal); long *pVal); II IMarshal public: STDMETHOD(GetUninarshalClass)(REFIID, LPV0I0, DWORD, LPVOID, DWORD, LPCLSID); STDMETHOD(GetMarshalSizeMax)(REFIID, LPVOID, DWORD, LPVOID, DWORD, LPDWORD); STDMETHOD(Marshallnterface)(LPSTREAM, REFIID, LPVOID, DWORD, LPVOID, DWORD); STDMETHOD(Unmarshallnterface)(LPSTREAM, REFIID, LPVOID *); STDMETHOD(ReleaseMarshalData)(LPSTREAM); STDMETHOD(OisconnectOb)ect)(DWORD); private: char m_szEventnaroe[256]; public: static char m_szFilename[256]; #endif // 丨defined(AFX_MYINTERFACE_H) 对象服务程序实现了 IMylnterface和IMarshal两个接口。 1 1 .4 .2 定义对象的类 淸单11-12给出了对象类的定义。我有意省去了实现IMylnterface接口的代码,因为这段代码208 第二部分高级COM编程技巧 永远也不会被调用。 清单1 1 -1 2 对象类的定义 II myinterface.cpp : Implementation of CObjectApp and DLL II registration. #include “stdafx-h* #include “object.h“ #include “myinterface.h“ #include ^include *. .WproxyWproxy .h“ #include H . .'\\proxy\\proxy_i.c“ #include iiiiiiiiimiiiiniiiiiiiiuiiiiiniiiiiimiiiiiuiiiiin/m // static long array!][3] = {{1,2,3}, {4,5,6}, {7,8,9}}; char Mylnterface: :m__szFilenamel2561 - {0}; long Mylnterface::m_recordnumber = 0; IJIIIIIII/IIIUIIIIIIIIIIIIIIIIIHIIIIIUIIIIIIIIIIIIIIIIIIIII // Implementation of IMarshal STDMETHODIMP Mylnterface::GetUnmarshalClass(REFIID riid, LPVOID pv, DWORD dwCtx, LPVOID pvCtx, DWORD dwFlags, LPCLSID pCISIO) { ATLTRACE(_T('Mylnterface::GetUnmarshalClass\n“)); II The event and file are only known on the same machine, if (dwCtx & MSHCTX_DIFFERENTMACHINE) { return CO_E_CANT_REMOTE; *pClsID=CLSID_marshalproxy; return S_0K; STDMETHODIMP nterface::GetMarshalSizeMax(REFIID riid, LPVOID pv, RD dwDestCtx, LPVOID pvDestCtx, OWORD dwFlags, LPDWORD pdwSize) ATLTRACE(_T{“Mylnterface::GetMarshalSizeMax\na)); if (dwoestctx & MSHCTX_DIFFERENTMACHINE) { return C0_E_CANTJ^M0TE; } *pdwSize=Sizeof(OBJECTMARSHALDATA); return S_0K; void EventTrigger(void • marshaldata); STDMETHODIMP Mylnterface::Marshallnterface(LPSTREAM pstm,第JJ章 调 度 209 REFIID riid, LPVOID pv, DWORD dwOestCtx, LPVOID pvDestCtx, DWORD dwFlags) { ATLTRACE(__T{ 'Mylnterface: -.Marshallnterface\n“)); OBJECTMARSHALDATA marshaldata; if (dwDestCtx & MSHCTX^DIFFERENTMACHINE) { return CO E CANT REMOTE; strcpy(m_szFilename, _c:\\Hello.MAfT); strcpy(marshaldata.szFilename, m^szFilename); strcpy(m_szEventnatne, _MY_EVENT“y; strcpy (marshaldata. szEventname, m_szEvervtnarte); std::of stream os; os.open(m_szFilename, std::ios_base::out | std::ios_base::trunc | std::ios一base::binary); os « arTay【m_recordnumbern0] « ■ • « array【mrecordnumberl【1] << _ • « array[m_recordnumber](2); os.close(); _beginthread(EventTrigger, 0, (void *)&marshaldata); return pstm->Write((void *)&marshaldata, S i 2 8 0 f (OBJECTMARSHALDATA), NULL); STDMETHODIMP Mylnterface::UnmarshalInterface(LPSTREAM pstm, REFIID riid, LPVOID *pv) { ATLTRACE(_T(“Mylnterface::UnmarshalInterface\n“)); return E_NOTIMPL; STDMETHODIMP Mylnterface::ReleaseMarshalData(LPSTREAM pstm) { ATLTRACE(_T(“Mylnterface::ReleaseMarshalOata\nB)); return E_N0TIMPL; STDMETHODIMP Mylnterface::DisconnectOb)ect(DWORD dwfleserved) { ATLTRACE(_T(“Mylnterface::DisconnectObject\n“)); char szl256]; strcpy(sz, m_szEventname); strcat(sz, -:STOP-); HANDLE hevent = ::OpenEvent(EVENT_ALL_ACCESS, TRUE, sz); ::SetEvent(hevent); ::CloseHandle(hevent); return SJ)K; void EventTrigger(void * marshaldata)210 弟二部分高级COM编程技巧 SECURITY_ATTRIBUTES sa; SECURITY^DESCRIPTOR sd; sa.nLength = sizeof(SECURITY_ATTRIBUTES); sa.blnheritHandle = TRUE; sa.IpSecurityDescriptor = &sd; if(!InitializeSecurityDescriptor(&sd, SECURITY_DESCRIPTOR_REVISION)) { ExitThread⑴ ; } if(ISetSecurityDescriptorDacl(&sd, TRUE, (PACL)NULL, FALSE)) { ExitThread(1); > OBJECTMARSHALDATA • p = (OBJECTMARSHALDATA*) marshaldata; HANDLE hevent = ::CreateEvent(&sa, FALSE, FALSE, p->szEventname); char sz[256]; strcpy(sz, p->szEventname); st「cat(sz, ■一ACK_); HANDLE hack : ::CreateEvent(&sa, FALSE, FALSE, sz); strcpy(s2 , p->szEventname); strcat(sz, '^STOP'); HANDLE hstop = ::CreateEvent(&sa, FALSE, FALSE, sz); HANDLE harray(2] = {hevent, hstop}; while (1) { DWORD dw = ::WaitForMultiple0bjects(2 , harray, FALSE, INFINITE); if (dwl=WAIT_OBJECT_C) { break; } std::ifstream is; is.open(Mylnterface::m_szFilename, std::ios_base::in | std::ios_base::binary); long recordnumber; is » recordnumber; is.close(); if (recordnumber«=-l) { Mylnterface::m_recordnumber++; if (Mylnterface::m一recordnumber>2) { Mylnterface::m_recordnuwber = 0; > recordnumber = Mylnterface::m_recordnumber; .> else { (recordnu»ber)++; if ((recordnu«ber>2) || (recordnumber iiinniiiiiiuiiiiiiimiiiiii/iiiiiniiiimiiiiiiiiiniiiu n212 第二部分高级COM编程技巧 STDMETHODIMP marshalproxy::get_propertyone(long * pVal) { ATLTRACE(_T(“marshalproxy::get_propertyone\n*)); std::ifstream is; is.open(m_szFilename, std::ios_base::in | std::ios_base::binary); is » *pVal; is.close(); return S_0K; } STDMETHODIMP marshalproxy::get_propertytwo(long * pVal) { ATLTRACE(_T(“marshalproxy::get_propertytwo\n“)); std::ifstream is; is. open (m^szFilename, std:: ios_base: in | std::ios_base::binary); is » *pVal; is » *pVal; is.close(); return S OK; } STDMETHODIMP marshalproxy::get_propertythree(long * pVal) { ATLTRACE(_T('marshalproxy::get_propertythree\n“)); std::if stream is; is.open(m_szFilenaine, std: :ios_base: :in | std::ios_base::binary); is » *pVal; is » *pVal; is » *pVal; is.close(); return S_0K; > STDMETHODIMP marshalproxy::Next(long * precordnumber) { ATLTRACE(_T('marshalproxy::Next\n“)); long recordnumber; if (precordnumber==0) { recordnumber = -1; } else { recordnumber = *precordnumber; } std::ofstream os; os.open(m_szFilenarae, std::ios_base:rout | std::ios一base::trunc |os « recordnumber; os.close(); HANDLE hevent = ::OpenEvent(EVENT一ALL_ACCESS, TRUE, m_sz£ventname); ::SetEvent(hevent); ::CloseHandle(hevent); char sz(256]; strcpy(sz, m一szEventname); strcat(sz, “_ACK“); hevent = ::OpenEvent(EVENT_ALL_ACCESS, TRUE, sz); ::WaitForSingleObj ect(hevent, INFINITE); ::CloseHandle(hevent); if (precordnumber!=0) std::ios_base::binary); ios_base::in | ios_base::binary) std::ifstream is; is.open(m一szFilename, std std is » recordnumber; is » recordnumber; is » recordnumber; is » recordnumber; is.close(); •precordnumber = recordnumber; } return E_NOTIMPL; III 1111 f 111111 I I I 1111111111111111111111111111111丨 1111II1111111 II Implementation of IMarshal STDMETHODIMP marshalproxy::GetUnmarshalClass(REFIID riid, LPVOID pv, DWORD dwCtx, LPVOID pvCtx, DWORD dwFlags, LPCLSID pClsID) { ATLTRACE(_T(“marshalproxy::GetUnmarshalCXass\n“)); return E_NOTIMPL; } STDMETHODIMP marshalproxy::6etMarshalSizeMax(REFIID riid, LPVOID pv, DWORD dwDestCtx, LPVOID pvDestCtx, DWORD dwFlags LPDWORD pdwSize) { ATLTRACE(_T(•marshalproxy::GetMarshalSizeMax\n“)); if (dwDestCtx & MSHCTX^DIFFERENTMACHINE) { return C0_E_CANT_REM0TE; > *pdwSize=sizeof(OBJECTMARSHALDATA); return S OK; STDMETHODIMP marshalproxy: -.Marshallnterf ace (LPSTREAM pstm,214 第二部分高级COM编程彳t 巧 REFIID riid, LPVOID pv, DWORD dwDestCtx, LPVOID pvDestCtx, DWORD dwFlags) { ATLTRACE(_T(-marshalproxy::MarshalInterface\n“)); return E_NOTIMPL; } STOMETHOOIMP marshalproxy: -.Unraarshallnterf ace(LPSTREAM pstm, REFIID riid, LPVOID *pv) { ATLTRACE(_T('marshalproxy::UnmarshalInterface\n')); OBJECTMARSHALDATA objectmarshal; pstm->Read((void *)&objectmarshal, Sizeof(OBJECTMARSHALDATA), NULL); strcpy(m_szFilenamef obj ectmarshal.szFilename); strcpy(m_szEventname, objectmarshal.szEventname); return InternalQueryInterface(this, _GetEntries(), riid, pv); } * STDMETHODIMP marshalproxy::ReleaseMarshalData(LPSTREAM pstm) { ATLTRACE(_T(“marshalproxy::ReleaseMarshalData\n“)); return S OK: } STDMETHODIMP marshalproxy::DisconnectObject(DWORD dwReserved) { ATLTRACE(_T(“marshalproxy::DisconnectObject\n-)); return E_NOTIMPL; 这个代理程序的不可思议之处就是它的透明度。在这个例子中,调度就是将数据保存在文 件中,而反调度就是从文件中读取数据。三个代理数据存取程序浏览这个文件并读取数据。代 理方法marshalpmxy^NextO将输入数据保存在文件中,同时调用同步事件来触发对象服务程序。 当对象服务程序完成工作时,它将调用另一个同步事件来通知代理服务程序。之后,代理便可 以从文件中读取输出结果了。 这个示例实现了一个效率很低的调度类型。虽然文件I/O在性能上不能与标准调度相比,而 且这种类型的调度不能跨越网络工作,但是,如何进行调度的原理应该已经说的很清楚了。 服务器对象产生一个线程来接收由代理程序生成的客户请求。代理程序向事件发出信号, 从而得到结果。 1 1 .4 .4 客户程序 最后一个重要部分就是客户程序(参见清单11-14),它将例示服务器对象,然后调用这个对 象的方法。第h 章 调 彦 215 滴 单 1 1 - 1 4 客户靖代码 #include ^include ^include •objectWobject.h“ #include “obj ect\\obj ect_i.c' char s 丨1024】; HRESULT GetUnknown(WCHAR * strProgID, IUnknown ** ppunknown) { CLSID clsid; HRESULT hresult = ::CLSIOFromProgID(strProgID, Sclsid); if (FAILED(hresult)) { II CLSIDFroraProgID failed std::cout « ■CLSIDFromProglO failed = ■ « _ltoa(hresult, s, 16) « std::endl; ::DebugBreak(); return hresult; IClassFactory * pCF; hresult = •• :CoGetClassObject(clsid, CLSCTX_SERVER, NULL, IID一IClassFactory, (void **)&pCF); if (FAILED(hresult)) { *ppunknown = 0; std::cout « “CoGetClassObject failed =“ « _ltoa(hresult, s, 16) « std::endl; ::OebugBreak(); return hresult; IID^IUnknown, **)ppunknown); hresult = pCF->CreateInstance(NULL, (void pCF->Release(); if (FAILED(hresult)) { *ppunknown = 0; std::cout « “Createlnstance failed = _ « _ltoa(hresult, s, 16) « std::endl; : :DebugBreak(); return hresult; } return S OK; HRESULT Getlnterface(IUnknown * punknown, REFIID riid IUnknown ** ppunknown) { HRESULT hresult = punknown *>QueryInterface(riid,216 第二部分高级COM编程技巧 (void **)ppunknown); if (FAILED(hresult)) { // Querylnterface failed std::cout « “Querylnterface failed = “ « _ltoa(hresult, s, 16) « std::endl; ::DebugBreak(); return hresult; return S_0K; int nain() { CoInitialize(NULL); std::cout « “Start“ « std::endl; IUnknown * punknown; HRESULT hresult = GetUnknown(LNObject.MyInterface.1a, &punknown); if (FAILHD(hresult)) { II GetUnknown failed return hresult; IMylnterface * p tor; hresult = Getlnterface(punknown, IID^IMylnterface, (IUnknown **)&p_tor); if (FAILED(hresult)) { II GetUnknown failed return hresult; long 1(3] = {0}; hresult = p_tor->get_propertyone(&ll0]); hresult = p_tor->get_propertytwo(&l{1J); hresult = p_tor->get_propertythree(&l[21); std::cout « 1[0】 <<■“<< 1 [1 ] <<■• << « std::endl; p__tor->Next(NULL); hresult = p_tor->get_propertyone(&l[0l); hresult = pjtor->get—propertytwo(&l[lI); hresult = p_tor->get_propertythree{&l[2)); std::cout « 1[0] « “ “ « 1 (1 ] « • • « « std::endl; std::cout « “End“ « std::endl; CoUninitialize(); return 0: 1(2 ] 1[2 ) 当客户程序调用ICUssFactory::CreateInstance()时 ,COM +将调用对象服务程序中的 My Interface: :GetMarshalSizeMax()、My Interface ::GetUnmarshalClass()和Mylnterface::第11章 调 复 217 Marshallnterface()方法,然后调用代理程序中的marshalproxy::UnmarshalInterface()方法。在这 些代码都被调用之后,调度进程就被建立了起来,这时,在客户进程和服务器进程之间的数据 调度工作,就变成代理程序和服务程序的责任了。其他运行本章示例所必需的代码在CD-ROM 上,可以在Chapter 11目录下的custommarshal子目录中找到。 1 1 . 5 小结 调度是COM+中最难也最隐蔽的一部分,你应该用心地理解它。在本章中,我们讨论了什么 是调度,以及调度下面隐藏了什么。为了解决如何使调度在后台工作的问题,我们讨论了RPC 机制的基本内容。 我们还讨论了类型库以及它们之间的差别,并给出了使用每种类型库的理由,这样 ,你就 可以根据实际情况做出选择了。 随后,我们讨论了标准调度和自定义调度,并用示例代码说明了它们的性能。这些代码可 以使你理解如何使用调度接口进行各种操作。第12章 COM的安全性 本章内容包括: • COM与DCOM的安全性对比 • Windows安全性 •模拟 •说明性安全性 •程序的安全性 12.1 COM与DCOM的安全性对比 本章讨论如何使用COM+的安全性来允许和拒绝对COM+服务程序的访问。我在本章中将简 要地介绍COM+安全性,因为这是在讨论分布式话题(尤其是MTS和事务)之前必须掌握的重 要内容。而在本书的第18章 (“COM+的安全性” )中,将讨论COM+特定的安全性问题。 在DCOM发行之前,对象的安全性不是一个重要的问题。因为对象不能跨网络汸问其他对 象,所以你只需要考虑同一台机器上的对象。控制同一台计算机上的对象是相当容易的,但是, 要在一个有200个工作站的网络上管理它们就有些困难了,除非你拥有DCOM提供的内置安全 性。 有一种普遍的误解认为DCOM提供安全性,而COM不提供安全性,这是不正确的。因为 DCOM所增加的安全性应用起来与传统的COM对象、分布式COM (D C O M ) 对象以及非分布式 COM对象是等同的。完全可以禁止分布式COM而允许COM对象使用DCOM的安全性。DCOM 安全性和分布式对象可以彼此独立地使用。 我通常把COM安全性和DCOM安全性当作同义词使用,事实上它们也是相同的。当我在介 绍DCOM安全性之前提到COM安全性时,我实际的意思是,“DCOM之前的COM安全性' 为了 理解DCOM安全性和COM安全性等同这件事,可以把DCOM包当作两个已经介绍过的COM子系 统来考虑:远程子系统和安全性子系统。 COM的安全性和SSPI COM使用微软的安全性支持提供者接口(SSPI)来实现安全性。这使得COM可以为任何实 体提供安全性,只要这些实体使用SSPI接口。在这一点上,你也许会问,“为什么COM不直接汸 问安全性协议? ” 因为微软的长期策略是使COM在不同的平台上都可以使用,包括不能实现安 全性的平台,如Macintosh和UNIX。 另一个COM不能直接访问安全性协议的重要原因是,COM实际上只是DCE RPC (分布式计 算环境远程过程调用)的面向对象版本。DCE RPC也是以同样的方式设计的,它可以利用各种 不同的安全性提供者。第72章 COM的安全性 219 安全性一一更准确地说是NT LAN Manager Security Service Provider ( NTLMSSF ) --- 是 第一个使用SSPI接口暴露自己的安全性提供者。NTLMSSP提供者在所有的NT工作站和服务器 上都可以使用,因此,它成为用于DCOM安全性的主要候选对象(有时是惟一的候选对象)。为 了继续DCOM安全性的讨论,你可以从学习有关安全性的基础知识人手。 在Windows 2000中,网络验证的默认协议是Kerberos v5验证协议。作为正在成型的验证标 准 ,Kerberos协议为互操作性提供了基础。它也增强了企业范围内网络验证的安全性。在 Windows 2000中,这个协议实现的关键部分包括以下内容: • 使用Winlogon单一签到结构综合初始验证。 •在Windows 2000的域内,使用Active Directory--- Windows 2000中的 0 录服务程序--- 作为域安全账号数据库。 • 通过使用SSPI,把Kerberos客户程序当作Windows 2000的安全性提供者来实现。 12.2 Windows安全性 Windows为不同的对象类型提供安全性:文件、设备、进程、线程、信号、共享内存以及注 册表的键等等。为了实现这一安全性,在创建每个新对象的同时,还要生成一个安全性描述符。 要想管理每个工作站上的每个对象是非常困难的,所以,NT通过将NT工作站划分为域来简化安 全性操作。 拥 有 者 (拥有SID)、主体列表(DACL)和需要审核的主体的列表(SACL) —起被统称为 安全性描述符(SD)。安全性描述符以对象拥有者、对象访问权以及对象审核的形式完整地描述 了安全性策略。安全性描述符(自相关形式)是一个内存结构变量,它使用偏移量而不是指针 引用它的元素(DACL和SACL等 )。这种相对安全性描述符可以直接写入注册表的键中,并且可 以安全地重新获得。 1 2 .2 .1 完善域的安全性 想象一个由一组互连的计算机组成的域,在这组计算机中,有一台被当作主域控制器,它 就是用于验证网络资源访问权的中央管理验证控制器。 在相互信任的关系下,可以存在多个域。也就是说,如果一个安全主体不能通过一个域的 验证,那么主域控制器可以尝试验证一个受信任的域的安全主体。这使我们可以通过较小的域、 以较小的规模管理大型网络的配置。 使用用户标识和密码来提供验证权限,可以确保安全的网络访问。除非验证权限被指定, 否则将使用本地注册表数据库和域注册表数据库来验证网络访问。 1 2 .2 .2 安全性描述符 正如本章前面提到的,在创建每个新对象的同时,还要生成一个安全性描述符。安全性描 述符由两个安全ID ( S I D ) 和两个访问控制列表(ACL)组成。两个SID分别代表对象的拥有者 和拥有组,而两个ACL分别是任意访问控制列表(DACL)和系统访问控制列表(SACL)。我 们将在本章后面部分详细讨论ACL、DACL和SACL。220 第二部分高级COM编程技巧 当你在NT中创建新对象时,应该把结构变量SECURITY一ATTRIBUTES传给新建的对象。 SECURITY_ATTRIBUTES中包含有SECURlTY_DESCRIPTOR结构。淸单丨2-1给出了这两个结 构的定义。 清单 12-1 SECURITY_ATTFUBUTES 和 SECURITY 一 DESCRIPTOR 定义 typedef struct _SECURITY_ATTRIBUTES { DWORD nLength; LPVOID IpSecurityDescriptor; BOOL blnheritHandle; } SECURITY_ATTRIBUTES> *PSECURITY_ATTRIBUTES, *LPSECURITY_ATTRIBUTES; typedef struct _SECURITY_DESCRIPTOR { UCHAR Revision; UCHAR Sbz1; SECURITY_DESCRIPTOR_CONTROL Control; PSID Owner; PSID Group; PACL Sacl; PACL Dacl; } SECURITY—DESCRIPTOR, *PISECURITY_DESCRIPTOR; 不要试图直接使用SECURITY_DESCRIPTOR结构,而是应该使用API函数来读写一个安全 性描述符。即使在SECURITY_DESCRIPTOR结构发生变化时,API函数照样可以继续工作。这 些函数包括: GetSecurityDescriptorDacl() GetSecurityDescriptorSacl() GetSecurityDescriptorGroup() GetSecurityDescriptorLength() GetSecurityDescriptorOwner() InitializeSecurityDescriptor() IsValidSecurityDescriptor() MakeAbsoluteSD() MakeSelfRelativeSO() SetSecurityDescriptorDacl() SetSecurityDescriptorSacl() SetSecurityDescriptorGroup() SetSecurityOescriptorOwner() 在NT中,有两种安全性描述符: • 绝对安全性描述符拥有指向ACL结构的指针。 • 相对安全性描述符自身包含有嵌入的ACL。 相对安全性描述符的优势是,它可以被传输(由于它格式简单)或者被保存在文件或注册弟 章 COM的安全性 221 表中。有两个函数可以帮助你在两种安全性描述符之间进行转换:MakeSelfRelativeSDO和 MakeAbsoluteSD()。 第8章讨论了如何在注册表中保存和载入相对安全性描述符,而你必须要做的另一件重要的 任务是,将这些相对安全性描述符保存到文件中。清单12-2描述了如何去做这件事。 清单12-2在文件中保存安全描述 #include 禁include //............................ // SaveSecurityDescriptorToFile //............................ int SaveSecurityDescriptorToFile(PSECURITY_DESCRIPTOR pSD, ofstream & ofs) { II convert the security descriptor to II self-relative security description PSECURITY_DESCRIPTOR pSRSO = NULL; DWORD cbSD = 0; if (!: -.MakeSelfRelativeSD(pSD, pSRSD, &cbSD)) { DWORD dw = ::GetLastError(); if( dw != ERROR_INSUFFICIENT_BUFFER ) { if ( dw 丨= ERR0R_BAD_0ESCRIPT0R_F0RMAT) { std::cout « “Error ::MakeSelfRelativeSD • « dw « endl; return 0; > else { pSRSO = pSD; else { pSRSD : (PSEOURITY_OESCRIPTOR) ::LocalAlIoc(LPTR, cbSD); if (I::MakeSelfRelativeSD(pSD, pSRSD, &cbSD)) { std::cout « “Error ::MakeSelfRelativeSD _ « ::GetLastError() « endl; return 0; II save the self-relative security description // in a file ofs.write((char *)pSRSD, ::GetSecurityDescriptorLeagth(pSRSD)); if((pSRSD I* NULL) & (pSRSD !* pSD))222 第二部分高级COM编程彳支巧 ::LocalFree((HLOCAL) pSRSD); return 0; int main(int , char **) { const int cSD * 8096; UCHAR ucBuf[cSD] = {0}; DWORD dw = cSD; PSECURITY^DESCRIPTOR psd = (PSECURITY_DESCRIPTOR)&ucBuf; HKEY hkey; LONG 1 = ::RegOpenKeyEx(HKEY_CLASSES_ROOT, H MyOb j e c t.MyOb j ect *, 0, KEY_READ, &hkey); 1 = ::RegGetKeySecurity(hkey, ofstream ofs; ofs.open(“C:WHELLO.B“, ios::binary+ios::trunc+ios::out); :: SaveSecurityOescriptorToFile(psd, o fs ); ofs.close(); return 0; 清单12-2使用RegGetKeySecurityO函数得到一个现有的安全性描述符。其他的几个API函数 为你创建、获得并保存不同对象的安全性描述符。要特别注意的是,RegGetKeySecurityO函数 用于保存相对安全性描述符。这些API函数包括: OWNER_SECURITY_INFORMATION| GROUP_SECURITY_IN FORMATIONj DACL_SECURITY_INFORMATION, psd, &dw); CreateOirectory() CreateDirectoryEx() CreateEvent() CreateFile() CreateFileMapping() CreateMailslot() CreateMutex() CreateNamedPipe() CreatePipe() CreatePrivateObjectSecurity() CreateProcess() CreateProcessAsUser() GetFileSecurity{) GetKernelObjectSecurity() GetPrinter() GetPrivateObjectSecurity() GetUserObjectSecurity() NetShareGetInfo() NetShareSetInfo() QueryServiceObjectSecurity() RegGetKeySecurity() RegSetKeySecurity() SetFileSecurity() SetKernelObjectSecurity() CreateRemoteThread() SetPrinter()弟J2章 COM的安全性 223 typedef struct _ACE_HEADER { 一 BYTE AceType; BYTE AceFlags; WORD AceSize; } ACE_HEADER, *PACE_HEADER; #define ACCESS_ALLOWED_ACE_TYPE #define ACCESS:DENIED ACE_TYPE #define SYSTEM_AUDIT_ACE_TYPE #define SYSTEM_ALARM_ACE_TYPE typedef struct _ACCESS_ALLOWED_ACE { ACE_HEAOER Header; ACCESS_MASK Mask; DWORD SidStart; } ACCESS_ALLOWED_ACE, *PACCESS_ALLOWED ACE; CreateSemaphore() SetPrivateObjectSecurity() CreateThread() SetServiceObjectSecurity() CreateWaitableTime() SetUserObjectSecurity() DestroyPrivateObjectSecurity() 1.访 问 控 制 列 表 ACL其 实 就 是 一 个 存 放 有零个 或 多 个访 问控 制 人 口 (ACE) 的列表 。S E C U RIT Y _ DESCRIPTOR结构中包含两种类型的ACL: —种是任意ACL ( DACL ),它列出拒绝和允许对资 源进行访问的所有ACE;另一种是系统ACL (SACL),它规定了应用于资源的审核策略。ACL 决定谁有什么样的资源访问权。清单12-3给出了ACL结构变董的定义。ACL结构变量只是访问 控 制列表 (ACL) 的头部,完整的ACL包括ACL结构变童和其后的零个或多个访问控制入U (ACE)的列表。 清单12-3 ACL定义 typedef struct _ACL I UCHAR AclRevision; UCHAR Sbz1; USHORT AclSize; USHORT AceCount; USHORT Sbz2; } ACL, *PACL; 2 .访 问控 制 入 口 ACE由三部分组成:一个类型变童(标识ACE是允许访问还是拒绝访问)、一个访问掩码以 及一个SID。在每个ACL中包含零个或多个ACE。这个对象规定,一个特定的SID (可能是一个 用户账号或是一组用户)对访问掩码中列举出的访问,有访问权或者无访问权(根据类型变量 的标识)。清单12-4给出了ACE结构的定义。 清单12-4 ACL和ACE定义 \ / )/ )/ V J 0 12 3 X X X X 0 0 0 0 / \ / V / V /\224 第二部分高级COM编程技巧 typedef struct _ACCESS_DENIED_ACE { ACE_HEADER Header; ACCESS_MASK Mask; DWORD SidStart; } ACCESS_DENIED_ACE, *PACCESS_DENIED_ACE; typedef struct _SYSTEM_AUDIT_ACE { ACE_HEADER Header; ACCisSJAASK Mask; DWORD SidStart; > SYSTEM AUDIT ACE, *PSYSTEM AUDIT ACE; typedef struct _SYSTEM_ALARM_ACE { ACE_HEADER Header; ACCESS_MASK Mask; DWORD SidStart; > SYSTEM_ALARM_ACE,叩 SYSTEM_ALARM_ACE; 有四种ACE类型:访问允许、访问拒绝、系统审核以及系统报警。每个ACE类型都是分开 定义的,但它们具有相同的头部。 3 .访问掩码和访问权限 ACE结构中的一个重要的成员变量就是访问掩码,它是一个32位的数值,每一位的定义 如下: 0-15 指定访问权 16-23 标准访问权 16 DELETE 17 READ 一 CONTROL 18 WRITE 一 DAC 19 WRITE-OWNER 20 SYNCHRONIZE 24 ACCESS—SYSTEM 一 SECURITY 25 MAXIMUM_ALLOWED 26-27 保留 28-31 通用访问权 28 GENERIC 一 ALL 29 GENERIC 一 EXECUTE 30 GENERIC 一 WRITE 31 GENERIC READ 通用访问权和标准访问权可以应用于所有的对象,不管它们的对象类型是什么。从名称可 以看出,指定访问权是指定给受到保护的对象类型使用的。表12-1给出了文件对象所对应的指 定访问权。第/2章 COM的安全性 225 表12-1对文件对象的指定访问权限 文件对象 访M 权阪 FILE_READ_DATA 0x0001 FILE_WRITE_DATA 0x0002 f il r .a p p k n d _d a t a 0x0004 FILE_RKAD_EA 0x0008 FILE一WRITE JEA 0x0010 FILE_APPEND_EA 0x0020 FILE_RHAD_ATTRIBUTES 0x0080 FILE_WR!TH_ ATTRIBUTES 0x0100 通用访问权也可以映射为标准访问权和指定访问权,对象的类型决定映射关系。 因为有SECURITY_DESCRIPTOR结构,所以你不应该试图直接访问ACL结构,但是你可以 使用API函数读写ACL结构,这些函数如下: AddAcces sAllowedAce() AddAccessDeniedAce() AddAce() AddAuditAccessAce() DeleteAce() FindFirstFreeAce() GetAce() GetAclInformation() InitializeAcl() IsValidAcl() SetAclInformation() 清单12-5描述了如何读取安全性描述符及其ACL和ACE的大部分值。 清单 12-5 SECURITYJDESCRIPTOR 结构 ^include #include int main(int, char **) { const int cSD = 8096; UCHAR ucBuf(cSD] = {0}; DWORD dw = cSD; PSECURITY^DESCRIPTOR psd = (PSECURITY_DESCRIPTOR)&ucBuf HKEY hkey; LONG 1 = ::RegOpenKeyEx(HKEY_CLASSES_ROOT, • “MyObject.MyObject', 0, KEY一READ, &hkey); 1 = ::RegGetKeySecurity(hkey, OWNER_SECURITY_INFORMATION|226 第二部分高级COM编程技巧 GROUP_SECURITY_INFORMATION| DACL_SECDRITY_INFORMATION, psd, &dw); 1 = ::RegCloseKey(hkey); std::cout « “SECURITY_DESCRIPTOR: • « psd « endl; BOOL bHasDacl, bDefaulted; ACL* pDACL=NULL; ::GetSecurityDescriptorDacl(psd, &bHasDacl, &pDACL, &bDefaulted); std::cout « “ACL: • « pOACL « endl; if (pDACL) { ACE_HEADER* pAce=NULL; for (int i=0; iAceCount; i++) { ::GetAce(pDACL, i, (void**) &pAce); std::cout « endl « “ACE: “ « pAce « endl; switch (pAce->AceType) { case ACCESS_ALLOWED_ACE_TYPE: case ACCESS_DENIED_ACE_TYPE: { TCHAR SzUser[_^MAX_PATH]; TCHAR szDomain[_MAX_PATH]; DWORD dwUserSize=sizeof (szllser); DWORD dwDomainSize=sizeof(szDomain); SID_NAME_USE use; ::LookupAccountSid( NULL, &(((ACCESS一ALLOWED_ACE*)pAce)->SidStart), ♦ szUser, &dwUserSize, szDomain, &dwDornainSize, &use); if (pAce >AceType==ACCESS ALLOWED_ACE_TYPE) { ~ std::cout « “AceType:“ « HACCESS_ALLOWED_ACE_TYPE“ « endl; } else std::cout « « std std cout :cout std::cout break; “AceType: _ _ ACCESS_DENIED_ACE_TYPE“ endl: default: std::cout • break; « “User Name: _ « szUser « endl; « “User Domain: * « szDomain « endl; « “Access Mask: * « ((ACCESS_ALLOWED_ACE*)pAce)->Mask « endl « endl; « 'Unknown AGE type“ « endl;第J2聿 COM的安全性 227 return 0; 4 .安全性ID 在安全模型中,是通过使用安全性ID来识别用户和用户组的。而生成这些ID 的原则是,确 保它们在时间和空间上都是惟一的。空间上的惟一指的足没有哪两台NT计算机可以生成相同的 安全性ID。清单12-6给出了SID结构的定义。 清单12-6 SID定义 typedef struct _SIO_IDENTIFIER_AUTHORITY { BYTE Value【6]; } SID_IDENTIFIER_AUTHORITY, *PSID_IDENTIFIER_AUTHORITY; typedef struct _SIO { BYTE Revision; BYTE SubAuthorityCount; SID_IDENTIFIER_AUTHORITY IdentifierAuthority; DWORD SubAuthority[ANYSIZE_ARRAY]; } SID, *PISID; 有些SID被作为知名的SID引用,因为它们代表的是众所周知的安全性主体。下面列出了这 些 S1D: Null SID S-1-0-0 World S-l-1-0 Local S-1-2-0 Creator Owner ID S-1-3-0 Creator Group ID S-l-3-1 Creator Owner Server ID S-l-3-2 Creator Group Server ID S-l-3-3 (Non-unique IDs) S-l-4 NT Authority S-l-5 Dialup S-l-5-1 Network S-1-5-2 Batch S-1-5-3 Interactive S-l-5-4 Service S-1-5-6 AnonymousLogon S* 1-5-7 Proxy S-1-5-8 ServerLogon S-l-5-8 (Logon IDs) S-1-5-5-X-Y228 第二部分高级COM编程技巧 (NT nonunique IDs) (Built-in domain) S-1-5-21 S-l-5-32 有几个API函数可以帮你使用、查询和修改SID,它们是: LookupAccountName() LookupAccountSid() IsValidSidO EqualSid() EqualPrefixSid() AllocateAncJInitializeSid() FreeSid() InitializeSid() GetSidldentifierAuthority() GetSidSubAuthority() GetSidSubAuthorityCount() GetSidLength() CopySid() 5 .访问令牌 当一个用户登录到他的NT计算机上,他就被陚予一个访问令牌。这个访问令牌包括,他的 用户S1D、一组群组SID、一个默认SID 和一个用于创建新对象的ACL。当这个用户创建新的进程 或者线程时,访问令牌将被复制到新对象中。最终,线程将试图访问一•个被保护的对象。这时, 验证权限过程将根据线程的访问令牌和资源的安全性描述符,来决定允许或拒绝这一访问。我们 的想法是只进行一次验证—— 在创建句柄时。以不同的权限获得同一对象的两个句柄是可能的。 访问+ 牌结构没有任何定义,但是,有几个API函数可以帮你获得、查询和修改访问令牌, 它们是: OpenProcessToken() OpenThreadToken() DuplicateToken<) GetTokenInformation() SetTokenInformation() AdjustTokenPrivileges() AdjustTokenGroups() 清单12-7描述了如何获得当前进程的访问令牌,以及如何转储访问令牌中的所有SID。我把 这段代码称作SID阅读器,因为它可以让你看到访问令牌中的所有S1D。 清单12-7 S丨D浏览器 #include #include strstream SidToString(PSIO psid)第i 2 聿 COM的安全性 229 str « ((UL0NG)(pia->Value[5] (UL0NG)(pia.>Value[4] (UL0NG)(pia->Value[3] (UL0NG)(pia->Value[2l II retrieve count of sub authorities DWORD dw = *::GetSidSubAuthorityCount(psid); II append subauthorities for (int i-0 ; i < (int)dw ; i++) { str « « *::QetSidSubAuthority(psid, i); > return str; > int main(int, char **) DWORD dwSize = 0; HANDLE hToken; II retrieve the current process' access token handle if (1::OpenProcessToken( ::GetCurrentProcess(), TOKEN_QUERY, &hToken )) { std::cout « “Error ::OpenProcessToken • « ::GetLastError() « endl; return 0; } II retrieve the size of the token group structure if(i::GetTokenInformation(hToken, TokenGroups, NULL, dwSize, &dwSize)) { OWORD dw = ::GetLastError(); if( dw 丨= ERROR_INSUFFICIENT_BUFFER ) { strstream str; // is the sid valid? if(!::IsValidSid(psid)) { return str; } II retrieve identifier authority PS I D_I DENT IFI ER__AUTHOR IT Y pia = ::GetSidIdentifierAuthority(psid); II append prefix and revision number Str « “S-* « SID_REVISION « II append identifier authority if ( (pia->Value[0] != 0) || (pia*>Value[l1 != 0)) str « (USHORT)pia->Value[0] « (USH0RT)pia->Value(1) « (USH0RT)pia->Value(2) « (USH0RT}pia.>Valuel3】 « (USHORT)pia->Value(4] « (USHORT)pia->Valuel5]; } else 8 ) 6 ) 4)230 第二部分高级COM编程枝巧 std::cout « “Error ::GetTokenInformation ■ « dw « endl; return 0; > } II allocate the token group structure PTOKEN_GROUPS pTokenGroups = (PTOKEN_GROUPS) ::GlobalAlloc( GPTR, dwSize ); II retrieve the token group structure if (!: :GetTokenInforfnation(hToKen, TokenGroups, pTokenGroups, dwSize, &dwSize )) { std::cout « “Error ::GetTokenlnformation ■ « ::GetLastError() « endl; } else { // dump all the group sids for(int i=0; i<(int)pTokenGroups->GroupCount; i++) < std::cout « SidToSt「ing(pTokenG「oups.>Grc^ps[i】.Sid).str() « endl; II clean up if ( pTokenGroups ) { ::GlobalFree( pTokenGroups ); } return 0; 上面的代码不能在Windows 95中运行,安全性API 只能被Windows NT和2000实现。如果你 运行清单12-7中的代码,输出结果将和下面的内容相似: S-1-5-21 * 1958420805•1419734996•339680022 -513 S-1-5-32-544 S-1-5-5-0-5466 S-1-2-0 按照顺序,这些SID分别代表,用户账号、总域、管理员的域、登录对话、本地用户以及交 互用户。你可以使用本节前面列出的知名SID与这些SID相比较。 1 2 . 2 . 3 验证 COM有一个函数可以允许对象编写者修改他的模块的安全环境。CoInidalizeSecurityO函数第/2章 COM的安全性 231 允许程序员指定可选择的验证服务、验证级别和模拟级别。服务器和客户都可以使用这个函数。 下面给出了这个函数的参数: HRESULT CoInitializeSecurity( PSECURITYJ)ESCRIPTOR pVoid, //Points to security descriptor LONG cAuthSvc, //Count of entries in asAuthSvc SOLE_AUTHENTICATION_SERVICE * asAuthSvc, //Array of names to register void * pReservedl, //Reserved for future use DWORD dwAuthnLevel, //The default authentication level for proxies DWORD dwImpLevel, //The default impersonation level for proxies SOLE_AUTHENTICATION-LIST * pAuthList, //Authentication information for each II authentication service DWORD dwCapabilities, //Additional client and/or server-side capabilities void * pReserved3 //Reserved for future use ); 如果这个函数没有被调用,那么COM将调用使用传统设置(系统范围的默认设置)的函数。 传统设置存放在 Windows 系统注册表的 HKEY_LOCAL_>IACHINE\SOFTWARE\Microsoft\01e 主 键中。第8章的8.4.3节中讨论了这些默认设置。 验证服务将在以下服务中选择:NTLMSSP、Kerberos, Snego和SChannei等等。当新的验证 服务出现时,它们将被加入这个列表。 •NTLMSSP是本章前面讨论过的安全性服务。在Windows 2000中,Kerberos验证取代了 NTLM验证。 •Kerberos是NT平台上较新的一种安全服务工具包,它与其他的安全服务相比有很多优势。 其中一个与COM有关的优势就是Kerberos协议允许模拟过程得到授权。在使用NTLM的 Windows NT系统中,模拟过程不支持授权,这样,对网络资源的访问有可能会失败。在 Windows 2000系统中的Kerberos实现是基于互联网RFC 1510 Kerberos协议的定义的实现。 • Siiego实际上不是验证服务,它只是一个使用其他验证服务的虚拟的验证服务。 • Secure Channel ( SChannel) 支持加密套接字协议层(SSL ) 和专用通信技术(PCT ) 协议。 这种验证服务面向的是,需要拥有互联网公开密钥的安全基础结构的系统。验证服务提供 者使用CAPI接口,允许用户指定各种各样的密码和证书。 验证级别就是请求安全验证的数量。表12-2中列出的验证级别是可用的。 表1 2 - 2 可用的验证级别 验证级别 定 义 Default 在Windows NT 4.0中 ,缺省是Connect。在Windows 2000中 ,使用了一个覆 盖运算法则 None 不执行仟何验证 Connect 当客户连接服务器时执行验证 Call 在每个RPC调用中执行验证 Packet 在每个包传输时执行验证 Packet Integrity 与Packet相丨5 ] ,在包没有被修改时验iiF. Packet Privacy 与Packet相同 ,保证包数据的完整并加密数据232 第二部分高级COM编程技巧 连 接 (C o m i c c t ) 和 调 用 (C a l l ) 这两个验ffi级别只对基于连接的协议可用。无连接协议必 须使用三个数据包(Packet)验证级别之一,或者不进行验证。 1 2 . 3 模拟 调用CoInhializeSecurityO函数吋还有一个模拟级别参数。表12-3中列出了可用的模拟级别。 模拟就是一个线程在与它所厲的进程环境不同的安全环境中执行的能力。 远程访问客户模拟发生在一个人要接管一个现冇的已验证连接的时候。这个人侵者在连接 被验证之后,获得连接参数,断开用户,并接管这个已被验证的连接。 远程服务器模拟发生在一台计算机被远程访问客户当作是远程访问服务器的时候。模拟者 假装验证远程访问客户的证书,然后便捕获所有来自这个远程访问客户的信息。 表1 2 - 3 模拟级别 校拟级别 描 述 Default 仅在Windows 2000中可用 Anonymous 服务器没舍读或者访问的权利 Identity 服务器没有访问的权利,佴是有读的权利 Impersonate 服务器有读和i方问权 Delegate 仅在Windows 2000中可用,服务器有读和访问的权利, 激活cloaking 当COM服务程序被运行时,也就是正常的可执行程序被运行时,这些程序通常会假定运行 它们的用户的身份。用户可能不是对所有的资源都有访问权。如果你不提供控制COM服务程序 费份的能力,那么这些访问权限对你来说可能太少也可能过多。如果缺少访问权限,客户对服 务程序的访问就可能会导致访问被拒绝的错误,而如果访问权限过于丰富,客户对服务程序的 访问就可能会导致安全性遭到破坏。配置服务程序,使之可以运行于一组既不缺少也不过剩的 访问权限中,是可能办到的。但是,如果某些客户需要一些其他客户不能获得的访问权限,该 怎么办呢?在这种情况下,你可以要求COM服务程序根据用户改变权限。这是通过实现的身份 和模拟被引人COM的。你不仅可以使用客户身份证实访问权限,还可以使用客户身份获得权限 来代表客户执行任务。 1 2 . 3 . 1 伪装 伪装能力决定了在模拟过程中能以什么样的身份访问服务程序。伪装为服务程序提供了一 种以某个身份(与它本身的身份不同)出现的方法,用于代表客户汸问另一个服务程序。模拟 级别指的就是客户陚予服务程序的权限的多少。 模拟过程不需要伪装也可以进行,怛是这可能不是最好的选择,因为在某些情况下,域后 的服务程序需要知道初始调用者的身份,而不使用伪装是做+ 到这一点的。如果没有体现初始 调用者身份的伪装,那么很难确保只有经过授权的客户才能访问远程计算机。在不使用伪装的 时候,下游服务程序看到的身份是对它进行直接调用的进程。 而如果没有了模拟过程,伪装也是没有用武之地的。伪装只有在客户为模拟或授权设置了模 拟级别的时候才有用处(较低的模拟级别不允许服务程序进行伪装调用)。伪装是否能够成功依赖第72章 COM的安全性 233 于模拟级別,而这一级别表示的是服务程序代表客户动作的权限有多少,以及跨越计算机边界的 次数有多少。下面的讨论将说明在模拟过程中,伪装和模拟级别的选择是怎样影响整个过程的。 在某些情况下,当客户将模拟级别设为RPC一C_IMP一LEVEL_IMPERSONATE时,服务程序 就 可 以 进 行 伪 装 。然 而,在实际中会受到一定的 约 束 。 如 果初 始客 户 将模 拟级别 设为 RPC_C_IMP_LEVELJMPERSONATE,那么中介的服务程序(扮演同一台计算机上的客户)只 能伪装跨越一条计算机间的边界,这是因为impersonate级的模拟令牌只能通过一条计算机边界。 在越过边界之后,就只有本地资源可以被访问了。出现在服务程序面前的身份依赖于所设置的 伪装类型,而如果没有设置伪装,那么下游服务程序看到的身份将是对它进行直接调用的进程。 为了伪装通过多条边界,必须指.定合适的伪装能力标志和delegate级的模拟。使用这种模拟 级别,客户的本地证书和网络证书都被交给服务程序,因此,模拟令牌可以通过任意数量的边 界。再说一遍,出现在服务程序面前的身份依赖于所设置的伪装类型,而如果没有为delegate级 的模拟设置伪装,那么下游眼务程序看到的身份将是对它进行直接调用的进程。 例 如 ,假设进程A 调用进程B ,进程B 乂调用进程C 。B设置了伪装,A 将模拟级别设为 RPC_CJMP_LEVELJMPERSONATEo如果A 、B和C在同一台计算机上,那么将模拟令牌从A 传到B, 再传到C是可以的。但是,如果A和C在同一台计算机上,而B在另一台计算机上,那么 模拟令牌将可以从A传到B ,但不能再从B传到C 。从B到C的调用将告失败,因为在伪装的情况 下B不能调用C。然而,如果A 将模拟级别设为RPC_C_IMP_LEVEL_DELEGATE,那么令牌就 可以从B传到C ,调用便会成功。 12.3.2 ColmpersonateClient()和CoRevertToSelf() CoImpersonateClientO函数允许COM服务程序可以使用调用客户进程的身份,在COM服务 程序没有足够的访问权限来代表调用客户进程执行任务时,你可能会用到这个函数。当COM服 务程序打算停止模拟客户并返回原来身份的时候,应该调用CoRevertToSelfO函数。 清单12-8给出了这两个辅助函数的实现过程。 清单 12-8 ColmpersonateC丨ient()和CoRevertToSe丨f()的实现 WINOLEAPI CoImpersonateClient() { IServerSecurity *pss; ::CoGetCallContext( IID_IServerSecurity, (void **) &pss); HRESULT hresult = pss->ImpersonateClient(); pss ->Release(); return hresult; } WINOLEAPI CoRevertToSelf() { IServerSecurity *pss; ::CoGetCallContext(IID_IServerSecurity, (void**)&pss); HRESULT hresult = pss->RevertToSelf(); pss->Release(); return hresult; }234 第二部分高级COM编程故巧 函数 CoImpersonateClient()和 CoRevertToSelf()使用了 在服务程序中实现的 IServerSecurity 接 口。清单12-9是这个接口的M1DL定义。要着電注意的是,对于标准调度来说,IServerSecurity 接口几乎总是在占位级被实现。惟一的例外是对自定义调度的响应。 顾名思义,IServerSecurity接口的CoGetCallContextO方法只对当前调用有效。 清单12-9 IServerSecurity 接 口的 MIDL 定义 interface IServerSecurity : IUnknown { HRESULT QueryBlanket [out] DWORD *pAuthnSvc, [out] DWORD *pAuthzSvc, (out) OLECHAR **pServerPrincName, lout] DWORD•pAuthnLevel, lout] DWORD ♦plmpLevel, [out] void **pPrivs, [out] DWORD •pCapabilities HRESULT ImpersonateClient(); HRESULT RevertToSelf(); BOOL IsImpersonating(); 另一个函数可以告诉你服务程序当前是否在模拟客户,这个函数就是IServerSecurity接口的 IsImpersonating()0 这个方法没有辅助函数,但是你可以实现自己的辅助函数,如清单12-10中所 不。你可能已经从代码中注意到,我们是在服务程序中,这就意味着ImpersonateClient()函数已 经被调用过了。 清单 12-10 Colslmpersonating()的实现 BOOL CoIsImpersonating() { IServerSecurity pss; CoGetCaliContext(IID一IServerSecurity, (void**)&pss); BOOL b = pss->IsImpersonating(); pss->Release(); return b; IServerSecurity接口的最后一个函数是IServerSecurity::QueryBlanketO方法,清单 12-11 给出 广 它 的 辅 助 函 数 (!!0(311670丨6 1 ^ 6 ( :11价丫0 。 清单 12-11 CoQueryClientSecurlty()的实现 WINOLEAPI CoQueryClientSecurity() { IServerSecurity *pss; •• :CoGetCallContext(IID—IServerSecurity, (void**)&pss);第72章 COM的安全性 235 HRESULT hresult ~ pss->QueryBlanket(); pss->Release(); return hresult; 这个函数用于查询调用服务程序的客户的安全设置。 1 2 . 3 . 3 伪装 我们通常把前面描述的delegate级模拟称为伪装。这种类型的模拟在对其他服务程序的调用 过程中,通过用客户身份掩饰中介服务程序身份的办法,隐藏了服务程序的真实身份。 1 2 . 4 说明性安全性 正如第8章中所讨论的,我们可以在注册表中为COM服务程序声明安全性,这有时被称为说 明性安全性或者完全进程安全性。注意,这里的说明性安全性和完全进程安全性不完全相同。 说明性安全性由Windows系统注册表中的设置组成,这些设置被用于COM服务程序被运行或被 访问时。而完全进程安全性不仅包括注册表中的设置,而且还包括为整个进程进行COM服务程 序设置的其他机制,如函数CoInitiaUzeSecurityO。 在第13章中,我们将讨论如何使用DCOMCNFG和OLEView两个工具,为COM服务程序配 置完全进程安全性的设置。 1 2 . 5 程序的安全性 本 章 的 前 面 部 分 曾 经 讨 论 过 程 序 安 全 性 的 两 个 重 要 组 成 部 分 —— 用于模拟客户的 IServerSecurity接口和用于设置完全进程安全性的CoInidalizeSecurityO函数。函数Colnitialize SecurityO为你的进程创建了一层安全外壳,也就是安全性设置。 1 2 . 5 . 1 安全外壳 那么什么是安全外壳呢?安全外壳就是一组安全属性,它们是: • 验证服务。 • 授权服务。 • 主体名称。 • 验证级别。 • 模拟级别。 • 验证身份。 • 权能。 • ACLo 客户程序和服务程序都要通过调用CoInitiaUzeSecurityO函数创建自己的安全外壳。如果客 户程序或服务程序未能调用这个函数,COM将代表进程使用默认值调用这个函数。 COM使用客户程序和服务程序的安全外壳,与代理程序的安全外壳进行协商。COM将选择 一种在客户程序和服务程序上都可用的验证服务(和一种授权服务以及与之相对应的主体名称),236 第二部分高级COM编程故巧 COM将选择由客户程序或服务程序指定的最高验i正级别,COM还将选择客户的模拟级别、验证 身 份 (与所选的验证服务相对应)和权能。 在创建代理时,安全外壳在协商。当代理创建完毕以后,客户程序将使用下一节介绍的 IClientSecurity 接 口 接管代理。 12.5.2 IClientSecurity IClientSecurity接口允许客户程序查询和修改接口代理的安全性设置。清 单 】2-1 2是 IClientSecurity接 口 的M1DL定义。 清单 12-12 IClientSecurity接口的MIDL定义 interface IClientSecurity : IUnknown { HRESULT QueryBlanket [in】 IUnknown *pProxy, [out] DWORD *pAuthnSvc, [out] DWORD ♦pAuthzSvc, tout] OLECHAR * *pServe「P「incName, [out] DWORD *pAuthnLevel, [out] DWORD ♦plmpLevel, [out] void **pAuthInfo, [out] DWORD ♦pCapabilites HRESULT SetBlanket [in] IUnknown *pProxy, [in] DWORD AuthnSvc, lin] DWORD AuthzSvc, [in] OLECHAR *pServerPrincName, [in] DWORD . AuthnLevel, 【in] DWORD ImpLevel, lin] void •pAuthlnfo, [in] DWORD Capabilities HRESULT CopyProxy ( [in) IUnknown *pProxy, (out] IUnknown **ppCopy ); } 这个接口有三个辅助函数:CoQueryProxyBlanket()、CoSetProxyBlanket()和CoCopyProxyO。 清单12-13列出了这三个辅助函数。 清单12-13 IClientSecurity帮助文件的实现 WINOLEAPI CoQueryClientSecurity(IUnknown * pProxy DWORD * pAuthnSvc, DWORD * pAuthzSvcf第/2章 COM的安全性 237 OLECHAR ** pServerPrincName, OWORD * pAuthnLevel, DWORD * plmpLevel, RPC_AUTH_IDENTITY_HANDLE * ppAuthInfoy DWORD * pCapabiliiies) { IClientSecurity *pcs; pProxy->QueryInterface(IID_IClientSecurity, (void**)&pcs); HRESULT hresult = pcs->QueryBlanKet(pProxy, pAuthnSvc, pAuthzSvc, pServerPrincName, pAuthnLevel, plmpLevel, ppAuthlnfo, pCapabilities); pcs->Release(); return hresult; } HRESULT CoSetProxyBlanket(IUnknown * pProxy, DWORD dwAuthnSvc, DWORD dwAuthzSvc, WCHAR * pServerPrincName t DWORD dwAuthnLevel, DWORD OwImpLevel, RPC_AUTH_IDENTITY_HANDLE pAuthlnfo 、 DWORD dwCapabilities) { IClientSecurity *pcs; pProxy->QueryInterface(IID_IClientSecurity, (void**)&pcs); pcs->SetBlanket(pProxy, dwAuthnSvc, dwAuthzSvc, pServerPrincName, dwAuthnLevel, dwImpLevel, pAuthlnfo, dwCapabilities); pcs->Release(); } HRESULT CoCopyProxy( IUnknown * pProxy, IUnknown ** ppCopy) { IClientSecurity *pcs; pProxy•>QueryInterface(IID_IClientSecurity, (void**)&pcs); pcs >CopyProxy(punkProxy, ppunkCopy); pcs->Release(); } CoQueryProxyBlanketO函数,被用来查洵将用于调用接口代理中的COM对象方法的安全性 设置。 CoSetProxyBlanketO函数,被用来修改将用于调用接口代理中的COM对象方法的安全性设 置。如果你把一个参数设为默认值,那么CoSetProxyBlanketO函数不会改变安全属性,即使这个 属性没有被设为默认值。 CoCopyProxyO函数使客户程序可以得到代理的一个拷贝,这样,以后对CoSetProxyBlanket() 函数的调用就不会影响到使用相同代理的其他客户程序的安全性。注意,CoCopyProxyO函数或 IClientSecurity::CopyProxy()方法的调用者有责任释放新的代理。调用CoCopyProxyO函数还可以 用于确保其他客户程序不能修改新代理的安全设置(因为它们一直使用的是原来的代理)。 1 2 .5 .3 访问和运行的安全性 访问权限决定了是否允许用户调用COM服务程序对象的方法,而运行权限决定了是否允许238 第二部分高级COM编程故巧 用户启动COM服务程序。有了这两个分开的权限,我们就可以配置客户程序,使它们不仅可以 访问已经在运行的COM服务程序中的对象,而且可以拒绝同样的客户程序后动COM服务程序的 权能。 在第8章中我们详细地讨论过这些权限。在那一章里,你学到了如何配置系统范围的默认权 限,以及如何配置COM服务程序指定的权限。 在第13章屮,我们将讨论如何使用DCOMCNFG工具配置访问权限和运行权限。 1 2 . 6 小结 本章讨论了如何使用COM的安全性来允许和拒绝对COM服务程序的访问。本章开始部分介 绍了 一些与COM有关的Windows安全性的基本概念。 接着,讨论了安全性描述符。我对这些描述符进行了访问并讨论了它们的用途。随后讨论 了不同的描述符类型,还给出了相关的注册表入口。 我们还了解了访问控制列表(ACL)以及用于访问它们的AF>I函数。然后,一个特殊的示例 程序用于通读当前系统的ACL。 最后一个重要的话题是模拟和伪装,如果想全面地理解COM的安全性,就必须理解模拟和 伪装。 COM的安全性是一个难点。但由于COM的安全性对你的分布式编程影响很大,因此,你应 该多花些时间来学习这部分内容。第13章配置和错误处理 这部分内容包括: •使用DCOMCNFG配置COM+对象 •使用OLE2View程序 •错误处理 13 . 1 使用DCOMCNFG配置COM+对象 本章将讨论如何使用DCOMCNFG和OLEVIEW两个高级实用程序,在系统范围的基础上, 配置COM+对象,还将讨论处理COM+错误代码的不同策略。 注 意 如果你不熟悉COM+的注册表设置,先看一下第8章 ,“COM+和注册表”,那一 章有助于理解注册表的一些术语o DCOMCNFG安装在计算机的system32文件夹中,这个实用程序是手动配罝COM+安全性和 远程访问的标准方法。它的用户界面(UI) 是一个具有四个选项卡的属性页(参见图13-1)。 nCEEB1BM43^11CF«F6 ] •• ••= •{Description of the server} 注 意 第二个注册表项有一个““ 键值,无论什么时候我提到” ”键值,我指的都是默认 鍵值。 清单1 3 - 1 注册表文件 MyProject.RGS HKCR { NoRemove Appin { {E5A784C4 - 3498 • 1102 -9C2B - 44455354»M®> = s * MyProject' 'MyProject.EXE' < val AppID = S {E5A784C4-3498 -11D2-9C2B-444553540000} } } > MyObject.RGS HKCR { MyObject.MyObject.1 * s 'MyObject Class' CLSID = s *{E5A784D3-3498 -11D2-9C2B-444553540000}1 } MyObject.MyObject = s 'MyObject Class' { CurVer = s 'MyObject.MyObject•1_ } NoRemove CLSID { ForceRemove {E5A78403-3498 -1102-9C2B-444553540000}= s 'MyObject Class * ProgID = s 'MyObject.MyObject.1' VersionlndependentProgID = s 'MyObject.MyObject ForceRemove 'Programmable' LocalServer32 = s '%M0DULE%'弟“/J章 配置和错误处理 241 val AppIO = s '{E5A784C4-3498-1102•9C2B•444553540000>1 VC++将这些RGS文件编译成模块的资源。当我们使用RegServer参数启动服务程序的时候, 这些RGS注册表项将自动被载入注册表《这些都是在调用Module.UpdateRcgistryFrom Resource(IDR_MyProject, TRUE)时完成的。清单 13-2说明了应该在哪里调用UpdateRegistryFrom Resource(),以及如何在主程序中编写自注册RegServer和UnregServer参数c 下面的代码是由 ATL AppWizard和ObjectWizard生成的。 清单1 3 - 2 可执行的自注册 LPCTSTR FindOneOf(LPCTSTR p1, LPCTSTR p2) { while (*p1 != NULL) { LPCTSTR p = p2; while (*p !- NULL) { if (*p1 == *p++) return pl+1; } p u + ; } return NULL; extern _CM int WINAPI _tWinMain(HINSTANCE hlnstance, HINSTANCE , LPTSTR~lpCmdLine, int ) { lpCmdLine = GetCommandLine(); //necessary for _ATL_MIN_CRT HRESULT hRes = CoInitialize(NULL); // If you are running on NT 4.0 or higher you can use the II following call instead to make the EXE free threaded. // This means that calls come in on a random RPC thread II HRESULT hRes = ColnitializeEx(NULL, COINIT_WULTITHREADED); _ASSERTE(SUCCEEDED(hRes)); _Module.Init(ObjectMap, hlnstance); _Modul©.dwThreadlD = 6etCurrentThreadId(); TCHAR szTokensl] = int nRet = 0; BOOL bRun = TRUE; LPCTSTR IpszToken = FindOneOf(lpCmdLine, szTokens); while (IpszToken 1= NULL) { if (1strcmpi(IpszToken, _T(“UnregServer“))==0) _Module • UpdateRegistryFroniResource{IDR_MyProject, FALSE); nRet = _Module.UnreflisterServer(); bRun = FALSE; break;242 第二部分高级COM编程技巧 if (lstrcmpi(lpszToken, __T(“RegServer“))=-0) { 一 Module•UpdateRegistryFromResource(IDR_MyProject, TRUE); nRet = _Module.RegisterServer(TRUE); bRun = FALSE; break; } IpszToken = FindOneOf(IpszToken, szTokens); } if (bRun) { hRes = Module.RegisterClassObjects( CLSCTX_LOCAL_SERVER, REGCLS_MULTIPLEUSE); _ASSERTE(SUCCEEOED(hRes )); MSG msg; while (GetMessage(&msg, C, 0 , 0 )) DispatchMessage(&msg); Module.RevokeClassObj ects(); } CoUninitialize(); return nRet; 1 3 . 1 . 2 创建自动服务程序 下面的几节将讨论几个示例,可以帮助你理解DCOMCNFG是如何作用于动服务程序的。 这些示例使用的是一个简单的进程外服务程序,它只有一个对象和一个属性。本节介绍这个服 务程序。 这个进程外服务程序是按照以下步骤,在VC++ 6.0中创建的: 1)启动VC++并在菜单栏中选择File,Newc 2 )在列表框中选择ATL COM+ App Wizard,在项目名称栏中键入MyProject,单击OK。 3 )在ATLCOM+AppWizard向导中,选择Executable单选按钮并单击Hnish,VC++的主窗口 重新出现。 4 )在菜单栏中选择Insert, New ATL Object。 5 ) 给新对象命名为MyObject,这样,接口就是IM yO bject,而C++对象就是CMyObject,之 后 ,又一次回到VC++的主窗口。 6 ) 右键单击IMyObject接口,然后在弹出菜单中选择Add Property。在Property Type组合框 中选择Long,在属性名称栏中键入MyProperty,并取消对Put单选按钮的选定(这将使屑性变得 只读)。 剩下的惟一一件事就是编写geUMyProperty处理函数的代码。在清单13-3到13-5中,我给出 了微软接口定义语言(MIDL)文件和CPP文件。茗73章 配置和错误处理 243 清单1 3 - 3 简单的本地服务IDL II MyProject.idl : IDL source for MyProject.dll // // This file will be processed by the MIDL tool to // produce the type library (MyProject.tlb) and marshaling II code. import “oaidl.idl“; import “ocidl.idl“; object, uuid(E5A784D2-3498-11D2-9C2B•444553540000), dual, helpstring('IMyObj ect Interface“), pointer_default(unique) ) interface IMyObject : IDispatch { [propget, id(1), helpstring(“property MyProperty“)] HRESULT MyProperty(Iout, retval] long *pVal); uuid(E5A784C3•3498-11D2-9C2B-444553540000), version(l.0), helpstring{“MyProject 1.0 Type Library“) 1 library MYPROJECTLib { importlib(,,stdole32.tlb“); importlib('stdole2.tlb'); UUid(E5A784D3•3498-11D2-9C2B•444553540000), helpstring(“MyObject Class“) I coclass MyObject { [default] interface IMyObject; 清单1 3 - 4 简单的本地服务对象头 II MyObject.h : Declaration of the CMyObject #ifndef _MYOBJECT_H_ #define :MYOBJECT:H: #include “resource.h“ II main symbols /////////////////////////////////////////////////////////// II CMyObject Class ATL_NO_VTABLE CMyObject : public CComObjectRootEx,244 第二部分高级COM编 程 巧 public CComCoClass, public IDispatchImpl { public: CMyObjectO { } OECLARE_REGISTRY_RESOURCEID(IDR_MYOBJECT) BEGIN_COM_MAP(CMyObject) COM^INTERFACE_ENTRY(IMyObject) COM_INTERFACE_ENTRY(IDispatch) END一C0M_MAP{) // IMyObject public: STDMETHOD(get_MyProperty)(/*[out, retval]*/ long ♦pVal); ^endif // MYOBJECT H _■ 一 ■ 清单1 3 - 5 简单的本地服务对象实现 II MyObject.cpp : Implementation of CMyObject #include “stdafx.h' ^include “MyProject.h“ #include “MyObject.h* /////////////////////////////////////////////////// // CMyObject STDMETHODIMP CMyObj ect::get_MyProperty(long * pVal) { II TODO: Add your implementation code here *pval = 10 ; return S_0K; } 上述代码不是手工编写的,而是用向导创建的。惟一要动手编写的代码是在清单13-5中的 CMyObject::get_MyProperty()方法中添加 *pVal = 10。 清单13-3是MyProject.dll的IDL源文件,在这里,你可以看到接口的声明。 清单13-4包含了CMyObject类的声明,它是实现IMyObject接口的类。你还可以在这里看到 CComSingleThreadModel 类。 清单13-5给出了CMyObject类的实现代码,因为只有一个用于得到属性的方法,所以它的内 容不多。 1 3 . 1 . 3 默认属性 DCOMCNFG属性页对话框的第二个选项卡是默认属性选项卡(参见图13-2)。这一页使你^ 1 3 t 配置和错误处理 245 可以改变我们在第8章中讨论过的四项分开的选择,它们都可以在注册表的AppID结构中找到。 D«taAPkop««n|o«taiiSMu#| * D_hifciCOMwHicoi^U HRESULT LaunchAndAccess() { II HRESULT returned from last automation call HRESULT hresult; II IDISPATCH interface pointer for the existing object LPDISPATCH pdisp = NULL; II hold exception info here EXCEPINFO excepinfo; II hold argument error here UINT uArgErr; // Convert the progid to a clsid CLSID clsid; hresult = ::CLSIOFroniProgID(OLESTR(“MyObject.MyObject.r ), &clsid); if (hresult)第73章 配置和错误处理 247 II CLSIDFromProgID failed return hresult; II Create the instance of the object LPUNKNOWN punk; hresult = •• :CoC「eateInstance(clsid, NULL, CLSCTX_SERVER, IID_IUnknown, (LPVOID FAR~*) &punk); if (hresult) { // CoCreatelnstance failed return ^result; II Retrieve the interface pointer hresult = punk->QueryInterface(IIO_IDispatch, (LPVOID FAR *)&pdisp); if (hresult) { II Query Interface failed return hresult; II Release the IUnknown reference, hresult = punk->Release(); II return an exception if the dispatch pointer is NULL if (pdisp==NULL) { return E UNEXPECTED; II Retrieve the dispatch id for the function name DISPID dispid; BSTR bstr = ::SysAllocString(OLESTRCMyproperty“)); hresult = pdisp•>GetIDsOfNames(IID_NULL, &bstr, 1, LOCALE_USER_DEFAULT, &dispid); ::SysFreeString(bstr); if (hresult) { II GetlDsOfNames failed return hresult; II Invoke the property GET DISPPARAMS dispparams = {0}; VARIANTARG variantarg; Variantlnit(&variantarg); ::memset(&excepinfo, 0 , sizeof(EXCEPINFO)); uArgErr = 0 ; hresult = pdisp->Invoke(dispid, IID_NULL, LOCALE一USER:DEFAULT, DISPATCH^PROPERTYGET, &dispparams, &variantarg, Aexcepinfo, &uArgErr);248 第二部分高级COM编程枝巧 if (hresult) { // Invoke failed return hresult; >; pdisp->Release(); if (variantarg.lVal!=l0) { // Wrong result return E_UNEXPECTED; }; return S_OK; }; int main(int, char **) CoInitialize(NULL); LaunchAndAccess(); CoUninitialize(); return 0; ______}__________________________________________________________________________________________________________________ 在 代 码 中 ,你会发现它调用CoCreate丨im an ce O 例 示 对 象 ,并重新获得接口。接 着 , MyPmperty属性被hwokeO方法引用。总而言之,这段代码例示了对象并汸问了厲性。 如果你使用默认安全性设置运行这段测试代码,那么自动服务程序将会运行并被访问.不 会出现任何问题。 现在拒绝访问权限。在Default Access Permissions对话框中,为你的用户账号添加Deny Rights。如果你使用这种设置运行测试代码,服务程序将可以正常运行,但是当你试图通过调用 CoCreate丨nstanceO函数来访问对象的时候,将会出现错误。通过查看任务列表,你可以证实服 务程序已经正确地运行了。 现在,如果将身份验证级别设为N o n e ,会怎样呢?回到前面--个厲性页,即图13-2所示的 默认属性页,将身份验证级别设为None。这一次,代码列表(指的是清单13-6) 可以正常地完 成工作。正如前一节所提到的,将身份验证级别设为None将禁止安全性检查3 在进行完这个小 测试之后,将身份验证级别复位到默认值。 2 .设置运行权限 在Default Launch Permissions框中单击Edit Default按 钮 ,你就可以修改注册表主键 HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\01e中的DefaultLaunchPermission键值。这项 设置决定了谁可以运行那些不指定自定义运行权限的对象。 现在拒绝运行权限。但是 ,在你尝试下一个试验之前,不要忘f 删除在上一个试验中创建 的 Deny Access权限 0 在默认安全对话框的Default Launch Pemnissions框中,为你的用户账号添加Deny Rights。如 果你使用这种设置运行测试代码(清单13-6),服务程序将不会启动,并且当你试图通过调用 CoCreatelnstanceO函数来创建对象的时候,将会出现错误。^ 1 3 t 配置和错误处理 249 如果你在命令行或在资源管理器中手动£1动服务程序,相同的测试将会成功。因为服务程 序已经被启动了,所以对CoCreatelnstanceO函数的调用将不会去启动服务程序c 3 .设置配置权限 在Default Configuration Permissions框中单击Edit Default按钮,你就可以修改注册表 HKEY_CLASSES_ROOT的子键中的安全性权限。这项设罝决定了谁可以修改那些不指定自定 义配莨权限的对象的配S 。 我绝对不会建议你修改这些设置3 你要做的最后一件事就是拒绝某人修改这个注册表键. 因为应用程序为了安装ft己必须拥有对这个注册表键的访问权,所以 ,除非你有特別的需要, 如远程安装应用程序或者禁止用户安装COM+对象,否则修改这个注册表键的安全性权限是+ 可取的。 1 3 . 1 . 5 配置COM+服务程序 单击Application标签,返回到COM+服务程序的列表。如果你从这个列表中选择了一个 COM +服务程序,并单击Properties按 钮,另一个属性页对话框将出现,你可以在这里配置 COM4•服务程序(参见图13-4 )。 在Genera丨页中有四项属性:应用程序名称、 应用程序类型、身份验证级别以及服务名称。 DCOMCNFG不支持你改变这些设置。这些设H 也可以在注册表中找到。应用程序名称就是注册 表子键 HKEY-CLASSES—ROOTAAppIDVmy- clsid} 的键值。 应用程序类型如下: • 进程内处理程序。 • 进程内服务程序。 • 本地服务。 • 本地服务程序。 • 远程服务程序。 DCOMCNFG检索对象的注册表子键HKEY_ CLASSES JROOT\CLSID\{my-clsid}和 HKEY_ CLASSES_ROOT\AppID\{my-clsid}, 它首先找到的设置被用作对象的应用程序类型。对于进程 内处理程序、进程内服务程序和本地服务程序,这一设置是在注册表子键 HKEY_CLASSESJROOT\CLSID\{my-dsid}中找到的。而对于本地服务和远程服务程序,这-- 设置则是在注册表子键HKEY_CLASSES_ROOT\AppID\{my-clsid}中找到的。第8章中曾经详细 地讨论过ApplD注册表树。 应用程序的位置就是上一段中讨论的应用程序类型注册表子键中的键值名。对于本地服务 程序、进程内服务程序和进程内处理程序,位置就是模块的本地路径。而对于本地服务,位置 则是服务的名称。对于远程服务程序,位置就是远程服务程序的名称。 图13-4 DCOMCNFG应用程序属性250 第二部分高级COM编程技巧 1 3 . 1 . 6 服务程序的位置 DCOMCNFG的位置选项卡中有三个复选框,如图13-5所示。 jLijy Ganaral Ih t kAowng Mtttngi «IDm DCOM to hX4M • « coar«ct I iwfcjfcort HyouiMtofMMih«voM»^Miian.*i*nl «xAc4tteon» Of* nmt wmdi yout »l»cliani P Akff\apolr«iian on iM Maputo 「RuntpplMtoanonOwtalDMrvc OK 图1 3 -5 选择DCOMCNFG服务器的位置 第一个复选框用于允许和禁止存储器的激活,这等同于设置注册表中的ActivateAtStorage值。 存储器的激活意味着COM+对象将在数据文件所在的计算机上运行。 第二个复选框用于通知COM+运行并连接本地计算机上的COM+服务程序c 第三个复选框用 于通知COM+运行并连接远程计算机上的COM+服务程序,选择这个复选框将在注册表子键 HKEY_CLASSES_ROOT\AppID\{my-clsid}中写人RemoteServer设置,并且使得下面的编辑框和 浏览按钮控件可用,这些控件允许你使用资源管理器式的浏览器指定远程服务程序的名称。 1 3 . 1 . 7 服务程序的安全性 DCOMCNFG的安全选项卡中有三个单选按钮,如图 13-6所示。这些单选按钮允许你在COM+对象的系统范围 默认值和自定义安全性之间转换安全性设置。 注 意 在你尝试下一个试验之前,不要忘记删除前两 个试验中创建的Deny Launch和Deny Access权卩艮。 你 还要终止任何正在运行的你的服务程序的实例,因为 任何当前运行的实例都会保留它们的设置。在你回复 到默认权限之后,运行清单13-6中的代码。使用默认 的运行和访问权限,一切都将正确运行。 p m 「疒 JWMMlMwhpewenem . 夂...ij 1.设置访问权限 这一页的第一组控件是访问权限单选按钮和编辑控 图13-6 DCOMCNFG服务器安全第7 3 章 配置和错误处理 251 件 ,它们用于修改注册表子键HKEY_CLASSES_ROOT\AppID\{my-c丨sid}中的AccessPermission 设置。 再 说一遍,在你尝试下一个试验之前,不要忘记删除前三个试验中创建的Deny Launch和 Deny Access权 限,还要终止任何正在运行的你的服务程序的实例。 选择Use Custom Access Permissions并率击Edit按钮,弹出Registry Value Permissions对话框。改 变设置,拒绝用户账号的访问权。现在,当你运行清单13-6中的代码时,对函数CoCreatelnstaiiceO 的调用将会失败,因为你的用户账号不能运行服务程序。 2.设置运行权限 这 一 页 的 第 二 组 控 件 是 运 行 权 限 单 选 按 钮 和 编 辑 控 件 ,它 们 用 于 修 改 注 册 表 子 键 HKEY_CLASSES_ROOT\AppID\{my-clsid}中的 LaunchPermission 设置 c 选择Use Custom Launch Permissions并单击Edit按钮,弹出Registry Value Permissions对话框。 改 变 设 置 ,拒 绝 用 户 账 号 的 运 行 权 。 现 在 , 当 你 运 行 清 单 13-6中 的 代 码 时 , 对函数 CoCreatelnstanceO的调用将会失败,因为你的用户账号没有运行权限。 如果你手动启动服务程序,并运行淸单13-6中的代码,对函数CoCreate丨nstanceO的调用将会 成功。和前面一样,因为服务程序已经在运行,所以对函数CoCreatelnstanceO的调)U就不必冉 去启动服务程序了。 3 .设置配置权限 这 一 页 的 最 后 一 组 控 件 是 配 置 权 限 单 选 按 钮 和 编 辑 控 件 ,它 们 用 于 修 改 注 册 表 子 键 HKEY_CLASSES_ROOT\AppID\{my-Clsid}的安全性权限设置。这项设贯决定了谁可以修改对 象的配置。 选择Use Custom Configuration Permissions并单击Edit按 钮,弹出 Registry Value Permissions 对话框。如果你不改变COM+的默认配置权限,那么下一个要弹出的对话框将与图13-7中所示的 对话框相似。 |0POM«iUM(t S o* cmIA cc« u a SYSTEM FttfConhdl lyotofAccttc iFviCorftol 3 图13-7注册键值权限 在进行下一步之前,确定你拥有管理密码。你可以通过删除Everyone入口,来演示对自定义 配 置 权 限 进 行 改 变 所 产 生 的 影 响 。 以 非 管 理 员 身 份 登 录 你 的 计 算 机 ,打 开 REGEDIT 和 REGEDT32, 并查看COM+服务程序的HKEY_CLASSES_ROOT\AppID\{my-clsid}注册表子键,252 第二部分高级COM编程故巧 你看到的将与图13-8和13-9中所示的内容相似。 REGEDIT编辑器报错,是因为它是在不知道系 统注册表安全特性的情况下被创建的。REGEDT32 编辑器正确地处理了安全性,并且把对用户不可用 的注册表键变为灰色。 ZECSESSESl o 图13-8 REGED1T显示了App丨D子键 图13-9 REGEDT32显示了 App丨D子键 1 3 . 1 . 8 服务程序的身份 DCOMCNFG的身份选项卡中有四个单选按钮,如图13-10所示。这些单选按钮允许你改变 COM+对象所使用的用于安全确认的身份。 tOOCXZOW-COOCNKi •3 1 1 j j KOCCODO) OOCOOD 」 «OCOZOO»HaXH» J <00021290 ^include “MyProject.h“ ^include “MyObject.hu iiiiiiii/imiiiiiinininiuiiiiiuiiiiiiiiuini II CMyObject STDMETHODIMP CMyObject::get_MyProperty(long * pVal) // TODO: Add your implementation code here cha 「 sz[256l; DWORD 1 = 255 ; ::GetUserName(sz, &1); ofstream of; of.open(“C:\\USER.LOG“, ios::out 丨 ios::app}; of « sz « endl; *pVal = 10; return S OK; 现在,改变用户的身份,将用户设置为与当前用户账号不同的某个非管理员本地账号,然 后运行清单13-6中的代码。哎 呀,这个账号没有足够的权利访问这个对象。噢 ,对了 ,将它从 进程列表中终止,重新开始试验。哎 呀 ,当前的用户没有足够的权利终止这个进程。那么你怎 么终止这个进程呢?重新启动计算机?不 ,为了终止这个进程,你必须注销当前账号,并以启 动这个进程的用户的身份重新登录。 这一次,在你运行程序之前,给COM+服务程序为所有用户添加自定义访问权限(在服务程 序的安全性/设置运行权限中)。如果再次运行清单13-6中的代码,C:\USER.LOG文件中包含的 将是运行程序用户的用户名,而不是当前用户名。 1 3 . 2 使用OLE2View程序 在你安装Windows时 ,OLE2View是没有被安装的,你可以在微软的网站上找到这个工具。 这个工具所提供的功能与DCOMCNFG几乎相同,但还是有一些不同之处,正是这些不同之处使 我很喜欢用这个工具。它的用户界面是一个有左右两部分的窗口,左面窗格使用了树状浏览方 式 (参见图13-11)。当你在左面窗格内选定对象时,它们将显示在右面的窗格中。254 第二部分高级COM编程i t 巧 Ffc Ot)Kt -JrJ A1 U ♦ fdToui^OObficti ‘ ? imwn^Oif Otieds • jd ♦ JJ 图 13-11 OLE2View 2.0 你会发现,OLE2View允许你对一组与DCOMCNFG中的配罝项目非常相似的项目进行设置。 另外,OLE2View可以显示给定的COM+对象的所有注册表入口,并显示所有的已注册类型库和 接口。 13.2.1 OLE2View 的缺点 不幸的是,大多数系统中没有安装OLE2View,而且想要随时随地安装这个工具几乎是不可 能的。我曾经发现许多配置会导致OLE2View程序变得很不稳定,而且安装程序也没有安装所有 需要的模块。 1 3 . 2 . 2 使用OLE2View配置COM+对象 为了使用OLE2View配置COM+对象 ,在菜单栏中打开Object Classes, All Objects branch, OLE2View将以ProgID为序,列出所有在你的系统中注册过的对象。在你选择了一个COM**■对象 之后 ,你将在右面的窗格中看到五个属性页。这些属性页是注册表、实现、激 活、运行权限以 及访问权限。注册表属性页显示了其他四页的所有设置,因此我们不讨论这一页。 我发现某些版本的OLEView ( 包括在写这本书时出现的最新的版本)存在bug。如果你在实 现属性页中,选择了一个进程内服务程序,又选择了一个本地服务程序,并在这两种服务程序 的标签之间转换,本地服务程序将得到InProCServer32注册表子键。为了解决这个问题,我呰经 试过删除实现属性页中进程内服务程序的模块路径,但是 ,进程内服务程序的模块路径还是被 本地服务程序的模块路径取代了。你 也 许 已 经猜到 了 ,我其实不愿意使用OLEView来配置 DCOM+的实现。 1 3 . 2 . 3 指定远程进程内服务程序的代理 除了允许你指定进程内服务程序、本地服务程序和进程内处理程序的模块路径之外,你还 可以指定远程进程内服务程序的代理(参见图13-12)。^ 1 3 f 配置和错误处理 2 5 5 WESMEssBSBsmmmmmmmmmmam p.N» Qb)Kl 時 gflgl &l 幽 ttl 「 U«« Surooat* Precmt P^hlcC^omSuiooO, 阁1 3 - 1 2 指定代理 很明显,你其实不会真的有远程进程内服务程序。为了使一个COM+对象成为进程内对象, 至少需要这个对象被载入同一个进程。许多COM+对象已经被编写成进程内服务程序,为厂利 用这些原有的对象,便产生了使用代理进程的概念。 为了配置客户程序和服务程序,使它们可以使用进程内服务程序的代理,你必须在COM+对 象的注册表子键HKEY_CLASSES_ROOT\CLS丨D\{my-clsid}中手动创建一个AppID入门。 你还 必须在客户端和服务器端都创建AppID注册表子键。在客户端的注册表子键HKEY_CLASSES_ ROOT\AppID\{my-c丨sid}中,应该有一个注册表人口RemoteServerName,用于识别代理程序和 进程内服务程序将要在其中运行的服务程序。在服务器端的注册表 +键 HKEY一CLASSES_ ROOT\AppID\{my-clsid}中 ,应该有一个DllSurrogate注册表入口。后面一个入口可以使用 OLEView创 建 ,具体使用的是Use Custom Surrogate复选框和Path to Custom Surrogate编辑控件。 选择Use Surrogate Process复选框创建键值名DllSurrogate。如果不选择自定义代理,那么将使用 默 认代 理 (DllHost.exe )0 你还可以使用代理在本地运行进程内服务程序,但是,需要在一个单独的进程中运行。为 了做到这一点,你必须设置客户端注册表中的DllSurrogate键值名。你可以使用OLEVievv来做这 件事。 为了运行远程进程内服务程序(或在单独的进程中运行本地进程内眼务程序),你可以使用 CLSCTX_LOCAL_SERVER标志调用CoCreateInstanceEx()函数。因为对象没有包含Local Server32入 口,所以COM+将查看HKEY_CLASSES一ROOT\App〖D\{my-c丨sid}注册表子键,得到 DllSurrogate 或 RemoteServerName 的键值。 激活属性页使你可以指定At Storage激 活、作为交互用户运行以及在远程计算机上运行(参 见图 13-13)。 Enable “At Storage” Activation复选框与第8章中描述的注册表设置ActivateAtStorage十分相 1.激活256 第二部分高级COM编程技巧 似。在存储器激活的意思是COM+对象运行于数据文件所在的计算机上。 N # O b i K t ' H m H ^ > G g l r l A 1 m i j y M T H . 0 #« 9 K ine Control 立 J t jr f tutton OTC n Q O w M f O M OTC , 蝴 FcweMweei DTC « 2 K M O ( n 4 > a S l 1 D 1 -M 0 i a n 9 7 C 9 UC« R« 9 Mf| li»i i iiH |uwcAPw**w»tlllJ 「 M A^Stauet-AcM en 广 L < u n e h t l t ^ m m c » 9 9 l i f t ; Q—w uM thr»»UH>r P ‘ 獯 4 Cowrrwid Cortrol 先 footer Contro • 4 . HMlsr Cortn • Q^orfiM an^vOTC » S 3 find OTC • tndudt Cortrd f B i a M c r c i a r c u t 丨 嫌 鬌 E n d • L a y o u t 5 U f t 」 i LweutMeeder End —ZJ • BliirbocOT C 1 » RopDor^rouoOTC H ^ O b W t D T C 〜 d 图13-】3 OLEView激活页 2.运行权限 运行权限属性页使你可以为对象指定自定义的运行权限(参见图13-14 )。 f U ObUO ^999 Http ^lrj AI M S ) Ml 5 Al DH^n tme Control £ bJXar OTC f? ® Chebto* DTC ♦ ^ Dflt* Com^nd Conool ♦ OaU fUrgt ^ool瓤 Cortro # HMdtr Corttff .? QfomMMovorc r€ J G n d ore f IfKiidd Cortid 3: Ij u M D T C ? i#vour ^oocer tnd :f l«rour foo<#f surt J 淦 f • layout U d '♦ % l^your HMber SUrt ^ n i M m D T C i B OpborOoup OTC f a n M m ^ Q m DTC « W ! O 0 1 * O A e ” Di杨WOWSTCSUCW »__—■ 1 i 4 j .1 广 Uw»weliunchpwiemont _.....… ..._ ------ LfcsJ 4 l 1 »|♦ fp … OtjKt OTC l€ ™ - «i ⑴ 一 • 一 p l ^ » 7 | M r … _____ — ___________ 图13-】4 OLEView运行权限页 要想详细了解如何使用自定义运行权限,请参见本章前面部分关于使用DCOMCNFG的 内容。 3 .访问权限 访问权限属性页使你可以为对象指定自定义的访问权限(参见图13-15 )。 要想详细了解如何使用自定义访问权限,请参见本章前面部分关于使用DCOMCNFG的 内容。^ 1 3 t 配置和错误处理 257 m Oblmt Mm 3 Al CMgrv-tra Cortrd 上J S IHeultonDK S 费 OTC t D«t«C0mir4C0rtJ0l t OflU lUnoe Foote Contro f OfltA (Ung^ Con&i « GQ o re 泽 uJDTC ■ Page Ottict OTC U i ^ FowM«n*Q«i DTC ctsoeoi^44M iD t«r-o«oro63cei A c M o n I launch Pm W om Ao m m Pvw iw ani | <1 »1 ^ UwiH^«EN«honpem*iioni — 1 C m t <1 _______ 1 “ M «s^ 4 : 图13-15 OLEView访问权限页 1 3 . 3 错误处理 COM+函数和对象的方法总是返回一个HRESULT ( 结果的句柄)。HRESULT实际上是一个 32位的数值,代表成功、膂告或者错误代码。HRESULT的高位是严重出错码。第零位代表成功 ( SEVERITY.SUCCESS ) , 第 一位代表失败(SEVERITY_FAILURE )。下 面四 位 保 留(总是设 为 零 )。第六位到第十六位代表设备,它们分别如下所示: FACILITY—WINDOWS 8 FACILITY,STORAGE 3 FACILITY-SSPI 9 FACILITY-SETUPAPI 15 FACILITY-RPC 1 FACILITY 一 WIN32 7 FACILITY,.CONTROL 10 FACILITY.■NULL 0 FACILITY ■MSMQ 14 FACILITY._MEDIASERVER 13 FACILITY..INTERNET 12 FACILITY._ITF 4 FACILITY._DISPATCH 2 FACILITY CERT 11 低十六位代表状态码以及特定错误或警告。微软保留了为除FACILITYJTF以外的所有设备 定义状态码的权利,所以微软以外的开发人员只能为FACILITY一ITF设备定义状态码。为了使错258 第二部分高级COM编程枝巧 误码的冲突达到最小化,接口开发人员有责任为他们的接口定义所有的状态码。这意味着,从 两个不同的接口返回的FACILITY—ITF状态码可以有两个不同的含义,但是,从两个对同一接口 的调用返回的FACILITY JT F状态码只有一个共同的含义。 注 意 有时,把从中间COM+服务程序接收到的错误码直接返回给调用客户程序是很方 便的。中间COM+服务程序指的是被一个客户程序调用、它又用于调用另一个COM+服 务程序的COM+服务程序。如果返回的错误码是FACILITYJTF类型,那么我就不推荐 使用这一策略,除非你的中间接口可以明确地识别这些错误码。正确的方法是,在接收 到意外错误码时,返回E_UNEXPECTED。 COM+为使用HRESULT提供了一些有用的宏,如表13-1所示。 表 13-1 HRESULT 的宏 宏 定 义 SUCCEEDEDO 成功则返回TRUE,失敗则返间FALSE FAILEDO 勺SUCCEEDED () 相反 HRESULT_FACILITY() 返回HRESULT 的简易代码 HRESULT_SEVERITY() 返回HRESULT_CODE ( ) 的严格代码 HRESULT.CODE 返WHRESULT的状态代码 MAKE_HRESULT() 从三个组件中建立 HRESULT (facility、severity 和 status ) 1 3 . 3 . 1 错误处理策略 一共有三种策略处理HRESULT^。第一种策略是简单地进行S一OK、零值和非零值的检验。 你应该记得,在本章的前面部分调用CoCreatelnstanceO时遇到TE_ACCESSDENIED HRESULT。 为了测试错误状态,你检验了HRESULT是不是非零值。这样可以充分地掌捤错误状态,并且适 当地终止程序的运行。这种错误处理策略对于某些不需要真正的错误处理的情况是可以接受的。 首选的方法是,使用SUCCEEDEDO和FAILEDO宏来检验HRESULT。这种策略不难实现, 并且是一种对于作为产品的应用程序较好的策略。清单13-8给出了一个使用这个策略的示例。 注意,这里的FA1LED0宏用于检验错误。 清单1 3 - 8 为错误句柄使用FAILED0 hresult = ::CoCreateInstance(clsid, NULL, CLSCTX_SERVER, IIDJUnknown, (LPVOID FAR &punk); if FAILED(hresult) { // CoCreatelnstance failed return hresult; 第三种策略是,使用指定的HRESULT进行检验。第二种策略和第三种策略的不同之处在于, 使用if语句进行检验和使用case语句进行检验之间的不同。淸单13-9给出了一个使用第三种策略 的示例。茗 炤 章 配置和错误处理 259 清单1 3 - 9 为错误句柄使用特定的HRESULT hresult = ::CoCreateInstance(clsid, NULL, CLSCTXSERVER, D_IUnknown, (LPVOID FAR &punk); • switch (hresult) { case E_ACCESSDENIED : II CoCreatelnstance failed because of access denied return hresult; case S一 OK: // CoCreatelnstance succeeded break; default: II CoCreatelnstance failed for unexpected reason return E_UNEXPECTED ; 注意,上面的代码不是在使用FAILEDO宏检验错误之后就停止了。我还检验了指定的错误 值, 你可以在需要的时候添加更多的检验。 对于为客户程序返回有限的错误信息来说,HRESULT是很方便的标识符。但 是,如果你想 返回比标识符所能表示的信息更多的信息,该怎么办呢?可能你还记得在ATL对象向导中看到的 —个叫做Support丨SupportErrorlnfo的复选框,这是另一种形式的错误处理,它使COM+服务程 序可以为客户程序返回非常丰富的错误信息。 13.3.2 通过ISupportErrorlnfo传递信息 ISupportError丨nfo接 U 以及它的两个近亲ICreateErrorlnfo和IErrorlnfo接 U 允许COM+类向客 户程序传递额外的信息。实现和使用这些接口的机制是很简单的。你的COM + 类必须实现 ISupportErrorlnfo接口。你的COM+类还必须使用ICreateErrodnfo设置错误信息,并且客户程序 必须使用IErrorlnfo接口来得到错误信息。 不 会 是 又 要 写 三 个 接 口 吧 ?幸 好 ATL已 经 很 好 地包含了两个接口,我只需要告 诉你 ISupportErrorlnfo接口就可以了(参见清单 13-10)。 清单 13-10 ISupportErrorlnfo的IDL interface ISupportErrorlnfo: IUnknown { HRESULT InterfaceSupportsErrorInfo( lin] REFIID riid); } 这也不是一个很重要的任务。使用ATL对象向导创建一个新的ATL COM+类。在Developer Studio的菜单栏中,选择Insert, New ATL Object0 在第一个向导页中,选择Objects分 类 , Simple Object对 象 ,并单击Next按 钮 。在 第二 个向 导 页 中 ,键入Oops作为名称并转换到 Attributes属性页。选择Support ISupportErrorlnfo复选框,单击ok。 你的COM+类现在支持丰富的错误信息,但是 ,由于你的类什么事情也不做,所以它不能报 告任何错误信息。你将通过为IOops接口添加一个叫做Throw的方法,了解到这一错误处理支持260 第二部分高级COM编程技巧 的强大作用。淸单13-11给出了COops类的声明。 清单13-11 COops类的声明 // Oops.h : Declaration of the COops #ifndef _00PS_H_ #define _00PS~H~ ^include “resource.h“ II main symbols ///////////////////////“//////////////////////////////////////////////// II COops Class ATL_N0—VTABLE COops : public CComObjectRootEx, public CComCoClass, public ISupportErrorInfo, public IDispatchImpl { public: COops() . DECLARE_REGISTRY_RESOURCEID(IDR_OOPS) DECLARE_PROTECT_FINAL_CONSTRUCT() BEGIN_COM_MAP(COops) COM_INTERFACE_ENTRY(IOops) COM_INTERFACE~ENTRY(IDispatch) COM_INTERFACE~ENTRY(ISupportErrorInfo) END 一 COM_MAP(} II ISupportsErrorlnfo STDMETHOD(InterfaceSupportsErrorlnfo)(REFIID riid}; II IOops public: STDMETHOD(Throw)(); #endif //_00PS_H_ 在COops::Throw()方法中,你可以通过使用CComCoClasszErrorO方法返回丰富的错误信息 ( 参见清单13-12)。 清单13-12 COops的定义 // Oops.cpp : Implementation of COops •include “stdafx.h“ #include “Server.h“ #include “Oops.h“ //////////////////////////////////////////////////////////////////////// II COops第 章 配置和错误处理 26/ STDMETHODIMP COops::InterfaceSupportsErrorInfo(REFIID riid) { static const IID* arr()= { &IID_I0opS }; for (int i=0; i < sizeof(arr) / sizeof(arr[0]); i++) { if (InlineIsEqualGUID(*arr[i],riid)) return S_0K; } 一 return S_FALSE; STDMETHODIMP COops::Throw() { Error(_T(“Another error*), IIDIOops); return E_FAIL; 注意,在清单 13-12的COops的定义中,有两个方法。COops::lnterfaceSupportsErrorInfo(VA- 法有一个简单的实现过程,如果REH1D支持丰富的错误信息就返回S_OK,否则就返回S_FALSE。 现在,只有一个n o ( 接口ID),所以只为那个接口返回S一OK。如果你有另外一个接口,比方叫 IID_IOops2,那么COops::InterfaceSupportsErrorInfo()方法的实现则应该变为清单13-13中所示的。 清单1 3 - 1 3 多接口支持 STDMETHODIMP COops::InterfaceSupportsErrorInfo(REFIID riid) { static const IID* arr[I = { &IID一loops, &II0j0ops2 ); for (int i=0; i < sizeof(arr) / sizeof(arr[0l); i++) { if (InlineIsEqualGUID(*arr[i],riid)) return S OK; } return S FALSE; } 清单13-14中COops^ThrowO方法的实现过程是,直接调用CComCoClass^EirorO方法并返回 E ^ F A I L o 清单 13-14 lErrorlnfo客户 // controller.cpp : Defines the entry point for the console application II ^include 'stdafx.h“ #import “.. \server\server.tlb262 第二部分高级COM编程技巧 int main(int argc, char* argvl】} { ::CoInitialize(NULL); { SERVERLib*.:IOopsPtn p(__uuidof (SERVERLib: :Oops)); try { p->Throw(); }' catch(_com_error e) { std::cout « “Error: “ « e.Error() « “ • “ « (char *)e.Source() <<■*•_ « (char *)e.Description() « std::endl; > } ::CoUninitialize(); return 0; 在这种情况下,我使用了 VC++对导人类型库的支持。当VC++遇到#import语句的时候,它 将为你的COM+对象生成一个类,使你可以非常直观地访问你的COM+对象。 不要想当然地认为COM+会发出异常信号,其实是一个由im port语句生成的封装类发出异 常信号的。如果你在使用C O M d g U 指针,方法返回时并不会发出异常信号。这样 ,去取回错 误信息就成为程序员的任务了(参见清单13-15)。 清单13-15另一个lErrorlnfo客户 // controller2.cpp : Defines the entry point for the console application. // •include “stdafx.h“ #include *../server/server.h' ^include •丨 “ /server/server 一 i.c“ class WideToAnsiBuffer { public: WideToAnsiBuffer() : s{0) {>; -WideToAnsiBuffer() { del⑴ }; void del() { delete 1] s; }; char * s; inline char * WideToAnsi(const WCHAR * pwchar) { long 1 = ::wcslen(pwchar)+l; static WideToAnsiBuffer s; s.del(); s.s = new char[l+l]; ::WideCharToMultiByte(CP_ACPl 0, pwchar, .1 , s.s, 1 , NULL, NULL); return s.s;第7 3 章 配置和错误处理 263 HRESULT GetUnknown(WCHAR * strProgID, IUnknown ** ppunknown) { CLSIO clsid; HRESULT hresult = ::CLSIDFromProgID(strProgIO, &clsid); hresult = ::CoCreateInstance(clsid, NULL, CLSCTX^SERVER, IID一 IUnknown, (void **)ppunknown); return S一 OK; HRESULT Getlnterface(IUnknown * punknown, REFIID riid, IUnknown ** ppunknown) { HRESULT hresult = punknown->QueryInterface(riid, (void ♦食 )ppunknovrfn)j return S_0K; int main() { ::CoInitialize(NULL); IUnknown * punknown = 0; IOops * poops = 0; HRESULT hresult = GetUnknown(L“Server.Oops.1“, &punknown); hresult = Getlnterface(punknown, IID_IOops, (IUnknown **)&poops); hresult = poops->Throw(); if (FAILED(hresult)) { lErrorlnfo * perrorinfo; ::GetErrorInfo(0, &perrorinfo); BSTR bstr = 0; perrorinfo->GetDescription(&bstr); std::cout « _We had an error\nOescription: • « ^WideToAnsi(bstr); perrorinfo->GetSource(&bstr); std: :cout « “ \r»Source: “ « WideToAnsi(bstr); punknown->Release(); ::CoUninitiaiize(); return 0 ; }___________________________________________________________________________________________________________________ 如果你看看清单13-15接 近结 尾的 部 分 ,你会发现我调用了GetErrorlnfoO函数来得到 lErrorlnfo接口。这个接口有几个成员,可以使你得到错误的不同属性(参见清单13-16 )。 清单 13-16 丨Errorlnfo的IDL interface lErrorlnfo: IUnknown264 第二部分高级COM编程技巧 HRESULT Get6UID([out] GUID * pQUID); HRESULT GetSource([out] BSTR * pBstrSource); HRESULT GetDescription([out] BSTR * pBst 「 Description}; HRESULT GetHelpFile([out] BSTR * pBstrHelpFile); HRESULT GetHelpContext([out] DWORD * pdwHelpContext); > 我使用 了 lErrorlnfo: :GetDescription()和 lErrorlnfo: :GetSource()两个方法来得到最基本的信 息。lErrorlnfo部分的所有源代码都在CD-ROM上 ,可以在Chapterl3目录下的errorinfo子目录 中找到。如果你单步调试两个客户程序的代码,将可以更好地理解这三个错误处理接口的强大 作用。 1 3 . 4 小结 本章给你提供了一个理解如何配置DCOM以及如何处理错误的坚实基础。你首先学会了如 何使用DCOMCNFG工 具,使COM+正常地运行于客户机上。我们还讨论了与COM+配置有关的 注册表的一些问题,学习了在看注册表的时候应该留意些什么。给出了使用VC++的几个示例 —— 这些示例将帮助你编写专门的工具,用以自动地进行某些配置操作。 我们以修改客户端配置从而消除错误的方式,讨论了客户端的COM+错误问题。这一部分十 分重要,这是因为客户端的配置必须正确无误,否则你的所有COM+开发都将白费。第14章 COM的互联网服务 本章内容包括: • 一个新的COM+传输协议 • 隧道TCP协议概述 • 使能CIS •代理服务器的配置 •配置技巧和已知的问题 • OBJREF 标记 •必要的编程改变 为了提供对隧道传输控制协议(TCP)的支持,产生了COM互 联 网 服 务 (CIS)。TCP协议 允许COM通过TCP端口80 ( 标准的HTTP端 门 ) 进行操作。这样,客户和服务器便可以在有代理 服务器和防火墙的情况下进行通信。这就产生了一种新型的基于COM的互联网方案。 除了新的COM协议之外,CIS还提供了一种新型的简单标记—— OBJREF标记_■ •简化了在 互联网方案中COM的使用。OBJREF标记代表了一个对正在运行的对象的引用,并且拥有一个 显示名,可以被嵌人HTML页面并被绑定于ActiveX控件或者客户的applet。 本章将讨论什么是COM互联网服务,它们如何工作,以及如何配置运行微软Windows的计 算机,使它们可以使用这些服务。 14.1 一个新的COM+传输协议 在许多互联网情况下,客户和服务器之间网络的连通性会受到几方面限制的影响,例如: • 一个过滤外界网络流量的代理服务器可以控制客户与互联网的连接。这是经常发生在运行 于公司环境中的应用程序上的情况,但是,它也可以应用于用户通过ISP连接到互联网所 运行的应用程序。 • 防火墙通常用于控制输人的互联网流量,定义什么样的网络端口、数据包和协议的组合可 以被接受,来保护服务器(或者客户)的网络环境。 在实践中,这些限制结果是,客户和服务器很可能只有有限的协议和端口组合可以实现二 者的对话。因为COM+在一个范围(1024-65535 ) 内动态地选择网络端口,而在这个范围内互联 网到内联网的网络通信是不允许的,所以想要在互联网上可靠地使用我们经常使用的COM+端 口 (由大量的RPC调用组成)是不 可 能 的 (即使它们可以完美地适用于内联网)。丨ft且 , 墙 经常被设置限制对端口 135的访问,而COM+却要依赖这个端U 提供多种服务。端口 135通常被 COM+用于RPC的验证,然而,因为诸如代理服务器之类的东西通常会封闭这个端口,所以需要 通过将这个端口打开才能进行验证。如同其他所有的端口一样(除了端口80),它意味着潜在的266 第二部分高级COM编程41巧 安全性风险,所以默认的状态就是关闭。 隧道TCP协议在COM+连接的开始阶段引入了一种特殊的握手方式,使得这个连接可以穿过 大多数防火墙和代理。在这次握手(由CIS控 制 )之后,通信协议就是基于TCP连接的COM协议3 除了本章后面列出的告诫之外,这意味着: 二 • 这个协议对客户和服务器都是透明的,也就是说,在使用CIS时 ,无论是客户代码还是服 务器代码都不需要修改。 •所有基于TCP协 议 的COM + 服 务都可 用 一 包 括COM +的 安 全 和 寿 命 管 理 (也就是 “pinging” ) 服务。 隧道TCP协议的局限性 隧道TCP协议受到以下限制: •它需要在CIS可访问的COM+对象所在的服务器端计算机上安装4.0或者以上版本的I1S ( 互 联网信息服务器),因为CIS的部分功能是使用ISAPI过滤器来实现的。 • 因为在初始的握手之后,隧道TCP协议是由非HTTP通信组成的,所以CIS需要代理服务器 和防火墙允许这样的通信通过对HTTP开放的端口。 注 意 因为有了这些限制,所以实际上CIS不支持回调。这就意味着,你的应用程序不 能使用连接点或者警告接收机制来执行通知。然而,如果CIS客户可以像CIS服务器一 样运行,并且可以像本章后面讨论的一样进行配置,那么就没有什么可以阻止客户接收 COM+的调用了—— 包括回调。 1 4 . 2 隧道TCP协议概述 如果客户的配置显示,与服务器的HTTP通信必须通过代 理服务器,那么客户COM+的运行环境将建立一个与代理服务 器的TCP/IP ( 互联网协议)连接。它将把HTTP CONNECT方 法发送给代理服务器,该代理服务器正在请求与服务器主机上 端口 80的连接。图14-1给出了隧道TCP协议的数据流图。 代理服务器将建立一个与服务器主机的TCP/IP连接。这里 假设配赏代理服务器时,允许在与客户连接的端口使用HTTP CONNECT方法。这个代理服务器上的端口配K 有时被称为 “使能SSL隧 道 ”。 如果客户的配置中不使用代理服务器,那么COM+的运行 环境将建立一个与服务器主机上端口80的TCP/IP连接。在这一 步之后,不管客户是否使用代理,它都与服务器主机的端口80 存在连接(可能有代理服务器作为中介)。现在客户将向服务 器发送RPC_CONNECT命 令 ,请求与服务器主机上的DCOM 服务器连接。 图丨4 - 1隧道TCP流第 章 COM 的互联网服务 267 为了响应RPC_CONNECT命令,服务器RPC运 行 环 境 (一部分由ISAPI的过滤/扩充对实现) 建立了一个与本地COM+服务器的连接。 客户和服务器现在已经建立了一个间接的TCP/IP连接,可以进行基于TCP连接的COM+对话 了。 1 4 .2 .1 配置隧道TCP协议 表14-1中所列的操作系统都支持隧道TCP协议。操作系统或服务包(service pack) 的分布式 文档会适当地提供安装说明。 表1 4 - 1支持隧道TCP操作系统 操作系统 客户机 服务器 装有DCOM95 !.2 的Windows 95 装有DCOM98 I.3 的Windows 98 装有SP4的Windows NT 4.0工作站 装有SP4的Windows NT 4:0服务器 Windows 2000工作站 Windows 2000服务器 14.2.2 Windows 95和Windows 98中的客户程序配置 CIS需要在Windows 95中安装DCOM95 1.2版或更髙版本。DCOM95 1.2版可以在微软的 COM主页下载,www.microsoft.com/com/default.asp。必须安装DCOM,这样Windows 95才能参 与分布式应用程序的执行。在默认状态,Windows 95不具备RPC能力。 在Windows 98上 ,你必须安装DCOM98 1.3版或更高版本。在Windows 98 OSR1中带有 DCOM98 1.3,你也可以在微软的COM主页下载它。再说一遍,对于Windows 95 , 必须安装 DCOM,才能拥有RPC能力。 注意你可以 从 http://www.microsoft.com/com/resources/downloads.asp 上下栽 DCOM95 和 DCOM98。 另一个你需要安装的程序是DCOM98配置工具,虽然它的名称是这样的,但是在Windows 95和Windows 98上都可以使用这个工具。当你运行这个程序时,它将把C1SCNFG程序展开到你 计算机的Windows\Systeni目录中。 为了使能CIS客户支持,在命令行运行CISCNFG工具,同时加上以下参数(参见图14-2): CISCNFG tcp_http CISCNFG用于配置DCOM使用的协议,它还可以与以下参数一起使用: • top ( 仅仅是基于TCP的DCOM协 议 )。 • http ( 仅仅是被隧穿的TCP协 议 )。 • tcp.http ( 先尝试基于TCP的DCOM协议 ,然后是被隧穿的TCP协议)。 通常使用的是tcpjittp,因为这个参数既选择了tcp又选择了http。在运行CISCNFG之后,必268 第二部分高级COM编程4支巧 须重新启动你的系统。 I ^ 3 【池 丨 刊 ^ m a 图 14-2 ciscnfg 工具 注 意 如果你依赖DCOM98的功能,你有两个选择:使用你的应用程序重新分配更新的 系统文件(D C O M 9 8 ),或者将用户指向DCOM98的Web发布。如果你的应用程序需要 从Web上下栽,那么我们推荐将用户指向Web发布这种选择,因为DCOM98很大,而且 许多用户可能已经有这个工具了。 14.2.3 Windows NT 4.0 SP4和Windows 2000中的客户程序配置 Windows NT 4.0 SP4和Windows 2000也支持CIS。 为了使能CIS,你需要向DCOM协议列表添加隧道TCP协议。你可以通过运行DCOMCNFG 并实现以下步骤来修改协议列表: 1)选择默认协议选项卡。 2 )用Add按钮添加隧道TCP/IP协议。 3 )重新启动系统,使这一改变生效。 注 意 如果配置了多个协议,那么会按照它们出现在协议列表中的顺序逐一地尝试使用 它们。 1 4 .2 .4 客户机代理服务器的配置 假设你的客户位于一个代理服务器后面,这时,你需要确认你的客户计算机得到了正确的 配置,可以使用代理服务器访问Web。为了配置客户计算机,使它可以使用代理服务器,选择 Internet属性对话框的connection选 项 卡 (如图14-3所 示 ),或者从IE进入Internet选项对话框。这 也可以通过本章后面讨论的一个注册表键进行设置。注意,其他使用HTTP的应用程序的RPC运 行环境也会使用这项配置,最有名的就是微软的IE。 不论是什么操作系统,对于所有在代理服务器后面的客户机,都需要进行代理服务器的 配置。第7 4 章 COM 的互联网敝务 2 6 9 嫌 ;^ UM»wcorvMclicinMa«C(innMty«ui J j conpu(artDth*lctonwt T。chargt yout Mttn0» di«c% Mieot cm o( ttww opbom 疒 Comk* lo mng «]B6iMn ^ £^mocttoi»»lnl«malurino_fac4<»e □ 图14-3 HTTP代理配置 14.2.5 Windows NT Server 4.0上的服务器配置 CIS需要你的Windows NT Server 4.0安装有SP4 ( 服 务包 ),还需要运行HS 4.0 (包括互联网 服务管理器ISM ) 。 【IS 4.0是Windows NT 4.0的Option Pack的一部分。 瞀告在运行微软代理服务器的计算机上不应该安装CIS, 因为CIS与微软代理服务器的 端口配置冲突。 在你的Inetpub目录下创建一个RPC子目录。例如,在命令行输人以下内容: 鸛d c:\inetpub\rpc 在以下的说明中将用%inetpub%\rpc来代表这个目录。 从Windows系统目录中将rpcproxy.dll文件复制到%inetpub%\rpc目录中。例如,在命令行输 入以下内容: copy \windir\\systen32\rpcproxy.dll c:\inctpub\rpc 进行以下的操作,为你创建的目录建一个虚拟的根: 1 )开始菜单中,选 择 程 序 (Program), Windows NT 4.0 Option Pack, Microsoft Internet Information Server,最后单击Internet Service Manager。 2 )在控制台树中(左面的窗格),选择Console RootyiIS//Default Web Site。 3 )右键单击Default Web S it e ,在弹出菜单中单击Create New,然后单击Virtual Directoryc 在New Virtual Directory Wizard中,输人以下内容: Alias to be used to access virtual directory RPC Physical path270 第二部分高级COM编程技巧 %inetput%\rpc Permissions Execute Access 不要关闭丨nternet Service M anager,将Default Web Site的连接超时改为5分钟,按卜面的方 法做: 1)在控制台树中(左面的窗格),选择Console Root/IIS//Default Web Sitec 2 )右键单击Default Web S ite ,在弹出菜单中单击Properties。 3 )在Default Web Site的属性对话框中,选择Web Site选项卡。 4 )将连接超时改为300。 5 ) 窄击ok。不要关闭Internet Service Manager。 6) 安 装 RPC Proxy ISAPI Filter。在 控 制 台 树 中 (左 面 的 窗 格 ),选择Console Root/IIS/选择默认协议选项卡。 2 )选择或取消选择Enable COM Internet Services on this computer复选框。 3 )重新启动系统,使这一改变生效。 1 4 . 4 代理服务器的配置 如果客户被设置为通过代理服务器访问互联网,那么必须设置代理服务器,对与客户连接 的 端 口 (默认端口为8 0 )使能HTTP CONNECT方法。这个代理服务器上的端U 配置有时被称为 “使能SSL隧道”。参考代理服务器的文档,详细了解如何为HTTP CONNECT方法配置端口。 1 4 .4 .1 配置微软代理服务器 如果你正在使用微软代理服务器,使能HTTP CONNECT是通过操作一个注册表键(例如 , 使用REGEDT32.EXE)来完成的,指定明P些端口允许HTTP CONNECT。在默认配置中,有两个 端口被使能:443 (h ttp s)和563 ( snews )。相关的注册表键是: HKEY_LOCAL_MACHINE\system\CurrentControlSet\services\w3proxy\parameters\ •SSLPortListMembers 键值由端口对的列表组成。对于默认代理配置 ( 在端口80上处理HTTP协 议 ),向现有的注册表键值 添加80 80对 。在做完这个修改之后,需要停止并重新 启 动微软代理服务器,使这个新的设置生效。在图 14-4中给出了使用REGEDIT配置微软代理服务器的示 意图。 jjjyl 图 1 4 - 4 使 用 REGEDIT 配 置 微 软 代 理 服 务 器2 7 2 第二部分高级COM编程技巧 1 4 .4 .2 防火墙的配置 CIS对防火墙配置的惟一要求是允许TCP/丨P协议无阻碍地通过端口 80。因为端口 80通常对 HTTP协议开放,所以这是标准方案。然而,在少数情况下,防火墙会对流入的信息进行所谓的 应用层过滤,这可能导致CIS的信息被拒绝。 1 4 . 5 配置技巧和已知的问题 在你配置CIS的时候,有几件事情可能会出问题。我已经在TechNet文档中找到了一些可能 出现的问题。 14.5.1 CIS客户端上不正确的代理服务器设置 客户端上的代理服务器设置不应该包含正斜杠(/)。例 如 ,弓丨用myproxy:80是正确的,而引 用http://mypr0xy就不正确。注意,代理服务器的设置(使用控制面板中的网络进行设置)保存 在注册表的下面这个键的键值中: HKEY_CURR£NT_USER\Software\Microsoft\Windows\CurrentVersion\Internet Settings' •ProxyServer 警告一台运行着微软代理服务器的计算机不能安装CIS服务器端程序,因为它们的端 口设置相互冲突。 当RPC代理、RpcProxy.dU安装在一台运行着微软代理服务器的计算机上时,它便不能正常 地工作。 1 4 .5 .2 关于Multihomed CIS服务器的问题 Multihomed CIS服务器的客户使用服务器的一个IP地址来激活对象的做法,只会导致失败。 为了解决这个问题,客户应该使用服务器名(如DNS名 )。例 如,不使用209.42.234.81,而使用 www.sourceDNA.com。 14.5.3 MTS对回调的使用 当客户和服务器在微软事务服务器(MTS)方案中共享一个事务时,MTS底层将对客户进 行回调。除非客户机被配置为CIS服务器,否则CIS不支持这一方案。然而,需要注意的是,对 于基本客户来说,这个问题不会妨碍绝大多数的事务方案。 1 4 .5 .4 有关HTTP离速缓存设备的问题 HTTP高速缓存设备(如Cisco LocalDirector ) 可能需要被禁止。 智 能路 由器 (如Cisco的LocalDirector)允许一组服务器作为一个单独的虚拟服务器出现, 虚拟服务器的IP地址注册在DNS服务器中,而服务器自身的IP仍然是未公开的。当虚拟服务器接 收到请求时,路由器将把请求分配给其中一个服务器。这样,整个一组服务器对于客户来说就^ 1 4 - f COM 的互联网服务 273 变成一台服务器了。 当Cisco的LocalDirector用于与IIS结合,在Web上进行已知负荷均衡会话操作时,确保路由 器的间隔时间符合或超过会话对象的超时间隔是很重要的。会话对象的超时时间的默认设®是 20分钟。我们可以通过在Global.asa文件中添加一行参数,将超时间隔改为15分钟: <% Session.Timeout = 15 %> 1 4 .5 .5 影响CIS的注册表键 表14-2中的信息给出了影响CIS的注册表键。 表1 4 -2 影响CIS操作的注册表键 注册表键 描 述 HKEY_CURRENT.USER\ 客 户 端 设 定 的 这 个 值 指 示 了 Server>:不应该包含“ / ” 0 Software\Microsoft\ 注意这个fid晋是被其他使用HTTP的应用程序与RPC运行时环境一起共享 Windows\CurrcntlVcrsion\ 的,例如1E。代理胀务器配段在控制囱板中通过打汗丨memet图标来设置 Internet ScttingsX ProxyScrver HEKY_LOCAL_MACHINE\ 客户和服务器设S 将要使用的DCOM协议。也可以用Windows95/98下 SOFTWARE\Microsoft\Rpc\ 的CISCNFG和Windows NT 4.0下的DCOMCNFG设g DCOM Protocols HEKY_LOCAL^MACHINE\ 服务器方设置的这个值标识a s 是否已经激活。可能的值是Y 和N。这 SOPTWAKE\Microsoft\01e\ 个属性可以用DCOMCNFG来设罟 EnableDCOMHTTP 14.6 OBJREF标记 为了允许客户建立弓已经运行在远程服务器上的对象之间的连接,CIS提 供 / 一种新型的简 单标记,OBJREF标记。这个标记提供了一种简单的机制,使我们可以引用嵌入HTML页面中的 远程对象。 OBJREF标记代表了对运行在分布式系统中的本地或远程对象的引用,它代表了在个别服务 器上的个别运行实例。如果对象实例终止,那么OBJREF标记无效。 如果标记是在代理到远程对象的连接阶段创建的,并且绑定于另一台计算机上,这个标记 将绑定到初始对象,以保持DCOM的简洁语义。如果在尝试绑定的过程中无法定位对象,不会 有新的对象被运行。 在许多方面,OBJREF标记的语义都与指针标记相似,实际上它代表了指向运行对象的远程 指 针 。然 而 ,OBJREF标记的显示名可以嵌人HTML页 面 ,并且可以被客户的小应用程序 ( applet) 或ActiveX控件绑定。 了解何时使用CIS Active Server Page ( 或者可以生成动态HTML内容的其他方法)可能将OBJREF标记的显示 名放在applet或ActiveX控件的一个参数中。然后,applet或ActiveX控件便可以使用这个标记来274 第二部 分高级COM编程故巧 连接远程服务器上的运行对象实例了。 • 为了使用OBJREF标 记 ,生成标记的服务器必须使用CreateObjrefMoniker函数来创建 OBJREF标记,并向标记将要绑定的对象传递一个接口。 1 4 . 7 必要的编程改变 当你使用隧道TCP协议时,有几件事情你应该id住 ,本节就讨论这些事情。 OBJREF标记代表了对运行在进程外服务器上的对象实例的引用,可以是本地的也可以是远 程的。这个标记标识了对象实例以及对象运行所处的计算机。OBJREF标记在许多方面都很像指 针标记,除了运行的对象是进程外的这一点。客户可以对OBJREF 标 ici调用方法 IMoniker::BindToObject(),并使用得到的指针访问运行的对象,不考虑它的位置。 与指 针标记的 一 个 重 要 的区别是 ,OBJREF标记的显示 名 可 以嵌入HTML页 面 ,而 H OBJREF标记所代表的对象可以被客户脚本、applet或ActiveX控件绑定。 OBJREF标记的主要用途是获得在互联网运行的对象实例的访问权利。Active Server Page (或者可以生成动态HTML内容的其他方法)将OBJREF标记的显示名放在applet或ActivcX控件 的一个参数中。applet或ActiveX控件的代码调用CreateObjrefMonikerFfi数创违基于这个显示名 的OBJREF标记,然后对得到的标记调用IMonikeniBiiidToObjectO方法,餃后得到对运行的对象 实例的访问权。接着,Active Server Page将把指向运行对象的指针返间给页面的客户。表14-3给 出 f IMoniker函数,并作了相应的解释。 表1 4 - 3 丨Moniker函数和CIS Imonikcr 画数 描 述 IMoniker: :BindToObjcct() 对OBJREF来说,pmkToLe丨t参数必须为NULL 山于OBJRF-F 代表了当前的运行对象,没有活动发生。如粜当前对象不冉运行. BindToObject()将和 E_UNEXPECTfcD—起火败 IMoniker:: BindToStoragc() 在包含运行对象的存储屮,这个力法为需耍的接n 获彳调度指 针。由于OBJREF代表了运行对象, 凶此没有对象被激活-如果 所代表的对象不洱运行,BindToObjeuU将和fc_UNEXPECTED 一起失败 IMoniker::Reduce(> 这个方法返冋 MK_S_REDUCED_TCLSELF IMonikcr::ComposcWith() 如果pm kRight是一个反标记,则返冋的代号为NULU 如果 pmkRight是圾左瑞为反标记的一个合成式,则返回标记坫太掉最 左端反标记的合成式。如果pmkRight既不是反怀iti又不记最左端 足反标记的合成式,则该方法检杏参数fOnly丨fNotGencric,名为 FALSE.该函数将两个标记组合为一个合成式,矜为TRUE,该力 法将*ppmkCotnposite设罟为NULL,返间MK_E_NEEDGENER1C。 IMoniker::Enum() 该 •法返回5 _0反并且设置ppenumMonikcr为NULL IMoniker :IsEqual 该方法返冋S_OK,如果*pmkO丨henE;- 个OBJREF杯记,并且 两个标记的路径是一样的。否则,该方法返凹S_FALSE lMonikcr::Hash() 该方法为标记计箅hash值第 章 COM 的互联网眼务 275 ( 续 ) lmoniker 涵数 描 述 •. I Moniker:: lsRunning() 山于OBJREF代-表了运行X、t 象,W此改力•法返冋T R U t, 除北 对象由于敁近一次调丨!1失败而不丨兮运行该力法忽略pmkToLefl IMonikcr::GctTimcOfLastChange() 该力法返回E_NOTlMPL I Moniker:: Invcrsc() 该方法返问一个anti-moniker,就像调用CreateAniiMoniker(J^jtj*i 果一样 I Moniker: :CommonPrefixWith() 如果两个标i己一样,该方法返MMK_S_US.终H.将*p[>mkPrcrix 设置为NIXU 如果其他标记不足O B JK E F h U d ,该/j•法传递所也 的杯记洽MonikerComnionPrefixWith()函数,这个函数在通常的& 苒处理所和其他的你汜。 如果没朽通)11的的缀. 该力法返回 MK„E_ 该方法返间E_NOTIMPLIMonikcr::RclativcPathTo() lMoniker::GetDisplayName() 该方法为OBJREF#记获得敁尔名称这个记水名称返卜丨一个64 位封装机器位置信息、process point和接r 1指针丨D ( 1PID ) 的编码、- 出于旅荇件考虑,显示名称严格限制了以作为LRL资源的7 符 IMoniker::ParseDisplayName() 如采pmkToLeft不为NULL,该力•法返WMK_E_SYNTAX lMonikcr::lsSystemMoniker() 该方法返回S_OK,同时传递m ksys_objrefm on〖ker 1 4 . 8 小结 如果你处理依赖于COM+的分布式技术,而且它位于防火墙后面时,你可能需要认真地考 虑使用CIS。它是解决防火墙(许多防火墙会妨碍COM+的正常工作)后被关闭端n 问题的一个 方案。 安装和配置CIS不难,而且它已经是操作系统的一部分了。在你处理防火墙和关闭的端n 问 题时,cis将为你提供解决方案。第 15章 MTS 本章内容包括: •商业事务 • 什么是MTS • 使用MTS的好处 • MTS的结构 • 配置MTS • MTS对象 • 高级MTS技巧 •创建基于MTS的应用程序 COM+的一个目标就是简化分布式应用柷序的开发,因此,COM+服务以更方便用户使用为 冃的,提供了MTS也提供的底层结构和更易于管理的进程。可是 ,虽然这简化了大多数开发人 员的工作,但却加重了其他人的工作。COM+不提供精密控制(MTS提 供 ),因而为了可以进行 这种控制,幵发人员从COM+ 回到MTS的次数是很多的。 这是本书要包括这一章的原因之一。MTS应该说是在COM+之 下 ,而在需要的时候又变成 在COM+之上。 另一个学习MTS的原因是,它对理解基本技术从而获得对COM+的全面了解是很有益处的。 情况就是这样—— 学习MTS可以帮助你理解COM+。 1 5 . 1 商业事务 大多数企业级应用程序涉及两种不同类型的项冃,支付和商品或服务。顾客可能会在网站 上定购一个商品,如CD。随后,这个顾客可能会输入他的信用卡号,为CD支付费用。可以打印 出的发票或收据在浏览器中生成。最后,Web应用程序将提示事务成功。 在数据库应用程序中,事务过程是一个由程序执行的动作,这个程序至少会影响两个遵循 商业规则的共享数据。例如,在一个客户/服务器数据库应用程序中,商业规则会规定,对应每 一个从相关数据库的存货表中删除的商品,收费记录必须添加到这个数据库或另一个数据库的 账目表中。 ’ 从编程的角度看,事务过程必须遵循四项标准,也就是ACID原则。根据ACID原 则,事务 过程必须遵循: • 原子事务过程—— 必须是要么全有要么全无的操作。例 如,如果存货减少,那么收入的增 加就是必然的。应用程序不能只减少存货,而不增加收入。同理,应用程序也不能只增加 收入,而不减少存货。只有当所有的项目都改变了的时候,才箅完成修改。 • 一致性—— 在一个事务过程完成之后,按照有关完整性的原则规定,数据库中的数据必须第/5 章 MTS 277 有效。这就是说,当所有的更新都完成了的时候,即使个别改变破坏了数据库的规则,数 据也必须有效。 • 隔离—— 同时发生的多宗事务,必须如同所有事务都在逐一进行一样修改数据,不允许一 个事务使用另一个事务仍未提交的数据。例如,如果A事务将减少存货中书的数最,那么 B事务就不能使用仍未提交的书的总数,直到A事务结束为止。 • 持久—— 事务中所有提交的完成是事务过程的结尾。在程序接收到事务成功的确认之后, 如果系统崩溃,或者网络断线,当系统恢复时,之前的改变必须仍然存在。 1 5 .1 .1 协调事务过程 分布式应用程序普遍存在的问题就是,如何协调涉及在多个互有差异的服务器上的多个数 据源的事务过程。为了保证原子事务过程的实现,必须要协调各个服务器。换句话说就是,如 果一个服务器不能提交事务,那么另一个服务器将无法继续这个事 务 。 为了成功地完成一个事 务,两个服务器必须都提交这个事务。 为了实现这一点,事务系统使用了事务管理器的服务,执行一个分两个阶段提交的协议。 所谓的两阶段提交是指,在提交一个事务之前,在这个协议中询问数据源或资源管理器是否准 备好提交数据。在得到所有RM肯定的应答之后,它们被通知提交数据。 项目资源管理器其实就是一个管理持久数据(或资 源)的系统。数据库是资源的一个例子, 而微软的SQL Server就是一个资源管理器的例子。其他资源的例子包括消息队列,文件等等。 为了简化使用两阶段提交来协调事务的过程,你可以使用微软分布式事务协调器(MS DTC )。 MS DTC第一次是出现在1996年4月的SQL Server 6.5中。后 来,MS DTC从SQL Server中分离出 来 ,现在成为微软Windows NT核心服务的一部分。 MS DTC可以协调涉及不同计算机上多个资源管理器的事务过程。 1 5 .1 .2 事务过程与COM 过去,人们都围绕特定的数据库系统创建胖客户或两层解决方案。在一个客户/服务器系统 中 ,数据库在服务器上,客户通过网络连接到服务器。客户遵循一组特定的规则(商业规则), 直接使用数据库系统,运行那些包含着显示数据和操作数据代码的应用程序。这些应用程序通 常是使用专门为特定的数据库编写的一系列AP丨函数访问数据库服务器,这些API提供了开始、 提交和中止事务过程的方法,这样,客户可以决定事务过程的长短。 胖客户使用大量的内存和驱动器空间,并且有许多附属物。为了提高性能和可扩展性,胖 客户被分为几部分,分别在不同的计算机上运行。数据库操作代码和商业规则代码也从前端分 离出来,成为中间层。 在Windows环境中的开发人员转而把COM作为 创建 中 间层组件 的 标 准 使 用COM,你可以 创建专用于多任务的单独的组件。例如,应用程序可以有一个用于更新存货的组件,一个用于 计算费用的组件,等等。然而 ,在不同的COM对象之间协调事务过程是很困难的,尤其是在这 些事务涉及多台计算机上的多个数据库时,更是困难。 为 T在使用COM编写中间层组件时更加容易,微软推出了MTS。278 第二部分高级COM编程技巧 1 5 . 2 什 么 是 MTS MTS是一个为基于COM的中间层绀件准备的容器,它充当的是事务处理(TP) 监控器的角 色。TP监控器是一种管理事务的软件,它简化了 ACID规则的实现过程,并 a 使用事务管理器, 在分布式应用程序中执行两阶段提交协议。简而言之,TP监控器就是一个软件场所,事务就发 生在其中一 像一个商场。MTS使用MS DTC的服务来协调COM对象之间的事务,而这些COM 对象可能涉及不同计算机h 的不同数据库资源。 . MTS也可以充泡对象请求代理(ORB )。ORB是一种可以管理中间层组件寿命的软件。然而, 在本章中你会知道,MTS不仅仅只是TP监控器和ORB,在许多方面,MTS是对标准COM的一种 增强。 1 5 . 3 使用MTS的好处 到 0 前为止,本 章 以 事 务 处 理 (毕竟这个 软 件叫事 务 服 务 )的 形 式 定 义 fM T S 。然 而 , MTS可以做更多的事情,而不仅仅是帮助协调事务。MTS可以增强和改变组件的行为方式c 为了无缝提供它的服务,MTS使用了侦听机制。侦听使MTS可以在对一个对象的每个调用 发生之前和之后监控这个调用。 因为MTS可以监控和侦听调用,所以除了协调事务之外,它还可以执行更多的功能。例如 , MTS使用侦听功能,可以实现比DCOM提供的安全机制更为复杂的基于角色的安全机制。侦听 所有的调用使MTS可以阻止来0 无权使用对象的客户的调用。MTS的这一特性减少了对用于直 接处理DCOM安全性API的组件的需求。你可以直接使用MTS资源管理器创建分组(或 角 色 ), 并把这些分组分配给组件中不同的接口。 基于角色的安全检验只是一个例子,用来反映使用MTS资源管理器改变一些设置(而不修 改实际的组件),会对组件或应用程序的行为产生怎样的影响。这种通过外部属性控制组件行为 的配置称为说明性程序设计。 存在于MTS中的COM对 象 (可以通过说明性属性修改它的行为),被称为配置组件。因为 MTS可以管理COM对象,并且可以使用侦听机制控制这些对象的行为,所以MTS可以为你提供 许多好处,下面几部分我们就详细讨论这些好处。 1 5 .3 .1 组件的代理进程 MTS消除了对创建进程外服务程序(如微软ActiveX可执行 程 序 )的需求。位于MTS中的 COM对象始终是进程内服务程序(ActiveX D L L )的一部分。当客户要例示一个在另一台计算 机 h 的COM对象时,MTS将启动一个代理进程(Mtx.exe)来管理这个与客户通信的COM对象。 想要了解代理进程的详细内容,请参考MSDN的ActiveX SDK文档c 注 意 COM+的代理进程叫做DUhost.exe。 1 5 .3 .2 基于角色的安全性 MTS消除了对直接访问COM安全性API的需求。另外,MTS还增强了DCOM提供的安全模第/5 章 MTS 279 在DCOM的默认配靑中,你只能对在同一个A ppID下 、属于同一个应用程序的一组组件的运 行和访问安全性进行控制。然而 ,在MTS环境中,你可以创建分组(或角色),并把它们分配给 应用程序的每个组件,甚至组件内的每个接n 。 注 意 在COM+中,你还可以把角色分配给接口内的每个方法。 1 5 .3 .3 准时激活 因为MTS可以侦听对每个组件的调用,所以MTS可以在对象的方法被第一次调用时延迟对 象的激活,这就是所谓的即时激化,我们将在第20章详细讨论这个过程。 15.3.4 MTS资源管理器 MTS资源管理器是一种具有MMC咬合形式的、很友好的工具,它允许你配置运行在MTS中 的COM对象。 这个工具的用户界面很易于使用,而且比用于配置DCOM服务器的Dcomcnfg.exe更加稳定。 1 5 .3 .5 事务协调 正如前面所提到的,MTS的一个主要工作就是在组件之间协调事务。MTS使用了事务民主 系统,换句话说就是,对象不能直接开始、提交或中止事务,而需要各个对象共同决定是否继 续事务。当客户对第一个标记为“处理事务” 的组件进行调用时,客户便启动了一个事务。 15.3.6 MTS与微软互联网倍息服务器的集成 虽然没有在本章详细讨论微软互联网信息服务器(IIS),但是它与MTS紧密地集成在一起。 对于使用Web页面初始化一个事务过程来说,这是一个真正的优势,可以为企业级应用程序提供 更多的灵活性。可以通过在ASP的头中指定Transaction = required,使ASP参与到事务过程中, 具体代码如下: <% TRANSACTION=Required LANGUAGE=“VBScript“ %> 15.3.7 MTS与微软消息队列服务的集成 微软消息队列服务(MSMQ)是为分布式应用程序通过消息进行通信提供的一种方法,应 用程序可以发送和接收作为事务一部分的MSMQ消息。例 如 ,对象可以发送消息,但是 ,只有 到事务被提交之后,这个消息才会被真正地发送出去。 现在,你已经了解了使用MTS的一些好处,接下来应该了解一下它的结构了。 15.4 MTS的结构 这一部分介绍MTS对象的分层结构,以及MTS内部是如何运作的。我们首先了解的是程序 包 ,MTS的管理单位。然后讨论组件和环境—— 这些对本书后面的COM+— 章是很重要的。最 后将讨论活动和角色。280 第二部分高级COM编程技巧 15.4.1 程序包 程序包就是一组具有共同目的的对象的集合,它经常被当作应用程序的中间层。在MTS中 有两种类型的程序包—— 服务器和库。服务器程序包运行于它自己的进程中,而库程序包则被 载入客户的进程空间。 注意因为本章集中讨论的是比较陈旧的COM和MTS技术,所以去了解COM+使用的 有关事务处理技术的新术语是比较明智的做法。在COM+中,程序包被称为应用程序。 一个对象不能同时厲于两个程序包。正如前面提到的,MTS提供了代理进程Mtx.exe来管理 在DLL中找到的COM对象。当你将一个COM类添加到一个服务器程序包中时,MTS将会为这个 COM 类修 改 注 册 表 设 置 。COM 类在 注 册 表 中保存配 置 设 置的 地 方 是HKEY_CLASSES_ ROOT\CLSID0 每个Coclass都有一个GU丨D, 也叫CLSID3 当客户要创建一个类时,它将调用COM API函数 CoCreatelnstanceO 或者 CoGetClassObject() 0 函数CoCreatelnstanceO和CoGetClassObjectO使用SCM得到类的实例e 客户在API函数中使 用CLSID来指定创建什么类。SCM浏览注册表,找到这个CLSID,然后查看子键InProcServer32, 得到包含这个类的DLL 的名称和路径。接着,SCM 将载入这个DLL,并调用人U 点 DllGetClassObjecto DllGetClassObject函数为SCM返回一个指向这个类的指针。3 你创建程序包,并将你的组件 添加到这个程序包中时,MTS将改变InPmcServer32键的键值。如果组件被添加到服务器程序包 中,那么MTS将删除InProcServer32子键中的键值,并 添 加 •个LocalServer32子 键 (这个子键用 于指定进程外服务程序的路径)。在LOCalSerVer32子键中,MTS将写入以下类似代码: D: \WINNT\System32\mtx•exe /p:{4B118EAB•8BA8-11D3-81D8-000000000000} 字符串的前一半是MTS代 理 进 程 (Mtx.exe)的名称和路径,后一半是这个呵执行程序的命 令行参数—— 它为包含最初服务器名的程序包指定了GUID。换句话说就是,客户可以继续清求 相同的CLSID,应用程序将在MTS的代理进程中透明地例示这个对象。MTS还将把程序包的 GUID添加到HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Transaction Server\Packages注册 表键中。 在上面的示例中,准确的程序包入口应该是HKEY_LOCAL_MACH丨NE\SOFTWARE\ Microsoft\Transaction Server\Packages\ {4B118EAB-8BA8-11D3-81D8-000000000000}0 在这个 键中,你将找到程序包的所有设置,以及一个包含程序包中每个组件的子键的组件子键3 除了创建被漭求的COM对象的实例以外,Mtx.exe还将载入MTS执 行 程 序 (M u e x .d ll)。 MTS执 行 程 序 (或叫MTS运 行环 境)负责为你的对象创建环境封装,我们将在下一部分详细讨 论环境封装问题。 注意如果组件被添加到库程序包中,MTS将把InProcServer32的键值改为指向MTS执 行程序(Mtxex.dll)。第/5 章 MTS 281 组件和环境 程序包存放组件。MTS组件等同于COM类或coclass,而MTS对象是MTS组件在内存中的 实例。 在MTS创建对象之前,它首先为这个对象创建一个环境封装(MTS在删除对象之前删除这 个环境封装)。环境封装包含了有关对象的执行环境的信息—— 安全设置和事务信息等等。MTS 将环境封装放在占位程序(在代理/占位层中) 和对象之间。有了适当的环境封装,MTS便可以 在每个调用发生之前和之后侦听调用。实际上,可以把环境封装看作是MTS对象活动的场所。 在本章的后面,我们将讨论如何始终维持环境封装与对象之间的关系。为了充分利fflMTS, 我们还将讨论如何访问对象的环境封装,以及如何调用它的方法。来自MTS组件的每个对象实例 都有与之相关的环境封装,而每个对象最多只能有一个与之相关的环境封装c 山MTS组件创建的 来自非MTS组件的对象实例没有它们自己的环境封装,但是它们可以访问创建者的环境封装。 1 5 .4 .2 活动 活动是一组对象,它们具有相同的并发条件。在M TS2中 ,只存在一个不能通过属性进行设 置的并发模型(MTS 2的这个方面在Windows 2000中将变得更加重要,因为系统管理员可能会改 变对象的并发条件)。M T S 2支持的惟一并发模型是串行模型,也就是每次只能有一个客户对活 动进行调用。当客户创建新的MTS对象时,MTS将幵始一个新的活动。创建对象的客户被称为基 本客户,而由基本客户创建并开始活动的对象被称为这个活动的根。根对象可以使用MTS A n 函 数 (CreatelnstanceO ) 邀请其他对象加人同一个 活 动 (以后将讨论如何去做)e CreatelnstanceO是 接 niObjectContext中的一个函数。 自此以后,活动中的任何其他对象也可以使用这个API函数来 邀请其他对象加入这个活动。活动可以涉及来自一个或多个程序包的组件,甚至是其他计算机上 程序包的组件。只有在相同活动中的对象才能够参与相同的事务。 1 5 .4 .3 角色 研究MTS的结构不能不讨论角色。正如早些时候提到的,角色是被定义的用户组,用于规 定对程序包、组件或接n 的用户访问权。例如 ,如杲你想让会计组的所有用户都有权访问一个 组件 ,而不希望工程组的用户访问这个组件,你需要将所有会计组用户放入给定组件的MTS角 色中。 15.5 配置MTS 对于MTS来 说,通常有两种环境:生产和开发。当你在生产环境中配置服务器时,可能需 要限制可以修改组件a 性的用户人数。MTS提 供 f — 个你可以配置的系统程序包,用以限制对 服务器中不同特性的访问。这个程序包有两个预定义的角色:系统管理员和读者。然而 ,这两 个角色中没有分配任何用户。在生产环境中,你将需要给这两个角色分配用户,并 a 要为这个 系统程序包开启安全检验。 为了确保开启安全检验,进行以下步骤: 1)运行MTS资源管理器。282 弟 二 部 分 高级COM编程技巧 2 )在左边的帘格中,打开My Computer卜'的Packages Installed文件夹。 3 )右键单击System package,然后在禅出菜单中选择Properties。 4 )单击Security选项卡, 选择Enable Authorization Checking复选框。 在默认设置下,MTS资源管理器只能浏览当前计算机上的属性。然而 ,你可以使用一台计 箅机在网络上管理另一台计算机的MTS设贯。 为了在MTS资源管理器中浏览另一台计算机上的设K, 进行以下步骤: 1)右键单击Computers文件夹,在弹出菜单中选择New, Computer。 2 ) 在Add Conipiitei•对话框中,键人你想要管理的计算机的名称,单击0 K C 你必须拥有对这 台计算机的访问权,而且你也必须是可以浏览设S 的读者中的一员,或是可以修改属性的系统 管理员。 注 意 你不能在一台运行Windows NT的计算机上远程管理一台运行Windows 95的计算机 编程管理MTS服务器 MTS提 供了 一系 列可脚本化的对象,使你可以编程管理你的服务器。这呰对象都基于 IDispatch接 U ,虽然用VC++编写程序来使用这些对象是绝对可以的,但是,大多数人更愿意使 用IDispatch接口来编写客户程序。MTS提供的用于管理的对象有: • Catalog Root对象一 允许你连接到一个本地或远程的目录并得到一个集合。 • CatalogObject--- 允许你得到和设置对象的属性。 • CatalogCollection---列举、增加 、删除和修改目录对象。 • PackageUtil—— 允许你安装和导出一个程序包。 • ComponentUti!— 在一个特定集合中安装组件,并导入注册为进程内服务程序的组件。 • RemoteComponentUtil---允许你将远程组件从远程计算机上的程序包中取出。 • RoleAssociationUtil—— 将角色与组件或接U 关联。 清单15-1在Packages Installed文件夹中创建了一个名为mcsdpackage的新程序包e 这个示例 是使用V B S c rip t编写的。这段代码首先创建了一个目录对象的实例, 然后使用方法 . GetCollectionO获得程序包集合。接着,程序调用了集合的Add()方法。 为了使用Windows Scripting Host ( WSH ) 运行这段代码,将清单15•丨中的代码输入到一个 Notepad文件屮,并 ft使用.vbs扩展名保存这个文件。双击这个文件运行。 清单15-1重获并使用包 • First, create the catalog object Dim catalog Set catalog = CreateObj e c t( 'MTSAdmin.Catalog.1u) _ Next, get the packages collection Dim packages Set packages = catalog.GetCollection(“Packages“) packages.Populate • Add a new package Dim newPackage第/5 章 MTS 283 Set newPackage = packages.Add newPackage.Value(“Name“) = “mcsdpackage' ■ Commit new package packages.SaveChanges ' Refresh packages packages.Populate 15.6 MTS对象 很难为编写中间层组件给出通用的指导方针,因为不同情况下的组件+ 同 。然而,还是有 一些技巧可以用于大多数项冃。 首先,在为数据库应用程序编写组件时,你不应该让中间层组件去做数据库的工作,如查 找一条记录和合并两个表。 其次,你不应该编写在MTS内和MTS外都可以工作的通用组件e MTS为每一个对象都创建 环境封装,MTS对象可以得到对这个环境封装的访问权,并且得到不同的属性。例如 ,如果你 的对象依赖于一个涉及某个事务的MTS,那么在你修改数据库之前,首先应该确认系统管理员 没有改变对象的配K 。相似的,如果你的对象必须有严密的安全性,你应该知道系统管理员可 以关掉你的程序包的安全性,所以你必须检查上下文环境,以确保安全检验确实是开妇的。 MTS对象还遵循着与标准COM对象不同的生存期路径。 1 5 .6 .1 为MTS开发对象 MTS组件就是具有以下必要条件的COM类 : • MTS组件必须存在于进程内服务程序中。 •必须实现 IClassFactory。 • 必须使用类型库调度或者标准的充分解释调度。 • 所有的组件接口和coclass都必须包括在类型库中。 1.进程内服务程序位置 MTS要求对象存在于进程内服务程序中(DLL)。MTS对象可以存在于库程序包内,也可以 存在于服务器程序包内。当客户请求MTS在库程序包中创建一个对象时,这个对象将被载入客 户的地址空间。当客户漭求MTS在服务器程序包中创建一个对象时,MTS将把这个对象载入 MTS提供的代理进程Mu.exe的地址空间中。如果你曾经创建过进程外服务程序,那么你应该知 道 ,运行进程外服务程序所必需的代码与你的组件中的商业代码相比,通常是微不足道的。 MTS可以替你处理运行组件所必需的底层细节。 2. IClassFactory的实现 每个COM的DLL都有一个标准人口点,DllGetClassObject,COM使用这个入口获得COM类 的实例。这个类必须实现IClassFactory。MTS使用IClassFactory::CreateInstance方法创建对象。 3 .类型库调度或者标准的充分解释调度 对象可以使用两种调度机制之一:类型库调度或者标准调度。MTS不允许使用自定义调度。 如果需要代理-占位DLL,那么必须使用/Oicf编译参数生成DLL源。这将生成一个“被充分解释”284 第二部分高级COM编程技巧 的代理-占位DLL。而且,如果你要使用代理-占位DLL,那么必须使用MIDL 3.00.44或以卜.版本 生成DLL源 ,并且你必须把这个DLL与MTS提供的Mtxih.lib库连接。 Mtxih.lib库必须是第一个连接到你的代理-占位DLL中的文件。 4 .类型库中的组件接口和coclass类 MTS读取一个服务程序的类型库,然后在MTS资源管理器中显示类、接 U 和方法的名称。 为了使用ATL创建与MTS兼容的COM服务程序,需要完成以下步骤•. 1 )打开VC++ 6.0,在File菜单中选择New。 2 )在New对话框的Projects选项卡中,选择ATL COM AppWizard,为项目键入一个名称,单 击OK0 3) 在ATL COM AppWizard中 ,Server Type选抒Dynamic Link Libraries ( DLL ),并选择 Support MTS复选框。 4>单击F inish ,然后单击OK。 这个过程生成的项目除了 “传统的库” 以外,还给链接步骤添加了两个新库,它们分别是 M tx.lib ( 是 MTS运 行环 境M txex.dll的 导 人 库 ,包 含各种MTS的API函数,如SafeRef和 Createlnstance ) 和Mtxguid.lib ( 包含MTS中各种类和接口的所有GUID )。另外,这个项目还被 连接到Delayimp.lib库 ,这个库允许服务程序延迟载入DLL,在现在这个例中,就是延迟载入 Mtxex.dll。这个项S 文件存在的一个小问题是,它把自注册步骤当作定制的步骤添加了进来, 这是不太好的,因为注册服务程序将把注册表设置变回到原始状态(在把DLL添加到程序包中 之 前 )。 为了创建可以访问它们自己的环境封装的COM对象,记住:使用ATL Object Wizard, 你可以 创建与MTS兼容、并能够充分利用MTS运行特性的COM对象。如何做呢?在ATL Object Wizard里 , 从Objects组中选择MS Transaction Server Component,就可以了。然而,如果你想修改已有的组件, 使其可以在MTS中运行,你可以通过完成以下步骤复制ATL Object Wizard的特性: 1)以一个与前面代码中给出的ATL简单对象等同的对象开始。 2 )添加一行代码:include 。 3 ) 为你的类在头文件中的类声明内添加DECLARE_NOT_AGGREGATABLE宏。MTS类是 不能被集合的。 . 包括了mtx.h头文件以后,你便获得了对几个MTS AP丨函数和儿个MTS 接口定义的访问权。 这些AP丨函数之一就是GetObjectContext()。 有了这个API函数,你便可以访问与你的对象实例相关的环境封装对象了。这个API函数的 用法如下: IObjectContext *pObjectContext = NULL; HRESULT hr = GetObjectContext(&pObjectContext); if (SUCCEEDED(hr)) II do something with the context wrapper here II release the context wrapper when done pObj ectContext->Release(); }第/5 章 MTS 285 5. 侦 听 只有进行侦听,才能实现MTS的强大功能。正如你已经知道的,侦听是通过环境封装发生 的。客户与环境封装进行交流,并把环境封装当成是对象。重要的是,一个客户(甚至另一个 对 象 )永远不可能回避环境封装和侦听,直接引用你的对象。因此,决不要把对你的对象的引 用发送给其他的对象。例如,设想这样一个例子,你的对象正在实现一个回调接n : ISomeObject *pObject; HRESULT hr = CreateSomeObject(&pObject); pObj ect->RegisterCallback(this); 七面代码的最后一行发送了一个对对象的直接引用。这样做是不正确的,你必须使用函数 SafeRef(),它引用的是环境封装。这 时 ,你才可以把这个引用发送给客户或其他对象。下面给 出了与对象共享回调函数的正确方法: ISomeObject *pObject = NULL; HRESULT hr = CreateSomeObject(&pObject); IMylnterface *pIMe = NULL; pIMe = SafeRef(IID IMylnterface, (IUnknown*)this); pObject•>Registe「Callback(pIMe); MTS 2使用了一个叫做准时(JIT) 激活的特性。在JIT中 ,当客户要创建对象时,MTS将创 建这个对象以及一个环境封装。MTS将封装插人到占位程序和对象之间,但暂时还不会把环境 封装绑定到对象上,直到对象被激活为止。激活发生在客户进行的第一次方法调用时。同理, 在对象被删除之前,MTS要进行取消激活。在取消激活中,MTS将删除环境封装。接着,对象 被删除。 JIT意味着,MTS 2中的所有对象在它们的生命周期中都有四个阶段:创建 、激 活、取消激 活和删除。因此,在MTS 2中 ,创建和删除过程与不使用MTS时相同,换句话说就是,在另一 个开发人员创建你的对象时,你的对象被创建,在所有的引用都被释放时,你的对象被删除。 然而,对象直到被激活后才可以访问环境封装。 同理 ,对象在它的析构函数中也不能访问环境封装,因为环境封装那时已经被断开连接 ( 实际上,除了在取消激活阶段环境封装仍然可用这一点之外,MTS 2使得取消激活和删除两个 过程没有什么不同之处)。COM+引入了对象池的概念,在对象池中,对象可以控制释放所有的 引用是否可以真正地删除对象,或者是否把对象保存在对象池中。 为了实现用于接收JIT通知的IObjectControl接口,需要进行以下步骤: 1 )向你的MTS兼容类的继承列表中添加IObjectControl接口 0 2 )将 这 个 接 【1添加到你的查询接口映射中: COM_INTERFACE^ENTRY(IObjectControl) 3 )实现IObjectControl接口-中的三个方法---Activate、CanBePooled和Deactivate : HRESULT Ctestobj1::Activate() BOOL Ctestobj1::CanBePooled()286 第二部分高级COM编程4支巧 return FALSE; } void Ctestobjl: :Deactivate() { 在MTS 2中 ,对象池是不可用的,因此,上面的代码应该返冋FALSE。 注 意 实 际 上 ,你也可以返回TRUE。有些人建议返回TRUE,这样,一旦对象池可用, 就可以马上投入使用。然而,除非仔细地考虑过所有与对象池有牵连的问題,否则不要 这样做。 , 6.创建容纳组件的程序包 在已经创建了中间层组件之后,将需要创建程庁•拉未容纳你的组件。 为了在MTS中创建程序包,需要进行以下步骤: 1)在MTS资源管理器中,打开Computers分支。 2 )打开你想要安装程序包的那个计算机分支。 3) 右键宇 iljPackages Installed分支,在弹出菜单中选择New,Package。Package Wizard对i3 框出现。 4 )单击Create An Empty Package按 钮 (我们将在本章后面部分讨论如何使用丨nsta丨丨Pre-Built Packages按 钮 )c 5 )为新的程序包输入一个名称(可以包含空格),单击Next。 6 ) 设置程序包的身份。以后你可以在程序包中改变这个身份。在开发过程屮,你需要使用 Interactive User身份。这项设置使用登朵用户的证书。使用Interactive User身份的副作用是,所 有的输出都出现在当前的WinStation中。这项设置存在的一个问题是,必须有人登录,程序包才 能执行。 7 ) 在生产环境中,你应该选择This User身份。当你选择了This User身份时,便可以输入一 个特定用户的证书,它可以在服务程序需要访问共享资源(如文件)时被实现。注意,决不能 使用这个设置显示输出,输出将不会出现在当前的WinStation中。例 如,如果你试图显示一个消 息框,那么消息不会出现,同时你的程序会因为等待用户的操作而傾在那里。 8 )单击Finish。 1 5 .6 .2 向一个程序包中添加对象 虽然你可以使用不同的机制向程序包中添加对象,但是更简单的方法是,使用拖放操作将 包含各个类的DLL移到你的程序包下的Components文件夹中。 为了使用拖放操作向程序包中添加对象,需要进行以下步骤: 1)选择Components文件夹。 2 ) 在Windows资源管理器中,把DLL文件拖入MTS资源管理器窗U 的石边窗格。MTS将会 添加类型库中描述的所有对象。 你也可以不使用拖放操作向程序包中添加对象。 在不使用拖放操作的情况下,为了向程序包中添加类,需要进行以下步骤:第75 章 MTS 287 1 )打开你想描入组件的程序包分支,右键单击Components文件火,在弹出菜单中选择New, Componentc 2 ) 如果你想添加某个DLL中的所有组件,那么单击Install New Component(s)c 这个选项与 使用拖放操作相似,不需要预先注册DLL。 3 ) 如果你想从已注册组件的列表中通过ProgID选择一个组件,那么选抒-Import Component(s) That Are Already Registered。这个选项是向程序包添加DLL中个別组件的惟一方 法。如果你想向一个程序包添加一些组件,向另一个程序包也添加一些组件,那么可以使用这 种方法。 在创建程序包时,一定要添加你的对象所依赖的类型库,同时如果你需要代理-占位DLL, 也可以添加一个。当你在另一台计算机上配置你的程序包时,这些将派上用场。 1 5 .6 .3 程序包的属性 程序 包有 许多 你 需要知道的属性。你 可 以右键单击一 个 程 序 包 ,并在弹出菜单中选择 Properties,打开Package Properties对话框c 在General选项卡中,你可以为程序包输入一个描述符,这在向另一台计算机导入程序包时 是很有用的。Security选项卡中包括一个有关程序包安全性的复选框,注 意 ,其默认值为No Security。这个选项卡中还包括了客户与程序包之间发生的调用的验证级別。 在Advanced选项卡中提供_T一个叫做Server Process Shutdown的设置,用 J••设置何时关闭服 务程序进程。这是MTS中的一个优化,你可以在对对象的所有引用都被释放之后,使用它关闭 服务程序进程。在服务程序删除了程序包中所有的对象实例之后,服务程序将仍然运行一段时 间 ,而这段时间的长短就是在Server Process Shutdown设置中指定的。这样做可以提卨与服务程 序进行对话的客户程序的性能。 注意在开发阶段,你应该把Server Process Shutdown设置设得尽可能低,因为在服务 程序正在运行以及DLL被栽入的时候,你将不能重新修改你的程序。 在Advanced选项卡中还有两项权限设置:Disable Deletion和Disable Changes。通过选择这 些设置,你可以防止其他用户使用MTS资源管理器修改你的程序包。也就是说,如果你选择广 Disable Changes选项,那么其他用户将不能修改这个程序包。厲性对话框中,除了权限设置以 外的所有选项将变得不可用。如果要对程序包进行修改,必须取消对权限设置的选择。 Identity选项卡允许你选择Security Identifier ( SID ),MTS将在【方问共享资源(如网络上的 文 件 )时使用这个SID。它正是在进行外部调用时,你的服务程序要模拟的用户ID和口令。例如, 你的程序可能会访问另一台计算机上的文件。 你可能需要使用特定的用户ID和 U令 ,或者使用当前登录的用户的1D和口令访问那台计算 机 。然而,你的服务器上可能根本没有任何人登录;或者,登录的用户可能没有访问共亨资源 的权利,这些都将导致你的服务程序不能正常工作。 因此,在生产环境中,你应该使用一个拥有足够权利访问共享资源的特殊用户。你为程序 包设 置的属性中最重要的要数激活类型,它在Activation选项卡中。可选的类型有Library Package或Server Package。库程序包用于进程内服务程序,而服务器程序包则将被载人MTS代288 第二部分高级COM编程技巧 理进程Mtx.exe。库程序包与服务器程序包相比有一些局限性,其中最显茗的就是它不能控制自 己的安全性,而决定它安全性的将是载入它的进程。 1 5 .6 .4 对象属性 对象也有一系列的说明性属性,其中最重要的要数事务支持属性。 MTS负责初始化一个事务,并调用MS DTC的服务来使用资源管理器和控制事务流。由组件 决定MTS何时通过说明性设置启动一个事务。系统管理员可以通过改变组件的事务属性来改变 这个组件的事务条件。 为了做到这一点,右键单击你要修改的组件,并在弹出菜单中选择Properties。接着,单击 Transactions选 项 R。在这个选项卡中,你可以选择四项设置:Requires a Transaction、Requires a New Transaction、Supports Transactions和Does Not Support Transactions。 当一 个 被 标 〖己为Requires a Transaction或Requires a New Transaction的对象被创建并被激活 ( 在第一个方法调用之后)时,MTS将启动一个事务。 使MTS启动事务的对象被称为事务的根。 如果某个对象是从一个事务中已经存在的对象中创建的,那么它也可以参与到同一个事务 中。对于参与同一个事务的对象来说,它必须是同一个活动的一部分。换句话说就是,必须使 用IContextObjectO接 U 的CreatelnstanceO函数创建对象。而且组件还必须标记为Requires a Transaction或者Supports Transactions。 如果组件被标记为Requires a New Transaction,那么MTS将为对象创建一个新的事务,这个 对象将不能参与到创建它的对象的事务中。被标记为Does Not Support Transactions的组件不能 参与同一个事务。 正如你看到的,改变组件中的事务属性设置可以对应用程序的行为产生巨大的影响。想象 一下,如果你已经编写了一个对象B, 希望它可以参与对象A所参与的事务,而MTS系统管理员 却决定将对象B标i己为Requires a New Transaction,这时会发生什么?你根本无法防止系统管理 员这样做。然而 ,你可以在类型库中为你的每个组件设置默认事务® 性 。为了做到这一点,首 先要在你的IDL文件的开始部分包括Mtxattr.h头文件: #include 这个文件声明了某些你可以作为coclass属性使用的属性。这些属性是TRANSACTION .REQUIRED, TRANSACTION_SUPPORTED, TRANSACTION_NOT_SUPPORTED以及 TRANSACTION_REQUIRES_NEW。为了使用这些属性,可以直接把它们添加到coclass属性中: [ UUid(01C72192-8CB0-11D3-81E1-0050BAA1DBA9), helpstring(“ctxobjl Class“), TRANSACTION_REQUIREO 1 coclass ctxobjl { (default) interface Ictxobjl;第75 幸 MTS 289 这些属性也不能防止系统管理员修改对象的属性设置,它们只为MTS提供默认的组件事务屁 性设》。也就是说,当这个组件被添加到一个程序包中时,事务属性将根据默认属性进行设置。 1 5 .6 .5 配置基于MTS的对象 MTS提供了一种机制,使你可以导出一个程序包的定义以及它的组件,并把这个程序包导 入到另一个服务程序中。MTS还提供了一种生成安装程序的方法,这个安装程序将在没有安装 MTS的客户计算机上,安装激活服务程序组件所必需的注册表信息。 注 意 客户计算机既不需要安装实现服务程序对象的DLL ( 用来创建对象实例),也不 需要安装MTS。客户惟一需要的是在注册表中安装类丨D 的GUID (CLSID ) 和接口的 GUID ( IID )0 客户还需要服务程序的类型库,这样,类型库的调度者便可以在服务程 序使用类型库调度时,在两台计算机之间调度接 口 ;或者,在服务程序需要标准调度时, 调度代理/ 占位DLL。从导出程序包生成的安装程序考虑到了安装类型库或代理/占位 DLL的问题,并且在注册表中添加了必要的键。,这个安装程序在Program Files目录下创 建了一个名为Remote Applications的子目录,并把类里庫或代理/ 占位DLL安装在那里。 然而,当服务程序将类型库作为资源嵌入DLL时,一定要把类型库文件(TLB) 作为单 独的DLL添加到程序包中。否則,MTS将把服务程序文件添加到客户端安装程序中, 而不是只添加类型库文件。 1 5 .6 .6 导出程序包 当你已经准备好将你的应用程序转移到产品服务器中时,首先需要导出你的程序包。 导出程序包的步骤如下: 1 )在MTS资源管理器中,右键单击你想导出的程序包,在弹出菜单中选择Export,这时将 出现Export Package对话框。 2 ) 为程序包输入确切的路径和名称。如果你想导出每个角色的用户ID, 可以选择Save Windows NT User IDs Associated With Roles复选框,这个复选框只涉及角色中的用户。如果你 不选这个复选框,角色信息仍然会被导出,但是角色内的实际用户ID将不包括在内。这通常是 在从开发阶段转向配置阶段的过程中你想要做的事情。单击Export。 3 ) 如果这不是你第一次在同一个地方导出程序包,Overwrite Files对话框将弹出,让你确认 覆盖旧的程序包和DLL。单击OK。 4) Export Package对话框关闭。当导出完成时,MTS资源管理器将重新出现,并弹出一个消 息框,告诉你程序包已经被成功地导出。单击OK。 你现在已经有了一个在.pak文件中的导出程序包。.pak文件是一个文本文件,包含了在另一 个服务程序中重建程序包所必需的设置。 1 5 .6 .7 导入程序包 在导出程序包之后,你便可以将它导入到另一个MTS服务程序中。290 第二部分高级COM编程技巧 导入程序包的步骤如下: 1)在MTS资源管理器中,打开Computers分支,然后打开My Computer。 2 ) 右键单击Packages Installed文件夹,在弹出菜单中选择New, Package。出现Package Wizard对话框。 3 ) 不是单击Create An Empty Package按 钮,而是单击丨nstall Pre-Built Packages按钮。出现 Select Package Files对话框。 4 )单击Add按钮,找到你要安装的程序包,然后单击Next。 5 )在Set Package Identity对话框中,指定程序包将在哪个账号下运行。i己住,当你在进行开 发时,选择Interactive User更加方便,但是,在生产环境屮,你应该选择This User并指定用户的 证书。单击Next。 6 ) 在Installation Options对话框中,键入一个目录,组件文件将被复制到这里。这个目录就 是组件将被注册的位置。 7 )单击Finish。 许多时候,你可能想要为使用分布式应用程序的客户提供一个用户界面,但又不想安装服 务程序组件。例如,为每台客户计算机都安装所有的组件是没有意义的,客户应用程序所需要 的是程序包中类的注册表信息。当你导出程序包时,MTS将创建一个客户子目录,其中包含一 个应用程序,用于向客户计算机中写入注册表信息。 1 5 . 7 高级MTS技巧 因为MTS的安全性是基于DCOM的 ,所以它的许多要素都基于DCOM的范例。然而,MTS 通过做两件事扩展了DCOM的安全性。第一,因为MTS提供了一个容器程序(Mtx.exe),因此 它可以控制进程外激活的安全性,并且使得程序员不必为直接使用COM安全性API编程而担心。 第二,因为MTS提供了更多的安全检查点,所以它具有更髙* 级的安全性。DCOM只提供两个 安全检查点:运行服务程序和访问服务程序。除了这两个检査点之外,MTS还 提 供 f 组件级和 接口级的检查点。而且,MTS还提供了几个简单的方法,把安全检查扩展到方法级。 1 5 .7 .1 为程序包和组件提供安全性 一个角色就是一组用户,它是基于每个程序包分配的。当你创建了一个新的程序包时,默 认情况下的安全性是不可用的。你可以通过以下步骤为程序包或组件提供安全性。 为程序包提供安全性步骤如下: 1 )在MTS资源管理器中,右键单击这个程序包,并在弹出菜单中选择Properties, 出现 Properties 对话框。 2 )在Security选项卡中,选择Enable Authorization Checking复选框,单击OK。 为组件提供安全性步骤如下: 1 ) 在MTS资 源管 理 器 中 ,右键单击这 个组件 ,并在弹出菜单中选择Propenies, 出现 Properties 对话框。 2 )在Security选项卡中,选择Enable Authorization Checking复选框,单击OK。弟 75 章 MTS 291 注 意 在 默 认 情 况 下 ,组件级的安全检查是可用的。换句话说就是,一旦你为程序包提 供了安全检验,必须马上创建角色并把角色分配给每个组件。否则,任何用户都将在访 问这个程序包时遭到拒绝 1 5 .7 .2 为程序包创建角色 在DCOM中 ,安全性是受每个进程的编程影响的。在MTS中也是这样,每个程序包都运行 在一个单独的迸程中。如果你看一下任务管理器,会发现客户每次例示不同程序包中的组件时, MTS都会运行一个单独的Mtx.exe ( 代理进程)实例。因为每个程序包都运行在不同的进程中, 所以角色的创建是基于每个程序包的。这就说明了为什么库程序包不能使用MTS基于角色的安 全检验。库程序包载入的是调用者的进程空间,并互随后就被绑定到这个进程的安全模沏。因 此 ,这个库程序包可以是安全的,也可以是不安全的。当Mtx.exe进程载入一个库程序包时(也 就是 ,当程序包由MTS COM对象创建时),这个程序包就是安全的。而如果是一个非MTS进程 创建库程序包,.那么这个程序包就是不安全的。 为程序包创建角色的步骤如下: 1)在MTS资源管理器中,选择并打开你要为之设立角色的程序包。 2 )右键单击Roles文件夹,并在弹出菜单中选择New, Role。 3 )在New Role对话框中,输人角色名称并单击OK。 4 )打开New Role分支,右键单击Users文件夹,并在弹出菜单中选择New,User。 5) 弹出 Add Users And Groups To Role对话框。 选择你要添加到角色中的用户并单击OK。你可以添加个别用户或者Windows NT安全组。 1 5 .7 .3 给组件或接口分配角色 在你创建了一个角色之后,便可以给程序包中指定的组件和接口分配角色。 给组件或接口分配角色的步骤如下: 1)在MTS资源管理器中,选择并打开你要为之分配角色的对象。 2 )右键单击Role Membership文件夹,并在弹出菜单中选择New,Role0 3 )出现一个Select Role对话框,其中包含一个所有程序包角色的列表。选杼你需要用的角色, 然后单击0 K 。 1 5 .7 .4 通过编程彩响安全性 MTS还允许你通过编程来控制方法级的安全性。为了做到这一点,你可以使用 IObjectContext接口 中的两个方法•• IsSecurityEnabled()和IsCallerlnRoleO。IsSecurityEnabledO方 法可以帮助你确定程序包的安全检验是否被激活,如果安全检验没有被激活,这个函数返回 FALSE。这个函数很重要,原因有二。第一,如果你使用的是库程序包,那么当程序包是从位 于Mtx.exe进程中的MTS对象内创建的时,该函数返回TRUE;而当程序包由一个MTS外部的客 户直接创建时,返回FALSE。第二,当一个程序包的安全检验被关闭时,IsCaUerlnRoleO函数总 是返回TRUE,因此,在使用IsCaller[nRole()函数之前,必须查看安全检验是否可用。清单15-2292 第二部分高级COM编程技巧 给出了测W:方法 级 安 全 性 的 代 码 。 清单15-2测试方法级安全性 HRESULT MyObj::SecuredMethod() { IObjectContext *pContext=NULL; HRESULT hr = GetObj ectContext(&pContext); if (SUCCEEDED(hr)) { if (!pContext>IsInTransaction()) return E_ACCESSDENIED; BSTR bstrAdmin = SysAllocString(LBAdmins“); BOOL bInRole=FALSE; if (1pContext ^IsCallerlnRole(bstrAdmin,&bInRole)) { SysFreeString(bstrAdmin); return E_ACCESSDENIED; } pContext->Release(); > II Add your security code here return S_0K; 1 5 .7 .5 直接调用者与原始调用者的对比 当一个MTS外部的客户要与MTS内的对象通信的时候,MTS将进行基于角色的安全检验。 而当MTS内的对象调用另一个MTS对象的时候,MTS不进行安全检验。设想一个例子,一个客 户可以访问对象A中的方法1,但不 能 访 问 对 象 B中的方法1 ; 如果这个客户调用对象A中的方法1, 而对象A又可以访问对象B中的方法1,那么这个客户就可能访问到对象B中的方法1。因为MTS 不检验MTS对象对另一个MTS对象的调用的安全性,所以,无法防止客户进行这种类型的操作。 这就暴露了一个安全隐患。 然而,MTS提供了几个API函数,可以帮助你确定客户的身份。MTS可以辨别原始调用者 ( 在一系列COM 对 象 中访 问第 一 个 对 象 的 客 户 )和直接调用者。你可以通过使用ISecudty Property接口中的方法,获得原始调用者和直接调用者的安全标识符。这个接口包含五个方法: GetDirectCreatorSID、GetOriginalCreatorSID、GetDirectCallerSID、GetOriginalCallerSID以及 Releases丨D 。要使用这个接口,只需对相关对象调用Querylnterface。 清单15-3演不了如何使用ISecurityProperty接口。 清单 15-3 丨SecurityProperty接口 IObjectContext* pContext = NULL; ISecurityProperty* pISecProp = NULL; PSID pSid = NULL; HRESULT hr = S_OK; II Get the object context hr = GetObjectContext(&pContext); if (SUCCEEDED(hr))第 15 章 MTS 293 { II Get a reference to the ISecurityProperty interface hr = pContext->Querylnterface( IID_ISecurityProperty, (void**)&pISecProp); II Obtain the creator's security ID hr = pISecProp->GetDirectCreatorSID(&pSid); II Add your security code here // Release the security ID pISecProp->ReleaseSID(pSid); II Release the interface pISecProp->Release(); // Release the context pContext->Release(); 1 5 .7 .6 负载均衡 本部分将讨论负荷的均衡问题,以及如何通过编程来实现负荷均衡。第20章将用一个应用 程序示例详细说明这个问题。负荷均衡的苺本目的是提高服务器的性能。虽然我们可以用多种 方式来衡量性能,但是,对于本部分所讨论的内容来说,性能意味着两件事:更多的用户和更 快的响应。 有两种负荷均衡类型:动态的和静态的。当操作系统可以基于性能统计信息,选择最适合 运行客户所请求的组件的服务器时,发生的是动态负荷均衡;而当一个程序是基于固定的请求 个 数 (例如 , 如果有超过50个用户提出请求,则使用服务器B) 来确定处理请求的服务器时,发 生的是静态负荷均衡。 因为MTS和Windows NT Server 4.0不能实现动态负荷均衡,所以实现负荷均衡的最简单的 方法是,通过使多个服务器导出相同的程序包,提高你的分布式应用程序的可扩展性,然后 , 让客户决定它要使用的服务器。更复杂而稳定的技巧可能要涉及到创建负荷均衡程序包。 使用这个技巧,每个客户都要与负荷均衡程序包进行通信。而负荷均衡程序包的工作就是 基于一个算法或轮转方法创建中间层组件的实例。 微软推出了几个产品以助于进行负荷均衡, 其中包括Microsoft Cluster Server ( MSCS )c MSCS允许几台Windows NT Enterprise Server计算机作为一个独立单位出现。虽然对MSCS的详 细讨论超出了本皐的范围,但是你可以在微软的网站上找到更多关于这个产品的信息,. www.microsoft.com/Windows/server/Technical/management/ClusterArch.asp。 1 5 . 8 创建基于MTS的应用程序 基于事务的应用程序的一个主要目标就是尽可能快地完成事务。当事务发生时,数据库系 统将锁住数据, 以防其他应用程序使用相关数据。 上锁可以防止并发,但对可扩展性不利。为了提高可扩展性,你必须认真设计你的对象, 使它们在实际的事务中不会花费过多的时间。本质上,可以通过更细粒度的方法调用实现这一 点。粒度将在第20章中详细讨论, 事务的恍化将在第16章 中 讨 论 。 有两个方面决定事务的寿命。一个涉及MTS中的侦听机制, 特別是JIT。另一个涉及MTS中294 第二部分高级COM编程技巧 的超时特性,这个特性会在指定的时间内自动中止事务的执行。要想理解这两个方面,首先必 须理解事务是如何在MTS中运作的。 活动是具有相同并发条件的对象的集合o 在MTS2中 ,当客户创建了一个MTS对 象时 , MTS将创建一个新的活动,并把对象实例放在这个活动中。然 后,这个对象便可以使用接口 IObjectContext中的Createlnstance()函数,邀请其他对象加人这个活动。 另外,只有属于同一个活动的对象才可以对事务进行表决。当被标记为Requires a Transaction 或Requires a New Transaction的对象被激活时,事务将拥有'-个惟一的人口点,同时事务歼始执行。 你可能还记得,对象是在客户进行第一次方法调用时被激活的。也就是说,在客户请求创 建对象的时候,对象被创建,但没有被激活;直到发生第一次方法调用时,对象才被激活。由 于对象的表决被保存在环境封装中,因此,表决开始于对象的激活,终止于对象的取消激活C 当事务的根被刪除时,MTS将提交这个事务或者中ih它。更确切地说,如果对象需要成为 事务的一部分,它必须是从第一个对象内创建的,并且它必须被邀请进入同一个活动c 因为 MTS 2是在COM发行之后推出的,所以活动中的对象不能使用标准COM AP丨函数创建同一个活 动中的MTS组件。如果你打算使用CoGetClassObject()或者CoCreatelnstanceO,那么被创建的对 象将厲于一个单独的活动。为了创建属于同一个活动的对象,需要使用接口IObjectContext中的 Create 丨 nstance()方 法 。 下面的程序演示了如何邀请对象CLS1D 一 FriendObject加人同一个活动。 IObjectContext *pContext; HRESULT hr = GetObjectContext(&pContext); if (SUCCEEDED(hr)) { IFriendObject *pFriendObject; pContext->CreateInstance(CLSID_FriendObject, _uuidof(pFriendObj ect), (void **)&pFriendObject); > 当一个对象的环境封装被删除时,表决就再也不能改变了。环境封装时刻留意被称为“高 兴” 的标志,肯定的表决意味着对象“高兴” ,而否定的表决意味着对象“不高兴”。如果环境 封 装 在 “不高兴” 的状态被删除,那么事务将被中止。有两种风格的表决,一种是暂时表决, 另一种是最终表决。使用暂时表决,意味着对象可以改变它的想法(虽然我不高兴,似是如果 你进行另一个调用,也许我会变得高兴)。而使用最终表决时,对象的意思是,“我很不卨兴 ( 或 高 兴 ),没有任何人任何事能改变这个事实。” 对象通过调用IObjectContext接口中的两个方 法之一控制表决的状态:EnableCommitO和DisableCommitO。EnableCommit()将 状 态 设 为 “高 兴”,而DisableCommitO将 状 态设为“不高兴' 这两个函数的调用方法如下: IObjectContext *pContext; HRESULT hr = GetObjectContext(&pContext); if (SUCCEEDED(hr)) pContext ->EnableComfnit(); pContext*>Release();第7 5 章 MTS 295 EnableCommitO和DisableCommit()进行的是暂时表决--- 它们不影响环境封装的寿命,只 影 响 “高兴” 标志位的状态。如果对象连同环境封装一起被删除,那么在那一时刻的最新表决 将是最终表决。 如果你的对象已经完成了它在事务中该做的所有事情,那么这个对象可以请求MTS为它取 消激活。作为取消激活的结果,这个对象还将被删除。记住,直到根对象被取消激活时,事务 才会结束。然而,当一个下级对象被删除时,这个对象将通知应用程序它已经完成了表决。为 了取消激活它自己,对象可以调用IObjectContext接口中的两个方法之一:SetComplete()和 SetAbort()。这两个方法将通知MTS对象已经完成了它的工作。 换句话说,当方法调用结束并通过环境封装提交时,MTS将取消激活这个环境封装,同时 删除对象。SetComplete()通知MTS,对象 想 在 “高兴” 状态被取消激活;而SetAbort()通知MTS, 对 象 想 在 “不高兴” 状态被取消激活。当调用通过环境封装返回时,取消激活发生。下面的程 序演示了对SetCompleteO方法的调用: IObjectContext *pContext; HRESULT hr : GetObjectContext(&pContext); if (SUCCEEDED(hr)) { pContext - >SetConiplete (); pContext->Release(); } 注 意 在 COM+中,对象可以选择是被删除还是被放入对象池中。基于前面提到的表决 的规则,你可以推出下面的规則: 如果对象使用DisableCommit表 示 “ 不高兴’’,它可以改变想法,使用 EnableCommit变 得 “高兴”。对象可以改变想法,直到它被刪除为止。 如果一个对象在方法调用中使用了SetAbort,并且在调用结束时不改变它的想法(例 如 ,使用SctComplete结束方法调用),那么MTS将删除这个对象,同时事务将被中止。 如果事务的根对象被删除,那么任何属于这个事务的对象也都将被删除。 1 5 .8 .1 使用MTS进行设计 许多开发人员喜欢在方法的开始部分调用SetAbortO方 法 ,然后 ,在他们确定这个方法已经 适当地执行了的时候,便调用SetCompleteO方法。这样,如果有一个预料之外的错误发生,致 使这个方法过早地退出,那么SetCompleteO方法将不会被调用。 你应该努力使在事务中花费的时间尽可能地少。因此,当你设计应用程序时,一定要遵循 以下原则。 首 先 ,尽量使 用函数SetComplete()和SetAbort(),而 不使 用函数EnableCommit(>和 DisableCommitOo使用SetComplete()和SetAbort(),意味着你的对象将被频繁地删除和重建,这 将对你的系统造成巨大的冲击—— 你的对象将没有归属。(实际上,你应该在假设对象没有归属 的情况下编写对象,在用于激活的函数中进行初始化,在方法调用中执行你的操作,最后在用 于取消激活的函数中进行彻底的淸理)296 第二部分高级COM编程技巧 其 次 ,在 一 个 处 于 “不高兴” 状态的下级对象被删除之后,事务将被中止。因为再继续执 行事务已经没有意义,所以 ,为了提高系统性能,一旦你已经决定中止事务,就应该马上删除 根对象。同理,你绝不应该从根对象内部调用EnableCommit()或者DisableCommitO。对于中间 层组件来说,让客户程序决定何时中止事务是不妥的。 如果你调用的是EnableCommitO,而不是SetComp丨ete( ) , 那么事务将继续执行,直到客户结 束对对象的所有引用为止。让客户做决定就意味着系统性能的下降,以及事务失败机会的增大。 这是因为MTS为事务设置了一个超时时间。如果你给客户程序返回一个信息,比它可以自己做 出决定,那么在用户应答之前,系统可能已经超时。于是,你不得不让事务失败,并返回一个 错误码,通知用户重试整个操作。如果有错误发生,一个设计良好的用户界面应该允许用户重 试一个事务,而不必重新输人所有信息,或者输入一个补救事务。 1 5 .8 .2 使用MTS扩展应用程序 当 人 们 使 用 “扩展应用程序” 这个术语时,他们通常指的是增加更多的用户。因为计算机 只能同时处理一定数童的用户,所以增加更多用户的方法只能是增加更多的服务器。在MTS应 用程序中,可以做到在独立的儿台计算机上运行某些组件。从IObjectContext接口的 CreatelnstanceO方法中,你可能已经注意到,无法指定对象被创建的服务器。然而,在另一台服 务器上创建对象并把它们加入同一个活动是可能的。为了做到这一点,你必须通过MTS资源管 理器通知一个MTS服务器,对象将远程创建。 你可以按下面所述方法来做。 1 5 .8 .3 远程管理 系统管理员可以使用MTS资源管理器远程执行不同的组件。 使用MTS资源管理器远程运行组件的步骤如下: 1)保证这台远程计算机中的操作系统是Windows NT 4 Workstation、Server或Windows 2000, 而且安装有MTS。 2 )证实远程计算机上MTS安装目录下的Packages目录对系统管理员可访问。 3 )程序包必须被安装在远程计算机上。 4 )你打算远程运行的组件一定不能安装在本地计算机中。 5 ) 在本地计算机中,右键单击Remote Components文 件夹,并在弹出菜单中选择New , Remote Components。于是出现Remote Components对话框。 6 ) 选择你要在其中运行组件的计算机,以及你要使用的程序包,Remote Coniponems对话框 将列出程序包中的组件。选择你要使用的组件,然后单ifeAdd。 1 5 . 9 小结 在程序设计中,事务是影响共享数据的行为,它必须遵循ACID原 则:原子 、— 致、隔离以 及持久。当事务涉及多个数据库系统时,事务系统将使用被称为两阶段提交的算法。要建造一 个底层结构,使COM组件可以共享一个事务、保证ACID原则并使用两阶段提交算法,是很困难第/5 章 MTS 297 的事情。微软通过MTS提供了这个底层结构,即TP监 控 器 (可以管理事务流的软件)和ORB ( 可以创建和管理中间层组件寿命的软件)。 MTS使用侦听机制监控与一个对象有关的所有调用。通过使用侦听机制,MTS可以为它的 客户提供除事务管理以外的更多服务。例 如 ,侦听使MTS可以提供基于角色的安全检验。正如 前面提到的,位于MTS中的组件是配置组件。 MTS使用说明性属性来改变对象的行为c MTS通过在占位程序和对象之间插入环境封装对 象 ,实现侦听机制。具有相同并发条件的对象作为活动的一部分运行。 在MTS使用的事务系统中,组件可以对事务的结果进行表决。事务发生在一个活动的内部, 只有属于同一个活动的组件才可以参与这个事务。为了保证下级组件能够成为同一个活动的一部 分 ,需要使用IObjectContcxt接口中的Createlnstance()方法。如果你打算使用CoCreatelnstanceO 方 法 ,那么MTS将把这个请求当作一个独立的客户请求进行处理,所创建的对象将属于另一个 新的活动。对象可以使用四个方法对事务的结果进行表决:EnableCommitO、DisableCommit(), SetComplete()以及SetAbort(),SetComplete()和SetAbort()还要求系统在方法调用完成时取消激活 这个组件。在设计分布式应用程序时,一定要注意尽可能快地删除组件,这样才能保证更高级別 的并发。第三部分组件管理与事务 这部分内容包括: • 作为组件管理器的COM+ • 作为事务协调器的COM+ • COM+的安全性 •COM 事务集成器 •负载均衡组件 • 优化Windows DNA应用程序 第16章作为组件管理器的COM+ 本章内容包括: • COM+编程及其他基于组件的服务 • COM+可扩展性特性 • COM+和标准COM组件 • 通向COM+组件之路 • COM+和状态 • COM+组件必备的条件 • 编写COM+组件 COM+的基本功能是,为分布式环境中客户的宿主机提供有关对象调度和管理的服务c 这 样 ,COM+就很像CORBA ORB ( Common Object Request Broker Architecture Object Request Broker)。它提供共享状态管理以及进程管理服务。虽然由COM+服务例示的对象在 COM客户看来就像正常的对象一样,但 是 ,它却可以提供若干有利条件,其中最重要的就是 可扩展性。 本章和接下来的三章将深入讨论有关COM+组件开发的几个重要方面。本章将讨论可扩展性 问题以及基础的COM+组件设计和编程问题。第17章将讨论事务以及使用数据访问服务的COM+ 集成问题。第 18章将讨论安全性问题以及COM+角色管理。第 19章将讨论使用大型机进行的 COM+事务集成,而且还将涉及COMTI。 在本章中,你将看到COM+程序设计的本质。同时,你还将学习到COM+最基本的方面,进 而扩大为与可扩展COM+应用程序密切相关的设计问题,以及COM+应用程序与其他类型的 DCOM程序之间的区别在哪里。300 第三部分组件管理与事务 16.1 COM+编程及其他基于组件的服务 为了掌握Windows平台上任何一个新出现的基于组件的服务,你必须能够iHl答以下三个 问题: • 公开类和接口是什么? • 这个服务与应用程序之间通常是怎样互相作用的? • 这个服务与外部服务是怎样集成的? 这些问题在大多数情况下会有R 益复杂的答案。对COM+来说也是这样。发现COM+类和它 们各自的接口通常是很简单的,只要仔细查看帮助系统中的所有方法和厲性,然后钻研对你有 用的调用就可以了。要理解服务所使用的对象实例方案以及给出你的应用程序的预期是很困难 的事情,同样,要了解如何运用各种各样的方法调用来解决给定的问题也是不容易的。上面的 第二个问题的答案也是很难简单地表达出来的,因此,大多数的引用都会被不适3 地进行处理。 通常 ,在回答第二个问题时,会使用示例加以补充。最后一个问题的答案经常是被笼罩在神秘 的面纱下,我们只能模糊地瞥见一点点所以然。通常也只有在无关的API帮助页面底部注解中发 现的代码片断才能够提供些微的答案。 就先前的观察而言,我不再关注那些可以在接口方法的帮助文档中轻松得到的东西,而开 始更多地关注COM+模型不很明显的简洁性以及它与其他服务是怎样相互作用的。COM+可能是 自事件驱动的GUI以来,Windows应用程序开发中最具创新性的方面,但令人吃惊的是,许多人 忽视了这一点。大多数开发人员在第一次使用COM+时 ,都会编写出完全失败的组件。这些首 次尝试通常都遗漏了COM+真正的强大功能。我将努力减轻你在研究COM+时的痛苦3 16.2 COM+可扩展性特性 COM+提供了儿个关键的特性,极大地增强了组件服务的可扩展性。其中的每个待性都试图 以一种对外部客户应用程序透明的方式,减少内部服务程序的系统开销。对于客户来说, COM+组件看起来与标准COM组件没有任何区别。 对高度可扩展的应用程序而言最关键的一点就是,有效的资源管理。创建所有资源的应用 程 序,会使内存空间越来越少,而且只要所有的客户不是同时运行,应用程序就可以支持很多 的客户。 另一个很有价值的技巧是,限制资源被建立和删除的频率。复用先前被分配的资源有利于 减少内存的分段,应用程序中出现的页错误(数据从硬盘到内存的传送)也会减少,使得砬用 程序可以拥有一个稳定的工作环境(内存中页的集合)。而且,在许多方案中,资源的重新初始 化过程通常比严格的初始化过程快。 为了获得这些好处,COM+提供了以下可扩展性特性: • 准时激活。 • 及早取消激活。 • 对象池。 • 资源池。第 / 6 章 作为组件管理器的COM+ 301 准 时 (JIT)激活使COM+可以只在需要的时候创建对象。及早取消激活允许对象在它们当前 的活动完成时马上被删除,这样 ,COM+便可以使对象的总数正好降至实际被使用的对象的个数。 池是用于在临时集合中维护资源,并在使用资源的请求到达时复用这些资源的方法。COM+ 为池化组件对象提供了便利。COM+还为资源分配器定义了一个模型,而资源分配器可以提供 仟何类型的池资源,如数据库连接。 注 意 实 际 上 , MTS不能池化组件对象。所有的准备都很适当。 在池方案中,MTS会进 行期望中的所有调用,但是,在客户使用完对象的时候,MTS将删除这些对象。COM+ 是第一个具有对象池特性的、基于COM的应用程序服务器。 16.3 COM+和标准COM组件 在本部分中,我们将讨论在COM+环境中使用标准COM组件的正面影响和负面影响。首 先 , 我们将创建一个标准COM组件并检验它的特性。接 着 ,我们将在COM+环境中配置和测试COM 组件。最后,我们将深入研究在COM+环境中运行标准COM组件的利与弊。 1 6 .3 .1 标准COM组件 作为使用COM+的开始,你可以在没有特定COM+支持的情况下,创建一个标准COM组 件 , 并测试它在COM+控制下的运行状况。为了进行这个测试,你需要创建一个基本的ATL DLL项 目,名为Staleful。接 着 ,给项目添加一个简单的ATL COM类 ,名为Sum ( 右键单击项目图标并 在弹出菜单中选择New ATL C la s s ,然后指定一个具有默认接口ISum的 简 单 对 象 )。为了支持 VBScript客户程序,你需要使用双重接口。 注意 双重 接 口 通 常 都存 在问 题。COM类可以有多个接口,这是COM+编程的有利条件 之一。声明同一个接口的多个实现是件危险的事情,因为单元之间的调度只会为给定接 口的第一个实例建立代理。三个双重接口在客户看来,是三个完全不同的IDispatch实现。 开发人员通常要做出选择的是,使用COM接口编程,还是创建提供单一IDispatch接口 以支 持 脚 本 (惟一不能直接处理COM接 口 的 流 行 客 户环 境)的类。为了简单起见,在 这里我们使用了双重接口,但是应该注意的是,真正的基于双重接口的应用程序,在新 的应用条件的冲击下,将产生严重的问题。 最后 ,给ISum接口添加两个方法,Accumulate()和A dd()( 右键单击这个接口,并在弹出菜 单中选择Add Method )。使用淸单16-1作为你定义和实现Sum类方法的指导,这里给出了头文件 和.cpp文件。注意,Accumulate()方法使用了一个整型属性m_iTotal,你必须把它添加到类的定 义 中 ,并在构造函数屮将它置零。 清单16-1简单的COM对象 II Sum.h : Declaration of the CSum #include * resource.h“ // main symbols iiiiiiiniiniuiiiiiiniiiiiii/iiiiniiiiiii302 第三部分组件管理与事务 II CSum class ATL_NO_VTABLE CSum : public CComObjectRootEx, public CComCoClass, public IDispatchlmpKISum, illo'lSum, &LIBID_STATEFULLib> { public: CSum{) { m一iTotal - 0 ; DECLARE_REGISTRY_RESOURCEID(IDR_SUM) d e c l a r e ! p r o t e c t _f i n a l _c o n s t r u c t T) DECLARE_NOT_AGGREGATABLE(CSum) BEGIN_COM_MAP(CSum) COM^INTERFACE^ENTRYCISum) COM_INTERFACE_ENTRY(IDispatch) END_COM_MAP() H ISum public: STDMETHOD(Sum)(int iValuel, int iValue2, int *piTotal ); STDMETHOD(Accumulate)(int iValue, int * pTotal); // Attributes (unmanaged state, yikes!) private •• int m iTotal; II Sum.cpp : Implementation of CSum #include “stdafx.h“ #include “Stateful.h“ ^include “Sum.h“ iiiiimininii/miiiifiiiiinnmiiuHumiii // CSum STDMETHODIMP CSum: '.Accumulate(int iValue, int *piTotal) if ( NULL == piTotal ) return E_INVALIDARG; m_iTotal += iValue; •piTotal = m_iTotal; return S_0K; STDMETHODIMP CSum::Sum(int iValuel, int iValue2, int *piTotal) if ( NULL == piTotal ) return E^INVALIDARG;$ 1 6 t 作为组件管理器的COM+ 303 •piTotal = iValuel + iValue2; return S—OK; 正如你看到的,类方法AccimuilateO需 要 使 用 对 象 状 态 信息(一个 对 象 的属性),然而 ,另 一个方法Sum()只 使 用暂 时 状 态 (堆 栈 变 童 )。这两个方法在MTS外都工作正常。一 会儿,你将 在MTS运 行 环 境 中 测 试 它 们 编 译 DLL。(你可以在CD-ROM上的Chapterl6\Statefum录中找到 编译好的.dll文件以及整个项目。) 接下来,你将要面对编写客户端软件这件麻烦的事情。为了不转移你的注意力,可以随便 拼凑一个简单的VBScript用作客户端程序。清 单 16-2给出了一个客户程序范例(你可以在CD- ROM上的Chapterl6目录中找到这个.vbs文 件 )。 清单1 6 - 2 客户程序示例 ' SumClient.vbsI ' Simple C0M+ Client VB Script 'Application Entry Point 1 Create Sum Object Set obSum = CreateObj ect(“Stateful.Sum.1“) Total = 0 do 'Set up menu Menu = _C0M+ Client Menu“ & vbCr & _ ■0 • Exit' & vbCr & _ _1 - Accumulate a value“ & vbCr & 一 •2 • Add two values' & vbCr & vbCr & 一 •Current Cumulative Total: _ 'Get selection Choice = InputBox(Menu & Total) 1 'Execute command Select Case Choice Case *■: 'This interprets the cancel button as a quit request Choice = *0* Case “0“: Case T : Value = InputBox('Enter a value to accumulate:N) Total = obSum.Accumulate(Value) Case “2“: Valuel = InputBox(“Enter the first value to add:“) Value2 = InputBox(“Enter the second value to add:') MsgBox obSura.Sum(Valuel,Value2), 0, “Transient Total“ 'Case Else: MsgBox “Bad choice' End Select304 第 三 部 分 组件管理与事务 Loop While Choice <> “0“ 'End of Application 测试你 的组件 。一定要保证你的客户脚本程序运 行 正 常 ,并且可以使用先前创建的迸程内COM服务程 序成功地创建了一个Sum对象。要运行客户脚本程序, 只需要在资源管理器中双击.vbs文件就可以了。你可以 在图16-1中看到运行中的脚本程序。 1 6 .3 .2 将标准COM组件用于COM+ 现 在 ,在COM+中测试这个组件。你需要创建一个新的应用程序S u m ,并将你的Stateful组 件添加到这个应用程序中。 在 组 件 服 务 控 制 台 中 ,右 键 单 击 COM 应 用 程 序 图 标 ,并 在 弹 出 菜 单 屮 选 择 N e w , Application。 使 用 向 导 对 话 框 创 建 一 个 新 的 应 用 程 序 Stateful,接 受 所 有 的 默 认 设 贤 。 为了把 你的 Stateful.cll丨添加到程序包中,需要选择这个新应用程序的Components文件夹,右键单右并在弹出 菜单中选择New,Component,然后添加这个已注册组件。这使COM+可以调整注册表入口,从 而使这个组件可以在mtx.exe环境中运行。 在组件服务控制台中,选择Stateful程序包的Components文件夹,并选择Status View,使你 可以监控对象的活动。双击.vbs源文件,在COM+环境中测试这个组件。瞧 !你甚至不需要修改 客户程序。COM+已经照料了每件事。 想在分布式配置下使用这个新的应用程序吗?没问 题,右键单击这个应用程序,并在弹出 菜单中选择Export,然后给出一个导出路径名。当导出完成时,可以在导出路径的客户子目录中 找到一个客户安装程序。只要在远程客户计算机上直接运行这个安装程序,任务就完成了。运 行客户端的脚本程序,这个程序将请求一个与服务器系统中支持组件的COM+服 务程序进程的 DCOM连接。 当你编写组件并在单机上的COM+环境中测试它们的时候,还需要注意两个问题。第 一 ,在 默认情况下,COM+服务程序进程不会立即关闭程序包。你 需 要停止运 行 你 的 程 序 包 (右键单 击程序包并选择Shutdown ) , 使连接器可以用一个新的构件覆盖这个组件的DLL。如果没能这样 做 ,那么将造成连接错误,因为这个DLL仍然在被COM+服务程序进程使用。第二,在你每次使 用V O + 创建组件时,VC++总是试图把这个组件作为标准COM组件进行重新注册。所 以 ,你需 要更新注册表中COM+应用程序的配置,在组件服务控制台中,右键单击这台计算机,然后在 弹出菜单中选择Refresh All Components0 1.安全问题 你的非COM+组件存在一些问题。有些问题现在就很明显,而有些问题则将随着不断的使用 逐渐暴露出来。一个显著的问题就是,你的组件现在运行在一个服务程序进程中,与它先前所 在的客户程序进程相比,它可能会拥有更加强大的访问令牌。无论你什么时候运行跨越进程边 界的分布式组件,都需要考虑安全问题。很难凭空决定到底允许客户程序做些什么,除非你非 —sag— 丨 si 图16-1运行中的脚本程序第/ 6 章 作为组件管理器的COM+ 305 常熟悉幵发环境的账号结构,否则即使知道谁是客户,也 不 会 对 你 的决定 (允许客户程序做些 什 么 )有任何实质性的帮助。为 此 ,COM+提供了一个强大的角色系统,我们将 在第18章中详 细讨论这个系统。 2 .事务问题 与非COM+组件有关的一个重要主题就是事务的行为。需要注意的是,如果组件在COM+外 部没有使用事务,那么它可能不会错过在COM+内使用事务的机会。但 是 ,如果你让非COM+组 件 使 用 事 务 ,会发生什么呢?答 案是 可 以预见 的 ,但绝不是一个好的结果。问 题在 于 ,是MS DTC ( 为了使COMV支 持 事 务 ,必须运行MS D T C )负责 保管 与 事 务 有关的统计 数 据 。如果 COM+发现你的类需要事务,那么当发生方法调用时,COM+会为这个类的每个对象创建一个事 务。如果一个对象被很快地删除,DTC将假设事务已经完成,并在可能的情况下提交这个事务。 而如果一个对象在事务超时之后仍然存在,而且没有明确地提交这个事务,那么这个事务将被 终止。这种随机行为对于需要事务稳定性的应用程序来说,是很不合适的。 你也许想在Stateful组件上试验一下自动事务。为了使你的S⑶ efu〖组件可以进行事务处理, 需要进行以下步骤:在组件服务控制台中,右键单击这个组件并在弹出菜单中选择Properties。 在Properties窗口中,选择Transaction选项卡中的Requires a Transaction。查看组件服务控制台中 显示的事务统计数据,使你可以跟踪对象事务的提交和中止速度。如果在事务还没有超时的时 候 ,取消 激 活你 的 对 象 (退出VBScript客户程序 >,这个事务将被提交。但 是 ,如果对象在事务 超时之后仍然存在(客户程序仍然运行),这个事务将被终止。虽然标准COM组件在事务中可能 会较为正常地工作,但是 . 在一系列的COM+调用中,至少要有一个组件使用COM+的事务工具, 以确保适当地完成事务。我们将在第17章中讨论创建事务组件的正确方法c 3 .可扩展性问题 正如你已经看到的,COM+可以毫无问题地使用非COM+组 件 ,那个脚本程序进程可以与 COM+服务程序进程进行无阻碍的通信。但 是 ,当你试图把这个组件用于频繁使用的高性能环境 中时,问题便出现了。这个组件根本不支持取消激活或者池。因为COM+不知道这个组件的存在, 所以在它长时间空闲的过程中,COM+无法删除它。实际上,这样做将中断AccumulateO方法,因 为这个方法依赖于保存在对象内存中的对象状态mJTota丨的值。为了提高这个对象的可扩展性, COM+惟一可以做的就是让服务程序进程一直运行下去(修改组件服务控制台中的超时设置)。服 务程序被关闭前的空闲时间默认值是三分钟,但是 ,你可以配置应用程序,使之无限地运行下去。 另一个问题是,每个对象实例都保留着一个mJTotal值的拷贝。这在一个多用户的、基于服 务器的应用程序中可能很不合适。正如你所知道的,在设计COM+组件 时 ,状态值的维护有几 个重要的分支。 如果你让COM+知道你的操作已经完成,会发生什么呢? 为了做到这〜点,你需要超越自我, 并且给你的应用程序添加一点儿CONU特 有 的 代 码 (本章后面将详细讨论)。在两个ISum方法的 结 尾 (函数返回之前),添加以下代码: //Tell COM+ that our work is complete HRESULT hr; IObjectContext * pOC; hr = GetOb)ectContext( &pOC );306 第三部分组件管理与事务 if ( FAILEO(hr)) return hr; pOC•>SetComplete( ) ; pOC->Release(); 这是一段样板代码,它将通知COM+你的对象已经彻底地完成了任务。3 然 ,对于你的组件 来 说 ,这个声明只是个谎言,COM+将会适时地惩罚这种犯上行为。除 非客 户释放 对象 ,否则 对象永远也不可能彻底完成任务。为什么呢?因为对象需要丨值的状态 负 责 ,而且只要 客户需要,对象必须继续运行。编译一下这个新的版本,看看有什么不同。为了可以直接使用 与COM+有关的MTS API函数 和接口,你需要给源程序添加mtx.h头文 件 ,并且连接mtx.lib和 mtxguid.lib 库文件。 重新编译你的组件。在测试这个组件之前, 你可能需要使用COM+对它进行重新注册,因为 VC++在完成了一次成功的编译之后,会自动把这个DLL当作进程内服务程序进行注册。在组件 服务控制台中,右键单击My Computer图标并选择Refresh All Components, 重新注册你的组件。 测试组件中的Add函数。工 作正 常 ,对吗?试着执行AccumulateO例程两到三次。不是很正常 吗? AccumulateO例程总是只返回当前值。 为了了解到底发生了什么,给组件的构造函数添加一个起说明作用的消息框,昆示Object Createdo代码如下: Csum::CSum () { m_iTotal = 0; MessageBox( NULL, 一T(_Sum Object Created“), It (“Statful.dll“), MB_0K ); } 为这个类在析构函数中创建一个相似的消息框。编 译 ,電新注 册 ,运行。啊 哈 !正如你所 看到 的 , COM+在调用之间扼杀了你的对象。查看程序包的组件文件夹的状态窗口就可以知道 真相。COM+知道了有一个未完成的对象存在,然 而 ,当对象完成了一个操作时,这个对象的 方法将通知COM+操作完成,从而使COM+毫不犹豫地删除这个对象。因此,你便丢失了每次调 用返回的mjTotal值 。但 是 ,你的对象现在变得更加可扩展了。如果你过去可以支持10个客户 ( 每个客户只有10%的时间使用你的对象),那么现在你可能有能力支持100个客 户。这 是 因 为 , COM+只对处理有效调用所必需的对象进行维护。 注意这 个 示 例 是 为 了 说 明问 题而设计 的 ,因此它的价值不大。因为这个组件所占用的 内存空间很小, 所以反复创建它与把它留在内存中相比,系统开销差不多。从这个例子 中学到的重要内容是,如何使COM+在客户与组件断开联系之前,取消激活你的组件。 另外,只有在模拟现实负荷的条件下进行測试,才能知道你的组件是否真的可以从能够 实现及早取消激活的设计中获益。 16.3.3 COM+对标准COM组件的好处 正 如 你 已 经看到 的 ,即使是普通的COM对 象 ,也可以通过持久的COM+服 务程序进程和^ 1 6 t 作为组件管理器的COM+ 307 COM+的 资 源 池 (如池化的ODBC连 接 )提高可扩展性。通过使用易于配置和易于导出的应用程 序 ,管理工作被极大地简化。通 过 进 程 的 隔 离 (在应用程序设置所定义的相互隔离的进程中运 行 组 件 ),可靠性得到了提高。所 以 说,即使是标准COM组件也可以从COM+应用稈序服务器环 境中获益匪浅。 1 6 . 4 通向COM+ 组件之路 像大多数事物一样,COM+不是从天上掉下来的,它是不断发展的应用程序设计技巧和实现 技巧的产物。下面的讨论将引领你去理解为什么现在会有COM+的存在。在本部分中,我所选 择的用于演示的语言和技术偶尔会涉及UNIX/Corba/Java。因为本书的重点是使用C++的COM程 序开发,所以我选择的都是与之关系密切的技术。 1 6 .4 .1 软件复用 在计算机刚刚出现的时候,程序员们满足于编写一次性程序,怛 是 、很快他们便发现了更 好的方法。数据存储技术的发展使存储并复用应用程序中的代码片断成为可能。由于先前做过 测试和调试,这些代码片断通常比新代码更加可靠。在对软件设计方法论的探索过程中,发生 过几次改良。 1.模块:行 为 的 分立单 元 (模 块 化 ) 早先的编程语言提供的主要是结构化编程,它 往 往 是 把 大 型 应 用 程 序 拆 分 成 “可复 用 的” 模 块。因 此 ,开发人员们苦思冥想着可以把他们的应用程序的函数组织成可复用的函数组的方 法 。这使得几个程序员可以在同一时间编写不同的模块,但 是 ,这样做是有代价的,他们必须 谨慎地设计那些将与其他模块进行通信的接口。C语言引以为荣的正是高度的模块化,所以它对 软件开发的影响比其他的任何语言都更为巨大。Windows以DLL的形式提出了一种二迸制版本的 可复用模块。在复用和测试通用模块中存在的主要问题与这样一个事实有关,那就是这些模块 都是从功能性角度出发进行设计的。解决这个主要问题的尝试往往会失败,而原因主要是围绕 与数据结构有关的集成条件等问题。因此,以往的开发人员很少留意数据和状态的重要性。然 而 ,功能性方法的确很好的映射为CPU的行为方式,并且提供了程序结构与执行速度之间良好 的性能折衷方案。 2 .类:数 据 与行 为 的 结合(封 装 ) 现在你有了一定的能力,而且每兆内存的价格比曼哈顿区公寓每平方英尺的价格便宜得多, 因此你已经得到了一些喘息空间。很 明显,接下来的一步就是使你的应用程序更大更慢。当 然 , 这些只是难懂的复用搜索带来的副作用。随着数据库技术的不断发展,大多数应用程序的结构 与它们所管理的信息之间的关系越来越紧密,而与它们所提供的处理过程之间的关系就不如前 者紧密。 现在进入对象和对象的类。类 ,提供了一种将一组例程与一组相关数据结合在一起的方法。 这些例程提供对外的接口,在这一点上很像模块。可以通过构造对象来创建类的实例。对象有一 个由类定义的数据结构的专用拷贝,代表它们的状态,同时它们与相同类的其他对象共享这个类 的成员函数。除了可以将问题空间的实体模拟为对象以外,这种方法还提供了比模块更好的模块308 弟 三 部 分 组件管理与事务 性。模块没有预定义的数据管理机制,它们所代表的是一组只能在应用程序中使用一次的行为。 模块的复用经常需要对全局数据结构进行管理,而全局数据结构是很容易被破坏的。类则 注重于一个更为整体的方法,这 个 方 法 将 实 体 (数据 和功能性)放在了设计考虑的最前沿。数 据和状态变成了重要的问题。可以创建一个类的多个对象,而这些对象则提供了一种划分全局 名字空间的方法。这 样 ,封装和实例便成为面向对象的数据管理中最为关键的问题。继承使你 可以复用原来已经存在的类、实现以及所有东西。但在许多方案中这样做存在问题,因为实现 的行为很容易变化,这也是许多设计者转而使用数据结构的主要原因。虽 然代码更易于复用, 但即使是这样,代码更多的还是被重新利用再生,而不是被严格地复用。 我创建了一个名为Rectangle的组件 ,和一个名为UseRectangle的客户程序。Rectangle组件 ( 清 单16-3)将根据一个类成员所代表的颜色,在它的窗口中绘制一个实心的矩形。客户程序 ( 清单16-4)在每次例示这个类时,都会把那个代表颜色的类成员变童设置为不同的值。这个例 子将说明数据封装的概念。 清单16-3拥有Color类成员的组件 // Rect.cpp : Implementation of CRect ^include ■stdafx.h“ #include “Rectangle.h“ #include “Rect.h“ iiiiniii/iiiiiiiiiiiiiiiiiiiiHiiiiniiiuniiiiniiiiiniiimii/iiiiiiifii II CRect STDMETHODIMP CRect::get^RectColor(long *pval) { *pVal : (long) m_RectColor; return S_0K; STDMETHODIMP CRect::put_RectColor(long newVal) { 一 m_RectColor = (COLORREF) newVal; return S_0K; > // Rect.h : Declaration of the CRect #ifndef _RECT_H_ #define _ RECT_H_ ^include Mresource.hM // main symbols •include // Rect.cpp : Implementation of CRect #include “stdafx.hM ^include “Rectangle.h“弟76章 作为组件管理器的COM+ 309 #include “Rect.rr ///////////////////////////////////////////////////////////////////////////// II CRect STDMETHODIMP CRect::get_RectColor{long *pVal) { 一 *pVal = (long) m_RectColor; return SJ)K; STDMETHODIMP CRect::put一RectColo「(long newVal) m_RectColor = (COLORREF) newVal; return S—OK; 清单1 6 - 4 使用Rectangle组件 II Machine generated IDispatch wrapper class(es) created by // Microsoft Visual C++ II NOTE: Do not modify the contents of this file. If this class is II regenerated by Microsoft Visual C++, your modifications will be II overwritten. #include “stdafx.h“ #include “rect.h“ iimiiuiiimiiiniimiiiintiimuiiiiiiiiiiiiiiiiiiiiiiuiniiiiiuiiii II CRect1 IMPLEMENTJ)YNCREATE(CRect1, CWnd) iniininiiiiiiiiiiiiiiiniiininiiii/iiuiiiniiiiiiiiiiiiiiiniiiiiiiim II CRect1 properties 111111 r 11111111 n i u 11111111111 n 111 u u 111 n u 111 n 1111111111111111 n 1111111 II CRect1 operations long CRectl::GetRectColor() { long result; InvokeHelper(0x1, OISPATCH_PROPERTYGET, VT_I4, (void *)& resu lt, NULL); return result; void CRectl::SetRectColor(long nNewValue) { static BYTE parmsH = VTS_I4;310 第三部分组件管理与事务 InvokeHelper(0x1, OISPATCH_PROPERTYPUT, VT_EMPTY, NULL, parms, nNewValue); > II UseRectangleView.h : interface of the CUseRectangleView class II11 n 11111 j 1111 n i /1 n 1111 n h i 111 m 11111111 n 111111 n i u 11 n h n i n 111111111 #if idefined (AFX_USERECTANGLEVIEW_H_8C04473E_1 A26_11 D4_B9F9_ _00105AA721 BE—INCLUDED」 #define AFX USERECTANGLEVIEW H 8C04473E_1A26_11D4_B9F9_00105AA721BE_ •INCLUDED—— #if _MSC_VER > 1000 #pragma once #endif II __MSC_VER > 1000 ^include Brect.h“ class CUseRectangleView : public CView { protected: // create from serialization only CUseRectangleView(); DECLARE^_DYNCREATE( CUseRectangleView) II Attributes public: CUseRectangleDoc* GetDocument <); f I Operations public: CRectl m Rectangle[8); BOOL mbRectanglesCreated; // Overrides II ClassWizard generated virtual function overrides //{{AFX_VlRTUAL(CUseRectangleView) public: virtual void OnDraw(CDC* pDC); II overridden to draw this view virtual BOOL PreCreateWindow(CREATESTRUCT& cs); protected: //>}AFX_VIRTUAL II Implementation public: virtual ~CUseRectangleView(); #ifdef _OEBUG virtual void AssertValid() const; virtual void Dump(COumpContext& dc) const; #endif protected: // Generated message map functions protected: //{{AFX_MSG(CUseRectangleView) //}}AFX_MSG第76章 作为组件管理器的COM+ 311 DECLARE_MESSAGE_MAP() #ifndef —DEBUG // debug version in UseRectangleView.cpp inline CUseRectangleDoc* CUseRectangleView::GetDocument() { return (CUseRectangleDoc*)m_pDocument; } #endif ///////////////////////////////////////////////////////////////////////////// //{{AFX—INSERT_U)CATION}> II Microsoft Visual C++ will insert additional declarations II immediately before the previous line. #endif / / !defined(AFX_USERECTANGLEVIEW_H_8C04473E_1 A26_11 D4_B9F9_ ^•001C5AA721BE INCLUOfD ) II UseRectangleView.cpp : implementation of the CUseRectangleView class II •include “stdafx.h' #include HUseRectangle.hM #include “UseRectangleDoc.h“ #include “UseRectangleView.h“ #ifdef 一DEBUG #define new DEBUG_NEW #undef THIS^FILE “ static char THIS一FILEl] = 一 FILE一 ; #endif ///////////////////////////////////////////////////////////////////////////// II CUseRectangleView IMPLEMENT_DYNCREATE(CUseRectangleView, CView) BEGIN_MESSAGE_MAP(CUseRectangleVi8w, CView) /7{{AFX_MSG_MAP(CUseRectangleView) “ ”AFX:MSG:MAP e n d_m e s s a g e _m a p T) iiiiiiiiiiiiiii i i i i i i i i iiiiiiiiiiiiiiiuiiinmiuiiiifuniuniinHi um II CUseRectangleView construction/destruction CUseRectangleView::CUseRectangleView() m_bRectanglesCreated = FALSE; CUseRectangleView::-CUseRectangleView() BOOL CUseRectangleView::PreCreateWindow(CREATESTRUCT& cs) {312 第三部分组件管理与事务 return CView::PreCreateWindow(cs); } /////////////////////////;/////////////////////////////////////////////////// // CUseRectangleView drawing void CUseRectangleView::OnDraw(COC* pOC) { CUseRectangleDoc* pDoc = GetDocument(); ASSERT^VALXD(pDoc); if( i m^bRectanglesOreated ) { for( int i=0; i<8; i++ ) { RECT Rect; Rect.top = 10; Rect.bottom = 40 ; Rect.left - 10 + i * 30; Rect.right = 10 + i * 30 + 25; m_Rectangle[i].Create( _ “ , WS_VISIBLE, Rect, this, 0x1001 ♦ i m_Rectangle[i].SetRectColor{ RGB( ( rand() & 255 ), • ( rand() & 255 ), ( rand() & 255 ))); iiiiiiiiiiiiiiifiiiiiuininiinmiiiii/iiiiiiifniininiinninii/iiiin II CUseRectangleView diagnostics #ifdef _DEBUG void CUseRectangleView::AssertValid() const { CView::AssertValid(); void CUseRectangleView::Dump(CDumpContext& dc) const { CView::Dump(dc); CUseRectangleDoc* CUseRectangleView::GetDocument() II non-debug •version is inline { ASSERT(m_pDocument->IsKin(30f(RUNTIME_CLASS(CUseRectangleDoc))); return (CUseRectangleDoc*)m_pDocument; > #endif //^DEBUG ////////////////////////////////////////////////////////“////“////“//“/// // CUseRectangleView message handlers 3.COM+ :从实 现中分离的接口 下一个改良示例遵循了分布式计算的做法。如果一台小型计算机的速度是大型机速度的I 分 之 一 ,那么二十台小型机的速度将是大型机速度的两倍,对吗?可以肯定的是,只要你能够第/ 6 章 作为组件管理器的COM+ 313 找到某种合适的方法,把这些小型机连接在一起,就可以实现零系统开销。当 然,大多数事情 的主导因素是价格。于 是 ,分布式计算的时代来临了。 • 因为你已经在使用对象,所 以 ,如果你可以在不同的计算机上分布使用它们,那将是很方 便 的 。这里 ,一个有待解决的问题是:一个系统上的对象如何与另一个系统上的对象对话?答 案当然是通过对象的公用接口。不幸的 是 ,在大多 数语 言中 ,对象的公用接口不能从对象的实 现中分离出来,而且在调用者的系统中不实现甚至不声明公用接U c 许多开发人员开始意识到, 在一个类中可以复用的最重要也最可能的部分是接口。于是,只使用纯虚拟函数创建C++类成为 建立接口的标准方法,而接口比类的实现更容易被复用。虽 然 一 些语 言 (如Java ) 直接支持纯接 口的定义,但这也正是一个缺点,即接口是需要使用指定语言进行编写的。 现 在 ,如果使用某种支持纯接口的语言声明接口,那么你便可以从实现中分离接口,而这 些接口将支持使用任何语言编写的对象。对象的重要部分(接口的实现)可以从对象的次要部 分 (接口 的 声 明 )中分离出来。这 样 ,在Windows的世界中,COM便提供了一种解决方案,使 得任何语言都可以通过接口定义语言(1 D L )声明对象接口。这促进了对象实现的二进制复用, 以及接口的复用。于是,COM便可以管理提供给调用者使用的接U 以及被调用的对象,并使客 户和服务器的位置透明。 对象分布的跨网络事务的系统开销问题通常是最少的。考虑这样一个事实,那就是在服务 器上创建的每个对象都有它自己专用的一组数据,用以保存它的状态。一百个客户就意味着内 存中的一百个服务器对象的拷贝。即使 如 此 ,你仍然可以通过增加计算机的数域来解决问题, 但这只是暂时性的。维护数据库连接并使用共享数据其他部分的对象,将很快断送无限可扩展 性原 则。但 无论如 何 ,COM在它引领接口时代和实现复用承诺的过程中,确 实引入 了 可 复 用 、 可分布的二进制组件。 1 6 .4 .2 性能、可扩展性和稳定性 这样看来 ,复用和可靠性是长远目标,对象和接口则只是为了实现这个目标而在特定时期 使 用 的 方 法 。但 是 ,如果为了在高要求的互联网驱动的计算环境中,提 供高性能 的 应 用程 序, 而不得不从头开始编写所有东西,这又有什么好处呢?我估计一点好处也没有。那 么 ,如何才 能使程序变得更快而且更加可扩展呢?答案是认真地进行状态管理c COM+:从行为中分离的状态 考虑这样一个问题:如果在响应客户创建对象的请求时,COM运行环境仅仅返回一个成功 码 (正如它经常干的),会发生什么?这将使创建对象变成一件很快的事情,而且减少了网络的 开 销,服务程序也可以不必创建和维护对象的状态。 当然,当你调用对象的某个方法时,服务 程序必须创建这个对象。如果服务程序只需要维护一个装满对象的池子,在你需要对象的时候 取出这个对象交给你,并在你使用完这个对象之后交给其他客户使用,怎么样呢?假设只有10% 的客户同时使用服务程序的对象,那么一百个对象就可以支持一千个客户。这使资源扩大了十 倍 !你可能已经注意到一个问题,那就是你得到的对象已经具有了从上个客户得来的状态,怎 么办?为了解决这个问题,对象必须小心地管理它们的状态。这也是真正的COM+组件的主要 特征。314 第三部分组件管理与事务 作为企业级应用程序服务器,COM+为 准 时 UIT) 创 建 对 象 、及早删除对象、对象池以及 许多其他的特性做好了充分的准备。为了在这个高级的应用程序服务器环境中运行,组件必须 删除所有的状态假设,并恢复任何必需的数据。 一个COM开发人员,不可能在向给定的对象类中添加一个庞大的数据结构这个问题上考虑 两 次 ,而COM+开发人员则会注意到更多的东西。通过认真的状态管理,COM+组件可以提供良 好的性能 、可扩展性和稳定性。没有一个对象可以比无状态对象(没有任何《性 的 对 象 )更快 地 被 创 建 、调 度 和 删除,也没有一个对象可以比无状态对象更透明地穿行于不同的CPU之 间 , 更没有一个对象可以比无状态对象拥有更稳定的生命周期。如果客户在执行一个无状态对象中 的方法时,对 象 被 删除,那么客户只需要重试一次,但 是 ,如果对象在被删除时正在跟踪你最 近的七次操作,那么你必须进行彻底的清理工作。不幸的 是 ,在许多情况下,状态是必不可少 的。因此,COM+提供了一系列方法,用于在有状态的对象中管理数据。 正如你看到的,程 序设计世界已 经远离了 一个旧时 代(使用紧密耦合的组件来解决复用问 题 〉,迎来 了 一 个新时 代 (分布式应用程序成为软件开发世界的福音)。在这个新时代中,实体 的识别和关系仍然十分重要,但另一个设计高潮必然随之产生。这个时代的重点是,从半可复 用的实现行为和不可复用的状态中分离出来的可复用的接口。接口、实现和状态的分离对可扩 展的分布式多层应用程序是十分关键的。因为你在阅读本书,所以我假设你已经掌握了有关接 口和实现的知识。接下来,让我们看一看状态。 16.5 COM+和状态 状态由一些要素组成,这些要素描述了一个特定对象以及它在面向对象应用程序中的当前 环境。状态可以是满的,也可以是空的。为了更加通用,状态是由某一时刻的所有相关数据项 的值组成的。一般而言,用对象属性的值描述它的状态。 适当地管理应用程序的状态是COM+应用程序设计中最为关键的方面之一。COM+ 对状态模 型有几点限制,如果你在设计可扩展的服务器应用程序,但却没有遵循这些原则,那么我劝你 还是应该遵循一下。这 样 ,在向COM+移植的过程中,你已有的应用程序会经历相对小一些的 冲击,并得到更好的可扩展性。 描述良好的对象通常有几个合法的状态。运行的对象可以有Ready、Failsafe以及Fire三个状 态。对象的某些状态转变可能是非法的,例 如 ,从Failsafe到Fire的状态转变就是非法的。停留 在非法状态的对象是应用程序失败的主导因素。例 如 ,在将Door属性设置为Closed的同 时,将 Ignition属性设置为Tme,是不好的。只要数据总是正确,没人会在意你的算法是否正确。因此, 减少那些会损害对象状态的例程的总数,是你最重要的软件可靠性策略之一。 1 6 . 5 . 1 状态的类型 状态有一些像行李—— 你不能把它随便地到处摆放,你必须随身携带它或者在某个安全的 地方寄存。有 些 东 西 (如你 的 名字> 你时刻都带着,而 另 一 些 东 西 (如你 的 午 餐 )你只带一会 儿。在确定需要怎样管理状态的过程中,确定这个状态的持久性是十分重要的一步。虽然有许第7 6 章 作为组件管理器的COM+ 315 多状态寿命的种类,但 是 ,下面讨论的三种类型是敁通用的。 1.暂时状态 暂时状态包含了只需要临时存储的数据项,通常在调用堆栈中维护暂时状态。例 如 ,方法 的参数和本地的变量组成暂时状态。被客户调用的COM+对 象方法总是以一种连续的方式完成 它们的工作,因 此 ,暂时状态不需要特殊地处理。这是一种非持久状态的形式,因为它不受持 久存储机制的保护。 2 . 绑定对象的状态 绑定对象的状态包含了那些绑定于对象寿命的状态。不幸的 是 ,如果你打算设计一个应用 程序平台,用于支持成千的公司用户或上万的Web用 户 ,那么你不可能很好地在内存中为每个用 户维护对象。这时,标准的对象属性通常会被改变为半持久状态。一般而言,对象的状态比客 户的调用持久,而在COM+中 ,对 象 的 状 态甚至比 实 际 的 对 象 (不是指逻辑上 的 对 象 )还要持 久。COM+可以调度多个不同的实际对象,以一个逻辑对象的形式为客户的请求提供服务。在 客户使用逻辑对象的时候,我们不想丢失任何信息,但 是 ,在大多数情况下,我们实际上也不 会把这些信息永远保存在数据库中。 3.持久状态 持久状态是你将要保存很长时间的信息,一般要经历应用程序的几次执行。在许多情况下, 持久状态就是那些你不能丢失的数据。通 常 ,它是一种你在数据库中谨慎维护、在事务中进行 修改并且箔要经常备份的信息。 1 6 . 5 . 2 状态存储 很 明显,为了减少服务器上的资源数量,你应该尽量减少组件的状态。然 而 ,不是所有对 象都可以无状态地运行。所以,如果你真的需要创建既可以维护状态又要求可扩展性的组件, 那么你必须选择一种用于存储你的持久和半持久状态的机制。下面的几部分将讨论用于状态存 储的各种方法以及它们各自的利弊。相互之间不断作用的主要因素是维护状态的负担和传送状 态的负担。如果你需要使用状态,那么你的选择是保持它或者从存储它的地方得到它。 1.客户端暂时存储 客户应用程序运行于客户的计算机上,这是客户/服务器方案的重大优势之一。增加一个客 户 ,增加另一个客户处理器来承担这个负荷。如果你正在试图减少你所拥有的相对较少的服务 器上的负担,那么让客户留意对象可能需要的任何状态将很有意义。客户端的Web页可以维护内 存变童或者标准的HTML参 数 ,并保存需要的状态。传统的客户程序可以创建内存结构,用于管 理必要的信息。这种数据可以被传回COM+对 象 。这 种 方 法 对 于 管 理 对 象 的 (半 持 久 )状态很 有 效 。客户端暂时存储的优势在于,这种方法将状态的维护负担分散给客户。客户越多,管理 客户状态的客户计算机就越多。而 且 ,当一个客户程序崩溃时,它只丟失了自己的状态。而这 个方法的缺点是,你必须在每一个方法调用中传递必要的状态数据。这将导致过多的系统开销 和额外的客户程序代码,而且这种方法在有些情况下甚至不可行。 2.客户端长期存储316 第三部分组件管理与事务 客户系统通常不在公司数据中心的保护范围之内,因 此 ,这些系统很少接受* 要数据的存 储任务。然 而,其他不重要的状态信息可以被长期地存储在客户端H•算 机上 。状态可以以Web cookie.注册表设置或者磁盘文件等形式被存储。客户端长期存储与客户端暂时存储存在相同的 利与弊。 3 .服务器端暂时存储 这 里,你 可能 想知道如 何管理组件的必要状态,才能够不降低已经负担很重的网络速度, 并且不需要重写所有的客户程序代码。如果你只想在对象的两次激活之间维护某些客户指定的 对象状态,那么MTS共 享属性管 理 器 (SPM)也许可以帮你。SPM是一个高速的属性数据仓库, 被设计用于维护共享对象的半持久状态。逬 然 ,你可以设计自己的系统用来获取状态,但是要 想超越SPM所提供的特性是不容易的。在 本 章 中 ,你将创建一些组件,其中之一将演示SPM的 使用方法。换句话说 就 是 ,你 可以 创 建 一 个 有 状 态 的 对 象 ,使COM+必 须 把 它保持在内存中 , 直到这个对象的客户释放它或者对象本身要求被取消激活为止。 4 .服务器端长期存储 上述所有的状态管理方案,都不能满足大型.企业级应用程序所需的严格的使用条件和巨大 的存储能力。多年来 ,各个公司都将重要信息置于数据库中。幸运 的 是 ,COM+已经将数据i方 问服务集成到它的运行环境中。COM+支持可以池化的资源,如 数 据 库 连 接 (降低了数据库总 的连接数量,改 善 f 数 据访 问)。复杂的数据存储方式(如数 据库)也可以用于MTS的事务过程, 提供稳定的数据操作服务,以避免不一致的持久状态(例如,被部分提交的事务)。数据库还支 持来自网络内不同主机的分布式数据访问c 有状态的对象和SPM是基于每个服务程序进程来处 理实例的数据的,不允许共享状态被用于多个服务器。当 然,总是会顾此失彼的,而在使用数 据库的情况下,失去的就是性能,因为数据库是最昂贵的数据存储方式之一。 正如你看到的,每个上述的状态存储方式都有它自己的一席之地,到底哪一个方法对你最 为合适完全取决于你的应用程序。 16.6 COM+组件必备的条件 现在,我们已经讨论过COM+的状态管理问题,接下来讨论一下更为复杂的COM+组件必备 的条件。下面列出了COM+组件必备的八个强制条件: •COM+组件要求标准类对象必须具有标准的IClassFactory接 U 。 因为COM+为了提高应用 程序的性能,要对对象的寿命迸行控制,所以可扩展性组件不能使用非常规的对象创建技 术。记 住 ,COM+侦听所有的对象创建请求,并且可以进行多次提高性能的简化操作。 只 有COM+直接调用COM+对象的类工厂。 • COM+S?要可以为每次调用返回一个新对象的IClassFactory::CreateInstance()方 法 。 COM+ 会 在 自 己 有 需 求 的时候创建对象。不能准确地为 每 次 调 用 生 成 一 个 新 对 象 的 CreatelnstanceO方 法 ,会破坏COM+所使用的寿命管理机制。Singleton ( 由类工厂生成, 它只创建一个对象,然后反复地返回指向这个对象的接口指针)是被明确禁用的。 •COM+组件必须在具有DUGetClassObject()例 程 (返回类的对 象)的DLL中被实现,COM+ 通过这个DLL例程获得对组件类工厂的初始访问权。第7 6章 作为组件管理器的COM+ 317 • COM+组件需要对标准的COM引用进行计数。如果在每次调用标准的AddRefO和ReleaseO 例程的时候,不能使引用计数器加一或者减一,那么COM+将无法管理对象的寿命。 •COM+组件不能与非COM+对象放在一起。因为COM+为了支持它的某些高级特性,需要 侦听方法调用过程,所以,一个逻辑对象的所有组成部分必须完全在COM+的管理底层结 构之内或者之外。这样可以防止子对象直接使用指向COM+对象的接口指针。 • COM+组件必须支持完整的DllRegisterServerO实现。COM+管理系统会对应用程序内的组 件进行下至方法级的管理。可以使COM+有效地进行这些管理任务的惟一方法是,通过调 用DllRegisterServerO,直接访问ProgID、CLSID、接口以及TypeLib信息c •COM+组件必须使用标准调度或者类型库调度。COM+不支持自定义调度,也永远不会调 用组件 的IMarshal接 口 。COM 的01eaiU32.dll可 以 通 过 使 用 类 型 库 信 息 ,调度只使用 automation类 型 (tType ILibrary调 度 )的接口。使用MIDL生成的代理/占位DLL (标准调 度 )的接口,也可以在COM+中正确地运行。 • 使用标准调度的组件必须使用3.00.44或以上版本的MlDL.exe编译它们的IDL,命令行参数 使用/O ic f,而且需要把mtxil.lib作为搜索列表中的第一个库链接到它们的代理占位DLL中。 只有使用标准调度的组件需要注意这一点。 正如你看到的,大多数ATL组件以及所有的VB和VJ组件在结构上都满足COM+的要求。真 正的诀窍是,保证你的设计和实现代码遵循COM+必需的并且强烈推荐的结构原则c 1 6 . 7 编写C 0M +组件 现 在 ,我们已经讨论了设计COM+所必需的重要条件,接下来让我们研究一下实现的具体细 节。这里有一*条好消息—— C0M+ API只有两个函数:SafeRefO和GetContextObject()。当 然, 这两个函数之一将返问一个COM接口指针,使你可以调用另外儿个方法,而这些方法中乂有某 些方法可以返回更多的接口指针,以此类推。 在开发你的第一个COM+组件之前,你需要先熟悉一下可以用于COM+的各种对象和接口。 下面一部分将讨论IObjectContext接 口 (它是由C0M+实现的最重要的接口)和IObjectContro丨接 口 (它是惟一一个COM+指定 的 、可以由组件自己实现的接口)的特性。 1 6 . 7 . 1 环境对象 环境可以使你找到自己的位置,而 a 可以使两次相同的操作得出不同的结果。你能从时速 50英里的汽车中取走车上的螺丝刀吗?这取决于环境。在下雨吗?我是在汽车里还是在摩托车 上?我的轮胎是新的Dunlop 207吗? 无状态组件可以很好地运行,但有时这些组件也需要一点环境,告诉谁在调用以及为什么 调用它们,或者事务是否在运行。因 此 ,C0M+要为每个COM+对象维护一个环境对象。图16-2 描述了 C0M+对象与它们的环境对象之间的关系。 COM+环境对象是不透明的,可以通过丨ObjectContext接口中的方法访问它们。对象可以通 过调用MTS的GetObjectContextO函数,获得指向它的环境对象的IObjectContext接口的接口指针。 表 】6-1给出了 IObjectContext接口中方法的列表。318 第三部分组件管理与事务 客户 活动连接 客户 未激活的连接 客户 活动连接 COM+戍用程序服务器 图16-2环境对象 表 16-1 丨 ObjectContext 方法 方 法 作 用 Creatclnstance() 在当前环境中创建一个对象 DisablcCommitO 在不连续的状态下声明一个对象的亊务更新(对象通过力•法调II!保留其状态) EnableCommit() 在连续的状态下声明一个对象的事务更新(对象通过方法调用保留饵状态) IsCallcrlnRoleO 标识对象的调用者足否赴特定的角色 IsInTransactionO 标识对象是否在事务中执行 IsSecuriiyEnableO 标识安全忭是否经激活 SetAbort() 终止当前事务(返回时对象可能被取消激活> SetComplete() 通知对象的亊务史新吋以被提交(返回时对象可能被取消激活) 1 6 .7 .2 对象控制 如果你想使用COM+的准时激活和及早取消激活特性,会怎么办呢?这 样做,你就是同意在 方法调用之间可以删除对象。如果你想通过让COM+把你的对象分配给当前正在请求服务的客 户这一方法,使 用 对 象池,又会怎样呢? 以上两个方案都会对对象的状态产生严電的影响。删 除对象就包括删除它的状态,而重新分配对象会将一个用户的对象状态传给另一个用户。为了 在这种混乱中正确地操作对象,它们必须是无状态的,或者必须给它们提供某种保存和重新获 取萆要状态信息的方法。 IObjectControl接口由COM+定义,并由COM+组件实现。COM+会在对象中奄找IObjectControl 接 口 ,如果发现,COM+将在对象生命期中的适当时候调用lObjectContro丨的三个方法。表 16-2 描述了 IObjectControl的三个方法。 表 16-2 lObjectContro丨方法 方 法 作 用 Activate() 在当前环境对象中其他方法被调用之前,当对象被激活时,被COM+所调用 CanBePoolcdO 被COM+调用以决定对象是否可重用 Deaclivate() 在当前环境对象中邦他方法被调用之后,当对象取消激活时,被COM+所调用系/ 6 章 作为组件管理器的COM+ 319 也许你还记得,在本章开始阶段,当我们通过调用SetCompleteO使能及早取消激活特性时, Stateful对象在两次方法调用之间丢失了它的状态。但是,正如你看到的,IObjectControl接口可 以解决这个问题。现 在 ,你有了一种方法,它支持及早取消激活和准时激活,并且不会丢失状 态。ActivateO调用使你可以为客户获得任何必需的状态,而DeactivateO调用使你可以保存将来 客户调用所必需的任何对象状态。你可能会问,为什么类的构造函数和析构函数不能满足要求。 有 两 点 原 因 ,第 一 ,某 些 操 作 (如 访 问 对 象 的 环 境 )在对 象 创 建完成 之 前 不 能发 生。 lObjectControl中的方法通过在构造之后和析构之前进行操作,回避了这个问题。第 二 ,如果对 象支持对象池,那么它可以被复用,而不必被删除和重建。 从CanBePooledO返回TRUE,将通知COM+你的对象支持对象池。这使COM+可以只维护一 个对象池,在客户请求服务时把对象快速地分配给客户,而不会产生用于对象创建和刪除的系 统开销。MTS 2以及以前版本实际上不支持对象池,但是它们仍然会调用IObjectComrol接 口 , 好像它们在为将来的兼容性做准备。COM+真正实现了对象池。如果你希望得到最佳的可扩展 性 ,那么你就应该尝试编写支持及早取消激活、准时激活和对象池的组件。 只有进行认真的设 计和测试,才能找到适用于给定组件的最优解决方案。 如果没有环境信息,那么ActivateO调用将毫无价值,你将无法了解谁在调用以及你需要恢 复哪些状态来执行任务。因 此 ,在大多 数组件 的设计 中 ,对ActivateO方 法 的 调 用 ,都是在从 COM+获 取了环 境信息(使用GetObjectComextO ) 以及环境指定的状态信息之后进行的。 1 6 .7 .3 使用ATL编写COM+组件 ATL使创建COM+组件变得容易。本书的所有COM+示 例 ,都是使用VC++ 6.0以及与之相关 的ATL内容编写的。让我们创建一个基本的ATL D L L ,你可以使用它来完成后面的几个示例。 虽然有关创建COM+组件的内容不是很多, 但ATL COM App Wizard还 是 为 你处理r 一些细节问 题。VC++ 6.0可以创建MTS组件 ,但不能创建C0M+组件。因此,我们下一步将要做的是创建 一个MTS组 件 ,它将运行于COM4•环境中。 首 先,在一个MTS组件内重新创建Stateful DLL和它的Sum类。随着本章的继续,你在前面 的试验中遇到的每个问题都将迎刃而解。下面创建MTS组件项目。从VC++主菜单中选择File, New,ATL COM App Wizard。输入你的项目名称和目录,然后在第一个出现的向导对话框中, 选择MTS support复选框。MTS组件永远是DLL形 式 ,它使MTS可以提供一个完整的运行环境。 如果你仍然使用前面例子中的项目名和类名,那么你将不必创建新的C0M +应用程序或者修改 客户端应用程序。 在为标准ATL DLL项目生成的文件中,DSP文件是惟一需要修改的文件。MTS组件将被链 接到MTS支持的库文件mtx.Hb和mtxguid.lib中。还有一点值得注意的是,VC 6.0引入了一种新的 性能特 性,它 使 你 的 程 序 可 以延 迟(准时 )载入指定的DLL。这可以使应用程序所占内存达到 最 少 ,并加速程序的初始化过程。ATL就是以这种方式链接到mtxex.dll ( MTS执 行 程 序 )的 ( 使用/delay丨oad参 数 )。在遇到/delayload参数时,需要使用帮助函数载入DLL。你可以使用自己 的帮助函数,或者直接链接到delayimp.lib中预先提供的例程,像ATL所做的一样。 接下来,向这个项目中添加一个新的对象。在Class View窗口中,右键单击这个项目的图标,320 第三部分组件管理与事务 并在弹出菜单中选杼New ATL Object0 接着,选择MS Transaction Server Component类 型 ,单击 Next。 为 你 的类输 入 一 个 名称(如Sum),然后在ATL Object Wizard Properties对话框中单击 MTS选项 卡,进行MTS的设置。 选择Support for IObjectControl, ATL Wizard将为所有的IObjectControl方法添加接口 支持和 启动代码。选择Can Be Pooled, ATL将生成一个返回TRUE的CanBePooled()方法。 在 创 建 了 这 个 类 之 后 ,你 会 发 现 ,ATL添加了一个智能指针属性,用于引用MTS的 IObjectContext接口。ATL在Activate()方法中添加了初始化这个指针的代码,并在Deactivate()方 法中释放这个指针。生成的代码如下: HRESULT CSum: •• Activate() { HRESULT hr : GetObj ectContext(&m_spObj ectContext); if (SUCCEEDED(hr)) return S_OK; return hr; BOOL CSum::CanBePooled() { return TRUE; > void CSum::Oeactivate() { * m_spObjectContext.Release(); > 添加前一个示例中的ISum::Add()和ISum::AccumulateO方法。在你的新类屮,你可以不必使 用SetCompleteO函数的大段代码,取而代之的是通过对象环境智能指针直接调用SetCompleteO。 Activate()和DeactivateO方法保证所有 其 他 的 方 法都可 以 自由地访 问你 的MTS环境对象。 STDMETHODIMP CSum::Accumulate(int iValue, int *piTotal) { if ( NULL == piTotal ) return E_INVALIDARG ; m—iTotal +* iValue; •piTotal = m_iTotal; //Tell MTS that our work is complete m_spObjectContext.>SetComplete(); //Return success return SJ)K; > 测试你的新COM+组件,功能和问题将与你前面遇到的相同。 虽 然你 现 在仍然会 在 两 次 调 用 之 间丢失状 态 ,但 是对 象 已经具备了儿种保存和 重新 获得 mJTotal状态的方法。Activate()方法为你的对象提供了一种恢复状态的方法,同时丨ObjectContext 接口提供的环境信息使你可以识别正在进行调用的客户的身份。同样,DeactivateO方法可以在释第/ 6 聿 作为组件管理器的COM+ 321 放之前被用于保存对象的状态。也就是说,mJTotal可以被保存,并在需要的时候被重新获得。 这个新的多用户COM+对象还需要保证所有的用户都可以访问nUTotal。使用MTS共享属性管理 器 ,可以轻松地解决这个问题。 1 6 .7 .4 共享属性管理器 在多个对象和激活之间保存半持久状态是一个很普遍的需求。因 此 ,MTS提供了一个名为 共 享属性管 理 器(SPM ) 的资源分配器。 每个MTS服务程序进程最多例示一次共享属性管理器,在它的控制下,将使这个进程内所 有的对象都可以访问共享状态。SPM提供了并发管理,这保证了共享状态不会因为同时来自多 个对象的读写操作而遭到破坏。共享厲性以命名组的形式进行管理,这将减少发生名称冲突的 可能性。在需要用到共享状态的时候,以往的应用程序经常会使用全局变量。SPM解决了与全 局变景有关的两个最大的问题:汸 问的 控 制 和全局名字空间 的“污染”。有关SPM的详细内容, 参见第15章。 1. SPM的缺点 在使用SPM设计组件时,需要着重注意几件事情。最重要的是,每个MTS服务程序进程只例 示一次SPM,这意味着,任何保存在SPM中的属性,对SPM所在的服务程序进程内的所有组件都 可用,而对这个进程之外的所有组件都不可用。因此,在两个都使用SPM的组件之间共亨•某些属 性 ,是不明智的。如果系统管理员在一个服务程序进程(应用程序)中重新配置了另一个进程中 的组件,那么这狴共享属性将不再被共享,同时每个服务程序进程将维护它S 己 的 - 套厲性数据。. 使客户程序可以连接运行于不同计算机上的同类对象的负荷均衡方案,也存在相同的问题。当你 需要使用应用程序范围的共享状态时,一个适当的数据库将是最合理的解决方案。 2. SPM的接口 使用SPM的组件需要包含mtxspm.h头文件。这个头文件定义了三个接口,用于访问SPM资源: • ISharedPropertyGroupManager • ISharedPropertyGroup • ISharedProperty ISharedPropertyGroupManager接口提供了对最髙层次SPM行为的访问,包括命名属性组的 创 建 、恢复以 及枚举。淸理SPM 的惟一方法是,关闭这个SPM 所 在的 服 务程 序进 程 。 ISharedPropertyGroup接口提供的方法,使你可以在已引用的组内部创建并获得属性。 ISharedProperty接口允许像变量一样设置和获取属性的值。 3 .给组件添加SPM特性 现 在 ,你可以使用SPM升级你的COM+组 件了。 以前,Statefu丨组件在一个厲性中维护对象 的状态,这种方法存在许多问题。首 先 ,无法在多个对象(可能在你的分布式多用户应用程序 中 运 行 的 对 象 )之间共享这个信息。这一点是好是坏取决于设计,但 是 ,对于我们这个例子来 说 ,我们的假设是,你希望所有的用户都可以累加mJTotal值 。正如前面讨论的,我们总是轻率 地假设,所有的客户会使用单独的COM+服务程序进程。 一 Dm 一 iTotal对于COM+进程中的所有对象都可用,你便引人了同步问题。决不能允许多个322 第三部分组件管理与事务 对象同时访问m一iTotal。你还需要保证mJTotal不会与共享 名 字 空间中的其他属性名冲突。最后 一点就是,为了使你的组件可扩展性更好,你已经牺牲了两次调用之间的属性保存。 SPM通过提供具有内置多线程访问同步服务的命名组属性存储机制,解决了以上所有的问 题。如果使用SPM保存总数 值 ,那么新的无状态Sum类将不再需要状态属性,也就是不再需要 m」Totalr> 清单 16-5给出了新的Sum::Accumulate()例程。 清单16-5 SPM实例 STDMETHODIMP CSum::Accumulate(int iValue, int *piTotal) { if ( NULL == piTotal ) return E_INVALIDARG; //SPM, Group and Property interface pointers ISharedPropertyGroupManager * pSPGM = NULL; ISharedPropertyGroup * pGroup = NULL; ISharedProperty * pProp = NULL; //Get interface pointer to SPM (n_spObjectContext->CreateInstance( CLSID^SharedPropertyGroupManager, IID_ISharedPropertyGroupManager, (void**) &pSPGM ); if ( NULL == pSPGM ) return E FAIL; //Create or open the property group BSTR bstrGroupName = SysAllocString( L“SumProperties“); long UsoMode = LockMethod; long IRelMode = Process; VARIANT_B00L fExists = VARIANT_FALSE ; pSPGM->CreatePropertyGroup( bstrGroupName, &lIsoMode, &lRelMode, &fExists, &pGroup ); SysFreeSt ring(bstrGroupName); pSPGM->Release(); if ( NULL == pGroup ) return E_FAIL; //Open the property BSTR bstrPropName - SysAllocString(L“Total''); pGroup->CreateProperty( bstrPropName, &f Exists, &pProp); SysFreeString(bstrPropName); pGroup*>Release(); if ( NULL == pProp ) return E一 FAIL; //Update the total弟/ 6 章 作为组件管理器的COM+ 323 VARIANT vtTotal; vtTotal.vt = VT_I4 ; vtTotal.lVal = 0 ; pProp->get_Value(AvtTotal); vtTotal.lVal += iValue; ♦piTotal = vtTotal.lVal; pProp->put_Value(vtTotal); pProp->ReleasG(); //Tell MTS that our work is complete m spObj ectContext•>SetComplete(); //Return success return S一OK; } 4 .共享属性组管理器 使用SPM时 ,通常有三个步骤。第一步是得到一个指向ISharedPropertyGroupManager接口的 接口指针。虽然在创建SPM时使用CoCreatelnstanceO是完全可以接受的,但是本例中使用的是环 境对象的CreatelnstanceO方法。在SPM中两者都可以使用。有关IContextObject:: Createlnstance() 的细节将在后面详细地进行讨论。不管你怎样得到接口 ISharedPropertyGroup Manager的接口指 针 ,每个进程内只能有一个SPM的实例被创建。 5. SPM属性组 下一步是打开感兴趣的组。每个SPM组都有一个独一无二的名称。组 的 名 称 很 像 的 名 字 空 间 ,这些名称把全局空间划分为各个被命名的组。你的组被命名为SumPmperties。类似用于 访问SPM的CreatelnstanceO调 用 ,CreatePropertyGroupO将 创 建 一 个 组 (如果 它 不 存 在 )并且打 开 这 个 组 (如果 它 存 在 ),简化了你的程序逻辑。你的这个组的主要任务就是维护总数值,即使 在你的调用完成以及你的对象被取消激活之后,也要继续维护。释放方式标志Process意味着你 的组需要在服务程序进程的生命期内- 直被维护,而另一个标志Standard则意味着在所有的未完 成的组接n 指针被释放之后,可以马上释放你的组。 6. SPM属性 最后一步是,打开并更新属性。SPM属性与其他变董没什么不同。属性更新代码需要通过 两次函数调用对属性进行访问,首先读取属性,然后用更新值修改属性。如果在这两次调用之 间允许其他对象修改这个属性,那么将造成信息丢失。为了解决这个问题,祢可以以 LockMethod隔离方式打开你的组,在方法调用期间,这种方式可以在组的层次上锁住SPM,使 你在读写属性的过程中独占访问权。另一种方式是LockSetGet,它可以通过在属性级锁住SPM, 来减少组件之间对于厲性组的竞争,但是它只对连续的get和put调用过程有效。 注意,你不能让SPM维护两个客户之间对话的状态,虽然它可能是在一个很忙的、不会轻 易造成服务程序进程超时的系统中。晚 上 、周 末 、进程崩溃以及任何其他可能的情况都会造成 服务程序进程和SPM的终止。SPM的确不是一种持久的数据存储方式。需要长期保存的或者需 要在几个服务程序之间共享的状态信息,应该保存在数据库中或者使用某种可靠的分布式数据 存储方式。324 第三部分组件管理与事务 1 6 . 7 . 5 在COM+ 内引用对象 在本章开始部分中,你的非COM+组件显示出很差的可扩展性,但是它可以正常地运行。不 幸的 是 ,许多标准COM组件实际上不能在COM+环境中 运行 ,失败的主要原因是,COM+要在 其运行环境中管理对象的寿命,而标准COM组件的实现又允许客户控制对象的寿命,这当然是 有冲突的。在COM+服务程序进程中创建的每个对象,都 拥 有 一 个 代 理 对 象 (充当实际对象的 前 端 )和一 个环 境 对 象 (维护与调用客户和调用本身有关的信息)。这使COM+可 以侦听客户的 调 用 (尤其是那些对IUnknown接 口 的 调 用 ),然后有选择地授权那些必要的调用。在对象支持 及早取消激活的情况中,COM+甚至可以在客户结束使用对象之前,释放对象。这意味着客户 实际上连接的是COM+的代理对象,而不是实际对象本身。 如果你让一个客户直接引用你的对象,然后COM+在这个对象的当前操作完成之后删除了它, 那么这时会发生什么呢?你先仔细考虑一下。OK,停止思考。结 果 是 ,虽然对象被刪除了,但 是 ,你背着COM+交出的访问权却仍然在客户手中!这意味着,你 千 万 不 要 (重复 一遍,千万 不 要 )将COM+对 象 的访 问 权返 回 给当前 对 象环 境以外的 客 户 。这个问题的解决方法是使用 COM+提供的 API 函数 SafeRefO: void* SafeRef ( REFIID riid UNKNOWN* punk ); SafeRefO函数使对象可以生成一个 接口指针,而把这个指针返回给当前对象环境以外的客 户是安全的。不这样做将使客户可以直接对对象进行COM/DCOM访 问 ,这 将破 坏 大多 数 (或者 全 部 )COM+的机制,导致难以应付的周面。SafeRef()工作起来很像QuerylnterfaceO函数,它得 到你需要的接口的ID, 以及你需要的对象的IUnknown接 口 ,最后返回一个指向那个接口的安全 的接口指针。与正常引用一样,安全引用也必须被释放。需 要 注意的 是 ,对一个安全引用进行 的QuerylnterfaceO调用和SafeRefO调用总是返回另一个安全引用。惟一可能发生的严重问题是, 两个不完全相同的IUnknown接口指 针,实际上可以指向COM+对象方法调用环境中的同-•个对 象 (一个指向安全引用,一个指向实际对象>。 1 6 .7 .6 在COM+内创建对象 非COM+组件的另一个重要弱点是,它们经常使用CoCreatelnstanceO函数创建一些不能在 COM+程序包中运行的对象。 当然,这些对象是在注册表配置的地方运行。这样做可能没什么 问题,也可能很糟糕。通常悄况下,在一个方法调用进人COM+环境之后,如果想要保持COM+ 的各种优越性和原则,这个方法调用应该保持不变。COM+ 不能控制运行于程序包环境之外的 组件 。你可能会遇到需要外部对象的设计方案,如 果是这 样,你必须保证遵循环境外的调用原 则以及其他需要考虑的事项。 那么如何在当前环境中创建新对象呢?这个问题的答案当然是使用环境对象。接 n lObjectContext提供了一个CreatelnstanceO方 法 ,除了它是在当前环境中创建对象这、-点之外, 它与CoCreate丨nstanceO函数十分相似。新对象将继承当前的活动(控制多个COM+服务程序的逻 辑 线 程 )、安全性设置甚至当前运行的事务。在第17章 中 ,我们将讨论接口ITransactionComextEx, 它也提供了 一个CreatelnstanceO方法。使用丨TransactionContextEx接 口 中的Create丨nstanceO方 法 ,第/ 6 聿 作为组件管理器的COM+ 325 与使用IObjectContext接口中的CreatcInstanceO方 法 很 相 似 (只不过前者使创建者可以控制新对 象的事务环境)。 1 6 . 8 小结 在本章中开发的示例应用程序充其量也就是个很平常的程序。难道你不感到高兴吗?你需 要的是创建一个高度可扩展的无状态COM+组 件 ,而它就这么简单。这正是COM+的全部内涵。 由你 编 写商业逻辑,由COM+处 理 髙 端 问 题 、主框架形 式的性能问 题以 及稳定性 问 题。如果 COM+很难使 用 ,那么它将没有任何价值。真正的技巧是留意有关状态的内容。在本章中你学 到的最重要的东西就是状态、行为和接口分离的概念,以及COM+系统的可扩展性原则。本章 还讨论了有关基本COM+组件开发的主要概念,以及与IObjectContext接口和IObjectControl接口 有关的实现问题。现 在 ,你已经掌握了设计髙性能MTS组件的方法,第 17章将讨论对高度可靠 的状态管理和稳定的应用程序而言十分关键的事务。第17章作为事务协调器的COM+ 本章包含的内容: •对事务的需求 • MS DTC ( 微软的分布式事务协调器) • 一 个简单的亊务例子 •事务协议 • COM+事务编程模型 •传播事务例子 •监视事务 •设计考虑 transaction ( 事 务 )或commitment control ( 提交控 制 )是指定义一套作为独立工作单元之间 的逻辑操作关系的能力。这意味着,如果一个操作失败了,那么所有操作完成的工作都将随着 一个简单的函数调用而前功尽弃,或者全部回滚。执行事务是计算机时代必须面对的最古老的 挑 战了 。绝大多数发生于企业级应用的、实际的以服务器为中心的操作需要事务保护。最 初 , 事务技术只用在大型计算机领域,但是随着关键应用转移到工作站并最终转向个人计算机平台, 事务技术也做好了准备。实 际 上 ,对许多关键应用来说,事 务至关重要 ,它使得没有事务支持 的平台不能运行这样的应用程序。 COM+提供事务服务类似丁•传统大型计算机事务进程处理过程,以及UNIX工作站中的一样。 COM+的美妙和与众不同之处在于它处理框架结构的组件特性。COM+是基于COM构建 的 ,所 提供的事物使得构造基于组件的企业应用成为梦想。在第16章 “作为组件管理器的COM+” 中 , 讨论了可升级COM+的一些特性,以及分布式组件的运行时环境。本章,你将通过事务服务使 用来学习COM+提供的大型计算机结构级的特性,这些特性完全是可靠的。 1 7 . 1 对事务的需求 你也许会怀疑究竟人们如何从COM+的使用中受益。回答这个问题最好是直接看一个实际的 例子。 设想你计划去堪萨斯旅行。那么在旅行这段时间里你需要机票、预定旅馆和出租车。当 然, 在你预定这些服务的时候,你需要使用信用卡来支付。你打电话给航空公司,并使用信用卡来 支付所需的费用。然后你打电话给出租车运营公司,选择你所喜欢的汽车,当然,这个时候你 也使用信用卡。但是当你打电话预定旅馆时,却发现旅馆在你要去旅行的那段时间已经没有房 间了。 这时你就不得不将去堪萨斯旅行的时间往后推一天了,在你再打完两个电话之后,飞机票第/ 7 章 作为事务协调器的COM + 327 和出租车的时间已经更改,以和你预定旅馆的时间相吻合。如果上面的这些工作能够一步完成, 三个预定可以顺利完成,那将是多么美妙的事情。 另一个办法是让航空代理帮助你处理所有工作。你需要做的只是打一个电话,然后支付所 需的费用。他们将告诉你在预定旅馆时所遇到的问题。当你选择旅行计划时,所有的事情应该 都已经OK了 ,旅行社将为你安排好所需的旅馆和出租车。 COM+事务可以使你的应用程序具备我刚才所描述的那种能力。旅行预定系统就是分布式支 持事务的分布式应用使其最有效的例子。旅行社可以将旅客的信息输入计算机然后再提交订单。 如果订单中的某一项,例如旅馆的房间不符合订单中的要求,那么订单中的其他部分都将取消。 这些信息都将通知给航空公司和出租车运营公司,告知计划已经改变。 有许多其他的复杂问题可以用事务来帮助解决。设想旅行社使用一个应用访问三台不同的服 务器。如果在定单到达时,出租车预定代理的服务器关机了,那么会发生什么事情。这意味着机 票预定和旅馆预定在没有出租车运行公司的确认下就完成了。当服务器当机的信息返回给航空公 荀的时候,也许在预定机票和旅馆的那段时间里,已经没有出租车可以使用了。如果旅行社具备 使用事务的应用程序,那么当机的服务器就不可能不给出相关的信息,事务也不可能被提交。 继续MTS的脚步 ,COM+给了开发者能够处理棘手事务的能力。本章将帮助你在你的应用 中使用这些优点。 1 7 .1 .1 定义的事务 通 常 ,事务是原子操作,只有当操作的所有部分都已经顺利完成时,整个操作才可能成功。 对于成功的事务来说,任何对已经存在的数据的改动都将是永久的。而不成功的事务将冋滚以 将原来存储的数据返回到以前的状态。这种对数据修改的事务处理方法帮助开发者保证存储的 数据的一致性。这意味着不会有异常事情发生。如果账户平衡数据库管瑰不允许出现负值,除 非贷方数据库有相应的信用记录,你必须确信只有合法的数据才能使用。如果你想将一个账户 的资金取出,存入另一个账户,那么你需要保证操作都发生或都没有发生。事务使得所有这些 改变可以原子地执行。 事务通常分成两个部分被提交(不要与本章后讨论的两段事务混淆)。在第一 部分 ,每个组 成部分表决提交还是终止事务。尽管数据库服务器是最通常的事务组成部分,但是实际上任何事 务相关软件都可以参与亊务。如果各个部分表决终止事务,事务将回滚到第二部分,所有已经存 储的数据状态都将回到事务处理发生以前的状态。如果决定提交事务,事务将提交到第二部分。 这时还不意味着操作就已经彻底完成;这只是说明各个组成部分迟早要被提交以完成事务。 迄 今为止,我已经介绍了事务的一般概念。事务就像计算机的存在一样,只冇数十年,但 是微软的分布式事务协调器(Microsoft Distributed Transaction Coordinator, MS DTC ) 作为工 具已经从事务基础构架转移到Windows操作系统了。我们将简单地讨论谈论MS DTC, 然后我们 学 习 资 源管 理 器 (它管理事务的大部分资源)。 17.1.2 ACID 事务具有几个完整的因素,称为ACID属性:328 第三部分组件管理与事务 •原子性 Atomicity • 一致性 Consistency •隔离性 Isolation •持久性 Durability Atomicity属性指的是事务的所有元素都是不可分割的单元(就像原子一样是最小的组成部 分 )。事 务 要 么 “全有” ,要 么 “全无” 。在一包含三个数据库表格的更新事务中,表格要么全部 被更新,要么都不更新。 Consistency属性使得存储的数据保持稳定。所有数据操作都不能被其他的并发操作所破坏, 也不能破坏其他并发操作的数据。这 样 ,你写人数据库时将不会触及到系统里的其他对象。 事务操作是与数据库的其他用户隔离的(isolated),或者是独立的。因此,没有別的系统元 素可以浏览部分的事务。他们只能看见当前事务以前或者以后的状态。这意味着没有任何对象 可以从另一个对象的事务更新的表中得到记录。 最后 ,事 务 是 持 久 的 (durable),这指当-一个事务被提交后,操作也将完成。一个事务消息 队列服务器在网络故障的时候也许会需要几个小时来发送消息。当 然,这时消息队列服务器已 经被提交以发送事务消息。 17.2 MS DTC MS DTC是微软的分布式事务协调器。它与SQL Server 6.5作为管理分布式事务的工具一起 推出,其中包含了几个SQL数据库。这 样 ,MS DTC在Windows平台上就起到了处理(TP > 监视 器的作用。DTC创建支持ITransaction接口的事务对象,表示每一个新的分布式事务。其他一些 接口支持两段提交方法,允许资源管理人员表决事务的所有操作是否成功。某个资源管理人员 的失败将导致整个事务的终止,这时DTC将通知其他资源管理器事务已经终止。如果所有的资 源管理人员都同意提交事务, DTC将提交分布式事务,并且再一次通知各个相关的部分。 COM+组件是与分布式事务管理器的复杂性不相关的,大部 分情况下,从不需要DTC直接调用 各种接口。实 际上 ,COM+提供自动的DTC事 务创 建活动, 自动的资源管理器DTC事务机构和 -个 简 单 的 IObjectContext接 口 ,该接口被已提交的对象或者终止事务的操作所使用c IObjectContext 本小节主要介绍IObjectContext事务特性。在第18章将重点介绍安全特性和IObjectContext接 口。丨ObjectContext的可用方法大部分是事务相关的。表 17-1列出了IObjectContext的方法。 表 17-1 丨 ObjectContext 的方法 H 法 作 川 CreatelnstanceO 在内前范围内创逑对象 DisablcCommilO 在不一致的状态下卢明事务的更新(对象跨力法调用W*丨保恃其状态i EnableCommit() 住相丨fij的状态下声明唞务的更新(对象跨方法调用时保持其状态) IsCallerlnRoleO 说明对象的调用者足否是特定的角色第/ 7 章 作为事务协调器的COM+ 329 ( 续 ) h 法 作州 IsInTransaclionO 说明对象是否正在事务中执行 IsSecurityEnablcdO 说明安全件是否被允许 SetAbort() 退出3 前 的 事 务 (对象处于未激活状态直到返回) SetCompleteO 声明对象的枣务更新可以提交(对象处于未激活状态& 到 返 问 ) CreatelnstanceO方 法提 供了 在MTS中构造新对象的首选方法c 当使用丨ObjectContext:: CreatelnstanceO时 ,新创建的对象需要或者支持事务继承当前对象c 调用CoCreatelnstanceO方法 创建对象不会向新对象传递当前的环境信息。因为这个原因,应该明确只有创建当前COM+活 动外的对象时才使用CoCreatelnstanceO方法。 DisableCommitO是一个有用的调用,它激活与安全相关的方法,以保护突发的事务提交行 为。例 如 ,当一个客户在事务提交超时时释放了对象,而某个方法已经提交了事务,这个方法 开始了一 个 事 务 操 作 ,并且有意或者无意地在操作完成之前就返回给客户c 对象已经关闭,但 是方法却依然要提交它。DisabaleCommitO将使事务失效,并且只有在明确地调用SetComp丨ete() 方法时才被提交。这 样 ,客户就不会释放对象,直到事务失效,MTS将毫无办法 ,只能终止服 务。客户将使用另外一个方法调用对象以完成事务,这时可以调用Enab丨eCommitO以使事务有 效 ;或者简单地调用SetCompleteO,它不考虑被取消的调用和提交的事务。 EnableCommitO使事务重新有效,它使事务隐式完成。这是新发布的事务的缺省状态。我并 不认识你,但是在我的关键任务服务组件中,你对我有用。使在每个内部方法调用顶部的事务 一直到事务被SetCompleteO隐式提交时失效,已经使我受益匪浅。 SetCompleteO是当客户因为在事务中执行了各种不同的操作并且将改变提交给资源管理人 员而感到高兴时被对象所使用的方法。调用SetCompleteO的对象方法应该返回S一0 K 以表示操作 的成功。不幸的是,调用SetCompleteO恰恰是在对象表决是否完成事务或者其他资源管理人员 表决准备终止亊务时 , 事务彻底失败了。在这种情况下,COM+使用CONTEXT一E一ABORTED 代替根对象方法的返回值(如果它是正确代码的话)。当然,对MTS来 说 , SetCompleteO只是一 个 信号,说明对象可能已经失效。强健的对象不应该调用SetCompleteO,除非它们可以被安全 地销毁。 SetAbortO是被需要退出当前事务的对象所使用的方法。这是一个重要的调用,它确保对事 务的修改能够回滚给所有资源管理器。对象决定退出当前事务的表决可以返回任何值给客户应 用 ,而且通常在返回后就立即失效。 IsInTransactionO只是依赖于当前的环境屮是否有活动的事务而返回Tme或者False。在需要 事务但是又不存在的情况下,往往需要手动退出,这时就可以使用IsInTransactionO了 。 17.3 —个简单的事务例子 为了使你更好地理解我上面所讲的内容,也为了能使你在脑海中对怎样在你的COM+代码中 实现事务有一个清晰的认识,我编写了下面这个小的例子程序。我尽量使它简单,以便你能理 解与事务相关的最重要的东西。330 第三部分组件管理与事务 下面的组件使用Visual C~m■中的ATL所编写,而客户程序则用Visual Basic所编写。它们都可 以在本书所附光盘的目录“Chapter 17\SimpleTrans” 中找到。 清 单 17-1给出了COM+组 成部分中比较重要的代码,而清 单17-2给出了为客户应用编写的 Visual Basic代码。清单的后面是程序的说明。 清单17-1 —个非常简单的事务组件 // First.cpp : Implementation of CFirst ^include *stdafx.h* ^include BSimpleTrans.hB #include “First.h_ ////“/////////////////////////////////////////////////////////////////////// II CFirst STDMETHODIMP CFirst: : DoIt{) { AFX_MANAGE_STATE(AfxGetStaticModuleState()) II Get the object context. 10bjectContext *pObjectContext = NULL; HRESULT hRes = GetObjectContext( ApObjectContext ); II Make sure we didn't get an error while getting II the object context, if( FAILED( hRes )) if( hRes == E一INVALIDARG ) m_strTransactionResult = _Invalid argument.*; else If( hRes == E_UNEXPECTED ) m_strTransactionResult = 'Unexpected error.■; else if( hRes == CONTEXT_E_NOCONTEXT ) m_strTransactionResult = “No context associated with object.“; else m_strTransactionResult = “Object context could not be obtained.“; return( hRes ); II Here we'll decide to be successful based on the system time. II If it is an even minute, we'll be succcessful. Otherwise II the transaction will abort. // Get the system time. SYSTEMTIME S t ; GetSystemTime( &st ); II Even if( ( st.wMinute & 1 ) == 0 ) { II Success. pObjectContext->SetComplete(); m_strTransactionResult = “Transaction completed.“;第77章 作为事务协调器的COM* 331 } II Odd else { II Failure and we abort. pObjectContext->SetAbort(); m_strTransactionResult = “Transaction aborted.'; } II Release the object context. pObjectContext*>Release(); return S一OK; } STDMETHODIMP CFirst::get_TransactionResult(BSTR *pVal) { AFX_MANAGE_STATE{AfxGetStaticModuleState()) II Return the string that alerts caller to the // transaction result. •pVal = mstrTransactionResult.AllocSysString(); return S—OK; 清单1 7 - 2 使用简单的事务组件的Visual Basic客户应用 Private Sub Command1 胃 Click(} Label2.Caption = “Executing Transaction“ Dim obj As Object Set obj = CreateObject(“SimpleTrans.First.1•) Res = obj.DoIt() Label2.Caption = “Result of DoIt(>:_ & obj.TransactionResult Set obj = Nothing End Sub 组件 中 使 用了 一个具有DoUO方 法 的CFirst类 。在Do【t()方法中发生的第一件事是调用了 GetObjectContextO。必须获得对象环境来提交(使用SetComplete()) 或 者 终 止 (使用SetAbort()) 事务。 接下来是错误检查代码,以确保IObjectContext指针无错误地被取得。我们不应该在NULL 或者非法的丨ObjectContext指针上执行任何操作0 在 本 例 中 ,提交还是终止取决于系统时间。如果返回的分钟是偶数,那么事务成功提交; 如果返回的是奇数,则事务失败。332 第三部分组件管理与事务 为了提交事务,仅需一个简中.的SetCompleteO调用。为了退出,使用SetAbortO调 用 。调用 ReleaseO方法释放IObjectContext,该方法最后将返回给调用者。 Visual Basic程 序 (清单17-2)非常好理 解 。它非常简单地演示了组件的使用。它先调用 Dolt()方 法 ,然后将结果赋给窗体上的一个标签。 这里有一点非常值得注意,为了使程序能够运行,在COM+的目录中组件必须作为应用的一 部 分 ,它的属性也必须设置正确3 将在后面的章节中讲述怎样设置COM+组件的属性。 17.4 事务协议 是 的 ,事务这个概念已经出现了很长一段时间,现在已经被多种平台独立地支持。这直接 导致了事情的复杂化。COM+程序环境支持许多不同的事务协议,这就是本章所要讨论的。 17.4.1 OLE 事务 微软如今几乎将所有的服务和体系结构都建立在COM基础之上。因此也就成为了OLE事务 标准的基础。OLE事务对于MS DTC、SQL Server和微软几乎所有其他事务产品是本机的。 17.4.2 XA 事务 X/Open UN丨X标准化组织升级了它的XA事务标准。XA事务被广泛地用在许多分布式环境 中 ,尤其是在UNIX中。因为这是当今大的数据库公司(如Oracle、Informix、Sybase等 等 )首选 的事务机制,因此在任何即将被企业使用的产品中塀弃XA事务都将是大的灾难。这 样DTC为 XA事务提供了支持。 这就像你所猜测的那样。由于微软更加倾向于集成和全面地测试SQL Server,而不是Oracle 8 , 因此XA事务配置可以完成一些相关的工作。试图在COM+系统中使用XA事 务 之 前 ,必须检 查拥有版本号的COM+帮助系统,以确保拥有正确的驱动程序修订版。TestOraclcXAConfig.exe 行命令可以为你检查正确的Oracle数据库事务提供支持。访问你的数据库厂商和微软站点得到最 新支持信息。 17.4.3 CICS和丨MS事务 现 在 ,我们将进入另外一个领域。不仅主框架支持独一无二的事务协议,而且也可以运行 于一个完全不同的网络结构中。它们确 实 存 在,因此你不能对此视而不见。由于其公认的可靠 性 ,主框架理所当然地成为了事务处理中的中心。如果允许在传统的基于Web的客户程序中调用 运行在COM+中间层服务程序中的组件,那将是—件非常美好的事情。幸运 的 是 ,DTC支持LU 6.2 Sync的级别2 , 并且在SNA Server和COMTI的帮助下,事务可以分布到大型丨丨•箅机环境中。 在第19章 ,我们将结合COM+讨论CICS和IMS事务的特性。 17.5 COM+事务编程模型 COM+事务编程模型中最重要的一点在于大多数的事情都是自动的,并且在代码中相当容易 修改。COM+非常复杂的服务现在开始变得容易使用了,然 而 ,各种规则依然保持不变,不管第 尺 章 作 为 事 务 协 调 器 的 C O M + 333 稈序设计任务多么简单。实 际 上 ,建立一个比较差的COM+组件非常简单。就 此 而言,应该认 真学习本章末有关程序设计的部分。 当构造组件时,需要考虑的基本的设计前提是,事务是非常昂贵的。尽量避开它们。如果 无 法避免事 务 ,那么尽可能使它小。为不断扩展的时间预定昂贵的资源只会增加事务的开销。 这意味着你必须尽可能快地完成事务。 下面是一个典型的事务序列过程: 1)客户调用事务COM+对象方法。 2) COM+接收调用并且分配一个与对象环境相联系的DTC事 务 。接着将 调 用 传递给 对象 , 如果需要,将创建并激活对象。 3 ) 对象的方法执行相应的行为,以激活其他对象的眼务和资源管理器,这些在事务中是自 动创建的。 4 ) 评估各种相关函数的结果,然后提交或 者终止事 务 (资源管理器将保留或者回滚事务的 改 变 )。 5 )对象将状态返回给客户,然后COM+将取消激活对象。 如图17-1所 示 ,上面这个过程在很大的程度上证明了基于组件事务的强大特性。事务中包含 的每个对象都以很好封装的方式完成了各自的任务。调用组件不需要关于所调用对象实现的详 细描述。被调用的对象为资源管理器更新了事务,这些操作是自动完成的。 Q 动的事务传播跟 踪了事务每个细节的成功和失败,而不管该事务下一级对象的任何行为。 DTC亊务 COM+服务器进程 资源管理器 图 1 7 -1 基于组件的事务 1 7 . 5 . 1 创建事务 在COM+中创建事务经常是一个声明的过程。你可以用过程方式生成事务,但是这通常不是 必须 的。事务通常由COM+以相同的方法自动创建,也就是事务自动地从对象传播到对象,或 者从对象传播到资源管理器。一 般地 ,唯一需要手动的地方是最后的一步,也就是选择提交还 是终止事务。 这些工作有时会在后台完成,取决于此时所讨论的资源分配器(Resource Dispenser, RD )。 环境对象存储激 活的事务参考 客户 ( 觸 彻334 第三部分组件管理与事务 —些RD将它们的功能隐藏在其自己的API中 (ODBC只是在后台增加了对某些功能的支持,例如 调用::SQLDriverConnect、::SQLConnect等 ),其他RD则需要事务清单中明确地列出。 1.自动事务 OBDmmilMBiBBHr3 上JS1 在一个事务对象调用后的任何 时 间 ,都可以创 建或者传播自动事务。唯一的例外是当事务对象调 用了一个以上的方法时。既然这 样 ,连续的调用将 使用已存在的事务。通过组建 管 理 控制 器,可以配 置对象以使用组件。只需右击当前类然后选择属性 即可。类厲性对话框的事务表显示了五个事务设置 选 项 (如图17-2所 示 ): • Disabled • Not Supported • Supported • Required • Requires New 2. Disabled 图17-2类事务设罝 该选项取消了从不访问资源管理器的有关事务处理的总开销。它模拟未配置的COM对象的 处理行为。 3. Not Supported 该选项是任何安装在COM+应用程序中的标准COM对象的默认设置。这种类型的对象在其 环境中从不维持一个事务处理;它们也不传递任何先前事务给其创建的对象。这种类型的对象 通常是遗留的组件,而不是为COM+特别设计的。 4. Supported 事务处理设置Supported是为毫无规律的类型准备的,它不是真的关心这足否进行事务处理, 但是如果需要它可以传递一个事务处理到下一个对象。如果一个客户机调用的该类型对象具有 一个事务,那么当前对象的环境就临时管理客户机的事务处理。在这种方式下,虽然当前对象 本身对事务处理确实没有兴趣,但是当前对象可以传递事务给别的对象,这些对象也是它创建 的并且需要事务处理。如果一个带有这种设S 的对象是由非事务客户机创建的,那么当前的对 象也是非事务性的。 5. Required 到目前为止最通用的设置是Required。该设 置 保 证所 有函数 调 用 (外部mnknown ) 运行于 事务处理保护环境之下。如果调用当前对象的那个对象已经有了一个事务,那么当前对象就继 承那个已经存在的事务处理。这 实 际 上总括f 在同一个事务处理中,直接或间接从同一个事务 中的基本事务对象中调用的所有不同类型对象。 6. Requires New 事务处理设置Requires New用于一种大部分运行在其单独事务的组件。例 如 ,如果你为一个 毛线衫工厂创建了一个OrderEntry ( 订 货 登 录 )对象,下一个订单的额外工作可能是在供货量减 TlMtCto^Tft TiwUWI第77章 作为事务协调器的COM+ 335 少的时候你得定购新的箱子。你需要一个事务处理来处理你的箱子定货,但你不希望仅仅因为 你的箱子定货存在问题就使已有的顾客订货失败。这个方案应该通过创建一个独立的类型来管 理 ,该 类 型 控 制 箱 子 定 货 并 设 置 为 需 要 一 个 新 的 事 务 处 理 。现 在 ,当OrderEinry对象调用 BoxOrder对 象 的 时 候 ,OrderEntry对象的处理不会传递给BoxOrder对 象 。MTS给BoxOrder对象 提供一种新的事务处理。 7. ITransactionContextEx TransactionContextEx接口提供了TransactionContextEx对象的三个外部函数的访问。也许听 到所有TnmsactionComextEx对象都需要新事务处理并不会让你感到奇怪。它给客户机和服务器 对 象 ,同时提供一个容易的方法来分配事务处理的空间,或者在没有事务处理时创建一个。使 用ITransactionContextEx接 口 时 ,必须包含txctx.h头文件。表 17-2列出 了ITransactionContextEx 接口的方法。 表 17-2 ITransactionContextEx方法 方法 功能 Aborl() 回滚从当前函数中返回的当前亊务中执行的所有事务处理的运行工作 Com m it() 试图提交从当前函数中返冋的当前亊务中执行的所有亊务处理的运行工作。( 如 果在事务处理中,任何对象调用SetAbort()或 者DisableCommiK > . 该箏务处理将 会 终 止 ) Createlnstance() 在TransactionContcxlEx对象的枣务处理中 创 建 一 个新的对 象( 如果创逑的对象 类制不支持事务处理,该对象仍然会被创逮,但 它 不 会继承该亊务 ) 一个非事务对象可以很容易构造一个TransactionContextEx对 象 (然后创建一个事务处理),然 后用TransactionContextEx接口的Createlnstance()函数创建新事务处理中的其余对象。这是一个重 要的特性,因为一个无事务的对象不能直接创建一个需要事务的对象。使用TransactionComextEx 对 象 ,通过ITransactionContextEx::CreateInstance()函数,一个客户机可以在同一个事务处理中, 创建一对事务处理需要的对象,保存和检查对象。保护两个对象之间的传递的单个事务处理成为 可能。在程序清单17-3中,你可以发现一个TransactionContextEx对象创建函数调用的示例。 清单17-3 TransactionContextEx对象创建示例 #include ITransactionContextEx * pTransactionContext; CoCreatelnstance( CLSID TransactionContextEx, NULL厂 CLSCTX_INPROC, IID_ITransactionContextEx, (void**)&pTransactionContext); ITransactionContextEx接口的Commit()和Abort()函数,仅仅是使客户机选中进行提交或者 回滚已完成任务的事务处理的结果。所有这三个方法实际上跟[ObjectContext接口的三个方法 SelAbort( ) 、SetComplete( >和Createlnstance()只有微小的差异。 8. IDL336 第 三 部 分 组件管理与事务 在创建确实可扩展且可再用的软件组件过程中,建立一个1DL文件描述你的接口,是一个很 有用的工具。IDL提供一个实施独立的环境,在其中程序员被其接n 的优点所吸引。这是很重要 的 ,因为在分布式COM+系 统中 ,接口是最有约束力的部分。一个C+ + 组件开发员不能避免 IDL编码过程。ATL和各种其他工具包产生大量现代COM +接 U 定义代码,怛是程序开发员还是 将调整接口和手动编码单元这种非常奥秘的任务留给次级向导去管理。在1DL世界中,如果更多 的系统开始软件构造进程,许多分布式对象模型的清晰度将会大大地提高。 COM +包含IDL,并且广泛应用在不同Component Management Console ( 组件管理控制台) 组件显示树中显示信息。COM +也为1DL增加了新的合并类属性。你可以用这些COM+的特定《 性为COM+类型指定事务处理要求。下面的列表给出了组件可用的COM+事务处理属性: • TRANSACTION_REQUIRED • TRANSACTION 一 REQUIRES_NEW • TRANSACTION-SUPPORTED 类的默认事务值是Not Supported, 并且可以通过前面说过的域性明确地指定另外三个值中 的任意一个。这些值定义在mtxattr.h头文件中,在使用事务处理标志之前必须在你的丨DL中包含 该头文件。这些事务处理类型库属性的最大益处体现为,当你的组件被添加到软件包中时, Component Management Console ( 组件 管 理控 制 台)按照默认的值使用它们c 作为一个程序幵 发 员,你或许很明白你的组件具有什么样的事务处理要求。而另一方面,系统管理员可能明白 或 者 不 明白。 当你增加 一 个带有 事 务处理属性的类 型到 软 件 包 时,Component Management Console ( 组件 管 理 控 制 台 )自动地提供特定类型库的事务处理设置。 如果COM+管理员知道他 们在做什么,可以将事务处理属性更改为他们喜欢的任何类型,将类型库值仅仅当作一个有帮 助的默认设置。因此没有理由不用合适的默认事务处理值标id 每个COM+特定的事务处理组件。 清单17-4显示了OrderEntry组件中的IDL代 码 ,该组件使客户在一个事务处理中可以下订单。 清单17-4 OrderEntry组件中的IDL代码 import “oaidl.idl“; import •ocidl.idl*; ^include ( object, uuid(E8482A00-A2F6-11D2-A512-00600893FB20), dual, helpstring{“lMTSOrder Interface“), pointer_default(unique) J interface IMTSOrder : IDispatch { (id(1), helpstring(“method PlaceOrder“)] HRESULT PlaceOrder( long AccountNutnber, long ProductID, long Quantity); UUid(E84829F4.A2F6-11D2-A512•006O0893FB20), ve「sion(i.0),^ J 7 t 作为事务协调器的COM+ 337 helpstring(“OrderEntry 1.0 Type Library“) 1 library ORDERENTRYLib { importlib( stdole32.tlbB); importlibCstdolea.tlb“); [ Uliid (E8482A01 • A2F6 • 11D2 • A512 • 00600893FB20) helpstring(“MTSOrder Class“), TRANSACTION_REQUIRED coclass MTSOrder { (default] interface IMTSOrder; 1 7 .5 .2 完成事务处理 事务处理组件必须淸楚地提交或者中止一个未完成的事务处理。不能成功完成该工作将使 事 务处理落空,如果在事务处理中客户机释放的对象超时,它将进行提交。如果对象没有被释 放而且事务处理超时,那么事务处理将被中止。让事情不确定对关键任务服务组件是一个不适 当的行为,并且因为这个原因,所有正确设计的事务处理组件清楚地提交或者中止其事务处理。 记住一个事务处理中涉及的所有对象在事务处理完成的时候都被撤消了。事务处理通过对象的 Obj ectContext 进行管理。 1. ObjectContext 每个COM+对象都有一个ObjectContext与其联系。ObjectContext维持与对象有关的暗含的 COM+状态信息。这里特别有趣的事实是ObjectContext跟踪有关当前事务处理的信息,如果有 的话。一个COM+对象可以通过调用COM+ API兩数GetObjectContext()使用ObjectContext,返回 一个 IObjectContext 接 口 指针。 一个对象的ObjectContext,在对象的构造器或者析构程序中,或者在任何IUnknown函数调 用 过 程 中 ,应该从不被访问。通 常 ,这是因为COM+既没有创建该环境也不会在这些对象的调 用过程中消除它。注意调用一个对象的IUnknown函数没有激活ObjectContext。ObjectContext索 引 (指向IObjectContext的 接 口指 针)不会传递给别的对象,因为它们在自己的对象环境外非法。 2 .再访IObjectContext 清单17-5是一个OrderEntry对象的唯一一个事务函数的示例。 清单17-5 OrderEntry对象的P丨aceOrder方法 STDMETHODIMP CMTSOrder::PlaceOrder( long AccountNumber, long ProductID, long Quantity) { //Ensure that we have a transaction if ( I m_spObj ectContext *>IslnTransaction()) return CONTEXT E TMNOTAVAILABLE;MB ,338 第三部分组件管理与事务 //Disable commit to keep an accidental II return in the body of our code from II committing the transaction on client II object shutdown m_spObjectContext•>DisableCommit(); //Compute total price and deduct account balance within II the current transaction (all sweaters are $20) double lfTotalPrice = Quantity * 20.0; HRESULT hr = ChargeAccount( AccountNumber, lfTotalPrice ); if ( FAILED(hr)) { //Abort transaction and return error m_spObjectContext->SetAbort(); return hr; //Reduce our inventory within the current transaction hr = ReduceInventory( Quantity ); if ( FAILED(hr)) { //Abort transaction and return error m_spObjectContext->SetAbort(); return hr; } //Commit transaction and return success raspObjectContext*>SetComplete(); return S—OK; } — 该程序段中有几个有意思的地方。其 中最重 要的是,除了程序上端的检查和保护程序设计 方 面 ,以及SetCoinPlete( )或SetAbort ( )函数依赖的返回值外,该程序代码缺乏隐蔽的事务处理 指令。注意ChargeAccount()和ReduceInventory( )这两个例程可以直接修改一个数据库或者创建 —个新的组件并让它完成这个工作。这 有 一点 小问题,因为事务处理自动地传送给任何新对象 或者支持事务处理的资源管理器。本书所带CD-ROM上的完整例子简单地用OLE DB来模拟SQL 服务器数据库中的两个表。注意这不是什么问题,如果你在记账,然后发现你到了账号的外面, 因为当Reducelnvemoi^ )返回一个失败的代码时事务处理直接中止了。该程序返回正在讨论的 账号在试图进行更新处理之前的状态。 因为你不想尝试这种没有事务处理的工作方式,例程中的第一行代码确认有一个可用的事 务处理。如果你发现自己没有事务处理,返回MTS代 码 ,指 出 事 务 处 理 管 理 器 (TM) 不可用。 注意下面一点很重要,如果在MTS外部运行,你依赖的获取并释放IObjectContext指针的 IObjectControl方法一直没有调用,导致在你试图使用ATL-provided m_spObjectCoiUext的时候都 出错。在使用前检查IObjectContext指针是一个不错的主意,特别是当你在MTS和直接COM+之 间连续切换使用组件时。 示范的例程立即禁止默示的事务处理提交。这使你确切地知道,不管发生什么,只要你没 有调用SetC0mplete( ) , 事务处理就中止。这是采用事务处理的一般思想,虽然不一定永远都是。 最后 ,如果SetAbort()函数的作用幸存下来,你假设所有事情工作正常,并且调用SetCompleteO第17章作为事务协调器的COM+ 339 函数来提交事务。 这 里 ,客户 机 收 到 下 面 三 个 值 中 的 一 个 :S_OK、CONTEXTJE_ABORTED或 者别的 值 。 “别的值” 让你感到迷惑,不是吗?这是事务的一个不太好的事实—— 任何事情都有可能失败。 如果所有的东西都正 常,客户机得到S.OK。如 果 事 务处理 不 能 被提交,客户机得到 CONTEXTJE_ABORTED或者一个应用程序定义的错误。但是,如果不返回以上两者中的任何 值 ,在你的DCOM调用中网络却失败了,客户机得到有关网络的HRESULT。因此事务处理不可 能提交,而且客户机也收到一个模糊的COM通信错误。如果这不能令你满意,你将必须提出一 个特定的应用程序方法,让客户机找到形成这种失败的事务处理调用的原因。这话没有什么其 他意 思 ,就跟它所表达的一样。如 果 可以 的话,最好的方法就是让客户机不关注事务结果。你 通常不必担心函数是否完成工作,并且所有函数处于一个大的事务处理块中。 前面的例子用了两个子例程,Reducelnventory()和ChargeAccount()。两者都利用COM+兼 容 资 源管 理 器 (该情况下是SQL Server)来完成其函数功能。 1 7 . 6 旅行社实例 现在可以看一个实例了,它模拟前面章节中给出的旅行社实例。我已经创建了一个称为类 CSubmitOrder的 组 件 (见清单17-6)。该类检查旅馆、航空公司和汽车的预定是否办好了,然后 将信息写入数据库中。 一个Visual Basic程 序 (清单17-7中 )是一个使用该组件的客户机应用程序。 在列出的程序代码后面,有程序的解释和说明。 、 清单17-6扮演旅行社职员工作的CSubmitOrder类 II SubmitOrder.cpp : Implementation of CSubmitOrder #include “stdafx.h“ #include “Travel.h* ^include “SubmitOrden.h“ #include *dboAirlineReservations.h“ #include 'dboCarReservations.h* #include _ dboHotelReservations.h■ ///////////////////////////////////////////////////////////////////////////// II CSubmitOrder STDMETHODIMP CSubmitOrder::0olt() { AFX_MANAGE_STATE(AfxGetStaticModuleState(>) II Get the object context. IObjectContext *pObjectContext = NULL; HRESULT hRes = GetObjectContext( &pObjectContext ); II Make sure we didn't get an error wtiile getting II the object context. if( FAILED( hRes ))340 第三部分组件管理与事务 if( hRes == E^INVALIDARG ) m_strStatus = “Invalid argument.*; else I f ( hRes == E—UNEXPECTED ) ra_strStatus - 'Unexpected error.“; else I f ( hRes == CONTEXT_E_NOCONTEXT ) m_strStatus = 'No context associated with object.•; else m_strStatus = “Object context could not be obtained.•; return( hRes ); > II Check and store the hotel information below. CdboHotelReservations Hotel; // Check to make sure the hotel reservations are valid, if( IHotelReservationOK()) { ni_strstatiis = “Problem with hotel reservations.'; pObjectContext*>SetAbort(); return( S_0K ); // Attempt to open the Hotel table. hRes * Hotel.0pen(); if( FAILED( hRes )) { m_strStatus = “Hotel did not open.“; pObj ectContext->SetAbort(); return( hRes ); II Store the information into the Hotel II class and then update the database. strcpy( Hotel.m_Date, m_strStartDate ); strcpy( Hotel.m_Type, m_strHotelType ); strcpy( Hotel.m_Name, mstrCustomerName ); Hotel.SetData(); Hotel.Insert(); II Check and store the car information below. CdboCarReservations Car; // Check to make sure the Car reservations are valid, if( !CarReservationOK()) { m_strStatus = 'Problem with Car reservations.“; pObjectContext•>SetAbort(); return( S_0K ); > II Attempt to open the Car table. hRes s Car.0pen(); if( FAILED( hRes )) { m_strStatus = “Car did not open.“; pObj ectContext->SetAbort();第17章作为事务协调器的COM+ 341 return( hRes ); II Store the information into the Car // class and then update the database. strcpy( Car.m_Date, m^strStartOate ); strcpy( Car.m_Type, m_strCarType ); strcpy( Car.m_Name, m_strCustomerName ); Car.SetData(); Car.Insert(); II Check and store the airline information below. CdboAirlineReservations Airline; II Check to make sure the Airline reservations are valid, if( !AirlineReservationOK<)) { m_strStatus = “Problem with Airline reservations.“; pObjectContext->SetAbort(); return( S_0K ); // Attempt to open the Airline table. hRes = Airline.Open(); if( FAILED( hRes )) { m_strStatus = “Airline did not open.“; pObj ectContext->SetAbort(); return( hRes ); // Store the information into the Airline // class and then update the database. strcpy( Airline.m_Date, m_strStartDate ); strcpy( Airline.m_Type, m_strAirlineType ); strcpy( Airline.m_Name, m_strCustomerName ); Airline.SetData(); Airline.Insert(); m_strStatus = 'Transaction completed.“; II Commit the transaction. pObjectContext->SetComplete(); II Release the object context. pObjectContext•>Release(); return S_OK; } BOOL CSubmitOrder::CarReservationOK( void ) II Perform checks here to make sure the II car reservation is OK. II This method always returns true for theII purposes of this example, return( TRUE ); > BOOL CSubmitOrder::AirlineReservationOK( void ) II Perform checks here to make sure the II airline reservation is OK. II This method always returns true for the II purposes of this example. return( TRUE ); } BOOL CSubmitOrder::HotelReservationOK( void ) { II Perform checks here to make sure the II hotel reservation is OK. II This method always returns true for the // purposes of this example. return( TRUE ); . > STDMETHODIMP CSubmitOrder::put_CustomerName(BSTR newVal) { AFX_MANAGE_STATE(AfxGetStaticModuleState()) m_strCustomerName.SetSysString( AnewVal ); return S_0K; } STDMETHODIMP CSubmitOrder::put_StartDate(BSTR newVal) { 一 AFX_MANAGE_STATE(AfxGetStaticModuleState()) mstrStartDate.SetSysString( &newVal ); return S_0K; } STDMETHODIMP CSubmitOrder::put EndDate(BSTR newVal) { AFX_MANAGE__STATE(AfxGetStaticModuleState()) m_strEncJ0ate. SetSysString ( AnewVal ); 342 第三部分组件管理与事务_______________________ return S_OK;茗/ 7 章 作为事务协调器的COM+ 343 STDMETHODIMP CSubmitOrder::put_AirlineType(BSTR newVal) { AFX_MANAGE_STATE(AfxGetStaticModuleState()) m_strAirlineType.SetSysString( AnewVal ); return S_0K; > STDMETHODIMP CSubmitOrder::put_CarType(BSTR newVal) { AFX_MANAGE_STATE(AfxGetStaticModuleState()) m_strCarType.SetSysString( &newVal ); return S_OK; } STDMETHODIMP CSubmitOrder::put_HotelType(BSTR newVal) { AFX__MANAGE_STATE(AfxGetStaticModuleState()) m_strHotelType.SetSy$String( &newVal ); return S_OK; } STDMETHODIMP CSubmitOrder::get—Status(BSTR *pVal) { 一 AFX_MANAGE_STATE(AfxGetStaticModuleState()) *pVal = m_strStatus.AllocSysString(); return S_OK; } 清单1 7 - 7 设置对象属性的一部分不错的客户机程序代码 Private Sub Command1_Click() Label2.Caption = “Executing Transaction“ Dim obj As Object Set obj = CreateObj ect('Travel.SubmitOrder.1 “) ob) .CustomerName = *Rick Leinecker'* obj.HotelType = 'Suite' obj.AirlineType = “First Class“ obj.CarType = “Red Mustang Convertible“ obj.StartDate = _10/10/2001“ obj.EndDate = *12/15/2002' Res = obj.Oolt() Label2.Caption = “Result of DoIt()& obj.Status Set obj = Nothing End Sub344 第三部分组件管理与事务 在我创建该组件前,我增加了一个带有三个表的数据库到SQL Server 7.0中 。一个表记录旅 馆的 信息,一个记录航空公司的信息,还有一个记录汽车的租赁信息。为了简单二些,我创建 了一个ODBC连接到数据库。 访问数据库表最简单方法是插入ATL数据消费者对象到目标对象中。我娃这样做的,并创建 了三个类,称为CdboAirlineReservations、CdboHotelReservations和CdboCarReservations。然后, 这三个类也准备用在主组件类中。 在 本 章 前 面 的简单 实 例 中 已讲述,CSubmitOrder类在它做任何实际工作之前先获得一个 lOBjectContext指针。并 且 ,出错检查确认该指针的获取过程没有任何错误c 我 创 建 了 三个虚设的方法称为 AirlineReservationOK( ) 、HotelReservationOK( )和 CarReservationOK( )0 这三个方法都简单地返回TURE。在这三个方法中,虽然业务逻辑要求判 断三个事情是否组合传递,依据它们事务处理可以进行吗? 在它决定航空公司、汽车和旅社预定可以通过以后,每种信息都记录到各自的数据库表中。 最后,该方法调用SetComplete()方法,然后调用K>BjectContext::Release()方法。 1 7 . 7 监视事务 Component Management Console ( 组件 管 理 控 制 台 )提供丫事务监视功能,称为To Whom It May Concern,它帮助管理员和程序开发员跟踪事务处理的执行次数,以及在几台服务器之间 平衡 事 务 处理 的负 载(见图17-3)。事务列表图提供了运行于当前机器上的In-Doubt事务清单。 当处理结构的某些部分(如远程数据库)停止响应时,便产生了In-Dcmbt事务。你可以在事务处 理列表中用鼠标右击那些有问题的事务处理,以便操作处理它们。事务数据图显示了事务处理 信息各种重要片断,包括临界值如响应时间数值和中止计数器。 W f l U 1 ILL g B — m — B— iiBtf :_i成 :% fiomcti JtTdo*.神 _____ ___________________一 一 〜-_ COI^ CC0^ Jt M m U 5 卜 為 c c e u A ocA c事 » OS OUK-Of-^ocm Poobd i • BS U N b K 货 S U N T vi ► Srneni AepkeBor SluHo AK F«UJ99 JtmMOxr C^otdr 图 1 7 -3 事务统计数据图 1 7 . 8 设计中的考虑因素 好 了 ,我们已经看过了COM+亊务 。为了将其综合起来,看一些涉及到COM + 事务处理组弟7 7章 作为事务协调器的COM+ 345 件结构的关键性设计因素。 1 7 .8 .1 提出细粒度的组件 不要维持跨越方法调用的事务,在每个方法调用包含一•个事务的粒度下运行的对象,其执 行情况比粗粒度下运行的对象好。要求用几个客户机函数来完成一个产生对象的运行工作,其 伸缩性很差,因 为 事 务 资 源 (DTC事务对象、数据库连接、COM+对 象 状 态 ,等 等 )必须暂时 由COM+维J寺。 细粒度的解决方法不仅仅是高度可扩展性,同时也更容易调试。如果每个函数调用都依靠 自己,那么复杂的方法相互作用在调试期间会经常出问题。紧接着事务处理的完成,所有对象 状态被消除掉了,用来减少潜在的出错。每个方法应该从常数数据状态开始,以常数数据状态 结束。如果这种设置失败,就只需要修复单个函数的活动。 所 以 ,无状态的解决方法是最优的COM+组件实施方案。如果你需要使用几个函数来完成一 个运行工作,一个折中方案可以通过维持函数间的状态对象来获得,但是提交事务处理是以逐 个方法调用为基础的。例 如 ,试图创建一个非事务的、状态基准对象,它创建一个对象对每个 连续的函数调用都需要一个新的事务。这种方式下,COM + 给状态基准对象仅仅维持最小的内 存 ,以函数调用函数为基础,进行提交或者中止每个次级的对象的事务。 1 7 .8 .2 定位靠近其数据源的组件 资源管理器使用离得越近的服务器,那么它完成与资源管理器有关的事务处理就越快。如 果你配置数据存取组件,作为同一系统中软件包的数据库进行存取,你通常获得的有效性能超 过使用远程控制软件包。关键在于当出现大量的缺页而可能轻易地损害具有潜在性能优势的应 用程序时,保证提供足够的服务器内存配置。 1 7 .8 .3 在同一应用程序中将使用相同资源的组件放在一起 资源分配器以单个进程为基础统筹资源配置。这意味着运行于分散软件包中的组件不能够 共享同一资源。因为COM +在应用服务器的进程内,管理线程创建和并行性问题,所以扩展多 个组件不成问题,且也 能 通过 更 有效 地利用共 享的资 源(如数 据库连 接 )提高总体的性能。注 意这是与故障分离管理方式相反的管理运行方式。与单组件相比,在同一进程中组件越多,整 个进程出现单组件故障的可能性就越大。将组件分散到不同的应用程序中可以将故障分离,但 降低了组件间的通信和资源共享的能力。 1 7 . 9 小结 本章中,你学习了事务的性质,以及COM+ 系统是怎样与主机型事务处理服务组件技术结合 在一起的。虽然COM +事务处理程序设计语法是琐碎的,但是在设计强健的软件组件中,涉及 的关键概念是非常细致的,请仔细阅读本章的全部内容。第 18章 “COM+的安全性”,讲述了 COM+框架中的安全原则。第18章 COM+的安全性 本章主要内容: •COM +的安全概念 • 安全支持供应商接口(SSPI) •COM +的声明安全性 • COM+的过程安全性 本章你将学习到COM*i•安全性的核心内容。由于许多重要的多用户程序有大量的安全性需要 考 虑 ,所以COM+提供了强大而且简单的安全事务,这些事务使得系统用户和组可以被映射到 COM+核心的应用角色中。COM+事务中有许多对管理员和编程人员是有效的安全特性,从以前 的经验可以预测,COM+能够使安全性编程更加容易。 尽管用户在一台服务器上被授权,但是还是有许多特定的环节,在这些特定的环节中包括 了一些不能被基于本地或者域级授权所处理的网络资源。在企业应用程序中,一个用户或者一 台机器能够除了正常的登录授权外,合法地进入网络是很重要的。 18.1 COM+的安全概念 COM+的安全性是按照中间件、多用户以及企业规模组件系统的要求设计的,这是安全性考 虑的顶级设计 。你 已 经 在 一 端拥有 客 户环 境,这个客户环境可能包括从网络传来的黑客程序 H ackU Bill,而另一端是数据中心,它一般包括了你的重要的商务数据。在大部分电子商务的解 决方案中 ,仅将尚未完全适应非确定性公共消费的行为排除了;而另一些解决方法采用一种特 定的应用程序安全机制,仅使用用户标识符和PIN进入到他们的系统中。尽管这两种方案都有其 局限 性,但是如果你在一个安全的企业环境中,管理员已经花了几年时间开发了一个用户、组 以及安全资源的基础结构,那么将会有什么发生呢?我怀疑他们根本不会对这个离题的安全机 制有好感。. 因此在你要建立一个强大的应用程序安全系统时,一个巨大的挑战就是这个安全系统要足 够的灵活,以适应你的组件和满足企业中已经存在的记账系统的需要。 另一方面,实际的企业都拥有熟练的MIS 成员。COM+从开发者的角度考虑,使得开发一个 重要的组件应用程序容易多了,但是它给本地系统的管理员留下了许多新的问题。例 如 , COM+是一个基于组件的系统,而不是传统的整体应用环境。正如我们以前讨论的那样,这就 意味着管理员能够重新组织组件和应用程序,以适应它们在特定的层次上的性能需要。如果在 进 程 中 打乱组件的顺序 (当 然,这个进程在大多数系统中是有内建安全边界的),你的安全模式 又会发生什么事情呢?对于大多数管理员而言,这是一个非常新的也可以说是非常有趣的问題。 因此对于开发人员和管理员来说,COM+ 又一次提出了一堆的新事物要考虑以及大童的概念^ 1 8 t COM+的安全i}±____347 要掌握。还 是 像 以 前 一 样 ,一个强大而简单的接口在配置环境和编程领域中已经开始流行了。 现在我们看一看COM+安全模型。 •18.1.1 角色 角色是整个COM+安全构漳中最核今的概念,一个角色代表一种用法。例 如 ,一个承担店员 角色的人负责跟顾客结账,而一个承担i 计角色的人负责平衡账目。在Componem Management Console ( 组件管理控制台,CMC)中角色的图标是一打帽子。因为同一个人可以同时负贲顾客 付账和账目,因此,他可以承担两种角色或者说同时带两顶帽子。 角色是在应用程序到应用程序这样一个基础上被定义的,角色可以由管理员根据企业的需 要 来 定 义 ,也可以由程序开发人员根据系统的需要定义,当然也可以根据二者的需要来定义。 这样 的话,•程序开发人员可以在不知道大量的开发环境结构的情况下,确保程序的安全需求能 够被很好地满足。为了使用他们的计算结构编制程序网,管理人员分配给系统用户和组以不同 的角色,这些不同的角色又是为了某个应用程序而建立的。 角色由系统构造者合理地安排,并由配置组件的管理员配罝后,两个系统之间提供安全检 查。NT内建的安全性可以满足管理员配置安全性的要求。例 如 ,管理员可以修改一个对于所有 电话销售职员都无效的类接口。组件能够提供它们自己的内部逻辑性来控制访问。例 如 ,一个 程序可以允许除了DeleteO之外的所有例行程序调用,这样的话,调用者必须是一个管理员。 就程序而言,一个简单的函数就可以完成COM+大部分的安全性检测工作:IobjectContext:: IsCallerInRole( )0 COM+中保留着上下文数据,这些数据描述了每一个COM+对象调用者的角色。通过检测调 用 者的角色,组件就可以确定调用者是否应该被允许执行该任务或者应该在多大程度上扩展其 能力。 这个功能可以通过IObjectContext::IsCallerInRole()方法来实现,下面是调用的代码: IObjectContext objContext = null; objContext = MTX.GetObj ectContext(); II Find out if Security is enabled, if( objContext.IsSecurityEnabled()) { //Then find out if the caller is in the right role, if( 1obj Context.IsCallerlnRole( “Manager“)) { II If not, do something appropriate here. } } else { //If security's not enabled, do something appropriate here. } COM+通过限制角色的权限,简化了处理分布式对象环境时的复杂性,但是仍然有一些细微 的地方需要考虑。例如 ,如果一个客户Bob以管理员身份调用了一个运行于服务器进程中的对象, 那么很明显的是,Bob的账号将是角色检测过程的决定性因素。如果第一个COM+对象要调用另348 第三部分组件管理与事务 外一个需要特定角色的对象时,又会发生什么呢?回答是不肯定的。如果第二个对象运行于和 第一个对象相同的进程中时,客户的身份是要被用于角色检测的。如果第二个对象运行的进程 是独立于第一个进程对象的,那么第一个对象的进程身份将被用于角色检测。这是因为例行程 序IsCallerlnRolK )检测的是直接调用者的角色,不是原始调用者的角色,而该例行程序用于管 理员指定安全性的自动使用以及在程序中被程序开发人员调用。下面我们来看看良接调用者和 原始调用者。 1.直接调用者 在COM+的调用序列中,对象的直接调用者被定义为进入当前进程的进程的身份。因 此 ,如 果客户调用对象A ,该对象运行于服务器进程中,那么直接调用者是客户。如果对象A调用对象 B, 对象B接下来调用对象C ,而且A 、B 、C都运行在同一个进程中,那么对于所有三个对象而 言 ,直接调用者是客户。如果客户调用在某一个服务器进程中的对象A ,对象A调用运行于另外 一个服务器进程中的对象B ,那么A的直接调用者是客户的进程,B的直接调用者是对象A的服务 器进程。 在 第 12章 “COM安全性” 中,记住学习如何使用ISecurityProperty::GetDirectCallerSID( )和 ISecurityProperty::GetDirectCreatorSID()方法。它们让你能够确走直接调用者。 2 .原始调用者 原始调用者是一种进程的身份,该进程运行于COM+之外,调用到COM+中执行一定的操作。 应该记住的是,一个COM+的操作是指一个执行的逻辑线程,它通过运行于一个或者多个COM+ 进程中的一系列COM+对 象 ,从一个外部客户处跟踪调用序列。图18-1描述了调用者身份的关系。 从调用者角度而言 最初调用者 莨接调用者 的调用者的身份 图1 8 - 1 调用者身份 如图所示,图18-1中进程利的身份是关键。如果进程不允许调用对象 B ,你就会破坏安全 性 。在COM+安全控制机制下,要成功地构造服务器到服务器的通信的话,通过应用程序的属 性来设置应用程序的身份经常是一个强制性的步骤。 1 8 . 1 . 2 安全性的职责 正如我们所看到的一样,COM+的安全性包括两个基本元素,应 用 程 序 设 计 (包括角色模型 和 其 代 码 )以及企业账号结构。并且有管理员将系统账号映射到应闱程序角色中,而 a 可能在 类和 接U 级上定义安全需要。下面我们将看看两个基本的COM+安全性应用方法,声明安全性 和过程安全性。 • 声明安全性是COM+安全性的一部分,你可以通过管理的方法来定义。也就是说,管理员第18章 COM+ 的安全姓 349 安排用户和组账号到应用程序角色中。管理员也可以分配脔要的角色到类和接口上,用于 限定哪些角色能够使用被保护的元素。 • 过程安全性是通过组件代码,在编程过程中管理的安全性。一些应用程序可以毫无阻拦的 通过声明安全性。重要的是,要注意到过程安全性使得程序开发人员可以在子方法级别上 选择替代的逻辑性,而声明安全性仅仅用于接口级。 因为包括在COM+系统中的安全性职责按这种方法分开,因此对于程序开发者而言,他们的 角色模型和组件需求之间的通信是非常重要的。对于每个应用程序、组件和接U 而言,如果能够 提供带有角色定义和有帮助描述应用程序的话,你将会有一个好的开始。对于过程级的需要如果 没提供声明支持,就会破坏你的安全性操作。例 如 ,如果你的程序需要店员角色,而管理员不能 提供这样的角色,那么你的程序将不能运行。我将提出图书馆模型角色的定义,这个定义在配置 的过程中和一些组件协同工作,非常像IDL事务的属性,但实际上并没有这样一个事务。 在本地机或者域的用户验证有效地保护了资源,仅有被授权的用户才能访问它们。这可能 满足分布式计算机环境(Distributed Computing Environmem,DCE ),在这个环境中要通过不同 的计算机和拓扑结构来通信。因为这个原因,为了提供给DCE额外的 安全 性,要调用安全的远 程 过 程 调 用 ( Remote Procedure Call, RPC )Q COM+被设计和应用在Microsoft的远程过程调用的顶层,该执行过程支持独立创建的软件 对 象之间复杂的相互作用。当它重新使用远程过程调用的许多优点时,包 括验 证和 个 人 服 务 , COM+也增加了另外的安全层到远程过程调用的API上 。 1 8 . 2 安全支持供应商接口 对 于验 证、消息 整 合、消息私人化、分布式应用协议服务的安全质童而言,为了获得整体 的安全性服务,Microsoft的安全支持供应商接口( Microsoft Security Support Provider Interface, SSPI)很好地定义了通用API。应用协议开发者可以利用这个接口获得不同的安全服务,而不修 改协议本身。 SSP1在应用层和安全层之间提供一个抽象层。下面是一些使用SSP1服务的方法: •传统的基于Socket应用程序能够直接调用SSPI例 行 程 序 ,而且可以通过发送请求和响应消 息来使用传送SSPI安全相关数据的应用协议。 •COM+提供了集成的髙级安全特性。应用程序可以使用DCOM调用安全选项,该安全选项 利用验证RPC和SSPI在较低层实现。应用程序不直接调用SSPI的API。 •W inSOCk2.0扩展了Windows Socket的 接 口 ,使得传输提供商能够展示安全性。这种方式把 SSPI安全提供商整合到网络堆栈中,通过公用接口既提供了安全性服务,又提供传输服务。 Winlnet是一个支持Internet协议之上的安全协议的应用协议,例如Secure Socket Layer (安 全 套接 层,SSL)。Winlnet安 全 协 议 对 安 全 通 道 提 供 商 (Windows NT中称之为SSL)使用了 SSPI接口。 18.3 COM+声明安全性 将一堆无安全性的组件包装成一个高度安全性的程序是完全可能的,这正是声明安全性所350 第三部分组件管理与事务 要做的全部事情,同时通过CMC配置组件和接口的安全性。 声明安全性是通过COM+的 目 录 (使用组件 服 务 管理 器(Component Services Administrator, CSA))来实现的。COM+安全性也可以在编程中定义。然而为了处理激活和访问控制的定义, 你通常会因为多种原因,想在高于单一组件的层上定义。首先你想创建一个安全性环境, COM+可以自动为你处理这个环境。其 次 ,它可以大大减少那些与安全性相关的代码编写。第 三 ,因为COM+运行时负责实施安全性,所以确保设置的安全性会在系统范围内得到实施。 许 多 安全 性 设 置将 机 器作为 一 个整 体来处理 (例如 ,设 置这 么一 个缺省的验证和假冒层, 无论DCOM是否能被用于其他机器,无论CIS是否有 效 ,等 等 )。最好将这些设置放进目录的同 一个部分,然 后 进 入 到 目 录 的 “Defining Component Identity ( 定义组件身份)” 部分,在这个部 分可以定义不同的事情,如哪个用户能够执行应用程序等事情。 图 18-2阐明了在一个COM+应用程序中安全性的设计。每一个应用程序都有一个角色Roles, 在这个文件夹中管理员可以为应用程序创建角色。角色对于一个特定的应用程序和它的组件而 言 也 是 明 确 的 。组 件 和 组 件 接 口 之 间 具 有 Role Membership文件夹。添加一个角色到Role Membership文件夹中可以使那个角色中的调用者使用所涉及的元素。没有在成员角色中出现的 用户是不能访问COM+安全系统的。这就使得管理员在一个给定的应用程序中可以在接口级设 置角色和定义访问权限。 ;! ^ ^ |J<> i l t f 丨叫 M l aB CJ COH^ Acofec^boos aJ £ 々 COH^ueflbM 劣 ^ US Irv-Proc*w $ A u S U tftM B ^0lTct5ttfr e Cju m t i « ^ VHuaiStudtoAPCPackaoo i ; CHtrtxted Trtmactlon CoorcJnotor Tr ^TK^rtrtn 1 J HiL . ... ■ ___________ l JtJ SubfCrtpOorvs 1 ,i* i 图18-2 COM+应用程序角色 记住声明安全性设置也可以逐步地使用COM+的管理组件来配置。 注意当通过IDispatch来访问接口的时候,基于接口的声明安全性不是带有双接口的函 数。然而,类级安全性仍然是适用的。 1 8 .3 .1 创建角色 创建角色是很容易的,只要鼠标右击Role文件夹,然后选择New、Role ( 如图18-3所 示 )。^ 1 8 t COM+的安全性 351 ?- lPKTTfeU —時 Trunowtino I m# 图18-3创建角色 每一个角色对象都有一个User文件夹。你可以通过鼠标右击User文 件夹 ,选择New、User, 就可以将系统用户和组添加到角色中。 18.3.2 将角色加入到组件和接口中 —些应用程序都能管理自己的安全性,检査调用者在程序上是否合理;另一些应用程序可 以执行一些安全性检测,还有一些应用程序会将整个业务留给管理员。COM+管理员可以在组 件和组件接口上配置安全性限制。这些类型的元素都有Ro丨e Membership文件夹。当安全性被启 用 时 ,COM+仅仅使得在指定角色中的调用使用被保护的资源。 18.3.3 启用安全性 COM+不会执行应用程序安全性检测,除非你在应用程序中启用安全性。为了启用应用程序 安 全 性 ,只 要 在 “Application Properties” 窗 口 中 选 择 “Security“选 项 卡 ,然 后 单 击 “Enable access checks“选项。组件也是可以按照同样的方法来设置。对于COM+来 说 ,为了检测组件及 其接口的授权,你必须在一个组件上启动检测功能。 值得注意的是,声明安全性检测只在COM+服务器进程(应用程序)边界上执行。COM+应 用程序的内部代用是不会被COM+仔细审查的,尽管对象很自由地、程序化地执行所有它们所 需要的检测。由于库运行在调用者进程中,所以库应用程序的安全性检测总是不可用的。 为了使用COM+的安全性,客户应该有DCOM安全性设置来标识假冒层和连接验证层。这些 设置可以使用DCOMConfig.exe来完成。尽管别的更多的包含的设置也能够工作,但是还是有一 些推荐的设置。要看更多的解释,可以参考第10章。 不 仅 仅 是 应 用 程 序 ,COM+管理系统本身也有一些安全性依赖。为了安全地使用COM+, 你应该为COM+系统应用程序进行安全性设置的检测和修改。COM+系统应用程序包括一些组352 第三部分组件管理与事务 件 ,这些组件用于保留和管理那些在COM+机器中有疑问的应用程序。在这个应用程序中配置 了两个角色:Administrator和Reader。被映射到Administrator角色的用户可以使用任何组件管 理控制台的特点。被映射到Reader角色的用户能够察看COM+的设®,但是不能修改当前的配 置。在域控制器上的系统中,COM+管理员账号必须拥有域管理员特权,以便他正确地管理 COM+0 1 8 .3 .4 验证 验证既可以在声明中执行,也可以在程序中执行。应用程序提供一个安全性属性,这个属 性可以让你配置应用程序验证层c 缺省的是Packet,它能很好地运行。表 18-1列出了验证层和它 们的含义。正如你所期望的,表格越往下,那么安全系统创建的安全级越高。 表1 8 - 1 验证层 层 描述 None 无论足从成用程序来的还焙到成丨程序去的通信都没有安全性检测 Conncct 仅仅在初始的连接上才旮安仝性检测 Call 在毎一次调用时进行安全性检测 Packet 发送者的标识被加密 Packet Integrity 发送者的你识和签名被加密(确保完馅忭) Packet Privacy 幣个包都被加密(确保个体性) 1 8 . 4 过程com+安全性 你已经知道了一个管理员如何完全确保应用程序在接u 层的 安 全 。我也已经介绍了许多有 关的东西,尤其是管理员能力的重要性。配置形式自由的组件程序并不是一件小事情,组件程 序是由应用程序和组件构成的,这些组件随着时间的逝去,改变机器和进程。 为了减轻对于管理员的声明方面的负担,或者是为了简化,以得到更多的细微的安全机制 控 制 ,开发人员会想到在他自己的安全性能中编写代码。在 本章 中 ,你将学习到一些编码中带 有低级安全操作的声明特点。 以下使用的例子都是属于你的Stateful Sum类的一个安全性明确的版本。为了了解这个例子, 你需要创建一个名为SecureSum的新应用程序。将Stateful组件加入到应用程序中,然后添加两 个 角 色 :Admins和Accumulators。分别给Admins和Accumulators添加一个用户账号,这样你就 可以测试这两个角色了。 1 8 .4 .1 识别用户 在你的Sum类 中 ,尽力实现的安全性是被限制在Accumulate方法上的。我建议你第一件要完 成的任务就是确保每一个用户都有属于他自己的一份累积汇总的拷贝.。在 一 个 多 用 户企业 中 , 说 起 来 容 易 做 起 来 难 。你 的 对 象 是 支 持 取 消 激 活 的 ,因 此你 可 能仍然 需 要共 享属 性 管 理 器 (Shared Property Manager)来保留调用间的状态信息。然而,你需要按照这样一种方式来命名这 些 属 性 ,以达到在所有的用户之间它都是惟一的。当然,Windows NT和Windows 2000有它自身第/S章 COM七的安全性 353 内建的特点。这个特点被认为是一个安全性标识或者说SID。SID被 指 派到 所 有 的 用 户 (和组 、 计 算 机 等 等 ),以提供某个特定安全规则的惟一参考。S【D是 一 个 不透明 的结 构,它可以通过 WIN32系统访问,但是它们可以以文本的形式出现。 注 意 应 该 记 住 的 是 ,随着基于Web的应用程序的不断发展,这样的应用程序设计被引 入 ,在这个应用程序设计中,真正的用户可能没有SID甚至可以是匿名的。 1. CoInitializeSecurity 函数 CoInitializeSecurity函数初始化了安全层,而且设置了特定的安全性缺省值。如果进程没有 调用CoInidalizeSecurity( >的 话,当第一次一个接口被调度或者被取消调度时,COM+会自动地 调用它,以注册系统缺省安全性。在那之前使没有缺省安全性包被注册的。函数原型如下: HRESULT CoInitializeSecurity( PSECURITY_DESCRIPTOR pVoid, •//Points to security descriptor LONG cAuthSvc, //Count of entries in asAuthSvc SOLE_AUTHENTICATION_SERVIC€ * asAuthSvc, //Array of names to register void * pReservedl, //Reserved for future use DWORD dwAuthnLevel, //The default authentication level for proxies DWORD dwImpLevel, //The default impersonation level for proxies SOLEJVUTHENTICATION_LIST * pAuthLiSt, /Authentication information for each authentication service DWORD dwCapabilities, //Additional client and/or server-side capabilities void * pReserved3 //Reserved for future use ); 当EOACLAPPID标志在dwCapabilities中被设置的时候,pVoid指向一个APP丨D , 而且所有的 给CoInitializeSecurity的其他参数都被忽略(而且必须为零)。 CoInitializeSecurity在注册表中寻找APPID主键下的验证级,并且用它来决定缺省的安全性。 如果pVoid是 一 个 空 值 (NULL ),那么CoInitializeSecurity就在注册表中查阅扩展名为.exe的应用 程 序 ,并使用APP1D存储在那儿。这样就设定了同样的安全性,而看上去就好像是进程还没有 调用CoInitializeSecurity ( ) 似的0 如果EOAC_ACCESS_CONTROL标志被设置,pVoid是一个指向IAccessComrol对象的指计, IAccessContro丨对象用于决定谁能够调用进程。 当CoUninitialize ( ) 被 调 用 时 ,COM+ 为了 IAccessControl和Release ( ) 而调用AddRef ( )。IAccessControl对象的状态不应该被改变。如果 EOAC_ACCESS_CONTROL被 指定 ,那么dwAuthnLevel不 能 为 空 (NULL)。 如果EOAC_APPID标志和EOAC_ACCESS_CONTROL标志都被设置的话,那么Colnitialize Security ( ) 函数将返回错误。 如果在dwCapabilities中EOAC_APPID标志和EOAC_ACCESS_CONTROL标志都没有被指定 的话,那么pVoid必须是一个指向Win32 SECURIYTJDESCR1PTOR的指针。一个安全性描述包 括两个ACL:判断ACL ( D A C L )和系统ACL (S A C L ),其中判断ACL指示谁被允许调用进程, 系统ACL包含了审计信息。COM+在DACL中查找COM_RiGHTS_EXECUTE标 志 ,以便找出哪 个调用者允许连接到进程对象上。SACL必须是空。一个没有带有ACE的DACL允许没有访问 , 一个空的DACL允许任何的调用者的调用。 如果pVoid是一个安全性描述符,那么SECURITYJDESCR丨PTOR的所有者和组必须被设贾354 第三部分组件管理与事务 ---应 用 程 序 应 该 调 用 AccessCheck ( 而 不 是 IsValidSecurityDescriptor ) 以确保在调用 CoInitializeSecurity ( ) 之前它们的安全性描述符IF.确地形成。COM+拷贝特定的安全性描述符。 如果应用程序传递了一个空(NULL)的描述符,那么COM+构造一个允许任何调用的描述符。 当一个CAPI句柄在pAuthList中被指定给了SSL入口 时 ,CAPI句柄一定 不是 自由 的 ,直到 CoUninitialize ( ) 被调用为止。如果列表被指定,但是没有SSL入口,那么在COM+首次与作为 验证服务的SSL通信的时候,COM+会尽力找到一个缺省的标识符。如果成功的话,COM+会保 存这个标识符作为缺省的。否则 ,客户对于SSL而言就是匿名的。 2. ISecurityProperty接 口 COM + 提供了 ISecurityProperty接 口 ,作 为 一 种 发 现 不 同 调 用 者 进 程 SID 的 方 法 。 ISecurityProperty通过在一个对象的ObjectContext上调用Querylnterface ( ) 来 获 得 。每一个 ISecurityProperty方法调用返回各自的SID指针 。因为你正在使用系统分配的资源,所以当你已 经通过调用ReleaseSID完成调用后,必须释放资源。 表 18-2 丨 Security Property 方法 ) i 法 功 能 GctDircctCallcrSID() 恢 钇 3 的正在执行的方法所调用的外部进程的安全件1D GetDirectCreatorSID() 恢茇直接创辻当前对象的外部进程的安全件1D GetOriginalCallerSID() 恢复初始化调!H序列的基础进程的安全性1D,当前的方法是从 调Iti序列中被调ttl的 GetOriginalCrcatorSID() 恢殳初始化活动对象所执行的操作的基础进程的安全性1D ReleaseSID() 释放山别的ISecurityProperty方法返回的安全性1D 程序淸单18-1所示的是一个新的内部Sum类方 法,它用來找出直接调用者的S[D。 清单18-1找出直接调用者的SID void CSum::GetDirectCallerSID( BSTR * pbstrSid ) //Initialize the security interface pointer, II the binary SID and the string SID ISecurityProperty * pSecurityProperty = NULL; PSID pSID = NULL; * pbstrSid = N U L L ; //Get the security property interface II from the ObjectContext m_spObjectContext->QueryInterface( IID_ISecurityProperty, (void**)&pSecurityProperty ); if ( NULL == pSecurityProperty ) return; //Get the direct callers' binary SID HRESULT hr = pSecurityProperty->6et0irectCallerSID( &pSID ); if ( FAILED(hr)) return; //Convert the binary SID to a text string and release第J8章 COM+的安全姓 355 II the binary SID and security property interface SidToString( pSID, pbstrSid ); pSecurityProperty >ReleaseSID(pSID); pSecu rityProperty->Release(); 正 如 你 所 看 到 的 那 样 , 步 骤 是 非 常 简 单 的 。 你 获 得 安 全 性 属 性 接 n , 并 且 调 用 GetDirectCallerSlD()。在这儿,捕获物就是COM+给你的一个二进制SID。如果你要使用这个SID 来创建一个SPM属性的话,你将不得不把它转 换成 文本(所有的SPM属性都有 文 本名 字)。帮助 函数SidToString()做了这个苦活。不幸的是,这不是一个系统调用c 程序清单丨8-2列出了代码。 清单18-2把一个二进制SID转换成文本 bool CSum::SidToString( PSID pSid, //Binary Sid BSTR * pbstrSid ) //String Sid { //Test Sid for validity if(!IsValidSid(pSid)) return false; I/Test BSTR for validity if(NULL == pbstrSid ) return false; //Set up working buffer and initialize input BSTR WCHAR wsSid[128]; * pbstrSid = NULL; //Obtain SidldentifierAuthority PSID_IDENTIFIER_AUTH0RITY psia = GetSidldentifierAuthority(pSid) //Prepare S-SID_REVISION- DWORD dwSidLength = swprintf( wsSid, LMS-%lu-“, SID_REVISION ); //Prepare SidldentifierAuthority if ( Valuei0】 != 0 } 丨丨(psia->Value[l] != 0 ) ) { dwSidLength += swprintf(wsSid + dwSidLength, L _ 0x%02hx%02hx%02hx%02hx%02hx%02hx•, (USHORT)psia->Value[0] (USH0RT)psia->Value【1 】 (USH0RT}psia->Value【2] (USH0RT)psia*>Value[3] (USH0RT)psia->Value|4] (USHORT)psia•>Value[5]); else { dwSidLength +* swprintf(wsSid + dwSidLength, L “ % l u “ , (UL0NG)(psia->Value[5] ) + (ULONG)(psia->Value[4] « 8) + (UL0NG)(psia->Value[3] « 16) + (UL0NGHpsia->Value[21 « 24) );356 第三部分组件管理与事务 //Loop through SidSubAuthorities DWORD dwSubAuthorities = *GetSidSubAuthorityCount(pSid); for(DWORD dwCounter=0; dwCounter第M 章 COM+ 的安全性 357 ( 续 ) h 法 功 能 EnableCommit() 声明对象的唭务性更新是一个一致的状态(对象通过“法调用来保 留它的状态) IsCallerInRole() 表示对象的调用者是否娃一个特定的角色 IsInTransaction() 表示对象是否在一个亊务中执行 IsSccurityEnabled(.) 表示安全性适否歼启 SetAbort() 取 消 S 前的琪务(对象能在返回时取消激活) SetComplete() 卢明对象的事务性史新能够被提交(对象能在返间时取消激活) 査看一下程序清单18-3中完整的Accumulate )方法的安全版本。在测试这段代码之前,确信 你已经创建了带有一个Admin角色和一个Accumulator角色的应用程序,而且那两个角色已经有 正确的用户成员。 清单1 8 - 3 安全的Accumulate()方法 STDMETHODIMP CSum::Accumulate(int iValue, int *piTotal) { //Make sure the security system is running if ( ! m_spObjectContext->IsSecurityEnabled()) return E一FAIL; //Ensure that caller is in required role // Admins and Accumulators can add to the total BOOL blnAdminsRole = FALSE; BOOL blnAccumulatorsRole = FALSE; HRESULT hr; hr = m__spOb j ectContext ->lsCallerInRole (L1* Admins*, & blnAdminsRole ); if ( FAILED(hr)) return E FAIL:• hr = m_spObjectContext•>IsCalle「InRole{L-AccumulatorsM, SblnAccumulatorsRole ); if ( FAILED(hr)) return E_FAIL; if ( 丨 blnAdminsRole && ! blnAccumulatorsRole ) return E_FAIL; y/Check input argument if ( NULL == piTotal ) return E INVALIDARG; //SPM, Group and Property interface pointers ISharedPropertyGroupManager * pSPGM = NULL; ISharedPropertyGroup * pGroup = NULL; ISharedProperty * pProp = NULL; //Get interface pointer to SPM mspObjectContext->CreateInstance( CLSID_SharedPropertyGroupManager, I ID_l5haredPn)pertyGn)叩 Ma n a g e r, (void**) &pSPGM );358 第三部分组件管理与事务 if ( NULL == pSPGM ) return E一FAIL; //Create or open the property group BSTR bstrGroupName = SysAllocString{ L“CallerTotals long UsoMode = LockMethod; long IRelMode = Process; VARIANT BOOL fExists = VARIANT FALSE; pSPGM->CreatePropertyGroup( bstrGroupName, & H s o M o d e , &lRelMode, &fExists, &pGroup ); SysF reeString(bstrGroupName); pSPGM->Release(); if ( NULL := pGroup ) return E FAIL; //Create or open the caller's private total property BSTR bstrSID; GetDirectCallerSID( AbstrSID ); pGroup->CreateProperty( bstrSID, & f E x i s t s , &pProp); SysFreeString(bstrSID); pGroup >Release(); if ( NULL == pProp ) return E_FAIL; //Update the total VARIANT vtTotal; vtTotal.vt = VT_I4; pProp->get_Value(&vtTotal); vtTotal.lVal += iValue; pProp->put_Value(vtTotal); pProp->Release(); //Only admins can see the total if ( blnAdminsRole ) *piTotal = vtTotal.lVal; else •piTotal = 0; //Tell C0M+ that our work is complete m_spObjectContext->SetComplete(); //Return success (after which COM+ deactivates us) return S OK; 这个例行程序的开始建立了基本的安全性信息。首 先 ,你检查安全性的启动和在你应用程 序中的运行,然后你看看调用者是否在一个或者两个授权角色中。如果这些事情中的任何一个 有 问题,你都会被剔出。接 着 ,你在SPM中工作到属性级,然后使用调用者的SID (使用两个以 前讲的函数 构造的 )创建/打开一个用户属性。在添加了新的值到汇总中后,你查看一下用户的^ 1 8 t COM+的安全•汝 359 角色,然后返回目前的汇总值,当然只有你是在Admins角色中才能査看汇总值(否则返冋零厶 1 8 . 5 小结 本章主题并不像安全性话题那样复杂,而且你已经确实地做出了一个完整的COM+安全性回 路 。你已经学习了基本的安全性构造,然后是声明机制和编程机制,这些机制使得COM+安全 性 有效。本章还讲述了各种和安全性相关的方法及接口,然后给出T 一个例子过程 ,用于举例 说明许多关键的COM+安全性特点。第 19章我们将看看在COM和大型机之间的事务整合问题。第19章 COM事务集成器 本章内容包括: •COMTI的要求 • 大型机和Windows DNA • CICS 和 CICS-LINK • COMT1组件创建器 •COMTI管理控制台 • COMTI运行时间 本章将学习COMTI,也就是通过Microsoft系 统 网 络 建 造 (System Network Architecture, SNA)服务器将COM+扩 胰到大型 机。本章分成5个部分c 第一部分和第二部分包括大划机和 Windows DNA中 的 角 色 。第 三 部 分 和 第 四 部 分 处 理 COMTI的 可 视 化 界 面 (组件 创 建 器 、 COBOL向导和COMT丨/MTS MMC )。Ai后一个部分使用一个假设的但完整的COMTI和SNA服务 器设置研究COMTI的 运 行 时间。下 一 个 在 线 的 (in the l in e ) 产 品就是主机集成服务器2000 ( Host Integration Server 2000 ) ( 代码名称为Babylon )0 比较重要的是,要解释一下,COMTI是倾向于支持大铟机的。而象S/38、AS/400和RS6000 这样的模型是真正中立的,COMTI就没有考虑它们。 注 意 如 果 你 不 想 使 用 大 型 机 或 者 别 的 传 统 的 系 统 ,你应该跳过这一章。 因为COMTI 是专门在Windows DNA模型中沟通C0M+和大型 机 的 (通过SNA服 务 器 )。COMTI在以 外的领域是没用的。 19.1 C O M T I的 要 求 我假定读者已经熟悉SNA Server 4 .0 ,并且拥有一个到IBM大型机的工作连接 c 大型机应该 在MVS v4.3或者 更新的版本 下操作 ,并且更新的版本必须带有兼容LU6.2的程序间高级通信 ( Advanced Program to Program Communication, APPC )c 为 r 运行事务程序,目前COMTI支持 两种IBM大塑机系统:消费者信息控制系统(Customer Information Control System, CICS ) 4.0版 和信 息管 理系统(Information Management System,IMS ) 4.0版 。本章仅仅着重介绍CICS程序。 如果在网 络中没有SNA Server, COMT丨是不能工作的。另 外 ,这儿所 有的例 子都 是 用 COBOL写 的 ,因为到目前为止,它是惟一被组件创建器中的COMTI向导所支持的语言。 注意 COMTI由一个开发环境和一个运行时间模型组成。二者都带有Windows SNA Server 4.0,它作为幵发环境的一部分。应该注意的是,在典型的SNA服务器安装过程 中COMT1缺省不被检测。如果SNA服务器已经被安装了 , 而且在SNA组中没有COMTI 图标,那么重新运行SNA的安装程序,加上COMT1选项就可以了 。’第79章 COM 事务集成器 36! 尽管我不会深入地讨论SNA服务器的安装和管理,但是我要简要地介绍一下SNA服务器和 通常的大型机,这样有利于不熟悉传统的应用程序的读者。然而为了充分利用C0MT1,读者应 该已经充分地掌握了SNA服务器的技巧以及设® 和管理一个SNA服务器的背景知识。本章并不 能替代Microsoft SNA Web站 点 上 ( www.microsoft.com/sna ) 大量优秀的SNA服务器资源和 COMTI资源。 注意 SNA Server存在一条很陡的学习曲线,尤其是对于那些非大型机编程人员而言。 SNA Server有许多未编入文档的缺陷和障碍,但是一旦系统配置好后,那么开发工作就 能很顺利地进行。SNA Server可能是被Windows操作系统创建的最强大的传统接口工 具 : 瞥告大型机系统和PC系统存在于不同的领域,这并不是一个过分的评价。如果你从 来没有在大型机系统中幵发过程序,你将惊奇地发现和普通NT术语学的许多差别。 在大型 机 中 ,简单的事情,例如编译、使用文件以及简单的人机交互,都和PC如 此的不同。起初大型机系统几乎视PC系统为背道而驰的系统5 你可能熟悉的许多术语 ( 接 口 、事务、客户机、服 务 器 > 对于大型机的幵发人员而言都意味着非常不同的事情。 如果你要从本章学到一件事情的话,那就是要清楚存在于大型机系统和PC系统间的技 术交流障碍。 19.2 大型机和Windows DNA 在学4C0M T I之前 ,你必须学会Microsoft SNA Serverc 而且要深入研究一点点大型机的历 史 :为什么它们是现在这个样子,为什么在2000年和将来它们仍然对于软件开发很重要。 注 意 先 向 熟 悉 大 型 机 的 老 手 们 道 歉 ;这个小小的部分不会对机器进行正确的评价,尽 管它们正如我们所知道的那样,改变了文明。然而,我并不是大型机领域的专家,而且 我仅仅讲一些最近的经验。 大型.机在过去和目前非常流行的主要原因就是它巨大的可扩展性和稳定性。在信息技术领 域 里 ,它们类似于Maytag工具。大型机的崩溃是非常少的,就好像哈雷彗星飞过那么少见。 但是为什么会这样呢? 什么使大型机如此可靠呢?为什么没有更多地使用它们呢?如果大 型机如此可靠,为什么软件B 子们 ,例如Sun和M icrosoft,不能将同样的可扩展性融入到PC的 操作系统核心中呢? 对于这些问题有很多答案,但是有一个是占主导地位的:紧密结合性。大型机没有上百种 第三方视频卡和声卡来选择,没有人类学 键盘、游 戏 杆 、指向设备或者数字照相机相匹配,没 有病毒检测软件和适当的GUI来追踪点击和窗口绘制事件,也没有网络浏览器、多 媒 体 、3D游 戏和ActiMate Barney,没有乐趣。 大型机仅仅是纯粹的、简单粗糙的计算和存储工具,不像PC用户可能曾经想象的那样。大 甩机是使用可靠性概念设计和建立的,因此被紧密地结合,以加强可靠性。大型机制造商并不 和成千具有竞争力的硬件供应商竞争,尽宵这些硬件供应商相信他们的产品更好。他们不必提 供额外的位于CPI和I/O之间的驱动层,也不必为了满足不同种网络和平台环境的偌要而提供复362 第三部分组件管理与事务 杂的安全与网络层。正是这个原因降低了复杂性,使得大型机和PC相比如此的灵活。 大型机甚至为了便于多用户操作环境,在结构层提供了硬件支持。大型机内核的作者都和 设计处理器指令序列的计算机工程师一起工作。当然.所有这些要付出 的 代价就 是 非常死 板而 不灵活的环境。这对于存储和恢复数据是有好处的。正是这个稳固而紧密的结构造就了大型机 的强大、可扩展性和容错性。但是很显然,甚至到今天为止,大沏机在许多领域里是很缺乏的, 并且相当的昂贵。这也就是PC和SNA Server的引入点。 紧密耦合:用纸牌建的房子 耦合在第2章有详细的介绍,但是我将用一个新的类比来描述它,因为他是理解大 型机伸缩性的关鍵。 什么是耦合? 紧密耦合的系统是一个很少带有或者根本没有独立组件的集成系统。 另一方面,松散耦合系统由许多对象或者组件组成,这些组件能够在不影响整体的情 况下互换,, 例如 ,一栋由纸牌建造的房子,那是非常脆弱和微妙的,但是很容易建造。他能够 经受住自己的重量而且通常是很吸引人的。在这个房子结构中强度来自于一种依赖性, 这种依赖性是纸牌从底部在各个方向上向上的力造成的。每一张牌都能承受几倍于自身 重量的力,如果有更加巧妙的摆放,还能承受更多的力。 然而,纸牌建的房子也有它自身的弱点。如果一张牌垮了,就很有可能整个房子都 倒塥。在房子建好以后,要想对它有所修改也是不太可行的。替换或者添加纸牌都可能 导致整个结构的崩溃3 这就是紧密耦合系统(大型 机 )。 将这种建造结构的技术与LEGO对比,没一个LEGO片能够被看成一个简单的能共 同操作的组件,这个组件带有已知的接口和方法,用LEGO方法建的房子能够承受更多 的结构滥用,很容易修改。 另外,LEGO结构建立以后可以被连接,而不影响整体。这 就 是松 散 耦合 系 统 (PC )。 19.2.1 SNA Server SNA Server是一个强壮的大型机系统和PC系统--尤其是Windows DNA框架之间的协议桥梁。 正如在本书的第1章中所讨论的那样,Windows DNA内在地基于组件,而 B.倾向于符合COM标 准的产品。如果把COMTI作为SNA Server和所有其他DNA构件之间理想的链接并没有什么惊奇。 在DNA被想出来之前SNA Server作为外围的东西很多年了,但是它仍然是今天DNA技术的强有 力的部分。图19-1表示了这个关系。 注意配置SNA Server可能是非常具有挑战性的工作。两个完全不同的系统在SNA下碰 撞 :大型机和Windows。要想成功地配置,就必须拥有两个领域中专家级的知识。 在配置过程中,SNA Server将不会总是精确地描述某一个配® 问题 ,而且它经常不清楚是 否有问题存在。当COMTI出问题后,看上去应该是没有问题的连接可能是断开的。在 两 边 (大 SJ机和W indow s)必须设置成堆的属性和值,而且如果时间有限的话,边尝试边纠错的方法也 是不可行的。第 川 章 COM事务集成器 大型计箅机 NT服务器 MVS 事务程序 (IP) AJ 6.2 SNA服务器 COMT! 客户机 图19-丨 COMTI和大型.机通过SNA Server连接 替 告 在 配 置 阶 段 之 前 和 之 后 ,如果没有一个能够熟练地工作在每一边(大型机和PC) 的人,使COMT丨正常工作几乎是不可能的。 在配置SNA Server之 前 ,确定一个大型机联系人是很重要的,这个联系人主要负赉设置大 型机的连接和安全账号。他还应该花至少两到三天的时间来确保安装过程顺利进行。 挡你设置SNA Server时 ,必须发送和配置一条从物理上连接一台大铟机的网络路径。完成 这项工作有上百个方法,但是最常用的方法是通过一个Ethernet DLC网络链接实现,因为大多数 环境都支持Ethernet的某些格式。幸运的是,SNA Server有许多很有帮助的向导帮助你完成网络 配置进程。一些向导甚至能产生一些由大型机操作人员填写的工作表。工作表然后将被用亍正 确配置SNA Server,并且确保所有的链接和安全值的匹配。 注 意 除 了 需 要 网 络 链 接 外 , C0MT1还需要一个LU6.2连接。在 这儿 , APP/LU6.2连接 向导特别的有用处。 1 9 .2 .2 在COMT丨之前 在COMT〖可用之前,有许多技术可以将数据从大型机传输到PC和 (vice versa)。这些技术 的范围从手动的ASCII FTP传输到笨拙的但是自动的屏幕推土机(将大哦机的输出窗口照下来, 然后把数据从X, Y 坐 标 系 中 “粉碎” 出 來 )。这些技术有错误的倾向,没有 很 好 地规划,而且 在分布式异类环 境(例如Windows DNA ) 下是不可预测的。 通过把包括在和大型机的通信中的管件(plum bing)封 装起来 ,COMT丨可以直接执行在大 型机上的程序并且向所有的PC端的客户提供一个可扩展的熟悉的COM接 口 ,以便消除这些错误 倾向技巧。 例如 ,当今最流行的方法就是数据存储。正如你将在下一个部分学到的那样,COMTI使你 能够容易地读写大型机的数据存储器,rfn没有任何的障碍C 19.2.3 COMTI 在COMTI的帮助下,SNA Server使得Windows DNA能 够访问大型 机 的 程 序 和 资 源 (例如364 第三部分组件管理与事务 DB2数 据 ),让 你 看 h 去好像它们是在网络t 的分布式对象。 表面上 ,COMTI通过在PC上创建一个大型机程序的代理来工作。这个代理看上i 象一个规 则的COM组 件 ,而且工作和行为也和规则COM组件相象。因此它可以得益于所冇的COM特点。 代理输出接口和方法,这些方法代表 了运 行 在大型机t 的 事 务 程 序 (TP)。一个C0MT1组件的 客户并+ 知道大彻机的是否存在。COMTI、SNA Server和大型机之间复杂的交换都是完全被封 装的。图19-1揭示了这个过程。 注意使用COMTI, Windows DNA对 象 (例如ASP页面,Visual Basice的ActiveX控 件 , 甚至Word文 桂 )能够访问大型机的数据和程序,看起来它们好像是COM Automation对 象。从来没有在两个不同的技术领域之间建立过这么精致的桥梁! 图 19-2中表明COMTI在Windows DNA框架(为了强调COMTI, 业务和陈述组建都被省掉f )c 为了 了解更完整的Windows DNA描 述 ,请参考第1章。 工具 分布式操作 用户界面和导航 \\ 环境 业务过程 — ................. ______.............................帽 , & --------------------------------------- 遗留数据 j ■ M M _ _ 幽 _ ■ m a m --------V wu------------------ ^ V 图丨9 - 2 遗留的数据和Windows DNA通过COMTI共存 19.2.4 COMTI瞀告 图19-2中的三层结构并不是Windows DNA特 有 的 ,也不是一个新的概念。一些二十世纪七 十年代编写的大型机应用程序就带有三层结构的思想。不幸的 是 ,这种思想很少见。大部分大 型机程序都有硬代码终端输出屏幕,还有它们的、Ik务逻辑和数据访问代码。这个庞大的结构对 于COMTI来说是很难操作的。 COMTI擅长于与大型机程序或者数据库通信,但是有一些限制和告诫。为了使TP和COMTI 正确地工作,它不准有任何的屏幕输出或者用户的交互作用。COMTI组件通常带有一系列的TP 参 数 (变长或者定长),并且期望有个结果返固。 MTS/DTC事务和大型机事务对比 在Microsoft的术语中,一个事务被描述为一个微小的工作单元。它是一个协议,这 个协议使得信息能够保持在一致和可靠的方式。在MTS和DTC中,事务过程中所做的 改 变 是 可 以 取 消 (撤 销 )或 者 永 久 保 留 (被提交 >,这要依靠一定的规則和准則。在大 型机领城里,事务是一个CICS程序或者TP。你可以运行一个事务程序,该程序可能或第/9章 COM 事务集成器 允5 者不可能采用一个两阶段提交协议,例如Sync Level 2。(那要追溯到程序员那儿。) 要记住的是,当同大型机进行通信时,幵发周期中将会产生许多误解。 如果COMT1必须要一个和互动TP的 接 口 (依赖于屏幕I/O), TP必须被修改,去掉它的终端 逻 辑 ,或者仅仅重新编写逻辑。 这是一个很好的描述三层结构方法如何减少问题的例子。如果TP在三层结构内编写,那么 所有的终端I/O都可能很容易地被Windows DNA表达层 所替代 ,而COMTI会透明地和大型机商 业逻辑以及数据存储相通信,同时不做任何更改。 19.3 CICS和CICS-LINK 本部分主要讲述来往于大型机TP程 序 的数据细节和 他 们 的操作环 境,用户信息控制系统 ( Customer Information Control System, CICS)。下一部分讲述 COMTI 组件创建器开发工具,这 个工具可以帮助你从PC上访问TP程序。然 后 ,COMT[让你能够通过SNA Server将大型机的可靠 性带入到分布式PC领域。这在接续的几个步骤中进行。首先,我要定义CICS操作环境。 CICS是IBM针对MVS的事务管理器的。CICS子系统包括的范围超过包括大型机程序(TP >。 资源和安全性的管理也可以包含在这个范围内。因为每一个TP都在它自己的地址空间内运行, 在这个范围内一个错误或者内存泄漏都不会使整个系统崩溃,这样就提高了可靠性。 COMTI可以调用和执行这些范围内的TP,并且可以在PC和大型机之间来回传递信息。一个 典型的TP可能从DB2表 、检查安全性或者执行业务规则中获得和更新数据。一些TP甚至可能执 行别的TP,只要这些TP在所需要的同一个事务范围内。注意的就是,尽管CICS在需要的时候透 明地管理事务,怛是事务并不是自动的,而且必须被指定在某个范围内或者在某TP级 。 在大型机上,两个最普遍使用的执行TP的方法是CICS和CICS-LINK。CICS和CICS-LINK® 序之间的主要不同在于他们如何通过调用来传递参数信息^本章的后面将讨论二者的一些优点 和限制。 - 注 意 有一些类型的COMTI组 件 :事务性的、非事务性的、固定长度、变长度、IMS、 CICS和CICS-LINK。这部分仅讲述非事务性的CICS和CICS-LINK, 因为它们是最常用 的。要了解别的事务性组件,可以参考COMTI的在线帮助。 在CICS TP中 ,程序逻辑必须执行一个CICS RECEIVE INTO语 句 ,来接收参数化的数据, 并且必须发出CICS SEND FROM指 令 ,将数据发送回调用者。CICS程序运行在惟一的事务丨D下 。 程序清单19-1是一个COBOL程序片断,它使用了CICS来 将 参 数 (END-IT)返回COMTI。请注 意EXEC CICS RETURN END-EXEC命令,它标志了CICS对话的结束。 清单19-1解释SEND FROM和SEND RETURN的程序片断 350700 999N0MAP. 3508^0 EXEC CICS SEND FROM(END-IT) LENGTH(0) ERASE END-EXEC. 350900 EXEC CICS RETURN END-EXEC. 另一方面,CICS-LINK TP就象代理。CICS-LINK程序被一个称之为CICS镜 像 事 务 (CICS Mirror Transaction, CSM1) 的特定范围所镜像,CSM1通过一个EXEC CICS LINK命令来传递控366 第三部分组件管理与事务 注 意 因 为 许 多 现 存 的 大 型 机 COBOL程序已经是和COMM AREA协同工作的结构,它 们不需要任何对COMTI的修改。因此,C1CS-LINK是创建COMTI组件的推荐方法。然 而 ,如果要使用变长记录的话,CICS-LINK就不能使用了,因为所有的进入和出去的 数据都必须在程序开始或者结束之前在COMM AREA中定义。 使用CICS-L1NK对 象 ,CSMI程序可以通过COMMAREA < DFHCOMMAREA的 缩 写 )接收 和发送参数,并且不需要任何额外的CICS所用的代码。程序淸单19-3中列出了一个你要在后面 部分使用的DFHCOMMAREA程序。要注意的是,每一行是如何清楚地定义名字、类型和变量: 长度的。正是为了这个原因,变长参数不能使用CICS-LINK。 清单19-3 CICS-LINK TP的连接部分包含COMMAREA,被用来传递来往于CQMTI的参数 003400* L I N K A G E SECTION 003600 LINKAGE SECTION. 003700 01 DFHCOMMAREA. 003800 05 CA-RETURN-CODE PIC S9(9) COMP. 鶴 9抑 05 CA-DATA. 抑 4 _ 10 CA-DT PIC X(10). 00401e 10 CA-REP-M0 PIC XXX. 004020 10 CAREP-YR PIC X(4). 004031 10 CA-CLOSE-OUT PIC X(10). 004040 10 C A - O B J ’CAR PIC S9(4) COMP. 抑 4041 10 CA-CR-PAC PIC S9(4) COMP. 004042 10 CA-0BJ-TRK PIC S9(4) COMP. 004043 10 CA-TR-PAC PIC S9(4) COMP. 004044 10 CA-OBJ•TOT PIC S9(4) COMP. 004045 10 CAT0TPAC PIC S9(4) COMP. 004050 CA-REP•DAYS PIC S9(4) COMP. 004060 10 CA-SA-CAR PIC S9(4)V9 COMP-3 004061 10 CA-SA-PU PIC S9(4)V9 COMP-3 004062 10 CA-SA-TRK PIC S9(4)V9 COMP-3 004063 10 CA-SA-T0T PIC S9(4)V9 COMP-3 004070 10 CA-DSLASTRTL PIC S9(9) COMP. 004080 10 CA-SELL DAYS PIC S9(4) COMP. 004090 10 CA-MAX-LINES CA-LINES OCCURS PIC S9(4) COMP. 004200 10 30 TIMES. 0 0 4 3 0 0 15 CA-RTLMTD PIC S9(9) 9999GSTDATE. EXEC CICS LINK PROGRAM('GSTDATE1) COMMAREA(GSTDATEREC) LENGTH(GDT-LEN) END-EXEC. 9999-GSTDATE EXIT. 制信息给一个TP。TP收到任何从调用者传来的输入参数后就进入它的运行时COMMAREA。所 有的输出参数都被存储在COMMAREA中 ,返回给调用者。CICS-LINK程序运行在CSMI事务ID 下 。程序清单19-2解释了这种思想。 消单19-2 —个函数使用CICS-UNK协议,通过COMMAREA传递参数 p p p p p P K K K K K K 2 2 2 2 2 2 R o n R R R R Y Y Y Y Y Y第/9章 COM 事务集成器 367 005100 抑 52抑 005300 0©54©0 15 CA-CURR-STK 15 CA-IN-TRANS 15 CA-0N-0RD 15 CA-PIPELINE-DS 15 CA-RTL-YTD 15 CA-RTL-60O 15 C A I N V M T 0 15 CA-WHLSL MTD 15 CA-WHLSL-YT0 CA- MESSAGE-OUT 瞀 告 C1CS-LINK程序不支持使用非绑定的记录或者在它们的参数通信协议中使用多次 发送和接收。另一方面,CICS程序支持多次发送和接收,因为它们能够在任何时候通 过大型机CICS SEND和RECEIVE命令请求数据。在这儿我将不会讲到多次发送和接收, 但是当选择通信方式的时候要考虑这个方面的事情。 确定所要使用的TP程 序 的 类 型 (CICS或者CICS-L1NK ) 是很重要的,因为下一个部分就要 讨论如何创建COM对象来协调每一个类型。 图 19-3是 图 19-1的 建 筑 结 构 图 ,它 图 解 了 一 个 更 真 实 的 带 有 多 个 运 行 于 大 型 机 上 的 TP (CICS或者CICS-LINK)的配置。在这儿也有客户端和MTS服务器之间的相互作用,而不是直 接与COMTI ( 如图19-1中所示的 )。 大型计筅机 CICS TP CICS-LINK TP IMS TP LU 6.2 NT服务器 SNA Server COMTI 运行 MTS ♦ 客户机 图 19-3 — 个更加苒实的COMT 丨配置 19.4 COMT丨组件创建器 004400 004500 004600 004700 004800 004900 005Q00 9(9) C0MP. 9(9) C0MP. 9(9) C0MP. .9(9) C0MP. 19(9) C0MP. 19(9) C0MP. >9(9) C0MP. 59(9) C0MP. >9(9) C0MP, ((5C). PIC PIC PIC PIC PIC PIC PIC PIC PIC PIC 组 件 创 建 器 (Component Builder ) 是一个COM程序员工具,它可以从大型机COBOL程序中 提取出参数的I/O信 息 。COMTI能够解析一个TP源 代 码 ,创建COM接 口 和 方 法 (带有适当的 OLE数 据 类 型 ),并且注册一个带有MTS包的组件。组件在MTS的保护下时,它能够在整个分布368 第三部分组件管理与事务 式环境中使用。另外,组件创建器还能指定事务性属性和大型机区域属性。 然而重要的是,尽管组件创建器是一个开发COMTI组件的重要工具,佴是它所有的特点都 可以手动使用标准COM,例如MIDL编译器、OLE View以及Regedit,来实现。组件创建器只是 使过程更加容易、更加可靠。 注 意 这 一 部 分 讲 述 了组件创建器如何工作,如何使用它的向导创建COMT丨组件。这 一部分绝对不是一个参考向导,也不可以替代产品所带的COMTI文档。为了更加透彻 地讨论组件创建器,你必须大力地研究COMTI文档。 图19-4显示了组件创建器的浏览器外观 ‘ 丨為丨沪卜 | gfi~二 •二' _ 1 “MM— Lgsr ; mi 1------------- ^w poiw w nm iM hctl p a w w te rto fM X fio d vh1072B( )Autmgm fTvtx 'r W ___________jaJi JW S .S E RS.CO_jws.eorr€0_soLCOOE s*w«g _jWS-0ej.TOT Long _lWSLOA_RnjrfTD Umq JW S-TRCKJ^MTD Long jW S.TOTALjrri^M TD Long jWS_CAf\.8A4£^VA*. G«»o«cy JWS.TFCK^SALE.AVA*. O w y 'JWS_TOTA^SAA£.AVA*. Owcy _J^S _IA 8T J3A T E 9nng D»^ci>o>» 1 CXeCXDtfBTypft RCS9WC0MP W>/Oul P C X 0 _jsvsoeiv\^«OGJSVSQCWJONT In/Owi WOur In/Oul ta/Oui kt/OMm/omWOu* In/OuiIn/OU WCSK9)C0MP P«CS4(V)C0MP PCSStIJCOMP PIC S»i9) COMP PICSS(5JVSCnC0MP^ PCS9(5JV90) CO»S^ P1CS9(5W )CO i«F-3 PtCXpO)«0哪pcxm 6.13PM 图 19-4 COMTI组件创建器 在COMTI组件创建器内创建组件有两种方法:手动或者使用向导。我将着重介绍向导,因 为它非常有效而且容易学会。然而,为了获得对于COMTI创建的接口的更好控制,手动方法是 一个被推荐使用的方法。 1 9 .4 .1 组件创建器COBOL向导 组件创建器中的COBOL向导是一种最容易的方法,它能既快叉正确地设IICOMTI组件c 它 包括了一个多步的处理,开始是确定TP类型和大型.机源代码。我将讲述两种TP: CICS和CICS- LINK ( 前一部分侧重于两者不同点)。 警 告 在 继 续 讲 例 子 之 前 ,要确信COMTI组件创建器是1.01版或者更高版的c 这个版本 包括了SNA Server 4.0 Service Pack 1 ( 在写本书的时候,仅有一个SNA Server 4.0 Service Pack )0第79章 COM 事务集成器 J69 为了核实安装的是哪个版本,你可以运行COMTI组件创建器,然后从Help菜申种选择About, 这个版本应该是vl.01 (build 0524 ) ( S P 1 ) 或者更髙,如图19-5中所示。 , OTMTmnwscfonlnl CompoftdntBuW ef Version 1 01 (Build 0524) (S P l) CopyiigMA 1S97>1938 M crosolt Corp. ^Jlnghtsre««fv«d 图 19-5 确信S N A Server 4.0 SP1 被安装 19.4.2 CICS TP CICS TP需要特殊的命令来告沂大型机,它能够接收参数,或者它有参数送回到调用者。这 些命令在组件创建器内是不会出现的,而且必须加到大型机TP的源代码里。 1.标识CICS TP 在你能创建一个大型机TP的COMTI代理之前,你必须知道TP做 什么 ,它希望什么样的参数 ( 如 果有 的话)作为输出 ,退出 时 它返 回 什么 参数(如 果 有 的 话 )。然后,你可以创建一个适当 的被命名的COM对象、接口和方法,以便在PC端使用它。这个创建过程通过从File菜单中选择 N e w ,就可以创建一个新的组件库。你在第二步中往库里添加方法。 督告确信在Remote Environment Type下拉框中选择了CICS (如 图 19-6所 示 )。 如果这个组件将参与事务,这时候你就能够选择Supports Transactions单选钮。这可以在后 面修改,修改是在MTS下做的。 m n s s s m m m Pronsocion Support 广 P aquir«» elronsocfioft Requires oflewtrafttodion r Supports 1 炫 Does not)t 鄉 port 一 w _ . ; l ---------. 图19-6在创建COMT丨组件的第一步中的新组件库对话框 2 .导入TP COBOL源代码 一个新的CICS对象创建并取名后,你就将准备建一个方法,用这个方法访问大型机上的TP。 记 住 ,为了COMTI能 够 正常工 作 ,大 型 机 要 求 的 数 据 类 型 (整型 、浮点 型 、数 组 等 等 )以及370 第三部分组件管理与事务 COM理 解 的数 据类 型 (BSTR,ints, variants, objects等 等 )必须正确转换。转换在运行过程中 通过COMTI代 理 完 成 ,但是数据类型是在编译的过程中被定义的。这正是COBOL向导介入的 地方。 注 意 为 了 这 一 步 ,你必须从大型机下我TP的COBOL源代码。FTP、email和终端模拟 方法都是可以使用的。 从File菜单中选择Import,然后选择COBOL Wizard。这将调出COBOL向 导 (如图19-7所 示 ), 向导将提示你提供TP COBOL源代码。 找到代码并且装入代码,然后浏览一遍。确保看见的代码就是被用来编译大型机上的TP 程 序 的 代 码 (如图19-8所 示 )。 注 意 不 要 低 估 这 条 建 议 !由于不一致的源代码,我曾经花了一整天的时间查找一个错 误。大型机TP程序是一个比我导入的COMTI更旧的版本。 由于一个字节去掉了所有的 变量,并且创建了垃圾数据,以至于COMMAREA不能工作。 C 0B01 (CICS) aC S Import COBOL W aard Thu ^ z A td h d p s you define m e th o d to rie c o rd tett that you can us« to a c ce u y o u mainhorrie B «t«d on your COBOL source, the w<2« d w i an automation rte rtd ce for po u lnthone > w l pedotm the foiow ng t« k t ; 1 S e led « COBOL to u c e bam acbom . 1 SdectaC O B O L 2 S p ecj(yw tie th»h» you « e vowkhg 她 method* ouecordxoU U you are working w ^hm ettiods. you tw i p^fform d ic*e ta ik ir>g w ih tn imothod.*5poc»iSpeedy the 1 ■ Select tho columns for ih e recoKfeet 图19-7 COBOL导入向导大大地简化了数据类型定义 瞀告在组件创建器(v l.0 1 )内有一个小错误,不能调整COBOL向导在800*600分辨 率以下3 你必须是在1024*768或者更高的分辨率,才能正确地看见向导对话框。 跟 着 向 导 我 们 继 续 ,你 可 以 看 见 导 人 选 项 。 对 于 这 个 例 子 ,你 可 以 选 择 Create a New M e th o d ,然后继续。(记录被留下来给你做练4 。)在下一个对话框中,你有机会为方法取名, 该方法将激活大型机主机内的这个TP (如 图 19-9所 示 )。命名TP是严格的,正如它在大型机里命 名 似 的 (它是区分大小写的);否则,TP将不能被找到,fft且组件创建也会失败。^ i 9 t c o m 事务集成器 m COBOL import L S o u rc * r « q u i r « c o e o L « m . . . i r •::! • Rhi congou. OOXtOO O IT A M V I S I O R . 0 0 1 3 0 0… … … … … ••… “ … 丨 001400 VOMINO-STOJUOt *ECTIOK. 001500 01 W-V0KK-TJt\.t9. O OliO O 001700 ooxaoo 001900 002000 002010 002010 001030 002040 OS OS OS 05 OS JOS OS 05 ,TOWC_ SOF •»-StlC8-C0 W-5M5-CD w - r o x m -0 » J :kM -T1CIC•觀TL-抓 -TOTAIr-RTL-irrD PIC 5 9 (4 ) PXC tt. CO«P. W^Cl •8 - t i •TOT •RTL. 9 9 .PIC P IC P IC S 9 (9 ) M C S 9 (9 ) P IC S9(91 T IC 9 » ( » C O IP VJLLOC C01P VlLOt COKP VALW COUP VALOZ zn o . zuo. rwo. SSKO. B erk Zl 图19-8开始时导入COBOL代码 COBOL N « n « You • Sp*o^a m e y u M tk OfMl Method Nomi Efltef »»e n o m # th e ly o u u $ « o n (he h o st to >mftn^&ctiOR ProQfm> Ne?rw Q a c k Orcr 图19-9输人大型机TP确切的名字 下 一步,你要指定数据在什么地方改变掌管权。对 于 输 入 (如 图 19-10所示 >、输 出 (如图 19-11所 示 )以 及 返 回 值 (如图19-12所 示 )选择01 Level Working Storage。程序清单丨9-4中表示 了图19-10中例子代码的完整工作存储区域。工作存储和C或者Pascal程序中的变量声明部分是同 义的。372 第 三 部 分 组件管理与事务 m m s B B s n m s m Seledlntx# Aiwe S«l»ci *>« m m I specif a ledelrnngiigm dckS*(R«d«lr<«to _^03 SW PIC S9(«) J o s sm 91C S9(4) C«V. vs-tots-o ttc XX. OS f»-5£|tfl-c 03 VS-KPITn MPCFDVtS fd-9EM«Ct> SVLCMI n c PIC w . WS-OM-ttf tic M (9) CONf. j^03 VS-CMI-»TL*«r» VZC ft(9> C M . [ b “ w s - n c x • 雖TL>«Rt fXC St(»> CMV. J ^ 0 3 V S -T 0T a L - RTL-« V » VXC M < 9 > COMP. l^as vs-cMi-sjkLB-jmin. ,ic n<9)V9 cm r-3. Vt*T«CK-SMJi*MnUL riC S9<3)V» CMT-3. ^ • 9 WS-T*TAL-SALX-JmkXL VIC S9(3)Vf CW -3 W S - U t f t - MTt MC X“_> . d S*i»a | UM«teaAD :Sacfc OsnoBt 图19-10 CICS需要一个01 Level [作存储作为输人 S«(*ei Output Af«a speoly t>#9roupltm9wtftprtMntf (h#c^pU(feomyourN)fitr«n«aadii /5r^Whnr«f^m s m ric st<4) c «fv ”c n . w-siM^ce-9 Mtpcrsmt i s -s z m -c o ia m ^ d a immi ^•3 SW ric St<4 写.3 vs-sns.ct r 3 OS V 9-9X M -C C -9 VS-»ITO-SfLCWC MC ^ 3 . ^ • 5 V S - 0 1 7 - T 0 T PXC S9 ( t ) C M . j ^ § 9 W S -CMI- ir T L -«T D M C f t C t ) C O W . •[^•9 vs-nac-ftTL-wr# n c S9 vie S9(9> caw -CMi-ua. F IC 9 9 . l « 9 V f- C A R - S J a .K -A V lI L P IC n < 5) V f •s vs-T»at*sjkL«-*mxL rxc s»<,>” its VS-nrKAL-SALC-M»IL PIC S f ( 3)V9 l«3 V9-LAS1-MTS FXC X ( It) . CtNP-3, ce»-2 “1 | QoipSeledtoft | Back N«wt> 图19-U CICS也需要一个01 Leve丨工作存储作为输出 图19-12中的向导页提示要一个返回值。如果你的TP程序将要返回一个状态码或者计算结果, 那么这个值就是它被指定的地方。如果你的TP不返回一个值,你可以跳过这一步。强烈建议所 有的TP程序都至少返回一个值,用来指示成功还是失败。这就使得以后的TP调试更加容易。 当以上的步骤完成后,向导会评估源代码,并且为这个TP创 建 方 法签名(如图19-13所 示 )。弟/9 聿 COM事务集成器 373 S9(9) COMP VALUE S9(9) COMP VALUE S9(9) COMP VALUE S9(9) COMP VALUE S9(5)V9 S9(5)V9 S9(5)V9 X(10). ZERO. ZERO. ZERO. ZERO. COMP-3 VALUE ZERO. C O M P -3 VALUE ZERO. COMP-3 VALUE ZERO. PIC S9(4) COMP. PIC XX. PIC 99. PIC +++9. 001200 DATA DIVISION. 001300*… … … … … … … … 001400 WORKING-STORAGE SECTION. 001500 01 WS-WORK-FIELDS. 001600 05 SUBI 001700 05 WS-SERS-CD 001800 05 WS-SERS-CD-9 REDEFIh 0 0 1 9 0 0 W S . SERS-CD 002000 05 WS-EDITED-SQLCODE 002010 05 WS.OBJ-TOT 0 0 2 0 2 0 0 5 WS, CAR-RTL-MT0 0 0 2 0 3 0 0 5 W S -■TRCK.RTL-MTD 002040 05 WS-■ TOTAL-RTL-MTD 0 0 2 0 6 0 0 5 WS ■CAR-SALE-AVAIL 0 0 2 0 7 0 0 5 W S -TRCK-SALE-AVAIL 0 0 2 0 8 0 0 5 WS-TOTAL-SALE-AVAIL 002090 05 WS•LAST.DATE 002100 V8-9tM-Ct> t l C XX. RZDKTIMCS »8-«E*#~CC »IC V8-KOXT£»>8QLCOCt PIC WS-ON-TOT FXC » ( 9 ) COW . OT-CI»- |tT t- « n » « C 9 9 (9) COW*. WS-TRCK-ira-BTD >XC S»<9J CO«P. BH-TCfTAt-RtV-KTI. P tC S9(9) CCfflP. «8-Clft-SALe>iV£lL ? t C S9(5)V9 C0»-3. tKK-SAU-KVAlU f»IC W (8JW ■-:>' T0TAb-8U.C-AVMt. ,IC S9(S)V9 COUP-3. z l W-TOTAU-Sfctt-AVAIV tfs-Lm-D&n Fie x(ioi S»I*CI I I QotoS^cfcon | . ■-(jt • 9lo sptdyortdstrtngnem. ^ ^ ^ ' ^ ' y :^ ¥ 卜 :• r: • 警告对于一些正确的COBOL源代码 , COBOL向导也可能会显示一个错误并且导入失 敗 (如 图 19-14所 示 )。如果发生了这样的事情,试着用不同的方法格式化COBOL代码 ( 带有或者不带行数,二进制或者文本FTP传输等 等)。我还没找出确切的导致这个小错 误的原因,而且Microsoft还没有认识到,也没有确认这是一个小措误。 3 .将组件添加到MTS包中 随着COBOL代码 的导 入 ,以及方法签名的产生,组件就能被添加到并且注册到一个MTS COMTI包中。从 工 具 栏 中 (或者从Tools菜单中选择Add To Package)选择Package图标。这就激 PIC PIC PIC PIC PIC PIC PIC PIC374 第三部分组件管理与事务 活 了 包 导 出 问 导 (Package Export Wizard, PEW )。 IL隱IHiMKBIWiMm 隱lfl_l IIIIHIIIIITHM CompMMnQ Vow Mn9 C 0 9 0 L import Haw»*uoc**«Mlyirrtport»cJyowCX)eCX»o»i*c« ft«cc*p»wh*rt wmmwyirtoimafcor tetg tidtBedi^tewiebecltto chaise ^wseledtone,劣...: iyo « ore eabi*wiw*yo«neleck)M l,ckk1>eFmhb«jc4MUc«1.1PSwJ^cVQIJ 9 TCW.Bw«»_Vm M«rac«1. PlCUV_R«oor^VQ2>MMl 1 p tcuvj 牧nn.inw*«up TCUV_S«t*0(£|rt«i*c*U p TodMTNn<_TCeLl««tatMl 1 J>V*h«taMortfv.T£n .1 姿 V*heW»Wjr01 oiiT 图19-16注意类似于MTS的外观以及COMTI MMC的感觉 Remote Envirement包括SNA Server下配置的大型机连接。如果你还没冇设置SNA Server, 这个文件夹就是空的。 每一个配置好的远程环境连接内都有组件和包,这些包完成大型机TP的真正功能。图19-16 中是两个带有CICS-Link扩展的环境。 警告如果没有这些环境,COMTI不能够访问大型机。如果每件事情都正确配置了, 在这个文件夹中的环境应该和SNA Manager列出来的环境相匹配。 当在组件创建器中使用包导出向导(Package Export Wizard ) 时 ,一个关鍵的问题就是 组件运行的远程环境。如果远程环境没有引入,那么包将被发送到Unassigned Components文件夹中,将不会起作用。 Unassigned Component文件夹存放了任何由组件创建器创建的组件,无论这个组件是否被指 定了远程环境或者一个COMTI包。这些组件将不会工作,必须在正确配置以后才能使用。 注 意 ,当选择COMTI组 件 时 ,MMC右边的面板显示的是一列属性。在 这儿,你可以很快地 看到哪个组件是CICS, CICS-LINK或者IMS。你也能通过选择它的属性来查看每一个组件的事 务 性 属 性 (单击右键,使用工具栏中的Properties图标,或者从Action菜单 中 选 择 )。该技术是和 整个MMC的MTS组件所用的技术是一样的。第/9 章 COM 事务集成器 377 图 19-17 SNA Server MMC 19.6 COMTI运行时间 本部分将把以前的各部分串起来,解释一个复杂,但在一定程度上可行的COMTI配置结构。 . 考虑图〖9-18,这个图有许多部分。在中心以下是大型机,它用于保存所有你感兴趣的数据 以及支持目前的上百个用户。在这个例子中,大型机有对DB2和VSAM数据库的访问。所有的这 些数据只要通过COMTI神奇的功能,就可以为PC客户端所使用。 最左边是基于浏览器的哑客户端,它不理解COM,但是仍然能够通过Active Server Page或 者 其 他形 式的 服 务 器端脚本语言使 用 它 的 服 务 。客户端请求一个Web页 ,结果是激活了一个 COMTI组 件 ,该组件可以获取任何必要的大型机数据,并且返回一个HTML结 果。 在右边顶部是WimlowS95和Windows NT客户端。这些客户端通过DCOM被 连接 ,并且拥有 COMTI组件的远程MTS代理的拷贝。COMTI组件是位于中间的机器,它直接和SNA Server连接c 具体看看下面的过程,这个过程是讲述Win95机 器 (最 右 边 )如何从大型机获得数据的。, 1) Win95客户例示了一个COMTI COM组 件 。这个组件可以在注册表内查找,你会发现它位 于MTS内。MTS知道这是一个远程组件,而且知道真正的组件在什么地方。一个连接到远程机 器上的DCOM连接被建立,它初始化了与远程组件的对话。这将导致在远程客户端创建一个普 通的COMTI类工厂。(记住,COMTI组件本身是代理。) 为了帮助开发者,Microsoft已经选择了MMC作为大多数Windows DNA服务器端产品的标准 开发平台。图19-17中是SNA Server M M C ,它也和COMTI很相像。 两 n n U— — .ai.■ - 1 i-■-■ -- i- ■ ■- ■ ;, Aoter. * SN m A ’ ① 二 € )■ . _■ fc— M—■ 1 • ■ 1 >*4i . . . • 一‘>*^ ■■■ «*>*»> i**1 ••»-♦*<<*< »>*«■' y fy •琴•_».卜-• ^ 一一 — , •. , .T06ErM«a»1 TCUV_R^ort_VtttJrt^»l \ TONXrOZtnM«c«” S> Nb^nookJot^OS MartMl 1 W#rt*c*V1 Tav_R«port_vm 1 p TCUVJt««c Toc%»T(«r^T05lnmUo*Vl P v ^ M a n ^ T t n IrtvfanU ■OvaKeWwrJW «maX«c«1J378 第三部分组件管理与事务 本地客户机 远程客户机 NT服务器 MTS COMTI 运行时 NT工作站 NT服务器 SNA 服务器 NT服务器 SQL 服务器 IBM 大型计算机 CICS W IN 95 MTS COMTI 代理远程 组件 图 19-18 — 个 复 杂 而 正 确 的 COMTI配 置 2) Win95调用大型机TP程序组件的方法。TP程序执行一个商规则,并在大型机日志中写人 标记。返回零就表示成功;别的值表明失败。 3 ) 远 程 组 件 (位于MTS下 的 )是被激活的,并且准备就绪了。通过DCOM传来一个方法请 求。任何从Win95客户端来的参数都要被调度,被传送给方法。 当远程组件接收到方法请求后,为方法分配的ID被 指 定 ,同时COMT1状态机制打开。状态 机制负责管理DTC范围以外的事务以及在大型机上的超时和错误。要了解更多的COMTI状态机 制 ,可以查阅COMTI文档。 如果事务是被支持的,那么COMTI就会支持DTC。 4 ) 组件被COMTI运行时间所截获,接着大型机通过SNA Server ( 远 程 环 境 )提供的LU6.2 连接调用该组件。 5) TP程序运行于它的区域之下,通过工作存储或者DFHCOMMAREA ( 分别是CICS或者 CICS-LINK)返回一个结果。 6) COMTI状态机制等待从TP发来 的响应 ,当有响应到达时,它就被调度并且转换成OLE 数据。 7)LU6.2连接被断开,释放COMTI资源,然后结果通过DCOM被调度到Windows 95客户端, 这是请求的发源地。第79章 COM 事务集成器 379 8) Win95客户端接收到响应,并且释放组件。 在这些步骤中还有许多要进行的,但是这儿仅有一些提纲挈领的概要。大量别的微妙的机 制 在两个领域中都被使用(安全性、内存管理等等)。图中对于别的客户端而言,操作过程基本 上是相同的。DCOM不会中被包括在内,但它是仅有的中间步骤。 在实际过程中,尽管有这些为数众多的通过网络和平台的相互作用,但是整个过程却难以 置信的迅速。一个对大型机典型的TP的调用,执行和返回只需要不到一秒的时间。 然而作为COMTI程序开发者而言,我知道这并不是什么魔法,而仅仅是C0MT1组件的精心 配置。 1 9 . 7 小结 本章介绍了大型机和COM事务集成器。大 型 机 (和它 的操作 系 统 )是非常灵活强大的计算 机 ,它们紧密的结合和严格的OS许可标准使它们强大。这降低了兼容复杂性,提髙了稳定性 。 和PC对比,大型机是紧密耦合的。PC非常容易使用,而且使用起来更有乐趣,但是它很少有对 它 们 环 境 (为测试的第三方硬件和工具)的控制。OS许可协议是非常松的,而且容易接受第二 方的不稳定性。 对于与大型机程序通信以及访问它们的资源来说,C0MT1是一个强大的工具,因此它弥补 了两种计算机世界的间隙(PC上的Windows DNA和大型机上的传统程序)。 COMTI支持两种通信方法:CICS和CICS-LINK。当数据要被传送到COMTI的 时候,CICS 要求 大 型 机 程 序 (TP)使用明确的SEND和RECEIVE调用。相反,CICS-LINK自动使用独特的 通 信 区 域 (COMMAREA ) 来读写COMTI截获的变董。 对于创建组件与大型机TP协同工作而言,COMTI组件创建器是一个了不起的工具。COBOL 向导能够导入COBOL源代码,生成TP的方法签名,注册带有MTS的结果。 最后,你看看为COMTI环境设计的既复杂又真实的结构,再看看它的高层操作。第20章负载均衡组件 本韋内容: • 负载均衡组件定义 • 负载均衡组件的必要性 •并行性和粒度尺寸 •动态负载均衡算法 • 负载均衡组件组设计 •负载均衡组件客户机设计 •坏消息 • +用中央并行处理服务器的负载均衡组件 如果你要开发企业应用程序,你会有两种相对立的力量。一方面是 你的销 售部门,他希望 有尽可能多的客户机同时使用该应用程序。这样才能驱动其商业运行模式并赚钱。站在另一方 的是你的硬件实现组,他们告诉你硬件所能支持的客户机是有限的。那么开发者该怎么办? 这 还 有 更 多 的 问 题 ,不 只 是带宽的 限 制 。如 果应 用 程 序 服 务 器死机 ,那么会发生什么事 情?你所有的客户机都会遭受不幸,并且当所有销售员工意识到自己精心制作的计划从计算机 操作窗口中消失而且要等待服务器恢复时,你会听见他们的抱怨。 正如你能从本章标题猜出的一样,解决上述问题的方法就是实现COM + 负载均衡。这是易 于实现,而且几乎可以在任何情形下都不用改动你原程序的解决方法。 在你深入地学习本章之前,我有必要告诉你,负载均衡组件并不是COM +的一部分,从写 这本书时起就是这样。在本章的结尾我将谈及写这部分内容的原因和其发展前景。 2 0 . 1 负载均衡组件的定义 负 载 均 衡 简单地讲,负载均衡就是在几台计算机之间分配工作。这些计算机常常被称为机 器群。通常有一个中心控制点在机器群中分配任务。它 定 义 为负载 均 衡服 务 器 ,或者路由 器。 路由器同时可以作为机器群中的一台机器。请想象 一 下 ,一台计算机在作为机器群中的一分子 处理某些任务的同时,能够像路由器一样管理和分配请求到各台计算机。 工 作 单 元 在论述负载均衡的时候将用到“工作单元” 这个术语。工作单元是机器群中某台 机器所执行的一个工作单元。工作单元的这种情形就像是微软的工程师所作的工作一样。我将 在 “并行性和粒度尺寸” 这一节中用较长的篇幅论述工作单元。 粒 度 尺 寸 本章中描述的粒度尺寸是指所分配的工作单元的大小。发放给机器群中某台计算 机的工作单元越大,其粒度尺寸越粗大。发放给机器群中某台计算机的工作单元越小,其粒度 尺寸就越精细。第20章 负栽均衡组件 38J 2 0 . 2 负载均衡组件的必要性 在深人讨论之前 ,我要预先说明几个开发企业应用程序面临的挑战。这将更有利于让你明 白实现负载均衡的必要性。从本质上讲,负载均衡给企业内部工作能力提供了一个好的应用程 序 ,并且使企业独一无二的任务需求完成得更安全。 2 0 .2 .1 可扩展性 可扩展性可扩展性是用来描述一个企业系统在永远递增的要求中顺利增长的能力。可扩展 性成为了企业开发者长期以来梦寐以求的神圣目标。很早以前,更好地使用一个应用程序的最 通常做法是使用更大更快的计算机,或者升级计算机以获得更多的可用资源。虽然更大更快的 计算机提供了更多的传输量,但是在最大最快的计算机也处理不过来的时候,工作任务就在那 一点上堆积起来。 当前企业的应用程序大部分是关于流量的。在大多数情况下,流 量越大,财政的支付就越 大 。对订阅服务、电子商务和以别的方式赚钱的免费服务如广告显示,这通常是正确的。 流量在增长的同时带来大量的难题。第一个你必须解决的问题是怎么对付额外的流量。在 功能不够或者有效功能堵塞的时候,你运行应用程序的硬件会超载。我想起了最近的一个例子。 当大不列颠百科全书在线运行的时候,提供者将百科全书的全部目录设为可访问性的,由于查 询需求过大,在几个小时之内,该站点的系统全部崩溃了。该站点用了三个星期修复之后才重 新运转起来。不用说,你当然想避免类似情况的发生。 设想一个小组开发了一个基于Web的应用程序。他们的目标是获得尽可能多的访问者和使用 者 。然 而 ,当流量超过承受能力的时候,如果你惟一的资源全在该网站服务器上,那么你就会 有麻烦了。一个单一的网站服务器,不管能力有多强,都不能无限地满足访问景的增长。 静态负栽均衡组 从一个开发者的观点出发,解决可伸缩性问题最容易的办法是使用静态负载均衡。这可以 通过简单地吩咐一半客户机使用服务器A ,另一半客户机使用服务器B来实 现。然而这个经典的 做法也有许多问题。首 先 ,指导每个访问者到正确的服务器这个管理工作是昂贵而且很难正确 执行的。假想你有100位访问者访问你惟一的一个服务器,你觉得太慢,于是就增加了一个服务 器 。你肯定想平均服务器的使用率,每个服务器都是50个访问者。那么你就要告诉50个访问者, 你想要他们使用你的新服务器,然后他们必须回到他们的用户入口处,在对话选项框中输入新 服务器的名字才能成功。如果他们 不这 么做,那 会怎么 样?如果他 们做了 ,但 是做得 不 正 确 , 那又会怎么样?如果不是你所告知的访问者,而是另外一些访问者这样做了呢?这个结果不只 是浪费了你的时间和费用,也浪费了用户的时间和费用。并且你没有办法控制你的访问者该在 什么时候,怎么样,或者是否有效地到你的新服务器上去。还 有 ,当你的用户达到了200个的时 候 ,你得增加第三个服务器了,并且还要不停地这样增加下去。如果电话公司告诉你,你的区 号在一年内改变了两次,你会怎么做?想想你客户的这种感受。我认为我们的负载均衡算法容 易使用而且可以决定性地控制访问者和服务器的分配。静态方法却什么都做不了。 其 次 ,静态负载均衡的第二个问题是不能依照访问者的流量要求给他们分配服务器。设想382 第三部分组件管理与事务 某个州的一次暴风雪使学校停课,所有回家的小孩想点击你的站点。如果你按地理位置最近的 原则分配访问者到某个服务器,那么遭受暴风雪区域的服务器会严重超载,而另一区域的服务 器则在浪费使用能力。因为国际时区的不同,也会发生同样的问题。美国东海岸的用户平静下 来 的 时候,澳大利亚和日本的用户却是最活跃的时候。静态负载平衡算法不能跟踪用户负载的 较大 变化 。当超载服务器上访问者人满为患的时候,空载服务器却空转着硬盘在袖手旁观。这 种感觉就像你在一个忙碌的银行职员前排着长队时,却看见另一个银行职员却闲在那里修指甲c 我们的负载均衡算法令人感到满意的是,它能在任何时候都能依据系统的实际负载量而不是假 设负载童来工作。静态算法却不能这么做。我们所需要的是动态负载均衡,这样才能使资源得 到均等的利用。我很快就会谈到这一点。 20.2.2 有效性 有效性是另一个要考虑的因子,就是说所给的系统是否能满足访问者的要求。如果机器停 了或者不是正常运转状态,它就是无效的。你能信任一个单一的网络服务器吗?如果它死机了 会发生什么事情?你的工作停止了直到服务器的问题解决了才能恢复。所以只依靠单一的服务 器 ,有效性就存在问题。 如果你按合同制作应用程序,有效性 问题就更 为突出。你告诉你 的 客户 该 程序 是可靠的 。 你还可以用该程序所经历的长时间强度试验来证明它的可靠性。但 是 ,如果硬件失效或者别的 东西出了问题导致服务器死机了,你的客户也是不会高兴的。 微软的负载均衡技术可以在这里帮上忙。不只是因为它的分配工作使应用程序可用,而且 它在儿台计算机之间分配工作使应用程序的效率更高。如果一个企业应用程序有八台机器,其 中一台死机了,其余的七台还可以将正常工作维持在一个可接受的水笮。 静态负载均衡的第三个问题是不容易处理服务器的死机。将服务资源分为几组是一种使系 统工作获得一定的连续性和稳定性的想法。如果你将其分为四组,其中的一组 出 问题了 ,你希 望另外的三组能尽其所能地承担起所有工作量。但这不是静态均衡的工作方法。所有的访问者 在故障服务器那里失去了与你的连接。你可以给他们一个备用的服务器路径,但是前面描绘过 的管理问题又会回来骚扰你。你会非常喜欢你的负载均衡技巧,可以动态地跟踪硬件的有效性, 指导新来的访问者绕开断点到达有效的硬件服务组。 20.2.3 灵活性 灵活性也是被现在的市场所看好的一个方面。利用负载均衡技术,你可以随意地增加或减 少硬件。这在相对低的费用上给了你很大的灵活性,你可以增加多种硬件,并且这些硬件可以 按其类型、价格和性能来选取。 2 0 . 3 并行性和粒度大小 在 这 一 章 ,我已提到了工作单元和粒度尺寸。我将在这一节里较多地讨论这个题目。你将 学习到工作单元是怎么定义的,以及其粒度尺寸怎样影响负载均衡和服务器性能。这个题目广 义上讲称为并行性。并行性是指系统在并联的不同机器上运行的能力。第20聿 负我均衡组件 3SJ 当用户调用C0Createlnstance()函数时,工作单元就开始了。请求传送到负载均衡路由器, 负载均衡路由器决定将该请求委托给机器群中的哪台机器。路由器给客户机返回一个索引,结 束了它对该请求的工作。从这时起,客户机和由路由器所选的服务器继续进行工作交换。 只要客户机不中断该工作的联系,他就一直是连着指定服务器的。在该工作关系的整个活 动时间内 ,指定的服务器保持与对象的联系并处理所有任务。当 客 户 机最后 解 除联系 时 (通过 一个Release()语 句 ),工作单元就结束了。图20-1表示出这个过程。 通过对 CoCrcatelnstanccO 的调用,客户机开 始一个工作单元 本地平衡路由 接收客户请求 为服务请求做出决定和 选择机群 返回客户杉L的引用 指沄的服务器群为对象 服务肓到引用被释放 图20-1对一个请求的路由选择概述 粒度尺寸是一个表示工作单元大小的相对量。精细的粒度尺寸是指工作单元包含很少的任 务 ,而粗大的粒度尺寸指工作单元包含比较多的任务。也 就 是 说 ,粒度尺寸精细的工作单元通 过CoCreatelnsUncd )语 句 例 示 ,但 是 后 面 做 得 很 少 ,只 完 成 很少的几个 工 作命令,就调用 Release()语句结束了。粒度尺寸较粗的工作单元也是以一个CoCreateInstance()语句开始,以一 个Release()语句结束,但中间有 多 个 工 作命令(或者至少是完成工作命令需要花费大量的时间 和 精 力 )。 并行性举例说明 为了说明粒度尺寸,我先列两个极端的例子。一种 情况是 ,有 一 个粒度尺 寸很 细 的 任 务 , 在它的创建到结束只有一个简单的语句。另 一种 情况,是一个粒度尺寸很粗的任务,它要工作 几星期,包含了千万条语句。. 你会问要求一个好的负载均衡系统时,选哪一种情况较好。答 案是 ,“看具体情况而定” C 很 明 显 ,大部分负载均衡系统都能很好地完成细粒度尺寸的工作。如果说你有一个应用程 序由粗粒度尺寸工作单元组成。客户机的要求也令人满意,并且所有的东西都处于一个很好的 均衡状态。但是客户机必须保持几天或几个星期的连接。如果是这样,你的平衡就像静态平衡 一样是脆弱的。如果大量的客户机因为楼内电源断电而断线,你的负载就变得不平衡了。 那 么 ,细的粒度尺寸就好,是吗?实 际上 ,不 是 。细的粒度尺寸易于实现负载均衡,但是 它比粗的粒度尺寸情形要花费多得多的额外开销。细的粒度尺寸有过多的CoCreateIfiStanCe() 语 句和ReleaSe ( ) 语句请求。它们不做任何关于你的应用程序设计出来要解决商业问题方面的实际 工作。384 第三部分组件管理与事务 如果粗的和细的粒度尺寸工作单元同时发送各自的数据包,开发者怎样决定选取哪种路由 器?微软的建议书是这样写的:“不要为了优化负载均衡而改变原程序模型。” 微软的立场是这 样 的 ,将COM +的负载均衡功能设计为使粒度尺寸有一个宽适用范围。他们反对你对所用的程 序模型做任何修改,因为与你试图通过改变程序模型来获得负载均衡所带来的益处相比,一个 设计得好的程序模型更有价值。 2 0 . 4 动态负载均衡算法 我将用一些时间讲述负载均衡服务器怎样像一个算法一样实现负载均衡。如果你思考几分 钟 ,你会产生两个可行的方法。第一个方法是基于机器群中每个服务器的当前负载考虑的。路 由器应该保持跟踪每台服务器的负载。 当一个新的请求产生时,路由器分配该请求到机器群中 负载最轻的服务器。这 种 方 法 称 为 “实时响应算法” 。 第二个闪现出来的非常明显的选择方法是“循环” 法 。路由器持有一张机器群服务器清单, 它按照该清单依次循环分配请求到各服务器。 微软已经用一个简申的混合算法实现了这两种方法。路由器按指定的时间间隔轮询各服务 器 ,该 时 间 间 隔 称 为 “轮询间隔”。1 服务器被轮询的时候,其响应时间被记录下来。依据这个 响应吋间,路由器判断哪些服务器在重荷运行哪些服务器在轻荷运行。并按照负载由轻到重的 顺 序 ,将服务器列成表。然后路由器按照表的顺序循环分配请求到各服务器。该顺序表在一个 轮询间隔内一直有效。 注意轮询间隔可以决定算法的两种方法中的哪种可以起最重要的作用。例 如 ,如果轮 询间隔很小,那么路由器在下一个顺序表制定好之前> 按表的顺序分配请求并不能完成 一圈。就是说负栽重的服务器还没有分配到新请求,下一个顺序表就已经制定好了。在 这种情形下,算法主要是基于实时响应的。 另一种极端情形是轮询间隔很长。这种情形 下 ,在下一个顺序表制定之前,旧的顺序表会被执行好几轮c 这意味着一段时间以后, 虽然每个服务器的负栽改变了,但实时响应算法不起什么影响,路由器执行的是比较简 单的循环方式。 COM +仅仅在一个任务开始创建的时候执行任务均衡。如果该任务支持即时激活,那么一 个任务第一次在某服务器上创建后,该任务将可以经常在此服务器上即时激活。 F面是即时激 活的工作原理:当计箅机处于非活动状态的时候,服务器上的实际任务就取消了,但是客户机 端的 代 理 、服务器的存根和与其连接的通道都保存了下来。客户机与特定服务器的连接是不会 断 的 ,服务器端的组件也是这样。通过即时激活负载均衡组件配置,你可以简单地证明这个特 性。你同时还要设置组件为自动完成状 态,因为 组 件 对返 冋的数据不 能 自 己 设 为 非活动 。点 “ 创 建(Create)” 按钮创 建 该 任 务 ,实施COM +负载均衡。点 击 “调用(Call)” 按钮调用组件 GetMachineName(获得机器名)。每次你这样做,任务就即时唤醒了。你会发现在你重复点击调 用 按钮时 ,组件的位置并没有改变。点击释放按钮使客户机释放该组件、消除实际组件、代理 存根和连接通道。如果你再点击创建按钮,创建工作会再次请求负载均衡。我将在第21章当我 们用分布式网络体系结构实施工具包的时候,再详细介绍即时激活。第20章 负栽均衡组件 385 2 0 . 5 负载均衡组件设计 在设计一个支持负载均衡的组件时,你有必要避开任何形式的位置依赖性,因为组件永远 不会知道任何一台机器将要提早运行的时间。例如 ,你不能依赖特殊文件的位置,比如 C:\mydirectory\somefile,除非你能肯定每台服务器在相同的位置有相同的文件。 位置依赖可以是灵巧的。如果服务器A上的某任务在0900小时输人一个国外消息到数据库, 并且服务器B上的某任务在0901小时输入一个本地消息,哪个输人实际上先发生?这依赖于两台 服务器所使用时间的零点。如果某天这些服务器处于不同的地理位置,你或 许 想 写F 你的应用 程序,对所有的服务器用单一的时间标准运行,不顾每台机器自己使用的时间零点设置。总部 设在伦敦的巴克莱银行,或许可以让他们的服务器按格林威治时间运转而不顾他们的地理位置, 然而缅因州的自由港居民L.L.Bean先生要求的服务必须按美国东部时间工作。 因为你不知道下一个组件任务会在哪台机器上创建,在设计负载均衡组件时你必须非常注 意状态管理。例 如 ,一个任务可以通过从客户机过来的参数检测到其初始状态,或从机器外部 运行的数据库中检测到起初始状态。任务还可以检测与机器有关的状态,如通过构造器字符串 检测特定机器的DSN。 2 0 . 6 负载均衡组件客户机设计 客户机应用程序在创建一个已负载均衡的组件的时候,与创建一个负载不均衡的组件所作 的没有任何不同。客户机只是创建一个像主机一样能指定负载均衡的路由器任务c 客户机并不 知道由调用产生的结果是运行于它指定的服务器,还 是运 行于作者委托其创建任务的服务器。 简单地说,你几乎不想让你的客户机去关心他们的组件是否负载均衡。 客 户机必须知道,如果组件是负载均衡的,眼务器的死机并不一定意味着在服务器恢复之 前客户机就不起作用了。该应用程序客户机还可以连接别的能够接受其负载的服务器。假设一 客户机创建一个对象后,经过负载均衡,它实际连接上了服务器A。客户机的对象进行了儿个函 数调 用 ,但是服务器A的电源断了并&A死机了。在这种情况下,在下一个时间段,客户机仍然 发送一个访问消息,会出现连接超时,接着客户机收到指示出服务器实际上已经死机的出错消 息。这 时 ,客户机会释放这个任务并重新创建一个对象,路由器将该对象的请求分配给仍然在 正常运行的服务器。通过这些,A客户会认为是应该使用一个负载均衡组件,以防在服务器失效 的时候能释放并重新创建该对象。 2 0 . 7 坏消息 在使用Windows 2000试用版2的时候,我对其实现负载均衡的前景感到非常兴奋。所有的事 情看起来都正常,我期望负载均衡会在每个人的企业开发工具包里面。但 是 ,当第三个试用版 送到我这里时,负载均衡功能不见了。看起来像是微软感到在发行Windows 2000之 前 ,有他无 法解决的问题。我在他们的网站上发现了微软的如下解释: 在 1999年9月13号 ,微软宣布了将来从Windows 2000到即将出现的微软中央并行处理服务器 都会重新部署COM +负载均衡,称 为 负 载 均 衡组 件 (CLB)。这个改变影响了Windows 2000试386 第三部分组件管理与事务 用版3。对Windows 2000的高级服务和数据屮心服务我们提早使用负载均衡组件。这个工具包的 改变不会影响Windows 2000的发行时间和质摄。Windows 2000中的卨级服务和数据中心服务中 的网络负载均衡和Windows系列所保留的核心组件不会受到这个改变的影响。 从顾客 的反应 来 看 ,负载均衡组件成为Windows 2000程序的一部分得到 了肯定。但是这 将让我们围绕负载均衡组件在管理、监控和调度方面增加功能。依照客户的反馈意见,微软正 在开发中央并行处理 器 ,它是运行于微软的Windows 2000操作系统的髙效率展开和管理功能 的网络应用程序。直接按照客户反馈意见,中央并行处理器将把负载均衡组件当作它的核心功 能部分。 微软受委托开发一套可创建高度可扩展的和多层次网络的程序,该程序具有分类特点并且 运行于Windows 2000操作平台。该程序包括网络负载均衡和给负载均衡组件增加的视窗分类功 • 能。网络负载均衡担当一个前端客户机,分配从客户机传过来的IP消息到32台服务器。并且他 是一个为电子商业站点增加可扩展性和提高效率的理想工具。服务组充当一个后端客户机提供 高效率的应用程序如数据库、与文件及打印服务。带有组的服务Windows 2000高级服务器版支 持双结点族,而且数据中心服务支持4个结点族。 使用我们发放的Windows 2000试用版2,顾客可以利用网络负载均衡和视窗分类程序建立 可扩展的多层次组方案。微软扩展的分类特性新功能,由负载均衡组件通过控制中间层或商业 逻辑层的负载均衡来补充。 当我们发放中央并行处押.器的 时候,负载均衡组件会成为视窗分类 方案的关键。 在我写这本书的时候,Windows 2000不再具有负载均衡能力,并且中央并行处理器也还需 要几个月才能发放。这将使解决负载均衡问题的时间延长,这不是一个好消息。 2 0 . 8 不用中央并行处理器的负载均衡组件 某些人不能等待中央并行处理器,需要现在就解决负载均衡问题。我已经开发了一套自己 的系统,你可以考虑用它解决你现在令人非常着急的负载均衡问题。 2 0 .8 .1 用SCM工作 本节的解决方案是模仿C O M +均衡服务功能。其 U标是使客户生成一个发送对象创建的请 求并送到一个独立服务器,该服务器委托服务器族中的某服务器接受该请求。第一个要解决的 问题是怎样围绕视窗服务控制管理器(简称SCM)工作。设il SCM不是用来将创建请求从远程 客户机传送到远程服务器的c 我们可以考虑控制管理器服务的新使用方法,但这需要做大M 的 工作。 然而在服务器端将会有更简单的更改方法。SCM定位服务器的代码,它崙要创建完成一个 任务的请求。第一步娃在分类表中找到一个分类对象。第二步是在登录库中搜寻能将分类对象 放入分类表中的可执行代码。通过在某服务器上预先注册一个可执行的分类对象来完成你所给 的 可 执 行 代码,你就可以跟踪SCM。 如果你对某些 给定 的分类,编 辑登录库 ,你同样可以使 SCM使用你想要的服务器。 用这个方法,SCM会执行可达到负载均衡的代码。它通过进一步给别的服务器创建请求来弟20章 负我均衡组件 387 完成。我们首先从特定分类对象开始: class ClassObiectShim : public IClassFactory CLSIO m_clsid; public: ClassOb}ectShim(CLSID clsid) : m_clsid(clsid) {} virtual -ClassObjectShim() {} II IUnknown STDMETHODIMP Querylnterface(REFIID riid, void **ppv); . STDMETHODIMP_(ULONG) AddRef (); STDMETHODIMPJULONG) Released; II IClassFactory STDMETHODIMP Createlnstance(IUnknown *pUnkOuter, REFIID riid, v o i d **ppv); STDMETHODIMP LockServer( BOOL bFlag); } ; 有一个要注意的事情是它的实现存储了一段CLSID代 码 。该代码可以作为 动态类 型使 用 。 CreateInStance( )方法完成了类的大撗工作。其实现程序如下: STDMETHODIMP ClassObjectShim::CreateInstance( IUnknown *pUnkOuter, REFIID riid,void **ppv) { *ppv = 0; COSERVERINFO CSi = { 0, 0 L E S T R(“MyServer“), 0, 0 }; MULTImqi = { riid, 0, 0 }; hr = CoCreateInstanceEx(m_clsid, 0 , CLSCTX_REMOTE_SERVER, &csi, 1, &mqi); if (SUCCEEDED(hr)) { (*ppv = mqi.pltf)->AddRef(); mqi.pltf->Release(); return hr; > 任何时候这个程序的实现都被记录为一段CLSID,其创建的请求被送到“ 我的服务器 (MyServer)^下面是一个可执行服务器所做工作的简单例子: server that does this: // LoadBalancer.exe388 第三部分组件管理与事务 int WINAPI WinMain(HINSTANCE, HINSTANCE, LPSTR, int) { HRESULT hr = Colnitialize(0); if (SUCCEEDED(hr)) { ClassObjectShim cos{CLSID一B a l a n c e d ) ; DWORD dwReg; hr = CoRegisterClassObject(CLSIO Balanced, &cos, CLSCTX_LOCAL_SERVER, RHGCLS_MULTIPLEUSE, &dwReg); if (SUCCEEDED(h r ” { MSG msg; while (GetMessage(&msg, 0, 0 , 0)) DispatchMessage(&msg); CoRevokeClassObj ect(dwReg); } Collninitialize(); } return hr; } 如果这段可执行的文件预先在服务器MyCLBS上运行,所有客户机创建均衡对象的请求会 被预置了CLSlD_Balanced的ClassObjectShim程序完成。在前面所示的Createlnstance实现程序的 基础上 ,请求会送到MyServer上的SCM,并在那里完成。如果平衡类注册在MyCLBS上 ,并且 Loca丨Server32指向代码如前面所示的文件LoadBalanccr.exe,服务器就会自动开始执行。这个程 序比COM+的情形要稍微慢一点点,因为在CLBS上 ,从SCM到提供对象类通道的COM服务器有 一个额外的LRPC调用,这个调用是必要的。但对于在没有COM +设备工作的时候,获得负载均 衡的基础设施,这只是一个很小的代价。 20.8.2 CoCreatelnstance 带来的问题 如果客户机通过调用CoGetClassObject() 函数和IclassFactory::CreateInstance()函 数,方法 1 仅仅做大S 的外围工作。例如,客户机通过调用一个运行于MyServer上的Balanced实例来结束: HRESULT hr; COSERVERINFO CSi = { 0, 0LESTR(,,MyCLBSM), 0, 0 > ; IClassFactory *pcf = 0; hr = CoGetClassObject(CLSID_Balanced, CLSCTX_REMOTE_SERVER, &csi, IIO^ldassFactory, (void**)&pcf); if (SUCCEEDED!hr)) { IBalanced *pb = 0; hr = pcf->CreateInstance(0, IID_IBalanced, (void**)&pb); pcf->Release(); if (SUCCEEDED(hr))第20章 负栽均衡组件 389 II use object on MyServer pb>Release(); } } 然 而 ,如果客户机程序是用CoCreatelnstance( ) 函数或CoCreatelnstanceEx()函数写成的, 它就不会工作。 HRESULT hr; C0SERVERINF0 csi = { 0 , 0LESTR(“MyCLBS-), 0, 0 >; MULTIJH mqi = I &II0_Ibalanced, 0, 0 }; IBalanoed *pb = 0; hr = CoCreateinstanceEx(CLSID—Balanced, 0, LSCTX_REM0TE SERVER, &csi, T, &mqi); if (SUCCEEDED(urj) { (pb = reintf rpret_cast(mqi.pltf))-> A d d R e f (); mqi.pltf->P.elease(); II use object on MyServer pb*>Release(); > 不幸的是,当前CoCreateInstance() 函数或CoCreatelnstanceEx( )函数的实现不支持返回一个 运行于服务器上的对象所参考的对象,而只支持CoCreateInstance() 函数或CoCreateInstanceEx() 函数本身所运行的目标对象。换句话说就是,如果一个访问者的客户机从称为MyCLBS远端服务 器上调用CoCreateInstance() 函数或CoCreateInstanceEx()函 数,并且MyCLBS上运行的目标类试 图返回一个创建任务索引到MyServ灯 上 ,客户机的调用通常会失败并返HRPC_E_INVAUD__ OXID,即,“找不到指定的输出目标。” OX1D是一台相关机器上对象存储区域的标识符。当一个接口指针指定在上下文环境中传输 时 ,被一个称为0 B JR E F 的低层数据类型标示出来。它是中性环境的并可以在导线中传输。大部 分对象在其接口指针通过环境传输数据的时候依赖于COM 的标准排序测试。 当他们的接口指定 好了位置后,标准OBJRER 的结果就包含了他们的OXID。如果某OBJREF没有指定环境传输的 目的单元,那么接收机将OXID 转换成一串PRC字 符串。其中要回调存在OXID 的 机 器 ,该机器 的地址也被编为标准0 B JR E R 。绑定的串可以用来建立一个RPC处理串 , RPC 串可以远程回调原 来机器上的OXID 进程。 服务器端的CoCreatelnstance() 函数或CoCreateInstanceEx()函 数 (是一 个交叉环 境调 用 ) 进行检查,以确认从执行机器返回来的OBJREF中的OXID 是可用的。(执行机器通常也是客户机390 第三部分组件管理与事务 呼叫的初始目标机器。)如果OXID 不在那台机器上,CoCreateInstance( ) 函数或 CoCreateInstanceEx()函数认为这个OXID是不可用的,因此返冋RPC_EJNVALID_OX:lD。所有 的对象必须同等,让人觉得COM的原理不太正确,但是用这种工作方式无疑是有合理的原因的。 因为它避免了在自己的机器上从一个对象类中返回一个索引对象的问题(这是你在MTS、ATL 和Visual Basic中做循环、单线程 、线程单元池的方法),但它能够从另一台机器的环境中返回一 个对象参考。这种情况不能也不准备在近期内做任何改变。 当一台客户机使用CoGetClassObjecK )的时候不会有问题,因为它返回一个对象到客户机所 呼叫的机器。不幸的是,绝大多数的COM 客户机程序都是用CoCreatelnstancd ) 函数或 CoCreateInstanceEx()函 数 写成 的, 或者是Java和用Visual Basic语言 写成 的小程 序 。因 此 , CoGetClassObject()的工作与实际环境的境况相差很远。 2 0 .8 .3 创建一个包套 CoCreateInstance()程序通过查找,得到被返回的对象参考的OXID。通过将返回的对象放置到 另一对象中,并返回一个替代的对象参考,该0XID消息可以隐藏起来D 下面是IEnvelope的接口 : [ uuid(5E7F74C0-E165-11D2-B72C-00A0CC212296), object 1 interface IEnvelope : IUnknown { [propput] HRESULT Letter([in) IUnknown *pUnk); }; 我修改了我特定类对象的Createlnstance实 现 ,以隐藏它想返冋到在局部创建的包套中的 对 象 。 如果该包套使用标准的编排序列,因为包套存在于客户机呼叫的机器的环境中,所以 CoCreatelnstancy )函数能够成功。但是,客户机将以一个在错误机器上以错误方式运行的对象 参考结束。实际上客户机所要的,当然是在最终目标服务器MyServer上运行的对象的参考c 通 过自定义的编排序列是可以做到的。 当远程访问试图编排一个接U 指针的 时候,它做的第一件事是通过调用Querylnteiface和请 求【Marshal检查对象运行的自•定义编排序列是否正确。如果该对象实现了这个接口,远程控制就 委托它将任务写到可中立环境0BJREF中。包套可以为MyServer中的活动远程控制对象自己定制 编排序列,并像标准0BJREF—样传输。 如果包套使用自定义的编排序列,CoCreatelnstam^ )函数依然能够成功,因为它只为标准 编排的0BJREF检查0X1D。虽然他们现在是在客户机进程的内部运行,客户机依然以错误的对 象类型结束。这个问题可以用一些灵活的手工技巧解决。 当一个对象自定义编排序列时,远程控制程序要求得到一个标示类的CLSID,该类具有解 释写到客户机0BJRER中的内容的能力。 当 在 0 标环境中收到顾客的0BJREF时 ,该类的一个 例子被示范出来,并要求解释那些数据并且返回一个参考给对象。通常有这种情况,解释自定 义编排序 列 的 对 象返 回一个参考给自己(当运行用数值编排的序列时更典型),但这并不是需 要 的。$ 2 0 t 负栽均衡组件 391 没有编排的对象可以随意返回一个参考给任何对象,并且其中有一个假代码c 如果 包 套6 定义编排序列,当它解除编排冋到客户机进程中时,它通过解除标准OBJREF的编排脱去ft己的 伪 装 ,并为实际的远程控制对象返回一个参考给域终代理。这是IEiwelope接门预先显示为使用 只写属性的原因。因为包套可以自己打开,所以没有必要再介绍Pmpget方法。 包套在GetUmnarshalClass中返回自己的CLSID,可以例示本类解释自定义负载量的例子。 在GetMarshalSizeMax() 和MarshalInterface()中 ,包套委托自定义的API为代表,API是标准编 排 的 ,于是它在自定义编排数据的时候写一个标准的OBJREF。在Unmarshallmerface()中 ,1 包 套从CoCreateInstance()或者CoCreateInstanceEx()中返回时,包套解除自定义负载景并给最终对 象返回一个参考。在这种情形下,代理指向实际的远程控制对象,不管该对象在哪里运行C 使包套欺骗性地工作需要在CLBS服务器和每一台客户机上注册该包套的类。这是一个附加 的负担,但是只需一个非常小的代价就可让CoCreateInstance()和CoCreateInstanceEx( )做发送对 象的工作。 实际上,这还有一个附加的代价。 ft定义编排序号的接口IMarsha丨包括ReleaseMarshalDataO 方 法 ,如果它在接收机的环境中解除负载量自定义失败,它就会被远程控制体系结构调用c 这 个调用给自定义编排对象一个机会,用来清除现存的服务器端资源。看一个例 子,如果一个自 定义编排序列的对象用套接字传送数据,那么最后应该给对象一个机会来关闭设立在 Marshallnterface( >中的套接字。 不幸的是,对 用数值编排的 对 象 (如包 套 ),这个方法不会被调用,因为服务器端的对象拷 贝已经被破坏u 对大部分的数值编排对象,这并不是一个大的处理工作,因为他们并不传输参 考给对象。包套并不传输对象参考,所以如果解除编排失败,参考就漏掉了e 幸 运 的 是 , COM+的无用消息收集器在六分钟内清理完这些漏掉的参考,所以这不会成为问题c 2 0 .8 .4 算法 用一个代理服务器主机来发送对象和包套,需要实现负载均衡的基本架构已经完成了c 所 缺少的只是用来选择远程控制机器的算法。敏锐的观察员会注意到我的所有代码迄今为止总是 向MyServer创建请求。有大量的方法可选择一个服务器来发送任务,包 括 (当然不只是)随机 法 、循 环 法 、CPU负 载 法 、调用时间法和在网络上汸问的扩张法。有这么广范的选择,负载均 衡算法应该使得这些主要算法能像一个可插人的组件一样都能随时起作用。 对我自己的可插入性算法,如下定义接口 : I uuid(741F3750.E3B1-11d2•8117.00E09801FDBE), object 1 interface ILoadBalancingAlgorithm : IUnknown { HRESULT Createlnstance([in) REFCLSID rclsid, [in] REFIID riid, [out, iid_is(riid)) void **ppv); 假设有一个预先创建的使用该接口的例子,就可以写出传送对象的方法Createlnstance。 怎样实现Createlnstance()取决于ILoadBalancingAlgorithin的具体实现。给定一个可用的服务392 第三部分组件管理与事务 器 清 单 (m_rgwszServers ) 和一个数据成员的计数值(m_nCount) , 一个随机算法可以如下实现: STDMETHODIMP CRandom::CreateInstance(REFCLSID rclsid, REFIIO riid, void **ppv) • { *ppv = 0; COSBRVERINFO csi = {0>; csi.pwszName = m_rgwszServers[rand() % m_nCount]; MULTI_QI mqi = { &riid, 0, 0 }; -HRESULT hr = CoCreatelnstanceEx(rclsid , 0, . CLSCTX_REMOTE_SERVER, &csi, 1, &mqi); if (SUCCEEDED(hr)) (*ppv = raqi.pltf)->AddRef(); mqi.pltf->Release(); } return hr; } 循环算法的实现与此相似,但我真心喜欢COM+的负载均衡风格,所以我最喜欢的算法还是 时间方法。记 住 ,在Windows 2000中 ,时间算法设立在拦截器中,它可以在运行COM+的环境 下封装每个对象的执行。完成这样的事情,需要深入到COM+的技术包中。 2 0 .8 .5 时间方法 COM+支持一种称为通道钩结构的非文件特征董。列 如 ,在Win32的数据头文件中他们是一 些半文件。微软在Windows NT4.0和Windows 2000中不再官方地支持通道钩。如果现在你对这 些东西不感兴趣或者害怕了,那么现在可以直接跳到下一章去。如果你坚持学习它,那么你就 是已经承认不放弃了,我将进行详细的描述。 通道钩是 一 个 在COM+进 程 中 注 册 的 对 象 ,进程 给 出 了 在请求中背出数据和像每 个远程 COM调用的一部分一样直接应答RPC送出的消息的机会。为支持时间方法,我建立了一个不发 送任 何数据的 通道钩 , 但是它记录一个呼叫到达服务器的开始时间,并 且紧随 着呼 叫的结束, 测量服务器所用的时间。 通道钩保存工时数据的堆栈结构CALLINFO: struct CALLINF0 { time_t tStart; // GUID guidCausality; II struct CALLINF0 *pNext ; II II start time of call current causality pointer to next callinfo on stack 它存储在本地线程存储器(TLS)中。当任何呼叫到达服务器的时候,通道钩的 ServerNotify方法就被调用。它创建一个新的CALLINFO结 构 ,并用当前时间和因果性关系将其弟20章 负我均衡组件 J93 初 始化 ,然后将它添加到堆找TLS中。 当一个呼叫准备离开服务器的时候,通道钩的ServerFillBuffer()方 法 被 调 用 (实际上是调用了 ServerGetSize() , 但是因为它指向一个非零长度,就调用了5 6 1 ^职 1出11仔61*())。ServerFillBuffer() 将顶部CALL1NFO弹出堆栈并查找余留下的节点,看看是否还有跟它有因果关系的CALLINFO。 如果 找不 到别 的 ,CALLINFO被弹出堆栈成为一个最高级的调用。ServerFillBuffer( )计算出 CALLINFO的开始时间和当前时间的差,并 将 他 们 赋 给 两 个 公 共 变 量 (后面将 更 多 地 讲 到 它 们 )。如果ServerH丨lBuffer( )找到了 一个匹配因果关系的CALLINFO,这个 CALLINFO将被弹出堆栈代表一个嵌套的调用。完成嵌套调用所用的时间自动包括在顶层调用的 时间内。 为使时间方法的可行,通道钩需要载入到服务器的进程中。因为我不想对服务器的代码作任 何 改 动 , 我选择通过一个代理/存根动态链接库(DLL)载入通道钩。这只需要极少的工作量;代 理/存根的代码将连接到一些称为NewDllMain的能提供一个新动态链接库人口地址的附加代码。 NewDllMain()创建- 个称为装入程 序 (Loader)的范例,它由通道钩动态链接库执行。创建 这个对象是为了加载钩动态链接库,它必须在代理/存根动态链接库已注册的那台机器上注册。在 DllMain()的运行中,创建并注册该通道钩对象。完成这些之后,释放加载对象;因为通道钩动态 链接库的DllCanUnloadNow()的 运 行 通常 返 回“错误(FALSE)” 消息,所以没有必要再持有它了。 这个程序要做的最后一件事情,委托由代理/存根底层结构提供的DUMain()函 数,代理/存 根底层结构在由MIDL编译程序生产的dlldata.c文件中。这个函数必须被调用 , 以给代理/存根动 态链接库一个初始化自己的机会。NewDllMain()函数取代最初的DllMain()函 数,入口连接开关 用来再变换入口地址 , 以使代理/存根动态链接库能通过入口进人NewDllMaiii()函数。文件编写 同时编译和连接MethodTimeHookPS.cpp,该文件含有新人□地址的代码。 如果是缺省值, 自动化对象链接和嵌入接口按照它们类型库中的消息,使用通用的排列码 建立空闲的代理和存根。只需要简单地建立一个标准的代理/存根动态链接库代替他们,时间方 法通道钩就可以用该接口工作。因为端口的发行向导定义不在应用终端语言(ATL)创建的IDL 文件库中,所以应用终端语言使得这更简单。MIDL编译程序对任意不在库中定义的接口生成代 理/存根代码,所以代码已经在这里了,只需要等着使用就行了。Visual Basic直接生成类型库, 但是IDL可以使用Ole View或者一个类似的工具做逆向工程设计。在安装的时候,自动化对象链 接和嵌入接口代理/存根动态链接库: 要在服务器嵌人类型库之后注册,这样类型库的注册消息 不会覆盖他们的注册消息。 , 通过简单地将时间方法通道钩动态链接库直接连进服务器进程,所有这些蹩脚的代理/存根 工作可以避免。但是这需要插人进入服务器的启动顺序的NewDllMainO,这是我试图避免的。 HRESULT hr = CoCreateInstance(CLSID_Load6r, 0 , CLSCTX—INPR0C_SERVER, IID_IUnknown , (void**)&pUnk); if (SUOCEEDEO(hr)) punk->ReIease(); 2 0 .8 .6 时间方法的算法 前一节介绍了时间方法通道钩怎样收集COM调用的相关数据。这些信息担当将用时间方法 类包装起来的时间方法算法输入的任务,该算法实现可插入性接口算法ILoadBalandfigAlgorithm。394 第三部分组件管理与事务 它保存了一个可用服务器清单并定期地挑选一个负载最轻的服务器。为完成这个任务,该算法需 要收集每个服务器中存储的时间数据。请注意,由通道钩算法ServerFiUBuffeK )更新的公共变量 处于映射到共享存储器特殊段中。 #pragma data_seg(“Shared“) long g_nCount = 0; long g_nTime = 0 ; #pragma data_seg() #pragma comment(linker, “/section:Shared , r ws“) 这 意味着,通过一台加载了通道钩动态链接库的给定机器上的所有进程,存于g_nC0UiU和 g—nTime中的数据是共享的c 从特定服务器中检索这些消息,变成了一个简单的在加载了通道钩动态链接库的服务器上 的进程中例示对象的问题。该对象对方法的一个调用会返回一个响应数据。被通道钩动态链接 库显露出来的装入程序类用来完成这些任务。下面是其接niLoa.der的介绍: t uuid(233108A2-E3CD-11D2•8117•00E09801FDBE), object 1 interface ILoader : IUnknown { HRESULT GetAverageMethodTime( lout, retval] long *pnAvg); }; 为 使一 个装人程 序对 象 能远程 创建 ,我使通道钩动态链接库支持使用标准COM代理进程 dllhost.exe来激活的方法。 MethodTiming()的运行结果是使用每台服务器上的远程装人程序对象收集时间数据。每次 取到 该数据,算法就使用反应每台机器当前状态的新消息来决定发送工作任务给哪台服务器。 它会发送所有的请求到那台服务器,除非它另外找到一个时间状态更好的服务器。所有这些任 务都在独立的线程上完成,以至于不使客户机创建请求的速度变慢c 研究时间数据和选取服务器的实际过程需要多花点精力。我找不到基于时间方法的负载均 衡算法 文 件 ,于是我自己杜撰了一个。它没有我想要的那么协调,但比 开 始的 时候要 好一 些 。 实质上 , 装入程序对象每个时间间隔钩取时间数据以返回“ 任务时间 ” , 这 里 “任务时间” 是指 所有测得的在该时间间隔内结束的COM调用所花的总时间。时间间隔由对象的线程算法控制, 线程算法每隔半秒钟轮询一次服务器。装入程序也按数据的表示长度轻微地修改信号,并钍其 内含的整数部分意味着分数数值将被丢掉。 像前面注意到的一样,所有的数据收集和分析工作在一个独立的线程中完成,以至于不影 响客户机创建请求。这个线程在时间对象初始化和执行MethodTimeMonitor函数的时候开始进行。 该函数通过一个指针传递给创建它的MethodTiming对 象 。每隔半秒线程唤醒一次并轮询对象服 务器表中的每一台机器。该表是一个HostTimelnfo结构的数组,它的每一条包括一个服务器名, 一个远程装人程序运行于该服务器的参考 , 和一个当前方法的平均时间:第20章 负载均衡组件 395 typedef struct HostTimelafo { OLECHAR *wsz; ILoader *pl; long nAvgMethodTime } HostTimelnfo; 线 程 依 照 数 组 (m _rghti)顺 序 ,调用每台服务器的装入程序来获得最后的时间数据,并将 他们的最低值存为一个索引(m_phti )。(一个更复杂的程序将独立轮询每个线程。) 请注意,仅仅通过应用当前时间值和以前读取的值之间的四分之一增墩,这将减弱时间变 化的程度。MethodTiming对象的函数Createlnstance()调用传输类对象的方法: STDMETHODIMP CMethodTiming::CreateInstance( REFCLSID rclsid, REFIID riid, void **ppv) { C0SERVERINF0 CSi = {0} ; csi.pwszName = m_phti->wsz; MULTI_QI mqi = { &riid, 0, C }; HRESULT hr = CoCreateInstanceEx(rclsid, 0, CLSCTX_REM0TE—SERVER, &csi, 1, &mqi); if (FAILED(hr)) return hr; (*ppv - mqi.pltf)->AddRef(); mqi.pltf->Release(); return hr; } 通过m_phti指针的识別,创建的请求传送到当前负载最轻的服务器。 我所作的所有关于轮询频率的结论和数据,全部是我在使用我的客户机和服务器迸行研究 的基础上得出来的。在别的情况下,测鲎值可能会改变。 2 0 .8 .7 负载均衡的实现 我所实现的负载均衡服务器原型使用这里所描述的所有技术,但这还有一些另外要注意的 事情。我运行基础的核心是安装于CLBS上的Windows N T ,在CLBS上为负载均衡类注册了传输 类对象。该负载均衡服务用到了特定的算法和一个服务器列表,这两项都定义于下面的注册表 项目 : HKEY_LOCAL_MACHINE\Software\DevelopMentor\LoadBalancing\RoutingServer (RoutingServer是CLBS的早期名。)RoutingServert建下面的子键指定客户机应用程序的服务器。 默认算法名的算法结果指定实施ILoadBalancingAlgorithm类的ProgID和CLSID。提供了四种 实现方法•. 随机 法 、循环法 、时 间 方 法 (这是我所描 述过 的 )和CPU负 载 法 (基于性能监视器 经过性能数据帮助库统计的状态)。 最后 ,因为负载均衡服务需要呼叫远程服务器,所以它不能像简单的系统一样运行。它必 // name of machine II Loader on machine ; If timing data for machine396 第三部分组件管理与事务 须在不同的用户账号下执行,才是其真实的工作情况。 由于带有COM+,类必须配置为支持负载均衡。指示他们要求的类必须通过在CLBS的新类 型项下注册以达到负载均衡。因为类有跟负载均衡服务器相同的AppID,所以它也必须注册以避 免激活标识出问题。如果它们不注册,服务器注册类对象的意图会成功,但客户机注册类对象 的意图却会失败,返 回 “CLASSJE_CLASSNOTREGISTERED”。带有COM+的客户机必须发送 创建请求给运行了负载均衡服务的CLBS。 2 0 .8 .8 其他均衡和分类技术 这还有几个关于COM+负载均衡基础结构和非预制性实施程序的普通话题。首 先,它有助于 理解服务、负载均衡和分类技术之间的关系。 NLB服务是一个少为人知的给Windows NT企业版的附加项,它是微软从帷幔研究室 ( Valence Research ) 买 来 的 (其最初的产品称为护航队)。它给由32台机器组成的计算机群提供 TCP服务负载均衡,从一个客户机的角度看过去,这32台机器表现为一个相同的IP地址。所有的 机器都能观测到到来的客户机连接请求,但由一个非文档形式的算法决定哪台服务器给客户机 提供服务。这个节点提供进一步的工作,直到该连接断开并有一个新的连接建立之后才停止。 Windows集 成 服 务 器 (WCS ) 通过用一个公共的IP地址配置一对互成镜像的服务器提供多 余的消息。两台机器连接起来共享挂在公用总线上的硬盘。如果一台出故障了,另一台非常快 地取代前者连续存取数据。 > NLB和WCS两者都有我在这里所描述过方法的类似负载均衡服务组件;它们三者都由客户 机发送请求到一个独立的IP地 址 ;它们都用多台并联服务器给这些请求提供服务。下面是它们 不同的地方。 首 先 ,负载均衡机理组件提供比NLB更精细的控制。因为它们的动作是基于CLSID参数化 的。NLB没有给定连接目的的概念,意味着它不能区分HTTP请求中送到的统一资源定位器,所 以它同等地处理所有的请求和计算机群中的机器。 其 次 ,负载均衡机理组件能通过两个以上的结点展开工作,而WCS就 不 行 。而另 一 方 面 , WCS结点共享硬盘,这对数据库存储服务和组用软件消息储存是非常重要的,但对大多数COM 服务器益处不大。 最 后 ,也 是最重要的 ,虽然三种方式对客户机的请求都使用一个独立的IP地址。但是NLB 和WCS群中的机器是共享这个IP地址的 ,而负载均衡组件群中的机器不共享这个IP地 址 。后者 的IP地址是 定 义 给 一 台独立的 机 器 (CLBS)的。如果这 台 机 器失效,那么全体服务崩溃。给 NLB或者WCS增加一个CLBS可以解决这个问题。 2 0 .8 .9 运行时编译执行技术活化 本节有助于理解下面两者之间的关系,COM +负载均衡基础结构或别的非预制性实施程序, 与MTS和COM +的运行时编译执行技术活化能力。对每种运行环境,一个对象在一次调用过程 中可以通过设置其完成位(通过 SetComplete( ),SetAbort() , 或在往后用SetDeactivateOnRetum( )) 将其设为非活动状态。^ 2 0 t 负栽均衡组件 外7 3 调用完成的时候,截取层释放对象。该对象被回位成为空闲状态,等待客户机产生的下 —次 调 用 (因此有运行时编译执行技术的称谓)。因为截取层停留在那个位置,新对象通常不停 地在同一台机器上同一个进程中的相同环境中进行重建,这就不存在负载均衡了。 负载均衡发生在连接创建的时候,不是在对象活动的时候。为了在对象活动的时候也支持负 载均衡,处于下层的远程控制层必须能够更新现存的用新网络地址发送任务的代理。每台带有现 存代理的机器上用OXID分解器保存的记录也必须更新,以使散布的无用消息收集器连通性检测 命令协议可以继续工作。这两者都是不平常的任务。但更复杂的是潜在的性能瞬时干扰,特别是 对连续不活动对象f t己。在每个方法调用中都完成这些工作,使得方法调用变得非常昂贵。 解决方法是让客户机释放其代理,断 开连 接 ,并通过新的创建请求重建连接,给负载均衡 器一个恢复均衡的机会。起先这看起来像一个令客户机不愉快的负担,但万一•它们失去了与特 定服务器的通信联系时,它们应该准备这么做。一些灵巧的代理可以隐藏从客户机来的详细数 据 ,但现在,我们将这个任务留给开发者自己处理。 2 0 . 9 小结 负载均衡可以解决很多企业应用程序中的问题。它 提 供 可扩 展性 、有效性和灵活性。负载 均衡不需要对原程序模型做任何改动,因此设计得好的组件已经有了负载均衡功能。 不幸的是,Windows 2000没有装载负载均衡服务功能。这是因为微软在发行Windows 2000 之前不能对付围绕负载均衡的所有问题。中央并行处理服务器将有负载均衡服务,并 a 具有负 载均衡之外的比任何东西更强的管理功能。但 是直到现在 ,你只能像以前一样依靠你自己的企 业应用程序中的资源进行负载均衡。 但 是 ,如果你在阅渎本书的时候能得到中央并行处理服务器,你 就幸运了。你在本章中所 学的原理和技巧将给你很大的帮助。第21章优化Windows DNA应用程序 本章包括以下内容: • 估计你的需要 • 优化技巧 • 使用Windows DNA工具包 • 检查测试的结果 开 发一 个企业 应用 程 序是困难的 ,因为往往需要好几个月的努力。在辛苦的开 发期之后 , 你还要经过一系列的测试过程,查找该程序在重负荷运行时陷入困境的情形。 为了解决问题,进入你大脑的第一件事情是给服务器升级,以实现该程序方案的正常使用。 但这在硬件和网络管理方面付出的代价是昂贵的,并且服务器总有停机的时候。这是你的客户 所不愿见到的。 如果你够幸运的话,你会容易找到一些可以获得性能增强的方法。你甚至能找到一些使审 查时间相对小的技巧。一些简单的事情如从一个文件到系统DSN的 改变,可 以 造 成 h 面所有变 化的结果。 这种简单修改的方案在大部分软件开发工作宰里会时不时地使用。一个简单的改变可以获 得性能的巨大变化并节省整人的工作时间。怛想要程序取得更大的期望值,会令你做许多的无 益 工 作 ,最终只 是发 现它 不 能 在重荷条件 下 运 行 。本章讲 述一 些Windows网络体系结构分布 ( Windows DNA ) 效能问题,并告诉你在实用之前怎样测试你的应用程序。 我将从一些Windows DNA的效能话题开始. 讲•一些你可以立即使用的优化技巧。我也将谈 一些使用微软Windows DNA效能工具包,优化你的Windows DNA应用程序的方法e 既然COM+ 是Window DNA的主体内容,DNA工 M•包自然也可帮助测试你的Windows DNA内部结构组件。 2 1 . 1 估计你的霈要 在你猛地冲进Windows DNA优化工作领域之前,你需要明确你的目标c 也有可能你的应用 程序不需要优化就已经好了。你需要的最重要的数据是应用程序必须支持的每秒处理量(TPS)。 它由两个变量决定:应用程序的判断时间和当前用户数量。 判断时间是每个处理的人机交互作用总时间。这 包 括读敁示器 、做决定和做选择或输入的 时间。如果你的应用程序是一个订货登记系统,判断时间是一个用户在用户界面浏览目录、决 定买什么并选想要的项H所花的时间总和。 计算所需的TPS,用户数除以判断时间,用如下公式表示: • 用户数 每秒处 理量 (TPS)=------------ 判 断 时 间 (秒 )第2/ 章 优化 DNA应用程序 399 手中有了TPS目标值,你可以判断你的程序能否承担设计方案的负荷童。该判断可以是你自 己通过测定得出的结果,也可以是使用Windows D N A 性 能 工 具 包 (在本章的下半部分将更多地 谈 到 它 )测定的结果。到此,问题还没有结束,当你测试并发现所有的东西在未来的12个月内 都能够很好地工作时,还要考虑下述问题: • 在程序升级之前的任何时候,会不会有流量增加超过程序的容量问题? • 你会依靠当前的组件版本设计下一版本的程序吗? • 会有别的程序配置在同一服务器上,负面影响该程序的工作能力吗? 在第一点上,我发现流量增长超过期望值是少有的。这并不是说它就不会发生,但管理和 市场部门通常有任何情况作出调整的能力,所以方案永远是令人充满希望的。现实中的问题是, 我在管理任何项目的时候一直是希望流量达到期望值。 在第二点上,管理部门经常认为,在下一个版本的程序中你仍然能使用原来的组件而不需 要 更换。他们没有认识到,去年发放的性能合适的东西在下一个版本的应用程序中可能成为瓶 颈。基于 更新、更有效工艺技术的新组件超过基于旧工艺的组件,这是一个普遍的规律。在配 置组件的时候,你要决定是否期望它在新版的应用程序中继续使用,而且它能否满足流最增长 的需求。你需要或不需要判断,是否有对方案的约束条件进行修改的必要。 第三点有点困难。一台本来精力旺盛的服务器容易因为过度利用而损坏。当某人的预算削 减的 时候,他的第一个目标就是服务器能够通融其增加一个附加的应用程序。如果在你的程序 所完整运行的服务器上安装另一个程序,你马上就要面对你的程序不能承受的效率瞬时干扰问 题 。为将来可能发生的事情做个准备,我已经被这种情况损害过好几次了。 2 1 . 2 最优化技巧. 在这一节,我将谈论一些优化Windows DNA应用程序的具体方法e 通 过 -些 有 帮 助 的 优 化 暗示 ,例如使用用户或系统DSN代替文件DSN,避免注测表存取访问,使用运行时编译执行技 术 (JIT)激 活 ,和一些别的能增加你的DNA应用程序效率的技巧。 2 1 .2 .1 使用用户或系统DSN代替文件D.SN 在前面提到了,一个用户或系统DSN比文件DSN提供更好的效能。这是因为文件DSN需要 较多的资源,包括内存和CPU计算周期。在微软DNA效率工具包中,文件编制控制的图形显示 了将文件DSN改为系统DSN时候的效能变化结果。那个例子中的效能提高到577% 。因为这个原 因,在任何时候都要避免文件DSN。 2 1 .2 .2 优化算法 ,特别是反复循环 —个简单的算法改变可以产生令人惊异的结果。这在循环中特别明显。例 如 ,比较下面两 段相似的循环程序代码: 循环类型一: for( i=0; i 我用两个类创建了一个COM +对 象 ,CVersionOne和CVersionTwo。CVersionOne在一个称为 Perform() 的程序中使用第一个循环。CVersionTwo也在一个同样称为Perform() 的程序中使用第 二个循环。每个类都有一个计算完成其循环方法所用毫秒数的变量,该变量称为Milliseconds。在 一台350MHz的Pentium II机器上,从一个Visual Basic程序调用这两个方法。我发现使用了一个临 时变量以减少内循环中计算童的第二段程序,很明显地比第一段程序快,比较结果如表21-1所示: 表2 1 - 1 两个循环结果的比较 类 奄 秒 数 效率提高百分数 CVersionOne 2975 无 CVersionTwo 2063 30.65 该应用程序代码和Visual Basic方案程序可以在本书所带光盘CD - ROM中的目录Chapter21 下面找到。 注意如果你用debug模式而不用别的任何方式编译COM对象算法,Visual C+ + 对编 译程序的影响,看起来没有而实际上是有的。我所列的代码程序几乎不会对编译程序 Perform()的优化起任何作用。Visual Basic程序对每个方法的调用所花的时间,仅仅是 得到一个零毫秒数值。 2 1 .2 .3 避免注册表存取访问 对独立的程序,注册表存取访问是足够快的。因为大部分独立程序对注册表的访问次数非 常少。对于中间层组件却是一个完全不同的问题。虽然对注册表的i方问仍维持在一个较小的数 目,但它迅速与一个大规模的程序复合。 我写了一个COM对 象 ,它打幵并读取一个单一的注册表字符串10000次 ,以找到一个真实反 映注册表读取访问代价的可靠数据。完成这项工作的程序代码如下面的清单21-1所示: 清单2 1 - 1 读取一个注册表字符串10000次 v o i d CWorker::ReadRegistryString( HKEY Key, DWORD dwSize, const char *pszKeyname, const cha「 *pszDataname, void *pRetbuffer )第2 /章 优 化 奶 DNA应用程序 401 HKEY hKey; if( RegOpenKeyEx( Key, pszKeyname, 0, KEY_ALL_ACCESS, &hKey ) 1: ERROR_SUCCESS ) return; RegQueryValueEx( hKey, pszDataname, NULL, NULL, (unsigned char *) pRetbuffer, &dwSize ); RegCloseKey( hKey ); STDMETHODIMP CWorker::Perform() char szBuffer[500]; DWORD dwStart = GetTickCount(); for( int i- 0;i« ont? bi«i r»iorr x f W t H T 测试中的铬注项 2 1 . 4 观察测试的结果 在你进行一次测试之后,当然要看并弄明白其结果。基本数据很容易看到,如每秒处理量 和请求发送量,因为在测试进行的时候你就可以从控制窗口看见这些统计童,如图21-3所 示。 这些数据都保存到了一个你可以用任何文件编辑器打幵的.out文件中。 2 1 . 5 小结 大规模的企业应用程序是复杂的。 kh它们在一个可以接受的水平运行,特别是在应用程序 负载很重的时候,是一个巨大的挑战。 你必须做的第一件事情足估计你的要求。一个极轻松运行的应用程序不需要优化,你没有 必要浪费时间这么做。 一些相对简单的考虑可以导致结果很大的不同。值得注意的事情可以按顺序列为:DSN类 型 ,算法效率,使用运行时编译执行技术,注册表的使用,避免内存漏失,语言选择和中间层 结构。对所列这些项目中某些因素的优化,可能导致结果产生超过1000%的不同。第四部分异步组件程序设计 本部分包含的内容: •松散耦合系统程序设计 • MSMQ管理机构和体系结构 • MSMQ程序设计 • 高级MSMQ程序设计 • 松散耦合事件 • 队列组件 第22章松散耦合程序设计 本章包括的内容: •什么是消息传递 •消息传递的优点 • 消息传递的弱点 • 同步与异步程序设计 • 可扩展性 ... •面向消息的中间设备 •微软消息队列服务器程序 两个系统差别越大,紧密连接之后的运行效率就越不相同。在一个系统上可能要花大量的 时间运行的工作,在另一个系统上却可以运行得很快。两个不同类型系统之间的通信进程也需 要长时间的转换过程。这种严重的不匹配性限制了传统的函数调用的有效性。本章讲述消息传 递在企业环境中创建松散耦合系统的益处。 2 2 . 1 什么是消息传递 消 息 传 递 (Messaging)是在两个应用程序组件之间发送一个完全封装数据组的进程。消息 传递在两个组件之间提供了一个松散通信通道,如图22-1所 示 。消息传递可以是单向或双向传 递 的 。消息传递组件可以存在于相同的线程上、同一个进程中的不同线程上、同一台计算机的 不同进程上,甚至可以存在于完全不同结构的不同计算机的不同进程h 。 Windows消息传递是一个消息处理更强更灵活的证明。完全用事件驱动的Windows用户界面 也 是通 过消 息、队列和消息处理操作的。相同的概念可以扩展到企业之间,即允许应用程序组 件通过网络进行合作。410 第四部分异步组件程序设计 发送系统 接收系统 图2 2 - 1 基 于 消 息 的 通 信 2 2 . 2 消息传递的优点 同传统功能耦合的应用程序相比,消息传递系统提供多个特有的优点。下面列出了这些优 点中的一些突出方面: • 不同系统之间的通信。 • • 高动态组件之间的通信。 • 异步通信。 • 完全脱机通信。 • 广播通信。 • 有效地利用带宽。 2 2 .2 .1 用消息传递加强大型应用程序的开发 消息传递可以解决大型应用程序开发中出现的几个棘手问题。消息传递系统经常用来解决 两个不能同时使用两个结点之间的通信问题。对邮政系统和电话系统的比较,是对面向消息服 务和联机通信之间关系的一个很好的说明方法。例 如 ,对一个从不接电话的人,发信是一个很 好的联系方法。并 且 ,可以跟踪信件是否被对方收到,它也可以通过别的方式转送。不太走运 的话,当你从休假期回家的时候,信件也许已经等你很长时间了。实 际上 ,在合适的环境中信 件比直接用电话交谈有更多的优势。 对应用程序开发和分布式组件通信,软件消息传递是一个相同的概念并有相同的应用方式。 通过消息传递,客户机组件在其方便的时候获得了发消息的能力。服务器可以在稍微晚一点的 时候收集队列化的消息。消息传递可以在两个不同系统之间传输的时候进行转换,如在PC机和 大型机之间。 当客户机的网络断了的时候,也可以发送消息并且消息立即被编成队列,这种情 况下,当然要等客户机重新连接到网络才能将消息最后实际发送出去。 2 2 .2 .2 消息传递更好地利用通信资源 消息传递比直接紧密连接的方法显得更好地利用了通信资源。紧密连接通过同步通信,经 常以函数调用的形式将客户机和服务器绑在一起。例 如 ,DCOM用RPC以同步方式调用远程控 制对象函数。在两个系统的大多数紧密连接方案中,必须维持连接或对话的方式,不管系统是 否是一直使用该连接。消息传递系统将消息送到立即保存消息的队列。在这种方式下,消息传 a 画 i 一一弟22章 松散耦合程序设计 411 递系统利用了通信通道的全部可利用带宽来处理消息,然后释放通道给别的消息传输。当资源 或目的文件的连接断了,或者因为网 络负载能力不 够或 准备断开连接 (如船只离开港U 或一个 售货员在飞机上使用膝上型电脑时)的时候,这种队列方法也允许将消息暂时储存起来。消息 传递也支持多点递送,让程序开发员能够只发送一次消息而让多个同级别的接收者接收该消息 的拷贝。函 数调 用在语义t 意味着调用者要等待被调用者完成这个调用,因此丧失了前面所说 的松散耦合的所有优点。 2 2 .2 .3 消息传递在不同系统中取得一致 消息传递可以一致化 。消息传递允许+ 同系统改变传输容量来进行通信。一夭我与西班牙 的一个顾客谈生意,我对她宁愿使用email而不使用电话的做法感到惊异。消息传递方法允许她 花时间将我发过去的英语资料翻译成西班牙语,并将她要发送的西班牙语文件翻译成英语。(可 以 从 《The Fifth Element》中引用Korben Dallas, Bruce Willis的 话 “我只会说两种语言,英语和 蹩脚的英语。” )为了能在她们国家的工作时间里联系上她,如果使用电话的iS ,我不得不在令 人迷恋的早晨起床,使用email却没必要那么做了。你可以直接将这个方案与基于1^的 应 用 程 序 作比较,后者必须与运行于主机上的某个软件建立通信关系。消息传递系统让你能够在两个使 用 本 地 格 式 (使用ASCII和EBEDIC ) 的系统之间进行处理,也使联机PC系统在批处理主机系统 储存信息要等待午夜的进程来处理的时候还能够进行交互工作。 最后 一 点 ,消息传递系统在高变动或不稳定系统中产生更加稳定可用的支点。例 如 ,如果 一个以前接收消息的组件不再需要了,很容易直接将该组件中的消息发送到二进制位存储器中。 这对组件的保留没有影响,只要不希望得到一个确认。你 会 说 ,“哇 ,那听起来像一个非常严重 的砍杀。” 嗯 ,是对 的 ,同时也不对。现实情况中事情是发展变化的。大系统的一些部件常常成 为多余的或者需要作多方面的修改。 当经过消息传递连接上了这些组件,甚至是受到严重损伤 的组件时,通常对别人也只会产生很小的影响。 考虑一段运行于某大型计算机上的代码程序,该大型计 算机关系 到某些企业财政稳定 性。 设想该代码通过了改进和测试,并用了超过5年的时间。它送一条消息给一个基于PC的 系统 ,并 且该消息很快就自制了足够的消息来执行基于PC的任务。你真的想修改这个大型计算机代码程 序吗?也 许 是 到 r 它 不可避免地要作废的时候了,但你必须通过绝大部分的情形来确认这 点。 于是怎么办?在这样的松散耦合系统中,你 可 以 增加 一 个 软件 (在大型计算机上或者PC上 )来 接受原始消息,增加所需要的消息,并将它发送给目标PC。不需要修改现存测试得很好的那个 软件 ,只用测试一7F 新加入的修改消息的下游组件就行了。这就是松散耦合的实质。 2 2 . 3 消息传递的弱点 那么,消息传递的弱点是什么呢?我认为,Windows平台上的消息传递和基于事务的服务器 程 序 (MTS/COM+) 的出现,是自Windows NT发行以来,在企业应用程序支持系统方面的最重 要进步。当然,每种技术都有它的生存时期,并且消息传递也许以后不再适应不停变化的模式。 下面是消息传递的一些弱点: • 典型缓慢的端到端处理。412 第四部分异步组件程序设计 • 异步执行使跨越组件的同步复杂化。 2 2 .3 .1 延长处理时间 消息传递并非总是都是最好的可行办法。按 照人类 的 标准,电子消息传递是非常快的。但 是 ,往返数据处理不可避免地要比交互通信会话慢一些。调用一个远程对象的函数并返回一个 数值,或者用另一个进程创建一个TCP连接其他进程并发送请求和接收响应,通常对高度交互的 应用程序组件工作得最好。 另一件要考虑的事情是编排数据对处理时间的影响。虽然我们经常谈论在同一台机器的进 程 服 务外编排数据所导致的效能瞬时干 扰,但 它比 起 编排分布式数据时 的性 能瞬时干 扰要小 得多。 2 2 .3 .2 异步执行 消息传递在函数交互调用方面缺乏自同步的功能。 当你调用的例程返回的时候,你通常知 道它已经完成其工作了。在另一方面,信息通常是异步处理的,由于这个原因,你无法知道该 信息在什么时候或者是否已经处理完了。通过松散耦合系统的同步行为,比在具有天生同步环 境中的同步行为,如在RPC、RMI或者IIOP中 ,表现得更复杂并且代价更昂贵。 2 2 . 4 同步与异步程序设计 松散耦合系统是异步的,而紧密耦合系统是同步的。我们中的大部分对开发同步应用程序 没有问题,因为它们比对应的异步情形简单,并且我们都拔除了对付异步应用程序的编程利器。 本节给你展现一种不同的东西,并指出一些要考虑的因素,这是当你开发异步系统时需要铭记 在头脑中的。 每种技术都有它的副作用,这是需要解决的。我总避免使用25音的 单词和 高科技,除非它 们真的 能 使人受 益。我的 自 动 化实验室老 师指 出 ,我在谈到一个汽化器运载工具时没有使用 carburetor这 个词。两次 或者三 次使 用短语“用空气混合燃料的多U 机械” 之 后 ,你会有足够的 理由来使用carburetor这个单词了。消息传递技术就是有点像carburetor之类的东西。特别是在处 理面向消息的软件时,你会经常使用下面的短语: • 紧密耦合系统—— 使用组件的有固定步骤、通过同步、函数调用型交互作用的应用程序。 • 松散耦合系统—— 异步连接的可用大董独立方式工作的系统。 • 异步编程—— 用不依赖于同步方式工作的编写程序组件。 • 重叠通信—— 用普通应用程序代码执行的并行I/O。 • 无闭塞丨/O— 可以接收I/O请求而不影响CPU线程的执行。 . • 立即运行—— 操 作 (通常是I/O) 在执行的时候不会导致线程来等待操作的完成。 所 有这 些短语,除第一条外,都应用于编程技术,该技术可以让一个组件与另一组件通信 而不需要影响CPU。因 此,组件A可以发送一个消息给组件B ,并立即继续其工作。 在许多方案中使用消息传递有巨大的益处。松散耦合系统的可扩展性和效率大大超过紧密 耦合系统。等一分钟:我还没有突出说明一个事实,紧密耦合系统,虽然不够灵活,但其端到漭22聿 松散耦合程序设计 413 端传送方式会更快一些吗?是 的 ,实 际上 ,在某些情况下它极其的快,在另一些情况下是偶尔 快一 些 。这个结果源于一个事实,就是松散耦合消息传递系统的规模和分布是以一种比基于调 用的系统更线性化的方式工作的。 在某些情况下,你可以用Windows的机制如信号装置来模拟同步运行。但这只能在某些而不 是全部情形下可行。 看一个WinSock中的例子。重脅WinSock网络I/O比同步WinSock网络I/O快是一个众所周知 的事实。但 是 ,为什么呢?如果你为网络写动作准备了一个缓冲区,并调用send()函数以块方式 发送I/O请 求 。这意味着内核将从你的线程夺走CPU,并将CPU给 一 些 别的准备执 行的线程^你 的I/O会在后台的短中断间隙中继续执行,但是你的应用程序线程锁住了,要等待I/O运行完毕。 这有两个要考虑的性能等级(通常是 这 样 ): • 应用程序性能。 显 然 ,两个因素应该综合考虑。在 前 一 方案中 ,分块I/O调用使两个效率都受损。第 一 点 , 应用程序任务停止运行以等待I/O完 成 。应用程序在这里真的无事可做了吗?第二点 ,系统需要 从你的线程调走CPU并找一个新线程运行。这可以看做是无害的,但是牵涉到了大量操作的执 行 ,包括下面所列的: • 保存应用程序线程的上下文信息,以便以后可以再继续执行。 • 设置系统结构,包括你线程的等待状态,以让等待状态完成之后,它可以继续工作。 • 找出下一个按优先次序列出准备运行的线程。 • 恢复所选线程的上下文消息。 注 意 ,在这个进程中,没有产生对系统用户有用的甚至是可以理解的东西。线程之间的上 下文切换对现代多进程系统是一个重要的影响性能项。你怎么避开其影响?我们将按次序看看 每一个效能优势点。 首 先 ,你要让自己程序的速度快起来。通 常 ,在你有机会对它们寻址之前,注意这些系统 效能问题的解决方法。为了提高应用程序的性能,你希望尽可能地延迟释放CPU,不用担心别 的在具有抢先式环境如Win32中的应用程序。你试图获得的是让上下文切换自然地适应于你应用 程序的内部进程。因此,你怎样对待I/O和保持使用CPU?你所寻求的技术称为重叠I/O。当你提 交一个I/O请求,然后在I/O系统在后台间断时间中处理该请求时,你继续使用CPU,这就是重* 1/0。使用重叠1/0,你可以在第一个程序正向网络中写的时候往内存缓冲区写第二个程序,让 I/O系统和CPU —直保持忙碌的状态。比较图22-2和图22-3所示的进程执行图。 • 系统性能。 图2 2 - 2同步I/O414 第四部分异步组件程序设计 应用程序 预备缓冲A 预备缓冲B 传输缓冲A 传输缓冲B 图22-3异步I/O 在 第二个例子中,线程更快地完成了自己的任务,并且通过消除一个任意的上下文切换减 少了系统的总开销。如果你希望异步消息传递超过同步函数调用,这种策略可以直接用在分布 式系统中的组件设备上。消息传递系统接收从应用程序来的信息发送请求并可以立即给调用者 返回控制消息。 2 2 . 5 可扩展性 重叠或异步i/o可以在两个使用消息传递的应用程序组件中实现。你不必调用其他组件中的 函数,然后让出CPU,等待远程控制组件函数返冋,仅是简单地发送一个消息然后继续你的工 作即可。如果你需要一个应答,可以不停地轮询应答队列来检查你要的答复。这种方法有效地 分离了 两 个 应 用 程 序 组 件之 间的耦合关系 这 个分离减 少了 应 用 程 序 组 件间的 依 赖关系 ,允许 组件在不同的环境中以不同的速率、不同的物理和逻辑距离运行。你每次增加两个同步组件之 间的距离,就增加了它们通信所需的总开销。因为对同步性质的函数调用,这类方式的网络传 输延迟影响调用链中从尾组件到起始调用荞之间的每一个单元。 在松散耦合系统中,你用了一种开火和忘记(fire-and-forget)策略。必须通信的组件简单地发 送一个消息给它的目标组件,然后进行它的下一个任务。肖然 ,还有负责该信息递送的消息传 递 软 件 的 总 幵 销 (这通常被显而易见地掩盖起來了),但是该组件木身不再受通信开销的影响。 进一步假定,如果你 控 制 ;T远程 组 件 ,消息队列在髙流量的情况下增大而在低流量时缩小,这 会更有效。也许只有一个服务器组件能提供一种特定消息服务,而多个客户机组件能发送消息。 分离这两种组件,使你能够在客户机中创建许多客户组件来提供迅速的响应。你的服务器端传 输时间相应地增长了,但是因为你的客户机组件没有莨接耦合到服务器的性能,所以通过限制 你的服务器组件给客户机请求提供快速应答,会有一个扩展得很好的系统。 设计具有良好可扩展和可交互操作的应用程序的实用技巧在于,把问题加 以分解,使各部 分尽可能独立地操作。一种松 散 耦 合 的 方 法 避 免 出现“最弱链环节” 的情况。松散耦合支持独 立 操作 ,使 各 组 件 以 身 的 步 伐 运 行 并 跨 越 多 个 系 统 。虽然这是属于深层次的问题。但我依然 在可能的情况下在本章将其作为提示给出,以帮助读者采用这种强有力的技术。 2 2 . 6 面向消息的中间设备 我相信你一定疑惑不解,在消息传递背后的神秘软件是什么?答案就是Message-Oriented Middleware(MOM,面向消息的中间设备)。现在有几种流行的MOM系统可用于各式各样的操作 平台。本节的详细讨论带你进入Microsoft Message Queue Server(MSMQ, 微软信息队列服务器第22章 松散耦合程序设计 4!5 程序)。MOM系统提供使分布式组件易于处理和接收信息的通信粘合。标准MOM系统由下面几 个部分组成: • 一 个 编程 接 口 (API)。 • 一套系统软件组件。 • 管理工具。 22.6.1 MOM 程序接口 MOM API ( 面向消息的中间设备程序接口)通常包括几个基本功能,它们使应用程序开发 者可以更容易地发送和接收信息。你很快就会看到,在短时间内得到并运行一套好的消息传递 系统是很容易的。经常提供各种各样附加的MOM A P I ,允许执行行政和管理功能,包括安全检 查 、性能调整和限吋交付等。 22.6.2 MOM系统软件 MOM系统也包括负责在计算机网络上移动信息的软件。这些信息保存在队列中直到它们被 传送到系统的下一个节点。这看起来很容易,实际上要解决几个很复杂的面向数据库的任务。 首先,如果接收到了超过一份以上的相同信息,信息接收者将会是非常迷惑的。下面说明 这种情况是很容易发生的。信息从一个队列传到下一个队列,如果在收到应答信息之前掉线了c 发送者没有其他选择只能在线路再次稳定之后重发一次信息。当两台或更多的服务器可以处理 某一 特 定 类 型信 息的 时候,会 出现另 一 个 问题。如果两 台 服 务 器并 发检索一信 息,那会怎么 样?最后 ,当一系列信息以一个错误的顺序到达,那乂会怎么样? 如果没有准备,这些问题都有可能导致一场灾难。使用应用程序逻辑避免这些问题将会是 多余的,因为这些问题已经被数据库系统在多年前解决了。因为这个原因,大部分MOM系统使 用数据库技 术 ,为 它 们 的 应 用 程 序开 发者实 现强健的甚 至有亊务 能力的消 息 传 递环 境。因此 MOM系统软件使应用程序的开发与大型计算机可靠性的结合变得容易。 2 2 .6 .3 管理工具 你 能发 现,MOM系统使应用程序开发变简单了,但是给系统管理员增加了新的管理任务, 就像分布式数据库的管理。队列在哪创建? 谁能对队列进行读写? 队列该选則那条路径进行通 信?组件和队列该怎样配置才能最有效地利用计算机的可用能力?要有效地配置一个分布式消 息传递系统必须回答上述问题。因 此,商用MOM环境与管理工具、支持基于队列结构和监控任 务的通用程序一起发放。 2 2 . 7 微软消息队列服务器 微软消 息队列 服 务 器 (MSMQ)是微软基于Windows应用程序幵发的MOM产 品。MSMQ提 供两种不同的API。第一种API由平面C型函数调用组成。第二种API是基于COM的 ,由一系列 ActivcX组件组成。你将在后面看到,在不同的环境中可以证明每种都是有效的。MSMQ也由一 系列系统组件组成,它们使用数据库技术严格管理队列行为和网络设备,该网络设备提供队列416 第四部分异步组件程序设计 之间的协议独立通信。最后,MSMQ提供一个微软管 理 控 制 台(Microsoft Management Console, MMC),管理和配置整个网络的队列、客户机和服务器。 22.7.1 MSMQ 连接器 真正的分布式应用程序经常需要将不同系统集成在一起。幸 运 的 是 消 息 传 递 系 统 用 “可.操 作性” 这个词刻划了它们的特征。微 软 设 计 fM S M Q 连 接 器 ,允许第三方连接到MSMQ。微软 在 内 部 使 用 这 个技 术使MSMQ通过Exchange Mail ( 交 换 邮 件 )很 容 易 进 行 互 操 作 。Level8 Systems (8级系统)也通过FalconMQ Bridge使用了该技术。FalconMQ Bridge得到了微软的许可, 并且微软允许MSMQ消息进入IBM MQSeries消息空间,反之亦然。作为COM程 序员 ,MSMQ是 可选的消息传递系统,但是也要知道别让自己进入窘境。 22.7.2 MSMQ 和别的 API 如果你对Messaging API(MAPI)、RPC或者WinSock很熟 悉,你会奇怪为什么你要学MSMQ 程序设计。很明显在MSMQ和别的通信程序设计界面之间会有一些重赍。MSMQ有一个特别明 显的优点就是支持与同级非运行状态对象通信的能力。没有一个别的常规丨PC程序设计环境允许 一个应用程序将请求队列化,这些请求将能够被一个同级别的对象在以后获得。API如MAPI支 持消息传递,但需要一个更精确的程序设计模塑(大部分以email为 对 象)。 22.7.3 MSMQ 和 Email 是 的 ,就是email。正如你期望的一样,MSMQ系统是一个合适email程序设计环境。为此微 软给email程序和互操作性提供了特别的支持。不同的MSMQ API例程支持email信息的组成和语 法分析。MSMQ对必须接收和处理email类型信息的应用程序是非常有效的。MSMQ信件信息有 它 自己 的 格式,但是通过MSMQ连接器它们可以容易地传送给Exchange Server(交换服务器)和 MAPI客户机。在第25章 “ 高级MSMQ程序设计” 中详细描述了MSMQ邮件程序设计。 2 2 . 8 小结 本 章 ,你学习了松散耦合系统的可伸缩性和优良的性能。你知道了消息传递系统的优点和 弱 点 ,并且学习了一种为支持面向消息的内部应用程序通信而特别设计的新中间件类型。你也 学习了微软消 息队列 服 务 器(Miscrosoft Message Queue Sever, MSMQ ) 并且回顾了一些基本 的MSMQ特 性 。在第23章 ,“MSMQ管理机构和体系结构” 提供了MSMQ体系结构的详细描述。 第24章和第25章各自描述了基础的和高级的MSMQ程序设计知识。第23章 MSMQ管理机构和体系结构 本章包括的内容: • MSMQ对象和属性' • 消息 •队列 •机 器 • MSMQ企业 •MSMQ客户机 • MSMQ管理结构 本章介绍MSMQ企业的各个部分,并且怎样配置和管理它们。你将学 习创 建、破 坏 、监视 和配置队列的工具。同时也能检验依赖于队列的应用程序和组件。 因为我这里还要讨论临时程序设计问题,所以我要指出有两种创建MSMQ应用程序的基本 方 法,MSMQ API和MSMQ ActiveX Component。可以选其中之一或另外的一种或者它们的综合 方 法 ,并进行比较。API提供最低级的支持,表明了各种MSMQ服务功能并且在大部分情况下只 要求少量的程序代码。ActiveX方法是为满足任何开发环境而设计的,并通过简洁的面向对象 COM程序设计模型提供足够程序设计能力。在 本 章 ,对程序设计问题,我将描述MSMQ API和 MSMQ ActiveX Component两种环境。 一个基本的MSMQ消息交换包括下述三个方面: • 发送应用程序。 • 目标队列。 • 接收应用程序。 客户机应用程序通过发送消息给MSMQ服务器监控的队列来使用MSMQ。如果目标队列不 在 本 地 ,发送者的本地服务器有责任通过MSMQ企业传递消息。该 消 息 通 过 -个队列传 递到 另 —个队列直到最后达到目的地。从MSMQ的角度看,消息的格式是任怠的,但发送和接收消息 的 端 点 必 须 能 明 0 该 消 息 的 内 容 。 队 列 充 当 了 一 个 中 间 人 的角色,使应用程序能在分布式 MSMQ企业内的仟何系统中在不同时间点上运行。队列的分离作用使客户机和服务器的MSMQ 系统组件变得非常独立。大部分的工作以异步方式逬行,使应用程序在发送消息的时候能使用 发射和搁置的方式进行。 23.1 MSMQ对象和属性 一个MSMQ系统被几种关键对象类型所定义:418 第四部分异步组件程序设计 • 消息。 • 队列。 • 机器D 这些对象存在于定义为MSMQ企业的内部管理单元中。消息是指能在企业中携带信息进行 传递的东西。消息具有灵活的结构,可以包含数据也可以完全就是空的。对后 一种情况,空消 息的接收也需要用到两个应用程序之间的所有通信设施。另一方面,消息可以钽括4MB的数据。 队列作为MSMQ企业内所有消息传递的目的地。消息传递到队列中,并非应用程序中。队列的 中间人性质,在同级对象之间提供一个独立的中间层,包括消息方案。因此支持松散耦合应用 程序的开发。MSMQ机器运行一个称为队列管理器(Queue Manager) 的进程。MSMQ机器是为 支持MSMQ企业内部队列服务而设计的计算机系统。 所有这作对象都用屈性描述,如 标志、类 型 、大 小 、超时和优先级。可变长属性列表允许 程序员在一般的情形下不需指定对象就可以工作。一些位用程序也许需要 对象带有 全部 属 性, 而另 一 些 应用 程序 也许根本 就 不 需要 任何厲性消 息。一 些对象厲性 由MSMQ设 置 ,另外的由 MSMQ管理员设搜,并且还有一些可以仅仅由程序设置。某些属性在询问的时候可以记录下来, 这也只是在对象创建的时候才可以,并且从那以后就保持只读属性。 MSMQ浏览器让筲理员可以在整个MSMQ企业内部创建对象并管理对象属性。MSMQ浏览 器 ,在MSMQ的早期版本中作为一个独立的程序运行,而在MSMQ的近期版本中作为嵌入式的 当在程序屮赋值的时候,厲性通过可变长属性数组来配置。这允许MSMQ超时定义新属性, 并且使程序接口灵活化。MSMQ广泛地使用这种模式,就像OLE DB和别的现代COM系统一样。 这些前面描述了的基本MSMQ对象支持许多属性。在特定的环境中,作为某些强制性属性的例 外 ,你可以定义并获得你的应用程序所需要的属性。清单23-1给你显示了怎样通过一个程序配 置队列的属性。 Microsoft Management Console ( MMC )Q 清单2 3 - 1 程序配置一个队列的属性 //Set up properties (we only need the path name property) QUEUEPROPID aPropId(1]; PROPVARIANT aVariantp ]; aPropIdl0] = PROPID_Q_PATHNAME ; aVariantl0].vt = VT^LPWSTR; aVariant[0].pwszVal = L-Svrl\\Orders //Set up the property struct MQQUEUEPROPS QueueProps; QueueProps.cProp = 1 ; QueueProps.aPropID = aPropId; QueueProps.aPropVar = aVariant; QueueProps.aStatus = NULL; //Property id array //Property value array //Set the ID //Set the value type //Set the value data //Queue properties struct //Number of properties //Properties id array //Properties value array //No prop error reports 正 如 你 所见到 的 ,这种扩 展结 构允 许许多属性通过相同的MQQUEUEPROPS结构传送给 MSMQ。变量用来储存属性值,以使许多不同的程序设计环境能够获得属性的设置。对消息和 机 器 (也称为队列管理器),也分别有相似的MQMSGPROPS和MQQMPROPS结构。弟 章 MSMQ管理机构和体系结构 419 2 3 . 2 消息 消息是除MSMQ之外所有事物存在的理由。它描绘MSMQ企业内部移动的消息的原子笮元。 消息展现的实际结果就是一系列简单的属性值。由 此,消 息有一系列的厲性可供选择。通 常 , 应用程序对消息的Body属性最感兴趣。Body属 性 可 以 是 任 何长度的 (最大可到4MB )。消息的 一些别的属性可以通过复杂的应用程序经常性地设置和检查。 消息属性 表23-1给出了MSMQ消息属性的清单 表23-1 MSMQ消息厲性 诚 性 描 述 PROPID_M_ACKNOWLEDGEMSMQ发送的确认消息的类型 PROPID_M_ADMIN_QUEUE 确认消息所用的队列 PROPID_M_ADMIN_QUEUE_LEN 确认消息队列的长度 PROPID_M_APPSPECIFIC 应用程序产生的消息,如一个羊精度整型数偯或者成川程 序定义的消息类型 PROPID_M_ARRIVEDTIME 消息到达队列的最大时问 FROPID_M_AUTH_LEVEL 表示在读取消息时要认证 PROPID_M_AUTHENTICATED 表示消息已经被MSMQ认证 PROPID_M_BODY 消息体 PROPID_M_BODY 一 SIZE 消息体的长度 PROPID_M_BODY_TYPE 消息体 的 类 型 (字符串、数 组、对 象 ) PROPID_M_CLASS 消 息 的 类 甩 (比 如,一个消息可以记普通的、确认或者是 报告消息> PROPID_M_CONNECTOR^TYPE 表示一些消息属性由MSMQ 外部产生 PROPID_M_CORRELATIONID 消息的相互关系标识符(丨京始消总丨D) PROPID_M_DELIVERY 消息焙怎 样传 递的 (优化通过發或恢g 能 力 ) PROPID_M_DEST_QUEUE 消息要驻留 的队列(发送消息的地方) PR0P1D_M_DEST_QUEUE_LEN H 的队列的长度 PROPID_M_DEST_SYMM_KEY 给外部队列发送加密消息时所崙的刈称密钥 PROPID^M_DEST_SYMM_KEY_LEN 对称密钥的长度 p r o p i d _ m _ e n c r y p t i o n _ a l g 加密消息体的筲法 PROPID_M_EXTENSION 附加的未格式化消息 PROPID_M_EXTENSION_LEN 附加的未格式化消息的氏度 PROPID_M_HASH_ALG 认iiH消息的混杂箅法 PROPID_M_JOURNAL 表示一份消息的拷贝储存在机器的U 忐中 PROPID_M_LABEL 应用程序定义的消息标志 PROPID_M_LABEL_LEN 消息标志的长度 PROPID_M_MSGIDMSMQ 产生的标识符 PROPID_M_PRIORITY 消息的优先级 PROPID_M_PRIV_LEVEL 表示消息是加密的 PROPID_M_PROV_NAME 密码提供器的名字 PROPlD_M_PROV_NAME_LEN 密砰提供器名字的长度420 第四部分异步组件程序设计 ( 续 ) 域 性 描 述 P ROFID_M _PROV _TYPE 密码提供器的类塑 PROPID_M_RnSP_QUEUE 说答消息的队列 PR0P1D_M_RESP_QUEUE_LEN 戍答队列的长度 PROPip_M_SFCURITY.C:()NTKXT 认证消息的安全忡信息 PROPID_M_SENDF.R„CFRT 认证消息使用的安全性证书 PROPlD_M_SENDER_CERT_LtN 发送证书缓冲区的长度 PROPID_M_SENDERID 发送消总的用户 PROPID_M_SENDERID,LEN 发送者标识符的长度 PROPID_M_SENDERID_TYPEPR()PID_M_SENDER丨D 屮的发送者标识符的类型。当前, MSMQ中发送者标识符的惟一类型是安个标识符(s m ) PROPID_M_SENTTIME 消息通过源队列发送的n 期和时间 PROPID_M_SICiNATURE 认证消息的数7 签名 PROPID_M_SIGNATURE_LEN 认证消息的数字签名的长度 PR0PID_M_SRC_MACHINE_1D 最汗始发出消急的计筲机 FROPID_M_TIMF._TO. BE.RECEIVED 接收消息的成甩稈序从队列中移走?肖息所用的时间, FROPID_M_TIMH_rO_RRACH_QUEUn 消息到达队列的时间 FROPID_M_TRACE 消息传递走过的路径 PROPID_M_VERSlON 州来发送消息的MSMQ版本 FROPID.M.XACT.STATUS.QUfilJE 事务状态队列的格式化名 PROPID_M_XACT_STATUS_QUECE_LEN 事务状态队列的格式化名的缓冲区K 度 2 3 . 3 队列 消息队列是一个缓冲机构,它是MSMQ开发松散耦合系统应用程序的有效工具。队列储存 消息直到适当的位用程序认为适合于取出该消息了。队列使不同类® 组件的通信成为可能,即 使它们足在企业内不同的时间和不M 的位置运行。 2 3 .3 .1 队列类型 虽然队列的基本功能都相同,为消息提供一个临时储存器。但是MSMQ和MSMQ应用程序 还是使用以下不同类型的队列: • 消息队列。 •管理队列。 • 应答队列。 • U 志队列。 • 死信队列。 • 报告队列。 应用程序或应⑴程序管理员创建前面三种应用程序队列类型。一个应用程序可能使用几种 队列或使用不同容量形式的单一队列。例如,一个服务器可以使用相同的队列管理和应答消息。 后二种类型的队列是系统队列。日志队列和死信队列由MSMQ创建和维护。消息的Class属性用第2 J章 MSMQ管理机构和体$ 结构 421 来标识管理中建立的确认消息和报告消息,以及死信队列和报告队列。应用程序可以接收但是 + 能发送消息到三种系统类型队列中。 2 3 .3 .2 消息队列 消息队列是保存无格式旧应用程序消息的那种队列。它们是大部分消息的目标队列,也是 接收标准消息的位置。消息队列通常由应用程序创建;然 而 ,一些应用程序或许也要求MSMQ 管理员创建特定的队列。 . 应用稈序消息队列可以是公有的也可以是私有的。公有队列注册于活动目录中(或Windows NT4中的临时代理中),并且在整个MSMQ企业内部可用。私有队列只在创建它的MSMQ机器中 才可以看见。为 f 支持应答消息,应用程序可以传递私有队列的名字给远程应用程序。 2 3 .3 .3 管理队列 管砰队列基本跟普通消息队列相同,只有一点例外:它们指定为MSMQ确认 消 息的B 标队 列 。我说不出它们为什么不能叫确认队列,但肯定是有其原因的。它 们由位用程序创建,用来 接收由MSMQ发送的状态消息。状态消息可以表示:一 个消 息到达/ 其 13的队列、或一个消息 确实被一个应用程序接收了,或者 对 两 者 都没有 确认(因为超时或其他操作失败了)。应用程序 标志要求逐条消息确认。确认队列不能是事务的。 2 3 .3 .4 应答队列 应 管 队 列 基 本 跟 普 通 消 息 队 列 相 同 ,只 有 一 点 例 外 :它 们 指 定 为 应 答 消 息 的 目 标 队 列 。 PROI>ID_M_RESP_QUEUE消息是MSMQtJt界 中 “对回答请求的优待” 。应用程序接收消息时需 要一个应.答 ,它可以在人站消息的PROP丨D_M_RESP_QUEUE属性中杏阅应答队列。这个队列可 以是私有的,因为发送应用程序目标应答队列不需要查阅队列名;实际上,它由原始消息提供。 2 3 .3 .5 日志队列 曰志队列的两种类型为机器H 志和队列日志。正如你预期的一样,机 器 H 志保持一个该机 器处理 的重要消 息的记录,而队列日志保持一 个 由 该关联队列处理 的重要消 息的id 录。每台 MSMQ机器都有一个机器日志队列,并且每个MSMQ应用程序创建的队列都有一个队列日志。 队列的PROPID_Q_JOURNAL属性在紧接着发送消息之后,拷贝所有消息到其队列日志中。消 息不使用日志队列,但使用它的PROPlD_M_JOURNAL属性集。消息在紧接着被成功传送到下 一企业队列或最终应用程序后被拷贝到机器H志中。虽然日志队列容量有限,但MSMQ从不淸 除曰志消息。很 明 显 ,在队列需要使用的时候,管理员或应用程序应该定时地检査并清除这些 队列。如果达到了队列的限额,新消息就不能送到队列中。 2 3 .3 .6 死信队列 死信队列存储不能传递的应用程序消息。每个MSMQ机器维持两个死信队列。一个为事务422 第四部分异步组件程序设计 性的 的死信,为非事务性的死信。消息被拷贝到队列链中最后一台成功收到该消息的机器的死 信队列中。只有被记录下来后,为非事务性的消息才会拷贝到R 志队列或者死信队列中。在另 一方面,失败的事务消息通常被拷贝到最后成功收到该消息的机器的事务性死信队列中。 2 3 .3 .7 报告队列 报告队列可以由MSMQ管理员的决定来创建。报告队列接收消息在MSMQ企业内处理过程 中的进程报告。每当消息被标志要跟踪其在一种资源、路 由 选 择 (中 间 物 )、目的服务器中的处 理 过 程 时 ,报告消息就由MSMQ产生了。报告消息具有MQMSG一CLASS_REPORT类 型 ,并且 在设置PROPID_MJTRACE消息属性的时候,需要获得目的报告队列的名字。 2 3 . 4 消息队列信息服务 MSMQ企业 ,站点和队列信息存储在消息队列信息服务(Message Queue Information Service, MQIS)数据库中。该数据库居留在Windows 2000系统的活动目录中,并且在Windows早期版本 的Microsoft SQL Server上顶级 运 行 。你 可 以 在已有SQL Server数据库的前活动目录苄安装 MSMQ服务程序,或者使用与带有前活动目录的MSMQ—起发放的有限SQL Server应用程序。 2 3 . 5 本地队列存储 MSMQ存储跟提供高速缓存消息一样,提供Local Queue Storage (LQ S,本 地队列存储)中 关于本地公共队列的队列消息。MSMQ SQL通 常位 于\program files\msmq\storage\lqs目录下。 2 3 . 6 队列属性 队列有多种属性,一些是可读/写 的 ,一些由MSMQ设置并且是只读的,并且还有一些只能 由队列创建时配置,它们如表23-2所 示 : 表2 3 - 2 队列厲性 域 性 描 述 PROPID_Q_AUTHENTICATE 表示队列只接收认证的消息 PROPID_Q_BASEPRIORITY 在MSMQ路由选择中队列的优先级 PROPID_Q_CTEATE一TIME 队列创建的时间和曰期 PROPID_Q_INSTANCEMSMQ 在创建队列时设S 的队列GUID PROPID_Q_JOURNAL 表示队列记录所有消息 PROPID_Q_JOURN AL一QUOTA 队列日志的蝻大值,笮位KB PROPID_Q_LABEL 队列涵示的标题 PROPID_Q_MODIFY_TIME 队列属性最后被修改的时间和日期 PRuPID_Q_PATHNAMI: 队列的全路径名(机器名,附 加 项 附 加 项 队 列 名 ) PROPID_Q_PRIV_LEVEL 需要对消息加密的队列 PROPID_Q_QUOTA 队列的最大值,中位KB PROPlD_Q_TRANSACTION 队列记爭务性的 PROPID_Q_TYPE 用于给队列分类的GUID弟2 J章 MSMQ管理机构和体系结构 42 j 2 3 . 7 优先级 MSMQ首先传递具有最高路由优先级的消息。为决定一个消息的路由优先级,MSMQ结合 消息路径上的下一个队列基本优先级和消息的优先级属性(PR0PID_M_PR10RITY)进行考虑。 MSMQ管理员使用队列的基本优先级控制消息的路由优先级和不同路线的应用程度。队列基本 优先级的范围为从-32768到32767,默认时的优先级为0。消息的优先级属性范围为0 - 7。普通 消息的默认优先级设置为3,而事务消息的默认而且也是惟一的优先级属性为0。队列中消息的 颀序仅由消息优先级来决定。 2 3 . 8 事务队列 事务队列与事务消息紧密相关。 只有事务消息才能发送到事务队列。在 创 建 时 ,就必须指 明为事务队列还是非事务队列,并且这种指定在后面不能改变。事务队列还额外地保证丫其可 靠性与有序性。就如前面所述,所有的事务性消息或者发送或者拷贝到事务死信队列中。 另外,在一个事务中就可以管理若千条消息,此时MSMQ可以保证所有的消息或者都发送, 或者都不发送。发往同一队列,在同一事务中的消息,将以发送的次序进入队列。一个事务中 的所有消息都有一个超时值,这个值在事务发送的首条消息中指定。此 外 ,他们的优先级置为0。 为了满足消息传递事务中的基准,这些设置是必需的。 事务消息只有在事务提交后才真正发送、并且在提交前的任意时刻,事务中的所有发送消 息都可以被冋滚。 2 3 . 9 标识队列 名字下面是什么?对于MSMQ队列 ,这要看你谈论的是什么名字。队列有好几个标识属性^ 队列也可以按类集中。这使得用Active Directory检索它们的时候的变得简单。MSMQ API的例 程MQLocateBegin( ),MQLocateNext( ), MQLocateEnd()和MSMQQuery ActiveX对象允许应用 程 序几 乎可 以 使 用 任 何 标准对公共队列 进 行 检索* 2 3 .9 .1 路径名 标识队列属性最常用的也许是路径名(PROPlD_Q_PATHNAME ) 。.. 一个队列的路径名由持 有该队列的MSMQ机 器名、反斜杠和队列名组成。例 如 ,大型计算机Sanron上用来存储新命令 的队列,其 路 径 名 是 SaiirorANewOrders。本地队列也可以由一个句点代荇大型i |•算机机名来表 示 ,如 ANewOrders。在C和C++字符串常鼋中别忘了要打反斜杠。 2 3 .9 .2 格式名称 不幸的是,大部分MSMQ例程 不 能 使 用路径名(这会 更 容 易 )打开一个队列。MSMQ比较 •哀欢用队列的格式名称代替路径名。以下几种MSMQ API例程提供格式名称检索服务: • MQPathNameToFormatName() • MQHandleToFormatName()424 第四部分异步组件程序设计 • MQInstanceToFormatName() MSMQQueuelnfo ActiveX对象提供类似的格式名称解析服务。格式名称是Unicode编码字符 串组成的一个GUID和一些辅助文本文件。清单23-2是不同格式名称类型的常用格式列表。所列 的每一项提供一个描述,一个模板和一个特定格式名称类型的例子。 清 单 2 3 - 2 格 式 名 称 类 型 Public queues PUBLIC=QueueGUID PUBLIC={9792EFBA-9549-11d2-8285•00A0C9929FD0} Journal for a public queue PUBLIC=QueueGUID;JOURNAL PUBLIC={9792EFBA•9549-11(12-8285•00A0C9929FD0};JOURNAL Private queue PRIVATE=MachineGUID\QueueNu(nber PRIVATE={7FC4449A-955B-11d2-8285•00A0C9929FD0}\0000000b Journal for a private queue PRIVATE=MachineGUID\QueueNumber;JOURNAL PRIVATE={7FC4449A•955B•11d2.8285•00A0C9929FD0}\0000000b;JOURNAL Public queue direct format DIRECT=AddressSpecification\QueueName DIRECT=TCP:208.167.209•145\New0rders Private queue direct format DIRECT=AddressSpecification\PRIVATE$\QueueName DIRECT=TCP:208.167.209.145\PRIVATE$\New0rders Journal for a machine MACHINE=MachineGUID;JOURNAL MACHINE={DF0EDE5A-955E.11d2•8285•00A0C9929FD0};JOURNAL Dead letter queue for a machine MACHINE=MachineGUID;DEADLETTER MACHINE={DF0EDE5A•955E-11d2-8285.00A0C9929FD0};DEADLETTER Transaction dead letter for a machine MACHINE=MachineGUID;DEADXACT MACHINE={DF0EDE5A-955E•11d2•8285•00A0C9929FD0};DEADXACT Foreign queue CONNECTOR=ForeignCNGUID C0NNECT0R={32CDC6FA-955F•11d2-8285 -00A0C9929FD0} Foreign transactional queue CONNECTOR=ForeignCNGUID:XACTONLY C0NNECT0R={32CDC6FA-955F-11d2-8285•00A0C9929FD0};XACTONLY 在大部分情形下,公有的和私有的格式名称都分别用来打开公有的和私有的队列。直接格 式名称可以用来从外部MSMQ企业打开队列,或者可以避开标准MSMQ消息路线发送一个消息。 直接格式名称/地址详细说明必须包括可解决的人型计算机名或者是一个TCP/IP ( 如前面的例子第23章 MSMQ管理机构和体系结构 425 中 所 示 )或 者 IPX/SPX地 址 洋 细 说 明 。MSMQ机 器 队 列 可 以 用 机 器 GUID 打 开 。用 MQGel MachineProperties( ) API例程和MSMQApplication ActiveX对 象 都 可 以 产 生 •个 机 器 GUID:最 后的连接器格式名字用来存取访问MSMQ连 接器 ,以实现向MSMQ企业外部发送消息的服务 2 3 .9 .3 示例标识符 每个公有MSMQ队列都赋有一个标准的128-bit全 局 惟 一 的 用 户 标 识 符 (GU1D )。队列的 GUID存储在队列的PROPID__Q_INSTANCEM性中。队列的示例标识符可以用来检索队列的格式 名称或者用手工构造它。 2 3 .9 .4 标志 队列的PROPID_Q_LABEL属性可以被设S ,也可以被程序或者MSMQ Explorer管理c 队列 标志属性给用人类可直接读的文本字符串进行搜索提供了条件,虽然情况还不太清晰但是是可 能的。 2 3 .9 .5 类型 PROPID一Q_TYPE队列属性提供一个可以标识队列询问服务类型的机理。开发员和管理员可 以给仟何GUID设置队列的类型。这允许应用程序开发员使用GUIDGen.exe或者uuidge丨丨.e x e ,创 建可区別程序中所使用队列类的惟一标识符。与标志属性相比,该属性提供了一个可读性较低 但是更精确的类沏识别机制。 2 3 .9 .6 私有队列 私有队列是为应用程序独占性地使用而创建的队列。私有队列不在Active Directory ( 有效 目 录 )中注册,并且由大型计算机中的LQS跟踪来代替。私有队列通过路径名创建,但是在路 径名的机器名和队列名之间要插入PRIVATES,如下所示:Sauron\PRIVATE$\NewOrdersc 私有 队列不能通过检索Active Directory查 到 ,但是将它当作应答队列提供出来时,它可以由位用程 序使用。 2 3 . 1 0 机器 MSMQ机器存储消息队列,并且由MSMQ管理员完成对消息队列的配置工作f 不幸的 是 , 不能通过编程的方法增加机器到MSMQ系统中。在MSMQ世界 中 ,消息在企、Ik内部的队列中移 动。这种MSMQ企业经常是由Active Directory企业组成的森林或Active Directory区域树的直接 反射。MSMQ企业按部分分化到各站点屮。站点通过卨速、可 靠 的 连 接 (通常为LAN)连接到 管理单元,从而组织起呓一系列的MSMQ资源。特 定站点 的 客 户 机 将 使 用 同-站点 的 服 务 器 提 高消 息 传 递的效能 ,并使管 理任 务简单化 。因而站点可以通过可靠性较低速度也较慢的连接 ( 通常是WAN)在整个企业内部配合运行。这种模式需要有一个方法使同一站点内的客户机和服 务器能发现关于可用队列的消息。同 样 ,也需要一种方法使站点内的服务器能发现整个企业的 常规组成部分。MSMQ在 活 动 目 录 (Active Directory ) 中保持着队列、站点和企业配置消息。426 第四部分异步组件程序设计 机器属性 MSMQ机器或队列管理器有好几个属性,如表23-3所示 : 表23-3 MSMQ机 器 ( 队列管理器)属性 域 性 描 述 PROPID一QM 一CONNECTION 标识该计算机的连接网络(CN ) 淸甲• PROPID_QM_ENCRYPTION_PK 标识该汁筲机的公共密钥 PROPID_QM_MACHINE_ID 标识该计算机 PR0P1D_QM_PATHNAMR 标识该计箅机的MSMQ路抒名 PROPID_0M_SITH_ID 标识该计箅机所定义的MSMQ站点 23.11 MSMQ 企业 一个MSMQ企业由不同的MSMQ控制器服务器维持。控制器服务器允许许多MSMQ站点连 接起来组成一个企业。一个站点是指一系列可迅速而可靠地进行通信的MSMQ服务器。每个站 点可以山一个或多个连接的网络组成。连接M 络也可以跨越好儿个逻辑站点。一个MSMQ企业 通过MSMQ管理员管理站点连接而组合在一起。 2 3 .1 1 .1 站点连接 管理员建立站点连接,以允许消息在站点直接传递。站点连接的成本由管理员指定,它允 许MSMQ在MSMQ企业内部选择效率最好的路线来传递消息。 2 3 .1 1 .2 连接的网络 所有处于相同的LAN段中MSMQ机 器 ,使用相同的协议,就称为存在于相同的连接网络上。 MSMQ管理员给MSMQ服务器使用的连接网络取名,以便企业管理更简单一些3 23.11.3 MSMQ控制器 MSMQ控制器服务器是指定的管理服务器基础设施=> 所备的管理器担任着路由服务器的角 色负责在整个企业内部传递消息。在控制器结构的最顶端是一个最主要者企业控制器(PEC)。 PEC为它的站点担任主要的站点控制器(PSC),支持内部站点通信和站点连接的管理。PEC也 在MSMQ企业设置和结构的维护中担当最重要的机器,从所有MSMQ的连接站点中将消息结合 在一起。MSMQ企业中每个附加的MSMQ站点必须有惟 一 一 个主要站点 控 制 器(PSC ), 负责在 该站点中配置消息。备份的站点控制器可以作为附加的路由服务器加入到站点中,外在几台机 器之间分配PSC的负载。MSMQ机器也可以组成无控制器路由服务器的形式,给附加站点之间 的通信提供支持。 23.12 MSMQ客户机 MSMQ客户机使用队列管理器发送和接收消息。MSMQ队列服务器是运行于各个独立客户$ 2 3 1 MSMQ管理机构和体系结构 427 机或服 务 器i l 的后台服务《队列管理器在发送和接收公共队列消息的时候使用队列服务器。私 有队列由队列管理器本身维持,并且通常只对运行于该队列管理器机器上的应用程序可用。非 独立的客户机不能运行MSMQ队列管理器服务程序,并且必须同步连接到具有运行PSC功能的 站点中的一个服务器上。独立的客户机在没有连接到MSMQ企业的时候也能发送消息。没有连 接的时候发送的消息被编成队列,等以后再递送出去。独立客户机也能通过不M 的站点挂接到 MSMQ企业,只要少董或者根本就不需要重新配置。 23.13 MSMQ管理机构 MSMQ管理机构通过MSMQ Explorer或者本区域中带有Active Directory支持功能的Active Directory Manager ( 活:动目录管 理 器 )实现的。MSMQ Explorer为MSMQ企业提供一个图解式 的显示,并 且 允 许管 理 员 在企业 内创建和配置站点与服 务器。虽然旧的棊于NT-4的系统使用 SQL Servei•和面貌 酷似为支 持MSMQ而 特 别 设 计 的 MMC, 现代 的MSMQ系 统 使 用 Active Directory和Microsoft Management Console。 MSMQ管理者通过MSMQ管理控制台,可以增加队列、删除队列 ,并配置消 息、队列 、机 器 、站点和MSMQ企业的一些另外方面以及对象属性。MSMQ机器也提供•一个Java控制板小程 序 ,用来配置本地MSMQ存储器和别的特定机器问题。 在 一 个 系 统 上 安 装MSMQ的 过 程 是 不 同 的 ,依 赖 于 你 所 用 Windows的 类型与你想用 的 MSMQ版本。当前,MSMQ服务台程序可以通过下列软件包安装: • Windows NT 4 Option Pack • Windows NT 4 Enterprise Edition • Windows 2000 Server Distribution 安装过程带领你通过向一个现成的MSMQ企业添加你的服务器或者创建一个新企业来完成 安装。安装过程让你能够配S 你新创建服务器中的MSMQ客户机安装共享。客户机安装共卒可 以用来在你的企业内快速配置别的Windows系统,如MSMQ客户机程序。 2 3 . 1 4 小结 本章所述的都是基础性的。在本章中,你对MSMQ企业的各部分有了一个大致了解。我向你 介绍了MSMQ队列、消息和机器,以及它们的一些属性。同时研究了MSMQ对象属性的配置和理 论。本章最后部分,讲述了它们怎样才能配合在一起工作,并且给各种管理任务和工具怎样连接 到MSMQ提供了一个方向。接下来的两章将进入更好的讨论题目-怎样用这些东西开发程序。第24章 MSMQ程序设计 本章包括的内容: • MSMQ 库 API • 用MSMQ库API创建MSMQ应用程序 • MSMQ ActiveX Control API • 用Raw COM+接口创建MSMQ应用程序 • 用Smart Pointer创建MSMQPZ用程序 • 用VBScript创建MSMQ应用程序 对MSMQ程 序设计而 言,有 两个基本 的 出发点 :直接使用MSMQAPI;或者 使 用MSMQ ActiveX组件。MSMQ AP丨是一个为各种各样的MSMQ特性和错综复杂的关系提供存取访问服务 的扩展集。 你也许能感觉到,API方法使MSMQ程 序 设 计 比 別 的 方 式 笨 重 但 是 ActiveX有一个巨大的 优 点 ,这就是COM+。MSMQ A PI本来是一个基于一系列函数和结构的C程序设计语言风格接U 。 ActiveX M SM Q 程 序 设计接U 明显是一系列COM+界面和对象。因此ActiveX方法对从java到 Visual Basic Scripting Edition的任何东西都通用c 为 了 证 明 在 脚 本 世 界 中 “消耗量是安全的” ,组件通常提 供一 些 必要 的简单 服务 设 置,如 MSMQ ActivcX组 件 所做的 。不要i t 这 些东 西过早地影 响你 ,我认为你们有大约95% 的标准 MSMQ应用程序所需要的不是别的,而是MSMQ ActiveX组件表现出來的特性。 在下面的两节中,我将同时向你介绍本地MSMQ 库AP丨和ActiveX M SM Q绀件。你将按顺 序使用每一种技术创建相同的应用程序,这同时也是给你评价它们两者各15的正面作用和反面 作用的一个机会。当然,这里没有理由限制你使用其屮的一个界面,而不使用另一个。对于大 部分应用程序的工作,你可以轻松地使用ActiveX组 件 ;而3 需要实际的运行结果时,你可以访 问本地API调用的函数。 示 例中 所 有 的字符和字 符串操作 都使 用Unicode字 符 编 码进 行存 储。在广泛流行 使W 的 Unicode字符编码和一直受大众欢迎的ANSI八位码之间,现在处于一个过渡时期。因为这个原 因 ,我建议在大部分应用 程序 中使 用 一般的字符格式和宏(TCHAR宏及你自己所定义的宏>。 我在这些程序中一直使用Unicode字符编码,以集中MSMQ的注意力并且使COM+方面的事情合 理 化 。在通常情况下,MSMQ和COM+两者都只使fflUnicode字符编码(这也足全世界都知道的>。 你也许有兴趣知道为什么Windows N T 和Windows 2000内部使用Unicode字符编码丫,因为在使 用ANSI字符编码的地方将使性能受到影响。 24.1 MSMQ 库 API 我在干什么?我是在一本关于C0M+的书中说明一个大的、单调的、功能函数性的消息传递$ 2 4 1 MSMQ程序设计 429 API叫?冋到话题上来。MSMQ适应于COM+是因为它拥有一系列的服务和技术,不仅仅是一个 进行程序设计的界面。你很快将看到MSMQ是给COM+附加的最精彩的像瑞典式自助餐一样的 部分。COM+和MSMQ在许多决定性的方面互相依赖。MSMQ给传输同步COM+函数提供支持, 并 且 在 应 用 程 序 之 间 持 续 地 移 动 COM +对 象 。它 在 处 理 队 列 方 面 起 重 要 作 用 ,这能够参与 MTS/COM+的处理和一些别的重要的COM+交互作用。MSMQ是COM+的面向消息中间件系统, 因此它非常适合于完成任何大型的COM +任务。 在开 始前 ;我将告诉你怎样建立一些简单的基础性示范程序。有超过25种不同的MSMQ库 函数凋用,但开始时你只需要用到很少的几个。你以后将继续构造许多更完整的例子,用到功 能确实强大的MSMQ特性库调用c 我不知道你怎么样,与宜接输人相比,我更喜炊剪切和粘贴样板文件启动程序e 它比一个 干 净 、简 洁 、一般的例子对专业程序员更有帮助。最后一件你要花时间做的事情是对50MB的 网络巨物进行分类。 3 你鉍后找到寻求的方法时,有 可 能 问题 已 经 变得 更糟,但用接下来的5 个小吋,你削减掉像肥肉•-•样的多余部分之后,你就可以使用它了。我将尽力使你不会碰上这 种问题。 你现在可以写一个具有如下功能的程序了,创建一个队列 ,发送一个消息到该队列,从该 队列中获得•-个消 息,最后删除该队列。你可以将它做成菜单驱动的,这样你就可以使用两个 通过网络处理消息的应用程序拷贝。实际上,在 完 成 r 用MSMQ AP〖和ActiveX组件对这个例子 的程序设计之后,你就可以一起使用它们,证明互操作性了。 2 4 . 2 用 MSMQ库 API创 建 一 个 应 用 程 序 在开始写你的第一个MSMQ应用程序之昉,你需要获得一个能进行本地MSMQ设计的环境。 前面一章已经讨论了手工安装MSMQ和管理队列的问题。下一步就是对这些相同的零碎工作获 得整体的看法。 建立中间件幵发环境是一个尝试性的工作。在微软环境的幵发中我确实喜欢的一件事情是 不用在建立中间件开发环境这方面担忧。开发MSMQ应用程序惟一需要做的事怙就是安装Visual C++ 6.0版 (或者更近发行的版本h 为了用MSMQ编译应用程序,你需要在程序中包含MSMQ数据头文件,MQ.H。你同时也需 要增加MSMQ数 据 库 ,MQRT.LIB,到 连 接 库 中 查 找 列 表 完 成 你 &用 程 序 的 连 接 这 些 是 在 Visual Studio shell中 完 成 的 ,通 过 选[Project]Settings([对 象 ]设 置 >,选 取 连 接 标 签 , 并在 Object/library模块中增加库名:edit box ( 编辑桐. )。最后一点,你需要在测试系统中安装MSMQ, 实现应用程序的成功运行。 2 4 .2 .1 格式名称 你将构造第一个Hello-MSMQ应用程序,作为MSMQ API控制台应用程序和用一个简单队列 通信的例子c 你将看到,所有的MSMQ库调用从MQ前缀开始。你的应用程序将在用户的命令下 在本地机器上建立一个队列c 所有队列都由C3U【D惟 - 定 义 ,GU1D在队列创建的时候由MSMQ分配 。许多MSMQ库调用430 第四部分异步组件程序设计 需 要 一 个 队列 格 式 名 称 ,该名 称 存 储为 一 个 以空 结尾的Unicode编 码 字 符 串 ,如PUBLTC = f5271149-8634-1 Id2-a4f0-abl9dda7f755。格式名称仅仅由它的B 标队列位置和GUID决定。公有 队列格式名称可以长达44个字符长度,而私有队列可以长达54个字符长度。 2 4 .2 .2 路径名 队列的路径名是跟队列同时创建的。路径名走义了队列的显示名称和队列的宿主机,如 : Tempest\BasicQueue。 你可以用一个号来表示本地机器的队列服务器,如这种方 式 :ABasicQueue0 2 4 .2 .3 查找格式名称 坚持格式名称不变是很有用的,因为这会产生一个事例依赖性。也 就 是 说 ,如果队列被解 散了并重建,第二次重建的队列将有一个不同的格式名称。因为这个原因,MSMQ提供了几种 查找队列格式名称的方法。你的第一个应用程序用路径名杳找格式名称,这种方法查找到的结 果通常是一致的。表24-1给出丫不同的格式名査找技巧。 表24-1格式名称査找函数 查找函数 描 述 MQPathNameToFormatNameO 从队列路径名中构造格式名 MQlnstanceToFormatName 从队列GUI屮构造格式名 MQHandleToFormatName 从开放队列句柄中构造格式名 获得队列路径名的消息是非常通用的方法,因为对惟一的队列描述而言,这是对我们更具 有直接可读性的东西。(路径名对队列的格式名称的关系,有点像Progld对COM对象的GU【D )。 通过路径名取得格式名称就是一个简单的问题了,比如用如下所示范的程序代码: //Get the format name of the queue using the pathname HRESULT hr; //Return status WCHAR wcsForraatNaine[64]; //Queue format name DWORD dwForinatNameLen = 64; //Format name but size hr = MOPathNameToFormatName( L“.WBasicQueue“, wcsFormat^ame, &dwFormatNameLen ); 现在你可以对该程序的关键部分有一个更细致的了解。 〖己住,用Visual C++6创 建 ,包括 MQ.H数据头,并且连接到MQRT.LIG库 ,这些是你进行MSMQ开发的必要步骤。该程序分成了 四个 普通 的 可重 复使 用 的例程 :CreateQueue( ) ,SendMessage( ) ,ReceiveMessage() 和 DestroyQueue()。我们先看CreateQueue()例程。 2 4 .2 .4 用属性工作 在你实际创建队列之前,你必须设置一个MQQUEUEPROPS结 构 ,该结构指定你建立的新 队列的厲性。MQQUEUEPROPS在MQ.H中如下定义:第24章 MSMQ程序设计 431 typedef struct tagMQQUEUEPROPS { OWORD cProp; QUEUEPROPID aPropID(); PROPVARIANT aPropVar(); HRESULT aStatusl); } MQQUEUEPROPS; MSMQ使用cProp字段判断你指定了多少个属性。另三个结构成员分别是一个属性的一元数 组 。APropID字 段是惟一 的MSMQ定 义 的 属 性 标 识 符 数 组 ,表示在 那 个变址位置 上 的 属 性 。 aPropVar字段是一个变体型数组,用来存储属性值。aStatus字段是一个HRESULTs数 组 ,用来存 储每个错误和正确的属性提交的关联代码。队列管理器和消息使用相同类型的属件结构。 在你的CreateQueue ( ) 例程 中,你只需要一个简单的属性,PROPID_Q_PATHNAME, 它用 来指定队列的路径名。虽然在队列创建的时候会设置好几个属性,但是路径名是这里真正所需 要的惟一属性。下面摘录的 程 序 代码 示范了 说 明 和初 始化与 数值数组 的 过 程 。这个例子忽略 了基于属性的错误,并且因此传一个空指针到队列属性结构中的aStatus字段。 “Set up properties (we only need the path name property) PROPVARIANT aVanant(l ]; / /Property value array 2 4 .2 .5 创建队列 接下来的一步,就是调用带有初始化了MQQUEUEPROPS结构的MQCreateQueue()库例程, 创建队列。MQCreateQueue()调用能够随意地返回新创建队列的格式名称。很像别的COM4•服务 功 能 ,所有通过MSMQ API处理的字符都用Unicode字符编码格式存储。这个应用程序内部使用 Unicode字符编码,因此你所有的库例程可以广泛地调用。公有队列的格式名称缓冲区至少应该 有44个Unicode字符编码字符长度,私有队列的格式名称缓冲区至少应该有54个Unicode字符编 码 字 符 K 度 。如果给 出 的缓 冲区小了 ,MQCreateQueue()调用会返回MQJNFORMATION _ FORMATNAME_BUFFER_TOO_SAMLL ( 最 近 被 授 予 了 世 界 最 长 符 号 的 大 奖 )消 息 ,在 dwFormatNameLen参景中设?J:所需要的缓冲区大小的方法如下: QUEUEPROPID aPropId[l]; //Property id array aPropId(01 = PROPIO_Q_PATHNAME ; aVariant[0].vt = VTlLPWSTR; aVariant[0].pwszVal = L'.WBasicQueue“; //Set up the property struct MQQUEUEPROPS QueueProps; QueueProps.cProp = 1; QueueProps.aPropID = aPropId; QueueProps.aPropVar = aVariant; QueueProps.aStatus = NULL; 2 4 .2 .6 解散队列 //Queue properties struct //Number of properties //Properties id array //Properties value array //No error reports 下 一 步,我们荇一下解散队列这方面。解散一个队列所有你所需要做的,就是解散格式名 称和 其认可 。下面是怎么获得 格式 名 称的技 巧。前面讨 论 过 ,你可以通过对MSMQ MQPath432 第四部分异步组件程序设计 NameToFormatName( >库的调用,由队列路径名称获得格式名称: //Create queue HRESULT hr; //Return status WCHAR wcsFormatName[64]; //Queue format name DWORD dwFormatNameLen = 64; //Format name size hr = MQCreateQueue( NULL, //Security descriptor &QueueProps, I/Queue properties wcsFormatName, //Receive buffer &dwFormatNamelen ); I/Size of buffer 创建总是比解散要难,从下面的这个例子中你就可以体会到,MSMQ没有返回值。 //Destroy queue hr = MQDeleteQueue(wcsFormatName); 2 4 . 2 . 7 打开队列 按照Windows方法,你在使用任何系统资源之前,你需要获得它的句柄。队列句柄通过调用 MSMQ MQOpenQueue()库例程来询问队列的格式名称而返回,同时返凹的还有存取〖方问和共享 参量。表24-2和表24-3给出了可使用的存取汸问和共享选项。在队列的打开状态,不能改变其存 取访问许可。 表2 4 - 2 队列存取访问许可 存取访问标忐 许可授权 MQ_PEEK_ACCESS 消息可以检验但不能_ 除 MQ_SEND_ACCHSS 消息可以发送 MQ_RECEIVE_ACCESS 消怠可以检验和删除 表2 4 - 3 队列共享设置 共享标志 何种共享 MQ_DENY_NON£ 队列对所有进程可用 MQ. DHNY. RECEIVE.SHARR 只有呼叫进稈可以从队列中获取消息 打开队列很像打开 •个W i n 3 2 对象。下面是一个例子: 1/Open the queue with send access QUEUEHANDLE hQueue; //Handle to queue hr = MQOpenQueue( wcsFormatName, //Queue format name MQ_SEND_ACCESS,//Access requested MQ_DENY_NONE,//Share mode 2 4 . 2 . 8 发 送 一条 消息 &hQueue); //Queue handle 现在,我们看看发送消息。发送一条消息有点像创建一•个队列,因为它们主要的任务是创建 一系列属性并将其传送到MSMQ库调用中。其不同的地方是,消息的属性存储在MQMSGPROPS 结 构中 ,而队列厲性存储在MQQUEUEPROPS结构中。MSMQ消息是一个消息属性的简单集合。第24章 MSMQ程序设计 433 几 乎 有 50种 不 同 的 属 性 支 持 MSMQ 消 息 对 象 。在 这 个 基 本 的 例 子 中 ,你 只 用 到 三 个 属 性 : PROPID_M_LABEL ( 给消息指定一个标志),PROPID_M_BODY—TYPE ( 指示消息包含的主体 内容的类型),和PROPID_M_BODY ( 消息包含的主体)。 消息标志在长度上可以达到249个字符,并且像所有MSMQ字符串一样,消息标志用Unicode 字符编码格式存储。当列举队列中的消息的时候,标志是可选的而且经常使用,它显示为一个字 符串。消息主体是一个任意字节长度的向量,除非你设置一个属性类型来描述数据字段,否则它 只对应用程序进程具有消息的意义。 为发送一个消息,应用程序调用带有三个参量的MQSend Message^例程 ,那三个参量为:询问得到的队列句柄、消息属性和一个可选的事务对象指针。下 一章,我将详细描述事务。清单24-1示范了通过MSMQ发送一个简单字节数组消息的例子。 ■ 清单24-1 —个简单的字符串消息 //Allocate message property objects MQMSGPROPS MsgProps; //Message MSGPROPID aMsgPropId[3]; //Message PROPVARIANT aMsgPropVar 【3]; //Message ^define MSGLABELSIZE 64 #define MSGBODYSIZE 128 properties struct property id array property value array label.counter label buffer //Set up message label static int s_cMessageNumber = 0; //Message WCHAR wcsLabel[MSGLABELSIZE]; //Message swprintf(wcsLabel,LMMessage number , ++s_cMessageNumber); aMsgP「opId 【0] = PR0PID_M_LABEL; aMsgPropVar[0J.vt = VT\PWSTR; aMsgPropVar[0].pwszVal = wcsLabel; //Set up message body WCHAR wcsBuf[MSGBODYSIZE]; wprintf(L“Enter message body: M); _getws(wcsBuf); aMsgPropId[1] = PR0PID_M_B0DY; aMsgPropVar[1].vt = VT~VECT0R | VT_UI1; aMsgPropVar(1 ].caub.pElems = (LPBYTE)(wcsBuf); aMsgPropVar[l].caub.cElems = (wcslen(wcsBuf) + 1) * sizeof(WCHAR); //Set up the body type aMsgPropId[2] = PR0PID^M_B0DY_TYPE; aMsgPropVar 【2] .vt = V T ^ U M ;— aMsgPropVar[2J.ulVal =~VT_BSTR; //Set up message property struct MsgP 「叩 s.cProp = 3; //Number MsgProps.aPropID = aMsgPropId; //Ids of MsgProps.aPropVar = aMsgPropVar; //Values MsgProps.aStatus = NULL; of properties properties of properties //No error reports //Send message MQSendMessage( hQueue, &MsgProps NULL ); //Queue handle //Message properties //Transaction434 第四部分异步组件程序设计 注意,这个程序可以只用PROPID_M_BODY厲性构造出来c PROPID_M_LABLE是修饰性 的 ,但在蕴含PROPID_M_BODY_TYPE的后面还有一个隐蔽的目的。我们的应用程序知道消息 主体经常含有多字节字符串。另一方_ ,MSMQ ActiveX组件却不知道。通 常 ,ActiveX组件只 支持下列类型: VT一 12, VT_UI2,V T J4 , VT一UI4,VT—R4,VT_R8, VT_CY,VT_DATE, VT_BOOL, VT一II,VT_UI1, VT_BSTR, VT__ARRAY,VT一STREAMED_OBJECT 和 VT_STORED_OBJECT0 另外的类型被ActiveX组件视为原始字节流。增加VT一BSTR类型属性 使API应用程序更容易与ActiveX组件应用程序相配合。 2 4 .2 .9 接收一条消息 下一步,你要增加一些程序用来接收消息。RecieveMessage( )例程从查找目标队列的格式名 称和获得队列的句柄开始工作,这就像是在SeruLMessagK )例程中所做的一样。下一步是构造 MQMSGPROPS对 象 ,它作为你想接收属性的缓冲区。你并不需要接收所有和任意原始消息队列 化的属性。MSMQ拋弃任何你在非封装消息结构中没有提供的屈性。这可以节省不需要的内存拷 贝。为接收消息,你也必须提供你要求的缓冲区大小的属性消息给MSMQ。给每个变长度属性缓 冲区提供属性长度是一个简单的事情。清单24-2的例子代码用四个属性与发送例子中的两个属性 进行比较,并且它示范了一个简单的消息接收方案。两个附加的属性是PR0 P1D_M_LABEL_LEN 和PROPID_M_BODY_SIZE。如果给队列数据提供的输出缓冲区太小,MSMQ给出如下的错误 MQ_ERROR_BUFFER_OVERFLOW (—个普通的错误,指示消息主体缓冲区太小)。 清单24-2接收消息 //Allocate message property objects MQMSGPROPS MsgProps; MSGPROPID aMsgPropId[4]; PROPVARIANT aMsgPropVar[4]; ^define MSGLABELSIZE 64 #define MSGBODYSIZE 128 //Set up message label and length WCHAR wcsLabel[MSGLABELSIZE ]; aMsgPropIdl®] = PROPID_M_LABEL; aMsgPropVar[0].vt = VT^LPWSTR; aMsgPropVar[0].pwszVal = wcsLabel; aMsgPropId[1】* PROPID_M_LABEL_LEN; aMsgPropVar[l].vt = VT_UI4; aMsgPropVa「[1I.ulVal =一MSGLABELSIZE //Set up message body and length WCHAR wcsB0dy(MSGBODYSIZE ]; aMsgPropId[2] = PR0PID_M_B0DY; aMsgPropVar[2].vt = VT^VECTOR | VTJJI1; aMsgPropVarl2].caub.pElems = (LPBYTE) wcsBody; aMsgPropVar(2).caub.cElems = MSGBODYSIZE * sizeof(WCHAR); aMsgPropIdl3) = PROPID_M_BODY_SIZE; aMsgPropVar[31.vt = VT^UI4; aMsgPropVar(3].ulVal =~MSGBODYSIZE * sizeof(WCHAR); //Message properties struct //Message property id array //Message property value array //Message label buffer //Message body buffer^ 2 4 t MSMQ程序设计 435 //Get first message hr = MQReceiveMessage( hQueue, //Queue to receive from //Timeout RECEIVE, //Receive or peek //Message properties //Overlapped structure //Callback function //Queue cursor //Transaction I/Set up message property struct MsgProps.cProp = 4; //Number of properties MsgProps.aPropID = aMsgPropId; //Ids of properties MsgProps.aPropVar = aMsgPropVar; //Values of properties MsgProps.aStatus = NULL; //No error reports 最后一步是调用MSMQ MQReceiveMessage()库例程。MQReceiveMessage()调用用到几个 附加参量。通常感兴趣的是超时参量。MQReceiveMessage()支持同步和异步调用。同步版本会 依照超时设置数值可能为阻塞和非阻塞。超时值为零产生轮询行为,我们将在下个例子中使用 这种功能。从数值1到符号常量INFINITE能导致进程被锁,直到消息可用或者用完指定的毫秒时 间数进程才解锁。重裔结构和回调函数在异步工作中使用,这一点我们将在下一章学习。 下一个感兴趣的MQReceiveMessage()调用参量字段是动作字段。表24-4列出了可用的动作 选项。 表24-4结束消息方式 接收方式 接收行为 MQ_ACTION_RECEIVE 读取当前消息并删除它 MQ_ACTION_PEEK_CURRENT 读取当前消息,并+刪 除 它 MQ_ACTION_PEEK^NEXT 读取下一个消息,并不刪除它 因为一个队列可能不只一个可用消息,所以MSMQ提供使应用程序可以查找整个队列的游 标 ,这有点像数据库程序中使用的游标。当前的例子设置MQReceiveMessagM )游标句柄字段为 NULL,因此限制你只能从队列的前部开始读。因为你并不需要处理,处理对象也设为NULL。 2 4 .2 .1 0 关闭队列 标准资源管理器要求我们释放任何目标锁定的句柄,并且MSMQ也不例外。下面的程序示 例示范了MQCloseQueue()例程 。 //Close queue MQCloseQueue(hQueue); 它就这些。清单24-3给出了MSMQ库版本应用程序的全部代码。 清单24-3 MSMQ库例子程序 // HelloMSMQ.cpp II II Simple MSMQ API Application 0, MQ—ACTIO &MsgProp NULL, NULL, NULL, NULL ):436 第四部分异步组件程序设计 //Dependencies ^include #incXude ^include ^include < m q .h> //Symbols ^define FORMATNAMESIZE #define MSGBODYSIZE #define MSGLABELSIZE #define QUEUEPATHNAME 64 128 64 WBasicQueue' //Prototypes void CreateQueue(); void SendMessage(); void ReceiveMessage(); void OestroyQueue(); “Application entry point int main() { int iChoice; * //User input buffer //User command processing loop do { //Display menu wprintf(L*Choose a command:\n“); wprintf(L“0 • Exit\n“); wprintf(L“1 • Create the Queue\n“); wprintf(L“2 • Send a Message\n*); wprintf(L“3 • Receive a Message\n“) wprintf(L“4 • Destroy the Queue\n“) wprintf (L“\n» ■); //Get selection iChoice = getch(); wprintf(L“%c\n\n*,iChoice); //Execute command switch ( iChoice ) { case 'C': break; case 'r : CreateQueue(); break; case '2*: SendMessage(); break; case '3': ReceiveMessageO; break ;$ 2 4 1 MSMQ程序设计 437 c a s e .4': OestroyQueue(); break; default: wprintf(L“Bad choice.\n\n“); break; } } while ( iChoice != '0'); //Return success return 0; void CreateQueue() { //Set up properties (we only need the path name property) PROPVARIANT aVariant[1]; //Property value array QUEUEPROPID aPropIdll); //Property id array aPropIdlC] = PROPID_Q—PATHNAME; aVariant(0].vt = VT_LPWSTR; aVariant[0].pwszVal = QUEUEPATHNAME ; //Set up the property struct MQQUEUEPROPS QueueProps; QueueProps.cProp = 1; QueueProps.aPropID * aPropId; QueueProps.aPropVar = aVariant QueueProps.aStatus * NULL; //Queue properties struct I/Number of properties //Properties id array //Properties value array //No error reports //Create queue HRESULT hr; WCHAR wcsFormatName[FORMATNAMESIZE); DWORD dwFormatNameLen = FORMATNAMESIZE; hr = MQCreateQueue( NULL, &QueueProps, wcsFormatName, //Return status //Queue format name //Size of format name //Security descriptor //Queue properties //Format name buffer //Check results if ( F A I L E D ( h r ) ) wprintf(L'Error e l s e wprintf(L“Queue } void SendMessage() &dwFormatNameLen ); //Format name size creating queue: %x\n\n“,hr); created with format name: %s\n\n_,wcsFormatName}; //Get the format name of the queue using the pathname HRESULT hr; //Return status WCHAR wcsFormatName[FORMATNAMESIZEJ; //Queue format name DWORD dwFormatNameLen = FORMATNAMESIZE; //Size of format name hr = MQPathNameToFormatName( QUEUEPATHNAME, wcsFormatName, &dwFormatNameLen ): //Check results if ( FAILED(hr))438 第四部分异步组件程序设计 wprintf(L“Error looking up queue format name: %x\n\n*,hr); return; //Open the queue with send access QUEUEHANDLE hQueue ; hr = MQOpenQueue( wcsFormatName, MQ一 SEND_ACCESS, MQ_DENYJ40NE, &hQueue7; //Check results if ( F A I L E D ( h r ) ) wprintf(L'Error opening queue: %x\n\n“,hr); return: //Handle to queue //Queue format name //Access requested I/Share mode //Queue handle //Allocate message property objects MQMSGPROPS MsgProps; MSGPROPID aMsgPropId(3]; PROPVARIANT aMsgFropVar[3]; //Set 叩 message label static int s_cMessageNumber = 0 ; WCHAR wcsLabel[MSGLABELSIZE ]; swprintf(wcsLabel,LMMessage number aMsgPropId(«] = PROPID_M_LABEL; aMsgPropVar[0].vt = VT'LPWSTR; aMsgPropVar(0].pwszVal = wcsLabel; //Message properties struct //Message property id array //Message property value array //Message label counter //Message label buffer %d_, ++s_cMessageNumben); //Set up message body WCHAR wcsBuf[MSGBODYSIZE); wprintf(L'Enter message body: •}; _getws(wcsBuf); aMsgPropId(1] = PROPID_M_BODY; aMsgPropVar(1].vt = VT_VECT0R | VT_UI1; aMsgPropVar[l].caub.pElems = (LPBYTE)(wcsBuf); aMsgPropVar[1 ].caub.cElems = (wcslen(wcsBuf) + 1 ) //Set up the body type aMsgPropId[2] - PROPID_M_BODY_TYPE; aMsgPropVar[2].vt = \rr_UI4; aMsgProp*^ar[21 .ulVal = VTBSTR; sizeof(WCHAR); //Set up message property struct MsgProps.cProp = 3; MsgProps.aPropID = aMsgPropId; MsgProps.aPropVar = aMsgPropVar; MsgProps.aStatus = NULL; //Number of properties //Ids of properties //Values of properties //No error reports //Send message MQSendMessage( hQueue, &MsgProps, NULL ): //Queue handle //Message properties I/Transaction^ 2 4 1 MSMQ程序设计 439 //Check results if ( FAILED(hr)) wprintf(L“Error sending message: %x\n\n“,hr); else wprintf(L“Message sent\n\n“); //Close queue MQCloseQueue(hQueue); } void ReceiveMessageO { //Get the format name of the queue using the HRESULT hr; WCHAR wcsFormatNameIFORMATNAMESIZE1; DWORD dwFormatNameLen = FORMATNAMESIZE ; hr = MOPathNameToFormatName( QUEUEPATHNAME, wcsFormatName, &dwFormatNameLen ); pathname //Return status //Queue format name //Size of format name //Check results if ( F A I L E D ( h r ) ) { wprintf (L__Error looking up queue return; format name: %x\n\nM,hr) //Open the queue with send access QUEUEHANDLE hQueue ; hr = MQOpenQueue( wcsFormatName, MQ_RECEIVE_ACCESS; MQ DENY NONE, &hQueue); //Check results if ( F A I L E D ( h r ) ) //Handle to queue //Queue format name //Access requested //Share mode //Queue handle wprintf(LMError opening queue: %x\n\n*,hr); return; //Allocate message property objects MQMSGPROPS MsgProps; //Message MSGPROPID aMsgPropId 丨4]; //Message PROPVARIANT aMsgPropVar 丨4 】; / / M e s s a g e properties struct property id array property value array //Set up message label and length WCHAR wcsLabel丨MSGLABELSIZE】; a M s g P r o p I d 丨0] = PROPID_M_LABEL ; aMsgPropVar[0].vt = VT_LPWSTR; aMsgPropVar[0].pwszVal = wcsLabel; aMsgPropId(1] = PROPID_M_LABEL_LEN; aMsgPropVar!1).vt = VT_UI4; aMsgPropVar丨1J.ulVal =~MSGLABELSIZE; //Message label buffer //Set up message body and length440 第四部分异步组件程序设计 //Message body bufferWCHAR wcsBody[MSGBODYSIZE]; aMsgPropId[2] = PR0PI0_M_B0DY; aMsgPropVar[2].vt = VTIVECTOR | VT_UI1; aMsgPropVar[2).caub.pElems = (LPBYTE)wcsBody; aMsgPropVar[2].caub.cElems = MSGBODYSIZE * sizeof(WCKAR); aMsgPropId(3) = PR0PID_M_B0DY 一SIZE; aMsgPropVar[3].vt = VT_UI4; aMsgPropVar[3).ulVal =~MSGBODYSIZE; //Set up message property struct MsgProps.cProp = 4; MsgProps.aPropID = aMsgPropId; MsgProps.aPropVar = aMsgPropVar; MsgProps.aStatus = NULL; //Number of properties //IDs of properties //Values of properties //No error reports //Get first message hr = MQReceiveMessage( hQueue, //Queue to receive from 0, //Timeout MQ_ACTION_RECEIVE,//Receive or peek &MsgProps, NULL, NULL, NULL, NULL ): //Message properties //Overlapped structure //Callback function //Queue cursor //Transaction //Check results if ( FAILED(hr)) wprintf(L“Error receiving message: %x\n\n“ ,hr) MQCloseQueue(hQueue); return; //Display message wcsBodyl(aMsgPropVar[3).ulVal)/sizeof(WCHAR)] = L*\0'; wprintf(L“Retrieved message label:\n\t%s\n“,wcsLabel); wprintf(L'Retrieved message body:\n\t%s\n\n“,wcsBody); //Close queue MQCloseQueue(hQueue); void DestroyQueue() { //Get the format name of the queue using the HRESULT hr; WCHAR wcsFormatName[FORMATNAMESIZE]; DWORD dwForiuatNameLen = FORMATNAMESIZE; hr = MQPathNameToFormatName( QUEUEPATHNAME, wcsFormatName, &dwFormatNameLen pathname //Return status //Queue format name //Size of format name //Check results if ( F A I L E D ( h r ) ) wprintf(L“Error looking up queue format name: %x\n\n“,hr); return;茗24章 MSMQ程序设计 441 //Destroy queue hr = MQDeleteQueue(wcsFormatName); //Check results if ( F A I L E D ( h r ) ) wprintf(L“Error destroying queue: %x\n\n“,hr); else wprintf(L“Queue destroyed\n\n“); } 24.3 MSMQ ActiveX控 制 API 在动态结构COM+对象上下文中,ActiveX组件是一种分布式结构,这也是相对十在COM+ 界面后部运行的COM+类行为而言的。这就是说MSMQ ActiveX组件的行为被很好地封装了。单 在Visual C++中 ,就有好几种使用MSMQ组件的方法,它包括原始COM+接 口 ,Class Wizard调 度封装器,和ATL型灵巧指针。在别的语言中还有大鲎的附加技巧可以使用。例 如 ,你可以试用 三种不同的方法:原始COM,ATL型灵巧指针,和你最后可以用Visual Basic小程 序 ,这也表示 出初级程序员的谦逊。 使用MSMQ ActiveX组件的第一个例子将使用原始COM +接口。所有COM+组件餺置COM+ 接口。另一方面,COM+用户在设计COM+对象时能使用任何级别的抽象可用度。级别最低最常 用也是最有效的方法是直接使用COM +接口。这通常意味着幵发者只需做相当少的工作,在其过 程中他们很容易步入圈套。比如,一个使用对象的小程序忘记了释放对象;脚本程序引擎注意到 了这点。一个初级的COM+接口程序开发员,如果没有释放接口指针会导致资源在他的手上泄漏。 正如你已看到的,这是一本关于COM+内核的书。因此讲述的都是应用程序底层知识。即使 你使用本书讲述的其他MSMQ开发方法,也会使今后的高层应用程序幵发变得容易。 在 “Hello • World” MSMQ应用程序中的第二个COM**■部分利用了ATL派生的买巧指针技术。 C~m■程序员总是以其功能强大而引以为豪,但同时又很欣赏许多利用了COM技术的应用程序的 简洁性,就像Visual Basic—样可以构建髙层应用环境。ATL在简单性上已经严重影响了C++中的 COM+编程模型,并且ATL已不仅仅是用来编写代码的模板集。Visual C++5引人了#import语 句 , 它可用来产生与COM+组件类型兼容的信息。Visual C++6改进了开发环境与调试工具,可以与 ATL更好地结合。也许最令人兴奋的基于ATL的技术是Visual C++7中引入的COM+编程技术。尤 其是Coclass属 性 (它可以自动地创建进程中通过基于ATL的代码将C— 类转换为COM*♦•类 )。 在第二个MSMQ ActiveX组件例子中,你会看到使用灵巧指针与ATL可使COM+编程更容易。 在最后一个例子中,你会写一个与前面例程、功能相同的一段Visual Basic脚本。并非给你 出难题。为了给一个关键性应用延长生存期而增加几行代码很值得。但若系统管理员想往企业 中增加几个特别的员工会如何呢?或者没有编写任何客户瑞代理就向你的服务器端应用程序提 供一个基于消息的脚本代接口又会如何呢?在这些情况下,Visual Basic脚本是最适合的。最后 一个例子演示了 MSMQ编程的髙端应用。 MSMQ ActiveX类 在MSMQ ActiveX组件设置中有10种不同的COM+类。这还有10个附加的对象类型用来专门442 第四部分异步组件程序设计 支持NiSMQ email。这看起来有点像MSMQ库AP丨总共管理的26个函数。关于一个好的COM+程 序设计模式,这有一个重要的需要记住的事情就是它通过模块化系统中的实体使程序开发过程 简单化了,这并不需要减少对象的总数R 。在问题领域,比较容易明白和使用一系列直接映射 到机理的对象。而对于一些非直觉接口随意减少的组件,却不太好使用。髙级程序设计系统遇 到每个对象有不只一个接U 的问题时,如V B S cript,进行工作时上面所说的问题就尤为突出。 实 际 上 ,MSMQ ActiveX组件设置已经被制作成对任何程序开发环境都能适应得较好,特别是在 较高级别时。 所有MSMQ COM+类型都从MSMQ前缀开始。每个MSMQ COM+类型支持一个用类型和前 缀I复合命名的附属接口。一个基本的应用程序需要用到三个MSMQ类型和它们的附属接口: MSMQQueuelnfo IMSMQQueuelnfo MSMQQueue IMSMOOueue WSMQMessage IMSWQMessage •第一个类是MSMQQueuelnfo。这个对象类型用在创建和解散队列中,而迀也用来打开一个 已经存在的队列。下一个你要用到的类是MSMQQueue。该队列类用来在MSMQ系统中展现实际 的 队 列 (示例队列),为从队列中浏览和取回消息提供支持。ft后一个介绍的类型MSMQMessage 类。MSMQMessage类用来展现个别通过队列对象发送和接收的消息。 2 4 . 4 用COM+接口创建MSMQ应用程序 用本地COM+调用创建COM + 应用程序,它提供许多强大的COM+优点。它当然记得下列 总开销,维持一般的COM + 接口、必需的IUnknown特 性、各种初始化和关闭问题。重要的是你 只需要学习COM+— 次 ,而各种问题在程序开发环境中总是一次又一次地存在。所以用本地 COM编制MSMQ应用程序会减少许多麻烦。 你的COM+应用程序版本不需要在对象设置中做任何详细的连接判断。毕竟,COM+程序设 计的一个特点就是动态的接口解决方法。下一步,注意你需要展现不同支持环境下的编译程序, 这依赖于你怎样处理自己的问题。在初级COM+例子中,你需要包含一个从MSMQ组件类型库 产生的头文件。所讨论的头文件是MQOAI.H,它是为Visual C++提供的^该头文件定义不同的 接口和存取访问MSMQ COM+对象所需要的类ID。 2 4 .4 .1 定义接口和GUID 这 是 一 个 “gotcha”。MQOAI.H头文件可以单独使用,因为它包含了别的COM+必 要 成 分 。 其中最重要的一点是直接包含了头文件objbase.h。这个问题是由于GUID定义于MQOAI.H中这 个事实导致的,如下所示: DEFINE_GUID(IID_IMSMQMessage,0xD7D6E074L, 0XDCCD,0x11D0,0XAA,Cx4B ,0X00,0x60,0x97,0X0D,CxEB,0xAE ); 通过头文件中的程序语句定义东西(就是说事实上分配存储器 可能导致一些连接程序损 伤的问题。使用预编译的头文件使问题更加恶化。因为这个原因,objbase.h用下面的程序语句第24章 MSMQ程序设计 443 定义 DEFINE_GUID: #ifndef INITGUID ^define DEFINE_GUID(name, 1, w1, EXTERN^C const GUID FAR name #else #define DEFINE_GUID(name, 1, wl, EXTERN_C const GUID name = { 1, w 1 , w2, { #endif // INITGUID w2, b1, b2, b3, b4, b5, b6, b7, b8) \ w2, \ b1, D1, b2, b3, b4, b5, b6, b7, b8) \ b2, b3, 1)4, b5, b6, b7, b8 } } 正如你所见到的那样,INITGUID必须这样定义一次,以产生真实的GU丨D 。如果你没有定 义1NITGU1D,你将以所有MSMQ GUID获得未分辩的索引结尾。如果你在多于一个模块中定义 INITGUID,你将因为重复的GUID和更多的连接错误结束。所以你不能不定义,也不能多次定 义INITGUID。你的简单应用程序中有一个源程序文件,它使你能够使用下面简单化的include 方 法 : //MSMQ ActiveX component header II ( W e need to define INITGUID to force the definition II of the MSMQ QUIDS ) #define INITGUID ^include 2 4 .4 .2 初始化COM 除了CoGetMallocO外 ,任何对COM+的方法调 用 ,需要适当地初始化。下面的几行程序设 置和关闭单线程应用程序中的单线程单元。除这些附加的异常情况外,maiiK )例程是一种与前 面的例子同等的另一类实现方式。调用CoInitialize()与调用CoInitializeEx()有相同的效果。 //Initialize COM CoInitializeExtNULL.COINIT^APARTMENTTHREADED); //body of main function • 參• //Uninitialize C0M+ and return success CoUninitialize(); 然而CoInitialize()在现代COM+程序开发中被禁止使用。如果你在编译CoInitializeEx〔)的调 用 函 数 时 出 了 问 题 ,你 要 试 着 定 义 _WlN32_DCOM 符 号 。CoInitializeEx()有条件地基于 _WIN32__DCOM符号或者_\^拊 32_1^中的数值大于等于0x0400来声明。例 如 ,NT5 bera2就有 CoInitializeEx()的问题,它不能通过将..WIN32_DCOM包含到你的对象符号清单 ( Project[Settings]C/C++tab ) 中来简单地解决。 2 4 .4 .3 创建队列 为了创建一个队列,你需要获得一个指向MSMQQueuelnfo对象的IMSMQQueue丨nfo接口的 指针。IMSMQQueiielnfo接口支持用Create()方法创建新队列。因为你已经从功能的API迁移到444 第四部分异步组件程序设计 面向组件的接U 、厲性和类似性对象,这在该应用程序要求的程序代码中扮演一个重要的角色。 特别地厂相对于查询函数调用,数值经常作为不同对象类塑的诚性设置并被取回。有一个简单 的CoCreatelnstance()调用,它将给你提供所需要的对象和接口 : //Get an interface pointer to a Queuelnfo object HRESULT hr; IMSMQQueuelnfo * pQInfo; hr = CoCreateInstance( CLSID_MSMQQueueInfo, NULL,— CLSCTX_SERVER, IID_IMSMQQueueInfo, (LPVOID *>&pQInfo ); 2 4 .4 .4 变体型 由你前面所学的,队列可以与事务的和全局的属性一起创建。你的基本需求不需要特殊的 特 性 ,因此你可以简单地设置所需的路径名并传送虚假的队列属性到丨MSMQQueudnfoxCreatd )函数调用。因为MSMQ接口为支持高级应用程序开发环境而设计,大部分方法参量采用BST和 VAR丨ANT的形式。VARIANT经常被自动操作使用,因为它们支持隐式的脚本环境。VARIANT 使用了一个vt字 段,标志存储的类型,还有一个体现所支持的全部变体型类型联合形式的字段。 下面是设置所要求的变体型和创建队列的程序代码•. //Create a default queue VARIANT varTransactional; varT ransactional.vt = VTBOOL; varTransactional.boolVal = FALSE; VARIANT varWorldReadable; varWorldReadable.vt = VT_B00L; varWorldReadable.boolVal = FALSE; pQInfo->put_PathName{L“.\\BasicQueue“); h 「 = pQInfo*>Create( &varTransactional, &varWorldReadable ); 24.4.5 BSTR 成功地创建队列之后,所创建队列的格式名称作为队列消息对象的一个属性存储起来。取 回队列的格式名称需要你使用BSTR。BSTR是Basic(Visual Basic)格式的字 符串,并且经常用 Unicode字符编码字符存储。BSTR是基本的WCHAR指 针 ,带有一个检验符:第一个字符前面的 两 个字节用 来存 储 字 符串的长度。BSTR在 COM+ 中 使 用 的 另 一 个重要 方面是 服 务 器经 常给 BSTR分配内存,并且希望客户机释放内存。需要一个指针指向BSTR的COM+方法通常喜欢回 传一v ^ B STr 缓 冲 区 给 客 户 机 ,放 弃 对 缓 冲 区 的 控 制 。 因 为 BSTR的 自 然 前 缀 ,通过系统 SysAilocString()和SysFreeString()调 用 ,更容易对它们进行分配和解除分配。下面是取回新创 建队列格式名称的程序代码: //Display format name BSTR bstrFormatName; pQInfo•>get_FormatName(&bstrFormatName); wprintf(L“Queue created with format name: %s\n\n#,bstrFormatName); SysFreeString(bstrFormatName);弟24聿 MSMQ稚序设计 445 注意,在前瓸两个程序代码段中,get一和put一方法用于MSMQ COM+对象的属性的操作。这 是初级COM+属性存取器。前面还有一些带有Get和Put前缀的相同封装版本例程,你的灵巧指针 程序例子将会用到它们。确认能够辨別它们两者的区别,因为初级版本返回HRESULT,而封装 版本会出现异常的情况。当然料想不到的异常从不可笑。 不像别的许多较高级COM+程序设计形式,着重指出在你用完接口指针之后你一定要确认释 放它们了。释放IMSMQQueuelnfo接 U 指针,完成CreateQueue()函数: //Release the Queuelnfo object pQInfo->Release(); 2 4 .4 .6 解散队列 解散一个队列几乎跟创建一个队列的过程差不多。你简单地创建一个MSMQQueuelnfo对 象 , 设置目标队列路径名,然后调用MSMQQueueInfo::Delete( >函数。下面是一个例子: //Setup queue info path & destroy queue BSTR bstrPathName; bstrPathName = SysAllocString(L“ .WBasicQueue*); pQInfo->put_PathName(bstrPathName); SysFreeString(bstrPathName); hr = pQInfo*>Delete(); pQInfo*>Release(); 2 4 .4 .7 打开队列 为了发送和接收消息,你需要打开一个目标队列。一个MSMQQueuelnfo对象可以给你提供 获得一个队列的接口指针和路径名的方法。队列需要表现为是一个MSMQQueue对象。在创建一 个MSMQQueuelnfo对 象后 ,你调用IMSMQQueueInfo::Open()函数,如果成功,它在最后的一 个参量中返回队列的接口指针。MSMQ