《COM原理及应用》学习笔记之第四章

第四章 COM 特性

1 、面向对象系统的三个最基本的特性

    封装性、多态性、重用性。

 

2 COM 特性的概述

COM 对象的封装特性是很彻底的,所有的对象状态信息必须通过接口才能访问;而 COM 的多态性完全通过接口体现出来,而且, COM 分别在三个层次上体现了多态性:接口成员函数、单个接口、一组接口(对象类别既 implemented category )。而 COM 的重用性相对复杂。

 

3 、重用性

    所谓重用性是指,当一个程序单元能够对其他的程序单元提供功能服务时,尽可能地重用原先程序单元的代码,既可以在源代码一级重用,也可以在可执行代码一级重用。

C++ 语言的重用性位于源代码一级,一个类可以继承于另一个类,从而把父类的功能重用。但对于 COM 组件则情形有所不同,因为 COM 是建立在二进制代码基础上的标准,所以其重用性也必然建立于二进制代码一级。

COM 重用性是指一个 COM 对象如何重用已有的 COM 对象的功能,而不是重复实现老的功能服务。按照 COM 的标准,实现这种重用性有两条途径:包容和聚合。

 

4 、包容和聚合

对象 B 调用对象 A 的相应成员函数实现 ISomeInterface 接口。因此,对象 B ISomeInterface 接口提供的功能可以超过对象 A 的接口功能,返回结果也可以不一致。甚至,对象 B 的接口与对象 A 的接口不一定相同。一般来说,对象 A 的生存期包含在对象 B 的生存期之内。

在聚合模型中,被聚合的对象 A 虽然直接向对象 B 的客户程序提供功能服务,但它的生存期仍受对象 B 控制,而且其他的一些行为也受对象 B 的控制,包括内部状态初始化、获取数据等等。

为了使聚合能够顺利实现,对象 A 必须能够适应在被聚合的情况下进行特殊的处理,尤其是接口的 QueryInterface 成员函数,在被聚合情况下,当客户请求它所不支持的接口或者 IUknown 接口时,它必须把控制交给外部对象,由外部对象决定客户程序的请求结果。

聚合涉及到聚合对象和被聚合对象双方的协作,并不是每个对象都能够支持聚合特性,但聚合体现了组件软件真正意义上的重用。而包容的重用性完全建立在客户/服务器模型相对性的基础上,实际上也就是客户程序和组件程序的嵌套关系。这是包容和聚合本质的不同。

 

5 、委托 IUnknown 和非委托 IUnknown

    对象创建函数 CoCreateInstance 的第二个参数 pUnknownOuter 用于解决聚合中 IUnknown 接口的问题。当其为 NULL 时表示正常使用,不为 NULL 时被聚合使用。内部对象实现两个 IUnknown 分别用于这两种情况:委托 IUnknown 和非委托 IUnknown delegating unknown nondelgating unknown )。

    按照通常使用方式实现的 IUnknown 为非委托 IUnknown ,而委托 IUnknown 在不同的情况下有不同的行为:当对象被正常使用时,委托 IUnknown 把调用传递给对象的非委托 IUnknown ;当对象被聚合使用时,委托 IUnknown 把调用传递到外部对象的 IUnknown 接口,即对象被创建时传递进来的 pUnkownOuter 参数,并且,这时外部对象通过非委托 IUnknown 对内部对象进行控制。委托 IUnknown 本身不进行任何操作。

    因为 C++ 类不支持同时实现两个 IUnknown ,所以委托 IUnknown 和非委托 IUnknown 不能都使用 IUnknown 类,但我们可以定义一个新的类。因为 COM 不是通过类名来识别接口,而是通过 vtable 来调用接口成员函数。

 

