3.2 流化表示法综述
在介绍流化的过程之前,应该先看看流化表示法的具体内容。其实,以二进制流的格式来表示消息,其整体结构我们在第2章已经介绍过了,即由消息头、消息类型块和消息体三个部分顺序组成,而每个部分(代码表示为类的实例)如何触发流化接口以及流化后的组成、顺序等也都在第2章介绍的流化专有与公共代码中有了很清晰的体现。我们已经知道了如何将一个复杂的数据结构(类的实例,类实例组成的向量vector等)转换为二进制流表示:无论多么复杂,其最基础的接口都是基本数据类型的流化(即StreamlizeTemplate.h中所定义接口的实现问题)。但是,我们发现,第2章并没有涉及这个重要问题:各个基本数据类型的流化表示法。而这正是本节要讲的内容。
关于这个问题,其实也是有很多种表示方法的,其中主要包括自定义字节流法、XDR表示法等。下面将分别对这两种方法进行介绍。
3.2.1 自定义字节流
顾名思义,自定义字节流是指消息体系的设计者自行设计的一种消息流化后的二进制流表示格式、规则,并据此来实现流化与反流化的接口。
很明显,第1章介绍的原始消息设计中,就采用了自定义字节流的流化表示法,它的核心思想主要包括以下两点。
· 采用竖线分隔符将一个结构实例的各个元素分开(这样的简单处理使我们不需要再担心反流化时不知道每个元素的流长度)。
· 对整型或浮点型数据采用sprintf转换为字符串(主要是为了实现平台无关),再用atoi或atof还原为数值。
当然,那时,我们紧接着分析了该方法的局限性,指出消息体系需要更加全面、完整的流化表示法,对此这里就不再啰嗦,但我们只是要说,对上述的自定义字节流法,经过改进后,当然也可以满足一个消息体系的需求,读者完全可以自行尝试。
3.2.2 XDR表示法
虽然自定义字节流可以满足需求,但本书要重点介绍XDR(外部数据表示法),因为它是本章内容实现的基础(第2章提到但并没有介绍的XDRMethod类,就是用来实现基于XDR表示法的流化方法)。
1.XDR简介
XDR是External Data Representation的缩写,可译为“外部数据表示”,它是SunSoft开放网络计算环境的一种功能。XDR提供了一种与体系结构无关的流数据表示方法,解决了数据字节排序的差异、数据字节大小、数据表示和数据对准的方式。使用XDR的应用程序,可以在异构硬件系统上交换数据。SunSoft用户可以免费获得RPC/XDR规范和源代码。
XDR在OSI模型的表示层presentation layer中实现。XDR允许把数据包装在独立于介质的结构中,使得数据可以在异构的计算机系统中传输。从局部表示转换到XDR称为编码,从XDR转换到局部表示称为译码。XDR使用软件来完成变换,所以在不同的操作系统中可以灵活的运用。另外,XDR独立于传输层(transport layer),Sun的远端程序呼叫RPC就是使用XDR。
XDR定义了以下几种数据类型:
布尔,字符,短整型,整型,长整型,浮点,双精度浮点,枚举,结构,字符串,固定长度数组,可变长度数组,联合及opaque data。
2.XDR的重要特性
XDR表示法有以下几个重要特性,读者必须了解。上一节简介的内容,其实很容易都可以找到,但本节总结的XDR特性,其资料却并不多见,一般只有真正用到过XDR的读者才会有所了解。
· 用XDR法来表示流数据,可以完全实现与计算机体系结构无关,即数据字节排序的差异、数据字节大小、数据表示和数据对准的方式等问题。即采用XDR法,就不需要再自行考虑平台相关问题。
· XDR法表示任何类型的数据,其流数据的长度都一定是4的倍数。举个简单的例子:在大多数平台上,short型是两个字节,int型是四个字节,但在XDR表示中,都应该是四个字节。很明显,对不足四个字节倍数的字符串与二进制流,XDR都会要求补足为四个字节的倍数的长度。细心的读者可能已经发现,在第2章关于流化接口的公共代码中(StreamlizeTemplate.h),我们没有给出关于short类型的接口定义。现在我们已经清楚:实际上,采用XDR表示法以后,short型并不会为我们节省传输流的大小,因此,还不如都用四字节的int型简单一些。
· XDR的接口只是实现流数据表示的编码/译码,以及其在相应内存段的存储与读取,但并不负责数据的网络传输过程(发送与接收),网络传输部分需要自行编码处理。
· 虽然说XDR与计算机体系结构无关(其中最主要的就是字节序问题:大尾Big Endian与小尾Little Endian),但其实实现这种无关的方法就是在所有平台上都统一采用一种字节序来处理。对XDR来说,都统一采用大尾Big Endian的字节序。
3.XDR接口
本节列举并介绍几个XDR重要的接口(函数、变量类型和宏定义),因为我们后面的内容中要用到它们。
C语言中,XDR的接口定义在xdr.h中,所以,要使用XDR接口,需要在源程序中加入#include <xdr.h>。xdr.h中定义了非常丰富的XDR操作方法与数据类型,这里我们主要介绍以下几个。
· 变量类型XDR
这是最重要的一个变量类型,用来定义XDR流对象。XDR流对象一般以指针形式使用,它是XDR的各种方法操作流数据(写入、读出、获取或设置位置)的入口(句柄),并且通常与用户自定义的一段内存(堆或栈)相关联。如:
XDR *xdrs;
定义了一个指向XDR流对象的指针xdrs。
· 枚举类型xdr_op及其元素XDR_ENCODE与XDR_DECODE枚举类型xdr_op有两个元素XDR_ENCODE与XDR_DECODE,这两个元素其实用以指明某个XDR流对象(以XDR变量类型定义)是用于发送还是接收流数据,若是发送(编码,写),则为XDR_ENCODE,若是接受(读,译码),则为XDR_DECODE。· 函数xdr_memcreate
该函数的C语言定义原型如下:
void xdrmem_create(xdrs, addr, size, op) XDR *xdrs; char *addr; u_int size; enum xdr_op op;
其中addr是实际指向用户自定义的那段内存,它与xdrs指向的XDR流对象相关联。XDR的其他函数则通过xdrs从addr内存段中读出或写入流数据,addr指向的内存段其长度不能超过size。同时,如上所述,op的值为XDR_ENCODE时,说明xdrs可用于写;op的值为XDR_DECODE时,说明xdrs可用于读。
· 函数xdr_destroy该函数的C语言定义原型如下:
void xdr_destroy(xdrs) XDR *xdrs;
用于释放xdrs指向的XDR流对象。注意,它只是释放流对象本身(句柄)的内存空间,但并不用于释放真正存储流数据的那段内存,即xdrmem_create中,指针addr指向的内存段。
· 函数xdr_int
该函数的C语言定义原型如下:
xdr_int(xdrs, ip) XDR *xdrs; int *ip;
根据xdrmem_create初始化流对象指针xdrs时枚举变量op的值不同(XDR_ENCODE或XDR_DECODE),xdr_int可以将整型指针ip指向的整型数变量以XDR格式编码后存入与xdrs关联的内存段,或将与xdrs关联的内存段中以XDR编码格式保存的整型数译码后读入到ip指向的整型数变量中。
· 函数xdr_float
该函数的C语言定义原型如下:
xdr_float(xdrs, fp) XDR *xdrs; floa t *fp;
根据xdrmem_create初始化流对象指针xdrs时枚举变量op的值不同(XDR_ENCODE或XDR_DECODE),xdr_float可以将浮点型指针fp指向的浮点型数变量以XDR格式编码后存入与xdrs关联的内存段,或将与xdrs关联的内存段中以XDR编码格式保存的浮点型数译码后读入到fp指向的浮点型数变量中。
· 函数xdr_double
该函数的C语言定义原型如下:
xdr_double(xdrs, dp) XDR *xdrs; double *dp;
根据xdrmem_create初始化流对象指针xdrs时枚举变量op的值不同(XDR_ENCODE或XDR_DECODE),xdr_double可以将双浮点型指针dp指向的双浮点型数变量以XDR格式编码后存入与xdrs关联的内存段,或将与xdrs关联的内存段中以XDR编码格式保存的双浮点型数译码后读入到dp指向的双浮点型数变量中。
· 函数xdr_opaque
该函数的C语言定义原型如下:
xdr_opaque(xdrs, cp, cnt) XDR *xdrs;
char *cp; u_in t cnt;
同样,该函数用于将固定长度的opaque数据转换成XDR编码,并写入相应内存段,或从相应内存段中读出XDR编码并转换成opaque数据。所谓opaque数据,一般指二进制流或字符串,在C语言中,一般都是用char型的数组表示。后面我们会看到,同样是char型数组的定义,在消息流化时,也有可能采用不同的方法处理,但其底层XDR接口都可以是xdr_opaque。
· 函数xdr_getpos
该函数的C语言定义原型如下:
u_int xdr_getpos(xdrs) XDR *xdrs;
该函数获得xdrs指向的XDR流对象当前的读/写位置。
· 函数xdr_setpos
该函数的C语言定义原型如下:
u_int xdr_setpos(xdrs, pos) XDR *xdrs; u_int pos;
该函数用于设置xdrs指向的XDR流对象当前的读/写位置。
3.2.3 平台无关
这即是指跨平台的问题。跨平台的问题其实有以下几个不同的领域:
· 跨平台运行
· 跨平台开发
· 跨平台通信
这里我们只介绍跨平台通信的问题。
各种基本数据类型的流化表示法中,有一个最核心的问题,那就是与平台无关。我们之所以选择XDR表示法,也是因为它很好地解决了流数据跨平台传输的问题。但XDR只是解决了数据字节排序的差异、数据字节大小、数据表示和数据对准的方式等问题,并没有解决所有的平台相关问题。
长年工作在跨平台开发环境下的读者应该清楚,至少还有以下几个问题需要考虑:
· 流数据在64位与32位体系间的兼容
· 流数据中字符串的编码方式
· 流数据在不同编程语言之间(如Java和C/C++)的兼容
而这几个问题都不是XDR所擅长的,需要消息体系的设计开发者自行考虑解决。
注意,以下各小节介绍的问题都只是限制在流数据编码格式的平台无关问题上,而不涉及编译后可执行代码的平台无关或源代码平台无关这样的问题。
1.Little Endian与Big Endian
大家知道,Little Endian与Big Endian的字节序问题是XDR表示法能很好解决的问题,但由于它是跨平台数据通信最重要的内容,所以,我们还是对其作以简单介绍。
Little Endian和Big Endian是表示计算机字节顺序的两种格式,所谓的字节顺序指的是长度跨越多个字节的数据的存放形式。简单地说,Little Endian把低字节存放在内存的低位,而Big Endian将低字节存放在内存的高位。
我们假设最小的内存单元是8位(一个字节)。例如,对4个字节的整型数1247752720,其16进制表示为0x4a5f3210,即其最低位为0x10,最高位为0x4a,假设存放这个整型数的内存单元的起始地址为0x10000100,终止地址为0x10000103,那么,在32位的Little Endian体系中,该整型数在内存中存放如下:
地址: 0x10000100 0x10000101 0x10000102 0x10000103
数据: 0x10 0x32 0x5f 0x4a
但在32位的Big Endian体系中,却存放如下:
地址: 0x10000100 0x10000101 0x10000102 0x10000103
数据: 0x4a 0x5f 0x32 0x10
如何判断一个体系是Little Endian还是Big Endian呢?其实写一个简单的程序就可以做到,读者有兴趣可以自行设计该程序,这里就不多说了(或者将一个整型数以字符串形式输出到一个文件中,通过UltraEdit或xxd等工具也可以很容易看出来)。
当然,流数据在不同平台之间传输时,一定要考虑Little Endian和Big Endian的问题,否则将会使数据的解释出现严重的错误。
2.32位与64位
这是流数据传输时平台无关的另一个重要问题。我们知道,在32位与64位机器上,在相同或不同的操作系统下,同一类型变量的字节长度可能是不一样的。这就是导致流数据在32位机器与64位机器间传输时,可能发生问题的内在原因。
举个例子,在Linux操作系统下的C语言中,long型的变量,在32位机器上是4个字节,但在64位机器上却是8个字节。因此,如果一个64位机器上的long型变量流化后,传输到32位机器上时,采用long型来接收和解释,则必然失败,并且可能导致整个流数据的定位失效。
对该类问题的解决,一般采用这样的方法:无论是在32位机器上,还是在64位机器上,我们都统一规定long型的变量只代表4个字节的整型数,无论在发送、传输、接收时都严格遵守这一规则。在XDR接口调用上,对long型与int型都统一用xdr_int来处理。这样一来,问题便得到了解决。如果我们需要8字节的变量,便采用double型或long long型来传输,这两种类型的变量无论在32位还是在64位机器上的各种操作系统下都是8字节长。除了long型以外,还有long double型在32位与64位机器上,在不同的操作系统下,字节长度也可能不同,需要采用同样的方法来处理。
其实,除了在32位与64位机器间传输外,在不同的编程语言间传输流数据时,也有同样的问题。例如,同是在32位机器上,C语言的long型是4字节,但Java语言的long型却是8字节。这需要采用同样的方法来解决。
因此,我们得出一个结论:在消息体系中,最好将long型全部当成4字节来对待,也就屏蔽了本节所讲的问题,这正是本书所介绍消息体系采用的规则。
3.UTF8与UTF16的转换
这个问题来源于不同编程语言之间的流数据传输,并且主要是针对字符串变量而言。
我们知道,不同的编程语言,其对字符串的编码的规则不同。如Linux/UNIX C/C++语言,一般采用UTF-8编码(一般以1~3个字节表示一个字符);对Java语言,其内部编码一般是UTF-16(统一以2个字节表示一个字符);而Windows下的VC++,一般采用wchar来存储一个字符,其实就是UTF-16(除非在工程配置中专门改变此配置)。因此,不难想象,在Linux/UNIX C/C++语言中的一个字符串,原样传输到Java语言程序接收后,一定是不正确的,这便是问题的所在。
如何解决这个问题呢?很简单,我们的思路还是“统一规定”,即规定无论是什么编程语言,在对字符串进行传输时,都统一转换成UTF-16的格式,即用两个字节代表一个字符。这样,在Linux/UNIX C/C++程序与Java程序之间传输字符串时,其过程如下:Linux/UNIX C/C++传向Java时,先将字符串转换成UTF-16再传输;反过来,从Java传向Linux/UNIX C/C++时,先以原字节流传输,Linux/UNIX C/C++接收到后,再转换成UTF-8。
我们不妨定义以下两个函数:UTF162UTF8和UTF82UTF16。本书给出这两个函数的实现如下,供读者参考。
void UTF162UTF8(char *str, int len) { char *p = str; unsigned short up[MAX_UNICODE_LEN]; if (len > MAX_UNICODE_LEN) { len = MAX_UNICODE_LEN; } for (int i = 0; i < len; i++) { UnicodeLoad(p, up[i]); } p = str; for(int i = 0; i < len; i++) { p+= encodeUTF8(up[i], (unsigned char *)p); if(!up[i]) break; } *p = '\0';
} void UTF82UTF16(char *str, int& len) { char *p = str; unsigned short up[MAX_UNICODE_LEN]; int maxlen = MAX_UNICODE_LEN; len = 0; while(( *p != '\0') && (len < maxlen)) { up[len] = decodeUTF8(&p);++len; } up[len] = 0; p = str; for (int i = 0; i <= len; i++) { UnicodeSave(p, up[i]); } *p = '\0'; } unsigned UnicodeLoad(char* &src,unsigned short &b) { unsigned size = 2; memcpy(&b, src, size); src += size; b = ntohs(b); return size; } unsigned UnicodeSave(char* &dest,const unsigned short &b) { unsigned size = 2; unsigned short up = htons(b); memcpy(dest, &up, size); dest += size; return size; } unsigned short decodeUTF8(char **p) { unsigned short buf = 0; unsigned short buf1; if (( **p & 0x80) == 0) {
buf = (unsigned short)(**p); ++ *p; } else if ((**p & 0xe0) == 0xc0) { buf = (**p & 0x1F); buf <<=6; ++*p; buf1 = **p & 0x3F; buf = buf | buf1; ++*p; } else if ((**p & 0xf0) == 0xe0 ) { buf = (**p & 0x0f); buf <<=6; ++*p; buf1 =**p & 0x3F; buf |=buf1; buf <<= 6; ++*p; buf1 = (**p & 0x3f); buf |= buf1; ++*p; } else { buf = (unsigned short) (**p); ++*p; } return buf; } size_t encodeUTF8( unsigned long value, unsigned char *buf) { if ( value <=0x0000007F) { buf [0] = (unsigned char ) value; return 1; } else if ( value <=0x000007FF) { buf [1] = ( unsigned char )((value & 0x3F) |0x80); value >>=6; buf[0] = (unsigned char )((value & 0x1F) | 0xC0); return 2; } else if ( value <=0x0000FFFF) { buf [2] = ( unsigned char )((value & 0x3F) |0x80); value >>=6; buf[1] = (unsigned char )((value & 0x3F) | 0x80);
value >>=6; buf [0] = (unsigned char )((value & 0x0F) | 0xE0); return 3; } return 0; //一定是发生了什么错误 } int getUTF16Len(const char *str) { const char *p; p = str; int count = 0; while( *p!= '\0' ) { if((*p & 0x80)==0) { ++count; if( *p!='\0' ) ++p; } else if ((*p & 0xe0) ==0xc0) { ++ count ; if( *p!='\0' ) ++p; if( *p!='\0' ) ++p; } else if (( *p &0xf0) == 0xe0) { ++ count; if( *p!='\0' ) ++p; if( *p!='\0' ) ++p; if( *p!='\0' ) ++p; } else { ++count; if( *p!='\0' ) ++p; } } return count; }
以上代码中,UnicodeSave与UnicodeLoad分别完成了将一个UTF-16字符以网络传输编码存入流对象中,或从流对象中取出一个UTF-16编码字符的功能;decodeUTF8从一个UTF-8的流对象中取出一个字符并转换为UTF-16编码;encodeUTF8则将一个UTF-16的字符编码转换为一个UTF-8的字符编码。最后还用函数getUTF16Len完成对一个UTF-8的字符串算出其UTF-16编码字符串的长度的功能,在后文我们会用到它。
在我们的消息体系中,只有正确地处理以上三个问题,才算是较全面地实现了流数据传输的平台无关。