CORBA GIOP消息格式学习
想要深入理解ORB的工作过程与原理,学习与了解GIOP消息格式必不可少。我们知道GIOP是独立于具体通信的更高级别的抽象,因此这里针对GIOP在TCP/IP上的实现IIOP协议进行学习与分析(IIOP是规范中要求所有的ORB厂商都必要实现的协议,因此GIOP规范中也对IIOP协议进行了具体的定义)。
根据CORBA Specification的相关阐述,GIOP规范主要包括了以下三个方面:
1、Common Data Representation(CDR) definition(公共数据表达定义),CDR定义了IDL数据类型序列化为底层的byte流的规范。
2、GIOP Message Formats(GIOP消息格式),GIOP消息是ORB之间的通信最小单元。GIOP消息的类型有对象请求,对象定位和通信连接管理等等。
3、GIOP Transport Assumptions(GIOP传输层注意项,不知道这里翻译成”注意项“是否合适)。GIOP规范描述了一些底层协议的实现在传输GIOP消息时需要关注的事项,GIOP给出了一些假设/建议,另外规范还描述了应该如何去管理连接(建立与断开),保证GIOP消息顺序等等。
通常情况下我们并不会去关注第三点。
好了,我们一点一点学习以上三点(大部分情况下我只是作为规范的搬运工而已)。
Common Data Representation(CDR):
CDR描述了IDL数据类型应该如何序列化。包括了以下几点:
1、Byte Ordering,也就是我们所说的大端小端,这里不多陈述大端小端的知识了,读者如有不懂可以自行baidu/google之。每个GIOP消息/Encapsulation(Encapsulation的阐述请点这里)都包含了一个用于标识大小端的标识。
2、Aligned Primitive Types基本类型(如int, short, string)的对齐,就像我们在c/c++接触到的内存对齐情况一样,如此可以使得我们更快地序列化/反序列化消息。
3、Complete OMG IDL Mapping完整的IDL类型映射:CDR描述了所有IDL类型的底层表示,除了基本类型以外,还包括了可以传输的pseudo-object(伪对象)比如TypeCodes(用于在DII和Any中表示参数的类型信息),Object Reference和异常等等(说它们是伪对象,是因为它们并不是真实的对象,而是对象的另一种表达,比如Object Reference可以用来传送一个对象,但Object Reference只是一个IOR)。
首先来看一下第二点,基本类型的序列化。
首先在边界对齐上,数据类型必须对齐到它的大小的倍数的位置上。比如依次序列化下面的struct:
module alignmenttest{ struct TestStruct{ short sa; short sb; char ca; char cb; long la; } }
octet index------------------- 0 1 short sa ------------------- 2 3 short sb ------------------- 4 char ca ------------------- 5 char cb ------------------- 6 7 ------------------- 8 9 10 long la 11 -------------------
这与我们的C语言在大部分机器的上的内存对齐规则是一致的。因此GIOP甚至可以不经过任何处理直接搬到内存中去就可以操作,使用。
Encapsulation
这里不知道怎么去翻译这个词,也许用”封装“比较合适吧,它就像是我们C语言中的struct,java中的class,代表一个集合,并且可以相互嵌套。GIOP规范这里所指的Encapsulation,还有以下两个含义:
1、Encapsulation与Encapsulation,Message之间的Byte Ordering可以是不相同的,即使它们之间是嵌套关系。
2、Encapsulation是一个抽象的表示,通常具体地它将会被编码成sequence<octet>,编码成sequence<octet>时,第一个octet代表着Byte Ordering。
Value Types
valuetype类似于一个加强版本的struct,但valuetype支持继承并且可以定义本地方法(包括构造函数)等而struct不支持,详细请看CORBA规范的说明。基于上面这些特性,在传输valuetype时候,需要包含valuetype的基类信息以便客户端进行向上转类等操作。在进行序列化的时候,需要首先序列化基类的元素,再序列化子类的元素。valuetype支持自定义序列化,但客户端和服务端的一致性由用户自己保证。需要注意的是,valuetype中的方法是作用于本地的,它并不会像interface中的方法论一样被传递到远程Servant。
Pseudo-Object Types 伪对象类型
这里主要说一下TypeCode,关于Object Reference的说明在后面IIOP消息中会有说明。TypeCode的作用上面已经提到,这里再重复一遍,TypeCode提供了一块数据中各个成员的信息,一般在确定动态类型Any的时候会用到。比如:
module...{ interface...{ void add(in long a, n long b, Any c); } }
在运行时Any c的具体类型才可以得知,那么在传输c的时候,需要附上c的序列化结构信息,服务端才能正确地反序列化/还原c,我们可以看一下Any类型的序列化过程相关代码:
public class CDROutputStream_1_0 extends CDROutputStreamBase { .... public void write_any(Any any) { if (any == null) { throw wrapper.nullParam(); } write_TypeCode(any.type());//首先写出TypeCode信息 any.write_value(parent);//写出值信息。 } .... }
下面是写出TypeCode信息的相关代码,它根据当前any类型的_kind来写出相对应的信息。比如对应于struct类型,那么写出的TypeCode信息将包括struct中每个成员的类型信息,这可能是为了应对IDL在客户端与服务端不致的问题。
public final class TypeCodeImpl extends TypeCode { .... public void write_value(TypeCodeOutputStream tcos) { ... TypeCodeOutputStream topStream = tcos.getTopLevelStream(); if (_kind == tk_indirect) { // The encoding used for indirection is the same as that used for recursive , // TypeCodes i.e., a 0xffffffff indirection marker followed by a long offset // (in units of octets) from the beginning of the long offset. int pos = topStream.getPositionForID(_id); int topPos = tcos.getTopLevelPosition(); tcos.writeIndirection(tk_indirect, pos); return; } tcos.write_long(_kind); // Bug fix 5034649: // Do this AFTER the write of the _kind in case the alignment // for the long changes the position. topStream.addIDAtPosition(_id, tcos.getTopLevelPosition()-4); switch (typeTable[_kind]) { case EMPTY: // nothing more to marshal break; case SIMPLE: switch (_kind) { case TCKind._tk_string: case TCKind._tk_wstring: // marshal the bound on string length tcos.write_long(_length); break; case TCKind._tk_fixed: tcos.write_ushort(_digits); tcos.write_short(_scale); break; default: // unknown typecode kind throw wrapper.invalidSimpleTypecode() ; } break; case COMPLEX: TypeCodeOutputStream _encap = tcos.createEncapsulation(tcos.orb()); switch(_kind) { case TCKind._tk_objref: case TCKind._tk_abstract_interface: _encap.write_string(_id); _encap.write_string(_name); break; case TCKind._tk_union: _encap.write_string(_id); _encap.write_string(_name); _discriminator.write_value(_encap); _encap.write_long(_defaultIndex); _encap.write_long(_memberCount); for (int i=0; i < _memberCount; i++) { if (i == _defaultIndex) { _encap.write_octet(_unionLabels[i].extract_octet()); } else { switch (realType(_discriminator).kind().value()) { case TCKind._tk_short: _encap.write_short(_unionLabels[i].extract_short()); break; case TCKind._tk_long: _encap.write_long(_unionLabels[i].extract_long()); break; case TCKind._tk_ushort: _encap.write_short(_unionLabels[i].extract_ushort()); break; case TCKind._tk_ulong: _encap.write_long(_unionLabels[i].extract_ulong()); break; case TCKind._tk_float: _encap.write_float(_unionLabels[i].extract_float()); break; case TCKind._tk_double: _encap.write_double(_unionLabels[i].extract_double()); break; case TCKind._tk_boolean: _encap.write_boolean(_unionLabels[i].extract_boolean()); break; case TCKind._tk_char: _encap.write_char(_unionLabels[i].extract_char()); break; case TCKind._tk_enum: _encap.write_long(_unionLabels[i].extract_long()); break; case TCKind._tk_longlong: _encap.write_longlong(_unionLabels[i].extract_longlong()); break; case TCKind._tk_ulonglong: _encap.write_longlong(_unionLabels[i].extract_ulonglong()); break; case TCKind._tk_wchar: _encap.write_wchar(_unionLabels[i].extract_wchar()); break; default: throw wrapper.invalidComplexTypecode() ; } } _encap.write_string(_memberNames[i]); _memberTypes[i].write_value(_encap); } break; case TCKind._tk_enum: _encap.write_string(_id); _encap.write_string(_name); _encap.write_long(_memberCount); for (int i=0; i < _memberCount; i++) { _encap.write_string(_memberNames[i]); } break; case TCKind._tk_sequence: lazy_content_type().write_value(_encap); _encap.write_long(_length); break; case TCKind._tk_array: _contentType.write_value(_encap); _encap.write_long(_length); break; case TCKind._tk_alias: case TCKind._tk_value_box: _encap.write_string(_id); _encap.write_string(_name); _contentType.write_value(_encap); break; case TCKind._tk_struct: case TCKind._tk_except: _encap.write_string(_id); _encap.write_string(_name); _encap.write_long(_memberCount); for (int i=0; i < _memberCount; i++) { _encap.write_string(_memberNames[i]); _memberTypes[i].write_value(_encap); } break; case TCKind._tk_value: _encap.write_string(_id); _encap.write_string(_name); _encap.write_short(_type_modifier); if (_concrete_base == null) { _orb.get_primitive_tc(TCKind._tk_null).write_value(_encap); } else { _concrete_base.write_value(_encap); } _encap.write_long(_memberCount); for (int i=0; i < _memberCount; i++) { _encap.write_string(_memberNames[i]); _memberTypes[i].write_value(_encap); _encap.write_short(_memberAccess[i]); } break; default: throw wrapper.invalidTypecodeKindMarshal() ; } // marshal the encapsulation _encap.writeOctetSequenceTo(tcos); break; } } ... }
Object Reference,对象引用
传送对象引用一般情况下传送它的IOR就可以了,IOR至少包含了以下信息:
1、版本信息,以保证低版本的ORB不会乱去解析高版本的IOR表示。
2、传输层面对象宿主机器的地址。
3、对象ID,用于唯一定位目标对象 。
Abstract Interface,抽象接口
Abstract Interface是一种比较特殊的类型,它是一个方法集合,interface和valuetype类型可以继承它,比如:
abstract interface Describable { string get_description(); }; interface Example { void display (in Describable anObject); }; interface Account : Describable {// passed by reference // add Account methods here }; valuetype Currency supports Describable {// passed by value // add Currency methods here };
如上面,如果在运行时anObject参数是Account,那么在序列化的时候,首先用一个boolean为true的值以表明它是一个Object Reference,然后紧接着这个对象的IOR。
--------------- TRUE --------------- IOR ---------------
如果在运行时anObject参数是Currency,那么在序列化的时候,首先用一个boolean为false值以表明它是一个valuetype,然后紧接着它的值。
--------------- FALSE --------------- Value ---------------
GIOP消息格式
首先我们来看一下GIOP消息的IDL声明:
module GIOP { // IDL extended for version 1.1 and 1.2 struct Version { octet major; octet minor; }; #ifndef GIOP_1_1 // GIOP 1.0 enum MsgType_1_0 { // Renamed from MsgType Request, Reply, CancelRequest, LocateRequest, LocateReply, CloseConnection, MessageError }; #else // GIOP 1.1 enum MsgType_1_1 { Request, Reply, CancelRequest, LocateRequest, LocateReply, CloseConnection, MessageError, Fragment // GIOP 1.1 addition }; #endif // GIOP_1_1 // GIOP 1.0 struct MessageHeader_1_0 { // Renamed from MessageHeader char magic [4]; Version GIOP_version; boolean byte_order; octet message_type; unsigned long message_size; }; // GIOP 1.1 struct MessageHeader_1_1 { char magic [4]; Version GIOP_version; octet flags; // GIOP 1.1 change octet message_type; unsigned long message_size; }; // GIOP 1.2 typedef MessageHeader_1_1 MessageHeader_1_2; };
magic表明这是一个GIOP消息,它的值为字符串GIOP的byte:"GIOP".getBytes("ISO8859-1")
GIOP_Version包含这个消息所使用的GIOP协议的版本信息,需要注意的是,它和IIOP版本号是两码事,虽然它们的IDL描述是一样的
byte_order只在GIOP 1.0版本中被使用,它指定后面元素如message_size(比如占据4个字节整形)是大端表示还是小端表示的。
flags在GIOP 1.1和1.2版本中使用,它和byte_ordfer一样都只占据一个字节,只不过flag中用bit表示一些信息:
|7|6|5|4|3|2|1|0| | | | | | | | | | | | | | | | ->byte order, 0: big-endian, 1: little-endian | | | | | | ---->后面是否还有fragments, 0: No, 1: Yes ---------------->保留
message_type,它的值表明了消息的类型,下面是消息类型的列表。
Message Type | Originator | Value | GIOP Versions |
---|---|---|---|
Request | Client | 0 | 1.0, 1.1, 1.2 |
Reply | Server | 1 | 1.0, 1.1, 1.2 |
CancleRequest | Client | 2 | 1.0, 1.1, 1.2 |
LocateRequest | Client | 3 | 1.0, 1.1, 1.2 |
LocateReply | Server | 4 | 1.0, 1.1, 1.2 |
CloseConnection | Server | 5 | 1.0, 1.1, 1.2 |
MessageError | Both | 6 | 1.0, 1.1, 1.2 |
Fragment | Both | 7 | 1.1, 1.2 |
message_size表明消息头后面的还有多个字节 (消息体Message Body的长度,不包括12个字节的消息头),这是一个unsigned long类型,它的字节顺序与上面byte_order域与flags域所指定的字节顺序是一致的。注意对于GIOP1.2,message_size + 12(消息头长度)的和一定需要可以被8整除(也许为了内存对齐)。
下面介绍一下消息类型,因为篇幅原因,这里只对Request Message和Reply Message介绍。
Request Message
Request Header的IDL定义:
module GIOP { // IDL extended for version 1.1 and 1.2 // GIOP 1.0 struct RequestHeader_1_0 { // Renamed from RequestHeader IOP::ServiceContextList service_context; unsigned long request_id; boolean response_expected; sequence <octet> object_key; string operation; Principal requesting_principal; }; // GIOP 1.1 struct RequestHeader_1_1 { IOP::ServiceContextList service_context unsigned long request_id; octet reserved[3]; sequence<octet> object_key; string operation; Principal requesting_principal; }; // GIOP 1.2 typedef short AddressingDisposition; const short KeyAddr=0; const short ProfileAddr=1; const short ReferenceAddr=2; struct IORAddressingInfo { unsigned long selected_profile_index; IOP::IOR ior; }; union TargetAddress switch (AddressingDisposition) { case KeyAddr: sequence<octet> object_key; case ProfileAddr: IOP::TaggedProfile profile; case ReferenceAddr: IORAddressingInfo ior; }; struct RequestHeader_1_2 { unsigned long request_id; octet response_flags; octet reserved[3]; TargetAddress target; string operation; IOP::ServiceContextList service_context; // Principal not in GIOP 1.2 }; };
request_id用于将服务器的响应信息与客户端发送的请求信息对应起来,因为客户端的同时可能向服务端发送多个对象的请求,而它们可以共用一个连接,服务器返回的信息将包含客户端发送的request_id,客户端利用request_id来完成响应信息的分发。
response_flags用于表示此请求是否需要返回值,它的最低位是1的话表示需要返回值,如何需要返回值,并且在DII中INV_NO_RESPONSE标志位没有被设置,那么response_flags必须设置为0x03。
如果此请求不需要返回值,或者在DII调用中设置了INV_NO_RESPONSE标志位,那么response_flags可以设置为0x00或者0x01
reserved的值应该设置为,它为未来保留使用。
object_key,在GIOP 1.0和1.1中,用于标识服务端的目标对象,它其实就是IOR IIOPProfile中的object_key的值。
target,但从GIOP 1.2开始,更换为使用target域来标识调用的目标。
operation是idl中定义的interface中的目标方法名。
service_context包含了从客户端传递到服务端的Service Context信息。
requesting_principal,在GIOP 1.0和GIOP 1.1版本中,它包含了关于客户端身份信息的类,用于访问控制和其它目的。在它已经被弃用,并且在GIOP 1.2中不再包含这部分信息。
Request Body
在GIOP 1.0和1.1中,Request Body被序列化成Encapsulation并立即被附在消息头的后面。从GIOP 1.2开始,Request Body的开始位置是对齐到8字节的,如果消息头被修改之后,Request Body不需要重新序列化。Request包括了以下几项内容(按顺序序列化):
1、所有的in和inout参数,按照它们在方法参数中出现的先后顺序(从左到右)。
2、可选的Context pseudo object(Context伪对象),这些Context pseudo object被序列化成sequence<String>,每个Context pseudo object被序列化成一对String,Property Name和Property Value。只有操作的IDL定义包含有一个Conext Expression的情况下才会出现Context pseudo object,并且只会包含Context Expression中声明的成员。
比如下面方法的
double example(in short m, out string str, inout p)
它的Request Body的将会是下面的结构:
struct example_body{ short m; long p; }
Reply Message
只有Request Message中的response expected位被设置为TRUE的情况下,服务器才会发送对应的Reply Message。Reply Message包括了所有的inout和out参数,并且可以包含有异常值。同样Reply Message包括了Reply Header和Reply Body两部分。
Reply Header
首先来看一下它的IDL定义:
module GIOP { // IDL extended for 1.2 #ifndef GIOP_1_2 // GIOP 1.0 and 1.1 enum ReplyStatusType_1_0 { // Renamed from ReplyStatusType NO_EXCEPTION, USER_EXCEPTION, SYSTEM_EXCEPTION, LOCATION_FORWARD }; // GIOP 1.0 struct ReplyHeader_1_0 { // Renamed from ReplyHeader IOP::ServiceContextList service_context; unsigned long request_id; ReplyStatusType_1_0 reply_status; }; // GIOP 1.1 typedef ReplyHeader_1_0 ReplyHeader_1_1; // Same Header contents for 1.0 and 1.1 #else // GIOP 1.2 enum ReplyStatusType_1_2 { NO_EXCEPTION, USER_EXCEPTION, SYSTEM_EXCEPTION, LOCATION_FORWARD, LOCATION_FORWARD_PERM,// new value for 1.2 NEEDS_ADDRESSING_MODE // new value for 1.2 }; struct ReplyHeader_1_2 { unsigned long request_id; ReplyStatusType_1_2 reply_status; IOP:ServiceContextList service_context }; #endif // GIOP_1_2 };
request_id用于把Reply Message与对应的Request Message关联起来,并不是每个对象的请求会单独占用整个连接,多个对象的请求可以通过单个连接发送到服务端。Reply Message中的request_id与对应的Request Message中的request_id的值是一致的。
reply_status用于表示对应请求的完成状态和Reply Body的内容 ,如何方法请求调用成功完成,那么它的值是NO_EXCEPTION,并且Reply Body中包含返回值。否则Reply Body中的内容将会是:
1、异常值。
2、要求客户端重发请求到另一个服务端地址上的对象。
3、要求客户端提供更多用于定位对象的信息。
service_context包含了服务端发送给客户端的ORB service信息
Reply Body
和Request Body一样,在GIOP 1.0和1.1中,Reply Header紧接着就是Reply Body。从GIOP 1.2开始,Reply Body的起始位置需要对齐到8字节。Reply Body的内容由Reply Header中的reply_status的值决定:
如果reply_status的值为NO_EXCEPTION,那么Reply Body首先序列化返回值,然后是IDL方法定义中的out和inout参数(从左到右顺序),如下面方法
double example(in short m, out string str, inout long p)
那么该方法请求在NO_EXCEPTION的情况下的返回值为:
struct{ return double; string str;
long p; }
如果reply_status为USER_EXCEPTION或者SYSTEM_EXCEPTION的话,那么reply body包含方法抛出的异常值。
当reply_status为SYSTEM_EXCEPTION,那么reply body的结构如下:
module GIOP { // IDL struct SystemExceptionReplyBody { string exception_id; unsigned long minor_code_value; unsigned long completion_status; }; };
minor_code_value的高20位表示厂商Minor Codeset ID(VMCID),低12位表示minor code。一些厂商(可以是多个)可能想定义一些特有的异常MInor code,那么它们应该向OMG去申请这个VMCID。注意OMG标准的VMCID为0x4f4d0('O', 'M')。
如果reply_status的值为LOCATION_FORWARD的话,那么Reply Body包含了一个对象的IOR,然后客户端ORB负责将原来的请求重新发送到这个IOR指定的对象上,这个过程对于客户端应用程序是透明的(客户端并不能知道这些过程)。
如果replay_status的值为LOCATION_FORWARD_PERM,它的行为表现和LOCATION_FORWARD几乎是一样的,另外它被服务器用来向客户端表明 ,它可能要将当前对象的IOR替换成新的,并且新的IOR和旧的还是有效的,但推荐使用新的。
如果reply_status的值为NEED_ADDRESSING_MODE的话,Reply Body包括了一个GIOP::AddressingDisposition,客户端负责使用request addressing mode(提供更多对象信息)重新发送请求到这个对象上。同样的,重发过程对于用于程序是透明的,ORB并不会通知客户端的程序这个过程。
completion_state根据Standard Exception Definitions的IDL定义,它有COMPLETED_YES, COMPLETED_NO, COMPLETED_MAYBE三种状态。