【翻译说明】最近,想看看DCOM的通讯能否在Linux平台上实现(其实是想实现OPC Client),就找了两篇文章,读了一下,发现还是翻译出来,供大家参考吧。
本文是其中一篇,原文题目《Understanding the DCOM Wire Protocol by Analyzing Network Data Packets》,作者:Guy Eddon 、HenryEddon,发表于1998年3月的MicrosoftSystems Journal原文较长,分两次登出。
我们从底层来讨论COM,通过分析网络上公开传输的数据包,你能了解COM的远程工作机制,这有助于你开发出更好的组件。【本文假设读者熟悉COM。】
大多数关于COM的文章都是从编程架构来描述的,它们告诉你为完成某个功能,而如何调用COM。在COM应用工作时,通过分析网络中物理传输的数据包,你能了解COM的远程工作机制,这有助于更好的理解COM编程模型,因而设计和开发更好的组件。
COM是构建交互组件的标准,DCOM是允许COM组件通过网络交互的一个高层次网络协议。我们认为DCOM是一个高层次网络协议,是因为它建立在几个已存在的协议基础之上。例如,假设一台计算机有以太网卡,并使用UDP协议,从最底层的以太网帧到最高层的DCOM,整个协议如图1所示,中间加着IP,UDP和RPC。图1只是许多可能配置中的一种,在RPC之下,可以有多种替代的协议。在服务器与客户机上,DCOM自动选择它下面的最好的协议。
图1 协议层次
以OSI七层网络模型来看看DCOM协议栈。如图2所示,OSI七层网络模型与本文的例子协议栈并列画出,注意图中是在Window平台下,其它平台实现的层次可能不同。
图2 OSI七层“蛋糕”
对协议栈中每层协议,数据在传输时,都包含一个数据头,而后是实际的数据,紧临的更上层协议将把它视为数据的一部分。例如,IP层包含一个数据头和数据体,IP数据体实际上包含UDP层数据头和该层的数据体,因此,通过网络传输的数据都包含协议栈中的每层协议的数据头和数据体(如图3)。
从图3中可以看出,DCOM不是一个独立于RPC之上的协议,它使用了RPC的结构体,与RPC共用了数据头和数据体,因此,为了表明在网络层次上DCOM与RPC的密切关系,DCOM协议经常被成为对象RPC或ORPC。ORPC高度综合了OSF DEC RPC协议的功能,例如,RPC中的身份认证,授权,信息完整性,加密等特性,在ORPC都有体现。
图3 协议栈
ORPC在两个方面扩展了标准的RPC:怎样调用远程对象的方法和如何表达、传输和维护对象的引用。
Spying on the Network Protocol 监视网络协议
对COM编程者来说,网络协议的每一层几乎都被隐藏,最有效的方法是在DCOM客户端和组件之间监视网络传输,这时,需要一类叫网络嗅探器的特殊的软件(或硬件),有许多第三方的软件能够监视网络通讯,有一个叫“网络监视器(Network Monitor)”,如图4所示,它是微软Windows NT和SMS产品中的一个工具,SMS中所带的是全功能的,Windows NT中所带的只支持有限的协议并且只能查看服务器端的通讯。
图4 网络监视器
在“网络监视器”的Capture菜单中,选择Start,打开它的捕捉功能,接着运行DCOM测试程序,产生网络通讯,然后返回“网络监视器”,在Capture菜单中选择Stop and View,停止捕捉功能,并显示已经捕捉到的数据包。“网络监视器”是一个能理解许多网络协议的相当聪明的程序,它不仅仅能显示原始的数据包,而且能以一种智能和描述的方式显示捕捉的数据。与DCOM有关的协议中,“网络监视器”能识别和理解以太网、IP、UDP和RPC协议,在Display菜单中选择Filter,可以仅显示特定协议的数据包。
目前,“网络监视器”不支持DCOM协议,所以你只能以RPC的眼光看DCOM,这不会成为一个永远的问题,因为“网络监视器”提供了一个文档化的公开的接口,可以创建理解特定协议的DLL,开发能理解DCOM协议的DLL,留给读者作为练习。
为了分析DCOM协议,我们运行“网络监视器”来捕捉一个叫InsideDCOM的COM类产生的包,客户端运行在一台叫Thing1计算机上,它激活运行在Thing2计算机上的InsideDCOM,调用CoInitialize后, 客户端调用CoCreateInstanceEx实例化远方的类,代码如下:
CoInitialize(NULL);
COSERVERINFOServerInfo = { 0, L"Thing2", 0, 0 };
MULTI_QI qi = {&IID_IUnknown, NULL, 0 };
CoCreateInstanceEx(CLSID_InsideDCOM, NULL,
CLSCTX_REMOTE_SERVER,
&ServerInfo, 1, &qi);
调用CoCreateInstanceEx产生的网络通讯如图5所示。客户端车程序运行在Thing1上,它的IP为199.34.58.3,远程组件运行在Thing2上,它的IP为199.34.58.4。
图5 IUnknown请求包
图5中,可以很容易地看到数据包中的各层协议。在RPC数据头,你能看到接口IID是B8 4A 9F 4D 1C 7D CF 11 86 1E 00 20 AF6E 7C 57。与在“封送GUID解释”一节中一致,实际的IID为4D9F4AB8-7D1C-11CF-861E-0020AF6E7C57, 即IRemoteActivation接口。
远程激活(Activation)
IRemoteActivation是一个由Service Control Manager (SCM)暴露出来的RPC接口(不是COM接口),不要被SCM迷惑,它管理WindonwsNT服务,运行在每台计算机上,进程名称为RPCSS.EXE。IRemoteActivation只有一个方法RemoteActivation,它被设计用来激活远程计算机上的COM对象。这是一个非常强大的功能,但在纯RPC中没有提供,纯RPC中,服务器必须在客户端连接到来之间启动。Windows95和98缺少必要的安全机制支持远程启动服务器进程,但是这些平台上也能用IRemoteActivation接口和远程激活。IRemoteActivation接口的IDL定义如下所示。
[ // no objecthere. Not a COM interface! uuid(4d9f4ab8-7d1c-11cf-861e-0020af6e7c57), pointer_default(unique) ] interface IRemoteActivation { const unsignedlong MODE_GET_CLASS_OBJECT = 0xffffffff; HRESULTRemoteActivation( [in] handle_t hRpc, [in]ORPCTHIS *ORPCthis, [out]ORPCTHAT *ORPCthat, [in] constGUID *Clsid, [in, string,unique] WCHAR *pwszObjectName, [in, unique]MInterfacePointer *pObjectStorage, [in]DWORD ClientImpLevel, [in]DWORD Mode, [in]DWORD Interfaces, [in, unique,size_is(Interfaces)] IID *pIIDs, [in] unsignedshort cRequestedProtseqs, [in,size_is(cRequestedProtseqs)] unsignedshort RequestedProtseqs[], [out]OXID *pOxid, [out]DUALSTRINGARRAY **ppdsaOxidBindings, [out]IPID *pipidRemUnknown, [out]DWORD *pAuthnHint, [out]COMVERSION *pServerVersion, [out]HRESULT *phr, [out,size_is(Interfaces)] MInterfacePointer **ppInterfaceData, [out,size_is(Interfaces)] HRESULT *pResults ); }通过IRemoteActivation接口,一台机器上的SCM与另一台机器上的SCM联络,要求它激活一个对象,即客户机上的SCM调用服务器上SCM的IRemoteActivation::RemoteActivation,要求它激活以CLSID(方法第四个参数)为标识的对象。RemoteActivation返回一个激活对象的封送接口指针和两个特殊的值:接口指针标识(IPID)和对象对外联络标识(OXID)。IPID标识了一个进程中一个对象的一个特定的实例。OXID是一个RPC字符串,绑定了与IPID标识的接口进行连接所必要的信息。我们将在后面详细讨论它们。
每种支持的网络协议,都有一个周知的SCM端口,每个端口都标识了一个基于网络协议的虚拟通讯通道。例如,当使用TCP或UDP时,这个端口是1066,当使用命名管道时,管道名称为\\pipe\mypipe,常用协议下SCM所使用的端口如图7所示。
Protocol String |
Description |
Endpoint |
Ncadg_ip_udp |
Connectionless over UDP |
135 |
Ncacn_ip_tcp |
Connection-oriented over TCP |
135 |
Ncacn_nb_tcp |
Connection-oriented using NetBIOS over TCP |
135 |
Ncacn_http |
Connection-oriented over HTTP |
80 |
图7 SCM 终端口
封送形式的GUID解释
通过网络传输的GUID应该根据IDL的定义进行解释。
typedef struct _GUID
{ DWORD Data1;
WORD Data2;
WORD Data3;
BYTE Data4[8];
} GUID;
由于GUID以低字节序被封送,所以再造GUID有两个步骤。第一步,重新分组在被捕捉的数据包中发现的GUID,使它看起来象一个标准的GUID,例如,一个数据包中,你定位到GUID:78 56 34 12 34 12 34 12 12 34 12 34 5678 9A BC,在第一步中,这个GUID被按标准的形式分组,如图8所示,这样就好看多了,是不是?现在低字节序的串要被整理为实际的GUID。GUID的前3部分(图8中的data1,data2,data3)需要按字节一个一个反转。GUID的最后一部分(data4)不需要修改,因为它是按简单的字符数组存储的。反转了GUID前3个部分后,经过第二步的处理,完整的GUID就是:12345678-1234-1234-1234-123456789ABC。
图8 标准形式的GUID
调用远程对象
对远程对象的方法调用,就是一个标准的DCE RPC调用:一个标准的请求协议数据单元(PDU)通过网络被发送,要求执行一个特定的方法。一个PDU是双方机器通讯的基本单位。请求PDU包含了要执行方法的所有输入参数([in]参数),但方法执行完后,应答客户端的PDU包含了所有输出参数([out]参数)。这看起很浅显,但实际上还是令人惊奇。一个远程的COM方法调用需要两个数据包:一个是客户端发给服务器端包含[in]参数的数据包,另一个是服务器端发给客户端包含[out]参数的数据包。19种定义的PDU类型如图9所示,注意其中某些类型是特定于面向有连接或无连接协议的。
PDU Type |
Protocol |
Type Value |
Request |
CO/CL |
0 |
Ping |
CL |
1 |
Response |
CO/CL |
2 |
Fault |
CO/CL |
3 |
Working |
CL |
4 |
Nocall |
CL |
5 |
Reject |
CL |
6 |
Ack |
CL |
7 |
Cl_cancel |
CL |
8 |
Fack |
CL |
9 |
Cancel_ack |
CL |
10 |
Bind |
CO |
11 |
Bind_ack |
CO |
12 |
Bind_nak |
CO |
13 |
Alter_context |
CO |
14 |
Alter_context_resp |
CO |
15 |
Shutdown |
CO |
17 |
Co_cancel |
CO |
18 |
Orphaned |
CO |
19 |
图9 PDU类型
有连接的协议,如TCP,在客户端和服务器端维护一个连接,保证信息送到的顺序与发送的顺序相同,无连接的协议,如UDP,不在客户端和服务器端维护连接,不能保证客户端的信息实际送达服务器端,而且,即使送达,信息包也可能与发送时的顺序不同。缺省情况下,DCOM在Windows NT之间采用无连接的UDP,但这并不能说DCOM不可靠,采用无连接协议时,RPC利用自身的机制保证信息包顺序和到达感知。
一个RPC PDU包括3个部分,其中只需要第一部分:
· 一个PDU头,其中包含协议控制信息。
· 一个PDU体,其中包含数据。例如请求或应答PDU分别包含了操作的输入和输出参数。这个信息以Network DataRepresentation (NDR)形式存储。
· 一个身份认证检查体,其中包含了认证协议的特定数据。例如,认证协议可以包含一个加密的校验和来保证数据包的完整性。
无连接协议的PDU头部的IDL结构定义如图10所示。包类型字段(ptype)标识了PDU的类型,它的值通常是图9中定义的19个之一。ORPC用类标识符字段(objec)保存IPID。接口标识符字段(if_id)必须是COM接口的IID。这似乎有点冗余,因为object字段的IPID已经标识了这个接口,但是,把IID放在if_id字段可以使DCOM在标准的OSF DCE RPC实现上也能成功工作。在Windows平台,RPC实现已被优化,方法调用可以仅依赖于IPID的内容,而忽略IID。最后,接口版本字段(if_vers)必须是0.0,这是因为COM接口在发布之后可能永远不会修改,COM接口不支持版本化,如果修改,应定义一个新接口。所有这些字段都可以在图5中的RPC头中找到。
typedef struct { unsigned small rpc_vers = 4; // RPC protocolmajor version unsigned small ptype; // packet type unsigned small flags1; // packet flags unsigned small flags2; // packet flags byte drep[3]; // data representationformat label unsigned small serial_hi; // high byte ofserial number GUID object; // object identifier(Contains the IPID) GUID if_id; // interface identifier (IID) GUID act_id; // activity identifier unsigned long server_boot; // server boottime unsigned long if_vers; // interfaceversion unsigned long seqnum; // sequence number unsigned short opnum; // operation number unsigned short ihint; // interface hint unsigned short ahint; // activity hint unsigned short len; // length of packeybody unsigned short fragnum; // fragment number unsigned small auth_proto; //authentication protocol id unsigned small serial_lo; // low byte ofserial number } dc_rpc_cl_pkt_hdr_t;
这样那样
所有通过网络的COM方法调用,PDU请求中包含的第一个参数比较特别,它在所有参数之前,叫ORPCTHIS。如果下面所示的COM方法:HRESULT Sum(int x, int y, [out, retval] int* result)
被调用,实际PDU请求的参数为: Sum(ORPCTHISorpcthis, int x, int y)。
ORPCTHIS结构的定义如下:
// Implicit ‘this'pointer which is the first [in]
// parameter onevery ORPC call.
typedef structtagORPCTHIS {
COMVERSIONversion; // COM version number (5.2)
unsigned longflags; // ORPCF flags for presence of
// other data
unsigned longreserved1; // set to zero
CID cid; // causality id of caller
ORPC_EXTENT_ARRAY* extensions; // [unique] extensions
} ORPCTHIS;
ORPCTHIS结构第一个参数指定了这个方法调用所采用的DCOM协议版本号,Windows95 1.0版和WindowsNT4.0补丁3之前,COM版本是5.1;WindowsNT 4.0补丁3,COM版本是5.2;Windows95 1.1中的DCOM和WindowsNT 4.0补丁3之后,COM的版本是5.3。由于每次远程调用都包含有ORPCTHIS结构体,所以DCOM的版本也就传递到服务器上。在服务器上,客户端的DCOM版本与服务器端的进行比较,如果二者的主版本号不匹配,错误RPC_E_VERSION_MISMATCH会传给客户端,但允许服务器上的次版本号高于客户端,这时,服务器必须将DCOM协议的应用限制到客户端版本的允许的范围。
因果ID(causality identifier ,CID)是一个GUID,它将那些多次相关的调用联系起来。例如,如果机器A上的客户端A调用机器B上的组件B,而组件B在返回给A之前,调用机器C上的组件C,这些调用被称为有因果关系。产生一个新调用(不是处理一个进来的调用)时,根据DCOM协议,就会产生一个新CID。如果是后续调用,同一个CID会被传播,即组件B会代表客户端A使用同样的CID,即使组件B采用连接点或其他机制回调客户端A也采用同样的CID。ORPCTHIS的扩展域字段允许COM调用附加额外的数据。
目前,只有定义了两个扩展:一个是用于错误信息(IErrorInfo),另一个用于ORPC调试。对ORPCTHIS的定制扩展,可以采用一个叫通道钩(channel hooking)的没有被文档收录的技术。关于通道钩的更多的信息,请参阅January1998 installment of Don Box'sActiveX®/COM column。
在每个COM方法的应答PDU中,有一个特别的外传参数(ORPCTHAT),它被插在所有外传参数之前,因此,如果一个如下的COM方法: HRESULT Sum(int x, int y, [out, retval] int* result),它的应答PDU将是 HRESULT Sum(ORPCTHAT orpcthat, intresult)。
ORPCTHAT结构的定义如下:
// Implicit ‘that'pointer which is the first [out]
// parameter onevery ORPC call.
typedef structtagORPCTHAT {
unsignedlong flags; // ORPCF flags for presence
// of other data
ORPC_EXTENT_ARRAY *extensions; // [unique] extensions
} ORPCTHAT;
喵!
在DCOM网络协议中,方法参数的传送按照OSF DEC RPC所规定的网络数据描述(Network Data Representation ,NDR)格式。NDR精确地规定了所有能被IDL理解的原生数据类型是如何被封装到数据包的,DCOM对NDR的仅有扩展是对封送接口指针的支持。在接口定义中,iid是一个IDL的关键字,它可以被认为是一个新的能被封送的原生数据类型:接口指针。使用“接口指针”这个词有点问题,因为它使人在精神上想起指向vtable结构(该结构包含若干指向方法的指针)的指针,但是一旦它被封送到数据包中,情况更本不是那样,它仅是一个获取某个对象的符号表示,因而它仅仅是一个对象参考而已。被封送的接口指针的格式由MInterfacePointer结构决定:
// Wirerepresentation of a marshaled interface
// pointer, alwaysthe little-endian form of an OBJREF
typedef structtagMInterfacePointer {
ULONG ulCntData; // size of data
byte abData[]; // [size_is(ulCntData)]
// data
}MInterfacePointer, *PMInterfacePointer;
跟在ulCntData之后的byte数组包含了实际的对象引用,它是一个叫OBJREF的结构(定义见图11)。OBJREF是一个用来表示对象引用的数据结构,根据采用的封送类型,OBJREF有三种形式:标准、指针或自定(standard, handler, custom)。
// Although thisstructure is conformant, it is always // marshaled inlittle-endian byte-order. typedef structtagOBJREF { unsignedlong signature; // Always MEOW unsignedlong flags; // OBJREF flags GUID iid; // interface identifier union { // [switch_is(flags), switch_type(unsignedlong)] struct{ // [case(OBJREF_STANDARD)] STDOBJREF std; // standard objref DUALSTRINGARRAY saResAddr; // resolver address }u_standard; struct{ // [case(OBJREF_HANDLER)] STDOBJREF std; // standard objref CLSID clsid; // Clsid of handler code DUALSTRINGARRAY saResAddr; // resolver address } u_handler; struct{ // [case(OBJREF_CUSTOM)] CLSID clsid; // Clsid of unmarshaling code unsignedlong cbExtension; // size of extension data unsigned long size; // size of data thatfollows byte *pData; // extension + class specific data // [size_is(size),ref] } u_custom; } u_objref; } OBJREF;
图11 OBJREF 结构
OBJREF的起始字段是一个无符号long,它是一个签名字段,内容是0x574F454D(十六进制),有意思的是,如果你按照低字节序重新组织一下(4D 45 4F 57),并转换成相应的ASCII码,结果是MEOW。有人推测这是Microsoft Extended Object Wirerepresentation的首字母缩写,但没人敢肯定。对MEOW结构最好的事情是,被网络监视器捕捉到大量的数据包中,我们可以很容易地找到一个对象的引用:就是找到MEOW。要注意的是,不管NDR其他部分的格式如何,一个封送的接口指针的网络格式总是小字节序的。虽然对象的引用可以有多种存储格式,不太可能,也不太希望传送一个表示字节序的标志(因而会增加数据包大小),因此COM的对象引用都是以小字节序传输。
OBJREF结构中,跟在MEOW签名字段后的是标志(flags)字段,它表示了对象引用的形式,可以被设置成OBJREF_STANDARD (1), OBJREF_HANDLER (2), or OBJREF_CUSTOM (4)。OBJREF结构最后的字段是IID,它表示了被封送的接口标识。图12是被捕捉到的,对图5请求PDU的应答数据包。图5中,调用CoCreateInstance方法请求一个对象的IUnknown接口,图12中,你能看到应答的PDU,它包含了封送的IUnknown接口指针(由“MEOW”标识)。
图12 IUnknown 应答包
The Standard Object Reference标准对象引用
上面OBJREF结构中标志(flags)字段(OBJREF_STANDARD,值为1)表示采用标准的封送方式。基于该值,OBJREF结构剩余的字段包含一个STDOBJREF字段和一个DUALSTRINGARRAY字段。STDOBJREF机构如下:
typedef structtagSTDOBJREF {
unsignedlong flags; // SORF_ flags
unsignedlong cPublicRefs; // count of references
// passed
OXID oxid; // oxid of server with thisoid
OID oid; // oid of object with this ipid
IPID ipid; // ipid of Interface
} STDOBJREF;
STDOBJREF结构的第一个字段是关于对象引用的标识(flags)字段,尽管这个标识字段的大部分值都被系统保留使用,但SORF_NOPING (0x1000)这个值可以用来表明对象不需要被ping(DCOM协议采用ping方式实现复杂的垃圾回收机制,以后将涉及)。STDOBJREF结构的第二个参数cPublicRefs规定了要传输的IPID的引用次数,设置对一个接口的引用次数,避免了客户端在每次调用远程方法时都需要调用IUnknown::AddRef。
STDOBJREF结构的第三个参数规定了拥有对象的服务器的OXID。一个IPID标识了一个进程中的一个特定对象的一个特定接口,但仅仅一个IPID不能包含一次方法调用的足够信息。DCOM和RPC都采用字符串规定远程方法调用的绑定信息。RPC绑定字符串包含了诸如调用采用的网络协议,组件运行的服务器地址等信息。当DCOM准备连接一个特定的OXID时,安全绑定字符串被用来判断哪些参数要传递给RPC基础架构。OXID是一个无符号的混合变量(64位),代表了这个连接信息。调用远程方法之前,客户端将一个OXID转换成一个RPC能理解的绑定字符串。这个转换将在下面讲述。
STDOBJREF结构的第四个参数是实现封送接口对象的对象标识(OID)。OID是64位值,会被用作ping机制的一部分。STDOBJREF结构的最后字段是封送接口的真正的标识(IPID)。