6 COM 接口调用的进程透明性

    客户程序创建 COM 对象具有进程透明特性,不管是进程内组件还是进程外组件,客户程序可以使用一致的方法创建 COM 对 象。对于进程内组件,无论是创建过程,还是客户程序对接口函数的调用过程,都可以按照一般的同一进程内部函数调用的过程来理解组件和客户之间的交互操作; 但对于进程外组件,实际的情形要复杂得多,因为组件程序户程序拥有不同的进程空间,所以,它们之间所有的交互过程都涉及到进程之间的通信过程。然而, COM 客户程序创建进程外组件程序成功后,它就得到了组件对象的一个接口指针,通过该指针间接调用组件对象的成员函数,如同调用本进程内的函数一样,这正是 COM 所期望达到的透明效果。

 

7 、进程外组件对象与客户程序之间通信过程

    接口指针所指的是本进程中的代理对象( proxy ),客户调用的是代理对象的成员函数,由代理对象通过跨进程的调用方法( LPC RPC )与对象进程中的存根代码( stub ) 通信,存根代码再调用组件对象成员函数。函数返回的顺序刚好相反。在这个交互过程中,可以看到,客户仍然在调用同一进程内的组件对象,而组件对象也被同一 进程内的客户调用,从客户和组件对象两个角度丝毫感觉不到进程的边界,所有跨进程的操作完全由代理对象和存根代码包揽了。

 

