通过分析网络数据包来理解DCOM协议(一)【翻译说明】最近,想看看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定义如下所示。
[cpp]
view plaincopy[ // 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头中找到。
[cpp]
view plaincopytypedef 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;
图10 PDU数据头
这样那样
所有通过网络的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)。
[cpp]
view plaincopy// 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)。
----------------------------------------------------------------------------------------------------------------------------
通过分析网络数据包来理解DCOM协议(二)DUALSTRINGARRAY结构
作为对象引用的一部分,跟在STDOBJREF后面的是DUALSTRINGARRAY结构,这个结构是一个大数组,由STRINGBINDING和SECURITYBINDING两部分组成。
[cpp]
view plaincopy//DUALSTRINGARRAYS are the return type for arrays of network addresses, arrays
// of endpointsand arrays of both used in many ORPC interfaces.
typedef structtagDUALSTRINGARRAY {
// # of entriesin array
unsignedshort wNumEntries;
// Offset ofsecurity info
unsignedshort wSecurityOffset;
// The arraycontains two parts, a set of STRINGBINDINGs
// and a set of SECURITYBINDINGs.Each set is terminated by an extra zero. The
// shortest arraycontains four zeros.
//[size_is(wNumEntries)]
unsigned shortaStringArray[];
} DUALSTRINGARRAY;
这个结构的前两个字段规定了后面数组的大小(字段wNumEntries)和若干STRINGBINDING结束偏移量或叫若干SECURITYBINDING开始偏移量(字段wSecurityOffset)。后续数组本身保存在aStringArray字段。
STRINGBINDING结构表示了绑定一个对象的连接信息。
STRINGBINDING结构的第一元素是wTowerId,它规定了用来联系服务器的网络协议,服务器由第二个元素(aNetworkAddr)指定。图13通常协议的“发射塔”编号(tower identifier),其中的NCA前缀表示"Network ComputingArchitecture.","CN" 表示面向连接协议而“DG”表示无连接的基于报文的协议。
Tower Identifier
Value
Description
NCADG_IP_UDP
0x08
Connectionless User
Datagram Protocol
NCACN_IP_TCP
0x07
Connection-oriented
Transmission Control Protocol
NCADG_IPX
0x0E
Connectionless Internetwork
Exchange Protocol
NCACN_SPX
0x0C
Connection-oriented
Sequenced Exchange Protocol
NCACN_NB_NB
0x12
Connection-oriented NetBIOS
NCACN_NB_IPX
0x0D
Connection-oriented NetBIOS
over IPX
NCACN_HTTP
0x1F
Connection-oriented over HTTP
图 13 Tower Identifiers
// This is thereturn type for arrays of string
// bindings orprotseqs used by many ORPC interfaces.
typedef structtagSTRINGBINDING {
unsignedshort wTowerId; // Cannot be zero.
unsignedshort aNetworkAddr; // Zero terminated.
} STRINGBINDING;
STRINGBINDING结构的第二个元素aNetworkAddr是一个Unicode字符串,指定了服务器的网络地址,例如,如果wTowerId是NCADG_IP_UDP,则一个有效的网络地址可以是199.34.58.4。每个STRINGBINDING 结构以字符null结束表明aNetworkAddr已完结。DUALSTRINGARRAY结构中最后一个STRINGBINDING结构以两个null字符表示,这之后就是若干个SECURITYBINDING结构了。SECURITYBINDING结构包含了认证服务(wAuthnSvc)与授权服务(wAuthzSvc)字段。wAuthzSvc一般会设为0xFFFF,表明采用缺省的授权方式。
// This value indicates to use default authorization constunsigned short COM_C_AUTHZ_NONE = 0xffff;
typedef structtagSECURITYBINDING {
unsigned short wAuthnSvc; // Must not be zero
unsignedshort wAuthzSvc; // Must not be zero
unsignedshort aPrincName; // NULL terminated
} SECURITYBINDING;
从总体上看,这些信息代表了向客户端封送一个接口指针所有的信息。在客户端空间,会产生一个代理与服务器端的桩进行通讯
IRemUnknown接口
IRemUnknown是一个COM接口,被设计用来处理远程对象的引用计数和接口查询。顾名思义,IRemUnknown是IUnknown的远程版,客户端用IRemUnknown接口操作引用计数或请求一个基于IPID的新接口,为和标准的COM计数规则一致,计数是基于每接口方式而不是每对象方式。IRemUnknown接口的定义如图14所示。
[cpp]
view plaincopy// The remote version of IUnknown. It is used by clientsto
// query for newinterfaces, get additional references (for
// marshaling),and release outstanding references.
[
object,
uuid(00000131-0000-0000-C000-000000000046)
]
interfaceIRemUnknown : IUnknown
{
HRESULTRemQueryInterface
(
[in]REFIPID ripid, // interface to QIon
[in]unsigned long cRefs, // count of AddRefs requested
[in]unsigned short cIids, // count of IIDs that follow
[in,size_is(cIids)] IID* iids, // IIDs to QI for
[out,size_is(,cIids)]
REMQIRESULT** ppQIResults // results returned
);
HRESULTRemAddRef
(
[in]unsigned short cInterfaceRefs,
[in,size_is(cInterfaceRefs)]
REMINTERFACEREF InterfaceRefs[],
[out,size_is(cInterfaceRefs)]
HRESULT* pResults
);
HRESULTRemRelease
(
[in]unsigned short cInterfaceRefs,
[in,size_is(cInterfaceRefs)]
REMINTERFACEREF InterfaceRefs[]
);
}
图14 IRemUnknown接口的IDL
你永远不需要实现IRemUnknown接口,因为与OXID相关联的每个COM环境(COM apartment)已经提供了这个接口的实现。COM中的IUnknown接口从不会远程化。在这里,IRemUnknown是远程化的,导致本地的QueryInterface, AddRef被调用,以及服务器上的Release被调用。IRemUnknown::RemQueryInterface与IUnknown::QueryInterface的不同在于前者能在一次调用中请求几个接口指针。标准的IUnknown::QueryInterface方法实际上被用来在服务器上这么做。这种优化方法是为了减少来回传输的次数。
由RemQueryInterface 返回的REMQIRESULT结构数组包含了对每个请求接口运行QueryInterface的HRESULT,也包含STDOBJREF,其中有封送的接口指针本身。
typedef struct tagREMQIRESULT
{
HRESULT hResult; // result of call
STDOBJREF std; // data for returned
// interface
} REMQIRESULT;
IRemUnknown::RemAddRef和RemRelease增加和减少由IPID标识的对象的引用计数。象RemQueryInterface一样,RemAddRef和RemRelease与它们的本地版本(AddRef,Release)的区别在于前者在一次远程调用中可以增加或减少在一个上下文(apartment)中多个对象的多个接口的引用计数,数值也可以多于1。设想一个场景:一个收到了封送的接口指针的对象,想把指针传递给其他对象,根据COM引用计数规则,在传递给其他对象之前,必须调用AddRef,这将导致两个来回的通讯:一个是得到接口指针,一个是增加引用计数。调用者在一次调用过程中请求多个引用就可以优化这种场景。因而,接口指针可以“被给出去”多次而无需多次的远程调用来增加引用计数。
封送一个接口指针时,DCOM的Windows上实现方式通常要求5个引用计数,这就是说接收客户端进程可以把接口指针封送给同一个进程中其他的4个上下文(apartment)或其他4个进程。只有当客户端想把接口指针第5次封送时,才需要一次远程调用增加一个引用计数。从性能方面考虑,客户端的AddRef和Release调用,并不直接转换成RemAddRef和RemRelease,直到本地所有的对象接口指针都被释放了,对远程的RemRelease方法调用才进行,那时,单次的调用将所有接口的引用计数减少到必需数量。在一个接口指针取消封送(unmarshal)和再封送(remarshal,上面提到的)时,会调用RemAddRef。
在上面的场景中,需要特别注意的是,客户端进程的一个组件返回了另一组件的接口指针,COM服务器从不允许一个代理与另一个代理通讯。例如,如果客户端进程A调用对象B,B返回了一个对象C的接口指针,后续的任何B对C的调用都是直接的,因为封送的接口指针的信息中包含了真正的对象实例所在的机器信息。对象B要调用对象C,对象B必需跟踪对象C的OXID, IP地址, IPID等。当对象B把对象C的指针传给A,它把所有的那些信息放到一个新的OBJREF给A,对象B在关系链条中就不需要了,从而节省了带宽和提高的总体的性能和可靠性。如果A对C调用经过B,当承载B的机器关掉后,调用就不能进行了。
发出CoCreateInstanceEx实例化远程组件后,你的客户端进程拥有了一个初始的IUnknown接口指针,通常,接下来要调用IUnknown::QueryInterface来获取另一个接口,如下面的代码片段:
hr=pUnknown->QueryInterface(IID_Isum,(void**)&pSum);
实际上,客户端经常获取所有需要的接口指针,供CoCreateInstanceEx使用。当客户端进程调用IUnknown::QueryInterface来获取ISum接口指针时,客户端地址空间的代理会调用服务器端的IRemUnknown::RemQueryInterface,图15是RemQueryInterface方法调用的网络传输包。你能清楚的看出DCOM请求了5个ISum接口指针的引用。
在服务器端,IUnknown::QueryInterface被调用,从组件中来获取一个ISum接口指针,这个接口指针以封送形式在STDOBJREF结构中返回给客户端。图16是给客户端应答的PDU。
图15 IRemUnknown请求包
图16 IRemUnknown应答包
为防止恶意程序调用IRemUnknown::RemRelease以及释放了其他应用程序还在使用对象,客户端可以申请私有引用。私有引用与客户端的身份存储在一起,一个客户端很难释放另一个客户端的私有引用。要注意的是,私有引用不能传递,每个客户端必须通过显式调用RemAddRef和RemRelease来申请或释放自己的私有引用。RemAddRef和RemRelease方法都接受一个REMINTERFACEREF结构的数组参数,REMINTERFACEREF结构指定了一个IPID以及公共和私有的引用数。从程序上来讲,客户端通过调用CoInitializeSecurity方法和设置EOAC_SECURE_REFS来指定它需要的私有引用。
typedef struct tagREMINTERFACEREF
{
IPID ipid; // ipid to AddRef/Release
unsignedlong cPublicRefs;
unsignedlong cPrivateRefs;
}REMINTERFACEREF;
IRemUnknown2接口
IRemUnknown2接口是DCOM5.2版引入的接口,它由IRemUnknown接口派生,IRemUnknown2增加了RemoteQueryInterface2方法,使客户端能够获得比STDOBJREF提供的更多一些的东西,如同RemQueryInterface一样,使用IPID,这个方法可以查询其他的一个或多个接口,但它不是返回一个STDOBJREF封送数据包,而是以二进制块形式返回任意的封送数据包(包括一个传统的STDOBJREF)。IRemUnknown2的IDL如下:
interface IRemUnknown2 : IRemUnknown
{
HRESULTRemQueryInterface2
(
[in]REFIPID ripid,
[in]unsigned short cIids,
[in, size_is(cIids)]IID *iids,
[out,size_is(cIids)] HRESULT *phr,
[out,size_is(cIids)] MInterfacePointer **ppMIF
);
}
OXID解析器
OXID解析器,象SCM一样,是RPCSS.exe的一部分。OXID解析器保存和为本地客户端提供与远程对象连接所需要的RPC绑定字符串,它也为拥有远程对象的本地对象发送和接收ping信号,在这一方面,OXID解析器支持DCOM的垃圾回收机制。
就像CoCreateInstanceEx实现CoGetClassObject和IClassFactory:: CreateInstance两者功能一样,IRemoteActivation接口也实现IRemUnknown 和IOXIDResolver两者的功能,所以一次通讯来回就够了。OXID解析器服务运行在同SCM一样的端口,OXID解析器服务同IRemoteActivation接口一样实现了一个叫IOXIDResolver 的RPC接口(不是一个COM接口),这个接口的IDL如图17。在图17中,接口头部没有object关键词,明显表明这不是一个COM接口。
[cpp]
view plaincopy[ // no object here. Not a COM interface!
uuid(99fcfec4-5260-101b-bbcb-00aa0021347a),
pointer_default(unique)
]
interfaceIOXIDResolver
{
// Method toget the protocol sequences, string bindings
// and machineid for an object server given its OXID.
[idempotent]error_status_t ResolveOxid
(
[in] handle_t hRpc,
[in] OXID *pOxid,
[in] unsigned short cRequestedProtseqs,
[in, ref, size_is(cRequestedProtseqs)]
unsigned short arRequestedProtseqs[],
[out, ref]DUALSTRINGARRAY **ppdsaOxidBindings,
[out, ref]IPID *pipidRemUnknown,
[out, ref]DWORD *pAuthnHint
);
// Simple pingis used to ping a Set. Client machines use
// this toinform the object exporter that it is still
// using themembers of the set. Returns S_TRUE if the
// SetId isknown by the object exporter, S_FALSE if not.
[idempotent] error_status_t SimplePing
(
[in] handle_t hRpc,
[in] SETID *pSetId // Must not be zero
);
// Complexping is used to create sets of OIDs to ping. The
// whole setcan subsequently be pinged using SimplePing,
// thusreducing network traffic.
[idempotent]error_status_t ComplexPing
(
[in] handle_t hRpc,
[in, out] SETID *pSetId, // In of 0 on first
//call for new set.
[in] unsigned short SequenceNum,
[in] unsigned short cAddToSet,
[in] unsigned short cDelFromSet,
[in, unique,size_is(cAddToSet)] OID AddToSet[],
// add these OIDs to the set
[in, unique, size_is(cDelFromSet)]OID DelFromSet[],
// remove these OIDs from the set
[out] unsigned short *pPingBackoffFactor
// 2^factor = multipler
);
// In somecases the client may be unsure that a particular
// bindingwill reach the server. For example, when the
// oxidbindings have more then one TCP/IP binding. This
// call can beused to validate the binding from the
// client.
[idempotent]error_status_t ServerAlive
(
[in] handle_t hRpc
);
}
图17 IOXIDResolverIDL
提交对象对外标识时,获取相关的RPC绑定字符串来与远程对象连接,是OXID解析器的任务。在每台机器上,OXID解析器维护一个OXID与RPC绑定字符串对应的缓存表。当一个客户端要求查询与OXID相对应的绑定字符串时,OXID解析器先查询本地的缓存表,如果找到,立即返回,否则OXID解析器与服务器上的OXID解析器联络,要求解析这个OXID,接着,客户端的OXID解析器会缓存服务器提供的这个绑定字符串。这样一个过程使得OXID解析器能迅速返回以后本地的客户端可能要求解析的信息。假如客户端将对象引用传给第三台计算机上的一个进程,那台机器上的OXID解析器没有缓存OXID绑定信息,那它就必须产生一个远程调用来得到绑定信息。
第一个方法IOXIDResolver:: ResolveOxid就是为了获得一个OXID对象,来解析OXID到相应的绑定信息。需要解析的OXID是方法ResolveOxid的第一个参数pOxid,当调用这个方法时,客户端按最适合到最不适合顺序指定协议序列(Tower IDs),它在arRequestedProtseqs数组参数中传递,服务器端的OXID解析器尝试解析这个OXID,然后返回一个DUALSTRINGARRAYS数组ppdsaOxidBindings,它包含了字符串绑定信息(同样以最合适到最不合适顺序)。以下的步骤是OXID的解析过程,假定客户端正在从一个新的OXID中解封接口指针:
1. 如果客户端进程中的COM运行时没有见过这个OXID,客户端询问本地的OXID解析器解析这个OXID.
2. 如果客户端的OXID解析器以前没有见过这个OXID,OXID解析器调用IOXIDResolver::ResolveOxid,请求服务器的OXID解析器返回相应的绑定字符串。
3. 服务器端的OXID解析器查询本地表,返回需要的绑定字符串给客户端OXID解析器。如果服务器端的OXID解析器没有找到所要的绑定信息,它就采用需要的协议调入服务器进程,一旦这种情况发生,就会产生一个新绑定字符串,被本地缓存,然后返回给客户端OXID解析器。
4. 客户端OXID解析器在本地表中缓存最合适的绑定信息,然后返回给客户端进程。
5. 客户端用给定的字符串绑定对象。
6. 客户端就可以调用对象的方法了。
由于不同的机器可能安装不同的网络协议,给每一个网络协议序列分配一个终端是一个费时费资源的操作。通常,服务器在启动时注册所有的网络协议序列,作为一种优化运行,OXID解析器可能要推迟协议注册,为了实现滞后协议注册,服务器端的OXID解析器一直等待,直到一台客户端机器调用IOXIDResolver::ResolveOxid。这样,就不是在初始化时注册所有的协议,而是在OXID解析过程中,ResolveOxid方法注册那些客户端要求的协议。
DCOM协议5.2版中,IOXIDResolver接口中加入了ResolveOxid2方法,在OXID解析过程中,这个方法允许客户端选择服务器端的DCOM协议版本。在下面的IOXIDResolver:: ResolveOxid2定义中,注意一下附加的最后一个参数。
[idempotent]error_status_t ResolveOxid2
(
[in] handle_t hRpc,
[in] OXID *pOxid,
[in] unsigned short cRequestedProtseqs,
[in, ref, size_is(cRequestedProtseqs)]
unsigned short arRequestedProtseqs[],
[out, ref]DUALSTRINGARRAY **ppdsaOxidBindings,
[out, ref]IPID *pipidRemUnknown,
[out, ref]DWORD *pAuthnHint,
[out, ref]COMVERSION *pComVersion
);
DCOM垃圾回收
当一个分布式系统可以提供优秀的可靠性,避免严重灾难的同时,系统中发生错误的可能性也大大提高了。从客户端的角度看,服务器或网络故障可以通过远程方法调用失败分辨出来,在这种情况下,将返回一个HRESULT类型值,如RPC_S_SERVER_UNAVAILABLE或RPC_S_ CALL_FAILED。
如果客户端发生错误,服务器端的情况比较复杂。客户端发生的错误可能影响或不影响服务器端。例如,一个无状态的对象总可以保持运行状态,如果客户端要求,总能返回信息,而不管客户端进程如何。一个维护状态的对象很明显要关心客户端是否存在,这些对象通常有一个诸如叫ByeByeNow的方法,客户端在调用代理对象的Release方法之前,需要调用这个方法,但是如果客户端自身崩溃或发生网络故障,客户端就没有机会通知服务器端的对象了,这种情况使服务器端处于一种不稳定的状态,因为它还在维护着可能已不存在的客户端的信息。
RPC采用一种客户端和服务器端之间的逻辑连接来处理上述情况,这种连接叫做上下文句柄。如果双方的这个连接不知怎么断了,服务器上,一个特别的函数(叫rundown routine)被调用,通知客户端连接断了。处于性能的考虑,DCOM没有使用RPC的上下文句柄,DCOM协议自己定义了一种ping机制,来判断客户端是否存在。Ping机制是很简单的,每隔一定时间,客户端发生一个ping信息给服务器端对象,说“我还活着”。如果服务器在规定的时间间隔内没有收到ping信息,则认为客户端已不存在了,所有它的引用数将被释放。
上面描述的简化的ping算法不能满足DCOM,因为它会产生大量的网络流量。在一个分布式环境中,存在着数百、数千甚至数十万以上的的计算机,网络容量可能就被简单的ping信息所占用。为了减少网络负荷,DCOM依靠运行于每台机器上的OXID解析器服务来检测客户端是否存在,然后发送一条基于机器而不是基于对象的ping信息。
每个OID的ping信息有16个字节,即使给每台机器发送一条信息,ping信息依然增长很快。例如,如果客户端持有5000个其他机器上的对象引用,每个ping信息大致有78K!为了进一步减少网络流量,DCOM引入了一个叫“delta pinging”的特别机制。通常,服务器都有一组相对稳定的被客户端引用的对象,采用“delta pinging”时,不是将每个OID包含在ping信息中,而是仅用一个叫做“ping set”的标识,代表了这一组OID,因此,5个OID的ping信息与一百万个OID的ping信息,其大小一样。
为了建立一个ping集(ping set),客户端需要调用IOXIDResolver::ComplexPing方法,该方法中AddToSet参数接受OID数组,就定义了一个ping集。定义好后,集合中所有OID就可以简单地用IOXIDResolver::SimplePing方法ping即可,给SimplePing方法传递的参数SETID是由ComplexPing返回的。ComplexPing可以随时被调用,向ping集中来添加或删除OID。
Ping机制激发的垃圾回收基于两个值:两次ping之间的时间和服务器认为客户端“失踪”时丢失的ping信息个数,把两者结合起来,它们的乘积就是服务器可以允许无ping信息的最大时间,之后服务器就认为客户端已“死亡”。Ping周期的缺省值是120妙,可以丢失3个ping信息,否则就认为客户端已“死亡”。目前,用户不能配置这些缺省值,因此,客户端引用被回收的时间是6分钟(3×120s),一旦服务器端OXID解析器认为一个OID已崩溃,这个OID的桩管理器被销毁,这个对象本身也被通知没有外部引用了,如果它还有内部(in-apartment)引用,它可以继续存在。通常,在这个时候,对象销毁自己,因而也回收了分配给客户端的资源。
[cpp]
view plaincopyIMarshal* pMarshal= NULL;
HRESULTCFactory::CreateInstance(IUnknown *pUnknownOuter, REFIID riid, void** ppv)
{
if(pUnknownOuter != NULL)
returnCLASS_E_NOAGGREGATION;
CObject*pObject = new CObject;
if(pObject ==NULL)
returnE_OUTOFMEMORY;
IUnknown*pUnknown;
pObject->QueryInterface(IID_IUnknown, (void**)&pUnknown);
CoGetStandardMarshal(riid, pUnknown, 0, NULL,
MSHLFLAGS_NOPING|MSHLFLAGS_NORMAL, &pMarshal);
pUnknown->Release();
//QueryInterface probably for IID_IUNKNOWN
HRESULT hr =pObject->QueryInterface(riid, ppv);
pObject->Release();
return hr;
}
图18 MSHLFLAGS_NOPING的使用
一些无状态的对象,例如像上面讨论的时间服务对象,就没有必要采用DCOM的垃圾回收机制。这些对象通常永久运行,不必关心一个方法调用完成后客户端的状况。对这类对象,通过给CoGetStandardMarshal方法传递MSHLFLAGS_ NOPING标志来关掉ping。图18给出了在实现IClassFactory::CreateInstance方法中使用MSHLFLAGS_NOPING的例子。
在退出之前,对象调用下面的方法释放标准的封送器。
pMarshal->DisconnectObject(0);
pMarshal->Release();
那些设置了MSHLFLAGS_NOPING标志的对象不会收到对它们IUnknown:: Release的调用。客户端可以调用Release,但这种调用不会传给远处对象本身。由于delta pinging机制的高效性,关闭对一个对象的ping并不会显著减少网络通讯。服务器上确实有很多需要ping机制的对象,DCOM必须采用OXIDResolver:: SimplePing方法给服务器上的对象发送ping信息。所不同的是,打上MSHLFLAGS_NOPING标志的对象将不会被加入到SETID中。
远程方法调用
我们理解了ORPC网络协议后,让我们实际查看一下一个远程调用所传输的数据。图19给出了客户端调用ISum::Sum方法所发出的请求PDU。紧接着ORPCHTIS参数的是Sum方法的两个传入参数x和y,这里这两个参数的值是4和9。
图19 ISum请求包
Sum方法在服务器端被运行后,生成应答PDU并被发送回客户端(图20)。可以明显看出,跟在ORPCTHAT参数之后是方法的输出参数13(4+9)。
图20ISum应答包
一旦你理解了在DCOM协议中COM是如何封送接口指针的,查看某个方法调用是一件简单的事情。图中的网络包与大部分方法调用都是类似的,只是方法的参数不同。我们希望这篇文章能够使你感觉到隐藏在DCOM之下的东西。本文中涉及的DCOM协议大部分是不可配置的,但在WindowsNT 5.0中,新的COM函数和标准接口被引入,允许开发者直接控制这些选项。
(完)