DUALSTRINGARRAY结构
作为对象引用的一部分,跟在STDOBJREF后面的是DUALSTRINGARRAY结构,这个结构是一个大数组,由STRINGBINDING和SECURITYBINDING两部分组成。
//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 |
NCACN_IP_TCP | 0x07 | Connection-oriented |
NCADG_IPX | 0x0E | Connectionless Internetwork |
NCACN_SPX | 0x0C | Connection-oriented |
NCACN_NB_NB | 0x12 | Connection-oriented NetBIOS |
NCACN_NB_IPX | 0x0D | Connection-oriented NetBIOS |
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所示。
// 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[]
);
}
你永远不需要实现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接口。
[ // 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
);
}
提交对象对外标识时,获取相关的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)引用,它可以继续存在。通常,在这个时候,对象销毁自己,因而也回收了分配给客户端的资源。
IMarshal* 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函数和标准接口被引入,允许开发者直接控制这些选项。
(完)