8 、列集( marshaling )与散集( unmarshaling

    列集是指客户进程可以透明地调用另一进程中的对象成员函数的一种参数处理机制。

代理对象用列集手段处理成员函数的参数,通过列集处理后得到一个数据包(数据流),然后通过一种跨进程的数据传输方法,比如共享内存方法,甚至是网络协议等,当数据包传输到对象进程后,存根代码用散集( unmarshaling ,列集的反过程)的方法把数据包参数解译出来,再用这些参数去调用组件对象;当组件对象成员函数返回后,存根代码又把返回值和输出参数列集成新的数据包,并把数据包传到客户进程中,代理对象接收到数据包后,把数据包解译出来再返回给客户函数,从而完成一次调用。

 

9 、连接

    连接是指客户进程与组件进程的一种依赖关系,简单地说,客户程序的一个有效接口指针就代表了一个连接。

    连接是在函数调用的过程中产生的,最常使用的 QueryInterface 就是一个很好的例子。

    连接是跨进程通信的基础,新的连接本身也是在其他连接的调用过程中产生的。

 

10 、不同参数的列集处理

    32 位整数只要把 4 字节的数据顺序装到数据包中或者从数据包中去出来即可;字符串或者结构类型的数据列集过程也可以按此方法处理。

    对指针的列集处理过程是:列集时,把指针所指的数据装到数据包中;散集时,在进程中分配一块内存,把数据包中的数据拷贝到内存中,所得内存的地址即为散集的结果。

如果函数的参数中包含了指向接口的指针,则情形要复杂得多。接口的列集包含了代理对象和存根代码的创建过程,实际上接口指针的列集过程也包括了连接的创建过程。

 

11 、列集过程的两种实现方式:

    自定义列集法( custom marshaling ),也称为基本列集法( basic marshaling architecture) 。其列集过程完全由对象自身控制,对象指定其代理对象的 CLSID ,代理对象控制了其所有接口的列集过程,包括接口参数的列集和散集,以及代理对象和存根代码之间的跨进程通信过程。       

    标准列集法( standard marshaling ),是由 COM 提供缺省的代理对象和存根代码,因为列集过程涉及到操作系统的一些复杂特性的编程,如共享内存操作或其他跨进程数据传输机制,甚至通过网络协议传输数据,所以 COM 提供了缺省的代理和存根代码以及一套标准的列集方法,可以处理常用数据类型的列集和散集,包括指针类型和接口指针类型。

    标 准列集法的原理以及其列集过程与自定义列集法完全一致,事实上标准列集法是自定义列集法的一个特例。但两者有一个基本的不同:自定义列集法其列集过程完全 由对象自身控制,所以它以整个对象为列集单位,即对象指定的代理对象和存根代码必须处理对象支持的所有接口;而标准列集法使用 COM 提供的标准代理对象和存根代码,实际上该代理对象和存根代码只是列集过程的管理器,因此,标准列集法是以接口为列集单位, COM 提供的很多标准接口,其列集过程已经由 COM 库提供了,程序员只需要提供自定义接口的列集代码即可。

 

12 、自定义列集

 

13 、标准列集

 

14 、标准列集的实现

    COM 已经提供了缺省的代理对象、存根管理器以及 RPC 通道,我们只需要实现每个接口的代理/存根模块。一旦系统中安装了某个接口的代理/存根程序并正确地进行了注册,则代理管理器和存根管理器会在需要的时候自动加载接口代理和接口存根。因此,从实现的角度来讲,我们的任务就是针对接口实现代理/存根程序。

    代理/存根组件是一个 DLL 程序,除了实现接口代理和接口存根之外,还应该实现相应的类厂,代理/存根组件要求类厂支持 IPSFactoryBuffer 接口,通过 IPSFactoryBuffer::CreateProcy IPSFactoryBuffer::CreateStub 成员函数创建接口代理和接口存根对象。接口代理对象支持两个接口:它本身提供列集特性的接口和 IRpcProxyBuffer 接口,其中 IRpcProxyBuffer 接口只有 Connect Disconnect 成员函数,被代理管理器用于创建或取消它与 RPC 通道的连接;它本身提供列集特性的接口的成员函数接受客户程序的调用,并把客户的调用参数放到 RPC 通道中,然后调用 RPC 通道的 SendReceive 成员函数,函数返回后,把返回值和输出参数解译出来。这些操作是接口代理对象应该完成的。与此相对应,接口存根只要实现 IRpcStubBuffer 接口,除了存根管理器所调用的几个与 RPC 通道连接的函数外,最主要的成员函数为 IRpcStubBuffer::Invoke RPC 通道调用此函数 以响应客户进程的 SendReceive 调用, Invoke 函数把客户进程传递过来的参数解译出来,然后调用组件对象的接口成员函数,并把返回结果或者输出参数经过 RPC 通道传回到客户进程的 RPC 通道中。

    接口代理对象和接口存根对象必须非常小心地处理接口成员函数的参数,尤其是一些指针或者结构参数,如果成员函数中包含接口指针类型,则还需要调用 CoMarshalInterface 或者 CoUnmarshalInterface 函数,以便创建相应的存根或者代理对象。在代理和存根中对参数的处理必须严格一致,否则会发生不可预料的后果。如果客户进程和组件进程在同一台机器上运行,则 COM 会 根据注册表中的接口信息,在两个进程中使用相同的代理/存根程序,所以我们只要保证接口的代理/存根程序中对参数的列集和散集格式一致,参数传递就不会有 问题;但如果客户与组件程序在两台机器上,则不能严格保证两个进程会使用相同的代理/存根程序,那么对参数的列集和散集最好使用统一的数据格式表示,以保 证参数传递的正确性。

    如果一个进程外组件实现了多个 COM 接口,那么是否需要为每一个接口实现其代理/存根组件程序呢?在这些接口中,如果它是 COM 提供的标准接口,或者是 OLE 标准接口,则 COM 或者 OLE 已经提供了其代理/存根程序,我们可以不管这些接口,直接使用即可;如果自定义的接口,则必须自己实现代理/存根程序,并注册到系统中,然后才能真正使用这些接口。

 

15 、自定义接口的代理/存根程序的实现

    Microsoft 提供了 MIDL 实用工具帮助我们建立自定义接口的代理/存根程序。首先我们使用 IDL (接口描述语言)语言建立接口描述文件,然后运行 MIDL 工具,它会根据接口描述文件生成一些 C 语言源代码文件,用这些源代码文件可以创建代理/存根组件程序。它为我们提供了接口代理/存根组件的一种标准实现方法。

    IDL 描述接口与 C++ 描述接口有一些相似之处,但 IDL 是一种平台无关的标准化描述语言。 Win32 SDK 提供了所有 COM 或者 OLE 标准接口的 IDL 描述,包括 unknown.idl (定义了 IUnknown 接口),可以在 Visual C++ include 目录下找到这些 IDL 文件。 Microsoft RPC 开发包中包括运行程序 MIDL.EXE 和接口列集使用的 RpcProxy.h Unknown.idl wtypes.idl ,以及 RPC 调用所需要的静态连接库和动态连接库。

    一般地,用 MIDL 程序可以产生实现代理/存根组件程序所需要的所有 C 语言源代码文件: ***.h 为接口说明头文件; ***_p.c 为接口代理和存根的实现文件; ***_i.c 为定义所有 GUID 描述符的文件; dlldata.c 包含代理/存根程序的入口函数及类厂所需的数据结构。

    运行 NMAKE 程序可以生成代理/存根组件程序,在集成开发环境中也可以生成。在集成开发环境中创建一个工程,并把 MIDL 生成的源代码文件的 DEF 文件加入到工程中,并在编译选项中加入 REGISTER_PROXY_DLL ,在连接选项中加入 rpcrt4.lib uuid.lib

    COM 库能够提供代理管理器和存根管理器,并且 MIDL 又能够自动生成自定义接口的代理和存根源代码,但目前, COM 库还不能在运行过程中根据接口的描述自动生成接口代理和接口存根来处理自定义接口的列集过程。

 

16 MIDL 创建自定义接口代理/存根组件程序的过程

(1)               编写接口的 IDL 文件;

(2)               运行 MIDL 工具生成相关的源代码文件;

(3)               编写 DEF 文件;

(4)               编写 MAK 文件;

(5)               编译连接得到接口/存根组件程序;

(6)               运行 regsvr32.exe 注册组件程序。

 

17 COM 的安全性

    安全性不是 COM 的主要目的,但既然 COM 是一种平台独立的软件模型,而且提供了跨进程甚至跨网络的客户/服务器软件结构,则安全性是不可缺少的保护机制。建立一种适合各种操作系统的安全性机制是不可能的,所以 COM 规范也只是提供了安全性机制框架。在 Windows 平台上实现的 COM 版本基本上基于 Windows NT 的鉴定服务( authentication service )机制。

 

18 Windows NT 安全机制

    Windows NT 作 为网络操作系统,具有完全的保护机制,系统的所有资源都是受保护的,这些资源包括文件、外设、进程、线程,甚至同步对象、共享内存、注册表中的键等等。所 谓受保护是指这些资源与特定的访问权限联系在一起,当这些资源被访问时,操作系统要对权限进行验证,以便允许访问或者禁止访问。

 

19 RPC 鉴定的 5 个层次

(1)               无鉴定操作即正常的 RPC 调用;

(2)               连接时进行鉴定;

(3)               每一个接口调用时进行鉴定

(4)               对每个请求进行鉴定,并对接收到的数据包进行完整性检验;

(5)               进行所有的鉴定并对数据包加密。

   

20 COM 提供了两种类型的安全性

    激活安全性( activation security ),不同于激发安全性( launch security ),包括 COM 对象如何被安全地启动、客户如何与对象建立连接,以及如何保护公共的资源,比如全局运行对象表、系统注册表等。

    调用安全性( call security ),是指在已经建立连接的基础上,客户调用组件程序的安全保护问题。

 

21 、激活安全性

    SCM COM 库中负责找到并启动组件程序的组件。当客户向 COM 库请求创建新的 COM 对象或者连接已经运行的组件对象时,负责处理请求的正是 SCM 。因此,激活安全性也通过 SCM 实现。

    激活安全性是进程一级的安全性,即进程中所有的对象和所有对象的成员函数共享的安全性,它分两种情况:静态安全性和动态安全性。当 SCM 接收到激活对象的请求时,它检查注册表中安全配置信息,以便满足合法用户的请求,这称为静态安全性检查;另一种情况是,在程序运行过程中设置进程的安全性,这称为动态安全性检查。

    Windows 提供的工具 DCOMCNFG.EXE (在控制面板中)可以对组件的安全性进行设置。

 

22 、调用安全性

    调用安全性的实现方法之一是使用 IClientDecurity 接口,方法二是使用 COM 提供的 API 函数。 IClientDecurity 是接口代理选择实现的接口,它的三个主要成员函数: CopyProxy QueryBlanket SetBlanket COM 提供了几个 API 函数封装了接口 IClientDecurity 的调用: CoQueryProxyBlanket CoSetProxyBlanket CoCopyProxy

    MIDL 生成的接口代理对象实现了 IClientDecurity 接口,并且系统代理管理器也实现了 IClientDecurity 接口,所以并不需要自己实现 IClientDecurity 接口。

 

23 Win32 线程和 COM 线程

    Win32 提供两种线程: UI 线程( user-interface thread ,也称为用户界面线程)和辅助线程( worker thread

    对应于 Win32 的两种线程, COM 也有两种线程类型:套间线程( apartment thread )(对应于 UI 线程)和自由线程( free thread )(对应于辅助线程)。

    COM 线程特性是针对特定的 COM 对象,而不是针对 COM 组件程序,所以在同一个 COM 组件中的不同对象可以运行在不同的线程类型上。

 

24 COM 线程的使用

    1 )进程内组件对象

    如果一个 COM 对象运行在一个套间线程中,那么此 COM 对象与 UI 线程中的窗口对象有很类似的特性。 COM 对象属于创建此对象的套间线程所有。套间线程通过消息控制函数被自动同步,所以,运行在套间线程中的 COM 对象,不需要进行同步处理,但套间线程外的客户的其他线程要访问此线程只能通过代理/存根实现。

    如果一个 COM 对象运行在一个自由线程中,那么同一进程中的其他线程(即客户线程)可以直接调用此对象成员函数,但对象成员函数必须进行同步处理,以保证其线程安全性。

    2 )进程外组件对象

    如果是进程外组件对象,则不管其运行在套间线程还是自由线程中,客户调用必须跨进程,因此调用始终是间接进行的,所以列集对于进程外组件对象是必须的,而列集的结果是自动实现同步的,对象成员函数可以不处理同步。

    套间线程中的对象被跨线程调用时,与跨进程调用有着类似的特性。所以套间线程有自己的 COM 库初始化和终结过程调用。

 

