March 1998
Understanding the DCOM Wire Protocol by Analyzing Network Data Packets |
We're going to examine COM from the bottom up. By analyzing the data packets physically transmitted across a network during the execution of COM-enabled applications, you can learn how COM's remoting architecture works. This will help you develop better components. |
This article assumes you're familiar with COM |
Guy and Henry Eddon are the authors of Active Visual Basic 5.0 (Microsoft Press, 1997). They are currently working on a book entitled Inside Distributed COM, to be published by Microsoft Press in March 1998. Both can be reached at guyeddon@msn.com.
|
Most articles about COM
approach this gargantuan subject from the perspective of the programming architecture. They tell you what COM function to call in order to perform a specific task. We're going to examine COM from the bottom up. By analyzing the data packets physically transmitted across a network during the execution of COM-enabled applications, you can learn how COM's remoting architecture works. This will help you better understand the COM programming model, and can help you design and develop better components.
While COM is a specification for building interoperable components, Distributed COM (DCOM) is simply a high-level network protocol designed to enable COM-based components to interoperate across a network. We consider DCOM a high-level network protocol because it is built on top of several layers of existing protocols. For example, assume that a computer has an Ethernet network interface card and is using the User Datagram Protocol (UDP). The layering of protocols ranges from the Ethernet frame at the lowest level to DCOM at the highest (see Figure 1). Sandwiched in the middle are IP, UDP, and RPC. Figure 1 shows only one of many possible configurations; many protocols could be substituted below the RPC layer. DCOM automatically chooses the best underlying network protocol based on the protocols available on the client and server machines. |
Figure 1 Protocol Layers |
|
Figure 2 OSI Seven-layer Cake |
|
Figure 3 Protocol Stack |
Spying on the Network Protocol
|
|
This call to CoCreateInstanceEx resulted in the transmission of the network packet shown in Figure 5. The client program ran on Thing1, which was configured with an IP address of 199.34.58.3, while the component ran on Thing2, which was configured with an IP address of 199.34.58.4.
The layering of protocols can be easily seen in the network packet in Figure 5. In the RPC header, you can see that the interface identifier is B8 4A 9F 4D 1C 7D CF 11 86 1E 00 20 AF 6E 7C 57. In accordance with the rules described in the section "Interpreting Marshaled GUIDs," the actual IID is 4D9F4AB8-7D1C-11CF-861E-0020AF6E7C57, otherwise known as IRemoteActivation.
Remote Activation
Interpreting Marshaled GUIDs
|
|
Since the GUIDs are marshaled in little-Endian format, the original GUID can be reconstructed via a two-step process. First, the GUID found in a captured network packet needs to be formatted to look like a standard GUID. For example, imagine that you located the GUID 78 56 34 12 34 12 34 12 12 34 12 34 56 78 9A BC in a network packet. In the first step, shown in Figure 8, the GUID is grouped into standard formation. Better already, isn't it? Now the little-Endian format needs to be taken into account to obtain the actual GUID. The first three elements of the GUID structure (Data1, Data2, and Data3 in Figure 8) need to be reversed on a byte-by-byte basis. The last element of the GUID structure (Data4) does not need to be modified since it is already stored as a simple byte array. Therefore, after reversing the first three elements of the GUID structure, the completed second step of the process provides the true GUID: 12345678-1234-1234-1234-123456789ABC.
|
Figure 8 GUID in Standard Form |
Calling All Remote Objects
Method calls made on remote COM objects are considered true DCE RPC invocations in that a standard Request Protocol Data Unit (PDU) is transmitted across the network requesting that a specific method be executed. A PDU is the basic unit of communication between two machines. The Request PDU contains all of the in parameters that the method expects to receive. When method execution is complete, a Response PDU containing the method's out parameters is transmitted back to the client. While this sounds rather obvious, it is really quite amazing. A remote COM method call requires two packets to be transmitted across the network: one from the client to the server containing the in parameters and the other from the server to the client for the out parameters. The 19 defined PDU types are shown in Figure 9. Note that some of the PDUs are specific to either a connection-oriented (CO) or connectionless (CL) protocol. A connection-oriented protocol, such as the TCP, maintains a virtual connection for the client and server between transmissions and guarantees that messages are delivered in the same order that they were sent. A connectionless protocol, such as UDP, does not maintain a connection between the client and server, and does not guarantee that a message from the client will actually be delivered to the server. Furthermore, even if the messages are delivered, they may arrive in a different order from that in which they were sent. By default, DCOM employs the connectionless UDP between Windows NT machines. But this does not make DCOM unreliable. When using a connectionless protocol, RPC ensures robustness by employing a custom mechanism for message ordering and acknowledgment. An RPC PDU contains up to three parts, only the first of which is required:
This and That
|
|
is transmitted in a request PDU as |
|
The definition of the ORPCTHIS structure is shown here: |
|
The first field of the ORPCTHIS structure specifies the version of the DCOM protocol used to make the method call. In DCOM for Windows 95 version 1.0 and releases of Windows NT 4.0 prior to service pack 3, the COM version is 5.1. For Windows NT 4.0 service pack 3, the COM version is 5.2. For DCOM for Windows 95 version 1.1 and Windows NT 4.0 post service pack 3, the COM version is 5.3. Since each remote method call contains an ORPCTHIS structure, the version of DCOM on the client machine is always transmitted to the server. On the server, the client's version of DCOM is compared to the server's, and unless the major version numbers match, the RPC_E_VERSION_MISMATCH error will be returned to the client. It is permissible for the server to have a higher minor version number than the client. In such cases the server must curtail its use of the DCOM protocol to match those features available in the client's version.
The causality identifier (CID) is a GUID used to link together what might be a long chain of method calls. For example, if client A on machine A calls component B on machine B, and component B, before ever returning to client A, proceeds to call component C on machine C, these calls are said to be causally related. Every time a new method call is made (not while handling an incoming method call), a new causality identifier is generated by the DCOM protocol. The same CID will be propagated in any subsequent calls made by component B on behalf of client A. This is true even if component B uses connection points or some other mechanism to call back into client A. The extensions field of the ORPCTHIS structure is meant to enable extra data to be sent with a COM method call. Currently, only two extensions are defined: one for extended error information (IErrorInfo) and the other for ORPC debugging. Custom extensions to the ORPC_ THIS structure can also be defined using an undocumented technique called channel hooking. For more information on channel hooks, see the January 1998 installment of Don Box's ActiveX®/COM column. In every response PDU for COM methods, a special outbound parameter called ORPCTHAT is inserted before all of the other out parameters of the method. Thus, a COM method defined as |
|
is transmitted in a response PDU as |
|
The definition of the ORPCTHAT structure is shown here: |
|
Meow!
|
|
The byte array that follows the ulCntData field contains the actual object reference in a structure called an OBJREF (see Figure 11). An OBJREF is the data type used to represent a reference to an object. The OBJREF structure assumes one of three forms, depending on the type of marshaling being employed: standard, handler, or custom.
The OBJREF structure begins with a signature field defined as the unsigned long hexadecimal value 0x574F454D. Interestingly, if you arrange this value in little-Endian format (4D 45 4F 57), and then convert each byte to its ASCII equivalent, the resulting characters spell MEOW. Some speculate that this is an acronym for Microsoft Extended Object Wire representation, but no one really knows for sure. The great thing about the MEOW structure is that when scanning through the mountains of packets captured by the Network Monitor utility, it is very easy to tell when you have hit upon an object reference: just say MEOW. Note that regardless of the format of the remainder of the NDR data, the wire representation of a marshaled interface pointer is always stored in little-Endian form. While object references can be stored in a variety of forms, it is not always possible or desirable to pass a header/flag indicating the byte-order (thus also increasing packet size). So COM object references are always transmitted as little-Endian. Following the MEOW signature is the flags field of the OBJREF structure, which identifies the type of object reference. The flags field can be set to OBJREF_STANDARD (1), OBJREF_HANDLER (2), or OBJREF_CUSTOM (4). The IID is the last field of the OBJREF structure. It specifies the interface identifier of the interface being marshaled. Figure 12 shows a network packet captured as a result of the request PDU shown in Figure 5. There the CoCreateInstance function had been called to request the object's IUnknown interface pointer. In Figure 12, you can see the response PDU returned to the client. It contains the marshaled interface pointer (decorated by "MEOW") of the object's IUnknown interface.
The Standard Object Reference
|
|
The first field of the STDOBJREF structure specifies flags relating to the object reference. Although most of the possible settings for the flags parameter are reserved for the system, the SORF_NOPING (0x1000) can be used to indicate that the object does not need to be pinged. (The DCOM network protocol uses pinging to implement a sophisticated garbage collection mechanism that is covered later on.) The second field of the STDOBJREF structure, cPublicRefs, specifies the number of reference counts on the IPID that are being transferred in this object reference. Allocating multiple reference counts on an interface obviates the need to make remote method calls every time the client calls IUnknown::
AddRef.
The third field of the STDOBJREF structure specifies the OXID of the server that owns the object. While an IPID identifies a specific interface of a specific object instance in a process, an IPID alone does not contain enough information to carry out a method invocation. Both DCOM and RPC use strings to specify the binding information needed to carry out a remote call. RPC string bindings contain information such as the underlying network protocol that should be used to carry out the call, as well as the network address of the server machine on which the component is running. Security binding strings help DCOM figure out which parameters to pass on to the RPC infrastructure when it is ready to connect to a particular OXID. An unsigned hyper (64-bits) variable, OXID, represents this connection information. Before making a call, the client translates an OXID into a set of string bindings that the RPC system understands. The details of this translation are covered below. The fourth field of the STDOBJREF structure specifies the object identifier (OID) of the object that implements the interface being marshaled. OIDs are 64-bit values used as part of the pinging mechanism. The final parameter of the STDOBJREF structure is the actual IPID of the interface being marshaled.
DUALSTRINGARRAY
|
|
The first two fields of the structure simply specify the total number of entries in the array (wNumEntries) and the offset where the STRINGBINDINGs end and the SECURITYBINDINGs begin (wSecurityOffset). The array itself is pointed to by the aStringArray element.
A STRINGBINDING structure represents the connection information needed to bind to an object. |
|
The first element of the STRINGBINDING structure, wTowerId, specifies the network protocol that can be used to reach the server via the second parameter, aNetworkAddr. Figure 13 describes the valid tower identifiers for common protocols. The NCA prefix for each tower identifier stands for "Network Computing Architecture." "CN" indicates a connection-oriented protocol, whereas "DG" refers to connectionless, datagram-based protocols.
The second element of a STRINGBINDING structure, aNetworkAddr, is a Unicode string specifying the network address of the server. For example, if the wTowerId value was NCADG_IP_UDP, then a valid network address would be 199.34.58.4. Each STRINGBINDING structure ends with a null character to indicate the end of the aNetworkAddr string. The last STRINGBINDING in a DUALSTRINGARRAY is indicated by the presence of two extra zero-bytes. After that come the SECURITYBINDINGs. The SECURITYBINDING structure contains fields indicating the authentication service (wAuthnSvc) and the authorization service (wAuthzSvc) to be used. The wAuthzSvc is typically set to 0xFFFF, which indicates that default authorization should be used. |
|
This information, taken in total, represents all the information needed to marshal an interface pointer to the client. In the client's address space, a proxy will be loaded and used to communicate with the stub on the server side.
The IRemUnknown Interface
|
|
The IRemUnknown::RemAddRef and RemRelease methods increase and decrease the reference count of the object referred to by an IPID. Like RemQueryInterface, RemAddRef and RemRelease differ from their local counterparts in that they can increase and decrease the reference count of multiple interfaces on multiple objects in the apartment by an arbitrary amount in a single remote call. Imagine a scenario where an object that received a marshaled interface pointer wants to pass it to some other object. According to the COM reference counting rules, AddRef must be called before this interface pointer can be passed to another object. This results in two round-trips: one to get the interface pointer and another to increment the reference counter. The caller can optimize this scenario by requesting multiple references in one call. Thereafter, the interface pointer can be given out multiple times without the need for additional remote calls to increment the reference counter.
The Windows implementation of DCOM typically requests five references when marshaling an interface pointer. This means that the client process receiving the interface pointer can now marshal it to four different apartments in the current process or to four other processes. Only when the client attempts to marshal the interface pointer for the fifth time will the COM runtime make a remote call to the object to request an additional reference. In the interest of performance, client-side AddRef and Release calls never directly translate to RemAddRef and RemRelease. Instead, a remote call to the RemRelease method is deferred until all interfaces on an object have been released locally. Only then is a single RemRelease call made, with instructions to decrement the reference counter for all interfaces by the necessary amount. A call to RemAddRef is made when an interface pointer is unmarshaled and also when the client needs to remarshal an interface pointer as mentioned above. It is important to note that in the scenario described above, where one component returns the interface pointer of another component to a client process, COM never allows one proxy to communicate with another proxy. For example, if client process A calls object B, which then returns an interface pointer for object C, any subsequent calls made by client A to object C are direct. This happens because the marshaled interface pointer contains information on how to reach the machine where the actual object instance exists. For object B to call object C, object B must keep track of object C's OXID, IP address, IPID, and so on. When object B hands client A a pointer to object C, it scribbles all that information into a new OBJREF for client A. Object B is no longer part of the relationship, thus saving network bandwidth and improving the overall performance and reliability. If the machine hosting B went down, A could no longer call C if the calls went through B. After issuing the CoCreateInstanceEx function to instantiate the remote component, your client process is in possession of an initial IUnknown interface pointer. This is typically followed by a call to the IUnknown::QueryInterface method to request another interface, as shown in the following code fragment: |
|
In practice, clients often acquire all needed interface pointers as part of the CoCreateInstanceEx call. When the client process calls the IUnknown::QueryInterface method to request an interface pointer for ISum, the proxy manager in the client's address space calls the IRemUnknown::RemQueryInterface method on the server. Figure 15 shows the network packet transmitted for the RemQueryInterface method call. You can clearly see that DCOM is requesting a count of five references for the ISum interface pointer.
On the server side, the actual IUnknown::QueryInterface call is executed to request an ISum interface pointer from the component. This interface pointer is then returned to the client in the marshaled form of a STDOBJREF. Figure 16 shows the response PDU returned to the client. To prevent a malicious application from being able to call IRemUnknown::RemRelease and forcing an object to unload while other clients may still be using it, a client can request private references. Private references are stored with the client's identity so that one client cannot release the private references of another. Note that private references cannot be provided from one object to another when passing an interface pointer. Each client must request and release its own private references by explicitly calling RemAddRef and RemRelease. Both the RemAddRef and RemRelease methods accept an argument that is an array of REMINTERFACEREF structures. The REMINTERFACEREF specifies an IPID and the number of public and private references that are being requested or released by the client. Programmatically, the client specifies that it needs private references by calling CoInitializeSecurity and specifying the EOAC_SECURE_REFS capability. |
|
The IRemUnknown2 Interface
|
The OXID Resolver
The OXID Resolver, like the SCM, is a part of RPCSS.exe. The OXID Resolver stores and provides local clients with the RPC string bindings necessary to connect with remote objects. It also sends ping messages to remote objects for which the local machine has clients, and receives ping messages for objects running on the local machine. This aspect of the OXID Resolver supports the DCOM garbage collection mechanism. Similar to the way CoCreateInstanceEx incorporates the functionality of CoGetClassObject and IClassFactory:: CreateInstance, the IRemoteActivation interface incorporates the functionality of both the IRemUnknown and the IOXIDResolver interfaces so that only one round-trip is needed to activate an object. The OXID Resolver service resides at the same endpoints as the SCM, described previously. Like the IRemoteActivation interface, the OXID Resolver service implements an RPC interface (not a COM interface) called IOXIDResolver, shown in IDL notation in Figure 17. Note that in Figure 17 the object keyword is conspicuously absent from the interface header, indicating that this is not a COM interface. When presented with an object exporter identifier, it is the job of the OXID Resolver to obtain the associated RPC string binding necessary to connect to the object. This is done on each machine by maintaining a cache of mappings of OXIDs and their associated RPC string bindings. The OXID Resolver maintains this local table, of which one is present on each machine. When asked by a client to resolve an OXID into the associated string binding, the OXID Resolver first checks its cached local table for the OXID. If found, the string binding can be returned immediately. Otherwise, the OXID Resolver contacts the OXID Resolver of the server to request resolution of the OXID into a string binding. The client machine's OXID Resolver then caches the string binding information provided by the server. This optimization enables the OXID Resolver to quickly resolve that OXID for other clients on the same machine that may wish to connect in the future. Should the client pass the object reference to a process running on a third machine, that computer's OXID Resolver service would not have a cached copy of the OXID's string bindings and thus would be obliged to make a remote call to the server in order to resolve the OXID for itself. It is the purpose of the first method, IOXIDResolver:: ResolveOxid, to resolve an OXID into the string bindings necessary to access an OXID object. The OXID being resolved is passed as the first parameter, pOxid, to the ResolveOxid method. When calling the ResolveOxid method, the client specifies what protocol sequences (Tower IDs), from most preferred to least preferred, it is prepared to use when accessing the object. The client passes this information in the arRequestedProtseqs array argument. The OXID Resolver service on the server attempts to resolve the OXID and then returns an array of DUALSTRINGARRAYS, ppdsaOxidBindings, containing the string binding information, again in decreasing order of preference, that may be used to connect to the specified OXID. The steps below explain the OXID resolution process. Assume that the client is unmarshaling an interface pointer from a new OXID:
In version 5.2 of the DCOM network protocol, the ResolveOxid2 method was added to the IOXIDResolver interface. This method enables a client to determine the version of the DCOM network protocol used by the server when requesting OXID resolution. Note the addition of the last parameter in the IDL definition of the IOXIDResolver:: ResolveOxid2 method: |
|
DCOM Garbage Collection
While a distributed system may offer excellent availability and protection against catastrophic failure, the probability of a failure somewhere in the system is greatly increased. From a client's perspective, a failure of the network or the server will be identified by the failure of a remote method call. In such cases, an HRESULT value such as RPC_S_SERVER_UNAVAILABLE or RPC_S_ CALL_FAILED will be returned. A more complicated situation exists on the server if a client fails. The failure of a client may or may not wreak havoc on the server. For example, a stateless object that always remains running and that simply gives out the current time to any client that asks will not be affected by the loss of a client process. An object that does maintain state for its clients will obviously be very interested in the death of one of those clients. Such objects typically have a method, called something like ByeByeNow, that clients call before calling Release on the proxy object. But the client may never have the opportunity to notify the object of its intentions if it or the network fails. This will leave the server in an unstable state, since it is still maintaining information for clients that may no longer exist. RPC deals with this situation using a logical connection between the client and the server processes called a context handle. If the connection between the two processes is broken for any reason, a special function called a rundown routine can be invoked on the server to notify it that a client connection has been broken. For performance reasons, DCOM does not use RPC context handles. Instead, the ORPC protocol defines a pinging mechanism that determines if a client is still alive. A pinging mechanism is quite simple. Every so often, the client sends a ping message to an object saying "I'm alive, I'm alive!" If the server does not receive a ping message for a certain period, then the client is assumed to have died and all its references are freed. The simplistic type of pinging algorithm described above is not sufficient for DCOM because it creates too much network traffic. In a distributed environment where there might be hundreds, thousands, or hundreds of thousands of clients and components in use, network capacity could be overwhelmed simply by the number of ping messages being transmitted. To reduce network traffic, DCOM relies on the OXID Resolver service running on each machine to detect whether its local clients are alive, and then send a single ping message on a per-machine basis instead of per-object. Even with one message being sent to each computer, ping messages may still grow quite hefty, as the ping data for each OID is 16 bytes. For example, if a client computer held 5000 object references to objects running on another machine, each ping message would be approximately 78KB! To further reduce the amount of network traffic, DCOM introduces a special mechanism called delta pinging. Often, a server has a relatively stable set of objects that are used by clients. With delta pinging, instead of including data for each individual OID in the ping message, a set of OIDs are pinged by a single identifier called a ping set that refers to all the OIDs in that set. When delta pinging is employed, the ping message for five OIDs is the same size as that for one million OIDs. To establish a ping set, the client calls the IOXIDResolver::ComplexPing method. The AddToSet parameter of the ComplexPing method accepts an array of OIDs that should define the ping set. Once defined, all the OIDs in the set can be pinged simply by calling IOXIDResolver::SimplePing and passing it the SETID value returned from the ComplexPing method. The ComplexPing method can be called again at any time to add or remove OIDs from the ping set. The pinging mechanism activates the garbage collection based on two values: the time that should elapse between each ping message and the number of ping messages that must be missed before the server can consider the client MIA. Taken together, the product of these two values determines the maximum amount of time that can elapse without a ping message being received, before the server assumes the client is dead. By default the ping period is set to 120 seconds; three ping messages must be missed before the client can be presumed dead. At the present time these default values are not user-configurable. Thus, 6 minutes (3 x 120 seconds) must elapse before a client's references are implicitly reclaimed. Once the OXID Resolver on the server machine decides that an OID is to be rundown, the OID's stub manager is destroyed. The object itself is thus notified that it has no external references. It may have internal (in-apartment) references, in which case it may elect to stay alive. Usually the object will destroy itself at this point, thus reclaiming resources allocated to the client. Some stateless objects, like the time server example discussed previously, have no need for the DCOM garbage collection mechanism. These objects usually run forever and don't really care about a client after a method call has finished executing. For such objects, the pinging mechanism can be switched off by passing the MSHLFLAGS_ NOPING flag to the CoGetStandardMarshal function. Figure 18 shows how to use the MSHLFLAGS_NOPING flag in an implementation of the IClassFactory::CreateInstance method. Just before exiting, the object should execute the following code to free the standard marshaler: |
|
Objects for which the MSHLFLAGS_NOPING flag has been specified will never receive calls to their IUnknown::
Release methods. Clients may call Release, but such calls will not be remoted to the object itself. Because of the highly efficient delta pinging mechanism, turning off pinging for an object will typically not cause a corresponding reduction in network traffic. As there are other objects on the server that do require ping messages, DCOM must still send a ping message to that machine by calling the IOXIDResolver::
SimplePing method. The only difference is that the object that has specified the MSHLFLAGS_NOPING flag will not be added to the SETID that is being pinged.
A Remote Method Call
From the March 1998 issue of Microsoft Systems Journal.
|