整编解编ACE解决方案
时间:2009-03-04 来源:liujianping
1. 基本概念
1.1 字节序
字节序就是超过一个字节的数据类型在内存中的表示方式。
约定的网络字节序采用大端表示法,但是intel架构的PC机,包括现在很多intel芯片的服务器都是用小端表示法。早期的大型机和小型机多用大端表示法。
现在来看,网络字节序采用大端表示法,其实是很不“环保”的。互联网中的绝大多数电脑是用小端表示法,可偏偏选用大端表示法作为网络字节序。这样导致了很多没必要的转换。如果网络字节序采用小端表示法,每天发生在互联网上的字节序转换会少得多,呵呵。
除非你写的是一个封闭的系统,且你可以100%肯定系统内所有机器都使用相同的字节序,否则还是应该坚持使用网络字节序(也就是大端(big-endian)表示法)来进行网络数据的编解码。
一般用htons,htonl进行本地字节序到网络字节序的转换,即(host to net),前者处理short即2字节整数,后者处理long即4字节整数。如果本地字节序也是大端表示法,则这两个方法什么也不做。在有的系统上这两个方法被实现成宏,以节约一次调用开销。于之对应的还有ntohs和ntohl,负责从网络字节序转换到本地字节序。
对于long long类型即8字节整数,就只能自己进行转换,比较麻烦。
1.2 紧缩
紧缩一般是为了避免不同主机间字节对齐的标准不一样,同时还可以节约带宽。比如下面的结构体。
struct a
{
char a_;
long b_;
}
在windows平台用vc编译,默认情况下这个结构体的实例的大小为8个字节,即按4字节进行了对齐。一般对于这样的数据在编码时会进行一次紧缩,紧缩后变为5个字节。解码时再对逐个成员赋值一次,恢复为本地标准。
自行处理这些细节,并不复杂,但很繁琐。而且出了问题调试很麻烦,一般要直接看内存buffer才能定位问题。所以尽量不要自己干这个事。
2. ACE_CDR组件
2.1 功能
ACE中提供了一组CDR类,专用于编解码,但它并不是专门针对我们在前面述情况的编解码情况,所以还必须进行定制。我们下面也按照不同的方面来讨论如果定制及使用ACE的CDR类来做网络数据的编解码。
ACE_InputCDR类负责解码,ACE_OutputCDR类负责编码。这两个类都提供流式操作,使用很方便。唯一不爽的是它不提供链式操作。因为每一个“<<”或“>>”操作符重载函数返回的都是一个布尔变量,不像标准库那样返回表达式左值的引用。
默认情况这两个类不对字节序进行任何转换,也就是总是使用主机字节序。如果两个主机间的字节序不一样,直接使用这两个类在一个主机上编码,再在另一个主机上解码,就会导致出错。CNPv1第四章中的例子通过在每一个消息头前面增加一个布尔变量来指定编码时的字节序,以解决这个问题。编码时首先把本地的主机字节序用一个布尔变量压进去,解码时首先解出这个布尔量,再用这个布尔量来设置ACE_InputCDR的字节序。这样做又麻烦又容易出错。由其不便于异种系统之间的对接。最方便的方法也是通用的方法,就是编解码都使用网络字节序,虽然会因为不必要的转换带来一点效率损失,但处理简单,也便于扩展。
2.2 使用说明
2.2.1 字节序问题
要指定编解码都使用网络字节序,我们可以通过在创建这两个类的对象时进行显示的指定,或是调用reset_byte_order方法进行设置。以创建对象为例:
ACE_OutputCDR ocdr(&mb, ACE_CDR::BYTE_ORDER_BIG_ENDIAN);
ACE_InputCDR icdr(&mb, ACE_CDR::BYTE_ORDER_BIG_ENDIAN);
对于ACE_OutputCDR类,第二个参数指定了要使用大端表示法对数据进行编码。对于ACE_InputCDR类,第二个参数指定了待解码的数据是用大端表示法编码的。
要注意的是,光有这样的指定还不够。缺省用ACE_OutputCDR进行编码时,它并不对写入的数据进行大小端转换,即使做了上述的指定。
解决方法是,在编译ACE时,在config.h文件中加下下面的行。
#define ACE_ENABLE_SWAP_ON_WRITE
这样就可以了。CDR类会自动的处理是否需要进行转换。
还有一点要注意的,BYTE_ORDER_BIG_ENDIAN这个枚举在较早的的ACE版本中没有。5.6.5及以前版本中是没有的,5.6.7中有,5.6.6我没有用过。如果你用的是较早版本,可以直接用硬编码的数字。
BYTE_ORDER_BIG_ENDIAN = 0;
BYTE_ORDER_LITTLE_ENDIAN = 1;
2.2.2 紧缩问题
在缺省情况下,ACE_InputCDR和ACE_OutputCDR这两个类是不处理紧缩的,统一按4字节对齐。即小于4字节的数据类型压进去后都占4个字节。这样做主要是处于效率的考虑。32位的CPU在处理按4字节对齐的数据时最快,不需要再处理额外的偏移。但这样明显不符合我们前面描述的情况,我们不希望编码后的数据f中留有空洞。要解决这个问题我们需要在config.h文件中再添加一行:
#define ACE_LACKS_CDR_ALIGNMENT
这样数据在编码时就会按实际的大小进行紧缩。注意修改了config.h文件后要重新编译ACE。
我们可以看一下CNPv1第四章的例子代码,这里存在一个不够严谨的地方。日志消息的消息头由一个布尔变量指示所用的编码字节序,以及一个 unsigned long指示后面消息体的长度。布尔变量虽然只占1字节,但是由于没有定义上述的宏,编码后也占用了4个。例子中的代码在编码日志消息时消息头和消息体是分别编码的,然后集中发送(使用了sendv_n)。
这里的问题是在代码中写死了消息头的长度为8字节。在缺省情况下布尔和整型都是4字节,没有错。但是一旦定义了上述的宏以后,布尔变量在编码后只占1字节,整个消息头的有效部分就只有5字节了,后面跟着3个字节未定义,然后再是消息体。解码的代码是顺序依次往后解。在定义了上面宏的情况下,没有跳过那3个未定义的字节,因此解出来的消息体都错位了。大家可以在定义了上述宏,并重新编译了ACE后,跑一下那个例子就会发现解码时会出错。
简单的解决方法就是在解码时,解完头后显式的跳过那三个字节。最正确的解决方法是不要假设数据在编码后占的大小,只管顺序的压,顺序的解。只要保证两端用的是同一套编译的ACE库就OK了。
2.2.3 ACE_OutputCDR类使用注意点
到此基本上就达到我们的要求了。但是还有一个值得注意的问题,就是ACE_OutputCDR类会对它使用的buffer进行一次边界调整。我们在后面再描述这个问题。
这次我们来处理边界调整的问题。留意下面的代码片段:
char buf[6] = {0};
ACE_OutputCDR ocdr(buf, 6, ACE_CDR::BYTE_ORDER_BIG_ENDIAN);
ACE_CDR::ULong temp1 = 88;
ACE_CDR::UShort temp2 = 66;
ocdr << temp1;
ocdr << temp2;
int result = peer.sendn(buf, 6);
上面的代码将两个数据压到buf中。这里面有一个隐藏的BUG。第二行用一个buf来构造ACE_OutputCDR对象时,ACE_OutputCDRr构造函数会进行一个复杂的操作。它先用这个buf构造一个ACE_Message_Block,然后对这个 ACE_Message_Block调用ACE_CDR::mb_align方法,进行一次边界调整。
如果刚好buf的起点不在4字节的边界上(不能整除4),则会将ACE_Message_Block的起点后移到4字节对齐的边界上。这会造成两个可能的后果。如果调整的确发生了(假如往后调整了2个字节),那么上述代码最后一行发送的内容,实际上就是错误的,因为错开了2个字节。更为严重的错误是我们的buf刚好是6个字节,我们也写入了6个字节,但是如果ACE_OutputCDR替我们做了一次调整的话,在写入的时候就会越界,破坏堆栈(覆写buf数组后面的两个字节)。
解决的方式有两个。如果是像上述的代码一样,用CDR类来对原始的buffer进行处理,那么可以通过在config.h文件中定义下例的宏来屏蔽对齐行为。
#define ACE_CDR_IGNORE_ALIGNMENT
注意要重新编译ACE。
另外一种方法是,如果是ACE在项目中用得比较普及的话,建议不要直接用原始buffer,改用ACE_Message_Block。实际上对齐时是调整了内部的ACE_Message_Block的base指针。如果总是通过ACE_Message_Block的base方法来得到实际buffer的起始,就不用担心会发生错位。这里唯一要注意的就是要为可能的调整留出空间,避免上面说的溢出。比如下面的代码:
ACE_Message_Block mb(1024 + ACE_CDR::MAX_ALIGNMENT);
我们需要一个1024大小的buffer,但是在实际申请空间时加一个冗余值 ,对齐最大也不可能超过这个冗余,这样就避免了压入数据时引起越界。
ACE进行一次对齐的原因是为了加快内存操作。结合前面“紧缩”部分的描述,我们可以知道,在缺省情况下,ACE在编解码时不但将Buffer的起始外进行对齐处理,里面的数据类型不论大小也是按4字节对齐的。有兴趣的可以看一下MAX_ALIGNMENT这个冗余量的值是8而不是4,我猜可能是为了兼容64位机器。
1.1 字节序
字节序就是超过一个字节的数据类型在内存中的表示方式。
约定的网络字节序采用大端表示法,但是intel架构的PC机,包括现在很多intel芯片的服务器都是用小端表示法。早期的大型机和小型机多用大端表示法。
现在来看,网络字节序采用大端表示法,其实是很不“环保”的。互联网中的绝大多数电脑是用小端表示法,可偏偏选用大端表示法作为网络字节序。这样导致了很多没必要的转换。如果网络字节序采用小端表示法,每天发生在互联网上的字节序转换会少得多,呵呵。
除非你写的是一个封闭的系统,且你可以100%肯定系统内所有机器都使用相同的字节序,否则还是应该坚持使用网络字节序(也就是大端(big-endian)表示法)来进行网络数据的编解码。
一般用htons,htonl进行本地字节序到网络字节序的转换,即(host to net),前者处理short即2字节整数,后者处理long即4字节整数。如果本地字节序也是大端表示法,则这两个方法什么也不做。在有的系统上这两个方法被实现成宏,以节约一次调用开销。于之对应的还有ntohs和ntohl,负责从网络字节序转换到本地字节序。
对于long long类型即8字节整数,就只能自己进行转换,比较麻烦。
1.2 紧缩
紧缩一般是为了避免不同主机间字节对齐的标准不一样,同时还可以节约带宽。比如下面的结构体。
struct a
{
char a_;
long b_;
}
在windows平台用vc编译,默认情况下这个结构体的实例的大小为8个字节,即按4字节进行了对齐。一般对于这样的数据在编码时会进行一次紧缩,紧缩后变为5个字节。解码时再对逐个成员赋值一次,恢复为本地标准。
自行处理这些细节,并不复杂,但很繁琐。而且出了问题调试很麻烦,一般要直接看内存buffer才能定位问题。所以尽量不要自己干这个事。
2. ACE_CDR组件
2.1 功能
ACE中提供了一组CDR类,专用于编解码,但它并不是专门针对我们在前面述情况的编解码情况,所以还必须进行定制。我们下面也按照不同的方面来讨论如果定制及使用ACE的CDR类来做网络数据的编解码。
ACE_InputCDR类负责解码,ACE_OutputCDR类负责编码。这两个类都提供流式操作,使用很方便。唯一不爽的是它不提供链式操作。因为每一个“<<”或“>>”操作符重载函数返回的都是一个布尔变量,不像标准库那样返回表达式左值的引用。
默认情况这两个类不对字节序进行任何转换,也就是总是使用主机字节序。如果两个主机间的字节序不一样,直接使用这两个类在一个主机上编码,再在另一个主机上解码,就会导致出错。CNPv1第四章中的例子通过在每一个消息头前面增加一个布尔变量来指定编码时的字节序,以解决这个问题。编码时首先把本地的主机字节序用一个布尔变量压进去,解码时首先解出这个布尔量,再用这个布尔量来设置ACE_InputCDR的字节序。这样做又麻烦又容易出错。由其不便于异种系统之间的对接。最方便的方法也是通用的方法,就是编解码都使用网络字节序,虽然会因为不必要的转换带来一点效率损失,但处理简单,也便于扩展。
2.2 使用说明
2.2.1 字节序问题
要指定编解码都使用网络字节序,我们可以通过在创建这两个类的对象时进行显示的指定,或是调用reset_byte_order方法进行设置。以创建对象为例:
ACE_OutputCDR ocdr(&mb, ACE_CDR::BYTE_ORDER_BIG_ENDIAN);
ACE_InputCDR icdr(&mb, ACE_CDR::BYTE_ORDER_BIG_ENDIAN);
对于ACE_OutputCDR类,第二个参数指定了要使用大端表示法对数据进行编码。对于ACE_InputCDR类,第二个参数指定了待解码的数据是用大端表示法编码的。
要注意的是,光有这样的指定还不够。缺省用ACE_OutputCDR进行编码时,它并不对写入的数据进行大小端转换,即使做了上述的指定。
解决方法是,在编译ACE时,在config.h文件中加下下面的行。
#define ACE_ENABLE_SWAP_ON_WRITE
这样就可以了。CDR类会自动的处理是否需要进行转换。
还有一点要注意的,BYTE_ORDER_BIG_ENDIAN这个枚举在较早的的ACE版本中没有。5.6.5及以前版本中是没有的,5.6.7中有,5.6.6我没有用过。如果你用的是较早版本,可以直接用硬编码的数字。
BYTE_ORDER_BIG_ENDIAN = 0;
BYTE_ORDER_LITTLE_ENDIAN = 1;
2.2.2 紧缩问题
在缺省情况下,ACE_InputCDR和ACE_OutputCDR这两个类是不处理紧缩的,统一按4字节对齐。即小于4字节的数据类型压进去后都占4个字节。这样做主要是处于效率的考虑。32位的CPU在处理按4字节对齐的数据时最快,不需要再处理额外的偏移。但这样明显不符合我们前面描述的情况,我们不希望编码后的数据f中留有空洞。要解决这个问题我们需要在config.h文件中再添加一行:
#define ACE_LACKS_CDR_ALIGNMENT
这样数据在编码时就会按实际的大小进行紧缩。注意修改了config.h文件后要重新编译ACE。
我们可以看一下CNPv1第四章的例子代码,这里存在一个不够严谨的地方。日志消息的消息头由一个布尔变量指示所用的编码字节序,以及一个 unsigned long指示后面消息体的长度。布尔变量虽然只占1字节,但是由于没有定义上述的宏,编码后也占用了4个。例子中的代码在编码日志消息时消息头和消息体是分别编码的,然后集中发送(使用了sendv_n)。
这里的问题是在代码中写死了消息头的长度为8字节。在缺省情况下布尔和整型都是4字节,没有错。但是一旦定义了上述的宏以后,布尔变量在编码后只占1字节,整个消息头的有效部分就只有5字节了,后面跟着3个字节未定义,然后再是消息体。解码的代码是顺序依次往后解。在定义了上面宏的情况下,没有跳过那3个未定义的字节,因此解出来的消息体都错位了。大家可以在定义了上述宏,并重新编译了ACE后,跑一下那个例子就会发现解码时会出错。
简单的解决方法就是在解码时,解完头后显式的跳过那三个字节。最正确的解决方法是不要假设数据在编码后占的大小,只管顺序的压,顺序的解。只要保证两端用的是同一套编译的ACE库就OK了。
2.2.3 ACE_OutputCDR类使用注意点
到此基本上就达到我们的要求了。但是还有一个值得注意的问题,就是ACE_OutputCDR类会对它使用的buffer进行一次边界调整。我们在后面再描述这个问题。
这次我们来处理边界调整的问题。留意下面的代码片段:
char buf[6] = {0};
ACE_OutputCDR ocdr(buf, 6, ACE_CDR::BYTE_ORDER_BIG_ENDIAN);
ACE_CDR::ULong temp1 = 88;
ACE_CDR::UShort temp2 = 66;
ocdr << temp1;
ocdr << temp2;
int result = peer.sendn(buf, 6);
上面的代码将两个数据压到buf中。这里面有一个隐藏的BUG。第二行用一个buf来构造ACE_OutputCDR对象时,ACE_OutputCDRr构造函数会进行一个复杂的操作。它先用这个buf构造一个ACE_Message_Block,然后对这个 ACE_Message_Block调用ACE_CDR::mb_align方法,进行一次边界调整。
如果刚好buf的起点不在4字节的边界上(不能整除4),则会将ACE_Message_Block的起点后移到4字节对齐的边界上。这会造成两个可能的后果。如果调整的确发生了(假如往后调整了2个字节),那么上述代码最后一行发送的内容,实际上就是错误的,因为错开了2个字节。更为严重的错误是我们的buf刚好是6个字节,我们也写入了6个字节,但是如果ACE_OutputCDR替我们做了一次调整的话,在写入的时候就会越界,破坏堆栈(覆写buf数组后面的两个字节)。
解决的方式有两个。如果是像上述的代码一样,用CDR类来对原始的buffer进行处理,那么可以通过在config.h文件中定义下例的宏来屏蔽对齐行为。
#define ACE_CDR_IGNORE_ALIGNMENT
注意要重新编译ACE。
另外一种方法是,如果是ACE在项目中用得比较普及的话,建议不要直接用原始buffer,改用ACE_Message_Block。实际上对齐时是调整了内部的ACE_Message_Block的base指针。如果总是通过ACE_Message_Block的base方法来得到实际buffer的起始,就不用担心会发生错位。这里唯一要注意的就是要为可能的调整留出空间,避免上面说的溢出。比如下面的代码:
ACE_Message_Block mb(1024 + ACE_CDR::MAX_ALIGNMENT);
我们需要一个1024大小的buffer,但是在实际申请空间时加一个冗余值 ,对齐最大也不可能超过这个冗余,这样就避免了压入数据时引起越界。
ACE进行一次对齐的原因是为了加快内存操作。结合前面“紧缩”部分的描述,我们可以知道,在缺省情况下,ACE在编解码时不但将Buffer的起始外进行对齐处理,里面的数据类型不论大小也是按4字节对齐的。有兴趣的可以看一下MAX_ALIGNMENT这个冗余量的值是8而不是4,我猜可能是为了兼容64位机器。
相关阅读 更多 +