25 、列集和同步

    COM 对象的不同线程模型影响的主要是列集处理和同步处理。结果列集处理的调用总是通过代理和存根间接进行,因此,其效率也自然有所降低,但列集使所有对对象的调用通过消息循环中转,所以调用被自动进行同步处理,某一时刻至多只能有一个调用在进行,所以 COM 对象不需要进行同步处理,也就是说 COM 对象可以不是线程安全的( thread-safe )。反过来,不通过列集处理的调用虽然是直接进行的,效率也比较高,但某一时刻可能会有多个客户同时调用,因此,对象必须要进行同步处理,以便保证对象是线程安全的。

 

26 、不同线程模型(客户线程与对象线程的不同组合)对列集和同步的不同要求

    1 )客户和对象运行在不同的进程中。客户调用进程外组件总是要通过代理和存根,所以列集是必须的。因此, COM 自动实现了调用的同步处理,对象不必进行同步处理。

    2 )客户和对象运行在同一个线程中。与对象处于同一线程中的客户调用对象总是直接进行的,而且同一线程中的调用不可能冲突,所以客户调用即不需要列集,而且对象也不必进行同步处理。

    3 )客户和对象运行在同一个进程中,对象运行在套间线程中,客户运行在另一个套间线程或自由线程中。因为对象运行在它自己的套间线程中,所以客户调用总需要列集处理, COM 自动实现同步处理,对象不必考虑同步。 COM 会自动为我们实现接口指针的列集处理,我们也可以自己对接口指针进行列集处理。

    4 ) 客户和对象运行在同一个进程中,对象运行在自由线程中,客户运行在另一个套间线程或自由线程中。当客户调用自由线程中的对象时,虽然对象被自由线程所创 建,但调用实际上在客户线程中执行,所以客户对接口的调用是直接进行的,因此接口列集是不必要的,但对象必须自己处理同步,因为多个客户有可能同时调用接 口成员函数。

 

