C# COM+ 编程指南


万水计算机编程技术与应用系列 C# COM+ 编程指南 [美] Derek Beyer 著 龚小平 史艳辉 杜大鹏 管英强 译 杜国梁 审校 中国水利出版社 内 容 提 要 .NET 框架是 Microsoft 公司为适应 Internet 发展与市场形势而提出的开发平台。C#是 Microsoft 公司为.NET 框架量身定做的首选语言。本书向读者概要地介绍了.NET 框架和通 用语言运行库的基本概念,进一步讲解了新的.NET 框架与已有的 COM+ (组件对象模型扩 展)技术的互操作方法,即如何在 COM+中使用.NET 组件以及如何在.NET 框架中使用已有 的 COM+组件;如何用 C#语言创建全新的符合.NET 规范的 COM+组件,这些组件可用于事 物处理、安全、事件、对象共享、事件排队以及远程处理。 为了读者更好地阅读本书,作者在附录中还介绍了 C#言的要点。在本书所附的光盘上包括 了本书中的所有源代码。 本书适合有志于学习 Microsoft 新的.NET 框架平台的开发人员。 Original English language edition Copyright ©by Hungry Minds,Inc. All rights reserved including the right of reproduction in whole or in part in any form. This translation published by arrangement with Hungry Minds,Inc. 北京市版权局著作权合同登记号:图字 01-2001-4560 图书在版编目(CIP)数据 C# COM+ 编程指南 / (美)拜尔(Beyer,D.) 著; 龚小平等译. —— 北京: 中国水利水电出版社,2002 (万水计算机编程技术与应用系列) 书名原文:C# COM+ Programming ISBN 7-5084-1O05-X I.C⋯ II.①拜⋯②龚⋯ III. ①C 语言 - 程序设计 ②因特网 - 程序设计 IV.①TP312 ②TP393.4 中国版本图书馆 CIP 数据核字(2002)第 013334 号 书 名 C# COM+ 编程指南 作 者 [美] Derek Beyer 著 译 者 龚小平 史艳辉 杜大鹏 管英强 译 出版、发行 中国水利水电出版社(北京市三里河路 6 号 100044) 网址:www.waterpub.com.cn E-mail:mohannel@public3.bta.net.cn(万水) sale@waterpub.com.cn 电话:(010)68359286(万水) 63202266(总机) 68331835(发行部) 经 售 全国各地新华书店 排 版 北京万水电子信息有限公司 印 刷 北京蓝空印刷厂 规 格 787 * 1092 毫米 16 开本 15 印张 327 千字 版 次 2002 年 3 月第一版 2002 年 3 月北京第一次印刷 印 数 0001 – 5000 册 定 价 30.00 元(含 1CD) 凡购买我社图书,如有缺页、倒页、脱页的,本社发行部负责调换。 版权所有·侵权必究 译 者 序 自从 2000 年以来,相信许多人都从各种媒体上听说过.NET。Microsoft 为.NET 给出的官方 定义是“.NET 是 Microsoft 用于 XML Web 服务的平台”(.NET is Microsoft’s platform for XML Web services.)。这是 Microsoft 为适应 Intenet 发展和市场竞争需要而提出的产品战略。.NET 框架成为 Microsoft 所有产品共享的平台。Microsoft .NET 平台包括许多建立在 XML 和 Internet 业界标准的产品族,可提供开发、管理、使用和体验 XML Web 服务的各项功能。 XML Web 服务将成为目前已经在使用的 Microsoft 应用程序工具和服务器的一部分,而且将 被引入到新产品中,以便满足所有的业务需求。XML Web 服务允许应用程序通过 Internet 共享数据,而不管使用的是什么操作系统和编程语言。其中共享数据的格式就是 XML(eXtended Markup Language,扩展标记语言)。C#语言是 Microsoft 在 C++ 和 Java 的基 础上为.NET 框架特意开发的完全面向对象的语言,是.NET 平台上的首选开发语言。但本书 并非讲解 C#语言的教科书.只是使用了 C#语言编写书中的所有代码。 本书的书名中还有一个缩略语 COM+,其全称是“组件对象模型”的增强版本。这也是 Microsoft 的一项较新的技术。1997 年 Microsoft 在加利福尼亚的 San Diego 召开的专业开发 人员大会上宣布了 COM+计划。这是一种对组件对象模型(Component Object Model,COM) 的扩展。COM+的目的是使用任何语言或任何工具来创建并使用软件组件。相对于.NET 框 架来说,COM+已经是较为传统的技术。那么在.NET 框架下可以使用以前开发的 COM+组 件吗?答案是肯定的,这也正是本书内容要达到的主要目的。.NET 系列丛书编辑 Michael Lane Thomas 如是说:“如果想要在今天的 COM+服务与未来用 C#语言开发出来的下一代服务之 间自由地‘跳华尔兹’是需要一些高超技术的,而本书可成为您的‘舞蹈教练’。”这句话可 谓一语中的地道出了本书的目的和宗旨。 如果读者有以上知识背景或是想要了解以上知识背景,则本书意义就突现出来。译音们 对以上知识并不很精通,对.NET 和 C#等新知识有极大的好奇心。翻译的过程也就是学习的 过程。错误在所难免,敬请广大读者(特别是行家)提出宝贵意见。 本书是多人努力的成果。龚小平、史艳辉、杜大鹏、管英强参加了翻译工作,其中,龚 小平翻译了前言和第 1 章到第 5 章,史艳辉翻译了第 6 章到第 8 章,杜大鹏翻译了第 9 章到 第 10 章,管英强翻译了附录。全书由杜国梁审校并统稿。参加本书其他工作(录入、打印、 校对等)的人有魏天超、梁国珍、任建畅、马相生、刘发来、董明、迟春和杨天华等。在此 对所有对本书作出贡献的人表示感谢。 译 者 2001 年 11 月 20 日 于防化指挥工程学院 作 者 简 介 Derek Beyer 现在作为 Web 开发专家工作于密歇根州的 Grand Rapids 市的 Meijer Stores 公司。 Derek 经常指导其他开发人员程序设计和开发技巧。他也负责实现并维护核心基础组件,如 Web 和应用程序服务器等。Derek 为公司的开发人员开发并宣讲有关 MTS、COM+、Visual Basic 和 Active Server Pagers 领域的开发指南。 Derek 也在以芝加哥为基地的咨询公司 March First 作顾问工作。他所从事的项目范围涉及从 开发主要基于 Internet 的消费者站点的应用程序到 SAP R/3 应用程序的 Web 集成。Derek 也在 COM+和.Net 主题的用户组会议上演讲。 闲暇时间,Derek 通常在健身房参加体育锻炼,或者是享受诸如狩猎和钓鱼这样的户外活动。 致 谢 真的很感谢为本书辛苦而勤奋工作的审稿和编辑团队。虽然本人的名字出现在封面上,但本 书其实是团队努力的结果。Matt Lusher 和 Eric Newman 担当了本项目的项目编辑并提供了 很大的帮助。Matt 以其专家的身份和丰富的幽默感使紧张的时光变得更轻松。Chris Jones 努力查找出在本人犯困和睡眼朦胧时所犯的语法错误。一个好的组稿编辑室能把全书结合成 一个整体并使每个人都保持快乐,而 Sharon Cox 这方面的能力就很令人惊叹。Sharon 无疑 使本人通常要处理的问题减少了许多,谢谢你,Sharon! 我对 Hungry Minds 的产品部也欠了 许多人情,这些人士都受易于本人的绘图和屏幕抓图中的错误。我还要感谢 Rolf Crozier, 本书早期的组稿编辑。Rolf 向 Hungry Minds 提出了出版本书的主意并使之启动。 在人们所喜爱的领域内遇到的最好的事情就是具有共同观点并能向其学刀的人们。Steve Schofield 是我开始学习新技术时曾经遇到的最热心的人士。他对.NET 的兴趣很具感染力。 Steve 也在本人需要将本书变成现实时向本人提供了与 Hungy Minds 内有关人员的联系方 法。Nick McCollum 是本书的令人敬畏的技术编辑。他使我保持诚实并帮助本人使许多主题 更贴近读者。还要感谢许多关键的 Microsoft 雇员,如 Mike Swanson 和 Shannon Paul。Mike 总是能提供帮助和本人所需要的一些东西。他也以微笑和点头来面对本人对许多技术问题的 抱怨。Shannon 给本人提供了关于 COM+事件的关键信息。他也使作者在进入一个主题时能 顺利地写作下去。谢谢你,Shannon。 本人现在已经明白写书是一项很大的工程。没有人能作出这么大的努力而没有来自家庭或朋 友的支持。本人幸运地有一个很优秀的支持系统。系统的基础是我的父母。父亲向我举例说 明真正的工作道德是什么。父亲是我曾经看到的最刻苦工作的人。很感激这些已经从我脑海 消失的工作道德。 母亲为我提供了无条件的支持和鼓励。必须感谢她能理解在我潜心写作 本书时几个月很难看到我。最后但并非不重要,我必须感谢 Jacque。Jacque 是很特别的朋友, 在编写本书的过程中忍受了本人偏执的冲动。她能够以她的同情和积极的能量在我低沉时使 我振作起来。谢谢。 前 言 欢迎阅读《C# COM+ 编程指南》一书。如果已经购买本书或正打算购买,您可能有许多希 望本书能回答的问题。最常见的问题是“COM+ 消亡了吗?”及“COM+ 在.NET 应用程 序中的角色?”。第一个问题的答案很明确,就是“没有”!Microsoft 包含在 Windows 2000 中的 COM+技术对.NET 程序员仍然可以使用。实际上,一些在早期只有 C++程序员能用的 COM+技术现在对 Visual Basic .NET 和 C#程序员也都可以用。第二个问题总是有点难以回 答。从作者这里得到的典型回答是“视具体情况而定”。COM+中的技术如分布式处理和列 队组件只能在 COM+中找到。需要确定是否应当使用特定 COM+服务时要自问的问题是“我 的应用程序中需要这种服务吗?”,如果答案是肯定的,就可以自由使用 COM+。如果答案是 否定的,COM+就不适合于该应用程序。 本书使用的所有代码都使用了新的编程语言 C#。C#是特意为.NET 开发的面向对象的编程语 言。实际上,.NET 应用程序是用 C#能编写的唯一的—种应用程序。贯穿全书作者都会指出 有助于编写更好的 COM+组件的 C#语言的特征。虽然所有的代码都是在 C#中编写的,但如 果喜欢的话这些示例也可以用 C++编写。 本书的读者对象 COM+不是适合编程新手的主题。如果读者以前从来没有开发过应用程序,本书就可能不太 合适。说到 COM+,话题就总是牵涉到分布式计算。如果已经开发过应用程序,特别是分 布式 Web 应用程序,本书所讨论的主题对读者就很有意义。 如果读者是.NET 编程或 COM+编程的新手也不要害怕。本书的第一部分介绍了.NET 的基础 知识及其与 COM 组件的交互。第一部分提供了理解.NET 应用程序如何工作及如何与传统 的 COM 组件交互所需的环境。作者强烈建议读者在阅读任何其他章节前先阅读第 1 章。第 1 章向读者介绍.NET 环境。如果不理解该环境如何工作,本书的其余部分对读者就没有太 大的意义。 对那些 C#新手来说,附录 C 提供了对该语言的介绍。附录 C 介绍该语言的基本功能如数据 类型、循环、流程控制语句以及在本书其余部分所用到的特定的语言特色。 本书假设读者并不熟悉 COM+编程。每章都介绍每种 COM+服务的基本功能和问题。读者 在用本书学习如何开发 COM+组件时不必是一个有经验的 COM+开发人员。 本 书 内 容 本书分成三个部分。每部分都提供理解下一部分所需的信息。本书的组织提供在.NET 中积 累 COM+编程的技能和理解 COM+编程所需的逻辑发展。 第一部分 与 COM+的互操作 第一部分介绍名为通用语言运行库(Common Language Runtime)的基本的.NET 运行库环境。 因为每个.NET 应用程序都运行于通用语言运行库中,如果要用 C#开发 COM+组件则理解这 种环境是极其重要的。第一部分的内容包括了与 COM 世界进行互操作的方法。说明了如何 从 C#应用程序中使用传统的 COM 组件。也说明了如何编写 COM 客户可使用的 C#组件。 如何开发使用 COM 组件或从 COM 组件中使用的分布式应用程序,理解 COM 与.NET 的互 操作是很重要的。 第二部分 COM+的核心服务 第二部分介绍 COM+的核心服务。所有的核心服务如分布式处理、基于角色的安全性、松 散耦合事件、列队组件及其他都是在第二部分介绍的。这部分的各章顺序是按从较为简单的 服务到较高级的服务(尽可能好地)组织的。 第三部分 高级 COM+计算 本书的最后一部分即第三部分介绍 COM+较高级的一些主题。第三部分介绍.NET 远程处理 框架。.NET 远程处理框架为开发者提供通过网络调用组件方法的途径。正如读者将会看到 的,用 C#写的 COM+组件可通过类层次插入到远程处理框架中。第三部分也讨论了现在的 Windows XP 所拥有的 COM+、Internet 信息服务器(Iternet Information Server)和 Microsoft 消 息队列(Microsoft Message Queue)的新功能(所有这些技术都在本书中使用了)。许多 COM+ 新功能都把重点放在为 COM+组件提供更稳定的环境上。 本书所使用的约定 任何书籍都使用一些约定帮助读者更好地理解原文。本书也不例外。作者在本书中使用了排 版和编码约定以帮助读者更清晰地理解原文。 排版约定 因为这是一本有关编程的书籍,作者介绍了许多代码示例。作者几乎原样复制了每个代码示 例(较长的代码则有自己的清单表号)。解释特定代码示例的段落经常引用示例中的代码。 如果引均示例中的代码,总是使用等宽字体。下面是第 5 章中的一个示例。 using System; using Microsoft.Comservices; [assembly: ApplicationAccessControl{ AccessChecksLevel = AccessCheckLevelOption.ApplicationComponent } ] public class SecuredComponent { // Some method implementations } 注意在属性标签内使用了关键词 assembly。这会告诉 C#编译器该属性是装配级的。在属性 的声明中,通过使用 AccessChecksLevelOption 枚举把 AccessChecksLevel 属性设置成应用程 序和组件。 上面的代码示例(从 using System 开始的行)全部设置成等宽字体。上面一段解释了代码示例。 在这一段中从代码示例个引用了关键词,如 assembly 、 AccessChecksLevel 和 AccessChecksLevelOption。不管在段落中的哪个地方看到等宽字体,都肯定是前面的或将要 出现的代码示例中所用的关键词。 代码书写约定 .NET 框架使用 Pascal 惯例命名其类、方法参数、枚举等。本书所使用的代码示例也遵守这 个惯例。Pascal 惯例在名称中将每个单词的首字母大写。例如,如果编写一个访问顾客订单 信息的类,就可能将它命名为 CustomerOrders。因为使用 Pascal 惯例,必须将 Customer 中 的 C 和 Orders 中的 O 大写。使用这个约定有助于使代码示例更具可读性。 本书使用的图标 本书介绍的许多主题都有相关的主题。很多时候如果要理解所讨论的中心主题则理解这些相 关主题是很重要的。然而,如果离主题太远则很容易失去读者。为了既介绍这些重要信息又 不失去读者,招这些主题放到了“注意”中。例如: “注意”解释相关主题。它们也用来提醒读者 C#有助于编写优秀的 COM+组件的特殊功能。 目 录 第一部分 与 COM 的互操作 第 1 章 理解.NET 结构 第 2 章 从.NET 中使用 COM 组件 第 3 章 从 COM 中使用.NET 组件 第 1 章 理解.NET 结构 本章内容包括: * 在通用语言运行库(Common Language Runtime)内载入和执行代码 * 自动内存管理 * 装配件 * 应用程序域 * 通用类型系统(Common Type System) .NET 框架(.NET Framework)试图解决 Microsoft Windows 环境中许多与应用程序开发和部署 有历史联系的问题。例如使用Visual Studio 6及其早期版本在C++中不能编写类而且在Visual Basic 内不能直接使用类。COM 已经通过允许已编译组件之间经二进制协议进行对话来试图 减轻这种不便。然而,COM 也有其缺陷。COM 没有提供运行时发现组件所提供的服务的 明白易懂的方法。.NET 框架则通过所谓的“映像”(reflection)概念提供了解决这一问题的机 制。错误处理是该框架解决的另一个问题。根据所作出的 API 调用,此 API 调用可能产生 一个错误或返回一个错误码。如果返回错误码,程序员必须具有可能返回的常见错误方面的 知识。该架构通过为所有错误产生一个异常简化了错误处理。Framework 库提供了对传统上 属于 C++程序员领域的低级特性的访问。Windows 服务、COM+ Object Pooling (COM+对象 共享)以及对如 HTTP、SMTP 和 FTP 之类的 Internet 协议的访问现在已处于 Visual Basic .NET 或 C#开发者的牢牢掌握之中。 正如读者所看到的,.NET 框架为运行于其环境内的应用程序提供了许多瞄准执行领域的服 务。所有为.NET 编写的程序(包括用 C#编写的 COM+组件)都运行在称为通用语言运行库 (Common Language Runtime,CLR)的环境内。为运行于 CLR 内编写的应用程序被看作是托管 代码。托管代码可利用 CLR 提供的服务。某些这类服务,如垃圾收集(Garbage Collection), 是 自动提供的。其他服务,如对软件的版本编号,则要求程序员干预。 本章节包括由 CLR 提供的服务。对 CLR 的理解能提供用 C#开发 COM+组件所需的适当基 础。 1.1 在通用语言运行库内载入和执行代码 正如前面所提到的,CLR 提供许多简化开发与部署应用程序的服务。CLR 能提供这些服务 的部分原因是所有应用程序都运行在称为虚拟执行系统(Virtual Execution System,VES)的相 同执行引擎上。实际上,它是编译器支持和某种允许 CLR 提供其服务的规则的运行库执行 的组合。这一节讨论应用程序可用的运行库支持以及提供这些服务所需的编译器和 VES 支 持。在整个这一章里,用术语 class(类)和 dll(动态链接库)来说明这些概念,因为它们直接应 用于 COM+编程模型。这些概念适用于所有类型与文件格式(exe 文件和 dll 文件)。 1.1.1 Microsoft 中间语言和元数据 编译 C#应用程序时,不会得到所期望的典型文件,而是包含描述组件的 Microsoft 中间语言 (Microsoft Intermediate Language,MSIL)代码和元数据的可移植的可执行文件(Portable Executable,PE)。MSIL 是 CLR 解释的指令集。MSIL 告诉 CLR 如何载入与初始化类,如 何调用对象上的方法,以及如何处理逻辑与算法运算。在运行时,CLR 的一个组件,即即 时编译器(Just In Time Compiler,JIT),将 MSIL 指令集转换成操作系统可以运行的代码。 MSIL 指令集对任何硬件或操作系统来说都没有什么特殊之处。Microsoft 已经把基础设置成 允许将 MSIL 代码移植到支持 CLR 的其他平台。虽然 Visual Studio .NET 和 Windows 2000 提供了运行 CLR 的唯一工具和平台组合,但可以想像,CLR 是可以移植到其他平台的。如 果真是这样,MSIL 代码就可直接移植到其他平台。当然,使用如 COM+所提供的特定平台 的服务会使应用程序较难移植到其他平台。 C#代码:真可移植? 如果应用程序使用了 COM+服务或其他只针对对 Microsoft 或另外的软件商的特定的服务, 就有机会出现运行服务在其他平台上不可用的情况。另一方面,如果应有程序使用了 System.Net.Sockets 名称空间提供的作 TCP/IP 所支持的服务。应用程序就可能相对来说用于 移植。TCP/IP 是一种大多数平台可能支持的支持度和通用性较好的服务。只要这种支持在 平台之间没有很大的互用。这类代码就有很高的可移植性。这里需明白的一点是 MSIL 和 CLR 为不同的软件商提供了一致设置的标准。虽然为 CLR 编写真正可移植的代码目前不是 事实,但不久就会实现。 正如前面所提到的,在 dll(Dynamic Link Library,动态链接库)和 MSIL 中都存在元数据。元 数据在整个 CLR 中广泛应用,如果要理解.NET 框架是如何操作的,它就是一个要掌握的 重要概念。元数据提供 CLR 进行注册(进入 COM+目录)、调试、内存管理和安全所需的关 于应用程序的信息。对 COM+组件来说,元数据告诉 CLR 和 COM+运行库诸如类应当使用 的事物处理级别和共享组件的最大和最小共享池容量方面的信息,提到只是一小部分。在注 册时查询元数据来为 COM+ Catalog(COM+目录)中的类设置适当的属性。当为类编写代码 时,可使用称为属性的编码构造来操作元数据。属性是.NET 框架内操作元数据的主要方法。 元数据提供了一种将应用程序内的所有信息保存于中心位置中的方法。用早期版本的 Visual Studio 编写 COM+应用程序的开发者将应用程序的信息保存在许多不同的位置。组件类型库 保存组件及其方法和接口的信息。Windows 注册表和 COM+ Catalog 保存 dll 位置和 COM+ 运行库必须如何载入以及激活组件的信息。另外,其他文件可能用来保存组件在运行库中所 需的信息。这种信息的错位会导致开发者和管理员的混乱。Visual Studio .NET 试图通过使 用元数据描述应用程序的所有从属性来解决这个问题。 元数据超越了代码中对属性的描述。编译器用元数据在 dll 内部构建表明类在 dll 内的位置 以及类所支持的方法、事件、域和属性的表。在运行中,Class Loader (类装载器)和 JIT 查询 这些表来载入和执行类。 1.1.2 类装载器 一旦把代码编写并编译完毕,就需要运行它,是不是? 当然是这样。然而,在 CLR 能够运 行类之前,类必须载入和初始化。这就是类装载器的工作。当应用程序 A 试图创建类 C 的 新实例时,类装载器将它已经知道的关于 A 的信息与从管理上定义的 XML 配置文件中获得 的信息相结合并决定 C 载入的物理位置(载入特定类型的过程在本章后面“装配件”一节会 进行更详细的介绍)。类装载器一旦发现该类,就把 dll 文件载入内存,并向 dll 的元数据表 查询类的偏移量。偏移是类装载器可以找到的类在 dll 中的地址。类装载器也查询元数据以 决定类在内存中如何排列。通常,允许类装载器在内存中任何它认为合适的方式来构建类, 但有时也需要编译器告诉类装载器怎样在内存中构建类。有三个选项可告诉类装载器如何排 列类: * autolayout 是缺省的设置且允许类装载器以任何类装载器能接受的方式将类载入内存。 * layoutsequential 强制装载器用其域以编译器发出的顺序排列类。 * explicitlayout 让编译器直接控制怎样在内存中构建类。 需要强调的是编译器负责生成正确的 MSIL 代码以指示类装载器应当如何在内存中排列类。 Microsoft 在 Tool Developer Guide(工具开发者指南)中提供了怎样指示类装载器排列类的文 档。Tool Developers Guide 是 Visual Studio .NET 产品文档的一部分。作为 COM+开发人员不 必为指定类的排列方案而担心。 类装载器担当了所载入类的快速验证和调用者的角色。类装载器检查类是否引用了其他还没 有载入的类。如果确实引用了,类装载器也载入该类,或者如果没有引用,则记录该事实以 备后用。类装载器也强制可访问性规则。例如,如果正在载入的类是另一个类继承来的,类 装载器会确保子类没有试图从封装类继承或扩充基类最终认可的方法。任何由已载入到新建 类的类作出的引用都会被验证。相反地,任何由新类作出的对已载入类的引用也都会被验证。 一旦类已经找出且验证为可安全地执行,类装载器就给每个已经为类载入的方法创建一个承 接体。此承接体在类的使用者和所调用的方法之间起中介作用。承接体负责激活 JIT。 1.1.3 即时编译器 即时编译器(Just In Time Compiler)负责将 MSIL 指令转换成本地机器码。它只在第一次调用 对象的方法时执行该任务。一旦激活,JIT 就在内存中保留转换后的 MSIL。随后对方法的 调用会直接进入本地机器码。 JIT 负责进行比类装载器所进行的更彻底得多的验证过程。JIT 的验证过程保证对类只进行 合法的操作。它也保证被引用的类型与被访问的类型是兼容的。例如,如果类 A 引用了类 Cfoo 的一个实例并调用 Cfoo 的一个方法,ToString(),JITer 保证对 Cfoo.ToString()的调用是 为 CFoo 的一个实例调用的。JIT 编译器此时也检查内存访问。JIT 不允许类引用不允许该类 访问的内存。此时也在多种级别上检查安全访问权限。 JIT 基于不是所有的应用程序代码总是都会被执行的观念来进行操作。不是将整个 MSIL 文 件都转换成本地码而浪费 CPU 时间和内存,JIT 只转换应用程序在任一给定时刻所需要的代 码。这是改善为.NET 框架所编写的应用程序的性能和可伸缩性的一种关键策略。 1.1.4 自动内存管理 分配和释放内存的任务在许多应用程序中常常是错误的一个来源,特别是那些在 C++中编写 的程序,在 C++中编写程序不同于在如 Visual Basic 之类的语言中,更多的是一种手工过程。 CLR 通过在一个托管堆中分配和释放内存来解决这个问题。 CLR 启动应用程序时创建和初始化托管堆。另外,CLR 初始化指向堆的基地址的堆指针。 堆指针包含下一个可用内存块的地址。图 1—1 展示了初始化后且在创建任何对象之前的托 管堆。 当在 C#中用关键词 new 创建对象时,CIR 从堆分配内存并将堆指针增加使之指向下一个可 用的内存块。图 1-2 显示了应用程序中首次调用 new 后的堆。 CLR 从托管堆分配内存可以比从传统非托管的 Winn32 堆分配内存快得多。在典型非托管的 Win32 堆中,内存的分配不是顺序的。当从 Win32 堆分配内存时,堆必须检查以寻找满足 请示的内存块。一旦找到内存块,堆维护的数据结构就必须更新。另一方面,托管堆则只需 增加堆指针。 在某些时候,堆指针增加到了堆的顶部,不再有内存可供分配。出现这种情况时,一种被称 为垃圾收集(Garbage Collection)的过程开始释放不再使用的资源。垃圾收集(Garbage Collection)以构建应用程序正在位用的所有对象的列表为开始。垃圾收集器查找的开始之处 是应用程序的根,其中包括: * 全局对象引用 * 静态对象引用 * 局部变星(对当前正在执行的方法而言) * 参数(对当前正在执行的方法而言) * 包含对象引用的 CPU 寄存器 应用程序根的整个列表由允许垃圾收集器向运行库查询的 JIT 编译器加以维护。一旦识别出 根的整个列表,垃圾收集器就访问每个根的每一个对象引用。如果根包含对其他对象的引用, 也会将这些引用加入到列表中。一旦垃圾收集器已经访问整个对象引用链,就检查堆栈以查 找末在列表中的任何引用。不在列表中的引用被视为不能访问并可以释放。释放不能访问对 象所占的内存后,垃圾收集器就压缩堆并将堆指针设为堆的下一个可用块。 似乎内存分配节省的任何时间现在都消耗在垃圾收集过程了。这还不是事情的全部。垃圾收 集器使用一种称为通用垃圾收集(Generational Garbage Collection)的技术优化垃圾收集过程。 通用垃圾收集假定关于应用应用程序的以下情况为真: * 新对象有比旧对象短的寿命 * 新对象的内存可以比旧对象的内存释放得更快 * 新对象相互之间有密切的关系 * 所有对象都可以在大致相同的时间内访问 * 压缩部分堆比压缩整个堆快 基于这些假设,垃圾收集器把堆逻辑地分成三代:0 代(Generation 0)、1 代(Generation 1)和 2 代(Generation 2)。0 代对象是新创建的还没有经过 1 个垃圾收集循环的对象。1 代对象是经 过一个垃圾收集循环后保存下来的对象。2 代中的对象已经经过了至少 2 个垃圾收集循环, 被视为最老的对象。垃圾收集过程发生时,垃圾收集器首先查看 0 代对象以查找任何可以清 除的无用存储单元。如果垃圾收集器能够从对 0 代对象的收集过程中重新获得足够的空间, 就不会向较老的对象收集。当需要重新获得足够的内存以响应请求时,垃圾收集器会工作到 0 代、1 代和 2 代。垃圾收集器也会只通过唯的一个子段来执行垃圾收集过程。这大大提高 了垃圾收集器的性能。 .NET 中的垃圾收集功能引起了很多争议。这种争议起源于程序员不清楚他的对象何时销毁 的事实。这称为“非确定性结束” (nondeterministic finalization)。非确定性的结束对“把 持着”指向文件或数据库连接的句柄之类昂贵资源的对象来说可能是特别的问题。在对象被 垃圾收集器销毁之前当对象等待释放其资源时该问题就会出现。 在传统的应用程序中,这不成为问题,因为当客户释放其对对象的引用时会调用对象破坏器 (或 Visual Basic 中的 Class_Terminate)。在这种情况下,当客户使用完对象之后,对象有机 会立即释放其资源。而在.NET 中,对象没有破坏器或 Class_Terminate 事件。如果在 C#中 编写应用程序,与 Visual Basic 的 Class_Terminate 事件最接近的是名为 Finalize 的方法。问 题在于是垃圾收集器调用 Finalize 而不是程序员。当客户释放其对象的引用时,就没有必须 调用方法了。如果在 Finalize 方法中关闭了资源,诸如数据库连接以及文件锁定一类的资源 在对象中仍会保持打开状态。Microsoft 对这类对象的处理方法是建议程序员实现 Dispose 或是 Close 方法。恰好在使用完对象之前,客户可明确地调用这些方法,以便释放资源。 在继续往下之前,先讨论一下 Finalize 方法的目标是什么以及它的使用价值是什么。首先, 正如前面所提到的,Finalize 方法由 Garbage Collector 调用,而不是对象的使用者。Finalize 方法不必由使用对象的应用程序调用。实际上,C#编译器不会编译没有完成公共终止器的 类。终止器必须声明为受保护型以便只有从对象继承的类可以调用 Finalize 方法。完成 Finalize 方法要记住的关键点如下: * 只在需要时完成该方法。执行成功与该方法的完成有关(详见下一段)。 * 只释放对象拥有的引用。不创建新引用。 * 如果从其他类继承,则通过 base.Finalize()调用基类的 Finalize 方法—假定它有 Finalize 方法。 * 只把 Finalize 方法声明成受保护的。现在,这是 C#编译器允许的惟一访问属性。 第一个项目符号的内容引出了重要的论点。当对象是用关键词 new 创建时,CLR 会注意到 对象已经实现 Finalize 方法。这些类型的对象被记录到称为结束队列(Finalizeation Queue)的 内部垃圾收集器队列。请记住当垃圾收集循环出现时,垃圾收集器会进入托管堆内寻找不能 访问的对象。如果垃圾收集在已经实现 Finalize 方法的堆找到不能访问的对象,就从结束队 列删除对象的引用并把它放到称为 Freachable Queue 的另一个队列。该队列的对象被视为是 可访问的并且不会被垃圾收集器释放。当对象被置于 Freachable Queue 时,唤醒另一线程以 调用每个对象的 Finalize 方法。垃圾收集器下一次运行时,就会看到达些对象不再是可访问 的并且把它们从堆释放。所有这一切的结果就是需要两次垃圾收集循环才能把带有 Finalize 方法的对象从堆释放。 正如读者可以看到的,CLR 为此在后台作了许多工作。这有时可能是好事也可能是坏事。 它可以提高执行效率因为跟踪内存泄露和错误的工作大大简化了,另一方面,这种类型的黑 相机制可能使监测应用程序正在真正做什么变得困难。幸运的是,其 SDK 带有几个可用来 帮助监控应用程序性能的性能计数器。与所讨论的问题有关的一些计数器是 JIT 编译计数器 (JIT Compilation Counters)、装载计数器(Loading Counters)和内存计数器(Memory Counters)。 这些计数器概要介绍如下: * JIT Compilation Counters(JIT 编译计数器) · IL Bytes Jitted/sec: 每秒钟将 IL 代码转换成本地代码的字节数 · # of IL Bytes Jitted: 从应用程序启动开始已经被 JIT 处理的 IL 字节数 · # of Methods Jitted: 从应用程序启动开始已经被 JIT 处理的方法数 * Loading Counters(装载计数器) · Current Classes Loaded:当前载入 CLR 的类的数目 · Total # of Failures: 从应用程序启动开始已经装载失败的类的总数目 · Total Classes Loaded:从应用程序启动开始已经载入的类的总数目 * Memory Counters(内存计数器) · #Bytes in Heap: 在托管堆内的总字节数。这包括所有的代。 · Gen 0 Heap Size:0 代堆栈的大小。也为 1 代和 2 代提供了类似的计数器 · #Gen 0 Collections:在 0 代收集到的数量。也为 1 代和 2 代提供了类似的计数器 1.2 装配件 装配件是 CLR 实现版本编号的地方。装配件也是名称解析的地方。装配件可以看成是包含 类型实现(如类和接口)、对其他装配件的引用和如 JPEG 之类的资源文件的逻辑 dll 文件。在 装配件的内部及其自身并不是应用程序。应用程序引用装配件以访问装配件的类型和资源。 可把.NET 应用程序想像为由一到多个装配件组成。对装配件的引用可在编译时或运行时作 出。通常引用是在编译时作出的。这类似于 Visual Basic 工程中设置对 COM 库的引用。这 些引用包含在一个称为装配件清单的装配件段中。 1.2.1 装配件清单 装配件清单包括 CLR 载入装配件及访问其类型所需的信息。具体来说,装配件清单包括了 下列信息: * 装配件的名称 * 装配件的版本(包括主版本和次版本号以及构建号与修正号) * 装配件的共享名称 * 装配件所支持的环境类型的信息,如操作系统和语言 * 装配件中所有文件的列表 * 允许 CLR 对包含类型的实现的文件和应用程序的类型引用进行匹配酌信息 * 该装配件所引用的所有其他装配件的列表。这包括被引用的装配件的版本号 通常,装配件清单存储在包含装配件的最常用的受访问类型的文件中。不太常用的受访问类 型存储在称为模块的文件中。这种方案对基于浏览器的应用程序来说工作得特别好,因为不 必一次就下载整个装配件。运货单确定需要时可以下载的模块。 因 1-3 所示为同时包含装配件的清单和在文件内实现的类型的文件的逻辑表示。 1.2.2 版本信息 如前面所描述的,装配件的清单包含装配件的版本。版本由 4 部分组成:主版本、次版本、 构建号、修正号。例如,.NET SDK Beta 2中System.Windows.Forms装配件的版本是1.02411.0, 其中 1 是主版本号,0 是次版本号,2411 是构建号,0 是修正号。CLR 将主版本和次版本号 与应用程序所请求的进行比较。如果主版本和次版本号与应用程序所请求的不匹配,CLR 就认为装配件不兼容。缺省情况下,CLR 以最高的构建和修正号鼓入装配件。这种行为被 称为快速修正工程(Quick Fix Engineering,QFE)。QFE 的目的是允许开发者部署对应用程序 的修正或补丁如修正安全漏洞。这些更改不应破坏对使用装配件的应用程序的兼容性。 1.2.3 共享名称 除版本号外,装配件清单还包括装配件的名称,这只是一个描述装配件的简单字符串以及一 个可选的共享名称(也称为“强”名称)。共享名称用于需要在多个应用程序之间共享的装配 件。共享名称是使出标准的公共密钥加密法生成的。具体地说,共享名称是开发者私人密钥 与装配件名称的组合。共享名称是在开发时用.NET SDK 或 Visual Studio .NET 开发环境中提 供的工具嵌入装配件清单中的。CLR 用共享名称确保应用程序引用的装配件确实是要访问 的装配件。 1.2.4 全局装配件缓存 既然已经有了唯一识别多个应用程序可以使用的装配件的机制,就需要一个存储这些装配件 的地方。全局装配件缓存(Global Assembly Cache)就是存储所有可以在应用程序之间共享的 装配件的逻辑文件夹。说它是逻辑文件夹是因为装配件本身可以存储在文件系统的任何地 方。装配件在部署时使用以下方法之一放在全局装配缓存中:了解装配缓存的安装器、 在.NET Framework SDK 中找到的全局装配件缓存工具 gacutil.exe(Global Assembly Cache Utilty),或通过将带有装配件清单的文件拖到\winnt\assembly 文件夹。\winnt\assembly 文件 夹是用 Windows Shell 扩展实现的,所以它可以从 Windows 资源管理器(Windows Explorer) 或我的电脑(My Computer)中查看。图 1-4 展示了从“我的电脑”中查看时全局装配缓存的 外观。 Global Assembly Cache 存储装配件的基本信息,包括装配件名称、版本、最近修改的日期、 用于签署装配件的公共密钥、含清单的文件在文件系统中的位置。将装配件加入 Global Assembly Cache 中有几个好处: * Global Assembly Cache 允许在应用程序之间共享装配件 * 应用程序可以获得几个方面的性能改善 * 对装配件引用的所有文件进行完整性检查 * 可在 Global Assembly Cache 中存储装配件的多个版本;如果存在多个版本,会自动应 用 QFE 全局装配缓存从两个方面提高装配件的性能。首先,全局装配缓存中的装配件不需要在每次 访问时都进行验证。如果还记得以前所讨论过的,当装配件被引用时 CLR 确保应用程序所 引用的装配件是要访问的那个。如果装配件位于全局装配缓存之中,会跳过验证过程。第二, 全局装配缓存中的装配件只需载入 CLR 中一次。多个应用程序访问装配件的单个实例。这 减少了全局装配缓存中装配件的载入时间。另外,由于所有的应用程序都在访问同一个实例, 被调用的方法有更大的机会已经被 JIT 编译。当然,使用全局装配缓存也有不利的一面。如 果大多数装配件都是与应用程序有关的(也就是,只有一个应用程序使用它们),通过将这些 类型的装配件安装到全局装配缓存中就会引入额外的管理步骤。 1.2.5 定位装配件 CLR 在能访问装配件中的任何类型之前,必须定位装配件。这是一个从应用程序的配置文 件开始的多步过程。应用程序的配置文件是 XML 格式的。除了用.cfg 的扩展名外它被赋予 跟应用程序相同的名称。配置文件如果存在的话,就在与应用程序相同的文件夹内。例如, 如果应用程序是 c:\program files\Microsoft office\Word.exe,应用程序配置文件就是 c:\program files\Microsoft office\Word.exe.cfg。配置文件在 CLR 试图定位装配件时提供几种信息。 * 要使用的装配件的版本而不是所请求的版本 * 是否强制 QFE * 应用程序是否应当使用所编译的确切版本(称为安全模式) * 可找到被引用装配件的精确位置(称为代码基) 下面的例子展示了名为 BindingPolicy 的配置文件中的一段。 BindingPolicy 段告诉运行库用装配件的哪个版本取代另一个版本。在这个例子中,告诉 CLR 用名为 myAssembly 的装配件的 2.1.0.0 版取代 1.0.0.0 版。注意主版本号和次版本号是不同 的。这改写了 CLR 的缺省行为,它通常不允许用不同的主或次版本号载入装配件。感兴趣 的其他标记是 UseLatestBuildRevision。该标记允许打开或关闭 CLR 的 QFE 策略。在该例子 中,已经把这个标记设为“no”这就通知 CLR 不使用有较大构建号或修正号的装配件。如 果忽略该标记或将其设为“yes”,CLR 就载入最大构建号和(或)修正号的装配件。最后, Originator 标记表示装配件创建者的公共密钥已经用于签署装配件。 配置文件的安全模式段告诉 CLR 是否应该只使用编译应用程序的装配件。如果启用安全模 式,CLR 只载入应用程序已经直接引用的装配件。安全模式用来将应用程序恢复到只能引 用原来编译应用程序的装配件的状态。下面的例子展示了如何启用应用程序的安全模式。 注意到安全模式是对整个应用程序启用的这一点很重要。一旦将安全模式设成“safe”,它 就适用于所有装配件。缺省情况下,安全模式是关闭的,然而,也可以通过将 Mode 属性设 成“normal”而明确地关闭。 除了改写版本规则之外,配置文件可以精确指定哪里可以找到装配件。Assemblies 集合通过 CodeBase 属性为每个应用程序的装配件指定位置。 在本例中,告诉 CLR myAssembly 的 2.1.0.0 版可以在 c:\winnt\myNewDll.dll 中找到。如果指 定了代码基而装配件没有找到,CLR 会产生一个异常。 但如果没有指定代码基会发生什么呢?此时,CLR 会启动一个称为探测的过程。当 CLR 探 测装配件时,它按下面的顺序在特定的一套路径中查找: 1. 应用程序的当前目录。引用装配件时,CLR 添加.mcl、.dll 和.exe 的文件扩展名。 2. 在应用程序配置文件中指定的任意 PrivatePaths。装配件酌名称也添加到该路径上。 3. 与任何语言有关的子目录 4. 全局装配缓存 第四步比较有趣。如果配置文件不包含 Originator 属性,就会停止探测,并产生一个异常。 然而,如果已经指定 Originator 并启用了 QFE,则 CLR 会从全局装配缓存中查找具最高构 建号和修正号的装配件。 现在来看一个探测的例子。假设应用程序中下列条件为真: * 应用程序的名称是 myapp.exe * 应用程序所在的目录是 c:\program files\myapp\ * 配置文件把 PrivatePaths 指定为 * 应用程序所引用的 myassembly 位于全局装配缓存之中且有相同的主版本和次版本号, 配置文件中包含 Originator 入口 * QFE 已启用 当前面的条件定义好之后,探测路径如下: 1. C:\program files\myapp\myassembly.mcl 2. C:\program files\myapp\myassembly.dll 3. C:\program files\myapp\myassembly.exe 4. C:\program files\myapp\myassembly\myassembly.mcl 5. C:\program files\myapp\myassembly\myassembly.dll 6. C:\program files\myapp\myassembly\myassembly.exe 7. C:\program files\myapp\complus\myassembly.mcl 8. C:\program files\myapp\complus\myassembly.dll 9. C:\program files\myapp\complus\myassembly.exe 10. Global Assembly Cache 1.3 应用程序域 就像装配件可以看成是“逻辑 dll”一样,应用程序域可以看成是“逻辑.exe”。在 Windows 平台上,Win32 进程为运行在系统上的应用程序提供隔离。这种隔离为应用程序提供许多服 务: * 一个应用程序中的错误不能损害其他应用程序 * 运行于一个应用程序中的代码不能直接访问另一应用程序的内存空间 * Win32 过程可以中断、开始和调试 CLR 能够通过其类型安全和严格的代码校验过程为应用程序域提供这些服务。CLR 能够比 传统的 Win32 应用程序以低得多的代价提供这些功能,因为它能在单个 Win 进程中为多个 应用程序域提供宿主,这减少了运行应用程序所需的 Win32 进程的数目。CLR 通过减少操 作系统必须在物理过程中切换上下文的次数从而提高了性能。另外,当应用程序域中的代码 访问另一个应用程序域中的类型时,可能只需出现一次线程切换,这与过程上下文切换形成 了对照,后者的代价比前者要大得多。 1.4 通用类型系统 通用类型系统(Common Type System)定义类型之间如何相互作用以及它们是如何用元数据 表示的规则。这些规则有助于解决与编程语言之间的复用有关的常规问题。在.NET 框架中, 几乎每个实体都可看作是类型。类型可以是类、接口、结构、枚举,甚至是如整数和字符之 类的基本数据类型。所有的类型都可以分成两个类别:引用类型和值类型。 在.NET 中,引用类型包括类、接口、指针和代表(类似于 C++中的函数指针)。本质上任何 引用类型都由三部分组成:代表类型当前值的位序列、位开始的内存地址、描述允许在类型 上所进行的操作的信息。因为这种描述相当含糊,所以就要使这些概念更具体。正如读者从 对垃圾收集的讨论所知道的,当使用 new 关键词时,CLR 为这些类分配一块内存并返回应 用程序的内存地址。这就是类的位开始的地址。“位序列”是任何域、属性、方法的参数、 方法的返回值等在任意时刻的值。引用类型的最后一个部分即“描述”,是描述公共和私有 域、属性和方法的元数据。引用类型有三种形式:对象类型、接口类型和指针类型。根据本 书的目的,可把对象类型想像为 C++中的类,接口类型想像为 C++中的接口,指针类型想 保为 C++中的指针(当然,C#也支持指针)。引用类型总是通过引用传递,而值类型总是通过 值传递。例如,当引用类型作为参数传给方法时,就把其内存地址传给方法。当值传递给方 法时,传递的是值的位的副本。 值类型可以构建为如整数之类的数据类型,或者也可以是如枚举或结构之类的用户定义类 型。值类型是代表类型在任何时刻表的位序列。与从托管堆创建的引用类型不同,值类型是 从线程堆栈创建的。值类型总是被 CLR 初拾化为 0。 每个值类型都有称为转向类型(boxed tyPe)的相应的引用类型。要访问值的转向类型,必须 把值强制转化为对象类型。看看下面的代码示例: int iNum = 1; Object oObj = iNum; oObj = 2; WriteLine(iNum); WriteLine(oObj); /* OUTPUT */ 1 2 这个例子中,声明了一个整数值类型 iNum 并设置成 1。然后声明了一个 Object 引用类型 oObj 并设置成 iNum。此时执行了隐式的从 iNum 到 oObj 的强制转化。引用类型可以转换成值类 型。但仅当所说的引用类型有相应的值类型时才是可能的, 1.5 小结 正如读者可以看到的,CLR 提供了许多功能和服务。其中的许多(如果不是全部的话)对那些 具有 Visual Basic 或 Visual C++背景的人来说完全是新的。对 CLR 是如何工作的越明白,就 越能处理所出现的问题,而且也越能计划好组件的执行。读者继续学习本书的余下部分时, 请记住下列关于 CLR 的关键点: * 元数据用于向 CLR 和 COM+运行库描述 COM+组件的属性—如事务级的。 * 全局装配缓存可用于存储由多个应用程序所使用的装配件。大多数使用 COM+服务的 C#组件由多个应用程序使用是可能的。 * 对于驻留于全局装配缓存内的装配件共享名称是必须的。 * 垃圾收集器可以大大简化组件的开发,但它也使访问如数据库连接之类的紧缺资源的组 件复杂化。 * 装配版本的编号规则可由应用程序的配置文件来改写。在下一章里,将讨论 COM 如何与.NET CLR 交互以及 CLR 如何与 COM 交互。 第 2 章 从.NET 中使用 COM 组件 本章内容包括: * 将类型库转换成.NET 名称空间 * 运行库可调用的封装器(Runtime Callable Wrapper) * .NET 和 COM 之间的线程处理与性能问题 正如读者可以从第 1 章所看到的,.NET 框架在开发应用程序时为开发者提供了许多可以考 虑的新功能。所有这些功能的结合要求对用 COM 开发应用程序的老思维方式进行调整。 然而,还不能扔掉现在所知道的关于 COM 的每一样东西。认为公司开始用.NET 框架开发 应用程序时,传统的 COM 会正好消失是不合逻辑的。实际上,在相当长的时期内,.NET 应用程序需要与 COM 交互而且反之亦然。 今天,许多大的电子商务、企业内部网和其他类型的应用程序都是用以 COM 为重要杠杆的 Microsoft 工具组构建的。因为时间总是很宝贵的,在一夜之间将整个应用程序都转换成.NET 是不可能的。另外,面对第三方的应用程序可能不得不继续使用 COM API。 所以如果想使用.NET 升级传统的 COM 应用程序组件或添加新功能又该怎么做呢?答案就 在.NET 的 COM 互操作(COM Interoperation,COM Interop)功能之中。COM Interop 规范允 许.NET 组件使用 COM 对象,反之亦然。 2.1 将类型库转换成.NET 名称空间 如果记得第一章的内容,就能想起.NET 组件是通过装配件与其他.NET 组件对话的。对.NET 组件与 COM 组件的对话来说,装配件封装器必须由 COM 类型库(TypeLib)产生。该框架提 供了一种称为类型库导入器(Type Library Importer) (tlbimp.exe)的工具来作这些工作。类型库 导入器实用工具以 COM 类型库作为输入并产生一个装配件作为输出。该工具产生的装配件 包含能调用 COM 组件方法和属性的承接体代码。这种承接体代码是将在下一节讨论的运行 库可调用封装的实际实现。 现在通过用 Visual Basic 6 创建一个简单的 HelloWorld COM 组件来看看 tlbimp.exe 是如何工 作的。HelloWorld 组件包括一个函数 Hello,它向调用者返回一个字符串。该函数的实现如 下所示: ‘ VB project name: prjHelloWorld ‘ class name: CHelloWorld Public Function Hello() as string Hello = “Hello World” End Function 假定工程名是 prjHelloWorld 而类名是 CHelloWorld,把该组件的 COM ProgID 设置成 prjHelloWorld.ChelloWorld。COM 服务器被编译成 prjHelloWorld.dll。 从类型库得到的相应信息描述如下: Library prjHelloWorld { Interface _CHelloWorld { HRESULT Hello([out, retval] BSTR* ); } coclass CHelloWorld { [default] interface _CHelloWorld; } 请注意 Visual Basic 工程名已经转换成类型库名。而且,Visual Basic 已经通过向类名添加下 划线创建了缺省接口。 一旦有了类型库,就作好了使用类型库导入器的准备。类型库导入器是一种需要许多参数来 构建装配件的命令行实用工具。此时,所有要作的工作就是生成一个简单、私有的装配件。 要做到这点,需转到命令提示符下,并且转到包括所创建的 dll 的目录下。下列命令生成称 为 AsmHelloWorld.dll 的转配件: tlbimp.exe /out:AsmHelloWorld.dll prjHelloWorld.dll /out:开关提供装配件文件的名称,HelloWorld.dll 提供含有类型库的文件名称。这样一来现 在就有了含有前面提到的承接体代码的装配件。但这种承接体代码是什么样子呢?可以试用 一下框架的 MSIL Disassembler(MSIL 分解器,ildasm.exe)看看装配件的内部。MSIL 分解器 (MSIL Disassembler)是读取装配件的元数据以查看装配件中提供的类型的另一种实用工具。 要用 MSIL 分解器查看装配件,请在提示符下输入下列命令: Ildasm.exe AsmHelloWorld.dll 运行该命令后,可以看到如图 2-1 所示的 MSIL 分解器窗口。 MSIL 分解器提供了相当多的信息。现在,所关心的只是三项内容: * .NET 名称空间 * 从 COM 得到的缺省接口 * COM 伴生类(COM CoClass) - CHelloWorld 在窗口的顶部,也就是关键词标签“MAINIFEST”的下面,可以看到名称空间 prjHelloWorld。 类型库导入器把 COM 类型库名映射成了装配件的名称空间名。在 prjHelloWorld 名称空间 的下面,可以看到 CHelloWorld 的 COM 缺省接口是作为.NET 接口实现的。而且从 COM 来 的类型名直接带到了.NET。在接口定义的下面,可以看到.NET 类的定义。在类的定义中.可 以找到通知.NET 运行库该类实现以前定义的_CHelloWorId 接口的“implements prjHelloWorld._CHelloWorld,”的那一行。最后是 Hello 方法的定义。正如可以看到的,该函 数不带参数并见返回一个字符串。方法的特征表示与在类型库中的定义有细微的变化。请注 意不存在 HRESULT。在本章的后面会讨论原因。还有,从类型库来的[out,retval]参数已经 被删掉了。 如果.NET 应用程序在生成装配件的同一个目录里,前述的示例就可以运行。但如果想使用 从多个.NET 应用程序来的新装配件该如何做呢?如果还记得第 1 章话,就明白装配件要在多 个.NET 应用程序之间共享必须放入全局装配缓存里: Sn –k AsmHelloWorld.snk k 参数通知共享名称实用工具(Shared Name Utility)生成 keyfile 文件。参数后的文件 名标识该实用工具将创建的新文件的文件名。在这种情况下,可把公共/私有密钥 对存储在名为 AsmHelloWorld.snk 的文件里。 一旦创建了 keyfile 文件,就可以用/keyfile 开关运行类型库导入器: Tlbimp /out:AsmHelloWorld.dll /keyfile:AsmHelloWorld.snk prjHelloWorld.dll 此时,就可以把装配件添加到全局装配缓存并从任何.NET 应用程序中应用它。 COM 对象的属性可以从其类型库个导入到装配件中。如果 CDog 伴生类实现名为 Breed 的 属性,类型库就包含了用户可以设置和获取 Breed 值的属性域。 Library Animals { interface IDog { [propput] HRESULT Breed ([in] BSTR); [propget] HRESULT Breed ([out, retval] BSTR*); } coclass CDog { [default] interface IDog; } } 当类型库导入器转换前面所示的类型库时,将为每个属性创建存取器方法。从逻辑上来说, 类是以下面的方式实现的。 Namespace Animals { Class CDog { // private get accessor method called by runtime private string get_Breed() { ... }; // private set accessor method called by runtime private void set_Breed(string) { ... }; // public class field that you will Interop with public string Breed; // public class property } } 如果开发人员正在使用 Visual Stuid0 .NET 并且在代码中引用了 Animals 名称空间,就只能 调用公共属性 Breed 并且不能提供 get_Breed()和 set_Breed()方法。根据代码行中 Breed 用在 何处(也就是说,是在等号的左边还是右边),运行库会调用适当的存取器方法。 存取器是允许截获域的赋值和提取的域。存取器类似于读者可能已经在用 Visual Basic 5 和 6 开发 COM 组件时使用过的 Let/Set/Get 属性例程。本书末尾的附录 C,“C#语言简介”,深 入 解释了存取器。 转换类型定义、枚举和模块 前述的例子意欲说明类型库的通用元素是如何映射到.NET 名称空间的。然而,类型库不是 只包含类和接口。它们也可能含有枚举(enums)、类型定义(typedefs)、模块级的常量和方法(此 外还有其他的内容)。 在 COM 中使用类型定义就像在如 Visual Basic 和 C++之类的编程语言中使用它们一样。类 型库导入器并不直接把 COM typedefs 导入装配件中。请看下面的 typedef: Library Animals { Typedef [public] int AGE; Interface _CDog { HRESULT GetAgeInDogYears( [in] AGE humanyears, [out, retval] AGE dogyears ); } } 因为类型库导入器不能直接导入 typedef,它产生如下的接口: Namespace Animals { Interface _CDog { Int GetAgeInDogYears([ComAliasName(AGE)] int humanyears); } } 请注意类型定义已经转换成它的基本类型:int。导入器也将 ComAliasName 属性添加到方法 的特征表示中。ComAliasName 属性可以通过熟知的映像技术访问。映像允许以决定类型的 接口、方法、构造器和其他类似细节为目的检验类型的元数据。如果感兴趣的话,Visual Studio .NET 文档在关于映像的主题中包含了更多的信息。 将类型库中的 enum 转换成装配件是相当直观的。考虑下面的在 COM 中定义的 enum。 Enum { GoldenRetriever = 0, Labrador = 1, ChowChow = 2 } Breeds; 类型库导入器以相同的名称和域将这个 enum 转换成托管 enum。托管 enum 可从托管名称空 间直接访问。 除 typedefs 和 enums 外,类型库还可定义模块级的方法和常量。当模块从类型库转换成装配 件时,将携带模块名并用来创建同名的类。新的.NET 类包含原模块的成员。例如,可以看 看一个名为 Security 的简单 COM 模块。 Module Security { Const long SIDToken = 0x009312; [entry(“ApplySidToThread”)] pascal void ApplySidToThread([in] long SID); } 当类型库导入器发现该模块时,它创建一个名为 Security 的新类,如下所示。 Public class Security { Public static const SIDToken = 0x009312; Public static void ApplySidToThread(long SID); } 在这个示例中,作者有意丢掉了一些与讨论无关的属性,如调用约定。这里要注意的重要事 情是原模块的成员转换成了新类的公共静态成员。 2.2 运行库可调用的封装器 既然知道了如何将 COM 类型库转换成.NET 装配件,就需要学会.NET 运行库如何与 COM 交互。就像前面已经提到的,类型库导入器生成的装配件起实际 COM 类的包装的作用。这 种封装器称为运行库可调用的封装器(Runtime Callable Wrapper,RCW)。RCW 有几个任务: * 保持对象的一致性 * 维护 COM 对象的寿命 * 代理 COM 接口 * 调度方法调用 * 使用诸如 IUnknown 和 IDispatch 类的缺省接口 2.2.1 保持对象的一致性 为理解 RCW 如何维护对象的一致性,可检查创建包含了 COM 对象的托管类的一个新实例 时会发生什么。在托管类上调用 new 有创建 RCW 的新实例和基本 COM 对象的新实例的效 果。当对 RCW 调用方法时,RCW 确保那些方法是由 COM 对象所支持的接口中的一个实 现的。RCW 通过在后台调用 IUnknown->QueryInterface()来实现这一点。当将 RCW 类的一 个实例从一个接口强制转化成另一个接口时,RCW 在其内部缓冲区中查看它是否已经具有 了对所请求的接口的引用。如果接口没有进入缓存,RCW 调用 IUnknown->QueryInterface 检查 COM 对象是否支持该接口。如果被请求接口不存在,运行库会产生一个异常。COM 对象一致性是由 RCW 通过不允许.NET 客户获得对基本 COM 对象不支持的接口的引用来维 护的。 2.2.2 维护 COM 对象的寿命 在第 1 章里,已经讨论过非确定性结束问题。非确定性结束处理.NET 中的类型被设置成 null 或超出范围时不必被销毁的情况。.NET 中的类型只有当发生垃圾收集时才会被销毁。这对 如引用了非托管 COM 对象的 RCW 之类的托管类来说可能是一个特别的问题。 COM 为维护对象寿命实现了完全的替代系统。在 COM 中,对象是按引用计数的。COM 客 户每次引用对象时,它就调用 IUnknown->AddRef(),而在每次释放对象时,就调用 IUnknown->Release(),这就允许 COM 对象消耗其内部引用计数。一旦引用计数达到零,就 释放实例。RCW 的工作方式与传统的 COM 客户在适当时机调用 AddRef 和 Release 是相同 的。差别在于 Release 是由 RCW 的 Finalize 方法调用的。通常,这不会产生太大的问题, 但有两种情况可能会有问题。 考虑一下 RCW 封装器如占用数据库连接之类紧缺资源的 COM 对象酌情况。如果像这样的 COM 对象只有当其引用计数变成零时才释放资源,资源可能会被占用到垃圾收集发生时。 很明显,这是对资源的浪费。 第二个可能发生的问题在当托管的应用程序关闭时。.NET 运行库并不保证在应用程序关闭 过程中调用终止器。如果在关闭应用程序前没有调用 RCW 的终止器,Release 不能在 RCW 占用的任何接口上被调用。System 名称空间中的 GC 类有两个可以减少这种问题的方法— ——RequestFinalizeOnShutdown 和 WaitForPendingFinalizers。RequestFinalizeOnShutdown 强 制运行库在关闭过程中调用类上的终止器。如果读者还记得的话,就会知道单独的线程会调 用类的 Finalize 方法。WaitForPendingFinalizers 通知运行库在关闭前等持终止线程的完成。 这些方法应当小心地加以使用,因为它们会延长应用程序的关闭时间。 关于非确定性结束问题有另一种方式。.NET 框架允许用户自己负责调用 Release ()。 System.Runtime.InteropServices 提供了一个称为 Marshal 的类。Marshal.RelaseComObject 带 有 RCW 的一个实例作为参数并将引用计数减 1。 Using System.Runtime.InteropServices; Class CMain { Public void MakeDogBark() { // RCW that maps to the CDog COM Class CDog dog = new CDog(); dog.Bark(); Marshal.ReleaseComObject ((object)dog); } } 在前面的代码示例中,一旦完成了 RCW 实例,就可通过调用 Marshal.ReleaseComObject() 来减少对基本 COM 对象的引用。如果则是唯一使用基本 COM 对象的客户,其引用计数就 变成零,并且释放其内存。当下一个垃圾收集发生时,就释放 RCW 的实例 dog。任何在引 用计数达到零后对 RCW 的进一步的使用都会产生一个异常。 2.2.3 代理接口 RCW 负责代理暴露给托管客户的接口并使用某些不直接暴露给托管客户的“标准”COM 接 口。可在任何暴露给托管客户的接口上调用任何方法。RCW 负责把这些方法调用导向给适 当的 COM 接口。通过这种方式,RCW 就不必在调用方法前将 RCW 的一个实例强制转化 成适当接口。 正如读者可能已经猜到的,RCW 的目的是使.NET 客户认为正在访问另一个.NET 客户,而 且使 COM 对象认为正在被 COM 客户所访问。RCW 能做到这一点的一种方法是将某个接 口对.NET 客户隐藏起来。表 2-l 列出了 RCW 可直接使用的较通用的接口。 表 2-l RCW 使用的接口 COM 接口 描述 Iunknown 当.NET 使用早期绑定访问 COM 对象时,RCW 使 用该接口。COM 的早期绑定是通过将 COM 类型库 导出到.NET 装配件中然后像普通.NET 类型一样访 问这些装配件类型实现的。当从这些装配件中的一 个在类型上调用成员时,RCW 就决定成员所属的 接口:如果接口没有缓存到 RCW 内部接口表,RCW 调用 IUnknown->QueryInterface,传 递 COM 接口的 名称。如果接口存在,则调用 IUnknown->AddRef。 如果接口不存在,会向客户产生一个异常 Idispatch .NET 客户使用后期绑定来访问 COM 对象的成员 时,RCW 就使用这一接口。与 COM 对象的后期绑 定是在.NET 中通过所谓的映像(Reflection)技术来 完成的 IsupportErrorInfo 和 IerrorInfo 如果 COM 对象实现这些接口,当 COM 方法返回 一个失败的 HRESULT 时,RCW 就用它们获取与错 误有关的扩展信息。RCW 把由这些接口提供的信 息映射到那些映射至.NET 客户的例外 IconnectionPoint 和 IconnectionPointContainer 这些接口在 COM 中使用以支持 COM 事件属性。 RCW 用这些接口将 COM 事件映射到.NET 事件 2.2.4 调度方法调用 除了所有其他责任之外,RCW 还负责调度从.NET 客户向 COM 对象的方法调用。RCW 代 表.NET 客户执行几种功能: * 把失败 HRESULT 从 COM 转换成.NET 异常。失败的 HRESULT 强制产生一个异常;而 成功的 HRESULT 则不会。 * 把 COM 的 retval 参数转换成.NET 函数的返回值。 * 把 COM 数据类型.NET 调度成数据类型。 * 处理从托管代码到非托管代码的转换。 2.3 线程处理问题 要用 COM 对象编写高效的. NET 应用程序,重要的是要明白 COM 和.NET 之间线程处理的 不同。COM 线程处理模型使用了单元的概念。在 COM 领域里,进程被从逻辑上分成一到 多个单元。单元可以有在其内运行的单个线程,或者也可以有多个线程。带单个线程的单元 称为单线程单元(Single Threaded Apartment,STA);运行多个线程的单元称为多线程单元 (Multi-Threaded Apartment,MTA)。当 COM 客户调用 COM 运行库创建组件的一个新实例 时,COM 运行库从 Windows 注册表读取组件的线程值。注册值通知 COM 运行库组件支持 哪种单元模型。大多数组件是 STA,包括那些 Visual Basic 6 所创建的组件。从其组件在不 同单元模型中运行的客户必须通过代理承接体对进行方法调用。代理承接体属性允许在运行 于不同单元中的客户和组件间无缝集成。然而,这种无缝集成是有代价的。当调用必须跨单 元进行时,应用程序就会遭受性能降低。这是由于要使方法调用工作顺利必须发生额外调度 的事实。 在 COM+中,单元被进一步分成“上下文”。上下文是包含 COM+属性的对象,如事物处理 的当前状态。每个单元可以有一到多个与之有联系的上下文。上下文是 COM+中最小的执 行单元,对象在任何时候只能运行于一个上下文中。 .NET 运行库并不完全遵循 COM 线程模型。缺省情况下,.NET 运行库中的对象运行于 MTA 中。如果 COM 对象和.NET 线程不支持相同的线程模型,调用必须通过代理承按体对来进 行。 要减少运行库边界的代价,必须明白所使用的 COM 组件的线程。如果正在使用 STA COM 组件,把当前.NET 线程的状态设置成 STA 是明智的。这可通过框架的 System.Threading 名 称空间里的 Thread 类实现。可通过调用下面的代码把当前.NET 线程的状态设置成 STA; System.Thread.CurrentThread.ApartmentState = ApartmentState.STA. 在创建任何 COM 对象前必须设置线程状态。通过这样做,RCW 可以直接调用基本 COM 组 件而不必通过代理承接体对。 2.4 小结 在本章中,我们讲解了通过使用类型库导入器将 COM 类型库转换成.NET 装配件的概貌。 正如读者已经看到的,这个实用工具有下列作用: * 将类型库转换成名称空间 * 将 typedefs 转换成其本地类型 * 将 enums 转换成.NET enums * 将模块级的方法和常量转换成静态类和成员 我们还探讨了 RCW 是如何负责从.NET 应用程序向 COM 组件的调度方法调用。简单地说, RCW 有下列作用: * 保护对象的一致性 * 维护 COM 对象的寿命 * 代理 COM 接口 * 调度方法调用 * 使用如 IDispatch 和 Iunknown 类的缺省接口 在下一章中,读者将学习如何使.NET 类为基于 COM 的客户所使用。 第 3 章 从 COM 中使用.NET 组件 本章内容包括: * 将装配件转换成 COM 类型库 * 用 COM 注册装配件 * COM 可调用的封装器(COM Callable Warpper) * .NET 组件的设计方针 像 COM 组件可以从.NET 应用程序中使用一样,.NET 组件也可从 COM 客户中使用。实际 上,完成这一点的开发模型与使 COM 组件对.NET 客户可用所用的开发模型类似。在本章 中,将讨论使.NET 装配件从 COM 可访问的必要步骤。 3.1 将装配件转换成 COM 类型库 .NET SDK 有两种工具可以用来从装配件生成类型库:类型库导出器(Type Library Exporter)(tlbexp.exe)和装配件注册工具(Assembly Registration Tools)(regasm.exe)。类型库导 出器以装配件作为输入并产生相应的类型库作为输出。装配件注册工具也从.NET 装配件产 生类型库并在 Windows 注册表中注册该类型库及其 COM 类。由于关注的不只是创建类型 库,让我们把重点放在装配件注册工具上。 装配件注册工具还是另一种命令行实用工具。首先看看它的一些较为通用的参数。表 3-1 标 出了在本节中使用的参数。 表 3-1 Assembly Registration Tools 选项 选项 描述 /regfile:RegFileName.reg 防止正常 COM 注册条目输入注册表。RegFileName.reg 包含已经进入 Windows 注册表的条目 /tlb:TypeLibFileName.tlb 为新生成的 COM 类型库指定目标文件。该开关不能与 /regfile 结合使用 /u and /unregister 不给任何已经从该装配件注册的类注册 现在考察一个简单的示例来观察该工具是如何应用的。请看下列的.NET 类: // Assembly file name: Animal.dll using System; namespace Animals { public class CDog { public void Bark() { Console.WriteLine(“Woof Woof”); } } } 把该类编译成 Animal.dll 并用装配件注册工具以下面的语法运行:regasm /tlb:animal.tlb animal.dll。然后,如果观察一下所产生的类型库,就会发现某些与下面相似的代码: Library Animals { CoClass CDog { [default] interface _CDog; interface _Object; }; interface _CDog : IDispatch { HRESULT ToString([out, retval] BSTR* pRetVal); HRESULT Equals([in] VARIANT obj, [out, retval] VARIANT_BOOL* pRetVal); HRESULT GetHashCode([out, retval] long* pRetVal); HRESULT GetType([out, retval] _Type** pRetVal); HRESULT Bark(); } } 可从检查 CDog 的 CoClass 伴生类定义开始。注意有两个接口:_CDog 和_Object。.NET 通 过允许一个对象继承另一个对象的成员支持单一继承。在.NET 中,每个类既可直接也可间 接从 System.Object 继承。当装配件注册工具读取装配件时,能够为每个公共类提取继承层 次。继承树中每个类的成员都是作为被估算的类的成员而添加的。所以当装配件注册工具发 现 Cdog 是从 System.Object 继承时,它向 CDog 添加 System.Object 的成员。 正如读者可以看到的,.NET 名称空间中的许多名称直接映射到类型库。例如,Animals 名 称空间就直接映射到库名。另外,类名 CDog 直接映射到 CDog 伴生类。也有装配件中的类 型名不能直接映射到类型库的情况。在.NET 中,类型名可以通过多个名称空间重复使用。 类可以在 Animals 名称空间和 Mammals 名称空间中存在。为理解装配件注册工具如何处理 这种命名冲突,可把装配件修改成包含 Mammals 名称空间。 // Assembly file name: Animal.dll using System; namespace Animals { public class CDog { public void Bark() { Console.WriteLine(“Woof Woof”); } } namespace Mammals { public class CDog { public void RollOver(){ Console.WriteLine(“Rolling Over”); } } } 如果重复以前的步骤,可以得到与下面的类型库类似的代码: Library Animals { CoClass CDog { [default] interface _CDog; interface _Object; }; interface CDog : IDispatch { // .. System.Object 成员的申明 HRESULT Bark(); }; CoClass CDog_2 { [default] interface _CDog_2; interface _Object; }; interface _CDog_2 : IDispatch { // .. System.Object 成员的申明 HRESULT RollOver(); } } 请注意,装配件注册工具已经向装配件 Cdog 中的第二个实例添加了下划线和一个数字 2。 因为 Animal.Cdog 第一次是在源代码中定义的,在类型库中就能保持其原名。Cdog 名称随 后的使用则加入下划线和运行索引为后缀。如果 Cdog 是接口而不是类,则应用相同的规则。 然而要预先警告的是,这种命名的规则在将来发行的框架中可能会发生改变。 在将装配件转换成类型库时有几个限制。例如,只有公共类和接口才能被导出。此外,要导 出的公共类必须实现无参数的构造器。任何如域和方法之类的静态成员不能被导出。如果希 望提供对静态成员的访问,必须将其封装到实例级的方法调用中去。 如果有符合这些标准的类且还想使它们对 COM 不可用。可使出 System.Runtime. InteropServices.ComVisible 属性。ComVisible 属性可用来 COM 隐藏装配件、类、和接口。 虽然这种属性的值可为真或假,但它不能使其他不可见类型对 COM 可用。被标记为私有、 内部的类型,或不具有缺省构造器(无参数)的类型不能使其对 COM 成为可见,而不管 ComVisible 属性的值是什么。 装配件中所包含的内容并不仅仅是类型的定义。正如已经从第 1 章所学到的,如果装配件是 在多个应用程序之间共享的,装配件就包含由四部分组成的版本号、简单的字符串组成的名 称、创始人的公共密钥,以及可选的强名称。当装配件被转换成类型库时,必须为 COM 客 户创建唯一的 TypeLib ID,以便找到注册表中的类型库。在转换过程中,装配件的简单名称 和创始人密钥用来生成新的 TypeLib ID。所给出的简单名称和创始人密钥总是生成相同的 TypeLib ID。 类型库也包含版本号。装配件的主版本和次版本号被转换成类型库的主版本号和次版本号。 装配件中的构建号和修正号被抛弃了。如果装配件不包含版本号,装配件注册工具和 TlbExp 实用工具使用 0.1 作为类型库的版本号。 3.2 向 COM 注册装配件 当装配件注册工具向 COM 注册装配件时,它把类所需要的所有条目和类型库放入 Windows 注册表。装配件注册工具的/regfile 开关用于将生成的注册表条目保存到一个文件中。该文 件可以复制到需要向 COM 注册装配件的其他机器。 现在就在启用/regfile 开关的情况下用装配注册工具运行 CDog 类。 // Assembly file name: Animal.dll using System; namespace Animals { public class CDog { public void Bark() { Console.WriteLine(“Woof Woof”); } } } 使用在前述代码中定义的 CDog 类,装配注册工具产生下面的注册表文件。请注意,当使用 了该开关时,注册表条目就不会进入 Windows 注册表。 REGEDIT4 [HKEY_CLASSES_ROOT\Animals.CDog] @=”Animals.CDog” [HKEY_CLASSES_ROOT\Animals.CDog\CLSID] @=”{AC480224-1FA7-3047-AE40-CCDD09CDC84E}” [HKEY_CLASSES_ROOT\CLSID\{AC480224-1FA7-3047-AE40-CCDD09CDC84E}] @=”Animals.CDog” [HKEY_CLASSES_ROOT\CLSID\{AC480224-1FA7-3047-AE40-CCDD09CDC84E}\InprocSer ver32] @=”C:\WINNT\System32\MSCorEE.dll” “ThreadingModel”=”Both” “Class”=”Animals.CDog” “Assembly”=”animal, Ver=0.0.0.0, Loc=””” [HKEY_CLASSES_ROOT\CLSID\{AC480224-1FA7-3047-AE40-CCDD09CDC84E}\ProgID] @=”Animals.CDog” [HKEY_CLASSES_ROOT\CLSID\{AC480224-1FA7-3047-AE40-CCDD09CDC84E}\Imple mentedCategories\{62C8FE65-4EBB-45e7-B440-6E39B2CDBF29}] 类的 ProgID 是根据 namespace.class-me 的格式生成的。装配件注册工具把 CDog 类注册为 Animals.CDog。 在前面所示的注册表文件中,已经突出显示了所有最感兴趣的主键。我们可从这种特定的注 册表条目学到很多东西。首先,COM InprocServer32 并不是所希望的 animal.dll,而是 MSCorEE.dll。该 dll 提供了允许 COM 客户与.NET 对话的间接的级别。这一 dll 实现将在下 一节讨论的 COM 可调用的封装器。 线程模型被定义为“Both”。这就允许.NET 类既可运行在单线程单元中也可运行在多线程单 元中—这要根据正在其内运行的非托管客户的单元模型而定。 最后,请注意装配件的全新注册表条目。装配件注册主键通知 MSCorEE.dll 哪个装配件正在 实现 ProgID 所标识的类。装配件的版本和地址也保存在注册表中。该信息在运行时被传给 装配件分解器以确保找到正确的装配件。 3.3 COM 可调用封装器 与运行库可调用封装器配对的是 COM 可调用封装器(COM Callable Wrapper,CCW)。正如 在前一节中所看到的,当通过装配件注册工具使.NET 装配件对 COM 可用时,MSCorEE.dll 被注册成 COM 服务器。该 dll 实现 COM 客户使用时所调用的 CCW。与 RCW 不同,CCW 是运行于.NET 运行库外的非托管代码。CCW 有下列作用: * 保持.NET 对象的一致性 * 维护.NET 对象的寿命 * 代理明确的接口 * 按需提供标准 COM 接口 * 在运行库之间调度方法调用 3.3.1 保持对象的一致性 CCW 确保在.NET 类和作为其封装器的 CCw 之间总是有一对一的关系。例如,当托管方法 调用返回另一个类实例时(不管是从返回值还是外部参数),运行库确保创建的是新的 CCW 实例。相反,当 COM 客户将其接口强制转化成基本的托管类支持的接口时,CCW 确保接 口是托管类所支持的,而且托管类的新实例不仅仅是为所请求的接口服务而创建的。 3.3.2 维护对象的寿命 当 COM 客户收到对 COM 对象的引用时,它调用 IUnknown->AddRef 以便增加对象计数。 相反,当它释放对对象的引用时,它调用 IUnknown->Release。CCW 为 COM 客户提供一个 Iunknown 接口以调用这些方法。因为.NET 类没有实现引用计数,当其内部引用计数达到零 时,CCW 维护该计数并释放.NET 类。一旦 CCW 释放其对.NET 类的引用,类就变得适合 于垃圾收集,并假定不再有对该类的托管引用。 从 CCW 所封装的.NET 类漏掉内存是可能的。如果在垃圾收集前,通过 CCW 使用.NET 类 的非托管进程关闭时,托管类不会从内存释放。要避免这个问题发生,非托管进程必须调用 CoEEShutDown 函数。该函数是 NET 运行库暴露的非托管 API 的一部分。一旦该函数被调 用,.NET 运行库就执行一次新的垃圾收集。关于该 API 的更多信息可以在 Visual Studio .NET 文档的 Tool Developers Guide 一节中找到。该 API 中的方法可以从任何能调用 Windows API 函数的编程语言中使用。 3.3.3 标准 COM 接口:Iunknown 和 IDispatch Iunknown 和 Idispatch 是 COM 领域中广为人知的两个接口,但它们在.NET 领域中没有意义。 CCW 负责按需提供这些与 COM 的接口。IDisPatch 可能是 COM 中最常用的接口。如 Active Server Pages、Visual Basic(非托管版)和 Visual Basic for Applications 之类的自动化客户专门 使用 IDispatch。IDispatch 暴露允许客户在运行时从类型库查询类型告息的方法。如果.NET 应用程序不提供预构建类型库,.NET 运行库就临时创建一个。该进程可能相当费时且大幅 度降低应用程序的运行速度。因此,随应用程序捆绑一个类型库是明智的。 如果事先知道自动化客户从来不会调用.NET 类,就可以不需要支持 IDispatch 接口。 System.Runtime.InteropServices 名称空间提供 NoIDispatch,这是一种不需要支持 IDispatch 接口的属性。 如果 COM 接口通过 IUnknown->QueryInterface 查询该接口,则返回 E_NOINTERFACE HRESULT。 3.3.4 代理接口 .NET 中的类可以实现任何数量的接口。当 COM 客户将其.NET 类的伴生类实例转化成一个 明确的已实现的接口时,CCW就创建包含该接口的方法的VTable(虚拟函数表)。通常在COM 中,VTable 包含指向接口所实现的函数的指针。作为替代,CCW 将承接体函数置于 VTable 中而不是函数指针。这些承接体负责在 COM 和.NET 之间调度方法调用。承接体函数在稍 后的“激活生命周期”一节有更详细的描述。 VTables (Virtual Function Tables,虚拟函数表)定义包含指向接口所定义的函数的指针的内存 块。当客户获得对接口的引用时,不管该接口是一个特殊接口还是类的缺省接口,它都接收 指向 VTable 的指针。 3.3.5 调度方法调用 一旦 COM 客户已经获得一个对类的接口的引用,它就开始调用该接口上的方法。客户使其 调用依赖于在接口的 VTable 中提供的承接体。这些承接体有以下几种功能: * 管理非托管代码与托管代码之间的转换 * 在这两种运行库之间转换数据类型 * 把.NET 方法返回值变成输出的 retval 参数 * 把.NET 异常变成 HRESULT 要防让 CCW 在两个运行库之间调度方法调用是可能的。 System.Runtime.InteropServices.PreserveSigAttribute属性用于在 COM 客户调用托管方法时维 护其特征表示。例如,具有下列特征表示的托管方法: long SomeMethod(string sParameter); 将被转换为下列格式的类型库: HRESULT SomeMethod([in] BSTR sParameter, [out, retval] long* pRetVal); 当使用这个属性时,方法在类型库中保持原样: long SomeMethod(string sParameter); 上面的第二点产生一个有趣的问题。转换数据类型也涉及调度。调度过程除以相反方式工作 外,其工作方式与前一章中所描述的调度进程是相似的。当参数被传给方法并从方法返回时, CCW 将.NET 数据类型转换成相应的 COM 数据类型。基本数据类型如字符串和整数在这种 情况下工作得很好。然而,其他托管类也可以从方法调用返回。从方法调用返回的托管类不 必注册成 COM 组件。COM 会为 COM 客户把托管类转换成适当的 COM 组件。 3.3.6 激活生命周期 通过初步了解当COM客户载入类并且开始调用方法时必须经过的每个阶段可以巩固这些概 念。在 COM 客户可得到托管类的一个实例之前,它必须找到 COM dll,将其装入,并得以 访问其类工厂。达此目的的过程如下: 1. 查询 Windows 注册表以便找出 CLSID,给定一个特殊的 ProgID(请记住,对于.NET 类来说,ProgID 就是 namespace.classname)。 2. 给定 CLSID,客户查询注册表的 CLSID 主键。“向 COM 注册装配件”一节显示 了.NET 类的该主键的样子。 3. 客户 COM API 的 DllGetClassObject 函数,传入 CLSID,得到对类的 ClassFactory 的引用。 类工厂 类工厂是负责创建其他 COM 类的实例的特殊的种类。大多数情况下,COM 中的每个类或 对象都由类工厂负责创建自己的实例。类工厂只负责创建它所代表的在的一个实例。用类工 厂控制类的创建过程与简单的调用 COM API 的 CoCreateInstance 相比有几个好处: * COM 类的作者可以用其所掌握的类知识提高类工厂的效率并改进创建过程 * 类工厂模型提供可以用来在创建过程中捕获错误的间接的级别 * 类工厂模块了对 COM 客户创建类的过程 类工厂也提供产生类的多个实例的有效方式。在如使用 COM+组件的 Web 服务器之类的多 用户环境中,类工厂用于在收到用户请示时创建 COM+对象的实例。实际上,COM+将类工 厂的实例放入缓存以改善 COM+对象的对象创建时间。 第 3 步是事情变得有趣的地方。MSCorEE.dll 是包括 COM+类的所有.NET 类的 COM 服务器。 当 COM 客户请求类工厂时,查询.NET 的类载入器(.NET Class Loader),看类是否已经载入。 如果类已经载入,就向客户返回类工厂。CCW 负责创建该类工厂井将它返回到客户。然而, 如果类没有载入,CCW 就在注册表中装配件的主键查找以确定装配件的名称、版本和地址。 该信息被.NET 装配件解析器终止。此时,装配件是通过正常的装配件定位算法定位的。第 1 章所讨论过的所有版本编号规则、配置文件改写和定位规则在此处也都适用。请记住,除 非装配件是在相同的目录中、在子目录中或者在全局装配件缓存中,装配件解析器不能找到 装配件。 一旦找到装配件,就将其传回到类载入器。类载入器负责载入类并确定装配件是否实现这类 (通过读取其元数据)。假定没有在装配件中找到类,装配件就创建一个类工厂并将其返回给 调用客户。 返回给客户的类工厂是标准的 COM 类工厂,能对其调用 IClassFactory.CreateInstance()以得 到封装了基本.NET 类的 CCW 的实例。IClassFactory.CreateInstance()带三个参数: * 指向 IUnknown 的指针 * 接口 ID(在类型库中定义的 GUID) * 用于保存对对象的引用的 void 指针 当客户传入 Iunknown 指针时,CCW 将指针的值与类工厂返回的类的实例进行比较。通过 这样做,CCW 保持了.NET 类的一致性。 当调用 CreateInstance()时,CCW 向客户返回接口并为接口方法建立 VTable。图 3-1 展示了 名为 IList 的为 MoveFirst、MoveLast 和 Goto 定义方法的 VTable。 客户调用 MoveFirst 和 MoveLast 时,CCW 管理在托管和非托管代码之间转换并捕捉方法产 生的任何异常。例如,如果是在客户已经到了表中的最后位置时调用 MoveLast 就可能产生 异常。产生这种异常时,CCW 就将该异常转换成 HRESULT。 3.3.7 .NET 组件的设计方针 我们已经碰到了几个设计用于 COM 的高效的.NET 类要用到的概念。这可能还不是非常明 显的,所以现在介绍几个关键的问题。 1. 使穿过 Interop 边界的流动最小化:正如读者可以看到的,许多 COM 和.NET 间的 低级交互作用是自动完成的。.NET 中的 COM Interop 提供了很大的灵活性。但这 种灵活性要付出性能的代价。当设计组件时,可以试着使客户必须穿过 Interop 边 界的流动最小化。 2. 采用信息传送结构:使穿过 Interop 边界的流动最小化的一种方式是实现一种信息 传送格式。不是遵循设置属性和调用方法的面向对象的途径,而是试图把多个参数 传送到一个单方法调用中。作为一种选择,最好传入一个包含所有方法参数的 XML 字符串。如果想使用如在 COM+中所使用的无状态编程模块或者如果想从使 用.NET 的远程处理的远程机器中调用对象,那么这类途径就很合适。 3. 提供类型库:当调用 IDispatch->GetTypeInfo 和 IDispatch->GetTypeInfoCount 时, IDisPatch 接口使用类型库。像前所提到的,.NET 运行库可以通过检查装配件的元 数据临时为这些方法调用生成类型库。该进程从性能的观点其代价是极其高昂的。 可以通过生成与部署自己的类型库来获得高的性能。 4. 关闭与处置:不同环境中对象寿命的差异可能导致某些奇怪的问题。占用紧缺资源 (文件句柄、数据库连接等等)的对象就应实现 Close 或 Dispose 方法,以便客户使 用资源之后.就将其释放。不要依赖垃圾收集或终正器来清理这些资源。 5. 使用标准数据类型: “标准”数据类型如整数和浮点数类型不必跨 Interop 边界。 但其他更复杂的类型如字符串和日期类型就会加以调度。此外还要记住,CCW 或 RCW 必须为返回对其他穿过边界的对象的引用(作为返回值或输出参数)的方法而 创建。 3.4 小结 本章讨论了如何通过使用装配件注册工具将.NET 装配件转换成 COM 类型库。装配件注册 工具不仅负责将装配件中的类型转换成类型库,也负责在 Windows 注册表中注册新类型库。 还要记住新值 Assembly 是在 InprocServer32 主键中创建的。Assembly 子键被传递给装配件 解析器以找到客户正在查找的装配件。读者已经看到了 CCW 如何在 COM 和.NET 之间的方 法调度过程中起重要作用的。 本章结束了本书的第一部分,与 COM 进行互操作。本书的这一部分想为读者提供继续学习 和在.NET 应用程序中学习实现 COM+服务所需的适当环境。本书的下一个部分将教给读者 从.NET 应用程序中使用诸如分布式处理、对象共享和队列组件一类的 COM+服务。 第二部分 COM+的核心服务 第 4 章 事务处理 第 5 章 安全性 第 6 章 事件 第 7 章 对象共享 第 8 章 列队的组件 第 4 章 事务处理 本章内容包括: * ACID 的要求 * 逻辑事务处理生命周期 * 物理事务处理生命周期 * 在 C#中编写事务处理组件 事务处理是 COM+基础服务的一种。实际上 COM+事务处理所提供给应用程序的好处是开 发者决定使用 COM+的大多数驱动因素之一。COM+事务处理对开发者来说是如此的带强制 性,因为能在单个事务处理中提供把事务处理服务捆绑在一起的粘合剂或管件,事务处理服 务包括 Oracle 和 SQL Server 数据库、CICS 应用程序和消息队列。这类管件代码是难以正确 设计和开发的。 本章将使读者学到事务处理的一些基本规则,如 ACID 规则。从这里开始,读者将学习 COM+ 如何提供诸如两阶段提交(Two-Phase Commit) 协议和自动事务处理应征(Automatic Transaction Enlistment)之类的服务,以便帮助人们遵循那些基本的事务处理规则。一旦深入 掌握了基本原理,就可用 C#和.NET ServicedComponent 类编写事各处理组件。遵循这些规 则,就可看到在 C#中开发组件时能避免一些陷阱和其他事情。 4.1 ACID 的要求 任何事务处理,不管它是不是 COM+事务处理,都必须有四个基本的特征才能成为事务处 理系统。这些规则可能看起来纯粹是学术性的不值得读者去注意.但深入理解它们会有助于 理解为什么 COM+会做某些事情。事务处理必须是:原子的、一致的、隔离的和持久的。 4.1.1 原子性 事务处理代表了一种要么完全成功要么完全失败的工作单元。事务处理,特别是 COM+事 务处理,为了能够执行可能有许多子任务要完成。例如,一个定单处理应用程序可能需要确 认来自顾客的数据,从一个数据库中减少顾客计数,向另一个数据库或消息队列提交订单。 假定顾客没有购买物品所需的足够资金;在这种情况下就不想提交订单了,这对吗?原子规 则表明如果因为顾客缺少资金决定中止事务处理,订单就不会被提交。也就是说,事务处理 的所有子任务必须成功地执行否则什么也不做。 4.1.2 一致性 一致性规则说明事务处理必须以处于一致状态的数据开始并且以处于一致状态的数据结束。 考虑一个应用程序正在更新两个数据库的情景。如果第一更新是成功的且临时提交了数据, 但第二次更新失败了,第一次更新必须把数据恢复到处理开始前的状态。通过恢复任何临时 的改变,事务处理系统能够在中止的过程中维护数据的一致性。相反,如果事务处理成功, 提交的数据不必确认任何与数据有关的业务规则。一致性是在应用程序设计中需要考虑的事 情。例如,如果数据中有一个包含三个十进制位置的十进制域,在事务处理过程中把域缩成 两个十进制位置。在 COM 中的一致性仅能支持到数据的改变能够被提交和恢复的程度。相 关的商业规则应由开发者来决定。 4.1.3 隔离性 隔离规则说明同时发生的事务处理必须不能相互看到彼此的工作。如果事务处理 A 正在更 新数据库而事务处理 B 试图查询相同的表,事务处理 B 必须看不到事务处理 A 正在更新的 数据,直到 A 已经提交或中止。通常,隔离规则会导致对数据库表的某种严格的锁定,特 别是在 COM+中。 表锁定限制另一个应用程序可以查看当前正在被另一个应用程序使用的数据级别。表的锁定 实际上是以两种方式进行的:读锁定和写锁定。读锁定允许其他程序从表中读取数据,但不 允许另一个程序向表中写入数据。这允许建立锁定的应用程序可以安全地从表中读取而看不 到其他应用程序已部分完成的工作。读锁定通常用于应用程序需要从表中选择数据之时。写 锁定防止其他应用程序对表的读取或写入。写锁定比读锁定的限制更严,因为它们对可以对 表进行操作的话动类型进行了更大的限制。写锁定通常用于需要在表中更新或插入数据的应 用程序。 事务处理中对数据库应用的锁定级别称为隔离级别。可以通过调节减少隔离级别从而改善性 能,或通过调节增加隔离级别。如果减少隔离级别能改进性能且增加隔离级别会降低性能, 那么为什么任何人都想增加隔离级别呢?原因是增加隔离级别减少了仍处于事务处理过程中 的另一个应用程序可以修改数据的机会。事物在处理中有四种可能的级别。表 4-1 列出并描 述了每个可能的隔离级别。 表 4-1 隔离级别 隔离级别 描述 读取未提交数据 这是最低的隔离级别(性能最高),这将允许读取当前正 在被另一个应用程序更新的表。这具有可能看到还没有 完全更新的数据的风险。这是所谓的“脏”读取 读取提交数据 该级别比读取未提交数据高一级。使用该级别的应用程 序会等待直到任何写锁定都被释放。这是一种稍微低效 的执行.因为应用程序必须等待其他应用程序释放其锁 定 可重复读取 该级别防止其他应用程序建立写锁定。其他应用程序可 以建立读锁定,但不能建立写锁定 可串行化 该级别在受处理影响的整个行的范围内建立读锁定和写 锁定(根据任务而定)。例如,如果应用程序正在从表内 进行 select *,然后就会在表的所有行内建立写锁定 在 Microsoft SQL Server 上运行的 COM+事务处理应用了可串行化的隔离级别。该隔离级别 的限制最严,常常导致表一级的锁定。不幸的是在 COM+中,Windows 2000 的隔离级别是 不能被配置的。 4.1.4 持久性 持久规则说明事务处理一旦被提交,其数据必须存储在不怕电源断电的永久状态中。如果运 行事务处理的计算机在提交处理后发生崩溃并立即重新启动,则事务处理中涉及的数据存储 在永久位置上。也就是说,一旦事务处理提交后,数据必须被数据库引擎存储于数据库中且 保存到磁盘上。还有,对正在进行的事务处理,持久性要求事务处理能够在发生如断电一样 的系统中断之后,能够在中断的地方恢复。通常,日志随着事务处理的发生而更新,从而允 许事务处理在中断后能够返回原处重新开始。如果系统中断发生在事务处理过程的中间,这 些日志文件就会被读取以决定处理是否应当被提交或中止。事务处理管理器负责维护这种类 型的日志文件并决定事务处理的某部分是应当提交还是应当中止。事务处理管理器会在本章 后面的“实际事务处理生命周期” (Physical Transaction Lifecycle)一节讨论。 4.2 理解 COM+事务处理过程 就像在本章的导言中提到的,COM+提供使事务处理组件工作的粘合剂。粘合剂(或基本事 务处理结构)不是那些在日常基础上与开发者所需的极其关切的典型组件。然而,对基本事 务处理如何工作的深入理解有助于设计事务处理组件和应用程序。 在 COM+中,事务处理分成两部分:逻辑事务处理和基本实际事务处理。事务处理组件运 行于 COM+运行库提供宿主的逻辑事务处理内。然而,实际事务处理以发生在逻辑处理内 的行为为基础被初始化。详细考察每一部分看看这些部分如何互操作从而形成完整的 COM+ 事务处理。 4.2.1 逻辑事务处理生命周期 当客户对事务处理组件进行第一次调用时,逻辑事务处理就开始了。组件运行于逻辑事务处 理内部并通过其上下文与它交互。逻辑事务处理驱动基本的实际事务处理。现在打入逻辑事 务处理的周期内查看它是如何工作的。 第二章简单介绍了上下文的概念。笔者将上下文解释成与单元有关,即每个单元可分成多个 上下文。上下文对事务处理组件特别重要。COM+中的每个组件在创建时都得到一个与之有 关的上下文。当客户实际实现事务处理组件时,COM+就会查看客户是否已经参与到事务处 理中。如果客户运行于事务处理中(客户可以是另一个事务处理组件)且组件支持在另一个事 务处理中运行,就创建组件,并且所创建的组件继承客户的上下文。可把上下文想像成一种 包含 COM+和组件相互对话且与运行库有关的信息的属性“袋”。 非托管组件(例如用 Visual Basic 6 编写的那些组件)通过 ObjectContext 的属性和方法操作上 下文。.NET 组件使用名为 ContextUtil 的类似对象。ContextUtil 驻留于 System.EnterpriseServices 名称空间中。如果将这两种 API 进行比较,就会注意到许多相似 的方法和属性。 1.说明性事务处理 COM+支持说明性事务处理。说明性处理允许开发者通过属性操作组件的事务处理特性。组 件的事务处理属性存储于 COM+目录中。可把 COM+目录想像成存储 COM+事务正确实现 和运行组件所需的所有信息的小型数据库。COM+支持五种事务处理属性的设置: * Disabled:从不运行于 COM+事务处理内的组件。具有该属性的组件是非事务处理性的。 * Not Supported:这是运行于 COM+中的组件的缺省值。该属性告诉运行库此组件不支持 事务处理。如果组件通过调用 ObjectContext.SetComplete 或 ObjectContext.SetAbort 选择 事务处理(见下面的“进行选择”一节),这些选择就不会计入事务处理的结果。具该属 性的组件是非事各处理性的。 * Supported:具有该属性的组件可在客户的事务处理中创建。如果客户未运行于事务处 理中,组件就不会在事务处理中运行。具有该属性的组件是事务处理性的。 * Required:该组件要求运行于事务处理中。如果存在这种组件的话,该组件运行于调用 者的事务处理中。如果调用者未运行于事务处理中,就会创建一个新的事务处理。 * RequiresNew:该组件必须总是创建于一个新的事务处理中,而不管调用者的事务处理 状态。 正常情况下,这些属性是在组件的类型库中指定的。当组件被安装到 COM+库或服务器应 用程序中时,就从类型库中读取属性设置并府用于组件。此后,这些设置可在任何时候加以 改变。当用 C# 或任何其他支持 CLR 的语言编写组件时,这些属性通过 System.EnterpriseServices 属性被直接插入到源代码中去。编译组件时,这些属性显示于装配 件的元数据中。正如读者将在本章的后面所看到的,在组件被安装到 COM+应用程序中时, 这一属性是从元数据中读取并应用到组件中去的。 2.即时激活 在前面已经说过当调用者对事务处理组件进行第一次方法调用时逻辑处理就开始了,当组件 创建时,逻辑事务处理就开始。根据一种称为即时激活(JITA)的概念,以上两种描述都是正 确的。然而,对事务处理组件而言,JITA 是必需的。实际上,当选择 Supported、Required 或 RequiresNew 事各处理属性时,该属性受到检查且不可用。请参考图 4-1 看一看在组件服 务资源管理器(Component Services Explorer)中的显示。 当客户发出创建请求时,COM+截获调用并返回组件对客户的引用而不创建组件。只要客户 调用组件上的方法,COM+就激活组件并调用方法。假定组件将其完成位设置为真,当方法 返回时就销毁组件。 完成位是上下文中组件用来通知 COM+其工作已完成的一个标志。方法返回时,COM+检查 组件的完成位以便决定是否应当销毁组件。缺省情况下,方法首次调用时该设置被置为假。 C#中,ContextUtil.SetComplete 和 ContextUtil.SetAbort 都将该完成位设为真。相反, ContextUtil.EnableCommit 和 Context.DisableCommint 则将该位设为假。如果已经用 Visual Basic 6 编写过 COM+组件,ContextUtil 类的这些方法看起来应当很熟悉。Visual Basic 6 中 使用的 ObjectContext 组件支持方法的相同设置,这些方法执行与 ContextUtil 类中相同的函 数。 COM+支持一种名为 autodone 的属性。该属性通过使用组件服务资源管理器应用于方法。 Auto-Dong属性将方法首次调用时完成位的缺省值由假改为真。.NET框架通过AutoComplete 属性提供这种支持,这会在本章的后面遇到。图 4-2 展示了 Auto-Done 处于启用状态的组件 服务资源管理器中的一个方法。 .NET 框架通过 System.EnterpriseServices 名称空间的 AutoComplete 属性支持 COM+的 AutoDone 属性。该属性应用于方法级别上且在组件安装时在 COM+目录中被自动设置。 JITA 模型很适合于事务处理组件。当每个方法调用都进行自动工作单元,然后退出并被销 毁时,事务处理组件工作得最好。如果允许 COM+在每个小工作单元后都销毁对象,组件 数据就没有机会能够在以后破坏事务处理。如果该模型中没有 JITA,客户必须在每次方法 调用后都创建对象。正如读者可能想象得到的,这可能会变得很冗长。因为 JITA 每次都创 建代表客户的对象,客户就不用担心其对组件的引用是否还有效。 3.进行选择 正如读者可能猜想的,每个组件都在事务处理的全部结果中得到一个选择。选择是通过把上 下文上的“consistent”(一致的)标志设置为真或假来作出的。如 SetComplete 和 EnableCommit 等 ContextUtil 对象的方法将该位设为真。SetAbort 和 DisableCommit 方法则将该位设为假。 如果组件处于一致的状态,它就能够提交处理,所以其选择就会被提交。相反,如果对象处 于不一致的状态,组件就不能提交其数据,所以它就选择中止。最初,该位被设置成真。 逻辑事务处理的完成位和一致位之间的组合可用来决定是提交还是中止。把完成位设为真会 强制 COM+检查组件的一致位。如果完成位和一致位都是真,对象的最终选择是提交事务 处理。如果说完成位是真而一致位是假,事务处理中止,而不管事务处理中任何其他对象的 选择。 除了 JITA,事务处理组件也要求同步化。同步化不让两个客户在同一时间调用同一组件的 方法。同步化对事务处理组件很重要;没有同步化,客户就可能进入另一个客户正在使用的 组件,这就违反了 ACID 规则的自动性和隔离性。 4.理解事务处理的作用范围 用这种方式开发事务处理组件看似简单。所要做的就是编写数据访问逻辑和业务逻辑,如果 有问题就调用 SetAbort,否则就调用 SetComplete。复杂之处在于要理解事务处理的作用范 围。笔者就因为没有完全理解事务处理组件的作用范围而深受其害。为理解逻辑处理,请看 一个示例。 假定有一个最终用户可用来输入销售定单的 GUI 应用程序。典型情况下,销售定单包括一 些客户名、数量等头信息,以及成行的数据。用户提交销售定单时,GUI 应用程序就创建远 程事务处理组件并开始调用组件上的方法向数据库提交销售定单。请记住每个方法调用会导 致四种操作: * COM+激活组件 * 调用组件的方法 * 组件在事务处理上进行选择 * COM+销毁对象 还有一个棘手的部分。如果 GUI 客户负责调用组件上的方法,就会为每个方法调用开始一 个新的事务处理,因为每次都会创建和销毁组件。如果首次方法调用成功而第二次失败,数 据库中的销售定单可能处于不一致状态。 有两个方法可绕过这一问题。第一种解决方法是使用 TransactionContext 对象。这个对象允 许非事务处理客户(如前面提到的 GUI 应用程序)控制或协调事务处理。TransactionContext 有提交或中止事务处理的方法和创建必须参与到事务处理中的其他对象的方法。然而,这种 途径有几个不足之处: * 它要求客户安装有 Transaction Context 库。 * 非事务处理组件不会从 COM+的自动事务处理服务得到好处。如果非事务处理组件失 败,事务处理可能中止也可能不中止。 第二种解决方法是创建使用其他子事务处理组件中的一个事务处理组件。这个模型较好,因 为事务处理的所有工作能够发生在 COM+运行库的保护之中。如果修改前面的例子,GUI 客户可以调用协调组件上的一个方法并且可以在同一时间将所有销售定单数据传给它。协调 组件可以负责具体实现子组件并调用方法进行数据库工作。这也是一个把业务逻辑和确认逻 辑从数据库-访问逻辑中隔离出来的好方法。协调组件可以实现应用程序的业务规则。假设 满足了业务规则,数据可以通过于组件输入。如果在一次调用中把所有的数据都传到协调组 件中,就会得到减少完成处理所需的在网络上传递的信息量的额外好处。现在,不用每次进 行方法调用时都遭受性能降低,而是对整个事务处理遭受一次性能降低就可以了。 在这个方案中,协调组件有 Requires 或 RequiresNew 事务处理属性。协调组件起事务处理的 根组件的作用。客户在该组件的一个实例上调用第一个方法时,事务处理就开始。一旦组件 已经将其完成位设成真,逻辑事务处理就结束了。 4.2.2 实际事务处理的生命周期 在本节中,读者将看到逻辑事务处理过程中在后台会发生什么并且在逻辑事务处理结束后包 括在事务处理中的实际资源会发生什么。 1.自动事务处理应征 当事务处理组件连接到如数据库服务或队列管理之类的资源时就开始实际处理。在大多数情 况下,当事务处理组件请求连接时,就从资源分配器分配连接。资源分配器负责维护如数据 库连接和线程之类的可变资源的共享。资源分配器也向本地事务处理管理器注册连接。向事 务处理管理器注册连接的过程称为自动事务处理应征(Automatic Transaction Enlistment)。图 4-3 说明了当事务处理组件请求这些资源中的一个时发生的过程。 例如,当组件用 ADO(或 ADO.NET,就此例而言)打开与 SQL Server 数据库的连接时,请求 就进入在 OLEDB 框架内实现的资源分配器中。资源分配器查看传递给 ADO 的连接字符串, 从而确定这种类型的连接是否存在于资源分配器的共享池中。如果共享池中存在与所请求连 接字符串相同的连接字符串的连接(如同样的 SQL Server、驱动器或用户 ID),资源分配器就 向本地事务处理管理器注册连接并向客户返回该连接。如果连接不存在,就创建一个并加入 到共享池中, 2.事务处理管理器 事务处理管理器负责跟踪组件和资源管理工具间的活动。资源管理工具管理数据资源。资源 管理工具知道如何把数据存储到数据库中以及在事务处理失败时如何恢复数据。 在典型的 COM+事务处理中,至少包括两台机器:一台机器运行组件,另一台机器运行数 据库。每台机器都必须有事务处理管理器参与到 COM+事务处理中。对 Microsoft 产品,分 布式事务处理协调器(Distributed Transaction Coordinator)服务实现事务处理管理器。Windows 2000 和 SQL Server 发行时都带有这种服务。 实际事务处理开始时,作为组件运行于相同机器上的事务处理管理器被指定为协调事务处理 管理器。协调事务处理管理器负责初始化两阶段提交协议。 3.两阶段提交协议 一旦逻辑事务处理中的每个组件都已经完成其工作且它们的每个完成位都被设成真,COM+ 就对事务处理进行评价以决定是否应当提交或中止处理。如果每个对象的一致性标志都被设 成真,COM+就指示本地事务处理管理器提交实际事务处理。实际事务处理是通过两阶段提 交(Two-Phase Commit)协议提交的。 在第一阶段中,协调事务处理管理器询问附属事务处理管理器它们是否已经准备好提交事务 处理。每个附属事务处理管理器响应一个提交或中止事务处理的选择。提交事务处理的选择 就是向协调事务处理管理器提交,这预示着资源可以提交并没成永久的。事务处理管理器作 出的任何中止的选择都会使事务处理毁灭。 在第二阶段中,协调事务处理管理器记录从附属事务处理管理器作出的选择。如果所有附属 事务处理管理器都选择提交,协调事务处理管理器就向附属事务处理管理器发出提交资源的 指令。如果要中止事务处理,协调事务处理管理器就向附属事务处理管理器发出回滚事务处 理的指令。在这个阶段中,每个事务处理管理器都指示其本地资源管理工具提交或回滚处理。 通常,资源管理工具维护几种受保护的贯穿整个事务处理的日志文件以提供这种功能。偶尔, 该日志允许在系统失败的情况下重启事务处理。 图 4-4 展示了协调处理管理器在两阶段提交协议中经历的过程。 4.3 在 C#中编写事务处理组件 在这一节中,读者将学习编写一个从 ContextUtil 类获得其事务处理 ID 并将其与类名一起记 录到数据库中的事务处理组件。你将学会使用提供组件支持同步化(Synchronization)即时激 活(JITA)和方法的自动完成的属性。另外,还将学习把类注册到 COM+应用程序中。 4.3.1 ServicedComponent 类 通过本书,读者会非常熟悉 ServicedComponent 类。它提供所有的 COM+服务,例如事务处 理、对象共享和自动注册到 COM+应用程序中。任何希望使用 COM+服务的 C#类都必须从 该类继承。 ServicedComponent 类存在于 System 名称空间中。为了从自己的代码访问该名称空间,需在 代码中使用 using System 语句。正常情况下,必须建立包含希望使用的名称空间的装配件的 引用。C#编译器是足够友好的,可以自动为每个组件或应用程序包含引用,因为 System 名 称空间的使用非常频繁。 .NET 用名称空间组织实现相似或互补功能的类、接口和方法等。例如, System.EnterpriseServices 名称空间实现许多用来与 COM+进行互操作的属性、接口和类。 定义一个从 ServicedComponent 继承的 C#是相对简单的。 Namespace ComTransaction { using System; using System.EnterpriseServices; public class CRoot : ServicedComponent { // 在此添加方法 } } 上述的代码包括使 C#成为 COM+组件必须完成的三个重要任务: * 为四个组件定义名称空间—ComTransaction * 声明正在通过 using 语句使用 System 名称空间 * 使类成为公共的 * 通过 ServicedComponent 从 ServicedComponent 类继承 当这个类被安装到 COM+应用程序中时,该类是可以从相似的托管或非托管组件中使用的。 请记住第三章中提到的,如果想从基于 COM 的非托管客户使用.NET 类,它必须是公共的。 4.3.2 基于属性的编程方法 下一步,向组件添加方法。 清单 4-1 事务处理组件 Namespace ComTransaction { using System; using System.Data.SQL; using System.EnterpriseServices; [Transaction(TransactionOption.Required)] public class CRoot : ServicedComponent { public void LogTransactionId() { SQLConnection cnn = new SQLConnection(“server=localhost;database=Transactions;uid=sa;pwd=”); SQLCommand cmd = new SQLCommand(); cnn.Open(); cmd.ActiveConnection = cnn; cmd.CommandText = “insert into TransactionId values(‘CWorker1’, ‘” + ContextUtil.TransactionId + “‘)”; cmd.ExecuteNonQuery(); cnn.Close(); } } } 我们已经添加了名为 LogTransactionId 的方法。该方法创建命令和连接对象并将类名和组件 的事务处理 ID 加入到数据库中。ContextUtil 对象用来得到事务处理的 TransactionId。还有, 我们已经通过属性[Transaction(TransactionOption.Required)]声明该组件需要事务处理。 该属性模式与 COM+属性模式很不同。传统的非托管 COM+组件不把属性存储在代码段内。 属性存储在 COM+目录内。这导致它们的相应组件属性的混乱。.NET 框架的体系结构已经 通过将属性直接存储在代码内改变了这种模型。在编译时,属性被转换成元数据并存储在装 配件中。这允许组件及其属性存储在同一个文件中。 属性可以在装配件级应用。特别是对 COM+而言,可以将装配件的属性设成影响 COM+应 用程序的名称、描述,以及库或服务器应用程序中是否安装了类。ApplicationName 属性、 Description 属性和 ApplicationActivation 属性都处于 System.EnterpriseServices 名称空间,可 用来分别指定 COM+应用程序的名称、描述和库或服务器设置。 COM+应用程序既可以作为库应用程序安装也可以作为服务器应用程序安装。库应用程序作 为其客户运行于相同的进程中,但服务器应用程序则运行于其客户进程之外的宿主进程中。 Windows 2000 上的宿主进程是 dllhost.exe。 4.3.3 把类安装到 COM+应用程序 既然已经有了能进行某些工作的完整的事务处理组件,就可以将它安装到 COM+应用程序 中。有两种方式可将 C#类安装到 COM+应用程序中;在运行时用惰性注册或使用.NET Framework SDK 所带的 Regsvcs 命令行工具。 惰性注册省去了把组件手工安装到应用程序中所需的一些步骤。托管客户创建从 ServicedComponent 继承的类的实例时,.NET 运行库有足够的智慧知道类必须向 COM+注 册。当然,类一旦被注册,运行库就不会注册它。惰性注册允许跳过创建 COM+应用程序 并安装组件的管理负担。然而该功能有几点不利之处: * 惰性注册只对托管客户才能用。因为类没有提前注册,非托管客户不能在注册表中找到 类。 * 注册组件存在的问题直到运行时才能捕捉到。如果组件安装失败,客户就会遇到问题。 * 注册类的进程必须运行在能创建 COM+应用程序的管理权限之下。如果进程运行于最 终用户的安全上下文中,最终用户必须有管理权限。 Regsvcs 命令行工具把 ServicedComponent 类在执行前注册。它把装配件的文件名作为输入 并创建 COM+应用程序。装配件内的所有这些 ServicedComponent 类部被注册到相同的 COM+应用程序中。装配件的名称被应用为应用程序的名称,除非被用装配件级属性加以改 写。该工具有许多能够被传递进来的参数。表 4-2 包含一些较有用的参数,笔者已经在使用 该工具的过程中用过。 表 4-2 有用的 Regsvcs 参数 参数 描述 /c 强制安装 COM+应用程序 /fc 查找已经安装在 COM+中的应用程序 /reconfig 重新配置应用程序和组件。这类似于右击组件服务 (Component Service)控制台中的组件文件夹并单击 Refresh(更新)按钮 命令行工具和惰性注册都要经过一个四步的过程把类注册到应用程序中: 1. 载入装配件。 2. 生成并注册类型库(与第三章中描述的过程相似)。 3. 创建 COM+应用程序。 4. 读取类的元数据并施加适当的属性。 4.3.4 JITA、同步化和自动完成 读者已经学到类如何通过 Transaction 属性被声明成是事务处理的。声明 JustInTimeActivation 支持、Synchronization 支持以及 AutoComplete 支持的方式差别不大。所有这些属性都在 System.EnterpriseServices 名称空间中。在清单 4-2 中,使用这些属性设置了 CRoot 类及带有 这些属性的方法。 清单 4-2 使用 AutoComplete 的处理组件 Namespace ComTransaction { using System; using System.Data.SQL; using System.EnterpriseServices; [Transaction(TransactionOption.Required)] [JustInTimeActivation(true)] [Synchronization(SynchronizationOption.Required)] public class CRoot : ServicedComponent { [AutoComplete] public void LogTransactionId() // 以下是数据库工作 } } } JustInTimeActivation 属性有两个构造器。缺省构造器( 不带参数的一个) 把 JustInTimeActivation 支持设置为真,第二个参数带一个可使 JustInTimeActivation 支持可用 或不可用的布尔值。注意该属性是在类级别上应用的。Synchronization 属性与 JustInTimeActivation 属性相似,因为它们都有两个参数。缺省构造器把 Synchronization 支持 缺省设置为 Required。Synchronization 属性还带有一个可用来设置对任何要求级别(Disabled、 Not Supported、Supported、Required 或 RequiresNew)的支持的 Synchronization 枚举的参数。 最终如读者可能希望的一样,AutoComplete 属性被应用在方法级别上。如果方法生成例外, 处理就中正;否则,它选择提交。当把 AutoComplete 属性应用到方法时,就在 COM+目录 中应用 AutoDone 属性。 4.3.5 开发根和工作者对象 在这一章的前面,笔者为事务处理组件推荐了一种设计模式。该模式包括一个协调其他组件 工作的根对象。根对象启动事务处理而由子组件进行工作。现在扩充前面的例子来使用这个 设计模式。下面的代码示例展示了一个称为 ComTransaction 的实现根对象及两个工作者对 象的名称空间。 清单 4-3 根及工作者类 namespace ComTransaction { using System; using System.Data.SQL; using System.EnterpriseServices; [Transaction(TransactionOption.Required)] public class CRoot : ServicedComponent { public void LogTransactionId() { /* 向数据库中记录类名和事务处理 ID */ CWorker1 worker1 = new CWorker1(); CWorker2 worker2 = new CWorker2(); worker1.LogTransactionId(); worker2.LogTransactionId(); } } [Transaction(TransactionOption.Supported)] public class CWorker1 : ServicedComponent { public void LogTransactionId() { /* 向数据库中记录类名和事务处理 ID */ } } [Transaction(TransactionOption.Supported)] public class CWorker2 : ServicedComponent { public void LogTransactionId() { /* 将类名和事务处理 ID 记录到数据库 */ } } } 为了清楚起见,笔者略去了数据库逻辑。首先,请注意在 CWorker1 和 CWorker2 这两个对 象上,笔者已经将事务处理支持设为 TransactionOption.Supported。这允许每个工作者对象 都参与到根的事务处理之中。CRoot 对象使用这两个类就橡它们是名称空间中的任何其他 类,表 4-3 表示了在客户创建了 CRoot 对象并调用其 LogTransactionId 消息后作出的向数据 库的输出。 表 4-3 数据库输出 ClassName TransactionId CRoot FF208BBB-D785-4c59-9EA1-D3B6379822FB CWorker1 FF208BBB-D785-4c59-9EA1-D3B6379822FB CWorker2 FF208BBB-D785-4c59-9EA1-D3B6379822FB 请注意事务处理 ID 对每个对象都是相同的。这证明每个对象确实运行在相同的事务处理中。 4.4 小结 在本章中,读者学习了 ACID 要求(原子性、一致性、隔离性和持久性)如何驱动 COM+的事 务处理功能和基本事务处理管理器。这些要求强制基础体系在资源被使用时将它们锁定。通 常,如果没有正确理解这些锁定的影响,则它们对事务处理性应用程序来说就是瓶颈。设计 事务处理组件时基本的经验就是使它们能工作、选择、快速退出并释放其资源。 读者已经学习了.NET 框架引入的一些新概念。基于属性的编程方法对 COM+开发者来说并 不是新的,但把属性直接包括到源代码中则是新的。这项技术是把属性及其组件放在同一位 置的方便的方法。这项技术也有助于简化组件的部署。最后,读者看到了 ServicedComponent 类是如何通过继承在 C#用程序中使用的。ServicedComponent 类和 System.EnterpriseServices 名称空间的属性的结合给用户的 C#类以 COM+支持。 在第 5 章中,读者将学习.NET 如何为 COM+的基于角色的安全性功能提供支持。 第 5 章 安全性 本章内容包括: * 理解 Windows 的安全性 * 连线认证 * 在 C#使用 COM+安全性 COM+安全模型提供了声明性安全性和计划安全性的机制。声明性安全性允许使用记录到 COM+目录内的属性部署组件的时候配置组件。在运行时,这些属性影响组件接口和方法的 可访问性。计划安全性允许组件在运行时控制其可访问性。事实上,声明性的安全性和计划 安全性是相互联系的。对使用计划安全性的组件而言,必须在 COM+目录中设置适当的属 性。 在第四章中,读者遇到了.NET 开发模型中心主题中的一个:基于属性的编程方法。当在 C# 中编写事务处理组件时,应在代码中为组件指定所希望的 COM+目录设置。这是传统的为 开发 COM+组件方式的一个重要的转变。在传统的开发模型中,开发组件,将它安装到 COM+ 应用程序,并且设置适当的属性。用这种开发模型的问题是通常要依赖管理员恰当地设置属 性。.NET 开发模型把该功能(或责任)赋予开发者。正如本章所看到的,.NET 框架安全属性 可能是在 COM+目录中设置每个与安全有关的属性的有力工具。.NET 中基于属性的编程模 型有助于确保正确配置应用程序的安全设置,因为安全设置是它们在 COM+目录中注册时 从装配件的元数据中读取的。 在本章中,会遇到 COM+安全模型的各个方面:认证、授权、基于角色的安全性。还有, 会看到.NET 框架属性是如何在 C#中用于正确配置组件的。而见,还会学到从一台机器到另 一台机器的连线认证是如何执行的。然而,在探索这些细节前,理解 COM+中的安全服务 是如何从基础的 Windows 安全体系结构(Windows Security Architecture)加以调节的这一点很 重要。 5.1 理解 Windows 的安全性 如果曾经看到过 Windows 安全 API(Windows Security API)或它的任何有关文档,就会明白这 是一个多么复杂的话题。幸运的是 COM+程序员只需知道基本的问题。 任何安全系统,不管多么复杂,都涉及到一个两步的过程:认证用户和授予用户访问权限。 认证是用户证明其身份的过程。认证确认用户有权做其所要做的。 5.1.1 认证 当登录 Windows 环境下的计算机时,用户提供用户 ID 和密码证明用户是其所说的那个 用户。当然,还有更复杂的认证技术,如智能卡和各种生物技术。然而,其目的是在网络上 建立用户存在以便能够使用各种资源。 但用户登录其计算机时会发生什么呢?让我们分成几个阶段来说。用户按下 Ctrl-Alt-Del 键时启动登录序列。该键序列向网络登录服务(Net Logon Service)发送一个被认为是安全注 意序列(Secure Attention Sequence,SAS)的消息。网络登录服务启动所谓的图形识别与认证 (Graphical Identification and Authentication,GINA)的图形用户界面程序。GINA 负责显示登 录 Windows 工作站时看到的用户 ID 与密码对话框。例如,如果用户正在便用智能卡,GINA 负责在卡上读取数据。 一旦用户输入用户 ID 和密码(以及可选的要登录的域,GINA 就将证明信息传给本地安 全授权(Local Security Authority,LSA)。LSA 负责根据用户提供的用户 ID 和密码来鉴别 用户。实际上,LSA 还把用户的证明信息传给另一个称为鉴别包的组件。认证包将用户的 证明信息与数据库进行进行对照加以确认,从而鉴别用户或拒绝证明信息。如果用户正在用 Windows 用户 ID 和密码登录工作站,鉴别包将证明信息与本地安全帐户管理器(Security Accounts Manager,SAM)数据库进行对照确认。在域登录的情况下,鉴别包查找域的目录 数据库。目录数据库驻留于活动目录(Active Directory)网络中的域控制器上。目录数据库 与本地计算机的 SAM 数据库相似,其差别在于它保存着诸如网络拓扑结构的配置数据以及 架构数据之类的附加数据。架构起一种定义如用户帐号和计算机帐号的对象在日录里有什么 属性的模板的作用。 如果鉴别包能够认证用户,就向 LSA 报告成功。此时,LSA 做两件事:生成登录会话 和访问标识。登录会话把当前登录到工作站的特殊用户进行一对一的映射。通常,用户不能 有两个登录会话运行于同一台机器上。 访问标识在这里特别有趣。它把用户定义成系统中的用户。正是这种数据结构类型包含 了所有用户信息,如惟一标识用户的安全标识符(Security Identifier,SID)以及用户所属的 任何组的 SID。访问标识还包含代表用户当前登录会话的 ID。 Windows 安全子系统以与 COM 中使用全局惟一标识符(GUID)相似的方式使用 SID。 就像类标识符(SLSID)或类型 ID 跨时间和空间是惟一的,SID 也是一样。SID 不仅代表用户 账号也可被保护或识别的每个其他实体,如计算机和组。 一旦建立了用户的登录会话和访问标识.操作系统就启动 Windows 外壳。用户运行应用程 序如 Internet Explorer 时,就将访问标识从外壳的进程空间复制到应用程序的进程空间中。 这样就有两个访问标识,都指向用户登录会话。 5.1.2 授权 用户开始访问受保护的资源时(如文件或甚至是组件),Windows 对照被访问项目的安全描述 符(SD)检查用户的访问标识。SD 是有权访问资源的听有用户与组的列表。SD 定义哪个用户 具有对资源的哪些权限。 SD 是一个由两个项目组成的运行库数据结构:系统访问控制列表(System Access Control List,SACL)和目录访问控制列表(Discretionary Access Control List.DACL)。Windows 将 SACL 用于审核目的。SACL 对 COM+开发者的重要性不大.因为它在 COM+安全中不起作用, SACL 用于 Windows 审核,而不是 COM+计划安全审核,这会在本章的后面遇到。 DACL 把用户或组的 SID 映射为诸如读取、执行、删除等访问权限。在运行时,Windows 检查位于用户访问标识中的 SID 并将它们与 DACL 中的 SID 相比较。如果没有任何一个访 问标识中的 SID 与 DACL 中的 SD 相匹配,用户就得到熟悉的“Access Denied”(拒绝访问) 的消息。在 COM 的情况下,这可能产生 E_ACCESSDENIED HRESULT。 DACL 通过 Access Control Entries(访问控制入口,ACE)将用户和组的 SID 映射到访 问权限。每个 ACE 都包含一个用户或组的 SID 及其相应的访问权限。在任何 DACL 中,可 能有两种类型的 ACE:允许访问的 ACE 和拒绝访问的 ACE。Windows 搜索 DACL 中 ACE 的列表决定用户对资源的有效权限。如果 Windows 遇到一个可施加于用户访问标识的拒绝 访问的 ACE,用户就被拒绝访问而不管任何其他允许访问的 ACE,如果没有发现拒绝访问的 ACE.用户就被同意标识拥有的所有 ACE 的所有访问权限。 5.1.3 特殊帐号 安装 Windows 操作系统时,安装程序会创建许多特殊的组和用户帐号。COM+在不同时间 使用这些帐号中的某些,如 Everyone、Authenticated Users 和 Interactive User。其他帐号如 System 帐号由基础服务和有利于 COM+的子系统使用。 如果读者像笔者一样,经常使用Everyone帐号在开发应用程序的过程中避开恼人的Windows 安全特性。但该帐号实际是什么呢?Everyone 帐号是一个所有被认证用户所属的运 行时组帐号。实际上,登录到 Windows 时,Everyone SID 就被置于访问标识中。因为 Everyone 组是运行时组,所以没有用户会被明确置于其中。笔者在应用程序开发过程中就经常给 Everyone 组以完全访问权以绕过安全(当然是临时的)并将精力集中于不同的问题。但给 Everyone 组以对文件的完全控制权会发生什么呢?在这种情况下,文件的 DACL 被设置为 null。笔者在这里要明确 null DACL 和空 DACL 的差别。DACL 如果不包含 ACE 则为空。 任何试图访问具有空 DACL 资源的用户部不会授予访问权。另一方面,如果同样的用户试 图访问具有 null DACL 的资源.则会授予同意访问权. Windows 提供了另一个称为 Authenticated Users 的特殊组。该组类似于 Everyone 组也是 操作系统维护的一个特殊运行时组。任何被认证的用户都是该组的成员。Everyone 和 Authenticated Users 之间的关键不同点是 Authenticated Users 不能包含 guest。Guest 是不被认 证但仍能访问某些资源的用户。 Windows 安装时 Guest 账号缺省情况下不能使用。如果 Guest 被设为可用,就允许不能 被认证的用户访问资源。 Interactive User 是代表当前登录用户的另一个特殊运行库帐号。COM+将该帐号作为服 务器应用程序的缺省身份使用。运行于 Interactive User 账号下的 COM+服务器应用程序继承 了当前登录到工作站的用户的访问标识。通常,这一设置只在用于开发目的时运行得很好, 但在将组件移植到生产环境中时它可能引起真正的问题。在典型的生产环境中,没有人会登 录到为组件提供宿主的服务器。如果,没有人登录,Interactive User 就不可能代表任何人(情 况几乎总是这样)。在组件正在服务器上运行“熄灯号”的情况下,当组件被访问时会创建 一个虚拟桌面。假如把“Logon as Batch Job”和“Access this Computer from the Network” 权限给予 Interactive User,则 Interactive User 就继承桌面的权限。这一处理方法的问题以两 种方式出现。第一种,当管理员登录到控制台时,COM+应用程序开始以管理员的权限运行。 由于明显的原因,这不太好。第二,如果有一个没有这些权限的人登录到服务器控制台,这 就间接地给予此人这些权限。Interactive User 帐号实际是为运行于用户工作站上的作为 GUI 应用程序的一部分的 COM+应用程序准备的。Interactive User 帐号以这种方案工作有两个原 因:某人已登录,而又想要 COM+应用程序运行在该用户的证书之下。 5.1.4 扮演 到目前为上,读者已经遇到的安全性是在他或她的机器上单个用户与受保护的资源相互 作用的范围内出现的问题。然而,在分布式计算环境中,用户需要与远程资源交互。要使客 户做到这一点,他或她必须能够通过网络将其身份(或访问标识)传到目的机器上。此时, 目的机器必须能够使用客户访问标识的副本代表客户访问资源。这个过程就是“扮演”。 当客户连接到远程服务器时,创建组件的过程,例如部分“谈判”过程涉及服务器可扮 演客户的级别。扮演级别可以是四个值中的一个,如表 5-1 所示。 表 5-1 扮演级别 扮演级别 描述 匿名的(Anonymous) 服务器不能看到客户的访问标识。在这种情 况下,服务器不能识别或扮演客户 识别(Identify) 服务器以读取客户的 SID 并决定其访问权限 为目的可以读取客户的访问标识。然而,服 务器不能扮演客户 扮演(Impersonate) 服务器可以扮演客户并访问本地资源。服务 器不能通过客户的证书访问其他资源 委派(Delegate) 包括扮演级别允许的对标识的所有权限,还 可将客户标识传递到其他远程服务器 一定要了解与这些级别有关的几个问题。首先,客户在扮演级别中起主导作用。这不是 真正的谈判,因为服务器不能选择。第二,就像在本章后面将会看到的,扮演级别可以在 COM+软件包上设置。这种设置对客户用来调用进入 COM+应用程序中的扮演级别没有影 响。COM+应用程序的扮演级别设置只有在 COM+应用程序中的组件自己起客户的作用时才 会应用。 5.2 连线认证 具体实现远程组件并调用其方法就是远程处理。传统上,Microsoft 远程处理模型使用了分 布式 COM(Distributed COM,DCOM)。DCOM 使用远程过程调用(Remote Procedure Call, RPC)协议作为其基本的通信机制。然而,使用 DCOM 有两个限制。首先,客户和服务器都 必须运行 COM 运行库。通常,当应用程序的一部分必须通过 Internet 相互对话时(其中可能 有 Windows 客户与 Windows 服务器的对话),这不成问题。但当客户在 Internet 上时,这种 模型会被破坏。通常,不能保证 Internet 上的客户会被配置成使用 COM。另外,Internet 上 的客户可能必须通过防火墙或者甚至使用执行网络地址翻译(Network Address Translation, NAT)的网络路由器与服务器进行通信。在这里讨论克服通过防火墙和 NAT 表的 DCOM 问 题的细节是不必要的,但这些可能是要克服的特别的危险问题,特别是如果不希望它们出现 在应用程序的设计中。服务器“农场”可能给使用 DCOM 的组件提供障碍.服务器“农场” 或集群把几个相同的 Web 服务器或应用程序服务器连接成单个的虚拟服务器。客户根据集 群的 IP 地址连接到集群上。从客户来的请求进入集群时,其中一个可用服务器处理该请求。 为使 DCOM 可在集群上使用,客户的方法调用心须总是返回到具体实现组件的原服务器。这 就是所谓的“亲和力”。亲和力破坏了处理工作在组成集群的服务器间的均匀分配,从而减 少了集群的处理能力。 由于 DCOM 有这些问题,Microsoft 采取的远程处理趋势是通过 HTTP 发送方法调用。 远程数据服务(Remote Data Services,RDS)是 Microsoft 的第一个通过 HTTP 提供远程处理 的技术。RDS 是作为 Microsoft 数据访问组件(Microsoft Data Access Components,MDAC) 的一部分提供的。使用 RDS,可以具体实现远程组件甚至从客户对数据库进行远程 SQL 调 用。由于 RDS 经过了 Web 服务器,就可以克服亲和力引起的问题。但仍有客户依赖性,因 为客户必须安装有 MDAC。 远程处理中的当前趋势使用了一种称为简单对象访问协议(Simple Object Access Protocol,SOAP)的技术。SOAP 用 XML 对援用方法的请求进行编码,并用 HTTP 把那些请 求传到组件。SOAP 使客户绕过依赖性,因为 XML 分析器和可以形成 HTTP 请求的网络组 件是仅有的需求。XML 分析器和 HTTP 组件的实现对 SOAP 协议来说是完全不相关的。正 如将在第 9 章所见到的,.NET 远程处理(.NET Remoting)体系广泛使用 SOAP。 SOAP 和 RDS 都使用终点来具体实现组件并调用其方法。SOAP 通常使用 Active Server Page(活动服务器页) (或 ASP .NET Web Service 页)作为其终点。RDS 用 Web 服务器扩充 dll 作为其终点。其过程如下。客户在某种代理对象上进行方法调用,代理对象负责将方法调用 加以编码并向 web 服务器递交 HTTP 请求。HTTP 请求的一部分是到终点的 URL。当 HTTP 到达时结点时,Active Server Page 或扩充 dll 作为终点解开调用,具体实现 COM+对象, 并进行所请求的方法调用。 理解 IIS 中的认证 根据Microsoft采用的远程处理趋势,讨论Microsoft信息服务器(Microsoft Internet Information Server,IIS)如何处理认证和如何扮演客户是特别有用的。IIS 是 Microsoft 的 Windows 2000 附带的 web 服务器。本节解释 IIS 如何与 Windows 的安全性一起工作来对发出 HTTP 请求 的客户应用程序加以认证。 缺省情况下,IIS 运行于 System 帐号下。大多数内建的操作系统服务缺省作为该帐号运 行。当请求 Active Server Page,或在现在情况下,为组件请求方法调用时,就会进入 IIS。 IIS 分配一个线程来执行请求。实际上,为优化性能,IIS 为服务于请求维护一个工作线程池。 由于 Syslem 帐号具有如此之多的的权限,IIS 不太愿意在该帐号下运行这些工作线程。它更 多的是用客户的用户账号(假定客户已经被认证)或 IIS 安装时创建的缺省帐号。缺省帐号按 IUSR_+服务器名的串接的方式命名。例如,如果 Web 服务器名为 www1,缺省的用户帐号 名就为 IUSR_WWW1。 如果每个请求看起来都是从 IUSR 而来的,这就违背了使用 COM+安全性的目的。由于 COM+安全性源于 Windows 的安全性,必须有一种方式将远程方法调用中的用户的证书映 射到 COM+组件。答案就是保护可能被实现的终点。一旦终点用正确的用户和组账号以及 访问许可进行保护,IIS 就会提示用户输入其证书,如果用户提供了正确的证书并有权执行 对终点的请求,IIS 就分配一个工作线程来处理请求。工作线程通过称为扮演标识 (impersonation token)的特殊访问标识扮演客户。作为 COM+开发者,对扮演标识所有真正需 要知道的是它对组件进行认证就像扮演标识是客户自己的访问标识一样。 IIS 提供四种对用户进行认证的方法: 匿名访问(Anonymous access) 基本认证(Basic authentication) 分类认证(Digest authentication) 集成的 Windows 认证(Intergrated Windows authentication) 匿名访问应当用于不需要保护的页或终点。在这种情况下,IIS 用它的一个工作线程扮演 IUSR 帐号,从匿名访问提高一级的是基本认证,可以把这想像成 HTTP 的缺省认证方法。 基本认证的大问题是客户的用户 ID 和密码以明码传送到网络。而另一方面,分类认证和集 成的的 Windows 认证不以明码传送用户的证书。在所有这些模式中,客户都基于其证书生 成散列码。散列码在网络上发送并被服务器分析。这种技木比基本认证安全,但它引入了客 户依赖性。如果希望在 Internet 上运行应用程序,这可能是试图要避免的东西,分类认证只 在 Windows 2000 客户(工作站或服务器)需要与另一个 Window 2000 服务器对话时应用。分 类认证要求域服务器发布在数据传送过程中所使用的证书。集成 Windows 认证在至少有一 方运行 Windows NT 时使用。 5.3 使用 COM+安全模型 读者在本章的开始部分已经学到,.NET 框架的安全属性把应用程序的配置控制交到了开发 者手中。就像将在本节看到的,.NET 框架为每个 COM+目录中定义的声明性安全属性提供 了相应的属性。除非另有说明,本节中所描述的所有.NET 安全属性都可在 System.EnterpriseServers 名称空间中找到。 5.3.1 认证与授权 在 COM+中配置安全的第一步是设置认证级别。认证在软件包一级上既可启用也可禁用客 户认证的程度也是在软件包一级设置的。图 5-1 显示了服务器应用程平的 security(安全) 选顶卡。 该选项卡上的第一个属性是 Authorization。当选中时,该属性为应用程序中的所有组件 启用基于角色的安全性。对于服务器应用程序,该属性有使认证级别的 enforced(强制的)选 项可用的额外效果。 位于 Authorization 以下的属性是 security level(安全级别)。正如图 5-1 可以看到的,安全级 别是两个值中的一个,进程级或进程级与组件级。该属性的缺省值是在组件和进程级(组中 的第二项)上执行认正。如果希望在组件中执行计划安全就必须使用该值。COM+不会只在 进程级别(组中的第一个选项)上执行访问权检查的应用程序中为组件初始化安全.NET 框架 包括名为 ApplicationAccessControl 的属性。C#应用程序使用该属性设置应用程序的安全级 别。因为安全级别是在应用程序中设置的,ApplicationAccessControl 属性必须在装配件中定 义。ApplicationAccessControl 属性有几个对应于 Security 选项卡上的不同 COM+属性的属性。 要设置 COM+应用程序的安全级别,可用 AccessChecksLevel 属性。下面的代码示例展示了 如何使用该属性以及如何向装配件应用 AccessChecksLevel 属性。 图 5.1 服务器应用程序 Security(安全)选项卡 using System; using System.EnterpriseServices; [assembly:ApplicationAccessControl( AccessCheckslevel=AccessChecksLevelOption.ApplicationComponent ) ] public class SecuredComponent:ServicedComponent{ //在此实现方法 } 注意笔者使用了属性标志中的 assembly 关键词。这告诉 C#编译器属性是装配件级的。 在属性的声明内,笔者用 AccessChecksLevelOption 枚举把 AccessChecksLevel 属性设置为应 用程序和组件。在进程级执行访问权硷查可确保调用者有执行应用程序的许可。如果情况是 这样,调用者可以具体实现和使用应用程序中的任何组件。如果应用程序是服务器应用程序, 就要依赖于 dllhost.exe(COM+为服务器应用程序提供的宿主 exe)来执行访问检查。然而,如 果正在开发库应用程序,就要依赖其他宿主执行那些检查。如果正在使用计划的、基于角色 的安全这就不可能是一种可接受的方案。如果认证只在进程级上发生,COM+不会初始化组 件的安全上下文。没有安全上下文,组件就不能看到谁在试图访问组件以及用户可能是什么 样的角色。 Authentication 属性定义 COM+如何严格验证调用者的身份。调用者可以以六个可能的值中 的一个为基础加以认证,如表 5-2 所示. 表 5-2 认证级别 认证级别 描述 None(无) 客户调用进入组件时从来不进行认证。把认证级别设成 none 与把应用程序的认证关闭 具有相同的效果 Connect(连接) 调用者连接到应用程序时执行认证 Call(调用) 对每个方法调用都发生认证 Packet(包) 对包加以分析以确保所有包都已到达且它们都是从客户 来的。这是缺省的认证级别 Packet Integrity(软件包的完整) 对包加以分析,以便确认在传递过程中没有变化 Packet Private(软件包的私有性) 将包加密以在网络上传送 表中越往下,认证级别变得越严格或保护性更高,例如,Call 确保有比 Connect 更高的认证 标准;Packet 确保比 Call 更高的标准,以此类推。然而请记住,为应用程序选择的认证级 别越高,影响性能的可能性就越大。 .NET 框架中,认证级别是在装配件级上用 ApplicationAccessControl 属性定义的。该属 性控制认证级别的属性在逻辑上称为 Authentication 。 Authentication 属性用 AuthenticationOption 枚举把属性设置成相应认证级别中的一个,笔者已经将该属性加到前述 的代码示例中。 using System; using System.EnterpriseServices; [assembly:ApplicationAccessControl { AccessChecksLevel=AccessChecksLevelOption.ApplicationComponent Authentication=AuthenticationOption.Connect } ] public class SecuredComponent:ServicedComponent{ //方法的实现 } 认证级别在调用者和应用程序之间进行协商。谁请求了较高的认证级别,谁就请求成功。 例如,如果调用者请求 Packet,而应用程序请求 Packet Integrity,认证级别就是 Packet Integrity。这种行为允许每一方指定最小的认证级别。重要的是理解这种协商是发生在两个 进程之间。由于库应用程序不运行在自己的进程之中而是运行在调用者的进程之中,该属性 的设置不足库软件包的一个选项。 本章对 Windows 的安全性的讨论引出了扮演。请记住扮演是客户允许另一个进程或线程使 用其身份的级别。在 COM+中,服务器应用程序中的组件自己作为客户时就是应用扮演。 在图 5-1 所示的安全选项卡的底部。可以看到扮演列表框。四个扮演级别的每一个对 COM+ 应用程序都是可用的。 Anonymous(匿名) Identify(识别) Impersonate(扮演,缺省) Delegate(委派) 如果正在开发在实现基于角色的安全的其他应用程序中使用组件的服务器应用程序,就必须 选择不同于 Anonymous 的扮演级别。这种设置有效地从所试图使用的应用程序隐藏了调用 者的身份。 再一次,在.NET 中使用 ApplicationAccessControl 属性决定了在部署时的这种级别。 ImpersonationLevel 属性允许通过从 ImpersonationLevelOption 枚举指定其中的一个值来指定 扮演级别。如果向前述的代码添加 Delegate 扮演级别,就会得到下面的代码段。 using System; using System.EnterpriseServices; [assembly:ApplicationAccessControl { AccessChecksLevel=AccessChecksLevelOption.ApplicationComponent, Authentication=AuthenticationOption.Connect ImpersonationLevel=ImpersonationLevelOption.Delegate } ] public class SecuredComponent:ServicedComponent; //在此实现方法 ) 5.3.2 基于角色的安全性 角色把在应用程序中执行相似功能的用户和组组合石一起。角色是在应用程序级定义 的。基于角色的安全性可以在组件、接口甚至方法级上执行。服务器和库应用程序可以实现 角色并将它们应用到其组件中。 图 5-2 展示了笔者机器上的一个应用程序的角色展开列表。清注意,可以在一个应用程 序中指定多个角色。另外,在每个角色中,可以添加多个用户和组。例如,在 Author 角色 中,就添加了 Nick 和 HMI Author 组。 图 5-2 应用程序角色 一旦已经为应用程序定义角色,就可以将它们应用到组件、接口和方法。COM+中的安 全是向下继承的。如果为特殊组件定义角色,组件的接口就会继承角色。图 5-3 展示了名为 CAuthor 的组件的安全选项卡。可以看到,Author 和 Editor 的角色郡列出来了。然而,笔者 将对该组件的访问限定在 Author 角色中的用户。 图 5-3 CAuthor 组件的安全选项卡 因为 Nick 是 author 组的成员,所以他能够访问 CAuthors 组件。调用者访问组件时,COM+ 从 Author 角色中的用户和组创建 SD。在示例中,安全描述符包含 Nick 的 SID 以及 HMI Authors 组的 SID。如果 Nlck 的 SID 在 SD 中,他就有权访问。如果不在其中,就得到“Access Denied”(拒绝访问)的错误。 安全角色可通过 SecurityRole 属性由 C#类和装配件定义。该属性可以在装配件级上定义就 像 ApplicationAccessControl 属性一样。该属性也可应用于类。它应用于 C#类时,有把该角 色应用于类的效果。使用 SecurityRole 属性时,有两个构造器可供选择。第一个构造器带一 个参数:希望创建或应用到类的角色名。该构造器的缺省行为将特殊的 Everyone 组置于该 角色中。第二个构造器(笔者选择实现的一个)带像第一个构造器一样的角色名(字符串数 据类型)和指定是否将 Everyone 组添加到该角色的布尔型参数。除将 Everyone 组添加到角 色外,必须依赖管理员通过用 Component Services Explorer(组件服务资源管理器)把用户和 组添加到角色。 SecurityRole 属性不能为组件的启用认证。正因为如此,需要使用 ComponentAccessControl 属性。该属性只在类一级应用。缺省构造器(无参数)为组件启用认证。该属性还有一个带布 尔型参数的构造器。通过这个参数,既可启用认证(传入值“真”)也可禁用认证(传入值“假”)。 笔者已经将 SecurityRole 属性和 ComponentAccessControl 属性添加到 SecuredComponent 类,笔昔还添加了名为 customers 的新角色,并且使 Everyone 组有权访问这一组件。 using System; using System.EnterpriseServices; [assembly:ApplicationAccessControl ( AccessChecksLevel=AccessChecksLevelOpti on.ApplicationComponent, Authentication=AuthenticationOption.Connec t, ImpersonationLevel=ImpersonationLevelOpti on.Delegate ) ] [SecurityRole(“customers”,true)] [ComponentAccessControl] public class SecuredComponent:ServicedComponent{ //在此实现方法 } ComponentAccessControl 属性看起来可能有点独特。请主义,笔者没有为该属性包括圆括弧。 C#编译器将这解释为希望使用该属性的缺省的无参数构造器。 当此类被编译并向 COM+用 RegSvcs 工具注册时,就创建 customers 角色,并将 Everyone 组添加到该角色。实际上,这意味者任何能够被认证的人都被允许使用该组件。图 5-4 展示 了当把 SecuredComponent 类安装到 COM+中时 SecuredComponent 类的安全选项卡的样子。 图 5-4 SecuredComponent 类的安全性选项卡 5.3.3 理解安全性的作用范围 将组件部署到 COM+l 应用程序中时必须考虑几个问题。读者已经遇到了这样的几个问题。 例如,如果应用程序需要增强认证级别或扮演级别,最好别在服务器包中部署组件。另一个 问题涉及基础客户(用户的进程)调用进入组件且该组件也调用另一个组件等问题时可能产 生的调用链。COM+强制角色以直接调用者的 SID 为基础进行检查。现在考察一个例子看看 这是如何影响部署决定的。假设用户 Nick 打开浏览器,进入一个受保护的 ASP.NET 页的 URL。因为该页是受保护的,他被提示要输入用户 ID 和密码。Nick 成功地被认证进入该页 时,IIS 指派它的一个工作线程处理 Nick 所请求的这—页。该线程在 Nick 的访问标识的一 个副本下操作。工作线程在 ASP 页内遇到创建新的运行于服务器包中的托管 COM+对象的 代码段时,启动 dllhost 的一个实例(假设还没有已经在 ASP 页内运行的实例)。同样 dllhost 的实例正运行于普通用户 ID 之下,因为现在已经知道 Interactive 用户不是基于服务器的部 署的好选择,Nick 的工作线程试图访问组件时,会将 Nick 访问标识中的 SID 与组件 SD 中 的 SID 相比较。如果发现了两个匹配的 SID,就同意 Nick 访问。到目前为止,一切看起来 很顺利,真的吗?然而,如果 Active Server Page 正在使用的组件访问 COM+软件包中的另 一个组件,又会发生什么呢?答案是从 dllhost 进程的访问标识来的 SID 被用于第二个组件 上的角色认证。这种情况下,Nick 的访问标识就不会用于对第二个组件的认证。图 5-5 详细 表示了这个过程。 所有的一切集中到一点就是直接调用者可能是也可能不是实际的用户。在应用程序中调 用组件的所有用户帐号的列表就是调用链。理解调用链是如何影响安全的很重要,因为它可 能在不经意间导致得到或拒绝用户对组件的访问。如果在应用程序中进入这种情况并在调用 链中的某个用户处希望增强安全性,就必须有计划地去做。 幸运的是,.NET 提供了一个有权访问调用链中的每个用户的集合类。SecurityCallers 类 代表调用链中的每个用户。SecurityCallers 集合中的每一项都代表一个 SecurityIdentity 类。 该类包含如用户账号名和扮演级别的信息。要得到对 SecurityCallers 集合的访问权,必须调 用 SecurityCallContext 类的 CurrentCall 属性。SecurityCallContext 类的 CurrentCall 属性是返 回组件的安全上下文信息的静态属性。使用该属性,可以得到对 SecurityCallContext 的 Callers 属性的访问权。Callers 属性返回 SecurityCallers 集合的一个实例。在下面的代码中,笔者已 经把 SecurityComponent 类从原先的示例扩充为能够整个了解这些类是如何一起使用的。 [SecurityRole(“customers”,true)] [ComponentAccessControl] public class SecuredComponent:ServicedComponent{ public void ShowCallChain(){ SecurityCallContext sc.SecurityCallContext.CurrentCall; SecurityCallers scs=sc.Callers Foreach(SecurityIdentity si in scs){ console.WriteLine{si.AccountName}; } } } 笔者最后想要强调的一点是,希望定出 COM+中安全的作用范围。安全性是在应用程序边 界上执行的。在前面的情况下,Nick 已经被拒绝访问组件 B,因为调用链跨越应用程序的 边界这就强制对直接调用者(COMuser)的角色成员资格进行确认。如果组件 B 像组件 A 一样 是在相同的 COM+应用程序中,即便从技术上他不在指定给组件 B 的任何角色中,Nick 也 会被同意访问。如果碰到这种情况,如果调用者在所希望的角色中,按计划决定可能是有用 的。SecurityCallContext 类实现一种名为 IsUserInRole 的方法。该方法获取角色名和用户的 帐号名作为输入参数,并且如果用户在角色中返回真,而如果用户不在角色中则返回假。要 得到用户的帐号名,可使用 SecurityIdentity 类的 AccountName 属性。 [SecurityRole(“customers”,true)] [ComponentAccessControl] public class SecuredComponent:ServicedComponent{ public void ShowCallChain(){ SecurityCallContext sc=SecurityCallContext.CurrentCall; SecurityCallers scs=sc.Callers; Bool CustomerInCallChain=false; if(sc.IsUserInRole(si.AccountName,”customers”)){ CustomerInCallChain=true; break; } } if(!CustomerInCallChain){ throw new UnauthorizedAccessException(“no customers found in call chain”); } } } 在前面的例子中,如果在调用链的任何地方都没有发现客户,就产生 UnauthorizedAccessException 错误。这是在 System 名称空间中发现的一个异常类。.NET 运 行库在任何调用者无权访问组件、接口或方法的时候生成该异常。 5.4 小结 我们已经在本章中讨论了许多内容。安全性在所有的计算机科学中可能是最难的话题中 的一个。如果至少熟悉 Windows 安全体系如何实现这些服务,则理解基于角色的安全性如 何在 COM+中工作会变得容易得多。根据远程处理中的当前趋势,也就是方法调用的 HTTP 的使用(Web 服务),笔者认为讨论 IIS 如何认证用户以及用户的防问标识如何用在调用链中 的不同点是特别重要的。除此之外,笔者希望读者已经能够明白.NET 框架中的属性和类如 何帮助简化组件的开发和部署。 在第 6 章中,读者将学习 COM+事件模型。还将学习.NET 的事件模型以及两者的比较。 第 6 章 事件 本章内容包括: 理解对 LCE 的需要 理解 LCE 体系 在 C#中编写 LCE 组件 多年以来,基于事件的编程方法是图形用户接口应用程序主要的开发模型。在许多用户接口 应用程序中单击按钮或移动鼠标都会触发事件。产生事件时,应用程序的另一部分能够作出 相应的响应。当事件的来源、事件本身、对事件作出响应的代码片断引包含在相同的应用程 序中时,这种开发模型较为适用。这种类型的事件模型就是一种紧密耦合事件(Tightly Coupled Event,TCE)。正如将在本章看到的,这种开发模型在分布式应用中就不太适用。 因为 TCE 对分布式或基于服务器的应用程序的限制,COM+支持所谓的松散耦合事件 (Loosely Coupled Event,LCE)系统。在本章中,将深入讨论分布式应用程序需要 LCE 的原 因。通过检验.NET 事件模型并将其与 LCE 比较从而实现这个目的。同时还差考察 LCE 的 结构,其中包括 LCE 组件的限制和要求。如果不包括目录和框架的适当属性,对 COM 特 点讨论就不是完整的。 6.1 理解对 LCE 的需要 TCE 包括.NET 事件模型和 COM 连接点(COM Connection Point)事件模型,TCE 主要 用于基于用户界面的应用程序中。其中有一个程序包括事件的发生和消失的全过程。在 TCE 中,事件源(发布者)和处理事件的方法或组件(事件订户)被紧密结合在一起。这种模型 适用于用户界面应用程序,但当想在分布式环境中实现它时就有不足之处。TCE 用于分布 式应用程序时会遇到下列常见的问题。 发布者初始化时间时订户必须正在运行 在事件到达订户前没有办法过滤或截取 订户必须有发布者的明确知识 发布者必须有所有订户的列表 为理解以上的每一点,请运行.NET TCE 的一个示例。 6.1.1 .NET 事件结构 .NET 框架的事件模型与 COM+事件模型有些类似之处。.NET 事件模型有发送和接收事 件的类。发送事件的类可以大致地等同于 COM+事件中的发布者。可以以把接收事件的类 想像为 COM+事件的订户。 事件类必须有某种机制用于向事件接收类发送事件通告和相关信息,这是通过一种名为 委派的特殊类型的类实现的。委派是指向方法的管理指针,如果熟悉 C 或 C++中的函数指 针,就可以把.NET 委派想像成它们的近亲。 正如可以在下面的例子中看到的,委派是用 C#关键词 delegate 声明的,后跟返回类型、 委派名和参数。委派是在事件源和接收者类的定义之外定义的。可将委派用于将事件源与事 件接收者联系起来。事件源定义委派类型的一个成员变量。希望预订事件的事件接收者必须 实现与委派的对应方法具有相同特征表示的方法。在运行时,将把事件接收者提供的方法与 事件源定义的委派联系起来。一会就会清楚地理解这一点。 方法的特征表示指的是返回类型以及方法参数的数目和数据类型,在.NET 事件中,接收者 类实现的方法可以绐以任何合法的方法名。重要的仅仅是方法的特征表示要与委派方法的特 征表示匹配。 典型的委派的特征表示是一个 void 型返回值和两个参数:对象类型和事件参数类型。如果 开发 WinForms 或 ASP .NET 页,将看到如单击按钮或鼠标移动的事件的特征表示是一个带 对象类型和以事件参数类型为参数的 void 型返回值。建立该 void 型函数的理由是许多接收 者可能正在从事件源接收事件。在这种情况下返回值是基本没有用处的,因为返回值只代表 被通报的最后的接收者。从其他接收者返回的所有其他返回值都丢失了。第一个参数以及对 象类型代表产生事件的类的一个实例-由于类型不是必然能够提前得知,该参数类型就是 SystenObject。通常,第二个参数可以是一个包含事件的事件参数的类。例如,在鼠标移动 事件中,事件参数可能包含鼠标的 x 或 y 轴的位置。 清单 6-1 展示了一个名为 EventSource 的事件源类和一个名为 EventReceiver 的接收者类 的示例。事件类使用了委派 MyEventHandler 用来定义名为 EH 的委派事件成员类型。关键 词 event 是用来声明委派类型的成员的 C#结构。该委派实例被捆扎到名为 EventMethod 的 EventReceiver 类的相应的事件方法。注意 EventMethod 类的特征表示与该委派类的特征表 示是匹配的,但其特征表示未被定义为委派类型。 清单 6-1 事件源和事件接收者类 namespace MyNetEvents { using System; public delegate void MyEventHandler(string msg); public class EventSource { public event MyEventHandler EH; private void OnEvent(string msg) { EH(msg); } public void FireEvent() { OnEvent{”hello”}; } } public class EventReceiver { public void EventMethod(string msg) { Console.WriteLine(“EventReceiver:”+msg); } } public class CMain { public static void Main() { //具体实现时间的目的类 EventReceiver er=new EventReceiver(); //具体实现新的时间源 EventSource es=new EventSource(); //与事件联系在一起 es.EH+=new MyEventHandler(er.EventMethod); //触发事件 es.FireEvent(); } } } 在具体实现两个类的实例后,就可以通过具体实现 MyEventHandler 类的新实例把 er.EventMethod 方法捆扎到 EH 委派。.NET 运行库为委派提供一个单参数构造器。正如可以 看到的。构造器带一个与委派有相同特征表示的方法的引用。委派的新实例被用+=操作符 捆扎到 EH 实例。最后,调用事件源类的实例上的 FireEvent 方法。FireEvent 方法是相当直 接的。它调用事件源的私有的 OnEvent 方法。正常情况下,只在某些条件变为真或方法 FireEvent 中发生了某些事件的情况下才调用 OnEvent 方法。OnEvent 方法是私有的;通常 情况下,只有在事件源类中的某些条件变为真的情况下才想让事件发生,并不需要事件源类 的客户来触发事件。 OnEvent 方法通过凋用 EH 委派触发事件。.NET 运行库负责提取将它们捆扎到事件的类的 列表并负责调用它们的事件。在这种情况下,运行库调用的方法是 er.EventMethod。 6.1.2 将 TCE 事件与 COM+的 LCE 比较 既然了解了紧密耦合事件是如何在在.NET 框架中工作的概念,现在要着重讨论在这一节的 开头提到的观点。第一点就是订户必须在发布者或事件源初始化事件前运行于 TCE 模型中, 前述的代码示例中,在能够将事件接收者的事件方法捆扎到委派和接收事件前,必须具体实 现事件接收者类的一个实例。在 COM+事件模型中,接收事件的类(称为订户)不必在 事件发生前被激活。 第二点是在事件被发送到接收者前无法过滤事件。.NET 运行库管理接收者列表并调用其事 件方法。不幸的是,它不提供任何防止向接收者进行调用的机制。在本章的后面,读者将遇 到 COM+,提供的过滤事件通报的方法。 实质上,最后两点表明,订户和发布者必须相互有明确的知识。在前述的例子中,订户 类是 EVentReceiver 类,而发布者是 CMain 类的 Main 方法。Main 方法本身具体实现事件接 收者类并且知道哪个方法必须用来捆扎事件。这要求明确了解接收者方法以及接收者的数目 与类型。COM+事件模型将这种信息保存在事件发生时 COM+运行库本身要访问的 COM+ 目录中。因为有这种服务,发布者不必知道它的订户是谁或者必须调用事件的哪个方法。 6.2 LCE 结构 COM+事件系统由于许多理由是松散耦合的。首先,发布者和订户不必相互了解。遇常, 发布者不必知道订户是谁或者有多少订户。订户能够在任何时候进入和退出而不会打断应用 程序。第二,在发布者初始化事件时订户不必是活动的。正如很快就会看到的,发布者初始 化事件时,COM+代表发布者激活订户。这些特点与在本章第一节中描述的紧密耦合事件体 系很不相同。 COM+事件系统是一种将事件传播到预订组件的 Windows2000 服务 使用 LCE 的应用程序由四个实体组成 发布者 事件类 订户 事件 发布者通过激活事件类井调用具方法启动进程。发布者调用的每个方法都代表一个单个 的事件。事件类被橄活时,COM+向其目录查询感兴趣的订户的列表。一旦确定了这一列表, COM+就将向事件类作出的方法调用重定向到订户类。图 6-1 说明了这个过程。 作为一种准则,发布者不能决定订户被通告的顺序。COM+事件系统作出了这种决定。 在本章的后面,将看到几个影响订户被通告顺序的技术。 订户需要实现发布者向事件类调用的方法,以便使这一切得到顺利进行。事件类不必实 现订户选择预订的方法。实际上,即使方法被实现了,发布者初始化事件时它们也不会被调 用。虽然发布者认为它有对事件类的简单引用,但它还是与 COM+事件系统进行对话,然 后由事件系统对订户进行调用。 为使订户预订事件类的事件(方法),订户必须实现事件类支持的接口。订户不必实现 事件类的所有接口,只需执行包含用户希望从其中接收事件的方法的接口。订户和事件类的 接口标识符(GUID)也必须是相互一致的。 在 LCE 系统中事件类必须满足某些标准才能正确地发挥作用。准则之一是类不能包含对其 任何方法的实现。如果事件类有不止一个订户,事件类的方法就一定不能包含任何输出参数。 也就是说,事件类不能包含作为参数同发布者返回一个值的方法。如果考虑一下,有两个理 由使这条规则是有意义的。首先,如果有多个订户,每个都正到达发布者前改变输出参数的 值,如何知道返回的值是否正确?发布者不可能知道。第二,如果发布者不必知道其订户, 就公然不关心订户的动作或返回值。 事件类必须只返回成功或失败的 COM HRESULT 值。如果应用程序全部都运行在托管 代码中,这不是需要特别关心的事情。然而,如果正在编写非托管订户所使用的托管事件类 就需要道这一点。 6.2.1 理解预订 COM+支持两种类型的预订:永久的和暂时的。永久预订保存在 COM+目录中,直到被 物理清除为止,对托管或非托管代码,这类预订都必须通过 Component Services Explorer 插 件来添加。.NET 框架不提供添加预订到 COM+目录的属性。 COM+ Administration API 是基于 COM 的并支持 COM 目录的有计划管理。所有 Component Services 插件的功能性(及其他)都可通过 Admin API 重新生成。 另一方面,暂时预订只在订户组件的寿命期内存在。当 COM+事件系统重启或所运行的机 器重新操作时,暂时预订也就不存在了。暂时预订必须在运行时用 COM+ Admin API 添加。 与永久预订不同,暂时预订通过发出对 COM+目录中已具体实现的组件的引用而添加。 还要了解另一种类型的预订。在用户登录到系统时,Per User 预订允许创建预订。当用户注 销时,这些预订是不可用的。这些类型的预订只在发布者和订户在同一机器上时才能工作。 6.2.2 COM+属性 LCE 组件应用程序的 COM+属性实际集中在事件类上。订户和发布者(如果发布者是 COM+组件)不需要设置事件系统的属性。读者将在这一节中考察事件属性及其值。在本章 的最后一部分,将看到这些属性是如何在.NET 框架中实现的。COM+事件属性有如下这些: Fire in Parallel(并行触发) Allow in-process subscribers(允许进程内的订户) Publisher ID(发布者的 ID) 1. 并行触发(Fire in Parallel) 并行触发属性影响事件系统通知用户的方式。图6-2展示的是在Component Services Explorer 中的 Advanced 选项卡中被选中的该属性。选中该属性后,COM+就全在同一时间 通 知订户。正常情况下,事件系统以串行方式通知订户。缺省情况下,该属性未被远中。 图 6-2 Component Services Explorer 中的 Fire in Parallel 属性 该属性对应用程序的性能有正面影响。因为 COM+初始化多个线程通知订户。如果该属 性没有选中,事件系统以一对一的方式通知订户,在调用下一个之前,它会等待每个订户返 回。 2. 允许进程内的订户(Allow in-process Subscribers) 缺省悄况下,Allow in-process(允许进程内的)属性是选中的。关闭该属性时,COM+ 不允许订户在发布者的同一个地址空间运行。即使订户被配置为库软件包,COM+也会为订 户创建一个新进程。这种缺省行为的理由是基于对安全的关心。因为发布者不必了解订户。 它就不能信任订户在其进程内能正确行动。然而,如果正在编写发布者应用程序,并觉得可 以信任事件类的订户,就可以选中 Allow in-process Subscribers 属性。选中该属性,就是以 稳定性换取性能。该属性设置可在事件类属性的 Advanced 选项卡上找到。请参考图 6-2 查 看组件眼务资源管理器中的该属性。 3. 发布者 ID(Publisher ID) Publisher ID 属性(也可从图 6-2 中看到)订户提供使用事件类的另一种方式。读者已 经 探索用户使用事件类的一种方式了:事件类的 CLSID。Publisher ID 允许使用对用户更为友 好的方法来创建预订。要明白该属性是应用到事件类的,而不是应用到发布者。其名称可能 容易产生误导,不要产生混乱。 6.2.3 控制订户的通知顺序 COM+提供两种方法,用来影响事件通知用户的顺序:发布者过滤和参数过滤。发布者 过滤实现起来比参数过滤要复杂一些,但它允许更高精度的控制。 1. 发布者过滤 发布者过滤是一种允许发布者指定当事件触发时应该使用的过滤组件的技术。过滤组件 负责作出应当将事件通知给哪个用户以及以什么样的顺序通知的决定。 发布者使用 COM+ Admin API 设置事件类的 PublisherFilterCLSID 属性。这是一种接受 过滤组件的 CLSID 的读/写属性。过滤仅仅是另一种既实现 IMultiPublisherFilter 接口也实现 IPublisherFilter 接口的 COM+组件。 具体实现事件类时,事件系统查看 PublisherFilterCLSID 属性并实例化相应的过滤组件。 当发布者在事件类上触发事件时,事件系统把控制传给过滤对象。过滤对象的责任是向订户 转发事件。过滤组件通过提取用户集并向每个订户触发事件实现这一点。当然,过滤组件可 以选择不触发特殊用户的事件。过滤组件触发事件时,事件系统接手并对订户初始化事件。 2. 参数过滤 参数过滤与发布者过滤结合使用,或单独使用。与发布者下同,订户组件实现这种技术。 通过在预订属性中的选项,订户可以定义能够影响事件系统是否触发方法的标准。 图 6-3 展示了订户组件的预订属性对话框。Option 选项卡上的 Filter Criteria 对话框定义 预订的参数过滤。该特定参数过滤定义,如果名为 Symbol 的参数等于“MSFT”而且 Price 参数不等于 60 时启用预订的规则。如果过滤标准中定义的条件评估为真,就为该订户处理 事 件。此外,如果定义了过滤标准且预订是为该接口的所有方法作出的,标准就被应用到所有 方法。例如,如果定义名为 Symbol 的参数且该参数没有在接口方法之一中找到,事件就会 失败,可在本章的最后一节中看到这是如何上作的。 图 6-3 用于预定的过滤准则对话框 用于准则字符串的操作符是相当简单化的。操作符包括等号和逻辑操作符如 AND、OR 和 NOT。如果需要更复杂的策略,就必须考虑使用发布者过滤。 参数过滤对通加订户的顺序没有直接的影响。参数过滤可用来确定,对于基于参数值的 运行时的特定预订是否要触发事件。该技术对决定哪个发布者得到通知和可能以什么顺序通 知有副作用。 6.3 在 C#中编写 LCE 组件 在本章的最后部分,读者将看到如何编写不同的事件和订户类。第一组类是一个编写使 用 LCE 的组件的介绍。这个例子由一个使用对事件类的静态预订的用户组件组成。第二和 第三个例子使用了 COM+的一些其他功能,即对象共享和事务处理。对每个例子来说,C# 控制台应用程序是发布者。 6.3.1 第一个 LCE 组件 清单 6-2 展示了一个包含订户类和事件类的简单名称空间。正如可以看到的,用户类是 MySubscriberClass,而事件类是 MyEventClass。这些类中的每一个都实现一个名为 IMyEvents 的接口。IMyEvents 接口包含一个方法(PrintName—用来输出调用事件所在类的名称。在 这种情况下,这—方法或事件的输出就是用户类的名称。 清单 6-2 一个简单的 LCE 组件 using System.Reflection; [assembly:AssemblyKeyFile(‘mykey.snk’)] namespace MyEvents { using System; using System.EnterpriseServices; [EventClass] public class MyEventClass:ServicedComponent,IMyEvents } public void PrintName(){} ) public class MySubscriberclass:ServicedComponent,IMyEvents { public void PrintName() { Console.WriteLine(“MyEvents.MySubscriberClass”); } } public interface IMyEvents { void PrintName(); } } 开始的两行应当看起来很熟悉。这两行定义了 AssemblyKeyFile 属性,以便可把该装配件安 装到全局装配缓存(GAC)中。虽然把这些类型的装配件安装到 GAC 中并不是严格要 求的,但这通常有助于部署应用程序。把配件安装到 GAC 中对 LCE 组件比对其他组件更 有意义,特别是对那些包含订户组件的装配件。由于发布者不必了解事件的用户,假设能够 把订户装配件作为发布者或事件类部署到相同目录就没有意义。 在 MyEvents 名称空间的声明的下面,可以看到事件类的定义。MyEventClass 事件类用 System.EnterpriseServices 的 EventClass 属性加以修饰。EventClass 属性告诉安装实用工具作 为事件类而不仅仅是通常的组件来安装。 定义的下一个类是名为 MySubscriberClass 的订户类。这个类只是一个实现共享接口 IMyEvents 的普通的 ServicedComponent 类型。当这个类编译和注册时,就可为事件类创建 预订。事件类的预订必须手工创建;没有支持将预订添加到事件的属性。 最后,定义了名为 IMyEvents 的接口。订户和事件类郡实现该接口。这一接口是真正把 事件类和订户结合到一起的粘合剂。 编译和注册这个装配件的方法同任何其他包含 ServicedComponents 的装配件一样。根据属 性 EventClass,Regsvcs.exe 工具明智得足以了解事件类必须作为事件类安装而不能作为通常 的 COM+组件安装。图 6-4 展示了这些类注册后在组件服务资源管理器中的外观。注意 IMyEvents 接口被列在每个组件的“Interfaces”文件夹之下。 图 6-5 展示了事件类的 Advanced 选项卡。笔者在此包括这一图示,就打算向读者展示该事 件类是确实作为事件类安装的。注意缺省情况下 Allow in-process subscribers 属性已经被设 定了。 图 6-4 订户和事件类的接口列表 图 6-5 MyEventClass LCE 属性 6.3.2 用组件服务资源管理器创建预订 既然已经有了事件类和订户组件,下一步就是创建预订。在图 6-4 中,每个类都有名为 Subscriptions 的文件夹。要创建预订,可右击订户组件的 Subscriptions 文件夹,然后单击 New Subscription(新预订)命令。这会启动 COM+的新预订向导(COM+ New Subscription Wizard), 如图 6-6 中所见到的一样。该图展示了订户类支持的所有接口。其中惟一感兴趣的接口是 IMyEvents。 图 6-6 COM+的新预订向导 创建预订的下一步是选择事件类。选定用于事件预订的接口后,就要选择要预订的事件 类。图 6-7 展示了作为支持 IMyEvents 接口的惟一可用的事件类列出来的 MyEventClass 事 件类。注意 Details 复选框已经被选中。选中该框时,就能够看到 CLSID 和对事件类的描述。 添加预订的最后一步是给预订命名,也可选择性的将其启用。在图 6-8 中,已经选中了 “Enable this subscription immediately”复选框。除非该框被选中,事件是不会被发送到这个 订户组件的。预订的名称只是人们起的一个对用户友好的名称。 图 6-7 选择事件类 图 6-8 启用预订 一旦创建了预订,就能够看到预订已在 Subscriptions 文件夹中列出来了。图 6-9 展示了 列在订户类的 Subscriptions 文件夹中名为 MySubscription 的新预订。 图 6-9 订户组件的预订列表 6.3.3 .NET 框架的 EventClass 属性 现在来详细讨论 EventClass 属性。正如在前面已经学到的,该属性是从 System.EnterpriseServices 名称空间来的。表 6-1 列出了该属性支持的属性。 表 6-1 EventClass 属性的属性 属性 数据类型 描述 AllowInProcSubscribers Bool 该属性允许或不允许订户在 有发布者的进程中运行 FireInParallel Bool 如果设置成真,该属性允许事 物系统以非同步方式通知事 件的用户 PublisherFilter string 该字符串由发布者过滤类的 全局惟一标识符(GUID)组成 如果得到了前述的事件类并定义了这些属性,就可获得与下面的代码相似的内容。为了 清楚起见,这里只包括了事件类的代码。 [ EventClass ( AllowInProcSubscribers=true FireInParallel=true ) ] public class MyEventClass:ServicedComponent,ImyEvents { public void PrintName(){} } 一旦该类被编译并注册,就可通过观察事件类的 Advanced 选项卡看到属性被应用。图 6-10 展示了 Fire in parallel 和 Allow in-process subscribers 复选框已经被选中的情形。 图 6-10 EventClass 属性使 LCE 属性可用 6.3.4 与事件一起使用事务处理 在本章的最后一节,将学习编写一个发布者、事件类和使用事务处理的订用户。这一节 的目的是演示与事件一起使用 COM+的其他服务。 为演示如何能够与事件一起使用事务处理,请修改前述的代码例子使其包括一个名为 MyTransactionRoot 的事务处理性 ServicedComponent 类。该类负责启动事务处理(通过 TransactionOption.Required 属性)并通过在事件类上调用 PrintTransactionId 方法初始化事件。 这一方法输出当前事务处理的事务处理 ID。在事务处理根组件启动事件之前,它输出自身 的事务处理 ID。所有类和共享接口的定义列于清单 6-3。 清单表 6-3 与事件一起使用事务处理 [EventClass] public class MyEventClass:ServicedComponent,IMyTransactionEvents { public void PrintTransactionId(){} } [Transaction(TransactionOption.Supported)] public class MySubscriberClass:ServicedComponent, IMyTransactionEvents { public void PrintTransactionId() { Console.WriteLine(ContextUtil.TransactionId.ToString()); } } [Transaction(TransactionOption.Required)] public class MyTransactionRoot:ServicedComponent { public void StartTransaction() { Console.WriteLine(ContextUtil.TransactionId.ToString()); MyEventClass ec=new MyEventClass(); ec.PrintTransactionId(); } } public interface IMyTransactionEvents { void PrintTransactionId(); } 在这个例子中,处理根组件 MyTransactionRoot 起发布者的作用。由于这个组件总是在 事务处理中被激活,所以事务处理被传播到订户组件。 实际上,如果编写具体实现事务处理根组件实例的控制台应用程序并调用 StartTransaction 方法,可以看到类似下面的输出。 654675f8-8b76-45f8-9d70-d17bc59046ad 654675f8-8b76-45f8-9d70-d17bc59046ad 正如可以看到的,根组件和订户组件都显示相同的事务处理 ID,这意味着两者都运行在同 一个事务处理中。 这里最微妙的问题可能是缺少事件类的声明性事务处理支持。注意这个类没有声明为事务处 理的。由于事件系统(不是事件类)把事件转发到订户,事件系统能够把事务处理从根 传播到订户。当然,如即时激活和处理属性行为的所有其他规则都会应用到发布者和订户。 6.4 小结 本章介绍了.NET 事件模型和 COM+事件模型。试图理解事件工作原理可能有点棘手,但不 要气馁! 这两种类型的事件之间不是对抗的模型而是相互补充的模型。实际上,.NET 事件模型在整 个.NET 用户接口应用程序)如 WinForms 和 ASP.NET 页面)中大量使用。每件事情都有 其时间与地点。这确实是有关事件的真实情况。紧密耦合事件适用于诸如.NET Windows Forms 应用程序和 ASP .NET 应用程序一类的应用程序。然而,分布式应用程序则会从 COM+ 提供的松散耦合结构中得到更大的益处。本章比较了 COM+ LCE 结构和在.NET Windows Forms 中使用的 LCE 结构。此外还演示了 System.EnterpriseServices 名称空间的属性和类如 何提供编写自己的 LCE 事件类和订户的方法。到现在为止,读者应当了解如何实现自己的 事件类和订户以及在何时与何地使用事件类和订户。 第 7 章 对象共享 本章内容包括: 理解对象共享 对象共享的要求 C#中的对象共享 当笔者第一次听说.NET 框架将支持对象共享时,激动得说下出话来。以前,对象共享是属 于 C++程序员的范畴。现在情况不同了!随着.NET 和 ServicedComponent 的引入,对象共 享现在进入了 Visual Basic 开发者。当然还有 C#开发者的工具库中。 在本章中,读者将学习对象共享到底是什么以及可共享的对象必须满足什么样的要求。正如 将会看到的,对象共享应当不是对每个对象部是可实现的。然而,在某些情况下,对象共享 可大大改进可伸缩性。 7.1 理解对象共享 简单地说,对象共享就是共享同一个 CLSID 预先实现的对象集。所谓预先实现,指的 是对象构造器已经被调用并已经为客户使用作好准备。对象共享是同质的,因为每个对象都 是相同对象的一个新实例;所以,共享中的每个对象都有相同钓 CLSID。图 7-1 展示了来自 于组件服务资源管理器的托管组件的 CLSID。 图 7-1 ServicedComponent 使用的 CLSID 附带说一下,Regsvcs 注册工具自动生戍 CLSID。Regsvcs 根据类方法的特征表示、装配件 的版本和装配件的强名(如果有的话)的混合为基础生成 CLSID。通过改变装配件的版本 创建井安装两个互不相同的类是可能的,如图 7-2 所示。 图 7-2 ServicedComponent 类的两个 CLSID 虽然这两个类的不同之处仅仅在于其版本号(1.0.1.1 和 1.0.1.2),但每个类都得到了自 己的 CLSID。假设这些是共享组件,COM+就会维护两个互不相同的共享。 重载 CLSID 的自动生成是可能的。System.Runtime.InteropServices 名称空间提恭可用于 修饰类、装配件、接口、枚举、结构或委派的名为 GuidAttribute 的属性。下面的代码示例 说明该属性的用法。 using System; using System.EnterpriseServices; using System.Runtime.InteropServices [GuidAttribute(“24B7B5C4-CBEA=4668-AF67-1E3D44F87A68”)] 这个 GuidAttribute 属性有一个以字符串形式 GUID 为参数的构造器。Visual Studio 有 一种叫做 GuidGen.exe 的实用工具可以用来创建独一无二的 GUID。 .NET 框架中的大多放属性允许在每个属性名的后面省略“Attribute”。通常,对 GuidAttribute 属性这不一个好主意。System 名称空间包含 Guid 结构。如果正在使用 System 名称空间,而且如果正在实观 ServicedComponents,就会陷入命名冲突。 7.1.1 何时使用对象共享 那么何时使用对象共享呢?如果它能提高可伸缩性,难道应该将其用于每个 ServicedComponents 吗?然而,这并不准确,有几条规则有助于决定何时使用对象共享: 当对象需要获取如数据库连接之类的紧缺资源时使用对象共享 当方法调用进行了少许工作,然后退出并使对象不活动(也就是把完成位设置为真)时使 用对象共享 当需要限制资源的并发连接数目时使用对象共享 当方法完成所花时间比构造对象所花的时间多时不要使用对象共享 对保存着客户状态(方法调用之后)的对象不要使用对象共享对象共享的目的是分摊跨越 多个客户的对象初始化的成本。创建新对象的第一个客户会遭受性能降低,但只要客户释放 对象,对象就返回到共享。由于不必等待对象的构建,需要 对象实例的下一个客户就得到第一个客户创建的实例。这是前面的列表中第一条规则的基 础。图 7-3 说明了该规则背后的概念。 前述列表的第二和第三条规则是密切联系的。如果相对于在构建对象的过程中完成的工 作量来说,组件只实现完成少量工作的方法,对象就是共享的较好候选者。换句话说,如果 组件完成的大多数上作对任何客户都是通用的,在对象构造器中进行下依赖于客户的工作就 有意义。这就允许组件的方法做相对少量的工作。而且,这就有机会把在构造器中执行的不 领依赖于客户的工作在多个客户上分摊。 在某些情况下,使用对象共享来限制可以同时访问给定资源(如数据库)的客户数量可 能是有意义的。例如,如果只同时有 100 个连接许可,就可以把对象共享配置成只允许 100 个对象进入其内。当然,这里的诀窍是确保客户只能通过共享中的组件访问数据库。 进入本章后面的要求一节时,可以看到前述列表中的第五个规则是如此的重要。有许多 理由认为可共享组件可能是危险的。 7.1.2 对象共享的属性 对象共享有三个既可使用组件服务资源管理器也可使用 System.EnterpriseServices 名称空 间的叫 ObjectPooling 属性加以定义的属性。这些属性如下: 最小共享容量(MinPoolsize) 最大共享容量(MaxPoolsize) 创建时限(CreationTimeout) COM+应用程序(可以是库或服务器应用程序)启动时,COM+具体实现最小共享容量 所要求的数量的组件。当客户请求到达时,这些最初的少量对象就为客户眼务。例如,如果 最小共享容量是 2,就由共享中的对象为前两个请求服务。对象的额外请求到达时,COM+ 就创建新实例直到达最大共享容量。一旦已经创建了最大数目的对象,客户请求就排队, 直到有一个对象变为可用为止(附带说一下,可以使用 MaxPoolSize 属性来控制对一个数据 库可以有多少同时发生的连接,就像在前一节讨论的一样)。客户等待向其返回一个对象. 直到已经达到创建时限为止。对象的创建请求可能因为下列理由而超时: 对象构建时间比创建时限的限制长 客户请求处于队列中的时间比时限值长 图 7-4 演示了 COM+如何使用这些属性来控制共享。 通常,决定这些设置的优化级别是一个反复试验的过程,最终会发现适用于应用程序的 设置。尝试决定最小和最大共享容量及创建时限值时有一些要考虑的事情。 方法返回需要多长时间 构建对象需要多长时间 在最繁忙的时间内能够期望有多少创建请求 在两方法调用之间客户能做其他工作吗 笔者强烈建议进行某些测试以决定构建对象需要多久以及运行最耗资源的方法调用需要多 久。本质上,至少需要把创建时限设置成在可能最差的环境下构建一个一个对象所需的时间。 应当期望每个客户请求不需要构建新对象。大多数情况下,应当期望由已经构建好并已添加 到共享中的对象为请求服务。在启动应用程序和共享填满最大容量之间总是存在一段时间。 在这段时间内,需要构建新对象,客户必须等待。当考虐创建超时的持续时间时要记住这一 点。 前面列表中的第三个问题应当影响对最大共享容量的考虑。需要足够可用的对象以便能 够在繁忙期间满足请求。第四个问题也应当影响最大共享容量。如果客户在方法调用之间通 常作其他工作,就可以利用 JITA.即时激活允许多个客户无缝地同时使用组件。下一节将深 入探讨即时激活和对象共享。 对象共享代表计算机科学中的一个经典折衷。使用对象共享时,可能会为性能而牺牲内 存。共享容量越大,消耗的内存就越多,可使越多的客户满意;在某种程度上,随着共享容 量的增长.可能共享的好处开始减小。这就是为什么在应用程序投入使用前的载荷检测非常 重要的原因。适当的检测可以给出共享设置的正确数值。 7.1.3 对象共享和可伸缩性 开发人员和设计师实现对象共享,因为他们希望这样做会给他们以更大的可伸缩性。不 幸的是,可伸缩性常常被误用和误解。先花几分钟看看可伸缩性的意思是什么。 可伸缩性是添加资源和用户所导致的对应用程序吞吐量的影响。如果随着用户负载的增加可 以添加资源并能够对吞吐量有正面影响,则应用程序就是可伸缩的。 有了 COM+的对象共享,通过在用户负载增加时增加最大和最小共享容量就可以有管理地 添加组件(也就是资源)。允许添加到共享的组件越多,能够服务的客户就越多。 即时激活(JITA)对可共享对象的可伸缩性具有有趣的效果。请记住 JITA 允许在方法 调用后使对象处于不活动状态而在下一个方法调用时,再将其激活。由于客户在方法调用之 间不能正确控制组件,组件就被释放回到共享池之中。一旦组件回到共享池,它就变得对其 他客户可用。图 7-5 展示了 JITA 如何允许两个客户同时虚拟地使用对象的同一个实例。 图 7-5 中,client 1(客户 1)从共享获得 CFoo 类的一个实例并调用 CFoo 的一个方法。 方法返回时,对象被置为不活动并被返回给共享池。此时,client 1 认为它仍然有对 CFoo 的有效引用。然后,client 2 请求 CFoo 的一个实例。因为该类的一个实例已经返回给共享池, client 2 得到与 client 1 所认为的 CFoo 的同一个实例。正是在此时,两个客户部认为他们有 对 CFoo 的同—个实例的有效引用。这就是可伸缩性。通过用一个对象服务多个客户,可以 在应用程序中获得可伸缩性。向共享池中添加对象时,能够支持的客户基础增加的因素应当 比添加的对象数目大某一因子。 不可否认,图 7-5 中例举的是一种相当极端的情况。正常情况下,不会只有两个客户和 一个对象。而且,客户不必返回它先前所使用的对象的同一个实例。然而,对象共享对可伸 缩性的影响仍然是相同的。 笔者需要澄清上述情况中的一点。在图 7-5 中,可以看到 COM+对象共享池分成两部分: 正在使用的对象和可用的对象。实质上,正在使用的对象是客户正在其中执行代码的对象。 可用对象代表客户可以访问的对象。共享容量总是正在使用的对象加上那些可用的对象的总 和。最大共享容量决不能超过这个值。 7.1.4 对象共享和非确定性结束 请记住,虽然 ServicedComponent 利用 COM+,但它们仍然运行在通用语言运行库 (CLR)之内。这意味着被共享的对象仍然会被垃圾收集,就像在第一章中所学的,这可以发 生在任何时候。正常情况下,如果一个类使用了紧缺资源(如数据库连接、文件句柄或网络 连接),则应该实现某些类型的 Close 或 Dispose 方法。然而,这种解决方式不太适合于对象 共享。因为编写可共享对象的目的就是在多个客户之间分摊对这些类型的资源需求花去的代 价,在每个客户使用完对象后再处理这些资源就没有意义。惟一可行的选择(至少笔者认为 是这样)是在类的终止器中释放这些资源. 7.1.5 对可共享对象的要求 COM+坚持可共享对象应符合某种要求。幸运的是,.NET 运行库符合大多数我们的这种要 求。如果能被共享,所有对象必须符合的四个要求如下: 必须是无状态的 必须支持 JITA 必须支持聚集 必须与任何线程都没有亲和力 总之,读者理解前两个要求。无状态性和支持 JITA 其实是密切联系的。笔者必须指出, 这些要求并不是必需的(非常强的建议)。编写不是无状态的和不支持 JITA 的共享对象从 技术上是可能的。然而,除非有很充足的理由使组件不是无状态的,就应当考虑这两个建议 的要求。 所有 ServicedComponent 都支持聚集。同样地,这不是必须在类中明确实现的那些东西。 聚集涉及一个内组件和一个外组件结合在一起形成一个组件。从客户的观点看,客户只处理 一个组件。COM+提供一个聚集了内组件的外组件,也就是共享组件。当 COM+聚集了一个 ServicedComponent 时,它就是正在聚集 COM 可调用封装器(CCW)。请回忆一下第 3 章的 内容,CCW 负责管理 COM 运行库和.NET 运行库间的转换。图 7-6 展示了从聚集的观点看 ServicedComponent 的外观。 图 7-6 聚集 CCW 对共享组件的最后的要求是它们必须与任何线程都没有亲和力。在本章的前面,读者看 到了多个客户可以同时使用单个组件。在这种情况下,多个线程都在访问组件。由于任何线 程都可能访问组件,组件不应该依赖于特定线程的数据,如线程本地存储。一个很好的例子 就是 COM+的共享属性管理器(Shared Property Manager,SPM)。SPM 是线程在内存中的 本地存储区的资源分配器。不要在共享组件中使用 SPM。 7.1.6 对事务处理对象的要求 除了前面提到的要求外,事务处理对象必须符合可共享的几个额外的要求: 必须手工募集资源 必须关闭资源管理工具的自动应征功能 必须实现 IObjectControl 接口 在第 4 章对事务处理性对象的讨论中,读者学习了 COM+提供一种称为自动处理募集的服 务。自动处理募集使资源(数据库连接)能自动募集到 DTC 事务处理中。在数据库连接能够 手式募集到 COM+事务处理中之前,自动募集行为必须设为不可用。这是前面列表中第二 条规则的基础。 事务处理与特定上下又有关-当在对象构造器中打廾数据库连接时,没有活动的上下文, 所以就没有处理,可供连接募集进去。然而,如果数据库连接是在与事务处理关联的特定上 下文中打开的,而且数据库连接在对象被返回给共享池时仍然保持打开时,下一个得到该对 象的客户就可能正在使用已经被募集到不同事务处理中的连接。 为说明这一点,请考察一下图 7-7 中描述的下列情况。两个客户(未被共享)是事务处 理性的 ServicedComponent。每个客户都要求其自己的 COM+事务处理,且都代表一个独一 无二的事务处理根。此外,每个组件部使用一个支持事务处理的共享对象。为使这一点更容 易理解,假设最小和最大共享容量都是 1。共享组件在首次对 Active 的调用中打开数据库 连接且在组件的终止器调用之后才释放连接。当 client 1 调用进入组件中时,共享组件执行 下列操作: 1. 打开数据库连接 2. 把数据库连接募集到第一个客户的事务处理中 3. 在数据库连接上执行一些工作 4. 其方法调用执行且 COM+把组件返回给共享池 5. 客户看到没有错误并提交事务 此时,事务处理被提交,但数据库连接仍然认为数据库连接被募集到第—个客户的事务 处理中去了。第二个客户作出对组件的调用时,数据库连接并没有重新打开,因为在第一个 地方它从来没有被关闭。共享组件试图在打开的连接上进行某些工作时,就产生一个异常. 因为数据库连接试图在不再存在的事务处理中操作。 事务处理性共享组件必须遵循的最后规则是实现 IObjectControl 接口。从 ServicedComponent 类继承的托管组件不必明确实现 IObjectControl 接口。从 ServicedComponent 继承的类只需 改写虚拟方法:Activate、Deactivate 和 CanBePooled。Active 和 Deactive 允许执行与上下文 有关的激活和清理工作。不一定非要实现这两个方法,但实现它们通常是一个好主意。另一 方面.CanBePooled 方法必须实现。如果该方法没有实现,COM+就假设它返回的是假。从 CanBePooled 返回假会使事务处理失败。CanBePooled 给出了一个检查组件状态的好机会。 如果确定对象不再处于一致状态,就可能从该方法返回假,且对象也不返回给共享池。 在进入下—节前,请考虑一下 COM+提供给共享事务处理组件的另一个特性。COM+为事务 处理组件维护了子共享。如果客户涉及了事务处理之中且客户调用进入支持事务处理的共享 组件中,COM+就检查对象共享池以决定先前已经成为客户事务处理一部分的组件是否存 在。这个特性的存在对性能有所改善。通常,使用另一个共享事务处理组件的事务处理组件 以快速接续的形式使用该组件。由于 COM+能记得共享组件先前曾经属于哪个事务处理, 它能只向客户返回组件的实例而不必把组件重新初始化到当前事务处理之中。如果没有发现 与当前事务处理匹配的共享组件。从共享中来的另一个组件就被返回给客户。 7.2 C#中的对象共享 本节将进行一个试验。先编写两个 ServicedComponent 类。一个类是共享对象,另一个 是普通的启用了 JITA 的类。每个类郡包含一个方法—ExecuteCategoryQuery,可对照名为 OfficeMart 的数据库执行选择。创建控制台应用程序用来测量创建这些组件的新实例和调用 ExecuteCategoryQuery 方法要花多久,因为关心的只是检查对象共享能提供的性能效益,不 必操心能否从查询中得到结果。不考虑查询结果,并假设如果没有发出异常查询就是成功的。 7.2.1 共享和非共享组件 通过考察清单 7-1 中的叫 ObjectPoolLib 名称空间中的类开始这里的讨论。为帮助读者更好 地理解每个框架属性和类是从哪里来的,下面的每一个元素都包含一个完全合格的名称空 间。 清单 7-1 共享和非共享类 [assembly:System.Reflection.AssemblyVersion(‘1.0.1.1’)] [assembly:System.Reflection.AssemblyKeyFile(‘mykey.snk’)] namespace ObjectPoolLib { using System; using System.Xml; using System.EnterpriseServices; using System.Data; using System.Data.SqlClient; [ System.EnterpriseServices.ObjectPooling ( true, 10, 100, CreationTimeout=1000 ) ] [System.EnterpriseServices.JustInTimeActivation(true)] [ System.EnterpriseServices.Transaction ( TransactionOption.NotSupported ) ] public class PooledObject:System.ServicedComponent { private System.Data.SqlClient.SqlConnection _cnn; private System.Data.SqlClient.SqlCommand _cmc; public PooledObject(){ _cnn=new SqlConnection( ‘server=(local);database=OfficeMart;uid=sa;pwd=’ ); _cmd=new SqlCommand(); _cmd.CommandType=System.Data.CommandType.Text; _cmd.Connection=_cnn cnn.Open(); } [AutoComplete] public void ExecuteCategoryQuery() { _cmd.CommandText= “select CategoryName,Description from Categories”; _cmd.ExecuteNonQuery(); } //ServicedComponent 的虚函数 public override void Activate() { //此处什么也不做 } public override void Deactivate() { //此处什么也不做 } public overide bool CanBePooled() { if(_cnn.State!=System.Data.ConnectionState.Open) { return false; } else { return true; } } } [System.EnterpriseServiced.JustInTimeActivation(true)] [ System.EnterpriseServices.Transaction ( TransactionOption.NotSupported ) ] [ System.EnterpriseServices.ConstructionEnabled ( Default=”server=(local);database=OfficeMart;uid=sa;pwd=” ) ] public class NonPooledObject:ServicedComponent { private string _sConnection; private System.Data.SqlClient.SqlConnection _cnn; private System.Data.SqlClient.SqlCommand _cmd; public NonPooledObject() { //此处什么也不做 } [AutoComplete] public void ExecuteCategoryQuery() { _cnn=new SqlConnection(_sConnection); _cmd=new SqlCommand(); _cmd.CommandType=CommandType.Text; _cmd.Connection=_cnn; _cmd.CommandText= ‘select CategoryName,Description from Categories’; _cnn Open(); _cmd.ExecuteNonQuery(); _cnn.Close(); _cmd.Dispose(); } //ServicedComponent 的虚函数 public override void Activate() { //此处什么也不做 } public override void Deactivate() { //此处什么也不做 } public override bool CanBePooled() { return false; } public override void Construct(string s) { _sConnection=s; } } } 清单 7-1 中的前两行看起来应当很熟悉。它们定义装配件版本和用来签署装配件的密钥 文件。请记住,需要把密钥文件添加到装配件以便能给它强名并把它安装到全局装配缓存中。 签署装配件不是严格必要的。笔者个人更愿意签署运行于 COM+服务器应用程序中的装配 件。由于服务器应用程序是作为 dllhost.exe 进程运行的,.NET 运行库的装配件解析器会在 \winnt\system32 中查找装配件,因为这是 dllhost.exe 载入的地方。除非要在\winnt\system32 下安装服务器应用程序装配件,否则必须把它们添加到全局装配缓存或在应用程序配置文件 中为它们定义一个地址。这其实仅仅是一种个人的偏爱。 名称空间中定义的第一个类是 PooledObject。修饰该类的第一个属性是 ObjectPooling。 表 7-1 和 7-2 分别列出了属性的构造器和属性。 表 7-1 ObjectPooling 属性的构造器 构造器特征表示名 参数 描述 ObjectPooling() 无参数 启用对象共享。缺省的最小和 最大的共享容量分别是 0 和 1048576。缺省创建时限是 60000 毫秒 ObjectPooling(bool) enabled 启用对象共享(为真)或禁用 (为假)缺省值与前面相同 ObjectPooling(int,int) minimum,maximum pool size 以最小和最大共享容量启用 对象共享。缺省创建时限是 60000 毫秒 ObjectPooling(bool,int,int) enabled,minimum,maximum 以最小和最大共享容量启用 对象共享或禁用对象共享 表 7-2 ObjectPooling 属性的属性 属性名 数据类型 描述 CreationTimeout int 以毫秒为单位指定客户等待 对象从对 C#关键词 new 的调 用返回的时间 Enabled int 指定对象共享是否可用 MaxPoolSize bool COM+在共享中允许的对象 的最大数目 MinPoolSize int 任何给定时刻共享池中对象 的最小数目 应用程序首次启动时,COM+创建 MinPoolSize 数目的对象。 已经在前面几章中检验了下面的两个属性—JustInTimeActivation 和 Transaction,所以 这里不必再讨论。 在 ObjectObject 类的构造器中,初始化了 SqlConnection 和 SqlCommand 对象的实例。 System.Data.SqlClient 名称空间里的类型只是为 Microsoft SQL Server 数据库而设计的。如果 需要访问另一个支持 Microsoft 的 OLE DB 规范的数据库(如 Oracle),应当使用 System.Data.OleDb 名称空间的类型。 在构造器中执行的最重要的任务可能是打开数据库连接。这允许与多个客户共享连接. 在构造器中打开连接是区分 PooledObject 和 NonPooledObject 类的惟一方法。 在清单 7-1 中,可看到在构造器下面定义的 ExecuteCategoryQuery 方法,该方法由 AutoComplete 属性加以修饰。必须为该方法设置 AutoComplete 属性,因为要使对象在方法 调用之后处于不活动状态并返回给共享池。请记住,当线程进入方法时该属性自动将对象的 完成位设成真。该方法的实现将 SqlCommand 类的命令文本设置成 SQL 的 select 语句并对 数据库执行 select 操作。由于不必关心从数据库获得数据,所以可使用 ExecuteNonQuery 方 法,在这种情况下就放弃了查询结果。 该类中的最后三个方法代表 IObjectControl 接口的方法。ServicedComponent 类把这些方 法定义为 virtual。由于对象从 ServicedComponent 类继承,所以不必实现 IObjectControl。 C#既支持 virtual 也支持 abstract 关键词。这两个关键词是“亲密兄弟”,但他们确实 实现稍微不同的行为。virtual 关键词可用于在类中修饰方法和属性。另一方面,abstract 关 键词也用于修饰类。子类必然不必实现定义为 virtual 的方法。对于 ServicedComponent 类的 Activate、Deactivate 和 CanBePooled 方法正是这种情况。子类必须实现定义为 abstract 的方 法。 由于现在在 NonPooledObject 类中没有进行任何与上下文有关的工作,在 Activate 和 Deactivate 方法中就没有任何事情可作。这里出现这两个方法是为了向读者表示在 ServicedComponent 衍生类中是如何表明的。CanBePooled 方法表明如何将其用来检验对象 的状态并决定是否将它返回给共享池。在示例中,观察连接在返回给共享池前是否仍然是打 开的。 代码清单中的下一部分是 NonPooledObject 类。该类执行的工作实质是与在 PooledObject 类 中作的工作相同的。在 NonPooledObject 类中只需作两件不同的事情。首先,由于 NonPooledObject 类不是共享组件,在构造器中并不打开连接。相反是在 ExecuteCategoryQuery 方法中打开连接的。 还要注意该类包含一个新属性:ConstructionEnabled。该属性允许 COM+在组件被激活 时传入一个字符串。该字符串可以是想要的任何东西,但在这里,传入的是数据库的连接字 符串。Default 属性定义注册组件时添加到目录的构造字符串。图 7-8 说明了该组件的 Activation 选项卡。注意 Enable object construction 复选框已经被选中且连接字符串已经添加 到文本域。 图 7-8 启用构造的组件的 Activation 选项卡 请改写来自 ServicedComponent 类的虚拟方法 Construct,这样以来就可访问构造字符串。正 常情况下,如果正在开放不托管组件,就必须实现 IObjectConstruct 接口以得到这种功能。 但 ServicedComponent 类为人们做了这件事。COM+在它调用 Activate 方法前调用 Construct 方法。 笔者没有在 PooledObject 类中实现 ConstructionEnabled 属性,因为需要连接字符串处于类的 构造器中。由于 COM+在对 Activate 或 Construct 的任何调用出现之前构建对象,所以不能 够利用这种特性。 7.2.2 分析客户 测试程序的代码示于清单 7-2 中。不可否认,这是一个有点简化的测试,但它有助于说 明对象共享的好处。 清单 7-2 对象共享客户 using System; using ObjectPoolLib; static void Main(string[] args) { int i=0; long lStart; long lEnd; lStart=System.DateTime.Now.Ticks; for(i=0;i<1000;i++) { ObjectPoolLib.PooledObject po=new ObjectPoolLib.PooledObject(); po.ExecuteCategoryQuery(); } lEnd=System.DateTime.Now.Ticks-lStart; Console.WriteLine(“Results for PooledObject:’+lEnd.ToString()); lStart=System.DateTime.Now.Ticks; for(i=0;i<1000;i++) { ObjectPoolLib.NonPooledObject npo=new ObjectPoolLib.NonPooledObject(); npo.ExecuteCategoryQuery(); } lEnd=System.DateTime.Now.Ticks-lStart; Console.WriteLine(“Results for NonPooledObject”+lEnd.ToString()); Console.WriteLine(Console.ReadLine()); } 该测试程序为每个共享和非共享类创建 10 个实例并确定调用方法所经过的时间。经过的时 间是通过循环前后滴哒声数目的差来计算的。顺便说一下,System.DateTime.Now.Ticks 代表 从 2001 年 7 月 1 日上午 12:00 开始出现的百纳秒间隔的数目。 在笔者的机器上运行这些代码时,得到下面的输出: Results for PooledObject:157726800 Results for NonPooledObject:216110752 别人的结果可能由于系统配置而有点不同。这里的用意是展示对这种类型的组件共享能 够快多少。得有多快。正如可从测试中看到的,对象共享可以提供非常大的益处。 7.3 小结 本章深入探索了对象共享世界。读者已经看到 ServicedComponent 类和 ObjectPooling 属性是 如何用来编写共享对象的。人们再也不必在 C++中编写这些类了。 读者已经得到了试图决定对象是否应当共享时适用于对象的标准。请记住,对象越通用, 就越可能适用于对象共享。 最后,读者已经看到了共享对象是如何与非共享对象比较的,还看到共享对象的性能有 神奇的提高。笔者希望这会激起读者对共享组件的爱好。 第 8 章 列队的组件 本章内容包括: 为列队组件作准备 介绍 Microsoft Message Queue(Microsoft 消息队列) 理解 COM+中的列队组件 与列队组件一起使用其他 COM+服务 在 C#中开发列队组件 当笔者需要与某人联系时(不管是用电话还是用 e-mail,都要经过一个确定联系该人 的最好方式的过程。这种情况已存在多年了。如果不与他或她谈话就不能捱过这一天,则会 打电话或传呼。然而,如果不立即联系上也能过去,则会发一个 e-mail。至少,对笔者来说, 电话提供的是同步的通信方式,而 e-mail 提供的是异步的通信方式。 在远程方法调用领域,列队组件提供了一种客户发达包含对远程组件的方法调用的消息 的方式。与使用 e-mail 的方式相似,可以使用列队组件提供异步通信。 在本章中,读者可看到列队组件是如何与其他形式的远程方法调用,如远程过程调用 (Remote Procedure Call,RPC)和简单对象访问协议(Simple Objece Access Protocol,SOAP) 等进行竞争的。还会遇到当试图在这两种方法调用之间作出决定时所用到的一些准则。 由于 Microsoft 消息队列(MSMQ)j 是在队列组件之背后的基本的传送机制,读者将获得 对其特性和体系结构的介绍。这决不是 MSMQ 的特性的完全解释,但可探索相关的特性, 因为它们与列队组件有关。 在本章的最后三节中,读者将学习列队组件的结构和设计这些类型的组件时会产生的一 些微妙问题。当必须使用 COM+的其他服务如安全时,某些这类设计问题就开始起作用。 在最后一节,将编写几个列队组件,学习通过使用.NET 框架和 C#编写一个基本的列队组件。 从那里去发,就可以转到更高级的技术,如把列队组件与松散耦合事件组合在一起。 这都是非常有趣的知识,作好准备! 8.1 为列队组件作准备 多少年来,开发者都在使用各种技术通过网络传送他们对远程组件的方法调用。这些技 术中的一些,如在后台使用 RPC 的 RPC DCOM,可提供同步通信。当客户具体实现组件时, 就向客户上的某种形式的代理组件发出实现请求,然后代理组件再把请求转发到为组件提供 宿主的服务器上。在服务器端,请求被提取,然后就具体实现组件。从现在开始,每次在远 程组件上设置方法或属性时,就通过网络传送调用,且在另一端被组件提取。客户每次作出 调用时,都要等待远程组件的响应。客户作出的方法调用越多,网络越拥挤,因而应用程序 的运行越慢。如果通道慢速网络与多话的客户联结,就很可能成为一个感到沮丧的用户。 即使是较新的协议如 SOAP 也以与 RPC 相似的方式行动。SOAP 把 HTTP 作为其传送机 制使用。对所有目的来说,HTTP 是—种请求响应型的协议。SOAP 客户以 HTTP 请示的方 式向 Web 服务器上的—些终点发出方法调用。终点可以是任何东西,但通常是 Active Server Page 或 ISAPl 扩展 DLL。当客户向 Web 服务器发送请求之后,在等待 Web 服务器的响应时 会暂停。 所有这些方法调用(RPC 和 SOAP)都有相同的继承限制: 它们不能保证方法调用的发送 客户必须等待每个方法调用跨过网络返回 如果客户作出了对远程组件的方法调用而衮有组件的服务器被关闭,客户就没有办法确 保方法调用成功。在这种情况下,客户只白几种选择:客户可以继续重试方法调用或放弃。 任何一种选择对用户来说都让人感到沮丧。 说 RPC 和 SOAP 是同步协议,是说方法调用是一个接一个作出的。客户每次调用方法时, 会要求发送参数和接收返回值的服务器的双向传递。所有这一切发生时,客户必须等待调 用返回。如果网络缓慢或不可靠,这会大大减慢应用程序。 如 IBM 的 MQ Series 和 MSMQ 之类的产品是为减轻这些问题设计的。这些产品提供两 个很大的好处:异步消息和发送保证。每个产品都带有 COM 和例程 API。开发者可用来向 计算机发送和从计算机接收消息。在本章中不会演示这些 API 的使用,但以下是当编写列 队 应用程序时通常需要执行的任务的纲要: 1.创建公共或私有队列(有管理地或有计划地)。 2.发送者打开队列,指定是否要查看、阅读和发送消息。队列的共享级别是在此时确定 的。 3.发送者在队列上创建要发送的消息。如消息体和优先权的属性是在此时设定的。 4.—旦配置好消息,发送者就把它发送到适当的目标队列。 5.接收者打开队列。 6.接收者查看队列或阅读消息。 不可否认,这是该过梧的过度简单化,但笔者认为读者理解了大多数列队应用程序如何 上作。这是与使用列队组件时基本相同的情况。然而,入队组件向用户隐藏了大多数细节。 正如可能猜想的,列队组件隐藏某些复杂性的事实是以灵活性为代价的。有些任务,如在队 列中查看消息,如果严格使用队列组件就不能执行。 列队组件提供人们希望从消息队列得到的典型特性,如保证发送和异步处理。还有如 果使用 MSMQ COM API 列从组件则列队组件隐藏所执行的队列通信工作。 列队组件特别适合于为同步方法调用提供几种便利: 组件的可用性和寿命 工作负载的时间安排 方法调用的可靠性 可伸缩性 由于客户和组件的寿命不必重叠,客户就能够与正在调用的组件独立操作。这在装有组 件的服务器不能用或客户与组件间的网络不能用时有好处。在服务器被关闭的情况下,从客 户来的调用可排队并存储,直到宿主服务恢复并运行。这对如膝上型电脑和掌上型装置的非 连接客户有好处。这些类型的应用程序在从网络断开连接时能继续发挥功能。 客户和组件的寿命不必重叠的事实使人们可以安排什么时候组件应该进行其工作。正如 可在本章的后面看到的,列队组件必须配置成服务器应用程序。该服务器应用程序必须运行 以便组件来处理方法调用。列队组件允许指定服务器应用程序(dllhost.exe)何时运行。这一 时间安排可被设置成在宿主服务器不太繁忙的时刻运行应用程序。 当经过网络进行列队组件的方法调用时,MSMQ 使用事务处理来发送消息。在组件一侧, 以事务处理方式从队列读取消息。这些事务处理不必是 COM+事务处理,但可从许多相同 的 特性获得好处。如果列队组件从队列读取消息时发生错误,读取就会返回,且消息返回到队 列。所有这一切都添加了一种比从非列队方法调用获得的更为可靠的发送机制。 最后的好处—可伸缩性—可能是最大的,至少在笔者的印象和是这样。首先,因为 客户不必在每个方法调用期间暂停,它能够以快速连续的方式作出许多方法调用并转栘到下 一个任务。客户即最终用户不必等待方法调用通过网络传送并被可能可用或不可用的远程组 件 接收。第二,由于服务器进程(dllhost.exe)可以在非高峰时间被安排处理消息,装有组件的 服务器在高峰时间就会有更多可用的资源,所有这些使应用程序以更快的速度作更多的工 作。 8.2 Microsoft 消息队列简介 如果不理解 MSNQ 的基础就很难理解列队组件如何工作。正应用程序中发生问题时如 果不熟悉基本的传送机制甚至会更难诊断。下回看看组成 MSMQ 网络的组件。 8.2.1 安装 MSMQ Windows 2000 Server 作为操作系统的核心组件提供了 MSMQ 2.0。可在安装操作系统时 或以后的任何时候选择安装 MSMQ。缺省情况 FMSMQ 是不安装的。 MSMQ 既可安装在 Windows 2000 工作组中也可安装在 Windows 2000 域中。MSMQ 是 与 Windows 2000 活动目录(Windows 2000 Active Directory)紧密整合在一起的。如果希望得 到活动目录支持,就要在域控制器上安装 MSMQ。MSMQ 扩展活动目录架构以便保存队 列配置和状态信息。 工作组安装选项从某种意义上说限制更多。例如,只有私有队列可在工作组配置中使用。 此外,路由服务器不能与工作组配置一起使用。与活动目录的集成允许使用内部证书来认证 消息。由于内部证书来自活动目录,在工作组安装中该选项不可用。外部证书是工作组安装 的惟一选项。外部证书来自于外部的授权机构。幸运的是对列队组件开发者来说,在工作组 安装中仍可以使用自己的组件。 根据是否正在客户、域控制器或成员服务器上安装 MSMQ,有许多可供选择的选项。对服 务器来说,可以选择把 MSMQ 作为路由服务器或具有公共或私有队列的普通服务器安装。 路由服务器的任务是从客户获取消息并把消息移到最后的目标队列。路由服务器不负责处理 输入的消息,除非那些消息是为本身的队列之一发送的。基于性能的原因,Microsoft 不推 荐在域控制器上安装路由服务器。路由服务器应当安装在成员服务器上。 客户可以独立安装也可以不独立安装,为发送消息,非独立的客户必须连接到网络。连接到 本地网络的工作站是非独立客户很好的候选者。对这一条惟一的例外是域控制器。MSMQ 不能作为非独立客户安装到域控制器上。另一方面,独立客户发送消息不必连接到网络。运 行于独立客户上的应用程序发送消息时,MSMQ 在本地存储那些消息,直到客户重新连接 到网络时为止。一旦客户重新连接,就可发送消息。膝上型电脑和掌上型装置是独立客户的 好选择。移动装置在使用时并不总是连接到网络。独立客户安装允许用户使用其应用程序就 好像连接到网络上一样。用户下一次连接到网络时,可以发送消息和处理他们的数据。 MSMQ 一旦安装到域中,就可使用计算机管理控制台(Computer Management Console)来管理 它。图 8-1 展示了在 Services and Applications 节点下的何处可找到 MSMQ。这个插件只对在 域中安装的 MSMQ 可用。 图 8-1 MSMQ 管理插件 8.2.2 理解队列 队列类似于邮箱。正如使用邮箱发送和接收邮件一样,应用程序使用队列发送和接收消 息。在 MSMQ 中,存在两种队列—应用程序队列和系统队列。应用程序队列可以是下列 任何类型的队列: 消息队列 管理队列 响应队列 报告队列 COM+列队组件及其客户使用消息队列发送和接收方法调用。消息队列可以是公共的或私有 的。公共队列是在活动目录内注册的,但私有队列只有在指定机器名时才可用。在 Windows 2000 域中安装列队组件时,COM+为组件创建公共消息队列。 发送程序发送消息时,它们可以要求来自 MSMQ 的响应。响应可以是消息到达队列时或者 消息从队列中被读取时产生,或者如果希望的话,两种动作都生成来自 MSMQ 的响应。除 了在管理队列是接收程序生成响应消息(发回到发送者)这一点上,响应队列类似于管理队 列。 当消息从路由服务器移动到路由服务器时,报告队列保存着 MSMQ 生成的消息。通过启用 对消息的跟踪,应用程序可以指定使用报告队列。 MSMQ 在内部使用系统队列。系统队列构成死信队列和日志队列。死信队列保存着不能被 发送到目标队列的消息。每个安装都有两个死信队列。一个队列保存着事务处理消息,另一 个保存着普通消息。读者将在本章的稍后看到列队组件如何使用事务处理的死信队列。 每当创建公共或私有队列时,MSMQ 都创建相关的日志队列。日志队列的目的是保存消息 的副本,直到消息从队列中读出为止。缺省情况下是不记录日志的。使用计算机管理控制台 (Computer Management Console)中的 MSMQ 进入队列属性就可启用队列的日志记录。图 8-2 展示了名为 helloworld 的队列的 General 选项卡。该队列是在 COM+列队组件安装后创建的。 8.2.3 MSMQ 消息 本质上,消息是由消息体及其决定消息路过网络时消息行为的相关属性组成的。消息的消息 体是实际的有效负荷或数据。许多属性可以被发送程序有计划地设置。一些较为通用的属性 是消息体类型、消息优先级、格式和目的队列。消息体类型属性描述包含在消息体内的数据 类型。消息体类型可以是任何数目的数据类型如整型、字符串型甚至其他对象如 DataSets。 消息优先级在关键消息路过网络时给它们以较高的优先级。缺省情况下,消息的优先级为 0。 有较高优先级值的消息比有较低优先级的消息路由更快。 图 8-2 启用队列日志记录 格式指定消息的有效负荷流向消息体属性的方式。缺省情况下,.NET 把有效负荷以 XML 的形式流动。其他格式是 ActiveXMessageFormatter(ActiveX 消息格式) 和 BinaryMessageFormatter ( 二进制消息格式) 。这些格式都以二进制格式流动数据。 ActiveXMessageFormatter 允许流动基本数据类型如整型和字符串型,以及类、枚举和任何 其他可以转换成 System.Object 型的类型。 使用列队组件时可以设置许多消息属性。虽然不与发送到列队组件的消息直接交互,但 能够通过使用标记设置一些属性。读者将在本章的后面遇到标记。 8.2.4 用 C#开发 MSMQ 应用程序 多少年来,开发者都是用 MSMQ COM 组件或 MSMQ API 函数开发使用消息队列的应用程 序。这些 API 允许开发者从队列读取,向队列写入,并执行管理任务如创建队列。.NET 框 架为 C#开发者提供了一组相似的 API。System.Messaging 名称空间(System.Mesgaging.dll)包 含开发利用 MSMQ 功能的.NET 应用程序所需要的所有类、接口和枚举。本节提供对 System.Messaging 名称空间和常用类的简单介绍,使读者能感受一下不使用列队组件开发消 息应用程序的感觉。 System.Messaging 中最常用的两个类是 MessageQueue 类和 Message 类。MessageQueue 类用 于读取、发送并窥视到达特定队列的消息。队列的名称和路径可以在类被具体实现时或使用 path 属性来指定。表 8-1 列出了 MessageQueue 类的构造器。 Message 类用于操作传来或发出的 MSMQ 消息的属性。该类包含消息体和消息的有效负荷。 通常,想要向队列发送消息时,可使用它的三个构造器中的一个创建新的 Message 对象。这 类的构造器如表 8-2 所示。 表 8-1 MessageQueue 构造器 构造器 描述 示例 MessageQueue() 创建没有绑定到一个队列的 MessageQueue 类的新实例 MessageQueue mq = new MessageQueue(); MessageQueue(string path) 创建 MessageQueue 类的新实例并将它绑定 到路径指定的队列 MessageQueue mq = new MessageQueue(“.\queueName”); MessageQueue(string path,bool sharedModeDenyReceive) 把路径中指定的队列绑定到 MessageQueue 类的实例并向从队列读取的第一个应用程 序的授予排他性读访问权 MessageQueue mq = new MessageQueue(“.\queueName, true); 表 8-2 Message 的构造器 构造器 描述 示例 Message() 创建空的消息类。必须填入所 要的属性 Message m = new Message(); Message(Object body) 创建新类,给传入的对象设置 消息的有效负荷。任何从对象 继承的类都能够被传递 My Class mc = new MyClass() Message m = new Message(mc); Message(Object body , IMessageFormatter formatter) 将消息体参数设置为等于消 息参数。格式参数指定消息体 参数如何流向消息 My Class mc = new MyClass() Message m = new Message(mc,new XmlMessageFormatter()); 请看一下从队列发送和接收消息会花费什么。清单 8-1 中的代码打开与名为 OrderQueue 的 私有队列的连接,发送名为 Order 的结构,从队列读取消息,并打印出 Order 结构的各域。 清单 8-1 从队列发送和接收消息 Using System; Using System.Message; namespace OrderMesaage { public struct Order { public string CustomerName; public string Sku; public int Quantity; } public class C0rderApp { public static vold Main() { //创建一个新的 Order order order; order.CustomerName = “Aeme, Inc.”; order.Sku = “skul23”; order.Quantity = 100; //打开 OrderQueue 队列并发送 Order MessageQueue mq = newMessageQueue (“Server1\\private$\\OrderQueue”); mq.Send(order); //指定想要使用的格式的类型 XmlMessageFormatter xmlf = (XmlMessageFormatter) mq.Formatter; //把想要从这一消息流出的类型告诉格式工具 xmlf.TargetTypeNames = new string[]{”OrderMessage.Order,OrderMessaging”}; //从队列中读取消息 Message m = mq.Receive(); Order orderIn = (Order)m.Body; Console.WriteLine(“Customer Name: “ + orderIn.CustomerName); Console.WriteLine(“Sku: “ + orderIn.sku); Console.WriteLine(“Quantity: “ - orderIn.Quantity); } } } MessageQueue 类是用表 8-1 中的第二个构造器创建的。请注意队列的路径。路径的第一部 分(Server1)是为队列提供宿主的机器名。路径的第二部分把队列指定为私有队列。最后一部 分是队列名。 一旦创建了新的 MessageQueue 实例,就只需向队列发送 Order 结构。不必为该队列定义格 式,因为已经缺省地定义了一个:XmlMessageFormatter。 然而,在读回队列中的消息之前,必须定义要从消息体提取的类型。在这种情况下,把消息 体转回 Order 结构中以便可以把定单 order 打印到控制台。xmlf.TargetTypeNames 定义需要 格式流回来的类型。指定给 TargetTypeNames 属性的字符串数组保存着结构和实现它的装配 件的完全合格的类型名。完全合格的类型名包含名称空间和类型名。 一旦格式知道要从消息体提取的是哪个类型,就可从队列自由读取消息。Mq.Receive()从队 列读取消息并返回 Message 类的一个实例。如果队列上有多个消息,该调用就返回第一个消 息。如果队列上没有消息,该队列就暂停(等待)直到消息到达或直到超过接收时限值。由于 该方法被重载,它就能通过传入一个 System。Timespan 实例而被调用,该实例定义调用等 待消息到达队列的最大时间量。 笔者不得不承认这个例子作出了某种非典型的假设。首先,在试图打开队列并发送消息之前,检查队列 的存在是很好的实践。第二,人们并不想使客户无限时的等待消息而强制客户暂停。这个例子并非打算作为 一个消息的真实的例子。然而,这个例子的目的是演示一些较为常用的任务,比如发送和接收消息以及使用 XmlMessageFormatter 等是如何用 system.Messaging 名称空间完成的。在本章的最后,读者将看到列队组件 如何隐藏许多这类工作的例子。 8.3 理解 COM+中的列队组件 列队组件提供了通过网络对远程机器作出方法调用的机制。正如读者已经知道的,MSMQ 提供了实现这一目的的传送功能。本节把 COM+列队组件体系分成各个部分,显示如何把 方法调用封装到 MSMQ 消息中以及在组件端又是如何解包的。还有,本节也提出了开发列 队组件时必须探索的设计因素。 8.3.1 客户和服务器的要求 如果不了解在客户和服务器上需要安装些什么,就难以理解列队组件如何工作。正如可能已 经猜到的,MSMQ 必须安装到客户和服务器上。MSMQ 为列队组件提供基本的传送机制。 也许最重要的要求和最初可能容易忽视的是组件必须在客户和服务器上都注册的事实。由于 正在处理 COM+组件,还必须安装 COM+。正如读者很快就能明白的,客户不访问组件的 本地版。本地版更像是向客户提供信息的模板。在列队组件被安装在本地的情况下,客户可 使用组件的列队接口。COM+可以使用组件的本地副本确定在出错时是否应当使用异常类。 使用组件时,不仅必须在 Web 服务器或应用程序服务器上部署组件,还必须在每个客户机 器上部署。如果只有一台客户机和一台服务器也不是什么坏事,但通常会在多台客户机部 署。.NET 的部署模型,特别是当它应用到 COM+组件时,比这优秀。在过去,如果把列队 组件部署到客户机,就必须找到一种部署组件、在 COM+中注册组件并配置适当属性的方 式。COM+的复制可能有助于这一点,但如果客户配置错误,事情就可能变糟。在这种情况 下,管理员可能不得不重新配置 COM+应用程序。因为 ServicedComponents 在其装配件中 直接包含 COM+属性,装配件只需重新注册即可。 8.3.2 记录器、收听器和播放器 图 8-3 图示了列队组件系统的结构。该系统中的组件是客户、记录器、收听器、播放器,当 然还有列队组件自己。正如可以在这个图中看到的,客户端和服务器端都必须为客户安装 COM+以便能够使用列队组件。记录器、收听器和播放器都是 COM+的组件,而 COM+使用 它们隐藏 MSMQ 编程的细节。 图 8-3 列队组件的结构 客户创建列队组件的实例时,就收到调用记录器所返回给特定 COM+组件的一个实例。一 旦客户有了对记录器的引用,就可以调用记录器的方法,就像它是实际的列队组件。记录器 接受方法调用就像它是列队组件并把它们捆绑进 MSMQ 消息。客户释放对记录器的引用时, 记录器将方法调用作为单个 MSMQ 消息提交给列队组件。消息进入列队组件的公共输入队 列等待收听组件加以处理。客户从不直接与列队组件通信。 读者可能想知道,非确定性的结束和垃圾收集是否在记录器对象的释放中起作用,毕竟,在大多 数情况下,直到垃圾收集器将托管对象收集起来之后,托管对象才被释放。幸运的是,对那些编写列 队组件的程序员来说,非确定性的结束不是问题。正如会在本章的后面看到的,应该明确地释放对列 队组件(如记录器)的引用。 在服务器端,收听器组件等待消息到达列队组件的公共输入队列。图 8-4 展示了名为 queued components 的 COM+应用程序的公共队列。该队列的名称与 COM+应用程序相同,在这里, 名称是 queued components。 图 8-4 列队组件应用程序的公共输入队列 收听器负责从应用程序的队列中取出消息并具体实现播放器对象。收听器不直接实现播放器 组件,而是具体实现另一个名为 ListenerHelper 的 COM+组件。该组件的 ProgId 是列队的 components.ListenerHelper。笔者在前面已经提到记录器、收听器和播放器都是 COM+组件。 在它们作为 Windows 2000 上标准 COM+安装的一部分而安装和注册的情况下这是真的。然 而,只有记录器和 ListenerHelper 是真正被配置的组件。图 8-5 展示了在组件服务资源管理 器的哪个地方可以找到记录器和ListenerHelper。COM+有名为COM+ Utilities的库应用程序, 其中装有这些组件。 图 8-5 COM+实用工具应用程序中 ListenerHelper 和记录器 ListenerHelper 组件的工作是创建播放器组件。每个传来的 MSMQ 消息被赋予一个播放器组 件。播放器组件读取消息的内容(方法调用),创建实际的列队组件,并回放客户中的每个方 法调用。播放器以与客户调用时相同的顺序回放方法调用。 既然读者了解了更多的关于列队组件结构方面的内容,通过扩展图 8-2 可复习这些概念。 图 8-6 列队组件的结构的扩展视图 图 8-6 以客户创建列队组件的实例开始。作为回应,客户得到对列队的已配置组件 components.Recorder 的引用。客户在该引用上继续进行方法调用,就像它真在使用列队组件。 一旦客户释放对记录器对象的引用,COM+就创建 MSMQ 消息并把消息发送到列队组件的 输入队列。整个过程代表步骤 1 至 5。 当消息到达组件的公共输入队列时,收听器从队列取出消息并创建播放器对象的实例。收听 器使用列队 components.ListenerHelper 组件来做这项工作(步骤 7)。然后,收听器把消息传给 播放器组件,后者依次回放消息中包含的方法调用。在步骤 9,播放器以与记录器录制时相 同的顺序回放调用。 8.3.3 实例化列队组件 本书直到此处,笔者才使用 c#的关键词 new 创建组件的实例。这在读者想要创建并直接访 问组件的实例时很有好处。希望使用列队组件时,关键词 new 会留下许多局限性。首先, 在大多数情况下,客户和组件驻留在不同的计算机上。需要有一种方式不仅能通知希望创建 哪个组件,而且还通知希望在哪个机器上创建。new 关键词不允许这样做。第二,正如笔者 在前一节提到的,具体实现组件后真正想要得到的是对记录器的引用,而不是对实际组件的 引用。 标记是允许指定在哪台机器安装要使用的组件的特殊类型的组件。实质上,标记是工厂组件。 如果熟悉 COM,就可能知道类工厂。COM 中的类工厂是为创建其他 COM 组件而存在的。 类工厂是特地为组件编写的。因此,“工厂”特别了解所创建的组件。标记与 COM 类工厂 相似。标记使程序员绕过指定计算机名的步骤,因为它们能在对象初始化期间获取输入参数。 需要指出的是不用直接创建标记。标记不是用通常创建类或组件那样通过使用关键词 new 的方式创建的。标记是通过向某些类上的方法传送一个字符串间接创建的。字符中指定标记 的名称且可选择地指定其他参数。标记是在后台为用户创建的。这将在下面几节更清楚地说 明。 要创建列队组件,可使用称为队列标记的特殊标记组件。队列标记知道如何创建记录器组件 (这解决了 new 的第二个局限)。可以传给队列标记的参数之一是装有列队组件的计算机。还 有,可以指定几个参数,用来影响把消息放到队列之后消息的行为。笔者不准备列出队列标 记可以带的所有参数,但它们中的几个是:消息的优先级、目标队列的名称、到达队列的时 间、消息路过时是否生成跟踪消息以及队列的日志级别。笔者认为读者已经有了一定的概念。 可参考 MSDN 文档关于列队组件的部分,查看队列标记参数的完整列表。 COM 开发者希望使用标记时,并不使用通常的 CoCreateInstance(C/C++)或 Visual Basic 命令 CreateObject。标记已经使用诸如 Visual Basic 中的 GetObject 或 C/C++中的 CoGetObject 类 似的方法创建了。C# 开发者有相似的方法用标记具体实现组件。框架的 System.Runtime.InteropServices 名称空间包含名为 Marshal 的用于创建标记的类。Marshal 类 包含许多方法可用于管理通用语言运行库(CLR)和Windows的非托管运行库(特别是COM运 行库)之间的相互作用。许多这种方法都在两个运行库之间处理内存管理。在本章中,主要 精力只集中在 Marshal 类的两个方法上:BindToMoniker 和 ReleaseComObject。这些方法都 是静态的,这意味着不需要创建 Marshal 类的实例就可使用。 可使用 BindToMoniker 方法创建标记,最终再创建记录器。这个方法带一个参数,即一个字 符串,代表标记的名称及其参数(如果有的话)。看一看以下几行代码就可使这个概念更清晰。 IQC iqc; iqc = (IQC) Marshal.BindToMoniker(“queue:/new:queued componentsNamespace.queued components”); iqc.SomeMethod(); 在前面的代码中,Marshal.BindToMoniker 创建名为 QCNamespace.QC 的列队组件的实例。 字符串参数指定标记的名称 queue 和列队组件的名称 QCNamespace.QC。在该字符串中使用 了两个标记。第二个标记是用/new:指定的 new 标记。这两个标记结合起来使用,以便创建 列队组件的实例,具体地说,就是 COM+记录器。其余代码的解释放在本章的最后。变量 iqc 是对记录器对象的引用,可把该记录器看作对实际的列队组件的引用。 这样一来就创建了对列队组件的引用,准确地说就是得到对记录器的引用。在本章的前面, 已经说明了有办法克服垃圾收集对列队组件引入的问题。问题是以下事实引起的,必须释放 对记录器的引用,以便使 COM+创建 MSMQ 消息并把消息发送给列队组件的输入队列。如 果依赖于垃圾收集释放这类引用,就不能确定何时消息将要提交给组件的输入队列。幸运的 是,Marshal.ReleaseComObject 方法调用允许明确地释放引用。该方法带有一个参数:从 BindToMoniker 调用获得的引用。请记住,因为记录器是非托管的已配置的组件,对它的访 问要通过与 COM 的互操作。当作出对比 ReleaseComObject 的调用时,就是在释放对运行库 可调用封装器的引用,接着就减小了记录器的引用计数。一旦引用计数达到 0,记录器就会 被释放.消息就会提交给队列。 8.3.4 异常处理 虽然列队组件为分布式应用程序提供了强有力的环境,但仍可能产生问题。例如,客户可能 没有足够的权限向组件的输入队列发送消息。在其他情况下,组件可能不能够处理已经到达 其队列的消息。第二种情况可能特别麻烦,因为它可能产生有害消息。为处理客户错误和服 务器端错误,COM+提供了一种产生问题时指定异常处理类的方式。在讲解具体内容之前, 先考察一下当客户和服务器处理消息遇到问题时会发生什么。 1.服务器端的错误处理 正如读者知道的,消息到达组件的输入队列时,收听服务器获得消息并把它传给播放器。即 使在消息已经到达输入队列之后,还可能产生问题。出于某种原因,消息可能无法读取。更 可能的情况是,消息内方法调用包含着组件不能处理的数据。 当播放器试图回放组件上的方法调用时,发生有害消息,而且一个或所有方法调用都会失败。 如果组件返回失败的 HRESULT 或产生异常,播放器就中止过程并把消息放回到队列上。当 消息回到队列上时,收听器就再次捡取消息并把它传给播放器,整个过程再次开始。如果没 有处理错误的机制,消息可能通过收听器和播放器不断地循环。图 8-7 说明了这一过程。 组件不能处理的消息被放到几个重试队列上。消息首次失败时,就放到第一个重试队列上。 第一个重试队列是 ApplicationName_0,ApplicationName 是拥有组件队列的 COM+应用程序 的名称。一分钟后,收听器从第一个输入队列取出消息并试着再次处理它。消息一旦从第一 个重试队列取出会被重试三次。经过这几次重试后,如果消息仍然不能处理,就被放到第二 个重试队列 ApplicationName_1 上。一旦消息被放到第二个重试队列,收听器会在试着再次 处理前等待两分钟。假如消息不断失败,就会被放到五个重试队列的每一个之上。消息每次 进入另一个队列时在每个队列上的持续时间都会变长。就像第一和第二个重试队列一样,消 息会在每个队列上重试三次。表 8-3 列出了每个重试队列、每个重试队列重试的次数,消息 处于队列中的时间长度。 图 8-7 列队组件中的有害消息 表 8-3 队列超时及重试次数 队列名 重试次数 在队列上等待的时间(分钟) ApplicationName_0 3 1 ApplicationName_2 3 2 ApplicationName_3 3 4 ApplicationName_4 3 8 ApplicationName_5 3 16 ApplicationName_DeadQueue 0 不确定 表 8-3 中最后的一个队列是组件最后剩余的队列。如果消息在重试过程中不能处理就进入这 个队列。该队列中的消息会保留到用 MSMQ 资源管理器手工删除之时。一旦消息被放入这 个队列中,收听器就不会再试图重新提取它。 最后剩余的队列和每个重试队列都是私有的且是在 COM+应用程序被标记为列队之时创建 的。如果不想要消息重试这么多次或花这么久才达到最后的剩余队列,可选择删除任何一个 或所有的重试队列。如果删除所有这些队列,有害消息就直接进入最后剩余队列。如果只删 除几个重试队列,所删除的队列下面的其他队列就优先向上移。例如,如果删除第三个队列, 第四个队列就像第三个队列一样行动。在这种情况下,第四个队列就在第三个队列失败后重 试。相应地,消息在第四个队列上等待 8 分钟。 1.客户端错误处理 由于许多理由,客户在把消息发送到组件的输入队列时可能发生错误。例如,客户可能没有 足够的权限提交消息。COM+以与处理组件端错误相似的方式处理这些类型的错误。这将在 下一节更详细地介绍。 与服务器组件不同,客户没有重试队列。消息被确定为不能发送时,就被移到客户的 Xact 死信队列。一旦消息进入死信队列,客户有一个重新使用消息的机会。作为找回消息的最后 努力,COM+支持使用名为异常类的特殊配置的类。 2.异常类 异常类是特殊类型的 COM+组件。它们是通过名为 Queued Exception Class 的 COM+属性指 定的。图 8-8 展示了名为 QCMarshal.MarshalClass 的列队组件的 Advanced 选项卡。在图 8-8 中,笔者已经指定了一个名为 QCMarshal.QC 的类。 图 8-8 组件服务资源管理器中的异常类 COM+以与处理服务器错误和客户错误相似的方式使用该例外类。该组件必须实现 IPlaybackControl 接口。在 COM+将消息放在最后剩余队列之前,COM+会查找组件是否已 经定义了异常类。如果异常类存在,COM+就调用 IPlaybackControl.FinalServerRetry 方法。 该方法允许异常类试着确定发生了什么样的错误并向开发者报告错误。对客户端错误,存在 相似的过程。消息进入死信队列前,COM+调用 IPlaybackControl.FinalClientRetry 方法。可 在服务器端使用与 IPlaybackControl.FinalServerRetry 方法相似的方法来使用这一方法。 除了支持 IPlaybackContro 接口之外,异常类必须实现列队组件的列队接口。在 COM+调用 IPlaybackContro 接口上的方法中的一个之后,可在异常类上重新播放方法调用。此时,异常 类有一个使事务处理发生的机会。如果异常类发出错误,消息就被放进死信队列或最后剩余 队列,这要根据错误发生在哪一端(服务器或客户)而定。 8.3.5 列队组件的设计考虑 使用列队组件或使用松散耦合事件的组件需要特殊的设计考虑。记得第 6 章曾经讲过,设计 松散耦合事件时,必须记住,方法调用是单向的:从发布者到订户。对列队组件也是这样。 列队组件可能是一种更极端得多的单向处理的情况,因为客户和组件的寿命不必重叠。牢记 这个概念有助于理解为什么必须符合本节所述的要求。 在讲解组件成为列队组件必须符合的要求之前,请先考虑一些问题,这是人们在决定是否应 当使用列队组件时首先向自己提出的一些问题。 客户和组件需要实时进行相互对话吗? 客户甚至还需要来自组件的响应吗? 方法调用必须保证使它通过网络吗? 作出方法调用的时间和组件处理它们的时间之间存在时间滞后吗? 这些问题是为了促使开发者思考同步调用和通过 MSMQ 消息作出的调用之间的不同之处。 例如,在第一个问题中,如果客户不必实时访问组件,就可考虑使用列队组件。作为通用的 规则,列队组件不对客户作出方法调用是成功还是失败的响应。如果客户愿意作出几个方法 调用然后进行其他处理,就可使用列队组件。第三个问题与 MSMQ 消息的有保证的发送有 关。因为列队组件使用 MSMQ 作为其传送工具,方法调用可能会保证到达组件。MSMQ 比 其他技术如远程例程调用提供了更有效的发送机制。 最后一个问题应能驱使人们考虑与批处理方法调用有关的问题。如果客户对于组件可能不能 及时处理方法调用的事实并不反感,就可使用列队组件对方法调用进行批处理。请考虑一种 订单输入方案,其中用户全天都在订单输入程序中输入订单。在一天结束时,每个人都回家 了,计算机就被闲置了。如果在每个人走了之后,那些订单能够在晚上处理(以批处理模式), 而不是在用户输入时处理会怎么样呢?列队组件非常适合于这种类型的处理。因为 COM+服 务器应用程序必须运行以便收听输入消息,可以把应用程序设置成在下班后启动。直到此时, 来自客户的消息仍然在组件的输入队列中。 除了前面提到的问题之外,列队组件在能够作为列队组件安装前必须符合某种要求。首先而 且最重要的是,方法调用必须不含输出参数或返回值。请记住,C#中方法调用的返回值在 被转换成组件类型库中的[out,retval]参数。也就是说,有返回值的 ServicedComponent 类中 的方法不适合于列队组件。 C#支持两个关键词,ref 和 out,这两个关键词在列队组件中是非法的。这些关键词都影响 方法参数的行为。ref 关键词强制方法参数通过引用传入。用 ref 关键词传入的参数必须在方 法调用前初始化。在方法执行过程中,方法可能修改参数的值,而把新值返回给客户。除了 客户不必在参数传入前初始化之外,out 关键词的用法与此差不多。如果接口包含使用这些 关键词个任何一个的方法,就不能对它们进行列队化处理。实际上,如果 Regsvcs 实用工具 发现了有这些关键词的方法,Regsvcs 实用工具就不会安装这样的组件(假设正在安装列队组 件)。 C#类,即使是那些由 ServicedComponent 派生的类,都不能作为参数传递给方法。因为客户 和组件的寿命不能重叠,当把类发送给组件时就必须制作它的副本。传递给组件方法参数的 组件必须在能够发送给组件前被调度到 MSMQ 消息中。ServicedComponent 类在缺省情况下 不支持这种功能。如果希望把托管类作为方法参数传递,它们必须由名为 IPersistStream 的 COM 接口派生而来。该接口能使记录器把对象调度到消息中且能使播放器从外来消息中反 调度组件。 8.4 与列队组件一起使用其他 COM+服务 其他 COM+服务,诸如事务处理、安全和松散耦合事件都可与列队组件结合使用。一些服 务如松散耦合事件提供了对列队组件很好的实现方法。其他服务,诸如基于角色的安全性, 都引入了限制。本节重点不是所有的 COM+服务,但会涉及最值得注意的服务。 8.4.1 基于角色的安全性 即使客户和组件的寿命不重叠,COM+基于角色的安全也支持列队组件。记录器把方法调用 封装到 MSMQ 消息中时,客户的安全上下文也被包含在消息中。消息到达组件时,播放器 将客户的安全上下文解包。播放器开始回放方法调用,列队组件看待方法调用就像它们是直 接从客户来的一样。由于这一原因,列队组件可以定义角色并允许或拒绝访问其基于这些角 色的接口。此外,如 ContextUtil.IsCallerInRole 的调用是基于原调用者即客户的上下文的。 8.4.2 事务处理 事务处理是列队组件的一个完整部分。COM+事务处理在记录器被激活时发生在客户端而当 ListenerHelper 返回未被配置的播放器对象时事物发生在服务器端。对服务器端对象,这意 味着如果对象支持或要求事务处理,则对象是在 ListenerHelper 的事务处理中激活的。如果 组件运行于 ListenerHelper 的事务处理中并调用 ContextUtil.SetAbort 或 ContextUtil.DisableCommit(),消息就会回到队列上并重试。如果组件总是中止处理,这可能 导致存在于组件的最后剩余队列的有害消息。 记录器被配置成需要事务处理。运行于事务处理中的客户在通过标记激活列队组件时将在其 事务处理中包括记录器。如果客户中止处理,消息就不会发送到列队组件的输入队列。由于 MSMQ 对 COM+处理的支持,消息会在处理失败的过程中从队列中取出。MSMQ 提供参与 DTC 事务处理的资源管理工具。 8.4.3 松散耦合事件 在 COM+提供的所有服务中,松散耦合事件实现了最接近列队组件的开发模型。像列队组 件一样,松散耦合事件提供从客户调用组件的单向处理过程。 列队组件可用于提供事件的异步通知。有几种把列队组件与事件结合使用的方式。第一种选 择是把事件类做成列队组件。在这种情况下,发布者使用 Marshal 类和列队标记具体实现列 队事件类。方法调用像通常一样生成并插入到 MSMQ 消息中。当消息到达事件类时,记录 器播放方法,事件向订户触发。 第二个选择是使订户的接口列队。在这个选项中,发布者像通常一样使用事件类但没有 MSMQ 交互。当事件触发时,事件系统把方法调用传给记录器,MSMQ 把消息发送给订户。 8.5 在 C#中开发列队组件 本节介绍如何编写三个列队组件的实现: HelloWorld 列队组件 松散耦合事件中的列队用户 实现异常类的列队组件 读者将看到.NET 如何通过从 System.EnterpriseServices 名称空间来的属性支持列队组件开 发。除非特别说明,所有这些属性都在这一名称空间内。 8.5.1 HelloWorld 列队组件 在这个例子中,客户和组件是在同一个名称空间中实现的。这可能不是通常的实现方法,但 可演示客户和组件如何交互。本例中的客户是在 MyApp 类中实现的控制台应用程序。 在 进入代码之前,请先检查列队组件的 COM+属性和开发模型。图 8-9 展示了拥有列队组件的 COM+应用程序的属性对话框。在应用程序级设置了两个列队属性:Queued 和 Listen。如果 设置了 Queued 属性,就创建公共应用程序队列和私有重试队列(在安装时)。Queued 属性允 许应用程序接收 MSMQ 消息。Listen 属性通知 COM+一旦应用程序启动就应当处理消息。 从应用程序往下的下一个级别是列队组件本身。在组件上指定的惟一属性是异常类。在图 8-10 中,Advanced 选项卡显示了笔者配置的名为 QCNamespace.QCException 的异常类。该 异常类属性可以是一个 ProgID 或 CLSID。 图 8-9 应用程序级属性 图 8-10 异常类属性 对组件的接口来说,与列队组件有关的最后的属性是 Queued。在图 8-ll 中,可以在 IQC 界 面的 Queuing 选项卡上看到已经选中的 Queued 属性。该属性启用时,客户可以使用它记录 方法调用。 图 8-11 IQC 界面的列队属性 现在看看 HelloWorld 应用程序。为指定应用程序级的属性,可使用.NET 框架的 ApplicationQueuing 属性。该属性的构造器将该属性设置成缺省值,这意味着应用程序被标 记为 Queued 而不是标记为收听外来消息。 为使收听可用,QueueListenerEnabled 属性必须设成真。装配件安装时,该属性使图 8-9 中 所见的 Listen 属性可用。 ApplicationQueuing 属性有另外一个名为 Enabled 的属性。Enabled 是一个指定应用程序是否 列队的 Boolean 型属性。使用缺省构造器可把该属性设成真并使应用程序的列队可用。在清 单 8-2 的代码中,已经把 QueueListenerEnabled 和 Enabled 属性设成真。 清单 8-2 HeIloWorId 列队组件 using System.Reflection; using System.EnterpriseServices; [assembly: Assemb_yKeyFile(“QCKey.snk”)] [assembly: ApplicationActivation[ActivationOption.Server]] [ assembly: ApplicationQueuing(Enabled=true,QueueListenerEnabled=true) ] namespace HelloWorld { using System; using System.EnterpriseServices; using System.Runtime.InteropServices; using System.Windows.Forms; [InterfaceQueuing] public interface IQC { void SayHello(string msg); } public class QC : ServicedComponent, IQC { public void SayHello(string msg) { MessageBox.Show(“HelloWorld.QC: “ +msg); } } public class MyApp { public static void Main() { IQC iqc; iqc = (IQC) Marshal.BindToMoniker(“queue:/new:HelloWorld.QC”); iqc.SayHello(“Hello!”); Marshal.ReleaseComObject(iqc); } } } 列队组件的开发围绕定义为列队的接口进行。接口在客户和组件间起胶粘剂的作用。接口提 供允许客户和组件同意它们之间如何相互交互的机制。在这个例子中,已经创建了名为 IQC 的接口。该接口只定义了一个方法:SayHello。SayHello 带一个字符串参数。注意该方法是 void 型的;它没有返回值。这是与本章前面提到的列队组件的规则是一致的。InterfaceQueuing 属性在组件注册时将该接口标记为列队的。该属性直接映射到图 8-11 中所见的 COM+属性。 列队组件类是在 IQC 接口的定义的下面定义的。像所有其他 C#中编写的 COM+类一样,该 类继承自 ServicedComponent。该类也实现 IQC 接口。该类的 IQC.SayHello 方法的实现在消 息框中显示 msg 变量。 到目前为止,还都是很直接的。这种基于接口的编程模型应当看起来很熟悉。在第 6 章中, 以相似的方式编写了组件。在控制台应用程序中事情可能要变得麻烦些。以定义接口变量 iqc 开始。这个变量保存着对标记返回的列队组件的引用。为激活标记和创建组件,可使用 Marshal.BindToMoniker 类。该方法的返回值总是一个 System.Object,而不管标记正在创建 的类型。为能够调用该对象上的方法,应把它强制转化成 IQC 类型。方法调用和赋值运算 操作符(等号)间的(IQC)完成这一任务。 一旦拥有对列队组件(特别是对记录器)的合法引用且已经将列队组件转化成一种可以使用 的类型,就可以调用其方法并释放引用。当使用完对象之后,调用 Marshal.ReleaseComObject 释放引用。此时,记录器创建 MSMQ 消息并试图将它置于组件的输入队列上。 8.5.2 松散结合事件与列队组件 可将前述的 HelloWorld 例子扩展成支持松散耦合事件。在下一个例子中,通过设置 EventClass 属性将前述的列队组件类转换成事件类。因为列队组件类现在是事件类,可以从 SayHello 方法个删除消息框函数,因为它不会以任何方式被调用。 当这些组件安装并注册时,列队组件被配置成事件类,且 IQC 接口被标记为列队的。QC 类 仍然起列队组件的作用。在客户调用 SayHello 方法并释放对象引用之后,方法调用被打包 进消息并发送到应用程序。在服务器端,播放器读取消息并回放方法调用。此时,COM+ 事件系统参与进来并通知订户(在这里是 QCSubscriber 类)。 清单 8.3 将事件与列队组件结合 using System.Reflection; using System.EnterpriseServices; [assembly: AssmblyKeyFile(‘QCKey.snk”)] [assembly: ApplicationActivation(ActivationOption.Server)] { assembly: ApplicationQueuing(Enabled=true,QueueListenerEnabled=true) } namespace HelloWorld { using System; using System.EnterpriseServices; using System.Runtime.InteropServices; using System.windows.Forms; [InterfaceQueuing] public interface IQC { void SayHello(string msg); } [EventClass] public class QC : ServicedComponent, IQC { public void SayHello(string msg) { } } public class QCSubscriber : ServicedComponent, IQC { public void SayHello(string msg) { MessageBox.Show(“HelloWorld.QCSubscriber: “ + msg); } } public class MyApp { public static void Main() { Iqueued component iqc; iqc = (IQC) Marshal.BindToMoniker(“queue:/new;HelloWorld.QC”); iqc.SayHello(“Hello!”); Marshal.ReleaseComObject(iqc); } } } 8.5.3 异常类 在这个最后的例子中,清单 8-2 中的列队组件类己扩展成定义异常类。ExceptionClass 属性 定义列队组件的异常类的 ProgID 或 CLSID。该类有一个构造器,其参数是代表异常类的 ProgID 或 CLSID 的字符串。如果指定异常类的 CLSID ,就使用 System.Runtime.InteropServices.GuidAttribute 属性定义一个 CLSID。如果不定义该属性,用 COM+注册类时也会定义一个。 清单 8-4 实现异常类 using System.Reflection; using System.EnterpriseServices; [assembly: AssmblyKeyFile(‘QCKey.snk”)] [assembly: ApplicationActivation(ActivationOption.Server)] { assembly: ApplicationQueuing(Enabled=true,QueueListenerEnabled=true) } namespace HelloWorld { using System; using System.EnterpriseServices; using System.Runtime.InteropServices; using System.windows.Forms; using COMSVCSLib; [InterfaceQueuing] public interface IQC { void SayHello(string msg); } [ExceptionClass{”HelloWorld.QCException”}] public class QC : ServicedComponent, IQC { public void SayHello(string msg) { MessageBox.Show(“HelloWorld.QC: “ + msg); } } public class QCExceptionClass: ServicedComponent, IPlaybackControl, IQC { //IQC 接口中的方法 public void SayHello(string msg) { MessageBox.Show(“HelloWorld.QCExceptionClass: “ & msg); } //IplaybackControl 接口中的方法 public void FinalClientRetry() { //准备最后的消息重试 MessageBox.Show(“HelloWorld.QCExceptionClass: FinalClientRetry”); } //IplaybackControl 中的方法 public void FinalServerRetry() { //准备最后的消息重试 MessageBox.Show(“HelloWorld.QCExceptionClass: FinalServerRetry”); } } public class MyApp { public static void Main() { IQC iqc; iqc = (IQC) Marshal.BindToMoniker(“queue:/new;HelloWorld.QC”); iqc.SayHello(“Hello!”); Marshal.ReleaseComObject(iqc); } } } 请记住例外类不仅必须实现列队接口也必须实现 IPlaybackControl 接口。IPlaybackControl 来自于 COM+服务类型库(comsvcs.dll)。为从装配件访问该接口,可使用类型库导入实用工 具(tlbimp.exe)。COM+服务类型库作为 COMSVCSLib 名称空间继续留了下来。通常在 FinalClientRetry 和 FinalServerRetry 方法中准备要调用的列队接口的方法。这里,只在屏幕 上显示了消息框,其中包括组件名和将要调用的方法名。 8.6 小结 如果读者通过阅读本章想保留什么内容的话,希望读者能理解列队组件在帮助获得 MSMQ 异步计算好处上的作用。在合适的地方使用列队组件时,它们可能大大提高应用程序的伸缩 性。读者进一步学习并开发.NET 应用程序时,问一下自己的客户是否需要立即从其组件获 得响应。如果答案是否定的,列队组件可能是一个好的解决方案。 第三部分 高级 COM+计算 第 9 章 远程处理 第 10 章 COM+和.NET 的将来 第 9 章 远程处理 本章内容包括: .NET 远程处理框架 SOAP 简介 远程服务组件 远程处理涉及两个跨越网络或是其他边界的互相通讯的应用程序。在使用.NET 以前,开发 人员使用诸如 DCOM 一类的远程处理结构。DCOM 使开发人员能够跨越网络或是跨越 Win32 进程的边界调用其他 COM 组件。一般来说,如果开发人员只在他们的内部网内进行 这种调用的话,它们的工作状况还是不错的。但是,随着 Internet 越来越普及,这种 DCOM 组件模型的弱点就出现了。当一个使用 DCOM 的应用程序需要进行跨 Internet 的调用时, 常常不得不穿越执行网络地址翻译(Network Address Translation,NAT)的防火墙、代理服务 器或是路由器。所有这一切对于 DCOM 应用程序来说就成了名符其实的“雷区”。 在远程处理方面最近的新进展是 Simple Object Access Protocol(SOAP,简单对象访问协议)。 SOAP 是一种连线协议,允许人们发送和接收跨网络的方法调用。SOAP 使用 XML 对方法 调用编码,并使用 HTTP 协议把这些调用传输到目的地。SOAP 解决了许多在 Internet 应用 程序中使用 DCOM 出现的问题。由于 SOAP 使用 HTTP 协议,来自 Web 浏览器的 SOAP 方 法调用和通常的 HTTP 请求,对于防火墙和代理服务器来说看起来是一样的。 .NET 远程处理框架对 SOAP 的支持几乎是无缝的。除此之外,它还通过使用其他传输和编 码器,例如 TCP/IP 和二进制编码器(也称为格式化工具),来支持远程处理。.NET 的远程处 理框架的“美妙”之处还在于,允许人们混合和匹配传输协议和格式化工具。人们还可以开 发自己的类,以便处理方法调用的网络和格式化问题。 读者可能会问,.NET 的远程处理与 COM+ 有什么关系呢?实际情况是,从 ServicedComponent 中派生的所有类都可插入.NET 远程处理框架。所有的 ServicedComponent 派生类都是从 System.ContextBoundObject 继承来的,而 System.ContextBoundObject 又是从 System.MarhsalByRefObject 继承来的。ContextBoundObject 比 MarhsalByRefObject 就保证一 个派生的 ServicedComponent 可安全地跨越网络或是应用程序域的边界进行传输。 9.1 .NET 远程处理框架 .NET 远程处理框架是一个复杂的论题。笔者认为光这个论题足可以写整整一本书。在本章 中,笔者并不打算讲述人们可利用.NET 远程处理框架所能做的每一件事。读者通过实现自 己的频道、ObJRef’s、格式化工具(这些术语将在稍后加以介绍)等就可以把此框架的意义大 大扩展。本章提供对适用于大多数.NET 类型(如 ServicedComponent)的远程处理结构和 框架做一个简单的介绍。 Marshal(调度)、endpoint(终点)和 well-known object(已知对象)这些术语在本章中将反复使用。 若不能至少理解一些这样的通用术语,要想进入像远程处理这样复杂的论题是相当困难的。 在继续之前,先要介绍一下在.NET 远程处理中这些术语的意义。 9.1.1 调度(marshaling)过程 当客户创建一个组件的实例时,实质上接收到一个指向实例驻留在内存中的地址的指针。这 种指针对于创建实例来说是有意义的。但是,指针在没有帮助的情况下,是不能传递到原有 应用程序域之外的。调度过程(marshaling)通过把在一个应用程序域有意义的对内存位置的 引用转化为在另一个应用程序域内有意义的地址,从而解决了这个问题。 但是,调皮过程涉及的范围比只把内存地址 A 转化为内存地址 B 更大一些。调度负责把方 法调用栈从客户传送到服务器。为了讨论本问题的目的,可把调用栈(call stack)看作是定义 执行时的方法调用的内存块。调用栈由以下元素组成: 参数类型和当前值 方法的可执行代码 返回值 方法抛出的任何意外错误 调度过程接过方法调用,然后将其转换为某种可跨连线(或是应用程序边界)发送的格式,从 而接收方应用程序可以检取消息并重新创建对象引用和方法调用。框架以相反的方向执行调 度:从服务器到客户。通常,方法包括需要调度返回到客户的输出参数、引用参数或返回值。 9.1.2 终点 在任何远程处理的讨论中,终点(Endpoints)都是共同的。如果读者熟悉 HTTP 的 URL,则 应 该很容易理解终点。在 HTTP 中,一个 URL 看起来是与下面类似的形式: http://www.hungryminds.com/mandtbooks/index.html 一个 URL 定义了指向 web 服务器的各种各样的终点。其中 http://标识了所使用的协议。对 于 Web 请求来说,协议自然是 HTTP。紧跟协议定义后的是 Web 服务器名称。在上面的 URL 中,Web 服务器是 www.hungryminds.com。Web 服务器名称后面跟的是文件夹和所请求的页 面。在本例中,我们请求的是 mandtbooks 文件夹中的 index.html 页面。文件夹和页面名告 诉 Web 服务器在哪个路径上可以找到所请求的资源。协议、Web 服务器名称和路径就定义 了一个终点。 在.NET 远程处理框架中,终点是以类似的方式定义的。想要启动远程组件的方法的客户必 须定义一个终点。与在 HTTP 的 URL 中的情况一样,客户必须通过提供协议、服务器名称 和资源名称来指明一个终点。例如,如果在想要从客户应用程序中访问名为 www.someserver.com 的服务器上的 Web 服务,则可将终点定义如下: http://www.someserver.com/myappfolder/mywebservice.aspx 这看起来很是相似。同样定义协议、Web 服务器和指向所要使用的资源的路径。在本例中, Web 服务器提供了试图访问的远程组件。当客户与 Web 服务器连接之后,mywebservice.aspx 页面具体实现了组件并进行所请求的方法调用。 但是,为了访问远程组件,.NET 框架允许使用其他协议,例如 TCP。正如读者在本章的“频 道”一节中所看到的,远程组件可在 Windows 服务中提供。为远程处理目的而提供.NET 组 件的任何 Windows 服务必须侦听 TCP/IP 端口,从而发现传进来的请求。在本例中,在提供 远程处理框架的终点时,客户必须指定端口号。例如,如果有一个客户想要与位于侦听 9000 端口的 Windows 服务上的远程组件相连接,则终点看起来与下面的形式类似: tcp://www.someserver.com:9000/RemoteComponentName 在本例中,笔者指定协议为 TCP。服务器名仍然是 www.someserver.com,但这次加上了端 口号 9000。当客户具体实现远程组件的实例时,远程处理框架具有足够的智能化,知道正 在请求的组件名是 RemoteComponentName。 9.1.3 已知对象 已知对象(Well-known objects)已经在远程运行库上加以注册。这些都是将类型信息以元数据 形式保存的对象。这些对象可认为是已知的,因为不管客户还是服务器都了解它们的存在以 及如何对它们加以处理。读者在下一节中将看到,可远程处理的对象可分为两个广泛的类别: 激活的客户和激活的服务器。这两种类型的对象都可认为是已知的。事实上,在本章中所编 写的所有的 COM+组件都可认为是己知的。 在上面有关终点的讨论中,笔者给出了一个 TCP 终点的例子。终点的最后部分定义了远程 对象的名称。这就是宿主应用程序在服务器上注册的己知对象的名称。 9.1.4 通过引用调度和通过值调度的对比 在设计远程类时应该考虑的首要事情之一是,想让客户获得它自己的类的本地副本呢还是让 类留在原来的服务器上,让客户远程地访问该组件。框架把前面示例中的类看作是 MarshalByVal(通过值调度)类型。当客户访问这种类时,框架复制正在运行的类的实例并将 其跨网络向客户传输。后面示例中的类可称为 MarshalByRef( 通过引用调度) 。 ServicedComponent 的派生类属于这种类别。 1.通过值调度 MarshalByVal 类不管何时被引用时都将以整体加以复制。实质上,类引用可以用以下方式变 为 MarshalByVal 类型(或是变为任何其他类型): 具体实现类的新实例 把类作为一个参数传送到方法调用 作为方法的返回值返回类的实例 访问另一个类的属性或是域 通过值调度的类必须实现 System.Runtime.Serialization.Iserialiable 接口或是用 System.Serializable 属性加以“装饰”。Iserializable 接口和 SerializabIe 属性在调度过程中用 于把类的内存中的代表转化为可在网络上传送的格式。Iserializable 让人们对类如何串行化 进行较多的控制。这个接口定义了一个方法: GetObjectData(SerializationInfo,StreamingContext) 。这个方法的第一个参数 System.Runtime.Serialization.SerializationInfo 是一个允许定义将要串行化的类型和值的类。 第二个参数 System.Runtime.Serialization.StreamingContext 是—个允许定义类的源和目的上 下文的结构。上下文可被指定为起源于另一个应用程序域或是一台位于别处的不同的计算 机。重要的是要注意到,MarshalByVal 类不必直接实现这两个选项。继承就足够了,既可是 直接的也可是间接的从实现 Iserializable 或是由 Serializable 属性引起的父辈类中继承。 用 System.Serializable 属性“装饰”一个类比实现 Iserializable 接口要简单一些。如果选择这 种方式,简单并易于实现,但却失去了某些灵活性。换句话说,人们得到的是运行库传下来 的。在已经用这个属性标记的类中,所有的宇段都被调度到目的应用程序域中了,但有一个 例外。System.NonSerializable 属性是用于(用 Serializable 标记的类的)字段的,而不是串行化 的。当运行库串行化一个类时,就跳过用 NonSerializable 标记过的字段。 2.通过引用调度 MarshalByRef 类驻留在创建这些类的应用程序域中。当客户具体实现新实例或是引用已有 的实例,MarshalByRef 对象留在原处不动。 为了使远程处理运行期间通过引用调度一个类,该类必须是从 System.MarshalByRefObject 或是从诸如 System.ContextBoundObject 的派生类中继承来的。正如读者现在所了解的,想 要使用 COM+服务的所有的类都是从 ServicedComponent 类派生来的。ServicedComponent 类是从 ContextBoundObject 类继承来的,而后者又是从 MarshalByRefObject 继承来的。这就 意味着,所有的 ServicedComponent 的派生类都可通过引用加以调度。图 9-1 说明了 ServicedComponent 的派生类的层次结构。 图 9-1 ServicedComponent 类的层次结构 MarshalByRefObject 和 ContextBoundObject 类被定义为抽象基类。像这样的抽象基类不能直 接实现。这些基类只能用作其他组件的基类。对于直接从基类继承来的 ServicedComponent 一类的类,字类必须实现类所定义的所有方法。顺便说一下,由于 ContextBoundObject 是个 抽象类,它就不一定实现 MarshalByRefObject 的方法。 3.在 MarshalByVal 和 MarshalByRef 中作出选择 如果要实现 ServicedComponent 的派生类,是选择 MarshalByRef 还是 MarshalByVal 还没有 定论。由于 ServicedComponent 是从 ContextBoundObject 继承来的,人们都知道,所有的 COM+ 组件都是通过引用调度的。但是,如果应用程序的其他部分需要在没有 ServicedComponent 帮助的情况下进行远程处理,如何来决定使用哪一个呢?笔者现在给出某 些准则以帮助作出决定。 能够跨网络移动而不会对性能造成影响的对象常常是通过值来调度的好的候选者。随着对象 变得越来越大,由于复制对象并跨网络传输所引起的性能影响,应尽量避免。 通常,维护着与特定机器有关状态的对象是不能通过值来调度的;这样—来,通过引用调度 就成了惟一的选择。考虑一个封装了对特定机器上文件访问的类。除非整个文件本身可以复 制到每一个客户,通过值来调度这样的类是不可行的。 许多大量使用 COM+服务的应用程序,如对象共享和事务处理,是部署在具有多个服务器 的服务器组上的。通常,这些服务器比典型的客户机的功能要强大得多。这些服务器常常配 置为执行特定的任务。当希望组件在服务器组上执行其任务时,通过引用调度这些组件是有 意义的。 4.上下文绑定的对象(Context-Bound Objects) 在继续讲述之前,先来解释什么是ContextBoundObject派生类以及为什么ServicedComponent 是从这个类继承来的。在.NET 远程处理中,一个上下文与 COM+中的上下文(曾在第 4 章中 讨论过)是类似的。可从两个角度来看上下文。一种方法是把上下文看作是类的属性袋。属 性袋中可能包括许多属性,用以告诉远程运行库,对此类的访问必须串行化,或者是此类正 在执行事务处理工作。还可以把上下文看作是应用程序域的子部。请记住,在.NET 中,应 用程序域是一个逻辑进程。一个 Win32 进程可以有多个在其内运行的应用程序域。由此可 得出结论,应用程序域可在其中有多个上下文。图 9-2 显示的是远程处理应用程序的 Win32 进程、应用程序域和上下文的清单。 图 9-2 上下文、应用程序域和 Win32 过程 每个应用程序域至少有一个上下文。这就是名为 default context 的上下文。当一个诸如 ServicedComponent 的类正在应用程序域内被实现时,可能创建新的上下文。当一个上下文 中的类型访问另一个上下文中的类型时,调用是通过一个对于发出调用的类型来说是未知的 代理对象来进行的。 ServicedComponent 是从 ContextBoundObject 中派生来的,因为所有的 COM+组件以及所有 的 ServicedComponent 派生类既可是在它们自己的上下文中创建的,也可以是在它们的创建 者的上下文中创建的。远程运行库必须能够把 ServicedComponent 派生类绑定到其上下文中, 以便保证不会违反像访问串行化这样的访问准则。 9.1.5 激活远程对象 为了调用任何种类的类上的方法,必须创建类的实例。当说到具体实现时,可远程处理的组 件分为两个类别:由客户激活的对象和由服务器激活的对象。在这两种情况下,客户继续使 用某种形式的对象创建 API,例如 new。当客户使用 C#的关键词 new 创建一个对象的新实 例时,就创建了由客户激活的对象(Client-activated objects)。另一方面,当客户调用方法时, 就激活了由服务器激活的对象(Server-activated objects)。 1.由客户激活的对象(Client-Activated Objects) 在由客户激活的对象中,客户控制着什么时候创建对象。当客户进行 API 调用创建组件的 实例时,远程结构创建远程组件并返回给客户它的引用。每次客户所做的实例化都在服务器 上创建一个新组件。 请记住,客户控制着什么时候创建由客户激活的对象。在远程结构的另一端,服务器并 不区分是由客户激活的对象还是由服务器激活的对象。客户决定什么时候使用特定的 API 来实现组件。 由客户激活的对象与由服务器激活的对象不同,允许使用参数化的构建器(parameterized constructors)来创建远程对象的实例。使用 new 关键词可能是使用参数化的构建器的最直接 的方式。使用参数化的构建器的另一种方式是利用 Activator.CreateInstance 方法。Activator 类是从 System 名称空间中来的。CreateInstance 是一种用于传递 object[]数组的重载方法。该 数组包括对象的构建器使用的参数。 2. 由服务器激活的对象(Server-Activated Objects) 由服务器激活的对象可分为两个类别:Singleton 和 SingleCall。运行在服务器上的远程运行 库而不是客户创建了实例。当客户调用对象上的方法时就创建这两种类型的对象。第一次客 户调用一个方法时,对象创建请求出现在网络发送上。.NET 远程框架并不支持使用参数化 的构建器来创建由服务器激活的对象。 SingleCall 对象与 COM+中的 JIT 组件非常类似。远程运行库每当客户调用方法时就具体实 现一个 SingleCall 对象(图 9-3)。请记住,如果一个组件已标记为启用 Just In Time Activation(JITA,及时激活)功能,COM+正好在方法调用出现之前自动地创建一个组件的实 例,除此之外,COM+在调用返回之后还销毁实例。SingleCall 对象以同样的方式工作。一 个远程对象的新实例为每个来自客户的方法调用服务。Singleton 对象仅当方法调用期间才 是活动的。 图 9-3 SingleCall 对象与多个客户 当客户进行方法调用时就创建了 Singleton 对象。进行方法调用的第一个客户经受了与创建 对象相联系的各种问题。从该客户或是从其他客户来的后续的调用接受来自原来的对象实例 的服务(图 9-4)。换句话说,Singleton 对象服务来自于一个实例的所有的客户请求。当对象 具体实现时,“租约”决定了 Singleton 对象的存活时间。“租约”具有与其相联系的缺省的 期限。一旦远程运行库确定“租约”已经过期,运行库就使对象失效。这件事一旦发生,对 象就成为垃圾收集过程(Garbage Collection)的合格对象了。 图 9-4 Singleton 对象与多个客户 当处理两类由服务器激活的对象时,理解状态是重要的。这两种类型的对象都是无状态的。 如果读者理解了在 COM+中的 JIT 组件是如何成为无状态的,就可以很好地理解 SingleCall 对象如何是无状态的。这种概念对两种开发模型来说是一样的。由于 SingleCall 对象在每个 方法调用之后被销毁,这样对对象来说就没有办法来保存任何状态—至少不能保存任何客户 的状态。 从技术上来说,Singleton 对象可以保存客户的状态,在某种情况下,还可以包括来自多个 客户的状态。如果对象使用由许多客户在以前设置的属性来更新数据库的话,数据库就可能 变为损坏的,在 Singleton 对象中避免保存状态的另一原因是,对象的“租约”在任何时间 都可能过期,而客户却不知道。如果发生这种情况,就不会生成客户可以检测到的意外错误。 原来的对象与远程运行库断开了连接。当进行下一个方法调用时,就又创建丁对象的新实例。 当对象销毁时,原来对象保存的状态也就丢失了。 如果需要在远程对象上维护状态的话,客户激活则成为最好的方法。由于新实例为每个客户 服务,这些类型的对象可以安全地保存状态。但是请记住,许多与非确定性的结束有关的准 则在此是适用的。如果对象保存着宝贵的资源,就应该实现某种关闭方法或是处理方法。如 果网络在客户能够调用这些方法之一之前出现故障,该种资源仅当远程对象变为不可得到时 才会释放(通过超出范围),这时“垃圾收集”过程就会出现。真到“租约”过期而且远程运 行库已经释放了对象时,对象才会超出范围。 9.1.6 代理 当客户具体实现一个远程组件的实例时,就能够调用组件上的方法,就好像它运行在自己的 应用程序域中一样。一旦用户想出了如何以所喜爱的方式来实现组件,就可以随便使用该实 例,就好像它是自己应用程序中的任何其他类型一样。代理使客户认为它正在处理实际的远 程对象的实例。事实上,代理是向远程框架转发方法调用,而远程框架接着又连接到远程机 器并向远程组件发出方法调用。远程框架实现两个代理,这两个代理协同工作以便提供以下 功能:透明代理和实际代理。 1.透明代理 透明代理(transparent proxy)截获来自客户的调用并将调用转发给实际代理对象。当客户创建 一个远程对象的实例时,不管对象的激活类型是什么(是由服务器激活的还是由客户激活 的),远程运行库返回一个透明的代理。当客户调用方法时,透明代理确认方法(及其参数) 是与要使用的类型一致的。换句话说,如果客户具体实现名为 Cfoo 的类并调用 Cfoo 的方法 (如 CFoo.DoSomething)时,透明代理截获对 DoSomething 的调用并探究以下几个问题: Cfoo 是否有 DoSomething 方法? 其方法的参数类型正确吗? 这是公共方法吗? Cfoo 是否处于另一个应用程序的域中? 透明代理使用远程对象装配件的客户本地副本来确认方法调用。看一下图 9-5 就会明白这一 点了。 图 9-5 透明代理截获方法调用 在图 9-5 中,读者可以看到控制台应用程序创建了 Cfoo 类的一个新实例。假设这是由客户 激活的对象而且 Cfoo 是从 MarshalByRefObject 继承来的。当客户对新的 Cfoo(步骤 1)调用 返回时,客户得到了透明代理的一个实例,虽然客户认为代理是 Cfoo。当发出对 DoSomething 的调用时,透明代理截获了该调用并对照客户的本地装配件中的类型信息对其加以确认。这 个装配件包括了 Cfoo 的定义。假设 DoSomething 的参数是正确的,则透明代理将调用转发 到远程处理的下一步骤:实际代理。 透明代理负责把客户的方法调用转换为可以传给实际代理的消息。消息中包括方法的所有参 数及其值。透明代理在运行期间对方法调用“拍”一个快照并将其转化为一个 System.Runtime.Remoting.Messaging.Imessage 接口。笔者将在本章的后面讨论消息。现在只 要理解透明代理负责构建消息并将其转发给实际代理就可以了。 透明代理是远程框架实现的内部类。程序员绝不能自己创建一个透明代理的实例。但是,有 时候,了解是在处理远程代理还是实际的实例还是有用的。 RemotingServices.IsTransparentProxy 是一个静态方法,可用来确定一个类的实例是透明代理 还是实际对象的实例。这个方法的参数是 System.Object。在前面的例子中,如果保存着 Cfoo 实例的变量名为 foo,这个调用可能是 RemotingServices.IsTransparentProxy(foo)。 2.实际代理与对象引用 实际代理(rea1 proxy)负责透明代理与其余远程运行库之间的通讯。笔者前面曾经提到,透明 代理将方法调用以 Imessage 接口的形式转交给实际代理。实际代理接过消息并将其转交给 适当的频道。笔者将在下一节讨论频道。现在,把频道想像为了解从线上如何接收与发送数 据的远程框架的一部分就可以了。 透明代理通过实际代理的 Invoke 方法把消息转发给实际代理。Invoke 方法只有一个参数, 就是要实现 Imessage 接口的对象。实际代理类是在 System.Runtime.Remoting.Proxies 名称空 间中定义的。实际代理类还是另一个客户不能直接实现的抽象类。正如透明代理的实现一样 实现实际代理。但是,与透明代理不同,如有必要,实际代理可加以扩展。 实际代理处理来自远程对象的通讯。当一个远程对象实例化时,实际代理将激活请求转交给 远程对象并等待响应。如果远程对象是通过引用来调度的,则返回的响应就是名为 ObjRef 的对象引用形式。ObjRef 是包括下面与远程对象有关信息的类: 远程对象的强名称 远程对象的类层次 远程对象的受支持的接口 远程对象的 URI 在服务器上注册的远程对象的频道列表 实际代理使用 ObjRef 中的信息来创建透明代理的实例。为了理解其工作原理,首先看一下 图 9-6,看一看当客户(通过激活)实例化远程 MarshalByRef 对象时发生了什么。 图 9-6 引用调度对象的客户激活 在图 9-6 中,当客户使用 new 关键词创建 Cfoo 的实例时,客户的激活请求发送到远程对象 的应用程序域。一旦创建了远程对象,远程处理运行库返回一个形式为 ObjRef 的远程对象 引用。在第 2 阶段,客户上的远程处理运行库看到了来自服务器的响应并创建 System.Runtime.Remoting.Proxies.RealProxy 的实例。实际代理读取来自 0bjRef 的关于远程对 象的信息并创建一个透明的代理(第 3 阶段)。一旦第 3 阶段完成,客户就可以自由地在远程 对象上进行方法调用。 9.1.7 频道 频道(Channels)是远程处理框架的一部分,可以接收消息并将其跨网络发送到目的机器上。 频道了解如何使用不同的网络协议(HTTP、TCP/IP、SMTP(电子邮件使用的)甚至还有 MSMQ) 进行通讯。.NET 框架在其门外支持的只有两种协议:HTTP 和 TCP/IP。为了使用上面的协 议或是任何其他协议,程序员必须实现自己的频道。与频道一起工作所需的类型位于以下名 称空间: System.Runtime.Remoting.Channels System.Runtime.Remoting.Channels.Tcp System.Runtime.Remoting.Channels.Http Channels 名称空间包括注册频道所需的类和实现自定义频道所需的接口。频道是为整个应用 程序域进行注册的。如果客户或是对象想要进行远程通讯,至少需要在应用程序域中注册一 个频道。在一个进程中,可在多个应用程序域中注册多个频道。 为了侦听同一个 TCP/IP 端口不能注册两个频道。为了理解这一局限,让我们返回一步并考 虑 TCP/IP 协议。请注意,当笔者在这里提到 TCP/IP 时,谈的并不是 TCP 频道,而是网络 协议本身。TCP/IP 把远程应用程序定义为 IP 地址加端口。TCP/IP 把一个端口映射为 Win32 进程。IP 地址指明机器。IP 地址与端口号一起定义了一个 TCP/IP 中的终点。TCP/IP 中的一 个准则是,不能有两个进程在同一端口上侦听信息流量。当笔者说在.NET 中不能有两个频 道注册于同一端口时,笔者确实认为这是 TCP/IP 所要求的。 频道划分为以下三个类别: 客户端频道 服务器端频道 既是客户端又是服务器端的频道 客户端频道(Client-side channels)根据 Channels 名称空间实现 IchannelSender 接口。客户端频 道只能从客户向服务器发送消息。服务器端频道侦听从客户来的消息。服务器端频道必须实 现 Channels.IchannelReceiver 接口。想要发送和接收消息的频道必须实现上面两个接口。 笔者前面曾提到,远程框架支持用于 HTTP 和 TCP/IP 协议的两种频道。正如读者可以猜到 的,HTTP 频道使用 Web 服务器为远程对象提供宿主。不管是客户还是服务器 HTTP 频道 使用来自 System.Net 名称空间的类型与 Web 服务器进行双向通讯。如果选择 HTTP 频道, 缺省地以 XML 格式对消息加以编码。XML 采用 SOAP 消息的形式。TCP 频道在传送消息 之前将消息编码为特殊的二进制格式。服务器端的 TCP 频道通常是以 Windows 服务为宿主 的。在这两种频道中,TCP 为应用程序提供较好的性能。通过 TCP 频道的消息在处理之前 不必通过 Web 服务器,而且不必由 XML 分析程序加以读取。但是 HTTP 频道确实提供了与 Internet 更大的互操作性。由于 HTTP 使用已知端口,如 80,这就比 TCP 频道对防火墙更为 友好。除此之外,如果客户正在通过代理服务器访问 Internet,HTTP 可以很容易地通过代 理服务器。 9.1.8 远程对象的寿命 在原来的 COM“时代”里,一个内部引用计数决定了对象的寿命。当客户将引用传递给组 件时,引用计数增加,当客户释放其引用时,组件的引用计数减少。当最后客户释放它保存 在组件中的引用时,引用计数变为 0,而组件将其本身从内存中删除。当 COM 组件被跨网 络远程处理时,问题就出现了。如果客户在释放对组件的引用之前断开与网络的连接,引用 计数不能变 0,而组件也不能删除其本身。.NET 远程处理结构使用稍有不同的处理方法。 当客户具体实现通过引用调度的类的实例时,就为该类创建了一个“租约”。该租约决定了 远程类的寿命。如果租约过期,而远程处理运行库不能续约的话,类就被取消引用并变为垃 圾收集过程的合适对象。 租约是实现 Ilease 接口的类型。Ilease 来自于 System.Runtime.Remoting.Lifetime 名称空间。 这个接口包括定义了租约内容的许多方面的属性: 租约中的剩余时间 租约的当前状态 租约的初始寿命 试图与发起人连接的时限 租约要求续签的时间量 事实上,租约中的剩余时间是,远程对象在与远程运行库断开连接并标记为可以进行垃圾收 集之前存活的时间。租约的状态可为以下几种: 活动的 过期的 已初始化但并未激活 未初始化 当前正在续约 上面列表中的第四种属性(未初始化)是租约合法的初始时间长度。一旦这个期间终结,如果 租约还未续签,则远程对象被释放。如果把这一属性设置为 null,相约就不会超时。在这种 情况下,附加于租约之上的远程对象,在应用程序域“撕毁”之前不会被释放。发租者是能 将租约续期的类。租约的发租者身分时限值决定了当远程对象试图与指定的发租者之一连接 时可以等待多长时间。如果租约快要过期了,而在由发租者时限指定的时间内发租者不能来 续期,则租约过期并释放远程对象。 上面列表中的最后的属性(当前正在续约)是当租约续期之后租期增加的时间量。租约可以多 种形式续期: 客户专门调用租约的 Renew 方法。 远程运行库与发起人联系从而续签租约。 客户调用远程对象上的方法。 客户调用方法时,为了租约续期,必须设置租约的 RenewOnCallTime 属性。 Ilease 接口定义了可用于以下目的的方法: 为租约注册发租者 续签租约 从租约中删除发租者 发租者是用 Register 方法来注册的。这是一个重载的方法,其参数之一是 Isponsor 接口。 Isponsor 接口还可在 System.Runtime.Remoting.Lifetime 名称空间中找到。重载的 Register 方 法除了 Isponsor 接口之外,还带有 TimeSpan 实例。TimeSpan 映射为 RenewOnCallTime 属 性。 租约是通过 ILease.Renew 方法来续期的。这个方法的参数为用于增加租约寿命的 TimeSpan 实例。 运行库保存着任何特定租约的发租者列表。当一个发租者要被从租约中删除时,就调用 UnRegister 方法。UnRegister 通过 ISponsor 的实例来注销发租者。这个实例是用在调用 Register 方法中的类之一。 租约是由 MarshalByRefObject 类的实现来初始化的:MarshalByRefObject 包括两个方法— GetLifetimeService 和 InitializeLifetimeService—用来初始化并返回实现 Ilease 接口的类。 GetLifetimeService 返回类型对象的实例。这个方法的调用者期望一个实现 Ilease 接口的类 型。任何 MarshalByRefObject 的实现应该保证,这个方法返回的对象可实现 Ilease 接口。 InitializeLifetimeService 方法初始化新租约并将其返回给调用者。通过实现这个方法, MarshalByRefObject 的实现可以初始化诸如发租者时限和初始寿命这样的租约属性。 9.2 SOAP 导言 当然,读者一定听说过与 SOAP 有关的问题,具体地说,SOAP 是与 Web 服务有关的。SOAP 是 Simple Object Access Protocol(简单对象访问协议)的缩写。SOAP 是一种规范。它建立起 了远程方法调用在网络上传输时可以采用的格式。SOAP 使用 XML 来描述与方法调用有关 的数据。在.NET 远程处理中充分支持的 SOAP 1.1 规范允许任何传输协议在线上携带 SOAP 消息。SOAP 使用的最通用的传输协议是 HTTP,但以下其他协议也可使用: 简单邮件传输协议(Simple Mail Transfer Protocol,SMTP) 文件传输协议(File Transfer Protocol,FTP) 消息队列(Message Queuing, MSMQ 或 IBM MQ Series) 远端过程调用(Remote Procedure Call,RPC) 这绝不是可与 SOAP 一起使用的协议的详尽的列表,但笔者希望这个列表能够给读者有关 SOAP 可以使用的协议非常广泛这样一个概念。为了讨论的目的,把 SOAP 使用的协议集中 于 HITP 上,因为这个协议是最通用的实现方式。 9.2.1 HTTP 头 为了使用 SOAP 和 HTTP 进行方法调用,客户必须了解如何生成正确的 HTTP 请求头。先 看一下人们常常可在 SOAP 请求中见到的 HTTP 头: POST /MyApp/MyPage.aspx HTTP 1.1 Host: www.myserver.com Content: text/xml Content-Length: 100 {crlf} <<这里是 Post 方法的数据>> 请求头的第一行指定了三种信息: HTTP 请求方法 所请求资源的路径 要使用的 HTTP 的版本 请求方法可以是任何合法的 HTTP 请求方法。两种最常用的方法是 Get 和 Post。当打开浏览 器并键入一个 URL 或是单击 Web 页面上的链按时,使用的就是 Get 方法。但也不总是如此, 有时当填入 Web 页面上的表单并提交时,常常使用的是 Post 方法。当处理 SOAP 时,最好 使用 Post 方法,这有几个原因。首先,Post 方法允许人们发送比 Get 方法更多的数据。第 二,当用 Get 请求传递信息时,该信息必须使用问号附加于路径的尾部。如果想在请求中发 送大量信息时,就有可能变得有点麻烦。请求中的第二行代表请求要达到的 Web 服务器。 通常,这是完全合法的域名,但也可以是 NetBios 名称或是 TCP/IP 地址。请求的第三和第 四行分别代表内容的类型和长度。对于 SOAP 请求来说,内容类型是 text/xml,这就意味着 后面的数据是 XML.Content-Length(内容长度)告诉 Web 服务器在请求中有多少字节的数据。 此处笔者将内容长度设置为 100 字节。在内容长度之后的回车换行符的组合({crlf})表示 HTTP 头部的结束以及有效负载的开始。在笔者的例子中,<>用来代表请求 的有效内容的。对于 SOAP 请求,这代表着 SOAP 消息。笔者将在下一节中讲述 SOAP 消 息。 9.2.2 SOAP 消息 SOAP 消息是一个 XML 文档,可由两部分组成:SOAP 头和 SOAP 体。消息的头和体要放 在称为 SOAP 信封的顶级 XML 元素内。从概念上来说,SOAP 消息看起来如图 9-7 所示。 图 9-7 SOAP 消息的逻辑结构 通常,SOAP 信封元素看起来与下面的形式类似: 这段代码清单的第一行指定元素名称。将 SOAP 信封元素命名为 Envelope 还是合乎逻辑的。 在每条消息中都必须存在 Envelope 元素。SOAP-ENV 部分是名称空间的别名。SOAP-ENV 别名用于提供一个指向在第二行中定义的名称空间的句柄。Xmlns 是用来指定规则的属性。 对于 SOAP 信封来说,规则是在 URL:http://schemas.xmlsoap.org/soap/envelope/中定义的。 信封结构包括定义头部和体元素的信息,以及其他影响对消息处理的属性。笔者一会再描述 其他属性。代码清单中的最后一行指定对方法调用编码的规则。编码规则定义了可用于 SOAP 中的数据类型。某些数据类型如下所示: Doubles (双精度) Strings (字符串) Boolean values (布尔值) Floats (浮点数) Arrays (数组) Dates (日期) SOAP 头是个可选的元素。如果存在消息头,SOAP 规范规定,消息头必须是 Envelope 元素 的第一个子元素。SOAP 头可用于以下多种目的,由一种都与自定义 SOAP 消息有关: 自定义认证 事务处理 将这个消息与其他消息相联系 路由消息 当人们看到实现 SOAP 头时,实现自定义认证或许是第一个想到的事情。在本例子中,接 收应用程序可读取头部信息,并在处理其余消息之前认证客户。以前提到的其余例子也以相 同的方式工作。消息头的目的是允许远程处理框架以任何适当的方法来扩展消息。 笔者想要回过头来讲述前面提到的 SOAP-ENV 名称空间的属性。这个名称空间定义了两个 影响消息头的全局属性:actor 和 mustUnderstand。Actor 属性(以 URI 的形式)定义了一个特 定头部元素打算使用的容器。SOAP 不能直接从发送者传送到信息的接收者,在其路上的某 处,其他应用程序可能需要读取消息并在将其继续发送之前对其执行某些处理过程。Actor 属性指出头部的哪个元素是为哪个接收者使用的。一旦接收者读取了头部元素,它必须对元 素进行远程处理或者用为下一个接收者准备的元素将其取代。 在 SOAP-ENV 名称空间中定义的第二个属性是 mustUnderstand。这个属性是个布尔值(设置 为 0 或 1)。如果设置为 1,消息的接收者必须或是处理头部元素或是拒绝 SOAP 消息并向发 送者返回一个错误。这个标记保证消息的处理者至少要试图处理元素而不能只是跳过。为了 感受这些属性如何施加于 SOAP 头,清单 9-1 中给出了一个示例消息头。为了易读性,没有 包括顶级的 Envelope 元素。 清单 9-1 SOAP 头的示例 MyAuthenticationMethod 头部之后是必须有的 Body 元素。Body 元素可包括以下三种类型的子元素中的任何一个: 方法调用 方法响应 错误响应(故障) 方法调用包括方法的名称、参数名称及其值。SOAP 规范的当前版本对一条消息只支持一个 方法调用。 假设客户进行如下名为 PlaceOrder 的方法调用: // 方法特征:Placeorder(string Sku, int CustomerNumber, int Qty) int 0rderNumber = someobj.PlaceOrder(“sku123”, 1009, 3) 在这个方法调用中,客户为消费者 1009 发出购买 3 单位的产品号为 skul23 的产品的定单。 用于此目的的 SOAP 消息体如清单 9-2 所示。 清单 9-2 SOAP 体示例 sku123 1009 3 在清单 9-2 中,方法参数名及其值被转化为方法名元素:PlaceOrder 之下的元素。当服务器 成功地处理这一请求时,就以方法响应消息的形式将响应发送回来。方法响应消息向发送者 返回方法响应值和输出参数。在示例中,PlaceOrder 方法返回一个整数但没有输出参数。生 成的 SOAP 消息如下所示: 988 方法调用响应的命名约定是在方法名称的结尾处添加单词 Response。类似的约定用于返回 值。但加的不是 Response,而把 Result 用于命名包括方法调用返回值的元素。如果在方法 调用中有输出参数,这些参数以与结果的子元素出现。 故障消息是在消息体中可找到的最后的子元素,当在传送的接收端出现某种错误或是当引起 需要显示的某种状态信息时,就发送故障消息。SOAP 的故障元素定义了四个组成故障消息 的下级元素: faultcode faultstring faultfactor detail faultcode 是完全与 HTTP 协议中的响应代码类似的错误代码。客户或是其他软件在编程时应 该使用 faultcode 来决定错误的原因。 faultstring 与 HTTP 中不时看到的错误消息类似。这个元素为用户或是开发人员提供了可以 查看的错误消息。 faultfactor 识别错误的来源,其形式是 URL 的形式。如果请求消息的头部元素已经被指定为 actor,而 actor 又产生了错误,则 faultfactor 指向 actor 的 URI。 detail 元素包括与消息体的特定处理有关的信息。如果这个元素不存在,故障消息的接收者 可认为原来的请求体还未加处理。下面是故障消息的一个例子: SOAP-ENV:VersionMismatch invalid namespace for SOAP Envelope 9.3 ServicedComponents 的远程处理 笔者前面曾经提过,ServicedComponent 派生类由于是 MarshalByRefObject 类的非直接子类, 而自动支持远程处理。这就意味着,任何 ServicedComponent 派生类可以由引用跨网络进行 调度。在本节中,读者要研究 3 种 ServicedComponent 类: 使用 SOAP 和 HTTP 频道的 SingleCall 组件 使用 TCP 频道的 singleCall 组件 由客户激活的 ServicedComponent 9.3.1 使用 SOAP 和 HTTP 的 SingleCall 组件 先以名为 Cfoo 的 SingleCall 服务组件开始。这个组件的代码显示在清单 9-3 中。 清单 9-3 使用 SOAP 的 SingleCall 组件 Namespace RemoteComponent { using System.EnterpriseScrvices; public class CFoo : ServicedComponent { public int PlaceOrder(string Sku, int CustomerNumber, int Qty) { //在此做添加定单的数据库工作 //返回定单号码 return OrderNumber; } } } 正如读者可以看到的,关于 Cfoo 类并没有什么特别的。为这个名称空间生成的装配件名是 RemoteComponent。当这个组件需要找宿主时,事情变得有趣起来。例如在本例中,笔者使 用 Web 服务器做组件的宿主。由于笔者正在使用 Web 服务器,也要选择 HTTP 频道和 SOAP 格式化工具。为了为组件提供宿主,必须创建一个虚拟目录。可使用 Internet Services Manager 插件来创建虚拟目录。假设在一台名为 www.myserver.com 的服务器上已经创建了一个虚拟 目录。虚拟目录的名称是 RemoteComponent。在 RemoteComponent 目录下必须有一个名为 bin 的目录。当客户试图访问 Web 服务器上的这个组件时,远程处理运行库查看 bin 目录, 以便找出装配件的 dll 文件。所需的最后一件事是配置 web.config 文件。这个配置文件要由 许多应用程序来使用,如 ASP.NET 页面。例如在本例中,web.config 文件告诉远程处理运 行库想要对这个组件使用的服务器激活是什么类型(SingleCall 还是 Singleton)。笔者在本例 中使用的 web.config 文件如清单 9-4 所示。 清单 9-4 将 web.config 用于远程处理 这里传达出的实际信息是在 wellknown 元素及其属性中。mode 属性指定为服务器激活模式 (在本例中是 singleCall)。type 属性指定完全合格的类型名(名称空间+类名)和装配件的名称。 不管是装配件名还是组件的名称空间都叫做 RemoteComponent。最后的属性是 objectUri。 在 URI 尾部的 RemoteComponent.soap 文件并不存在。当 Web 服务器看到对带有.soap 扩展 名的请求时,就激活了处理程序。用于.soap 扩展名的处理程序向远程处理运行库转发该请 求。 这就是在 Internet 信息服务(Internet Information Services)中为组件提供宿主所要做的大致的 一切。请注意,笔者没有指明频道或是格式化工具。由于正在为 Web 服务器中的组件提供 宿主,远程处理运行库认为是 HTTP 频道和 SOAP 格式化工具。清单 9-5 中的代码是用于 RemoteComponent 类的客户的。 清单 9-5 RemoteComponent 类的客户 using System; using System.Runtime.Remoting; using System.Runtime.Remoting.Channels; using System.Runtime.Remoting.Channels.Http; using RemoteComponent; Public class ClientApp { public static void Main() { HttpChannel http = new HttpChannel(); ChannelServices.RegisterChannel(http); Type trc = typeof(RemoteComponent); RemoteComponent re =Activator.GetObject(trc, “http://www.myserver.com/RemoteComporent/RemoteComponent.soap”); int OrderNumber = Re.PlaceOrder(“sku123”, 1009, 3); Console.Writeline(OrderNumber.ToString()); } } 事实上,客户要通过三步处理过程: 1. 创建并注册一个 HttpChannel 2.创建一个 RemoteComponent 的新实例 3.通过调用 PlaceOrder 方法使用 RemoteComponent Activator 类是来自 System 名称空间的静态类,它可用于创建当前应用程序域内或域外的对 象。GetObject 方法定义了所创建的远程组件的类型和组件的终点。typeof()语句是一种 C# 关键字,可返回类型为 system.Type 的对象。有了在 RemoteComponent 装配件的本地副本和 终点中指定的类型信息,远程处理运行库就能够找到并使用远程组件。请注意,客户指定的 终点是与服务器上的 web.config 文件中指定的路径是相同的。远程处理运行库使用客户和服 务器上的路径信息来匹配对比 RemoteComponent 组件的请求。由于这是一个由服务器激活 的组件,直到 PlaceOrder 方法被调用时才具体实现该组件。 在清单 9-5 中,客户在代码中指定了终点和频道信息。可以在配置文件中包括这种信息。在 清单 9-6 中,笔者将这种信息拿出代码并将其放在名为 ClientApp.config 的配置文件中。 清单 9-6 ClientApp.config 配置文件 //文件名:ClientApp.config 在这个配置文件中,笔者创建了一个已知类型:RemoteComponent.Cfoo。当客户启动时,就 指导远程框架装入配置文件并把 CFoo 注册为客户上的已知类型。由于已经指定了 HTTP 频 道,远程处理运行库通过这个频道把请求转发到 http://www.myserver.com/RemoteComponent/RemoteComponent.soap。显示在清单 9-7 中的客 户代码已经被大大地简化了。 清单 9-7 在客户中使用 ClientApp.config using System; using System.Runtime.Remoting; using RemoteComponent; Public class ClientApp { public static void Main() { RemotingConfiguration.Configure(“ClientApp.Config”); CFoo foo = new CFoo(); int OrderNumber = foo.PlaceOrder(“sku123”, 1009, 3); Console,WriteLine(OrderNumber.ToString()); } } RemotingConfiguration 类用于代表客户来配置远程处理框架。一旦配置文件被装入,就可以 自由地使用远程类,就好像它是本地类一样。 9.3.2 使用二进制格式化工具和 TCP 的 SingleCall 组件 在客户和服务器上使用 TCP 频道并不比使用 HTTP 频道困难。但是如果使用 TCP 频道,就 必须找到与 Web 服务器不同的宿主来为组件提供宿主。例如在本例中,笔者任用一个控制 台应用程序为组件提供宿主。通常,人们想在 Windows 服务中为组件提供宿主,这样一来 某人就不必登录到服务器控制台了,但是在本例中,控制台应用程序就足以演示 TCP 频道 了。可以在组件端使用配置文件来配置远程处理框架。这一过程的配置文件与前面使用的 web.config 文件看起来是相似的,且是以与客户注册 ClientApp.config 文件类似的方式注册 的。由于已经讲过如何使用 web.config 文件,笔者不打算在下一示例中再重复这些步骤。相 反,笔者通过代码注册已知类型。控制台应用程序为组件提供宿主使用的代码显示在清单 9-8 中。 清单 9-8 用于 RemoteComponent 的控制台应用程序 Using System; Using System.Runtime.Remoting; Using RemoteComponent; public class ComponentApp { public static void Main() { TcpChannel tcp = new TcpChannel(8000); Type t = typeof(RemoteComponent.CFoo); string uri = “tcp://www.myserver.com/RemoteComponent/”; RemotingConfiguration.RegisterWellKnownServiceType( t, uri, WellKnownObjectMode.SingleCall); Console.WriteLine(“Listening for Requests on port 8000 _”); Console.ReadLine(); } } 笔者为本例选择的端口是 8000。远程处理框架在这个端口上侦听外来的请求。对 RegisterWellKnownServiceType 的调用把 RemoteComponent 注册为 SingleCall 对象。笔者将 最后两行放在 Main 方法中,这样一来,只要注册了 Cfoo,应用程序就不会退出。如果控制 台应用程序还未运行,就不能侦听请求。 如果笔者继续前面的客户 ClientApp,就不必改变它的任何代码。为了使用正确的终点、频 道和端口,只需要改变配置文件,正如清单 9-9 所示。 清单 9-9 从 ClientApp.config 中使用 TCP 频道 //文件名:ClientApp.config 对配置文件的修改改变了组件在端口 8000 上使用 TCP 协议的组件 URL 和侦听 TcpChannel 而不是 HttpChannel 的频道。笔者不需要改动客户代码是使用配置文件来配置远程处理框架 的好处之一。 请注意,这些例子都没有指明格式化工具。使用远程处理框架把格式化工具与频道混合并匹 配是可能的。当指定一个诸如 HttpChannel 一类的频道时,远程处理框架把缺省的格式化工 具与其相关联。HttpChannel 使用的缺省格式化工具是 SOAP;而 TcpChannel 使用的缺省的 格式化工具是 Binary 格式化工具。 9.3.3 由客户激活的 ServicedComponent Cfoo 可根据客户请求来激活。这一任务既可通过对 new 的调用也可使用 Activator.CreateInstance 方法来完成。在本例中,笔者选择了 Activator.CreateInstance 方法。 服务器端的代码可以不变。可用任意多的宿主,如Web 服务器、控制台应用程序或是Windows 服务来为组件提供宿主。在本例中,假设使用 Web 服务器来为组件提供宿主。客户使用的 代码显示在清单 9-10 中。 清单 9-10 由客户激活的 ServicedComponent 类 using System; using System.Runtime.Remoting; using System.Runtime.Channels; using System.Runtime.Channels.Http; using RemoteComponent; Public class ClientApp { public static void Main() { RemotingConfiguration.Configure(“ClientApp.Config”); CFoo foo = (CFoo) Activator.CreateInstance(Typeof(CFoo)); int OrderNumber = foo.PlaceOrder(“sku123”, 1009, 3); Console.WriteLine(OrderNumber,Tostring()); } } 在清单 9-10 中,客户装入配置文件并调用重载的 CreateInstance 方法之一。当这个方法执行 时,创建请求发向远程组件。远程组件的构建器被调用,把 ObjRef 传递给客户。当客户上 的远程处理运行库接收到 ObjRef 时,就创建了 RealProxy,接着再由 RealProxy 创建透明代 理。一旦 RealProxy 创建了透明代理,就向客户返回透明代理的引用。 CreateInstance 总计有 8 个重载方法:其中一些重载允许创建组件的实例,而不必引用装配 件。在这种情况下,该方法返回一个 System.Runtime.Remoting.ObjectHandle 类的实例。这 个类可用与向应用程序域而不是向客户的应用程序域传递对 RemoteComponent 的引用。这 个方法的另一个重载允许人们向远程组件构建器指出参量。例如,如果 RemoteComponent 类具有非缺省的构建器,就可以把 System.Object 数组传递给重载之一。数组中的每个元素 代表指定给组件构建器的一个参量。 9.4 小结 .NET 远程处理框架是一个广泛的专题。单独一章确实不足以讲述其所有的方面。在本章中, 笔者试图给读者以该框架的基本组件以及诸如 SOAP 相关技术的一般概况。 作为一名使用.NET 框架的 COM+开发人员,具有许多远程处理组件的选项。由于 ServicedComponent 派生类对 COM 客户是可见的,这样就可以使用 DCOM 的选项。正如读 者在第 8 章中所看到的,也可以使用排队组件通过 MSMQ 来传输方法调用。正如在本章所 看到的,.NET 远程处理框架提供了远程处理组件的第三种选项。.NET 远程处理框架比前面 的选项具有更多的可扩展性。经验不足的开发人员使用该框架的缺省功能就可快速地入门, 当然经验较多的开发人员可以编写出自己的格式化工具和频道。 第 10 章 COM+和.NET 的未来 本章内客包括: COM+ l.5 的新功能 IIS 6.0 的新功能 MSMQ 的新功能 本书到目前为止,已经讲述了 Windows 2000 中 COM+的每一个主要功能。笔者认为,如果 让读者进一步了解一下名为 Windows 2002 Server,也就是 Windows 2000 Server 下一版本 COM+的新功能还是有意思的。在写作本书时,Windows 2002 服务器上的 COM+的下一版 本代码名称为 COM+ 1.5,不过这个名称在发布以前很可能发生变化。除了要讲述 COM+ 1.5 的新功能之外,笔者还要向读者介绍一下 IIS 6.0 和 MSMQ 的内幕。正如读者在本章中将要 看到的,这些功能许多都致力于改善 Web 应用程序的健壮性。在继续之前,应该向读者指 出,本章中的信息来自于 Windows 2002 的 β2 版。在本章中叙述的某些功能有可能不会放入 产品的最终版本。相反,在本章中未提及的功能却有可能出现在产品的最终发行版中。 10.1 COM+ 1.5 的新特性 随着 MTS 和 COM+逐步成熟,提供了很多服务于组件的新特性。COM+ 1.5 继续发展,它 的许多新功能为应用程序提供了更为可靠的环境。某些功能可帮助人们改写和部署应用程 序。甚至在本书正在写作时,Microsoft 又在其 β2 版本中增加了九种新功能: COM+应用程序作为 Windows NT 服务 应用程序分区 应用程序进程转储 组件别名 可配置的隔离级别 低内存量激活门 进程回收 公共和私有的组件 应用程序共享 10.1.1 作为服务的 COM+应用程序 Windows 2000 提供了像 IIS、COM+ Event System(事件系统)和计算机浏览器(Computer Browser)这样的服务。甚至 Windows 2002 中的 COM+ System 软件包都已经配置作为服务来 运行。服务可以配置为自动开始(当 Windows 启动时)或是手工开始。 COM+ 1.5 提供了一种新的激活选项,允许一个 COM+应用程序变成 Windows 的服务应用程 序。图 10-l 显示的是 COM+应用程序中这一功能的新选项。 应用程序在作为服务运行之前必须配置为服务器应用程序。在图 lO-1 中笔者选择厂 Run Application as NT service(作为 NT 服务来运行应用程序)选项,从而使这个应用程序可以作为 服务来运行。当施加这个改变时,就可以像配置 Windows 中的其他服务一样来配置这一服 务了。 当用户单击了 Setup New Service(建立新服务)按钮时,就出现了如图 10-2 所示的 Service Setup (服务配置)对话框。在此对话框中,用户可以配置以下任何常用服务选项: ∗ ∗ ∗ ∗ ∗ 服务名 启动类型(手工或自动) 错误处理 运行服务的帐户 对其他服务的依赖性 图 10-1 新服务激活选项 图 10-2 Service Setup 对话框 服务名就是显示在 Services MMC 插件中的名称。如果服务的启动类型设置为自 动的,当操作系统启动时服务也就启动了。另一方面,如果选择了手工选项,管 理员必须进入 Services 插件中并启动该服务。错误处理选项用于指定造成服务 不能启动的严重程度。根据严重程度可采取不同的补救措施。服务运行其下的用 户帐户也可以在这一步骤中加以配置。正如任何其他服务一样,用户可以选择是 把服务作为本地系统帐户来运行还是作为另一个本地用户帐户或是域用户帐户 来运行。在图 10-2 所示对话框的底部,有一个指定应用程序所依赖的其他 服务的选项。例如,如果通过 IIS 访问应用程序,就可以把 IIS 指定为依赖程序。 当把应用程序配置为作为服务来运行,人们就可以在 Services 插件中看到它。 图 10-3 显示的是在 Services 插件中列出的 NtServices 应用程序。虽然不能看 到全部描述,但这个域是从应用程序的描述域中接收信息的。 图 10—3 作为 Windows 服务的 NtServices 应用程序 那么什么时候这种功能有用呢?有几种情况下可使应用程序从这一功能中获得好 处。某些时候,人们可能想要一个应用程序在本地系统帐户身份之下运行。虽然 出于安全原因,这样做并不总是明智的,但由于其他原因也许需要某种东西不得 不这样做。人们有时想要应用程序在操作系统启动时就启动。当用户不再想使用 它们时重新启动服务器是明智的。这一功能允许在用户请求启动应用程序之前启 动应用程序。用这种方法,每一个请求应用程序中的组件的用户就不会遭受启动 服务器软件包的性能降低。沿着此路走下去,人们可能想要把包括共享组件的服 务器应用程序配置为服务。如果应用程序作为服务启动,组件共享池就可安 放停当(当然是以最小值)并可随时使用。同时,当服务器应用程序启动并安放组 件共享池时,第一个用户就不会遭受性能降低。 10.1.2 应用程序分区 应用程序分区(Application Partitions)允许把一个应用程序的多个版本(服 务器或是库程序)安装在计算机中。在一台计算机中安装应用程序的不同版本允 许人们配置软件包的不同方面,如安全设置和角色。如果要分区的应用程序是服 务器应用程序,就可配置为在不同的用户帐户下远行。图 10-4 显示了应用程序 和两个应用程序分区的逻辑关系。在本图中,Application A 配置为处于 Partitions A 和 B 中。 图 10-4 一个应用程序在两个分区中 读者应该理解,应用程序事实上是从一个分区复制到另一分区的。驻留在应用程 序中的组件不会改变。创建应用程序分区时以任何方式改变组件都是不必要的。 根据客户的安全性证书,COM+将服务于来自于任何一个允许的应用程序分区的组 件请求。 先在 COM+的 Application Partitions 文件夹内创建一个分区,就可建立应用程 序分区。创建新分区的第一步是给分区取一个名字。在图 10—5 中,笔者把新分 区称为 SalesAccounting。在这一步骤中,Partition ID(分区标识)自动地赋于 该分区。Partition ID 是一个 GUID,与赋于任何应用程序、接口或是组件的 GUID 类似。除了 Partition ID 之外,分区具有在功能上与应用程序属性类似的四个 属性。分区属性如下; 分区名称 ∗ ∗ ∗ ∗ ∗ 分区标识(Partition ID) 描述 禁止删除(Disable Deletion) 禁止改变(Disable Changes) 当应用程序分区创建之后,它就被列在了 COM+ Partitions 文件夹中了。 在图 10-6 中,读者可以看到列在 COM+ Partitions 文件夹中新 SalesAccounting 分区。 图 10-5 创建一个新应用程序分区 图 10-6 新的 SalesAccounting 分区 可在分区内部创建应用程序,就如同创建任何正常的应用程序一样。当在分区内 创建应用程序之后,就可以复制到另一分区去。不同的应用程序分区可以映射为 不同的用户。用户既可是本地机器用户也可是域用户。 读者可能已经在图 10—6 中注意到了 Base Application 分区。这是没有指派给 特定分区而作为缺省的应用程序分区而存在的。 分区本身可以组织为分区集。可为本地机器用户或是域用户创建分区集 (Partition Sets)。在域控制器上创建的分区集可为域用户和用户组所用。在活 动的录域中,用户被映别进 Organization Units(OU,组织单元)中。可使用 OU 来定义对特定集的访问。当一个域用户请求作为分区集一部分的一个应用程序 时,用户的 OU 被映射为一个分区集。如果用户的 OU 不能被映射为一个分区集, 用户就只能具有对 Base Application 分区内的应用程序的访问权。用户域的标 识还帮助 COM+确定应用程序所在的分区。附带说明一下,Base Application 分 区缺省地是每个分区集的一部分。 10.1.3 应用程序进程转储 COM+应用程序可以配置为在应用程序出现故障时转储其进程的映像。管理员可手 工转储一个运行的应用程序。转储就是运行时应用程序在内存中的快照。这种信 息以后可用于诊断诸如与应用程序相联系的内存意外等问题。图 10-7 显示了一 个应用程序的属性对话框上的 Dump(转储)选项卡。缺省地,COM+把应用程序的 内存状态转储到%systemroot%\system32\com\dmp 文件夹。在 Dump(转储)选项 卡中可配置转储映像的数目。 图 10-7 应用程序的 Dump 选项卡 应用程序可在运行时加以转储。管理员可以对运行的应用程序拍摄无侵犯式的快 照。开发人员可使用一种工具,例如在 Platform SDK(平台软件开发工具箱)中 提供的 Windows 调试工具来查看转储的信息。 10.1.4 组件别名 组件别名(Component aliasing)允许人们在应用程序或是在多个应用程序中安 装一个组件的多个实例。当为一个组件起了别名时,该组件就获得了一个新的 ProgID 和新的 CLSID。 管理员或是开发人员可以选择性的指定新的 ProgID 和新的 CLSID,但是 Component Services 插件为其提供缺省值。图 10-8 显示的是 Alias Component(为组件起别名)对话框。利用这一 对话框,就可以指定在其中为组件起别名的应用程序。 图 10-8 Alias Component 对话框 使用组件别名,开发人员可为一个组件起一个别名,以便指定多个构建器字符串,或许是为 了用于不同的数据库。对于起了别名的组件,其任何组件属性(例如,事务处理支持、激活 特性和并发级别)都可加以改变。 10.1.5 可配置的隔离级别 在第 4 章中,笔者曾利用 COM+事务处理讲过隔离级别。隔离级别决定了事务处理过程中 资源锁定的级别。隔离级别越高,则锁定方案越受限制。最高的隔离级别是 Serialized(串行 化的)。这是 Windows 2000 上在 COM+受支持的惟一隔离级别。 另一方面,COM+的下一版本允许把隔离级别配置为下面的任何一种 任意(Any) ∗ ∗ ∗ ∗ ∗ 读取未提交数据(Read uncommitted) 读取提交数据(Read committed) 可重复读取(Repeatable Read) 串行化的(Serialized) 选择比 Serialized 低的隔离级别可改善性能和应用程序吞吐量。但是一定要清楚,选择与 Serialized 不同的隔离级别有可能让数据的其他读取者看到数据处于不一致的状态。一个组 件在能够设置隔离级别之前,必须具有 Supported(受支持的)或更高的事务处理属性。 10.1.6 低内存激活门 低内存激活门(low-memory activation gate)特色的目的是在内存不足的条件下不许创建组件。 当在内存不足的条件下创建组件时,会在应用程序中出现问题。如果应用程序具有内存泄露, 泄露可耗尽可用的内存。当这种情况出现时,应用程序反应迟缓而必须关闭。 COM+通过对照阀值检查应用程序的虚拟内存使用率来防止组件的低内存激活。如 果可用的虚拟内存处于阀值之下,COM+就使客户的创建请求失败。COM+为所有应 用程序预先确定了阀值。客户不能改变这一阀值。 10.1.7 进程回收 笔者曾经遭遇到无数的反应迟缓的应用程序。当应用程序变得反应迟缓时,人们 所能做的惟一一件事就是进入 Component Services Explorer(组件服务管理器) 将其关闭。应用程序变得反应迟缓的原因很多。除非从变得反应迟缓的应用程序 收集运行时的信息,否则不可能决定问题之所在。 在如.NET 这样的受管理的环境中运行应用程序可减少这样的问题, 但是仍然存 在这一问题。如果某人编写了不太好的代码,不管是受管理的还是不受管理的, 他或她的应用程序注定要失败,使用人必须不时地关闭这样的应用程序。 Microsoft 认识到需要不时地关闭府用程序是一个事实。进程问收可用于关闭反 应变得迟缓的应用程序。管理员或是开发人员可使用许多准则来决定应用程序何 时应该关闭: ∗ ∗ ∗ ∗ ∗ 寿命限制 内存限制 过期时限 调用限制 激活限制 寿命限制(lifetime limit)为应用程序指定一个时限(以分计)。当应用程序启动 时,COM+也启动时限。只要应用程序已经运行到了由寿命限制指定的时间,COM+ 就将其关闭。下一个客户的具体实现请求启动应用程序,重复上述过程。 内存使用率超过了内存限制准则的应用程序也要被关闭。如果应用程序的内存使 用率超过了内存限制的时间超过了一分钟就被关闭。内存限制可以设置为 0-1048576kb 之间的任意值。 图 10-9 Component Services 对话框中的 Pooling & Recycling 选项卡 COM+使用过期时限(expiration timeout)值来决定已经回收的进程何时应该关 闭。过期时限值与寿命限制不同之处在于,前者适用于回收的进程,而后者适用 于还未回收的进程。 调用限制(call limit)决定了在关闭前应用程序服务进行调用的次数。调用可以 是实例化组件请求,也可以是方法调用。 激活限制(activation limit)指定在关闭之前应用程序可服务于组件创建的次 数。使用 JITA 的组件可能比使用同一组件实例服务于多个方法调用的组件需要 较高的限额。 为了适应这一新功能,Microsoft 在 Application Properties(应用程序属性) 对话框中增加了一个选项卡。图 10-9 显示了 Component Services 中的新的 Pooling & Recycling (共享与回收)选项卡。在 Application Recycling(应用 程序回收)域中,回收准则都设置为缺省值。 10.1.8 应用程序共享 读者可能已经注意到图 10-9 中的名为 Application Pooling(应用程序共享池)的另一个域。这 个特色允许多个 Windows 进程作为一个应用程序。当应用程序启动时,COM+创建共享池 容量所定义的那么多个 dllhost.exe 的实例。当客户请求来到时,这些请求在这些 dllhost.exe 的实例间均匀地分配。图 10-10 演示了从三个客户来的请求如何接受来自四个 dllhost.exe 实例作为单一应用程序的服务。在这个图中,任何一个 dllhost.exe 实例可服务于任何客户请 求。 图 10-10 应用程序共享 10.2 IIS 6.0 的新功能 好像当前每—个应用程序都以某种方式使用 Web 服务器。许多 COM+应用程序使 用 IIS 使组件可为 Internet 或是公司内部网上的用户所用。IIS 和 COM+的功能 是紧密联系的。事实上,IIS 的某些功能直接来自于 COM+。由于这种紧密的集成 存在于 IIS 和 COM+之间,因而讲述 IIS 6.0 的新功能就特别有用。 IIS 6.0 提供了许多管理员和开发人员可以使用的增强特性。通常,目标为管理 员的特性对开发人员也有兴趣。虽然笔者并不讲述 IIS 的所有新功能,但确实要 讲述开发人员特别感兴趣的那些功能。下面要讨论的功能可有助于增加应用程序 的性能、伸缩性和稳定性。 ∗ ∗ ∗ ∗ ∗ ∗ ∗ ∗ ∗ ∗ ∗ ∗ ∗ 新服务器结构 应用程序共享池和 Web“花园” 服务器模式 工作进程管理 ASP 模板缓存细调 对元库(metabase)的 XML 支持 10.2.1 新服务器结构 在 IIS 5(Windows 2000 版)中,Web 服务器的内核功能驻留在一种名为 W3SVC 的 服务中。在 IIS 6.0 中,W3SVC 服务的结构已经修改了。新的 Web 服务器结构由 四种新的或者说是修改过的组件组成: 新内核模式驱动程序:http.sys Web 管理服务(WAS: Web Administration Service) XML 元库 应用程序的工作进程 图 10-11 图示了这些组件服务于请求的顺序。当一个对 URI 的请求来到时,比 http.sys“看到”请求并将其向上传递给 Web 管理服务(WAS)。WAS 使用来自它 的 XMLL 元库中的数据来向请求施加准则或设置。WAS 接着将请求传递给 Web 应 用程序。Web 应用程序可从另外的工作进程运行。 对 IIS 最令人激动(至少对笔者是如此)的增强是使用了名为 http.sys 的内核模 式驱动程序。这个文件位于 TCP/IP 栈的紧上方,这就意味着它可比老版本的 IIS 处理请求快得多。http.sys 有以下几种责任: 从线上获取 HTTP 请求并将其传到 Web 服务器中 限制 HTTP 请求的带宽 记录日志文本 图 l0-11 IIS 6.0 的 W3SVC 结构 http.sys 实现响应缓存机制。当一个特定 URI 的请求来到时,http.sys 检查它 自己的内部缓存,看是否已经发送了对那个 URI 的响应。如果在缓存中发现了所 请求的 URI,http.sys 就从缓存中提供服务,而不必把请求转发给 Web 应用程序。 这就有了较快的响应时间,因为 IIS 为了处理请求不必从内核模式切换到用户模 式了。这种增强甚至允许来自 Active Server Page 的输出也可以这种方式加以 缓存。图 10-12 显示了两个客户正在请求同一个资源 (http://myserver.com/someApp/default.asp)。为了这里讨论的目的,假设 Client B 的请求来到了,http.sys 已经在缓存中具有来自 default.asp 的输出。 Client B 的请求就由缓存内容来服务。如果 Client C、D 和 F 存在,每一个都 请求同一个 default.asp 页面,它们的请求也都由缓存来服务。这就允许 Client A 的请求代价在所有客户间分开了,其结果是伸缩性大大增加。 http.sys 还实现请求排队机制。如果 IIS 在新的请求到来之前不能处理原来的 请求,则把请求排队。随着请求越堆越高,http.sys 将这些请求排到与应用程 序有关的共享池中。当应用程序所用的队列排满时,IIS 向用户返回一个 HTTP 错误。在图 10-13 中,来自 Clients A、B、C 和 D 的请求充满了名为 someApp 的应用程序请求队列。由于 Client D 填充了队列,对 Client F 的请求无法提供 服务。结果,Client F 收到了一条错误。 图 10-13 http.sys 请求队列 Web 管理服务(WAS,Web Administration Service)是 W3SVC 服务的另一个主要 组件。与 http.sys 类似,WAS 运行在内核模式,这就意味着,处理请求可以比 运行在用户模式下要快。WAS 既负责配置 http.sys,也负责配置应用程序的工作 线程。WAS 从 IIS 的元库中获得信息。IIS 的以前版本把元库保存在二进制文件 中。元库包括 web 服务器及其应用程序的大多数配置信息。例如,元库中包括着 诸如应用程序是否应该运行在进程之外、使用多少个线程来处理请求等信息。在 IIS 6.0 中,元库是以可用 Notepad 或是其他文本编辑程序编辑的 XML 文件的形 式保存的。 工作进程出现在图 10-11 的顶部。工作进程是 Web 服务器的动力。这些进程运行 ASP 或者 ISAPI 应用程序。在前面的例子中,someApp 应用程序可以配置为在一 个工作进程内运行。Default.asp 或是其他页面的代码是在这个进程内执行的。 由于 IIS 把 Web 应用程序与核心服务器进程分开了,所以可比在自己的进程内运 行用户代码提供更为稳定的环境。虽然这个模型提供较好的稳定性,但却由于在 Web 服务器与工作进程之间不可避免的切换降低了性能。 10.2.2 应用程序共享池和 Web“花园” 在 Windows NT 的 IIS 4 中,在 mtx.exe 的实例中,web 应用程序(与 Web 服务器 一起)既可运行在进程内也可运行在进程之外。Windows 2000 的 IIS 5 通过允许 应用程序在 dllhost.exe 实例内共享而扩展了上面的概念。在 IIS 5 中,只存在 一个共享池。应用程序可运行在进程内、进程外或是在惟一的应用程序共享池中。 IIS 6 的出现提供了多个应用程序共享池。图 10-14 显示了多个应用程序如何运 行在一个应用程序共享池内。一个应用程序共享池可以运行一个或多个应用程 序。 图 10-14 应用程序共享池和 Web 应用程序 应用程序共享池是稳定性、性能和伸缩性的折衷。有助于 Web 服务器变得更稳定的最好方 法之一是使 Web 应用程序运行于进程之外。如果在 Web 应用程序中出现了某种非常严重的 错误,只有运行应用程序的进程需要关闭,而不是整个 web 服务器。但是正如任何折衷一 样,这种方法也有缺点。当对一个进程之外的 Web 应用程序发出请求时,该请求必须跨越 进程边界。这可能是昂贵的代价。在某些情况下,Web 服务器必须启动一个工作进程来处 理请求。这对性能来说代价可能更为昂贵。在进程外运行太多的应用程序也可能降低伸缩性。 随着越来越多的工作进程的启动,操作系统必须提供资源(CPU 时间和内存)来处理每个进 程。 共享池通过使应用程序运行在进程外,同时仍然能减少处理请求所需的工作进程的数目,从 而提供了两者兼顾的方法。由于一个工作进程可服务于多个应用程序的请求,Web 服务器 就不必创建像每个应用程序运行在它自己的进程内那样多的进程。 笔者的某些同事曾经说过,共享只是把问题从 Web 服务器移动到了另一进程中。其论点如 下:如果一个不太好的 Web 应用程序能够使 Web 服务器瘫痪,那么它也可使共享池进程瘫 痪,接着使共享池中所有的其他应用程序瘫痪。这当然是一个引入注目的论点。但是,这个 论点忘记了有关共享的两个重要的观点。第一,Web 服务器服务的不只是应用程序。Web 服务器还服务于静态页面。静态页面几乎不会使 Web 服务器瘫痪。如此一来,静态页面就 可安全地在进程内接受服务。如果要作出选择的话,笔者认为,大多数管理员更愿意能够服 务于静态页面,而不愿意什么也不服务。第二,在 IIS 6.0 中,可有多个共享池。一个不太 好的 应用程序没有必要使 Web 服务器或其他应用程序共享池瘫痪。如果应用程序共享适当,当 一个共享瘫痪时,使之瘫痪的只是依赖于出故障的应用程序的那些应用程序。IIS 6.0 通过引 入名为 Web“花园”的特性进一步扩展了共享的概念。Web“花园”(web garden)是一个由多个 工作进程服务的应用程序共享池。Web“花园”为应用程序共享池提供了对错误的容忍性。如 果一个应用程序引起了工作进程暂停,在“花园”中的其他工作进程可以接收其余的请求。图 10-15 显示了工作进程与 web“花园”之间的关系。在 Web“花园”内的一个工作进程可服务于 任何客户请求。 图 10-15 工作进程和 Web“花园” IIS 6.0 对应用程序共享池提供了附加的功能。这个功能就是所谓的快速错误保 护(Rapid Fail Protection)。当共享池中的工作过程反复地出现故障时,IIS 就禁用应用程序。当应用程序失败而 IIS 将其禁用时,http.sys 返回一个错误 信息。该错误是 HTTP 503 错误响应,这就告诉客户所请求的服务是不可用的。 该功能是有用的,因为可防止有问题的应用程序消耗太多的资源。 10.2.3 服务器模式 IIS 6.0 可以配置运行在两种模式下:标准的和专门的应用程序。标准模式 (standard mode)是用于 Web 服务器的,这种服务器必须为从 IIS 5 中迁移而来 的应用程序提供向后的兼容性。某些应用程序,例如 ISAPI(Internet Server API) 过滤器(从客户中读取外来的原始数据)必须运行在标准模式的 IIS 6.0 服务器 上。 在专门的应用程序模式(dedicated-application mode)中,所有应用程序都运行 于进程外。每个应用程序和站点运行在应用程序共享池中。由于使用共享池可以 实现隔离,IIS 6.0 在提供稳定性的同时可以比 IIS 的以前版本提供更好的伸缩 性。 10.2.4 工作进程管理 读者现在应该已经明白,工作进程是 IIS 6.0 最重要的组件,IIS 6.0 提供许多 目的在于改善工作进程稳定性的特性。某些更为有意义的特性如下所示: ∗ ∗ ∗ ∗ ∗ ∗ ∗ ∗ ∗ ∗ ∗ 工作进程回收 工作进程标识 “健康”监测 空闲时限 按需启动 处理器亲合力 工作进程回收(worker-process recycling)与 COM+中的进程回收行为是一样 的。IIS 的工作进程可以配置为根据如下准则重新启动: 经过时间 服务请求的数目 预先设定的时间表 对 ping 的响应 虚拟内存使用率 (请想像一下 COM+的内存门) 当 IIS 决定必须回收一个工作进程时,它允许出错进程完成正在服务的请求,此 时不再有新的请求提交给该工作进程。在 IIS 等待该工作进程完成对请求的处理 期间,它启动一个新工作进程来代替原来的进程。 一旦原来的工作进程结束处 理就被销毁。在某些情况下,工作进程可能不能完成对请求的处理。在这种情况 下,IIS 在销毁进程之前等待一段可配置的时间量。关闭工作进程并启动一个新 进程叫做“重启动”(restarting)。 Web 应用程序可配置为在特定的用户标识下运行,正如 COM+应用程序一样。在 IIS 的以前版本中,Web 应用程序既可作为本地系统帐户(或是其他什么 IIS 正 运行其上的帐户)运行也可以作为缺省用户帐户(IUSER_MachineName 或 IWAM_MachineName)运行,这个特性可用于对应用程序提供或多或少的访问权, 这些看需要而定。 以前提到过的 ping 是 WAS 的一种服务。可使 WAS 向工作进程提供健康监测能力。 WAS 向 Web 服务器上的工件进程保持打开的通讯频道。WAS 周期性地进行检查, 以便查出通讯频道是否打开。如果 WAS 确定该工作进程已经从频道上掉线,它就 假设该工作进程不能继续服务请求。当这种情况发生时,工作进程就重启动。 不做任何工作的工作进程自白地消耗着服务器的资源。工作进程可配置为在一段 可配置的不活动时间之后加以关闭。这就是所谓的“空闲时限”(idle timeout)。顺便说一下,这也是 COM+(在 Windows 2000 上)对应用程序提供的支 持。 IIS 6.0 的“按需启动”(on-demand start)功能允许工作进程在 Web 服务器或 是操作系统启动时启动。如果额外的工作进程正在等待超时,这可能是对资源的 无效使用。从一个工作进程中请求资源的第一个客户将遭遇性能降低。初始的性 能降低是由于工作进程的启动。这种降低的代价是允许的,因为它可以在多个客 户请求间扩散。 工作进程可以配置为运行在特定的处理器,并且只能在特定处理器上才能运行, 这就是所谓的“处理器亲合力”(processor affinity)。处理器有内部缓行来 保存数据。如果进程所在的 CPU 能够从它的缓存中而不是从内存或是磁盘中获得 数据的话,应用程序的性能就可得到提升。把工作进程绑定到 CPU 上可增加从 CPU 缓存(而不是从内存)中使用数据的机会。 10.2.5 ASP 模板缓存细调 ASP 模板缓存(ASP template caching)功能适合于传统的 ASP 页面,而不是 ASP.NET 页面。当发出—个对 Active Server Page 的请求时,必须激活 ASP 引 擎来编译页面、运行代码并产生输出。所有这一切代价都是昂贵的,特别是处理 带有大量代码的 ASP 页面时。ASP 模板缓存特性将 ASP 编译后的输出保存剑磁盘 上。与按需启动—样,请求页面的第一个客户,在页面编译时经受了初始的性能 降低。但是,只要页面个再变化,IIS 就以预先编译好的页面为以后的请求服务 了。 10.2.6 对元库的 XML 支持 通常,当开发人员变得激动并开始谈论一个产品的新功能时,他或她谈论的最后 —件事是诸如元库(metabase)一类的事。元库实际上更应该是管理员份内的事。 但是理解元库可为应用程序做什么的开发人员比不管元库的人要做得更好。 元库包括 Web 服务器及其应用程序的大多数配置信息。在 IIS 以前的版本中,元 库以二进制文件形式加以保存,只能用 MetaEdit(Platform SDK)一类的工具 或是使用 IIS 管理库编写的应用程序才能读取。为了简化这一点,IIS 6.0 将其 元库以 XML 文件形式加以保存。正如读者可能了解的,XML 可保存为简单的文本 文件,可由任何如 Notepad 一类的文本编辑器读取。 在 IIS 6.0 中有几种新功能用来支持 XML 版本的元库: ∗ ∗ ∗ ∗ ∗ ∗ ∗ 在 IIS 运行时编辑元库 元库的历史记录 备份元库 与服务器无关的元库备份 正如前面提到的,WAS 使用元库来配置 http.sys 和 Web 应用程序。通过读取元 库最新的副本并创建一个元库的内存表示,从而达到目的。如果在 IIS 运行时管 理员编辑了元库,操作系统的一个功能通知 IIS 该文件已经改变。文件发生变化 时,WAS 重新读取元库并施加新的变化。这一功能的好处是,为了使变化更新, 不需要重新启动 Web 服务器。这有助于减少 Web 服务器的不服务时间, 每次管理员修改元库文件时,IIS 增加文件的版本号并将原来版本复制到 history 文件夹。history 文件夹中的每个文件都用一个版本号加以标记,以便 在需要时管理员能够返回原来元库的版本。这个功能提供了对元库的偶然损坏或 是配置失误的安全保障。 IIS 6.0 中有三种方法来备份元库: 保护备份 非保护备份 传统备份 保护备份(secure backup)可用密码加以保护。如果需要恢复元库密码是必需的。 非保护备份(unsecure backup)不需要密码。在本书写作时,传统备份(legacy bakup)只能通过编程来实现。备份也可以独立服务器。不依赖于服务器的元库对 于向新安装的操作系统恢复 IIS 配置信息是有用的。管理员可以恢复备份的元 库,而不用手工配置所有的设置。 10.3 MSMQ 的新功能 在第 8 章中,读者看到了 MSMQ 如何为列队组件提供内在传输机制。在该章中处 理的 MSMQ 版本是 2.0。Windows 2002 将发行 MSMQ 的新版本——3.0。这一版本 具有许多对开发人员和管理员来说感兴趣的增强功能: ∗ ∗ ∗ ∗ LDAP 支持 分发列表 队列别名 消息触发器 在 MSMQ 3.0 中的 LDAP 支持允许 MSMQ 客户查询域控制器以便获得与 MSMQ 有关的 信息。在 MSMQ 2.0 中,MSMQ 必须被安装在域控制器上才能支持与活动目录 (Active Directory)的集成。在活动目录安装中,MSMQ 客户与安装在域控制器 上的 MSMQ 服务器“会话”,以便查询公共队列和类似的信息。使用 LDAP 时,MSMQ 3.0 客户可以查询域控制器从而获得公共队列而不必通过域控制器上的 MSMQ 服 务器。 分发列表(Distribution Lists)允许向许多消息队列或者说是分发列表发送消 息。在这种意义下的分发列表与电子邮件中的分发列表不同。在这种意义下的分 发列表只能包括公共队列、别名和其他分发列表。 队列别名(queue alias)是在活动目录内指向另一公共或是私有的队列的指针。 在第 8 章中,笔者曾说,公共队列是在活动目录中公布的或是广告的,但是私有 队列却并非如此。私有队列仅当知道队列名和拥有队列的机器名时才是可以访问 的。MSMQ 的新的队列别名功能允许私有队列在活动目录内广告并包括在分发列 表中。 消息触发器(message triggers)功能使开发人员或管理员把一个操作与一组用 于外来消息的准则关联起来。操作可以启动另一个应用程序或是创建一个组件并 调用其方法。把操作与消息准则放在一起形成了一个规则。如果触发器与队列关 联,每条到达队列的消息引起触发器触发。仅当消息满足触发器的准则时,消息 才被接受。从前,触发器仅能作为 P1atform SDK 的插件才是可用的。 10.4 小结 如果本章吊起了读者对 Windows 2002 渴望的“胃口”的话,肯定不止你一个渴 望 Windows 2002。笔者需要这里的许多功能,如进程回收和 ping。读者可能已 经注意到,在 IIS 和 COM+中的许多功能是类似的,因为 IIS 的许多功能是从 COM+ 中得到的。这两种技术在几个版本中都联系紧密。 情况很可能是,某些功能并不会出现在产品的最终版本中。其他在本章中未论及 的功能,诸如对.NET 通用语言运行库的支持却可能会增加到最终的版本中去。 附录 A CD-ROM 包括的内容 本附录向读者提供本书所附 CD-ROM 内容方面的信息。 CD-ROM 包括本书中的所有源代码以及名为 OfficeMart 应用程序的演示版。 OfficeMart 演示了几种本书中讲过的 COM+技术。本书可搜索的电子版也包括在 此 CD-ROM 中,电子版图书可以用 Adobe Acrobat Reader 阅读。 A.1 系统需求 确保计算机满足列在本节中的最小系统需求。如果读者的计算机不满足大多数要 求,本书所附光盘中的内容时可能会遇到问题。 对于 Windows 2000、Windows XP Beta 2 或是 Windows NT 4.O: ∗ ∗ ∗ ∗ ∗ ∗ ∗ ∗ ∗ ∗ ∗ ∗ ∗ 运行在 450 MHz 或更高(推荐 600 MHz)的 Pentium II 处理器的 PC 机 对于 Windows Professional至少要有 96MB 内存(推荐 128MB),对于 Windows 2000 Server 至少要有 192MB 内存(推荐 256MB) 在系统盘上有 500MB 磁盘空间,在安装驱动器上有 2.5GB 空间 操作系统:Windows 2000、Windows XP Beta 2 或 Windows NT 4.O 视频:800×600 分辨率,256 色(推荐 16 位色) 鼠标:Microsoft Mouse 或是兼容的指示设备 CD-ROM 驱动器—双倍速(2x)或更快 除了上面列出的要求之外,还需要 1MB 的附加硬盘空间来安装各章和附录中的示 例代码以及安装 CD-ROM 上的 OfficeMart 应用程序。 A.2 在 Microsoft Windows 中使用光盘 为了在硬盘上安装光盘上的项目,请遵照以下步骤: 1.将光盘插入计算机的 CD-ROM 驱动器。 2.打开 Windows 资源管理器(explorer.exe)。 3.把 OfficeMartWeb 文件夹复制到 Web 服务器的根文件夹中。 4.右 击 OfficeMartWeb 文件夹。如果在属性域中的“Read-Only”(只读)复选框 处于选中状态,则应去掉其上的选择标记。 5.当单击 0K 按钮时,将询问用户是否想要将改变应用于 OfficeMartWeb 文件夹 及其所有的子文件夹和文件。选择“Apply”(应用)。 6.在 Windows 资源管理器中,移动到 OfficeMartWeb\bin 文件夹并运行 RegOfficeMart.bat。 A.3 本书所附光盘中有什么 本书所附光盘中包括示例的源代码、应用程序和本书的电子版。下面是按类别排 列的 CD-ROM 内容介绍。 A.3.1 源代码 列在本书中的每个程序都包括在光盘的 BookExamples 目录中。 A.3.2 OfficeMart 演示应用程序 OfficeMart 的演示应用程序演示了几种 COM+技术的使用: 排队的组件 分布式事务处理 松散耦合的事件 即时激活(Just In Time Activation) 组件构建 对象共享 OfficeMart 使用的其他技术(未包括在 CD-ROM 中的): ∗ ∗ ∗ ∗ Microsoft SQL Server 2000 Active Server Page .NET OfficeMart 是一个想像的办公用品供应公司,在 Internet 上有一个 B2C 的网站。 这个应用程序允许 Internet 上的顾客定购办公用品。顾客导航到定货单入口 ASP.NET 页面并键入他们的顾客身份号(ID)、想要购买的产品的 SKU 号和数量。 这个应用程序将注意力集中于如何以有趣的方式使用 COM+服务,以便解决获取 顾客定单、施加某种商业逻辑,从而既可处理也可拒绝定单。为了将注意力集中 于 COM+技术,笔者取消了一些在 B2C Web 站点中常常会遇到的东西: 认证及认证机制 产品目录 A.3.3 OfficeMart 的结构 OfficeMart 应用程序的结构尽量简单,以便读者和笔者都能将精力集中于应用 程序所使用的 COM+技术上。除了几个 Active Server Page .NET 页面之外, OfficeMart 结构由两部分组成:Microsoft SQL Server 数据库和 COM+ Library。 现在把这两部分解释如下: Database(数据库):在 OfficeMart 应用程序中有两个数据库:Inventory 和 OfficeMart。Inventory 数据库包括一张名为 Products 的表。这个表保存着在 Web 站点上卖出的每样产品的产品数 据。OfficeMart 数据库保存着顾客信息和定单信息。为了易读性起见,在数据库中没有增加 用户定义的保存步骤。所有的 SQL 代码与应用程序内联在一起,因而用户可以更容易地看 出将要干什么。 COM+ Library:OfficeMartLib 工程包括 OfficeMartLib 名称空间,其中保存着所有 在应用程序中使用的 COM+组件: Orders 类:这个类包括一个允许 ASP.NET 页面提交定单的方法。Orders 类是个 列队组件(Qneued Component)。其概念是 Orders 类可运行于应用程序服务器 中,这个服务器可独立于为 Web 站点提供宿主的 Web 服务器之外。列队组件 结构允许非同步地提交定单。这就提供了在第 8 章提到过的更好的伸缩性。 Customers 和 Products 类:Customers 和 Products 两个类包括在购买过程中更 新顾客信用和调节产品存货(分别地)的方法。这两个类是在 Orders 类的交易内 操作的交易组件。 VerifyCustomer 和 VerifyProduct 类:这两个类确认顾客的存在、产品量足够 以及其他事项。这不是 ServicedComponent 类。将其逻辑移动到各自的 ServicedComponent 类中还是诱人的。包括在这两个类中的逻辑在本质上并不是 交易的。如果把 Verify 类与其各自的顾客和 Products 类合并的话,则交易工作 就不会招致分布式交易的负担。不但对象创建时间要长一些,而且数据库锁定 也会是严重的。 InventoryConn 和 OfficeMartConn 类:这两个类是共享的组件,管理着与两个 应用程序数据的连接。这两个类由 Verify 类使用,以便改善性能。笔者假定, 能够在 100 毫秒之内打开数据库连接。 OrderEvt、CustomerSubscriber 和 AdminSubscriber:OrderEvt 是一个 COM+事 件类。当一个定单失败以及当定单成功提交给数据库时就引发事件。 CustomerSubscriber 使用这一事件通知顾客(通过电子邮件),定单是成功还是 失败。AdminSubscriber 将成功的定单在日志文件中记录为信息消息。未成功的 定单在日志中记录为错误消息。笔者在此实现事件类以便为在应用程序中插入 附加功能提供一个简单的方式。由于公司内的不同部分都想要得到关于定单的 通知,开发人员所要做的是实现另一个 Subscriber 类。例如,当顾客在购买商 品时钱不够,可编写一个 Subscriber 类通知会计部门。 A.3.4 《C# COM+编程指南》一书的电子版 本书完整的(且可搜索的)文本以 Adobe 的 PDF 文件格式放在了本书所附的光盘 上,可用 Adobe Acrobat Reader(也包括在光盘中)来阅读。关于 Adobe Acrobat Reader 的更多信息,请访问 www.adobe.com 站点。 A.4 故障排除 如果在安装或使用光盘上的程序时遇到了困难,请尝试以下措施: ∗ ∗ ∗ 检查 OfficeMartLib.dll 的位置。OfficeMartLib.dll 装配件必须位于 OfficeMartLib.dll\bin。 该装配件也必须安装在全局装配件缓冲中。 确认所有的服务都在运行。下面的服务必须运行,以便 OfficeMart 程序正常工作:World Wide Web 发布服务(IIS)、SMTP 服务、COM+事件系统、分布式事务处理协调器 (Distributed Transaction Coordinator,DTC)服务和 SQL Server。 确认 SQL 脚本运行正确。如果 SQL 脚本运行正常,应该在 Inventory 数据库中看到 Products 表。OfficeMart 数据库应该包括一个 Orders 和 Customers 表。 如果读者仍然有问题,请给 Hungry Minds Customer Service 打电话,电话号码 是(800)762-2974。在美国之外可打(317)572-3993 号码。Hungry Minds 将只对 应用程序本身的安装和其他一般的质量控制项目提供技术支持,其余请向程序销 售商或是作者咨询。 附录 B COM+的共享属性管理器 在第 4 章中,笔者讲述了资源分配器。笔者将资源分配器描述为负责维护可变资 源(如数据连接池或是线程)的共享池。那里的讨论主要集中于资源分配器在事务 处理中的角色。COM+的共享属性管理器(COM+ Shared Property Manager,SPM) 是另一类型的资源分配器。SPM 管理的不是数据库连接而是内存。受管理的内存 可用于在组件间保存状态。SPM 为多个组件提供受保护的访问内存。 直到此时,读者已经通过基于属性的编程方法操作了大多数 COM+服务。COM+的 SPM 是不使用属性的少数服务之一。对象模型是严格地基于 API 的。所谓“基于 API 的”,笔者指的是人们可通过 System.EnterpriseServices 类和它们的方法 来利用服务。 在介绍之前,通过检查当两个或多个线程访问同一共享的内存时所出现的某些缺 陷看一下没有 SPM API 会是什么样子。 B.1 在线程间共享内存 在 C#应用程序中,通过使用 static 修饰符,内存可在线程间共享。本节中的示 例访问用 static 修饰符声明的全局变量。为了理解本节中的代码,读者应该清 楚地理解 static 修饰符。如果读者对此不甚了解,笔者下面就先介绍一下有关 内容。 B.1.1 static 修饰符 static 修饰符的目的是提供对内存的共享区域的全局访问方法。static 修饰符 可施加于下面的任何构成上 方法 ∗ ∗ ∗ ∗ ∗ 属性 运算符 域 构建器 静态(static)类型并不与任何类的实例、结构或是其他类型相联系。对于这一点 很好的例子是 ContextUtil 类。此类的属性和方法都是静态的。读者看到过笔者 创建一个 ContextUtil 类的实例吗?没有。静态类型只能通过它们的类或是结构 名加以访问。在 ContextUtil 类的情况下,通过调用静态的 ContextUtil.ContextID 属性可获得上下文的 ID。 图 B-1 展示了静态类型与访问它们的线程之间的关系。应用程序内的任何线程可 以访问声明为 static 的任何类型。 图 B-1 线程和静态(static)类型的关系 B.1.2 内存冲突和 static 修饰符 假如了解其缺陷的话 static 修饰符是可安全使用的有用构件。一般来说,共享 内存出现的问题是,同时被多个线程所访问时未受到保护。这是 COM+中的 SPM 的整个基础之所在。为了理解 COM+ SPM 的价值,请看一个使用类的静态域来共 享内存的示例应用程序。在笔者讲完 SPM API 之后,还要回到这个示例上并使用 COM+的共享属性改正其问题。 清单 B-1 中的代码使用了来自 System.Threading 名称空间的 Thread 类。这个类 有一个构建器,它有一个 ThreadStart 委派作为输入。ThreadStart 委派是一个 指向没有参数且返回 void 类型的方法的指针。委派本身是可创建的类型。在 Thread 的构建器中,笔者创建了 ThreadStart 委派的一个新实例,传入方法名。 当 Thread.Start 方法启动线程时,笔者传递给 ThreadStart 构建器的方法被执 行。一旦创建了两个线程(tl 和 t2),就可调用 ThreadStart 方法自由地启动线 程。在两个新线程完成工作之前,执行 Main 方法的线程就可能退出。事实上, 在这段代码示例很可能出现这种情况。为了防止这一点,笔者在每个线程上都调 用帅 Thread.Join。这个方法强制调用者阻塞,直到线程完成其工作为止。 清单 B-1 用多个线程访问共享内存时出现的问题 using System; using System.Threading; namespace StaticProperties { public class CApp { public static void Main() { // create two new thread classes Thread t1 = new Thread(new ThreadStart(Start1)); Thread t2 = new Thread(new ThreadStart(Start2)); // start each thread t1.Start(); t2.Start(); // wait for each thread to finish t1.Join(); t2.Join(); // print the value of iCount Console.WriteLine(CStatic.iCount.ToString()); } public static void Start1() { CStatic.iCount = 2; int iSomeValue = 4; // force the thread to sleep so the second thread can // interrupt its work and change the value of iCount Thread.Sleep(500); int iResult = iSomeValue / CStatic.iCount; } public static void Start2() { CStatic.iCount = 0; } } public class CStatic { public static int iCount; } } 这两个线程(t1 和 t2)试图访问同一个静态整数值:iCount。该整数是名为 Cstatic 的类的成员。 第一个线程(t1)企图用 iCount 去除 iSomeValue。第二个线程(t2)在第一个线程计算之前将 iCount 的值改变为 0。对 Thread.Sleep 的调用强制第一个线程“睡眠”半秒钟。当第一个线程“睡 眠”之后,就为第二个线程提供了一个执行并将 iCount 值改变为 0 的机会。 在 Main 方法中,当笔者启动了每个线程时,必须为每个线程调用 Thread.Join 方法。这就强 制执行 Main 方法的线程等待,直到所有其他线程都完成工作为止。 Main 方法中的最后一行是从不执行的。当第一个线程进入“睡眠”状态,第二个线程将 iCount 改变为 0。当第一个线程恢复执行时,它试图用 0 去除 iSomeValue。这就出现了 System.DivideByZeroException(被 0 除的)错误。 笔者承认,这是个不太恰当的示例。确实如此,这是个编写的故障。实际上,当第一次向一 组用户发布应用程序时,这些类型的问题并不总是明显的。可能要花几天甚至是几个星期才 能看到达类问题的出现。但是,当问题确实出现时,要解决就要花更长的时间。如果第一个 线程在启动时不管用什么方法能够锁定 iCount 域,在完成任务时解除锁定,那么第二个线 程就不能将 iCount 变为 0。幸运的是,COM+ SPM 提供了这种锁定机制。但读者在解决这 个问题之前,必须先学习使用 SPM。 B.2 共享属性管理器 API COM+把共享属性组织到组中。这些组提供为进程组织属性的名称空间。此外,组还有助于 减少在属性间可能有的命名冲突。例如,一个组不能有两个属性同名,但是两个不同的组中 却可有同名的属性。 在.NET 中 SPM 使用的 API 是由三个类和两个枚举(将在稍后叙述)组成的。以下这三个类是 在 System.EnterpriseServices 名称空间中定义的。 ∗ ∗ ∗ SharedPropertyGroupManager 类 SharedPropertyGroup 类 SharedPropertyClass 类 图 B-2 说明了这些类及其相互之间的层次关系 B.2.1 SharedPropertyGroupManager 类 使用名为 SharedPropertyGroupManager 的类就可创建并访问组。当说到共享属 性时,这是程序员直接实例化的惟一的类。这个类的构建器没有参数。如果实例 化这个类,其代码可能会与清单 B-2 中的代码类似: 清单 B-2 SharedPropertyGroupManager 类的具体实现 using System.EnterpriseServices; public class SomeComponent : ServicedComponent { public void DoSomething() { SharedPropertyGroupManager gm = new SharedPropertyGroupManager(); // do something with the Group Manager } } 使用这个类的大多数情况可能是用于创建属性组。为此,可使用 CreatePropertyGroup 方法。这个方法创建一个可用于创建属性的属性组。这个方法的特征表示看起来 与下面代码类似: SharedPropertyGroup CreatePropertyGroup ( string name, ref PropertyLockMode dwIsoMode, ref PropertyReleaseMode dwRelMode, out bool fExists ); 这个方法的第一个参数是代表组名的字符串。如果两个组件必须在本组内共享属 性,它们必须使用这个参数传递过来的组名。 第二参数是 PropertyLockMode。PropertyLockMode 是 System.EnterpriseServices 名称 空间的一个枚举。这个参数决定了当被访问时属性在内存中如何锁定。只有两种 选择:PropertyLockMode。Method 和 PropertyLockMode.SetGet。当选择 PropertyLockMode。 Method 模式时,本组中的所有属性都被锁定,直到(该组件的)方法操作返回时 为止。另一方面,当属性被设置或读取时,SetGet 方法只引起锁定单个的属性。 如果要在组内的几个属性上执行统一的读取和写入,在方法级上锁定属性是有用 的。当在组内读取或是写入一个属性时,另一个线程不能改变组内的另一个属性。 这是以较高的开销为代价的。如果属性的读取或写入操作不是互相依赖的,就应 该选择 setGet 锁定模式。 PropertyReleaseMode 是另一个来自 System.EnterpriseServices 名称空间的枚举。 释放模式决定了何时本组中的属性从内存中释放。当把释放模式设置为 System.EnterpriseServices.Process 时,本组占据的内存只当宿主进程结束时 才释放。把模式设置为 System.EnterpriseServices.Standard 时,当最后的客 户释放了对该组的引用时,就释放内存。读者可能已经注意到了,不管是 PropertyLockMode 参数还是本参数都是使用 C#的 ref 关键字加以声明的。这样做的 原因一会就会清楚。 在这个方法中的最后一个参数是个布尔标记,告诉调用者组是否存在。由于这个 参数是由 out 关键字标记的,因而在调用方法之前不需要加以设置。笔者应该清 楚地说明,这个方法不仅用于创建属性组,而且还用于获得对已有的属性组的引 用。这就是使用 fExists 参数的原因。这也是属性锁定模式和属性释放模式参数要 用引用加以传递(C#的 ref 关键字)的原因。如果带有同名的组存在的话,fExists 参数就设置为 true(真)。属性锁定模式和属性释放模式参数被设置为属性组创 建时使用的值。如果该组存在,那么向这些参数传递来的任何值都被忽略。一般 来说,如果不能确定想要的组是否存在,就应该使用这一方法。 如果对枚举不熟悉的话,使用这个方法是要点技巧的。使用枚举的关键是把它想 像为只是一个数据类型。如果笔者把前面的 SomeComponent 加以扩展以便调用 CreatePropertyGroup,那么其代码如清单 B-3 所示: 清单 B-3 创建一个属性组 using System.EnterpriseServices; public class SomeComponent : ServicedComponent { public void DoSomething() { bool bGroupExists; // declare the property lock mode PropertyLockMode lm = PropertyLockMode.SetGet; // declare the property release mode enum PropertyReleaseMode rm = PropertyReleaseMode.Process; SharedPropertyGroupManager gm = new SharedPropertyGroupManager(); SharedPropertyGroup pg; pg = gm.CreatePropertyGroup( “myGroup”, ref lm, ref rm, out bGroupExists; ); // use pg to create properties } } 在清单 B-3 中,笔者把属性锁定模式定义为 SetGet 而把释放模式定义为 Process。当这个调用返回时,bGroupExists 是 false, 因为组并不存在。用于本 组的返回值是一个 SharedPropertyGroup 的实例。 如果知道所要访问的组是存在的,那么可以使用 SharedPropertyGroupManager.Group 方 法。这个方法的特征表示如下: Public SharedPropertyGroup Group(string name) 正如读者所看到的,这要比 CreatePropertyGroup 方法容易得多。这个方法的惟一参 数是组名,而以 SharedPropertyGroup 类的形式返回组。清单 B-3 可以修改为使用这 个方法,诸参看清单 B-4。 清单 B-4 使用 Group 方法 using System.EnterpriseServices; public class SomeComponent : ServicedComponent { public void DoSomething() { SharedPropertyGroupManager gm = new SharedPropertyGroupManager(); SharedPropertyGroup pg; pg = gm.Group(“myGroup”); // use pg to create properties } } 不用传递属性锁定和释放模式并试图计算出属性是否存在,笔者传递想要访问的组名。如果 已知属性组已经存在,这个方法可以简化代码并使之更具有可读性。 B.2.2 SharedPropertyGroup 类 在清单 B-3 和 B-4 中,可以看到 SharedPropertyGroupManager 类的方法是如何创建并授予对 属性组访问权的。这些组是作为 SharedPropertyGroup 类的实例返回的。与 SharedPropertyGroupManager 类不同,人们并不直接创建 SharedPropertyGroup 类的实例。 SharedPropertyGroup 类的目的是创建属性。它实现了许多方法,可用来根据名称或是根据在 组中与其他属性的相对位置来创建属性。以最常用的方法开始: Public SharedProperty CreateProperty( string name, out bool fExists ); 这个方法的格式看起来有点熟悉。它以表示属性名的字符串作为输入,而返问一个 bool 值 作为输出参数。正如 CreateProperty Group 参数一样,这个布尔标记指明属性是否存在。由 于这是一个 out 参数,任何在方法调用之前设置的值都被忽略。这个方法的返回值是 SharedProperty 类。正如读者在下一节中所看到的,SharedProperty 类代表来自于共享组的单 个属性。 在清单 B-5 中,笔者使用 SharedPropertyGroup.CreateProperty 已经扩展了 SomeComponent. DoSomething 方法,使之包括 SharedProperty 的创建。 清单 B-5 创建 SharedProperty using System.EnterpriseServices; public class SomeComponent : ServicedComponent { public void DoSomething() { bool bGroupExists; bool bPropertyExists; // declare the property lock mode PropertyLockMode lm = PropertyLockMode.SetGet; // declare the property release mode enum PropertyReleaseMode rm = PropertyReleaseMode.Process; SharedPropertyGroupManager gm = new SharedPropertyGroupManager(); SharedPropertyGroup pg; pg = gm.CreatePropertyGroup( “myGroup”, ref lm, ref rm, out bGroupExists; ); SharedProperty sp = pg.CreateProperty( “myProp”, bPropertyExists); } } SharedPropertyGroup 类可以通过位置来创建组。用来达到目的的方法如下: Public SharedProperty CreatePropertyByPosition( int position, out bool fExists ); 在这个方法中,位置号代替了属性名。位置可以是任何合法的整数值,包括正数 和负数。笔者应该指出,用一个方法创建的属性不能由另一个方法加以访问。例 如,如果使用 CreateProperty 方法创建属性,就不能在以后用 CreatePropertyByPosition 号加以访问: 这些属性创建方法中的每一种都具有相应的属性提取方法。如果一个属性是用 CreateProperty 创建的,其后只可再用 CreateProperty 来访问,或是使用 Property 方法来访问。Property 方法采用字符串作为输入参数并返回一个 SharedProperty。Property 方法仅在属性已经创建之后才是有用的。 当属性已经用 CreatePropertyByPosition 加以创建,就可位用 PropertyByPosition 方法。与 Property 方法不同,PropertyByPosition 方法采 用一个整数而不是代表属性名的字符串作为输入参数。假设属性已经存在, PropertyByPosition 号可像清单 B-6 中所示加以使用。 清单 B-6 从共享属性管理器中捕获意外 using System.EnterpriseServices; public class SomeComponent : ServicedComponent { public void DoSomething() { bool bGroupExists; bool bPropertyExists; // declare the property lock mode PropertyLockMode lm = PropertyLockMode.SetGet; // declare the property release mode enum PropertyReleaseMode rm = PropertyReleaseMode.Process; SharedPropertyGroupManager gm = new SharedPropertyGroupManager(); SharedPropertyGroup pg; pg = gm.CreatePropertyGroup( “myGroup”, ref lm, ref rm, out bGroupExists; ); SharedProperty sp = pg.CreatePropertyByPosition( 5, bPropertyExists); } public string DoSomethingElse() { SharedPropertyGroupManager gm = new SharedPropertyGroupManager(); SharedPropertyGroup pg = null; SharedProperty sp; try { pg = gm.Group(“myGroup”); } catch (ArgumentException ae) { Console.WriteLine(ae.Message); } try { sp = pg.PropertyByPosition(5); } catch (ArgumentException ae) { Console.WriteLine(ae.Message); } catch (NullReferenceException nre) { Console.WriteLine(nre.Message); } return “”; } } 在清单 B-6 中,笔者已经向组件中添加了一个方法:DoSomethingElse。在这个方法 中,假设属性组和属性已经创建。但是,笔者并不完全相信属性组和属性已经创 建。如果客户在调用 DoSomethingElse 之前调用 DoSomething 方法,DoSomethingElse 不应 该捕获任何错误。当然,进程中的另一个组件也可以创建组和属性。如果组和属 性没有创建,笔者对方法调用已经添加了某种错误处理过程。在第一个 try-catch 代码块中,笔者企图捕获一个 System.ArgumentException 错误。如果向 SharedPropertyGroupManager.Group 方法传递过来一个非法的组名,这个意外就出现了。 在第二个 try-catch 代码块中,笔者企图访问位置号为 5 的属性。请注意,笔者 使用的位置号是 0 或 1。由位置号创建的属性不需要像人们想像的那样要按顺序 创建。可以按自己喜爱的方式给属性编号。如果试图访问位置 5 处的属性,而它 又不存在时,则出现 System.ArgumentException 意外。这个意外仅当笔者获取对名 为 myGroup 的引用时才出现。在笔者的代码示例中,如果笔者调用 Group 方法时 myGroup 不存在,则出现 System.NullReferenceException 错误。可能的情况是,读者 想要做的不只是向屏幕上打印出错误,但打印锗误还是有指导意义的。 B.2.3 SharedProperty 类 到目前为止,笔者在本附录中完成的工作已经引导我们接近了共享属性本身。共 享属性是通过一个名为(令人惊奇的) SharedProperty 的类加以访问的。共享属 性以 object 实例的形式加以保存。object 是 C#关键字,用来表示 System.Object 类。正如后面要学到的,根据它所能保存的类,就能使共享属性具有多变性。 为了在 SharedProperty 类中访问 System.Object 的实例,可使用 Value 属性。 Value 属性可以读出也可以写入。共享属性管理器(Shared Property Manager) 把读和写的访问权锁定到这一属性上。清单 B-7 中的代码展示了如何使用 SharedProperty 类自属性中读取字符串值。笔者再一次将以前的组件中的 DoSomething 方法加以扩展。 清单 B-7 使用 SharedProperty 类 using System.EnterpriseServices; public class SomeComponent : ServicedComponent { public void DoSomething() { bool bGroupExists; bool bPropertyExists; // declare the property lock mode PropertyLockMode lm = PropertyLockMode.SetGet; // declare the property release mode enum PropertyReleaseMode rm = PropertyReleaseMode.Process; SharedPropertyGroupManager gm = new SharedPropertyGroupManager(); SharedPropertyGroup pg; pg = gm.CreatePropertyGroup( “myGroup”, ref lm, ref rm, out bGroupExists; ); SharedProperty sp = pg.CreateProperty( “myProp”, bPropertyExists); if (bPropertyExists) { Console.WriteLine((string)sp.Value); } else { sp.Value = “some interesting string”; } } } 一旦获得了对 SharedProperty 的访问权,就可以自由地向其赋值或是从其中读 出。在笔者的例子中,如果属性存在,就将其值写到控制台上。如果 DoSomething 方法已经创建了属性,就用一个字符串为其赋值。此处一个有趣的事发生了。请 注意,如果属性存在,笔者必须将 object 类型转化为 string 类型。这就是所谓 的“转向”(boxing)。为了更新读者的记忆,下面是有关“转向”的简单介绍。 转向(Boxing)把一个值的类型,如整数或是字符串,转化为诸如指向值类型的对 象一类的引用类型。反转向(unboxing)是转向的反操作:反转向将指向值类型的 引用类型转化为值类型。在清单 B-7 中,如果属性存在,笔者就把值反转向为一 个字符中并将其输出到控制台上。如果属性不存在,笔者只为其赋一个字符串值。 虽然看一下代码这并不明显,字符串“some interesting string”被转向为 Value 属 性。这称为隐式的转向变换。C#编译器智能化得足以清楚笔者想把字符串转化为 一个对象类型。如果笔者确实想要向其他读者明确说明笔者的代码,可以像下面 的代码一样书写: else { sp.Value = (object)“some interesting string”; } 此处笔者明确地把字符串转化为对象类型。虽然在技术上把字符串转化为对象是 不必要的,但可有助于经验较少的开发人员理解将要干什么。 诸如类一样的引用类型还可以保存为共享属性。当引用类型被读取或写入时,既 可明确地出现引用转换,也可隐式地出现。在明确地引用转换中,所要求的转换 类型必须在应用程序的代码中实现。另一方面,隐式的引用转化就不需要所需类 型的声明。通常,这是因为 C#编译器可以决定所要引用的类型。为了进一步说 明这一概念,笔者已经修改了 DoSomething 方法。这个方法作为共享属性增加了名 为 Cname 的类。 清单 B-8 把类的实例保存在共享属性管理器中 using System.EnterpriseServices; public class SomeComponent : ServicedComponent { public void DoSomething() { bool bGroupExists; bool bPropertyExists; // declare the property lock mode PropertyLockMode lm = PropertyLockMode.SetGet; // declare the property release mode enum PropertyReleaseMode rm = PropertyReleaseMode.Process; SharedPropertyGroupManager gm = new SharedPropertyGroupManager(); SharedPropertyGroup pg; pg = gm.CreatePropertyGroup( “myGroup”, ref lm, ref rm, out bGroupExists; ); SharedProperty sp = pg.CreateProperty( “myProp”, bPropertyExists); // create a new instance of CName and add it as a property CName name = new CName(“David”, “Roth”, “Lee”); // this is an implicit reference conversion sp.Value = name; // this is an explicit reference conversion name = (CName) sp.Value } // this is the class we will instantiate and add to the SPM public class CName { public string FirstName; public string LastName; public string MiddleName; public CName(string sFirst, string sLast, string sMiddle) { FirstName = sFirst; LastName = sLast; MiddleName = sMiddle; } 笔者创建了 CName 类的实例之后,将其赋给 SharedProperty 的 Value 属性。当 这个赋值进行时,编译器执行隐式的引用转换,从 Cname 引用类型转换为对象引 用类型。在代码的下一行中,笔者将对象引用(sp.value)转化为 Cname 类型。这 是显式的引用转换。 B.3 解决静态问题 既然笔者已经讲述了共享属性管理器 API,我们就可以解决本附录前面出现的共 享内存的问题了。在第一节的结尾处,笔者曾经做出结论说,如果能够锁定 iCount 静态域,以使在第一个线程完成之前另一线程不能将其修改,笔者就不 会得到 DivideByZeroException 错误。为解决这一问题,笔者使用共享属性管理 器 API 将 iCount 变量放在受保护的共享属性中。这一示例的代码显示在清单 B-9。 清单 B-9 修正共享内存问题 using System; using System.Reflection; using System.EnterpriseServices; using System.Threading; [assembly: AssemblyKeyFile(“C:\\crypto\\key.snk”)] namespace SharedProperties { public class SC : ServicedComponent { public void Start1() { PropertyReleaseMode rm = PropertyReleaseMode.Process; PropertyLockMode lm = PropertyLockMode.Method; bool bPropertyExists; bool bGroupExists; SharedPropertyGroupManager gm = new SharedPropertyGroupManager(); SharedPropertyGroup pg = gm.CreatePropertyGroup( “CounterGroup”, ref lm, ref rm, out bGroupExists); SharedProperty prop = pg.CreateProperty( “Counter”, out bPropertyExists); prop.Value = 2; int iSomeValue = 4; Thread.Sleep(500); int iResult = iSomeValue / (int) prop.Value; } public void Start2() { PropertyReleaseMode rm = PropertyReleaseMode.Process; PropertyLockMode lm = PropertyLockMode.Method; bool bPropertyExists; bool bGroupExists; SharedPropertyGroupManager gm = new SharedPropertyGroupManager(); SharedPropertyGroup pg = gm.CreatePropertyGroup( “CounterGroup”, ref lm, ref rm, out bGroupExists); SharedProperty prop = pg.CreateProperty( “Counter”, out bPropertyExists); prop.Value = 0; } } public class CApp { public static void Main() { Thread t1 = new Thread(new ThreadStart(Start1)); Thread t2 = new Thread(new ThreadStart(Start2)); t1.Start(); t2.Start(); t1.Join(); t2.Join(); Console.WriteLine(“Done”); } public static void Start1() { SC sc = new SC(); sc.Start1(); } public static void Start2() { SC sc = new SC(); sc.Start2(); } } } Main 方法应该看起来很类似。再一次,笔者创建两个线程、将其启动并等待每 个线程的完成。这次,不是在线程方法内作数学运算,而是让组件做所有的工作。 每个线程创建一个 SC 组件的实例。第一个线程调用 SC.Start1。SC.Start1 包括 与前面示例同样的代码,但有一点不同,就是从名为 Counter 的共享属性中获得 计数,而不是从静态变量中获取计数。由于笔者希望在整个 SC.Start1 方法调用 期间 Counter 属性被锁定,于是就将 PropertyLockMode 设置为 Method。如果将 其设置为 SetGet,Counter 属性只当线程从中读取或是向其写入时才被锁定。这 就意味着如果将 PropertyLockMode 设置为 SetGet 第二个续程仍然能够改变该属 性。 第二个线程调用 SC.Start2 ()方法。在 SC.Start2 ()中,避免重复创建属性组 和属性还是有吸引力的。如果假设创建了组和属性而且如果使用 SharedPropertyGroupManager.Group 和 SharedPropertyGroup.方法来获取组和 Counter 属性的 话,编写代码还是很快的。利用这一方法的问题来自于以下事实,作为一个开发 人员,笔者并不了解何时操作系统去安排线程。例如第一个线程可能在它创建了 组之后却在创建属性之前就切换了。如果安排了第二个线程且试图访问还没有的 属性,情况就糟糕了。 当这个应用程序运行时,线程一和线程二启动并分别在 SC 类上调用它们的方法。 正如前面一样,线程一进入“睡眠”状态,这使线程二有机会改变 Counter 属性。 线程一还没有结束,共享属性管理器仍然锁定 Counter 属性,结果,第二个线程 就不能把计数改变为 0。当第二个线程等待 Counter 属性锁定的释放时,它处于 阻塞状态。当第二个线程阻塞时,第一个线程又被再一次安排为运行。这就为 SC.Start1 提供了完成工作并释放对 Counter 属性锁定的机会。当 COM+释放锁定 时,第二个线程可以将属性值改变为 0 并退出。当这段代码运行时,就不会收到 DivideByZeroException 错误。问题就这样解决了。 现在读者可以看到共享内存对于多线程应用程序可能会引起的某些问题。共享属 性管理器为人们提供了保护组件的共享内存的方便的方法。使用从本附录中学到 的课程武装自己,就可以编写出更为有活力的应用程序来。 附录 C C#语言简介 C#语言是最近才加入 Visual Studio 大家庭的。该语言的语法与 C++甚至是 Java 有许多相似之处。C#常是进行.NET 编程特别是 COM+编程极好的语言选择,因为 C#设计为与.NET 框架无缝地一起工作。另外,C#提供面向对象的语言特性使其 很适合于基于组件的编程工作。 如果读者刚开始学习.NET 且对 C#不很熟悉,则本附录是一个很好的起点。在本 附录中读者将学到 C#语言的特性。而且,还会学到开发者在任何编程语言中都 要完成的通用任务。 C.1 名称空间 本书所编写的每个组件都包含在名称空间中。名称空间不是进行 COM+组件开发 所严格需要的,但使用名称空间确实提供了许多便利。首先,名称空间提供了一 种辨认代码的方法。具有相似特性或功能的类型可在一个名称空间中编成一组。 如果类被编组到一到多个名称空间内,其他可能需要使用这些类的开发者将容易 理解你的代码。第二,名称空间提供了一种解决代码中不同类型之间的命名冲突 的方法。为理解第二点,看看下面的代码示例。 Namespace NsOne { class CFoo { } } Namespace NsTwo { class CFoo { } } 前述代码包含两个类,都命名为 CFoo。通常,有相同名称的两个类不能存在于 同一个装配件中。使用名称空间就允许人们在同一装配件中定义两个类 CFoo。 引用第一个 CFoo 类时,也必须使用名称空间名。例如,为创建 CFoo 类的新实例, 编写了像 NsOne.CFoo = new NsOne.CFoo ()这样的代码。通常,编写.NET 应用 程序时,在引用类型时不必使用正在引用类型的名称空间的名称。然而,因为在 前述的例子中有命名冲突,必须使用名称空间名引用 CFoo 的第一个实例。 注意这里在引用 CFoo 时使用了点记号。在 C#中,所引用的任何东西几乎都使用 句点。名称空间也不例外。实际上,名称空间也可以包含句点。在.NET 框架中 会频繁地见到这一点。例如,上面代码示例中的 NsOne 名称空间可以改成 MyApplication.NsOne 且仍然是合法的名称空间。 名称空间也可以包含其他名称空间。例如,可以把前面的两个名称空间置于名为 MyApplication 的单个名称空间中。 Namespace MyApplication { Namespace NsOne { class CFoo { } } Namespace NsTwo { class CFoo { } } } 通过添加 MyApplication 将名称空间写为 MyApplication.CFoo,客户仍然可以 访问 NsOne.CFoo 类。 不管怎么说,必须能够通知 C#编译器,应用程序中将要使用什么名称空间。C# 关键词 using 服务于这个目的。这个关键词让编译器知道在代码中引用它们时到 哪里去解析类型。希望使用一个前面的 CFoo 类的客户可以在其代码的项部有一 个 using 语句,如下面代码所示。对那些已经在 C 或 C++中开发了应用程序的开 发者来说,using 关键词类似于在 C 或 C++程序中包含头文件的#include 编译器 指令; using MyApplication.CFoo; 在.NET 框架中命名的一些名称空间可能相当长。如果习惯于使用名称空间名来 引用类型,对长名称空间名这可能变得很冗长。幸运的是,C#允许为名称空间名 起别名。别名是名称空间的简称。别名一旦定义,就可像全名称空间名一样用于 引用类型。如果觉得每次需要名称空间的一个类型时 MyApplication.CFoo 写起 来太长,就可以这样做: Using NSl = MyApplication.CFoo; 现在,客户需要引用该名称空间中的类型,只要使用 NSl 名称空间; Using NSl = MyApplication.CFoo; NS1.CFoo = new NS1.CFoo(); C.2 流程控制语句 C#有许多语句可以在代码中用来控制应用程序执行的流程。在这一节中,读者将 学到一些基本语句如 if-else 语句和转向语句,另外,还会学到允许跳至代码中 不同名称空间的跳转语句。读者可能已经注意到这些语句与 Java 或 C++有很强 的相似之处。这种相似之处是为什么常常将 C#与那两种语言相比较的部分原因。 作者曾经听到有的程序员说能够用任何具有 if 语句的语言来编程。如果读者曾 经使用不止一种语言编写过代码,或者是曾经看过不同语言的代码,可能会发现 大多数 if-else 语句是很相似的。C#在这方面也没有很大的不同。在 C#中,要 检验的条件放在圆括号中。如果条件为真就执行大括号内的操作。下面的代码说 明了 C#的一个简单的 if 语句。 If (iVar == 10) { // do something here } 像大多数语言一样,C#支持 else 语句。如果语句中的条件判断为假,就执行 else 语句中的代码。 If (iVar == 10) { // do something here } else { // do something else } 在第一个条件判断为假的情况下指定额外的条件可能是必要的。C#支持这种目的 的 else if 语句。 If (iVar == 10) { // do something here } else if (iVar == 20) { // do something else } else { // iVar does not equal 10 or 20 } if 语句也可以嵌套,如下面的例子。 If (iVar == 10) { if (name == “fred”) { // do something here } } C.2.2 Switch 语句 Switch 语句允许对照多个常量判断一个表达式。Switch 语句提供了对多个 if-else 语句的彻底替代。典型的 Switch 语句如下所示。 switch(iVar) { case 1: Console.WriteLine(“one”) break; case 2: Console.WriteLine(“two”); break; case 10: Console.WriteLine(“ten”); break; } 语句先将变量 iVar 与值 1、2 和 10 加以对比。在这个语句中,iVar 是表达式,每 个 case 语句包含一个常量以便对照表达式加以测试。注意在每个语句的结尾, 必须加入一个 break 语句。C#中的 case 语句必须以某些类型的跳转语句如 break 或 goto 等结束。大多数情况下,使用 break 语句。C#不像 C++一样支持从头执 行到底的 case。而是一旦发现一个与表达式匹配的 case 语句,就执行该 case 语句并退出 switch 语句。 在 case 语句中,字符串和整数类型是惟一受支持的数据类型。数据类型可以是 名称空间中的任何下列类型: ∗ ∗ ∗ Int16 Int32 Int64 执行前面的语句时,将 iVar 的值与语句中的每个常量相比较。如果其中的一个常 量等于 iVar,就执行该 case 语句。一旦执行了该 case 语句,控制就转到 switch 语句外。如果没有参数匹配就可执行 default 语句。 switch(iVar) { case 1: Console.WriteLine(“one”) break; case 2: Console.WriteLine(“two”); break; case 10: Console.WriteLine(“ten”); break; case default: Console.WriteLine(“Could not find a match!”); Break; } 在这个例子中,如果 iVar 不等于 case 语句中的任何值,就执行 default 语句。 C.2.3 跳转语句 跳转语句允许转移到代码中的不同地方。读者已经看到了 C#的一个跳转语句: break。break 可用于循环或 switch 语句。 for (int j = 0; j < 10; j++) { if (j == 7) break; EvtLog.WriteEntry(j.ToString()); } Console.WriteLine(“finished”); 当 j 等于 7 时,退出循环并执行 Console.WriteLine 语句。在这种情况下,j 永 远不会增加到超过 7。 continue 语句也可用于循环中。continue 的工作方式与 break 相似,但遇到 continue 语句不会退出循环。可把 for 循环修改成用 continue 语句。 for (int j = 0; j < 10; j++) { if (j == 7) continue; EvtLog.WriteEntry(j.ToString()); } 在此处,j 等于 7 时,控制跳回到循环的顶部。EvtLog.WriteEntry 方法在这段时间 内不执行。当 j 增加到超过 7 时,EvtLog.WriteEntry 方法接着执行。 很多年来 goto 语句受到了很多负面的注意。像很多事情一样,如果过度使用它, 就可能产生混乱的代码。典型地,goto 用于跳到代码中的标号处,如下面的例子 所示: If (SomeValue == True) { goto MyLabel; } // some other program code MyLabel: // this code will execute goto 也可用于在 switch 语句内跳到 case 语句,如下所示: switch(iVar) { case 1: Console.WriteLine(“one”) break; case 2: Console.WriteLine(“two”); break; case 3: goto case 2; case 10: Console.WriteLine(“ten”); break; case default: Console.WriteLine(“Could not find a match!”); break; } 如果 iVar 的值为 3,goto 语句就把控制传到第二个 case 语句。即使第二个 case 语句没有 被判断为真,它仍然执行,因为已经通过 goto 语句将控制转移到该处。 return 语句用于在方法调用内将控制返回给调用者。对有返回值的方法来说, return 语句 用于返回这些值。void 类型的方法不必使用 return 语句。 Int GetId() { int i; return i; } C.2.4 异常的处理 .NET 框架中的许多类都会在某个时间产生异常。C#支持这些情况下的一种异常处理机制。 C#的异常处理是通过使用 try-catch 代码块执行的。基本的想法就是将可能导致异常的代码 放到 try 块内。catch 语句捕捉使用 catch 块内的代码可能产生的错误。 try { DatabaseObject.Open(); } catch (DBException dbe) { // log the error or report something to the user } 如果前面的数据库不能打开,它可能产生一个异常。catch 语句在错误到达用户 前将它捕 捉。在一些情况下,代码可能产生不止一种类型的错误。例如,打开前面的数据 库时,它可 能不产生 DBException 型的异常,而产生一种通常的异常。为捕捉每个可能产生的 异常,可 以将多个异常处理语句组合起来。 try { DatabaseObject.Open(); } catch (DBException dbe) { // log the error or report something to the user } catch (Exception e) { // log the error or report something to the user } finally 语句可用在 try-catch 块的结尾。finally 块内的代码总是执行而不管 产生的异常。 即使没有产生异常,也要执行 finally 块内的代码。 try { DatabaseObject.Open(); } catch (DBException dbe) { // log the error or report something to the user } catch (Exception e) { // log the error or report something to the user } finally { Console.WriteLine(“executing finally statement”); } C.3 在 C#中编写循环 C#支持几种循环机制,如那些在 C++和 Visual Basic 中所见到的。前面 for 循 环曾经用来演示 break 语句。for 循环有三个表达式:初始化语句、要判断的表 达式和迭代语句。初始化语句初始化循环变量,有时也可以包括变量的声明。循 环中的表达式判断为假时,循环就退出。迭代语句使循环计数器增加。在循环中, 循环计数器通常用在表达式的判断期间。 for (int i = 0; i < 10; i++) { // do something interesting } 在这个 for 循环中,声明了整数 i 并初始化为 0。对循环的每一次重复,i 的值 都增加 1。一旦增加到 10,表达式就判断为假,并退出循环。 for 循环也可用于向下计数。 for (int i = 10; i > 0; i--) { // do something interesting } 在前面的循环中,i 初始化为 10。循环的每次重复都使 i 减 1。一旦 i 达到 0, 循环就退出。 do 循环和 while 循环是相似的。两个语句都执行代码块直到条件判断为假。do 循环中的表达式设在循环的底部。不像 while 循环,do 循环至少执行一次而不 管表达式的判断结果如何。 do { i++; } while (i < 10); 即使 i 初始化为 10,该循环也会执行一次。另一方面,while 循环中的条件是在 循环执行前判断的。while 循环还把条件放在循环的项部。 while (i < 10); { i++; } foreach 循环可用于集合类或数组。这些循环迭代通过集合或数组中的每一项, 直到它们达到项目的终点。 ClerkMonitor cm = new ClerkMonitor(); Foreach (ClerkInfo ci in cm) { Console.WriteLine(ci.Description); } 在这个循环中,ClerkMonitor 是一个包含 ClerkInfo 类型项的集合类。循环中的每次 重复都以 ClerkInfo 的一个实例给变量 ci 赋值。在这种类型的循环中,元素类型(在 这里是 ClerkInfo)必须在循环表达式内部声明。 C.4 方法参数 方法参数可用影响参数的行为的特定语句加以修饰。本书的许多地方使用了这种 语句,所以可能要对本节加以特别注意。 ref 语句允许通过引用传入一个参数。方法执行时,它会看到方法被调用前参数 在代码中被设置的值。如果方法改变了参数的值,方法返回时新值是可见的。 注意下面列出的代码中,Ch 肋 gevdue 方法以关键词 Df 修饰参数 j。 public class SomeClass { public void ChangeValue(ref j); } 下面列出的代码说明 ChangeValue 方法的用法。因为 j 是一个 ref 参数,必须创建自己整数 实例以便把 j 传给 changVevalue。如果参数 j 不是 ref 参数,可以只简单地调用 sc.ChangeValue(1)。毕竟,把值 1 传给 ChangeValue 会使代码更具可读性。然而,对一个 ref 参数不能那样做。如果要把一个常量如值 1 传给 ChangeValue,ChangeValue 方法就不能够 改变常量的值(记住常量一旦指定一个值就不能被改变)。如果把常量 1 传给 ChangeValue, C#甚至不会编译下面的示例。 int j = 1; SomeClass sc = new SomeClass(); sc.ChangeValue(ref i); Console.WriteLine(j.ToString()); 如果方法 ChangeValue 把 j 的值改成 2,方法返回时就显示 2。因为在方法调用前,j 被设成 1,方法调用时 ChangeValue 就把 j 看成等于 1。要处理定义 ref 参数的方法可能需要点技巧。 一定要记住在定义方法的时候和调用方法的时候都要使用 ref 语句。Ref 参数也必须在传给 方法前被初始化。 out 参数的工作方式与 ref 参数相似,但对 out 参数来说,作为参数传递的类型不必在传递前 初始化。方法在把控制返回给调用者之前给参数指定一个值。在方法必须返回多个参数的情 况下 out 参数非常有用。 ResourcePool rp; SomeClass sc = new SomeClass(); sc.GetCount(out rp); Console.WriteLine(rp.ToString()); 在前面的示例中,变量 rp 没有初始化。为调用该方法,声明了一个变量 rp,并把它传给方 法调用。就像 ref 语句一样,out 语句必须在调用方法时使用。 C.5 数组 在 C#处理数组可能有点难,特别是如果用户具有 c++或 Visual Basic 背景更是如此。在 C# 中,数组可以是一维的、多维的或锯齿形的。数组也可以是任何的引用或值类型(举几个例 子说,如 System.Object、int、double、bool 和字符串)。所有的数组,不管其类型如何,都 继承于 System.Array。Array 类包含许多有助于用数组工作的方法和属性。该类允许执行二 进制的搜索、复制数组和索引数组。 看看整型数组是如何声明的: int[] iAry = new int[10]; 在 C#中,数组是用方括号声明的(相对于 visual Basic 中的圆括号)。在许多语言中,方括 号放在变量名之后,但在 C#中方括号却放在数据类型之后。数组是用关控词 new 创建的。 数组一旦创建就不能修改其维数。数组的维数必须用确定的值声明,如上述例子中的 10, 或者是常量。C#编译器不允许用变量表达式声明数组。也就是说,下面的代码示例在 C#中 是非法的。 int i = 10; int[] iAry = new int[i]; //非法! 数组元素是通过方括号访问的。C#数组元素的起始位置是 0。这类似于 C++处理数组的方式。 下面的代码用方括号访问数组起始位置上的元素。 iAry[0] = 100; 数组也可以在创建时初始化。 int[] iAry = new int[10] {1, 2, 3, 4, 5, 6, 7, 8, 9, l0}; 要初始化数组,初始值必须封在大括号内。 像前面已经介绍的,数组可以是多维的。多维数组以二维或多维包含其中的元素。二维数组 可像下面一样声明。 int[,] iAry = new int[3,2]; 当使用两维或多维的数组时,笔者喜欢将它们想像为表格或矩阵。前面的二维数组在内存中 排列开来很像图 C-10 逗号表示这个数组有两维。该数组是 3 个元素长 2 个元素宽。添加逗号并指定元素的长度可 以扩展数组的维数。下面的代码创建一个 4 元素长,3 元素宽,2 元素深的数组。 N N N N N N 图 C-1 二维数组 3 个元素宽 2 个元素宽 int[, ,] iAry = new int[4, 3, 2]; 多维数组可以在创建时初始化。前面的数组可以像下面一样初始化。 int[,] iAry = new int[3, 2] {{12, 3}, {4, 23}, {99, 0}}; 如果继续对数组使用表的概念,则它被初始化时就有点像图 C-2。 iRay [3, 2] = {12, 3}, {4, 23}, {99, 0}} 12 2 4 ○23 ○99 0 iRay [1, 1] iRay [2, 0] 图 C-2 初始化一个二维数组 C.6 基本数据类型 这里的基本数据类型是指整数、字符、字符串、双精度和浮点数等。C#有关键词起这些类 型的别名的作用。在 C#中,关键词指的是 System 名称空间中的结构,例如,int 是代表 System.Int32 结构的关键词。表 C-1 列出了 C#的数据类型关键词及其 System 名称空间中的 相关结构。 表 C-1 C#关键词 系统结构 大小(位) 取值范围 Sbyte System.SByte 8 -128 to 127 Byte Short System.Byte System.Int16 8 16s 0 to 255 -32768 to 32767 Ushort System.UInt16 16 0 to 65535 Int System.Int32 32 -2147483648 to 2147483647 Uint System.UInt32 32 0 to 4294967295 Long System.Int64 64s -9223372036854775808 to 9223372036854775807 Ulong System.UInt64 64 0 to 18446744073709551615 Char System.Char 16 0 to 65535 Float System.Single 7 digits 1.5 x 10-45 to 3.4 x 1038 double System.Double 15-16 digits 5.0 x 10-324 to 1.7 x 10308 bool System.Boolean 8 true or false decimal System.Decimal 128 1.0 x 10-28 to approximately 7.9 x 1028 枚举是特殊的数据类型。枚举包含一个已命名的常量的列表。它们能使代码更具可读性,因 为它们允许通过使用名称而不是具体的值来引用常量。在 C#中,枚举是用关键词 enum 声 明的。枚举可以声明为 byte、sbyte、short、ushort、int、uint、long 和 ulong。缺省情况下故 举定义为 int。 enum Animal { Dog, Cat, Fish, Bird } 必须用逗号分隔枚举中的每个成员。如果枚举成员的值没有明确定义,第一成员缺省设置为 0。在示例 Animal 中,成员 Dog 等于 0,Cat 等于 1,以此类推。如果需要,成员可以初始 化成常数值。 enum Animal { Dog = 3, Cat, Fish, Bird } 前面两个例子没有定义枚举的基本类型。在这里 Animal 被缺省设置为枚举。像下面的例子 一样,Animal 可以定义为 long 类型。 enum Animal : long { Dog = 3, Cat, Fish, Bird } C.7 结构 结构(structure,英文也写为 struct)是一种可以包含其他数据类型(int、char、double 等)、构造 器、方法和域的数据类型。结构是值类型,这对开发者来说意味着许多事情。首先,值类型 的内存是从线程堆栈分配的。从线程堆栈分配内存比从应用程序堆分配内存可获得性能提 升。第二,处理结构时,是在直接处理结构本身,而不是引用结构。这对于在程序中将结构 传给不同的方法时就很有意义。结构被传给方法时,传递的是结构的副本而不是指针或对结 构的引用。在下一节会看到与类是不同的,因为类是通过引用来传递的。结构在多个值代表 同一个概念或实体的情况下很有用。例如,如果需要在应用程序中代表地图坐标的情况下就 很合适。代表地图坐标的结构可能包含代表经度和纬度的整数以及代表位置的字符串。 在前一节中,看到了 C#中的每个数据类型关键词如何从 System 名称空间中为结构起别名。 作为开发人员,可用关键词 struct 定义自己的结构。 struct Location { int longitude; int latitude; string location; } 上面是一个名为 location 的包含地图坐标和位置描述的结构。在应用程序中,可以声明该结 构的实例并设置域的值。 Location Detroit; Detroit.longitude = 1000; Detroit.latitude = 99; Detroit.location = “Joe Louis Arena”; 在这个例子中,作者自己初始化了结构的域。然而,有时可能需要让结构自己初始化域。要 作到这一点,结构必须实现一个构造器。构造器是结构初始化时调用的方法。构造器可以包 含可用来初始化域的参数。一个结构可实现多个构造器。 如果要初始化 location 结构的域,可以实现带有 longitude、latitude 和 location 参数的构造器。 struct Location { int longitude: int latitude; string location; Location(int iLongitude, int iLatidude, string sLocation) { longitude = iLongitude; latitude = iLatitude; location = sLocation; } } 要使用这个新的构造器客户代码也必须修改。用带参数的构造器具体实现结构时客户必须使 用 new 关键词。 Location Detroit = new Location(1000, 99, “Joe Louis Arena”); 结构也可以实现方法。例如,Location 结构可能需要一个方法以从一个位置移动到另一个位 置。 struct Location { int longitude; int latitude; string location; Location(int iLongitude, int iLatidude, string sLocation) { longitude = iLongitude; latitude = iLatitude; location = sLocation; } void Move(int iLongitude, int iLatitude, string sLocation) { longitude = iLongitude; latitude = iLatitude; location = sLocation; } } 当处理结构时,必须记住以下规则: 结构不能声明无参数的构造器 实例域不能在结构中初始化 结构不支持继承 C#编译器代为生成无参数的构造器(也称为缺省构造器)。缺省构造器用来把结构的域初始化 成其缺省值。对整数类型,这意味着它们被初始化成 0;字符串则被初始化成空字符串(“”)。 如果用户企图实现自己的缺省构造器,编译器则会发出蜂鸣声以提示有错误。 在 C#中的大多数环境下,可以把域初始化成某种值。但在结构中这是不合法的。例如,在 结构中声明 longitude 域时,把它初始化是非法的。 struct Location { int longitude = 1000; // illegal statement for a struct!! int latitude; string location; } 要记住的最后一个有关结构的规则是它们不支持继承。继承允许通过继承属性、方法等,从 而从另一个类中派生功能。如果需要继承,就必须使用类。在下一节将会学到更多关于类的 内容。 C.8 类 类与结构很相似。类和结构的主要区别是类是引用类型,而结构是值类型。类作为参数传递 给方法时,传递的是对类的引用而不是构成类的实际位数。在这种意义下,引用是内存中类 所驻留的位置。类初始化时,内存是从托管内存堆分配的,而不是从线程堆栈分配的(第 1 章会提供更多关于.NET 托管内存堆的信息)。 像结构一样,类也包含域、方法和构造器。下面的例子把前一节的 location 结构转换成类。 class Location { int longitude; int latitude; string location; Location(int iLongitude, int iLatidude, string sLocation) { longitude = iLongitude; latitude = iLatitude; location = sLocation; } void Move(int iLongitude, int iLatitude, string sLocation) { longitude = iLongitude; latitude = iLatitude; location = sLocation; } } 像结构一样,类也包含域、方法和构造器。下面的例子把前一节的比 ahon 结构转换成类。 class Location { int longitude; int latitude; string location; Location(int iLongitude, int iLatidude, string sLocation) { longitude = iLongitude; latitude = iLatitude; location = sLocation; } void Move(int iLongitude, int iLatitude, string sLocation) { longitude = iLongitude; latitude = iLatitude; location = sLocation; } } 类和 Location 结构的惟一区别是使用关键词 class,这个 C#关键词是用来声明类的。 像前一节已经介绍的,C#可以支持继承。继承常常用于类之间有父子关系的时候。例如, 名为 Dog 的类可能有与哺乳动物的共同特征有关的属性,如毛和牙齿。类 Dog 也可能有方 法如 Bark()和 Sit()。另一个类例如 Labrador,可使用 Dog 类的功能并在此基础上用方法 retrieve()将其加以扩充。 class Dog { string Fur; int Teeth; Bark() { ... } Sit() { ... } } class Labrador : Dog { Retrieve() { ... } } 类 Labrador 通过在自己的定义(:Dog)中声明类 Dog 而继承了类 Dog 的域和方法。客户创建 类 Labrador 的实例时,能够访问 Fur 和 Teeth 属性并调用 Bark()和 Sit()方法。客户也能够调 用类 Labrador 实现的 retrieve()方法。例如,下面的客户代码在 C#中是合法的。 Labrador lab = nev Labrador(); lab.Bark(); 有些语言允许类从多个基类继承。基类是子类可以继承的任何父类。C#(或任何.NET 语言) 不支持多重继承。 .NET 中一些类的行为像基类一样严格。C#提供关键词 abstract 支持这种需要。抽象类不 能直接实现。类 Dog 可以通过向类的声明添加关键词 abstract 很容易地转换成抽象类。 abstract class Dog { string Fur; int Teeth; Bark() { ... } Sit() { ... } } 也可将类中的方法声明为抽象方法而不使整个类变为抽象类。从带抽象方法的基类继承的类 必须实现父类的抽象方法。另外,父类不必提供它所定义为抽象的方法的实现。正如可以在 下面示例的 Bark()方法中看到的,通过向方法的声明添加关键词 abstract 可将方法声明为抽 象方法。 class Dog { string Fur; int Teeth; abstract Bark() { ... } Sit() { ... } } 封装类可以想像为与抽象类相反。其他类不能继承封装类。在这个例子中,如果类 Dog 被 标记为 sealed 而不是 abstract,类 Labrador 就不能从类 Dog 继承。 sealed class Dog { string Fur; int Teeth; Bark() { ... } Sit() { ... } } class Labrador : Dog // this is an error! { Retreive() { ... } } 类的域或方法可声明几个能影响成员可访问性的修饰语。修饰语可以是下面的任何一个 public(公共的) protectd(受保护的) private(私有的) protected internal(受保护的内部的) 公共成员可从任何客户或于类加以访问。只有子类可以访问受保护的成员。只有实现成员的 类能够访问私有成员。只有同一个项目中的其他类型可以访问内部成员。只有同一个项目中 的子类能够访问受保护的内部成员。 这些修饰语中的一些可用于类本身。定义于名称空间内的类可标记为 public 或 internal。然 而,本身是其他类的成员的类可用任何访问修饰语标记。 C.9 属性 属性类似于域,因为它们代表着类或结构的成员的类型。属性封装了对域的访问。属性允许 验证客户正试图指定给域的数据。它们也可用于根据客户请求提取其中的值。从客户的观点 看,属性看起来正像任何其他的域。就像域一样,属性是用数据类型声明的。下面的类实现 了一个名为 EngineSize 的属性。 class Car { private string m_EngineSize; public string EngineSize { get { return m_EngineSize; } set { m_EngineSize = value; } } } EngineSize 属性是用 get 和 set 方法实现的。这些方法称为存取器。不管属性何时出现在等 号的左侧或客户何时需要读取属性值时,get 存取器都会被调用。不管客户何时给属性指定 一个值,set 存取器都会被使用。客户可以像使用类的域一样使用属性。 Car car = new Car(); MessageBox.Show(car.EngineSize); 在 set 存取器的内部,等号右侧的变量称为 value,这是一个代表客户正在传入的值的特殊关 键词。不管属性的数据类型如何,value 关键词总是可以使用。 属性可根据实现哪个存取器而设成只读或只写。例如,如果要使 EngineSize 属性只能写只 需简单地省略 get 存取器。 class Car { private string m_EngineSize; public string EngineSize { set { m_EngineSize = value; } } } 如果客户试图读取属性的值,编译器就产生一个错误。 C.10 索引器 索引器是 C#一个很优秀的功能。它们允许把类当作数组来使用。类的元素可被逐个迭代就 像它们是正常数组的元素。就像数组一样,索引器是用方括号和一个索引号访问的。索引器 以与属性相同的方式使用存取器。 class CIndexer { string[] names = new string[3] (“bob”, “joe”, “ralf”); public string this [int index] { get { return names[index]; } set { names[index] = value; } } 这个类实现了封装一个名称数组的索引器。笔者想在这里保持事情的简单化,但程序员通常 需要执行一些逻辑操作以确定客户是否已经给出超出范围的索引号。在索引器的声明中,用 的是关键词 this。类用关键词 this 引用它们自己当前正在运行的实例。接着,在括号内定义 了索引变量。类用该变量确定客户希望访问索引中的哪个元素。 客户以下面的方式使用索引器。注意客户如何像数组一样使用类。 //客户代码 CIndexer indexer = nev CIndexer(); for (int i = 0; i < 2; i++) { Console.WriteLine(indexer[i]); } C.11 不安全代码 C#和 Visual Basic 最值得注意的一个区别是 C#允许使用指针。指针运行于不安全的上下文 中。.NET 运行库看到在不安全上下文中声明的代码时,并不检验代码是否是类型安全的。 即使指针指向的内存是从受管理的内存中分配的(关于托管内存和垃圾收集器见第 1 章),垃 圾收集器也不能从堆栈看到指针。因为垃圾收集器不认识指针,必须作出特殊预警以保护应 用程序的指针。 public unsafe static void Main() { int I = 12; int* pI = &I; int J = *pI; Console.WriteLine(J.ToString()); } 上面的 Main()方法是用 unsafe 关键词声明的。该关键词告诉 C#编译器下面的代码块可能包 含指针。使用 unsafe 关键词的应用程序必须用/unsafe 编译器选项来编译。使用命令行编译 器或 IDE(集成开发环境)时可使用该选项。 pI 指针是用*操作符声明的。 “与”操作符(&)返回变量 I 在内存中的位置。在 C#中,*和 &的工作方式与在 C++中相同。*操作符只用于指针。不要将这个与 C#中的乘法操作符混淆, 虽然都使用相同的符号。*操作符用于返回在指针的内存地址中包含的值。另一方面,&操 作符返回类型的内存地址。在 pI 指针的声明中,I 的内存地址存储在 pI 中。 虽然下面的代码能编译和运行,还是有潜在的问题。如果整数 I 是类的一个域,且垃圾收集 恰好在指针的指定之后运行,应用程序的运行就可能出现问题。 public unsafe static void Main() { SomeClass sc = new SomeClass(); int* pI = &sc.I; // this could cause problems!! int J = *pI; Console.WriteLine(J.ToString()); } 垃圾收集可能在指针的指定之后运行。如果发生这种情况,sc 的内存地址及其域 I 就可能改 变。这会使指针无效,就像指针指向一个是 null 或被另一个类型占用的内存地址一样。 为避免这个问题,可用 fixed 语句在内存中固定实例 sc。通过在内存中固定类,可以阻止垃 圾收集器改变类的位置。 public unsafe static void Main() { SomeClass sc = new SomeClass(); int J; fixed (int* pI = &sc.I) { J = *pI; } Console.WriteLine(J.ToString()); } 在 fixed 语句中,指针的声明及其赋值可写在括号内。任何必须用指针运行的代码都可放在 跟在 fixed 语句后的代码块中。 读者可从本附录看到 C#是包含许多功能(如支持流程控制语句、循环、数组等)的全功能语 言,这是现代编程语言所希望的。本附录的目的不是教给 C#的全部细节而是介绍这种语言 并指出本书用过的一些功能。既然已经阅读了本附录,在学习本书的其余部分时就可能不会 对 C#的语言特点及其一些古怪形式感到陌生。 附录 D 补偿资源管理器 补偿资源管理器(Compensating Resource Manager,CRM)执行与第 4 章中讨论过的资源管理 器相似的任务。资源管理器是分布式事务处理极其重要的部分。它们提供对托管的资源如消 息队列和数据库受保护的访问。CRM 提供开发许多能提供资源管理器的大多数服务的 COM+组件的方法,而不需要付出努力去开发全范围的资源管理器。与全范围的资源管理器 不同,CRM 不提供数据隔离性(隔离性是第 4 章中的一种 ACID 规则)。隔离性在数据被改 变时对其他客户隐藏数据变化。CRM 提供全范围资源管理器的提交和回滚功能。 CRM 最常见的应用是提供对文件系统受保护的访问。应用程序必须经常访问文件系统以写 入数据以及移动或删除文件。CRM 也提供了管理 XML 文本文件的很好的方法。因为 XML 变得更广泛的被采用,XML 文档包含在事务处理中必须托管的商业数据是可能的。因为 Windows 文件系统没有资源管理器,CRM 通过允许在 COM+事务处理中保护对文件的访问, 从而帮助填补了这个缺陷。CBM 也允许停留在所熟悉的 COM+事务处理开发模型中。 在 C#中编写 CRM 所需的类在 Systsm.EntepriseServices.CompensatingResourceManager 名称 空间内。在这一节中,读者可通过用该名称空间内的类来学习编写 CRM。而且还要学习 CRM 的体系结构及 CRM 组件和应用程序必须符合的要求。如果在阅读本附录前未读过第 4 章, 则作者强烈建议读者这样做。第 4 章为读者提供理解本附录的概念和技术所需的背景。 D.1 补偿资源管理器简介 CRM 由三个组件组成: 工作者 补偿器 职员 工作者组件是 CRM 对客户可见的部分。工作者组件实现 CRM 的业务或数据逻辑。对所有 的意向和目标来说,工作者组件是 CRM 能识别的标准 COM+组件。 工作者组件是一种事务处理组件,其事务处理属性最好是设置成 Required。Required 确保工 作者运行于客户的事务处理中。而且如果客户没有事务处理,Required 则确保组件仍然运行 于一个事务处理中。如果工作者没有运行于事务处理中,这在相当程度上会使 CRM 的整个 目标失败。CRM 多用于客户是运行于事务处理中一个组件的情况下。如果客户退出其事务 处理,CRM 的工作是可以回滚的。 工作者组件必须向日志文件写入一些条目。然后,补偿器用这些条目既可回滚工作者的工作 也可把它变成持久的。日志条目必须在工作者执行其工作前写入。这个概念称为“提前写入”。 为理解为什么“提前写入”如此重要,请考察一下下面的情况。工作者组件执行五行代码, 每行代码都以某种方式修改数据。在第六行,工作者向日志文件写入一个条目。某一天,当 工作者正在运行时,有人碰到了服务器的电源线并且是在第三行代码正要执行时将它碰掉 了。断电导致整个系统关闭(作者知道有不间断电源可以防止这种情况)。此时,数据处于不 一致的状态。由于工作者没有向日志写入任何东西,无法确定哪些数据已经更新了又有哪些 数据还没有更新。工作者可以通过在执行工作前向日志写入记录避免这种情况。这不能保证 灾难性错误不产生问题,但有助于防止出现这类问题。然而,提前写入会产生另一个问题, 如果工作者使用了提前写入且如断电之类的问题在写入后立即发生,日志记录可能看起来什 么也没发生。补偿器必须有足够的智慧知道如何处理这种情况。在本附录的后面,读者将学 到处理这种情况的技术。 补偿器既可提交工作者的工件也可取消其工作,这要根据事务处理的结果而定。如果工作者 的事务处理提交了,COM+运行库就激活补偿器提交事务处理。补偿器通报(通过方法调用) 工作者写入的每条记录。此时,补偿器可能查看每条日志记录并用其中的信息使工作者的操 作永久化。 在事务处理中止的情况下,补偿器必须取消工作者所执行的任何工作。工作者写入的每条日 志记录都会通知补偿器。这就给了补偿器取消任何工作者已经执行的工作的机会。 补偿器可能多次接到关于事务处理结果的通知(提交或退出)。如果在事务处理的最后阶段发 生错误就可能发生这种情况。由于这个原因,补偿器的工作必须在补偿器被调用的每一次都 产生相同的结果。如果补偿器是以这种方式写入的,就称为是“等幂的”。例如,如果补偿 器打开一个 XML 文件并添加一些元素,这就不能认为是等幂的。如果补偿器多次被调用, 就可能向 XML 文件添加多个元素。另一方面,如果补偿器打开一个 XML 文件并改变一个 元素的一个属性,这可认为是等幂的。假设属性值每次都被设成相同的值,则多次改变 XML 元素的值不会产生不同的结果。实际上,如果没有任何帮助,等幂性是一项较难的事情。在 大多数情况下,实现足够的逻辑,可以使补偿器的操作等幂。例如,如果必须将一个元素跟 加到一个 XML 文件中,就可以实现逻辑以确定元素是否存在。如果元素不存在,就可以添 加。通过检查元素是否存在,就可事实上使操作成为等幂的。该规则使程序员知道在事务处 理的最后阶段补偿器可被多次调用的事实。 一定要清楚客户从来不会使用补偿器组件。是 COM+运行库在适当的时间具体实现并使用 补偿器。作为 CRM 开发人员,工作者和补偿器组件都要开发。 职员组件有两个责任:向分布式事务处理协调器(Distributed Transaction Coordinator,DTC) 注册补偿器和向日志文件写入记录。补偿器必须向 DTC 注册以便使 DTC 知道一旦工作者的 事务处理结束时激活哪个组件。工作者对象用职员执行这个操作。在.NET 框架中,职员类 的构造器被调用时工作者就注册补偿器组件。其他选项,如哪种状态(事务处理中止、事务 处理成功等)应该通知补偿器,也在此时定义。在下一节会更详细地讲述这些选项。 职员的主要工作是向日志文件写入记录。日志记录在写入硬盘前先写入内存缓冲区中。这改 善了性能,因为使对硬盘的访问达到最小。当缓冲区写满时,再把记录写入硬盘中。然而, 这就产生一个问题。如果工作者日志没有存储在硬盘上而是在临时的内存缓冲区,则如果应 用程序存在导致应用程序崩溃的问题就可能丢失日志记录。为避免这个问题,职员有一种方 法能强制将内存中的记录写入硬盘。强烈建议工作者组件在开始工作前使用这个种方法。读 者将在后面学习如何做到达一点。 图 D-1 展示在事务处理范围内和 CRM 的逻辑范围内所有这些组件如何配合到一起。在图 D-1 中,可以看到工作者组件运行于客户的事务处理中。也可看到工作者、补偿器和职员一 起工作就形成了 CRM。 COM+运行库 客户事务处理组件 工作者 Clerk 补偿器 应用程序日志文件 工作者使用客户向 DTC 注册补偿器 补偿资源管理器 向 DTC 注册 的补偿器 事务处理边界 分布式事务处理协调器 包含 CRM 组件的应用程序启动时 COM+ 创建日志文件。日志文件位于 %systemroot%\system32\dtclog 目录。这是保存分布式事务处理协调器的日志文件的同一个 目录。日志文件是用带扩展名的日志文件的应用程序 ID 命名的。请记住,应用程序 ID 是 一个用来在 COM+内惟一识别应用程序的 GUID。该目录内的 CRM 日志文件可能看起来像 {57206245-EAA4-4324-92CD-0DBAB17605D5}.crmlog(包括花括号)。 不幸的是对我们开发人员来说,CRM 日志文件是用记事本(Notepad)或其他文本编辑器难以 查看的二进制文件。正如读者已经知道的,每个 COM+服务应用程序都必须运行于配置好 的帐号下。缺省情况下,这是 Interactive 用户帐号。服务器包也能被配置成运行在不同于 Interactive 用户的用户帐号下。如果应用程序被配置成运行在不同于 Interactive 用户的帐号 下,该应用程序的日志文件就会受到保护以便只有该用户能访问这个文件。然而,如果应用 程序被配置成以 Interactive 用户的身份运行,日志文件就会从父目录(dtclog)继承权限。偶尔, dtclog 文件夹缺省地从 system32 文件夹继承权限。那么为什么要这么麻烦地保护 CRM 日志 文件呢?日志文件可能包含敏感信息,如帐号或决定放入日志中的任何其他内容。应当注意 如果应用程序的身份改变,管理员就不得不在日志文件中改变安全设置。COM+不会自动为 用户作出改变。 COM+通过创建和管理 CRM 日志文件并在处理完成时激活补偿器提供对 CRM 的支持。要 得到这种支持,COM+ 服务器应用程序包必须选中 Enable Compensating Resource Managers(启用补偿资源管理器)属性。图 D-2 显示了该属性位于应用程序的 Advanced 设置 选项卡的位置。 图 D-2 启用 CRM 支持 如果没有这一设置,COM+就不提供前面提到的任何服务。另外,除非启用该属性,职员组 件也不能实例化。 COM+支持 CRM 的恢复阶段。如果应用程序由于操作系统的崩溃或应用程序自己内部一些 不能恢复的错误而停止就会发生恢复阶段,应用程序再次启动时,COM+读取应用程序的日 志文件以确定是否有事务处理没有完成。如果存在没有完成的事务处理,COM+就与 DTC 服务联系以确定事务处理的结果。应用程序不会自己启动。应用程序中的组件被激活时它就 启动。这可能不是在 CRM 上执行恢复的最好时间。笔者建议查看 COM 管理 API(COMAdministration API)的文档以便找出系统引导时或是操作不繁忙时启动 CRM 应用 程序的方法。用这种方法,可以在客户试图访问组件时避免潜在的代价高昂的恢复操作。 D.2 用 C#开发补偿资源管理器 就像在本附录的开头提到的,CRM 是用 System.EnterpriseServices.CompensatingResourceManager 名称空间的类开发的。除非特别指 出,本节提到的所有类都是该名称空间的。 在接触该名称空间的真实情况前,先编写一个简单的 CRM 应用程序感受一下该名称空间的 类如何交互。在这个例子中,控制台应用程序起客户的作用。控制台客户调用工作者组件将 一个目录从一个地方移到另一个地方。如果该事务处理成功,目录被从位于 C:\temp 中的临 时位置移到最终的目的地。如果事务处理失败,就被从临时目录移到其原先的位置。补偿器 组件负责将目录从临时位置移到源目录或目标目录。为使补偿器知道应当使用什么样的源目 录和目标目录,工作者将两个目录都记录到日志文件中。该应用程序的代码列在清单 D-1 中。 清单 D-1 CRM 示例应用程序:移动目录 using System; using System.IO; using System.Reflection; using System.EnterpriseServices; using System.EnterpriseServices.CompensatingResourceManager; [assembly: AssemblyKeyFile(“C:\\crypto\\key.snk”)] [assembly: ApplicationActivation(ActivationOption.Server)] [assembly: ApplicationCrmEnabled] namespace XCopy { [Transaction(TransactionOption.Required)] public class CWorker : ServicedComponent { private Clerk clerk; public override void Activate() { clerk = new Clerk(typeof(XCopy.CCompensator), “Compensator for XCOPY”, CompensatorOptions.AllPhases); } public void MoveDirectory(string sSourcePath, string sDestinationPath) { clerk.WriteLogRecord(sSourcePath + “;” + sDestinationPath); clerk.ForceLog(); int iPos; string sTempPath; iPos = sSourcePath.LastIndexOf(“\\”) + 1; sTempPath = sSourcePath.Substring(iPos, sSourcePath.Length - iPos); Directory.Move(sSourcePath, “c:\\temp\\” + sTempPath); } } public class CCompensator : Compensator { public override bool CommitRecord(LogRecord rec) { string sSourcePath; string sDestPath; string sTemp; int iPos; GetPaths((string)rec.Record, out sSourcePath, out sDestPath); iPos = sSourcePath.IndexOf(“\\”); sTemp = sSourcePath.Substring(iPos, sSourcePath.Length - iPos); Directory.Move(“C:\\temp\\” + sTemp, sDestPath); return false; } public override bool AbortRecord(LogRecord rec) { string sSourcePath; string sDestPath; string sTemp; int iPos; GetPaths((string)rec.Record, out sSourcePath, out sDestPath); iPos = sSourcePath.IndexOf(“\\”); sSourcePath.Length - iPos}; Directory.Move(“C:\\temp\\” + sTemp, sSourcePath); return false; } private void GetPaths(string sPath, out string sSourcePath, out string sDestination) { int iPos; iPos = sPath.IndexOf(“;”); sSourcePath = sPath.Substring(0, iPos); iPos++; sDestination = sPath.Substring(iPos, sPath.Length - iPos); } } public class CClient { static void Main(string[] args) { CWorker worker = new CWorker(); worker.MoveDirectory(“c:\\dir1”, “c:\\dir2”); } } } 从顶部往下看一看这段代码。首先,声明要使用的名称空间,因为该应用程序在文件系统上 执行工作,必须声明 system.IO 名称空间。该名称空间包含在文件系统内移动目录的 Directory 类。最后的 useing 语句声明 CompensatingResourceManager 名称空间。这是另外一个对你来 说新的名称空间。所有其他的名称应当看起来很熟悉,就像在本书的几乎每个其他的 ServicedComponent 类中看到的。 在装配件属性一段,将 COM+应用程序定义成作服务器包运行,因为这是 CRM 的一个要求。 还要注意名为 ApplicationCrmEnabled 的新属性。该属性启用应用程序的 CRM 支持。当应用 程序在 COM+中注册时,它也使图 D-2 中的 Enable Compensating Resource Managers(启用补 偿资源管理器)复选框被选中。 在 XCopy 名称空间中定义的第一个类是工作者组件:CWorker 。该组件是从 ServicedComponent 类继承的,就像任何其他 COM+组件一样。该类要求事务处理。 CWorker类每次被激活时,都创建Clerk类的一个新实例(请记住,激活和实例化在COM+ 中是两个不同的东西。COM+在 COM+运行库调用组件根据 ServicedComponent 类重载的 Activate 方法时激活组件。COM+在组件的客户调用 C#关键词 new 时实例化组件。)Clerk 类 构造器在 COM+运行库内注册补偿器组件。Clerk 定义两个构造器,它们的第一个参数不同。 前面的示例包含以 system.Type 类为第一个参数的构造器。typeof()关键词出现在本书的其他 几章。正是为了更新读者的记忆,typeof()关键词就是为给定的类型返回 System.Type 类的 C#关键词。在这个例子中,传入了 Compensator 类的名称。 -------------------------------------------------------------------------------System.Type 类是使用反射的 应用程序的起始点。反射(Reflection)是一种开发者确定类型的各种特征的技术。使用反射, 开发者可以确定类在其中装备了什么样的属性,类支持多少个构造器以及类型支持的每个方 法和属性,此外还有其他内容。在 Clerk 类的情况下,typeof()关键词的类型允许.NET 运行 库确定类支持什么样的方法。------------------------------------------------------------------------------- Clerk 构造器中的第二个参数是一个可用于监控 CRM 的描述域。最后一个参数是一个枚举, 用来告诉.NET 运行库和 COM+要通知事务处理的哪个阶段。在这里,要通知处理的所有阶 段,所以传递 CompensatorOptions.AllPhases。有时可能只通知处理的提交或中止阶段。通过 为该枚举传递不同的值,可以只通知这些阶段。 CWorker 类的 MoveDirectory 方法执行该 CRM 的有关业务工作。在工作者组件作任何实际 工作前,它必须首先在 CRM 日志文件中记录将要做什么。在做任何工作前记录其操作, worker 组件就是在实践前面提到的提前写入技术。在示例中,向日志文件写入了单个记录。 该记录包含源目录和目标目录。Clerk 类的实例用于写入日志条目。注意作者将源目录和目 标目录组合到一个日志条目中。不想向日志写入两个条目(一个是源目录而另一个是目标目 录),因为在事务处理提交时这会产生两个补偿组件的通知。如果出现这种情况,补偿器就 会变得混乱,因为它在每个通知上只得到源目录或目标目录。writeLogRecord 方法不强制将 记录写入日志。而是将数据写入前面提到的临时缓冲区。要将记录永久写入日志,必须调用 ForceLog 方法。 一旦写入日志条目并将它强制写入日志文件,就可接着处理工作者组件。将源目录移到位于 c:\bmp 中的临时目录。现在还不想将目录移到目标目录,因为不知道事务处理是会提交还是 会中止。基于事务处理的结果,让补偿器决定目录是应当移到目标目录还是回到源目录。 接着,在源代码中定义补偿器。补偿器组件继承自 Compensator 类。该类是从 ServicedComponent 类派生来的。应用程序在 COM+中注册时,工作者和补偿器都作为服务 组件而出现。 Compensator 类提供许多在事务处理的所有阶段使用的虚拟方法。为保持这第一个示例的简 单,笔者只实现了 CommitRecord 和 AbortRecord 方法。COM+在事务处理提交或中止时调 用这些方法。 如果事务处理提交,就读取日志记录确定目标目录。这时必须进行少许字符串处理以便解析 出每个目录的路径。一旦得到目标目录,就将目录从其临时位置移到目标目录。如果事务处 理中止,就将目录移回到源目录。 工作者组件的客户是简单的控制台应用程序。它创建工作者组件的新实例并调用 MoveDirectory 方法,传入源目录和目标目录。在实际的应用程序中,客户最可能是另一个 事务处理组件,但控制台应用程序为此目的工作得很好。 如果在 MoveDirectory 方法内任何事情都正确进行,事务处理就提交。一旦 MoveDirectory 方法返回,事务处理就结束且 COM+激活补偿组件,调用 CommitRecord 方法。 当然,事情并不总是照所希望的那样进行。例如,源目录可能不存在。在这种情况厂,就会 产生一个异常,这就会毁坏事务处理。这就可能在 AbortRecord 方法中下一行的补偿器中产 生问题。如果源目录不存在,工作者就不能够将目录移到临时位置。如果事务处理中止,补 偿器就试图移动不存在的临时位置中的目录。为纠正这种情况,向补偿器和工作者添加少许 逻辑使它们保证不会试图访问不存在的目录。清单 D-2 中的代码展示了一个功能更强的 MoveDirectory 方法。类似的逻辑可放到补偿器的 CommitRecord 和 AbortRecord 方法中。 清单D-2 功能强大的MoveDirectory方法 public void MoveDirectory(string sSourcePath, string sDestinationPath) { clerk.WriteLogRecord(sSourcePath + “;” + sDestinationPath); clerk.ForceLog(); int iPos; string sTempPath; iPos = sSourcePath.LastIndexOf(“\\”) + 1; sTempPath = sSourcePath.Substring(iPos, sSourcePath.Length - iPos); if (Directory.Exists(sSourcePath)) { Directory.Move(sSourcePath, “c:\\temp\\” + sTempPath); } } 现在,只在源目录在文件系统中存在的条件下才移动目录。这避免了事务处理的中止,因为 不会试图移动不存在的目录。 当然,大多数应用程序要求比这更复杂的逻辑。例如,如果客户没有移动目录的适当权限, 就可能要中止事务处理。清单 D-3 中的代码只在用户有正确的权限条件下才移动目录如果 客户无权移动目录,事务处理就中止。 清单D-3 用于检查访问权限的修订过的MoveDirectory方法 public void MoveDirectory(string sSourcePath, string sDestinationPath) { clerk.WriteLogRecord(sSourcePath + “;” + sDestinationPath); clerk.ForceLog(); int iPos; string sTempPath; iPos = sSourcePath.LastIndexOf(“\\”) + 1; sTempPath = sSourcePath.Substring(iPos, sSourcePath.Length - iPos); if (Directory.Exists(sSourcePath)) { try { Directory.Move(sSourcePath, “c:\\temp\\” + sTempPath); } catch (SecurityException se) { clerk.ForceTransactionToAbort(); } } } 在这个版本的 MoveDirectory 中,捕获了 System.Security. SecurityException 异常。如果客户 无权移动目录就会产生这个异常。例如,可以假定装有 CRM 的服务器应用程序正在作为 Interactive 用户帐号运行。Interactive 用户帐号允许应用程序运行在直接调用者的安全上下文 下。这个练习的更复杂的实现是通过使用 COM+基于角色的安全检查调用链验证调用链中 的每个用户有权移动目录。然而,简单地捕获错误对这个例子就足够了。 ForceTransactionToAbort 方法是这里要注意的重要事情。就像名称所表明的,该方法强制事 务处理中止。这就允许实现确定事务处理是否应当中止的逻辑,而不是只依靠要产生的错误 或自己产生一个错误。 CommitRecord 和 AbortRecord 方法不是事务处理完成时 COM+调用的惟一方法。在实际事 务处理的第一个阶段期间也可能通知补偿器(见第 4 章)。在这个阶段期间,在补偿器上调用 下列三个方法(按下列的顺序)。 1. BeginPrepare 2. PrepareRecord 3. EndPrepare 所有三个方法都是虚拟的。它们只有在决定应用程序中需要它们的条件下才被调用。实现这 些方法并不是必要的。在 CRM 事务处理的恢复阶段这些方法不被调用。这些方法的目的是 允许补偿器在期待事各处理将要被提交的情况下准备其资源。如果事务处理不会被提交,就 没有什么理由准备资源。因为这个原因,如果事务处理已经中止,就不会调用这些方法。 在准备阶段的方法被调用后,就按下列顺序调用提交方法。 1. BeginCommit 2. CommitRecord 3. EndCommit BeginCommit 向补偿器传递一个布尔型标志。该标志表明在恢复阶段补偿器是否会被调用。 如果参数的值为真,补偿器就会在恢复阶段被调用。读者已经看到了 CommitRecord 方法。 这是补偿器应当用于提交工作者的工作的方法。EndCommit 方法通知补偿器它已经收到了 所有的日志通告。 补偿器是以与事务处理提交时通知它的方式相似的方式被通知中止事务处理的(当然,不包 括准备阶段)。在中止事务处理的过程中按顺序调用下列方法。 1. BeginAbort 2. AbortRecord 3. EndAbort 就像 BeginCommit 方法一样,BeginAbort 方法是用一个指明补偿器是从正常操作调用还是 从应用程序的恢复中调用的标志来调用的。 最后要介绍的技术是在 CompensatingResourceManager 名称空间的类中建立监控支持。 ClerkMonitor 类是一个包含 ClerkInfo 类的列表的集合类。ClerkInfo 类能访问与所有当前运 行于应用程序内的补偿器有关的属性。ClerkInfo 类支持下列属性。 补偿器的 ActivityID 用于注册补偿器的 Clerk 的实例 补偿器类实例 补偿器注册时指定的类 (实例 ID) InstanceID 工作的事务处理单元 在下列的代码中,笔者已经向 XCopy 名称空间添加了另一个类。 public class CMonitor : ServicedComponent { public void ListCompensators() { ClerkMonitor cm = new ClerkMonitor(); cm.Populate(); ClerkInfo ci = cm[0]; Console.WriteLine(ci.Description); } } 一旦创建了 ClerkMonitor,就必须调用 Populate 方法用所知道的补偿器和相关的 CRM 信息 填充集合。向屏幕输出的 Description 域是工作者组件注册补偿器时所用的同一个 Description 域。因为这个应用程序只有一个工作者和一个补偿器,只需访问集合的第一个索引。如果存 在更多的工作者和补偿器,可以用 foreach 循环的方式通过集合进行循环。
还剩218页未读

继续阅读

下载pdf到电脑,查找使用更方便

pdf的实际排版效果,会与网站的显示效果略有不同!!

需要 10 金币 [ 分享pdf获得金币 ] 0 人已下载

下载pdf

pdf贡献者

zerocyc

贡献于2014-12-23

下载需要 10 金币 [金币充值 ]
亲,您也可以通过 分享原创pdf 来获得金币奖励!
下载pdf