八. COM跨进程特性 进程外组件 列集 标准列集过程 总体结构 存根 代理 接口列集器 ORPC通道 标准列集的实现 自定义列集 进程外组件的使用 列集 列集的概念 接口指针的列集 标准列集过程 总体结构 存根 代理 接口列集器 ORPC通道 标准列集的实现 自定义列集 IMarshal接口 例子
1.进程外组件 1.1 进程内COM对象的缺陷 1.缺乏错误隔离. 对象的运行时错误将直接引起客户程序的崩溃. 2.安全环境共享 对象在客户进程中,使用的是客户进程的安全环境与权限. 这意味着特权客户程序创建的对象可以进行危险的操作; 较低权限的用户创建的对象可能无法访问一些重要的资源,从而无法达到计划的目的. 3. 很难实现分布式计算 进程内的对象使得“多个客户进程共享同一个对象”非常困难. 进程外的COM组件也是实现DCOM的基础.(另外一台机器上的进程.) 无论哪种方式(进程内,进程外,远程)对客户而言,是透明的. 进程外的COM对象具有重要的意义.
1.2 进程外组件的使用 进程外COM组件的实现以exe的形式存在.可以独立执行. 它 不会引出函数DllRegisterServer 和DllUnregisterServer. 相反,它执行时会检测命令行中是否有参数/ RegServer或/UnregServer, 以决定是否对注册表进行注册(注销). 注册表中它所支持的COM对象项中用LocalServer32键代替InprocServer32键. 进程外组件没有输出DllGetClassObject以让客户程序得到类厂对象,相反当客户调用CoGetClassObject创建类厂对象时: 在CoGetClassObject函数内部,它找到EXE组件的程序位置后,发现是一个进程外组件, 于是启动组件进程,然后等待... 组件进程启动后(使用了/Embedding参数),调用CoInitialize初始化,创建所有的类厂,调用CoRegisterClassObject把类厂注册到COM中. 客户进程得到了组件的类厂信息,创建类厂,然后创建对象…. 组件程序没有引出DllCanUnloadNow函数. 但是其判断是否可以卸载的条件与之相类似. 组件进程退出时,使用CoRevokeClassObject函数在COM库中注销掉其所支持的类厂. CoRegisterClassObject与 CoRevokeClassObject要配对使用,以保证COM信息的一致性. 进程外组件与客户进程之间使用RPC进行通讯.如下图所示:
2. 列集 客户程序 (客户进程) 代理对象 组件程序 (组件进程) 存根代码 LPC/RPC 组件对象 进程外组件与客户进程之间使用RPC进行通讯. 在客户进程与组件对象之间是代理对象和存根对象. 代理和存根直接使用RPC. 这里的RPC是经过了扩展的RPC, 称为ORPC. 在ORPC中, 调用请求和返回结果要经过列集和散集的过程. 其定义如下.
2.1 列集的概念 列集(marshaling) 和散集(unmarshaling) marshal的字典含义:编组、调度、引导、安排 整顿、配置、汇集、排列、集合 定义:是指客户进程可以透明地调用另一进程中的对象成员函数的一种参数处理机制 在调用过程中如果涉及到数值或指针的传递,则列集过程如下: 数值: 比如一个32位整数, 把四个字节的数据顺序装入到字节流中即可. 地址: 一个进程中的地址对另一个进程没有意义. 因此,列集时是把地址中的数据取出来封装到数据包中,散集时,在客户进程中分配一块内存数据包中的数据拷贝到内存中,然后返回内存地址. 接口指针: 实际上列集更重要的工作在于获取对象的接口指针.客户程序的一个有效接口指针代表客户进程到组件进程的一个连接.列集一个接口指针远比一个一般的指针要复杂. 以下讨论的列集一般都指接口指针的列集.
2.2 接口指针的列集 接口指针列集的结果是把它变为一个可以被传输的字节流,字节流的内容唯一地标识了对象和对象所处的环境. (即套间 Apartment 见后, 现在可以理解为运行环境) 列集过程分为两种:标准列集和自定义列集.由于列集要使用到底层的传输协议,而这些代码往往对所有的对象而言是类似的,所以COM提供了标准列集法, 凡是没有特别指明的,都是使用这种方法. 为了效率等因素,对象可以选择自己控制底层的通讯.这时称为自定义列集法. 标准列集方式下的接口指针列集数据流. 见下图:
接口指针的列集过程是由COM库函数CoMarshalInterface完成的: MEOW FLAGS IID STD FLAGS cPublicRefs OXID (Object Exporter ID) OID (Object ID) IPID (Interface Pointer ID) cch secOffset Host Addresses Security PackageInfo 签名符号 标准/自定义列集方式 被列集的接口IID 与标准列集有关的标志 引用计数 对象引出标识符 对象标识符(存根管理器) 接口指针标识符(接口存根) 字符数 安全信息偏移 其中OXID代表了对象的运行环境(套间). 代理需要使用此OXID来解析成网络或者IPC(进程间通讯)的地址,以便与对象的套间进行联系. OXID的解析工作由OXID解析器(OXID Resolver, OR)完成. OR是RPCSS服务的一部分. 接口指针的列集过程是由COM库函数CoMarshalInterface完成的:
HRESULT CoMarshalInterface( IStream *pStm, //列集数据的存放位置,是一个流.底层介质可以是磁盘,内存,或自定义的介质. 见结构化存储. REFIID riid, //列集指针的类型 IUnknown *pUnk, //列集指针,当然它应该是riid类型的. DWORD dwDestContext, //目标环境. void *long pvDestContext, //保留. DWORD mshlflags //常规列集还是表格列集(写到一个全局的接口表中,可以被多次散集) ); 散集过程由函数CoUnmarshalInterface完成 HRESULT CoUnmarshalInterface( IStream *pStm, //包含有列集内容的流 REFIID riid, // 散集指针类型 void * * ppvObj //存放散集指针的位置 一般而言,除非在进程内(而且套间类型相吻合),散集的接口不是原来的接口本身,而是一个代理.
3 标准列集过程 3.1 总体结构 如果一个对象没有实现IMarshal接口,那么它的引用都是按照标准列集方式进行. 列集所使用的通讯协议: COM使用ORPC (Object Remote Procedure Call)来进行远程的通讯. COM对MS RPC其进行了扩充,以支持面向对象的调用。称为ORPC。 ORPC使用标准的RPC数据包,附加上专用于COM的信息,如接口指针标识符。在ORPC数据包经过列集后的数据按照NDR格式保存(网络数据表示法Network Data Representation)(CORBA使用CDR Common Data Representation,Web服务使用XML)。 标准列集的通讯机制如下图:
标准列集的proxy和stub结构 客户进程 代理对象 组件进程 ITF1 客户程序 ITF2 ITF n 代理 管理器 IRpcChannelBuffer 系统 RPC 组件对象 存根代码 存根 IRpcProxyBuffer IRpcStubBuffer 通道 标准列集的proxy和stub结构
3.2 存根 当CoMarshalInterface第一次确定对象希望使用标准列集时, 就创建一个特殊的COM对象: “存根管理器”. (Stub Manager). 存根管理器与COM对象一一对应, 被对象标识符OID标识(见接口的列集数据图). 并且拥有一个对COM对象的引用.可以理解为一个进程内的客户. 存根管理器并不知道如何处理ORPC请求.它针对每个COM接口管理一个“接口存根”对象(interface stub).接口存根是用IPID来标识的. 接口存根知道关于这个接口的所有细节,它知道如何把ORPC请求消息中出现的所有[in]参数都散集出来,并且调用实际对象中的方法,然后把HRESULT结果和所有[out]参数列集到ORPC相应消息中去. 接口存根也有一个对COM对象的引用.
接口存根实现IRpcStubBuffer接口 class IRpcStubBuffer : public IUnknown { HRESULT Connect(IUnknown *pUnkServer) = 0; //把接口存根与目标COM对象联系起来 void Disconnect() = 0; //释放对象 HRESULT Invoke(RPCOLEMESSAGE *pMessage, IRpcChannelBuffer *pChannel) = 0; //当ORPC请求到达对象一方时,COM库会调用Invoke方法, *pMessage包含所有经过列集的[in]参数,也要利用RPC通道把处理结果发送回去 IRPCStubBuffer* IsIIDSupported(REFIID iid) = 0; ULONG CountRefs() = 0; HRESULT DebugServerQueryInterface(void **ppv) = 0; void DebugServerRelease(void *pv) = 0; };
3.3 代理 当CoUnmarshalInterface把一个标准列集得到的对象引用散集出来的时候,它会创建一个“代理管理器”(proxy manager). 和存根管理器一样,它也不懂COM接口的任何知识,它也要针对每一个接口创建一个“接口代理”对象(Interface proxy),并且把这些对象都聚合在其内部.让客户感觉所有的接口都是从这个代理管理器上实现的. 代理管理器实现了IUnkown的三个函数.并且对AddRef和Release进行了优化处理,使得这些操作非到最后,只是增减本地的一个引用计数.这样以减少网络开销. 接口代理把客户的调用请求转换成为ORPC请求消息(列集[in]参数).并且把ORPC相应消息中的[out]消息和HRESULT散集出来,返回给客户进程.
每个接口代理实现IRpcProxyBuffer接口. class IRpcProxyBuffer : public IUnknown {HRESULT Connect(IRpcChannelBuffer *pRpcChannelBuffer) = 0; void Disconnect() = 0; }; 接口代理管理器通过这个接口把接口代理与RPC通道连接起来,Connect方法把RPC通道保存起来 接口代理接到方法请求后,通过IRpcChannelBuffer接口的GetBuffer和SendReceive方法处理远程方法调用
3.4 接口列集器 接口代理和接口存根分别由代理管理器和存根管理器创建.它们共享同一个CLSID. 包含两个分叉实现的实体称为接口列集器(Interface marshaler). 接口列集器的类厂没有实现接口IClassFactory (有一个成员函数CreateInstance,以创建对应的COM对象) ,相反,它实现了接口 IPSFactoryBuffer. [ uuid(D5F569D0-593B-101A-B569-08002B2DBF7A),local,object ] interface IPSFactoryBuffer : IUnknown { HRESULT CreateProxy( [in] IUnknown *pUnkOuter, // 代理管理器指针 [in] REFIID riid, //请求的远程接口指针的IID [out] IRpcProxyBuffer **ppProxy, //输出接口代理指针 [out] void **ppv ); // 远程接口指针 HRESULT CreateStub ( [in] REFIID riid, // 请求的远程接口指针IID [in] IUnknown *pUnkServer, // 实际对象指针 [out] IRpcStubBuffer **ppStub ); }// 输出接口存根指针 接口列集器的CLSID存放在注册表中.
3.5 ORPC 通道 为了使用ORPC通道,COM提供了一个通道(channel)对象, 通道对象封装了ORPC的功能,它实现了接口IRpcChannelBuffer. typedef struct tagRPCOLEMESSAGE { void *reserved1; unsigned long dataRepresentation; // endian/ebcdic 编码方式 void *Buffer; // 载荷 ULONG cbBuffer; //载荷长度 ULONG iMethod; // 方法 void *reserved2[5]; ULONG rpcFlags; } RPCOLEMESSAGE; //ORPC消息的表示 class IRpcChannelBuffer : public IUnknown //ORPC通道 { HRESULT GetBuffer(RPCOLEMESSAGE *pMessage, REFIID riid) = 0; //分配缓冲区 HRESULT SendReceive(RPCOLEMESSAGE pMessage,ULONG *pStatus) = 0; //发送ORPC请求并接收相应 HRESULT FreeBuffer(RPCOLEMESSAGE pMessage) = 0; //释放缓冲 HRESULT GetDestCtx(DWORD *pdwDestCtx, void **ppvDestCtx) = 0; HRESULT IsConnected() = 0; };
4 标准列集的实现 COM已经提供了缺省的代理对象、存根管理器以及RPC通道 4 标准列集的实现 COM已经提供了缺省的代理对象、存根管理器以及RPC通道 我们只需实现每一个接口的代理/存根组件。参数和返回值的数据类型是关键。 首先使用IDL语言描述接口 编写IDL文件。产生dictionary.idl MIDL.exe是Win32SDK提供的工具。它能编译idl文档以产生以下代码:命令行: midl dictionary.idl 则产生下面的文件: dictionary.h —— 包含接口说明的头文件,可用于C或者C++语言; dictionary_p.c —— 该C文件实现了接口IDictionary的代理和存根; dictionary_i.c —— 该C文件定义了IDL文件中用到的所有全局描述符GUID,包括接口描述符; dlldata.c —— 该C文件包含代理/存根程序的入口函数以及代理类厂所需要的数据结构等。(DllGetClassObject等函数)
准备一个DEF文件 LIBRARY MyLib DESCRIPTION 'IDictionary Interface Proxy/Stub DLL' EXPORTS DllGetClassObject @1 PRIVATE DllCanUnloadNow @2 PRIVATE GetProxyDllInfo @3 PRIVATE DllRegisterServer @4 PRIVATE DllUnregisterServer @5 PRIVATE 创建一个空的 win32 dll工程 加入以上5个文件 project-》settings-》C/C++-》preprocessor definitions-》REGISTER_PROXY_DLL project-》settings-》Link-》object/library modules -》uuid.lib rpcrt4.lib 以上4,5,6 也可以编写一个MAKE文件在编译选项中加入REGISTER_PROXY_DLL连接选项中加入rpcrt4.lib、uuid.lib来完成 编译,注册。(代理与存根都是DLL,不要与进程外对象混淆) 在实际的编程工作中往往并不这样进行处理。因为集成开发环境已经提供了对IDL文件的编译支持。IDE可以启动MIDL对IDL进行编译。不需手工编写makefile。并且可以把代理存根和可执行代码编译在一起。
5. 自定义列集 为了性能的原因,我们有可能使用自定义列集.接口指针经过自定义列集后的数据流结构如下: MEOW FLAGS IID CLSID cb data 签名符号 标准/自定义列集方式 被列集的接口IID 自定义代理的CLSID 自定义列集数据的字节数 自定义列集数据 一个对象如果实现了IMarshal,则表明它希望使用自定义列集法.
5.1 IMarshal接口 IMashal接口是使用自定义列集或标准列集的标志: class IMarshal : public IUnknown { HRESULT GetUnmarshalClass( ...) = 0; //获取自定义代理的CLSID, 由CoMashalInterface调用. HRESULT GetMarshalSizeMax(...) = 0; //获取自定义对象引用的大小 由CoGetMarshalSizeMax调用 HRESULT MarshalInterface( ...) = 0; //对接口进行列集,写入流中 由CoMarshalInterface调用 HRESULT UnmarshalInterface(...) = 0; //从流中散集出接口来 由CoUnmarshalInterface调用 HRESULT DisconnectObject(...) = 0; //关闭连接 由CoDisconnectObject调用 HRESULT ReleaseMarshalData(...) = 0; //释放列集数据 由CoReleaseMarshalData调用. };
列集过程(第一次)发生在对象进程中 首先向对象查询是否实现了IMarshal接口,如果实现了,则调用其GetUnmarshalClass成员函数获取代理对象的CLSID (如果对象没有实现IMarshal接口,则指定使用COM提供的缺省代理对象,其CLSID为CLSID_StdMarshal,这是标准列集的过程 )。 调用GetMarshalSizeMax函数确定列集数据包最大可能的大小值,并分配一定的空间。 调用MarshalInterface成员函数建立列集数据包。 散集过程(第一次)发生在客户进程中 从stream中读出proxy的CLSID 根据CLSID创建一个proxy 获取proxy的IMarshal接口指针 调用IMarshal::UnmarshalInterface,把stream中的数据传给proxy,proxy根据这些数据建立起它与对象之间的连接,并返回客户请求的接口指针
自定义列集的要点 对象必须实现IMarshal接口 代理对象也必须实现IMarshal接口,并且代理对象与进程外对象之间协作 代理对象必须负责所有接口的跨进程操作 典型用途:提高跨进程调用的效率,使用缓存状态等优化技术
5.2 自定义列集例子 假定客户已经建立了它与类厂之间的连接,也就是说它通过CoGetClassObject获得了类厂的接口指针 5.2 自定义列集例子 假定客户已经建立了它与类厂之间的连接,也就是说它通过CoGetClassObject获得了类厂的接口指针 客户要通过类厂创建另一个COM对象,而这个对象使用custom marshaling 客户调用IClassFactory::CreateInstance创建对象,并返回对象的接口指针 注意这里类厂对象使用的标准列集, COM对象使用的自定义列集.
通过类厂建立代理对象和组件对象自定义列集过程