27 、套间线程

在套间线程的主函数中有一个消息循环,而且主函数必须对 COM 库进行初始化。 COM 在套间线程中创建了一个隐藏的窗口,主函数的消息循环负责接收消息并分发消息(包括客户对对象的调用的消息)。

对于运行在套间线程中的 COM 对象来说,因为这样的对象只能被此线程访问,其他的线程只能通过代理/存根调用接口函数,所以对象可以不必担心同步问题,但对象仍然需要保护全局变量,因为对象的成员函数被所有的同类对象所共享,因而函数有可能会重入。进程内组件 DLL 程序的入口在多线程环境下有可能被同时访问到,因此,这些入库函数如 DllGetClassObject DllCanUnliadNow 仍然需要进行同步处理,以保证多线程访问时不会发生冲突。进一步来讲, DLL 组件程序的类厂也必须满足一定的要求,以保证多个线程同时访问类厂对象时不会引发冲突问题,尤其当用类厂对象创建多个组件对象时,类厂必须是线程安全的,即内部提供了同步处理。使类厂线程安全只需对引用计数操作进行同步保护即可。

如果套间中的函数要把接口指针传给另一个线程,不管此线程是套间线程还是自由线程。列集和散集是必须要进行的,列集处理分两种情况:自动列集和手工列集。自动列集的情况比较简单,凡是通过 COM 传递的接口指针, COM 都会自动列集,包括装入接口代理和存根代码等等。手工处理列集也是可能的,因为客户线程与对象线程在同一个进程中,因此,通过其他途径传递接口指针也很方便。

 

28 、自由线程

    自由线程在概念上与 Win32 的辅助线程完全一致。它们只有一个主函数,当主函数执行完成后,线程就自动结束。在自由线程的主函数中,必须调用 CoInitializeEx 函数,而且 dwCoInit 参数必须指定为 COINIT_MULTITHREADED ,以便 COM 知道这是一个自由线程。自由线程中的 COM 对象必须是线程安全的,所有的同步工作由对象自己处理。

    与套间线程类似,自由线程即可以由客户程序创建,也可以由类厂创建,但不管哪种情况, COM 对象总是由自由线程的主函数来创建。

 

29 、进程内组件的线程模型

    通常进程内组件并不调用 CoInitialize 或者 CoInitializeEx 标识其对象所使用的线程模型,但是, COM 需要知道进程内对象的线程模型,以便正确处理跨线程情况下接口指针的传递以及对象调用的同步处理,所以,我们要在系统注册表中指定对象的线程模型。

    对于进程内组件程序,为了支持多线程的情形,不管是套间线程模型还是自由线程模型,其入口函数 DllGetClassObject DllCanUnloadNow 应该是线程安全的,尤其需要对引用计数包括对象引用计数器以及锁计数器等进行同步保护。

    在实际使用过程中,通常对象被客户线程所创建,因此客户线程模型与对象的线程模型有可能不一致,这种不一致性包含两种可能:支持套间线程模型的对象被自由线程所创建,则 COM 会生成一个套间线程来运行对象,并把列集后的接口指针传给客户线程;第二,支持自由线程的对象被套间线程所创建,则 COM 会生成一个自由线程来创建对象,并把接口指针经列集后(可以优化)传给套间线程。

    套间线程模型的 COM 对象,如果用到了自定义接口,则即使是进程内组件程序,也需要提供自定义接口的代理/存根程序,以便客户跨套间调用对象时能够使用正确的接口代理和接口存根。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值