7.4. Serialization:如何封装数据
要将文字数据透过网路传送很简 单,你已经知道了,不过如果你想要送一些 "二进制" 的数据,如 int 或 float,会发生什麽事情呢?这里有一些选择。
1. 将数字转换为文字,使用如 sprintf() 的函数,接着传送文字。接收者会使用如 strtol() 函数解析文字,并转换为数字。
2. 直接以原始数据传送,将指向数据的指针传递给 send()。
3. 将数字编码(encode)为可移植的二进制格式,接收者会将它译码(decode)。
先睹为快!只在今晚!
[序幕]
Beej 说:"我偏好上面的第三个方法!"
[结束]
(在我热切开始本章节之前,我应该要跟你说有现成的程序库可以做这件事情,而要自制个可移植及无错误的作品会是相当大的挑战。所以在决定要自己实作这部分时,可以先四处看看,并做完你的家庭作业。我在这里引用些类似这个作品的有趣的资料。)
实际上,上面全部的方法都有它们的缺点与优点,但是如我所述,通常我偏好第三个方法。首先,咱们先谈谈另外两个的优缺点。
第一个方法,在传送以前先将数字编码为文字,优点是你可以很容易打印出及读取来自网路的数据。有时,人类易读的协定比较适用於频带不敏感(non-bandwidth-intensive)的情况,例如:Internet Relay Chat(IRC)[27]。然而,缺点是转换耗时,且总是需要比原本的数字使用更多的空间。
第二个方法:传送原始数据(raw data),这个方法相当简单[但是危险!]:只要将数据指针提供给 send()。
double d = 3490.15926535;
send(s, &d, sizeof d, 0); /* 危险,不具可移植性! */
接收者类似这样接收:
double d;
recv(s, &d, sizeof d, 0); /* 危险,不具可移植性! */
快速又简单,那有什麽不好的呢?
好的,事实证明不是全部的架构都能表示 double[或 int]。[嘿!或许你不需要可移植性,在这样的情况下 这个方法很好,而且快速。]
当封装整数型别时,我们已经知道 htons() 这类的函数如何透过将数字转换为 Network Byte Order(网路字节顺序),来让东西可以移植。毫无疑问地,没有类似的函数可以供 float 型别使用,全部的希望都落空了吗?
别怕![你有担心了一会儿吗?没有吗?一点都没有吗?]
我们可以做件事情:我们可以将数据封装为接收者已知的二进制格式,让接收着可以在远端解压。
我所谓的 "已知的二进制格式"是什麽意思呢?
好的,我们已经看过了 htons() 范例了,不是吗?它将数字从 host 格式改变[或是 "编码"]为 Network Byte Order 格式;如果要反转[译码]这个数字,接收端会调用 ntohs()。
可是我不是才刚说过,没有这样的函数可供非整数型别使用吗?
是的,我说过。而且因为 C 语言并没有规范标准的方式来做,所以这有点麻烦[that a gratuitous pun there for you Python fans]。
要做的事情是将数据封装到已知的格式,并透过网路送出。例如:封装 float,这里的东西有很大的改善空间:[28]
#include <stdint.h>
uint32_t htonf(float f)
{
uint32_t p;
uint32_t sign;
if (f < 0) { sign = 1; f = -f; }
else { sign = 0; }
p = ((((uint32_t)f)&0x7fff)<<16) | (sign<<31); // whole part and sign
p |= (uint32_t)(((f - (int)f) * 65536.0f))&0xffff; // fraction
return p;
}
float ntohf(uint32_t p)
{
float f = ((p>>16)&0x7fff); // whole part
f += (p&0xffff) / 65536.0f; // fraction
if (((p>>31)&0x1) == 0x1) { f = -f; } // sign bit set
return f;
}
上列的代码是一个 native(原生的)实作,将 float 储存为 32-bit 的数字。High bit(高比特)[31]用来储存数字的正负号["1"表示负数],而接下来的七个比特[30-16]是用来储存 float 整个数字的部分。最後,剩下的比特[15-0]用来储存数字的小数(fractional portion)部分。
使用方式相当直觉:
#include <stdio.h>
int main(void)
{
float f = 3.1415926, f2;
uint32_t netf;
netf = htonf(f); // 转换为 "network" 形式
f2 = ntohf(netf); // 转回测试
printf("Original: %f\n", f); // 3.141593
printf(" Network: 0x%08X\n", netf); // 0x0003243F
printf("Unpacked: %f\n", f2); // 3.141586
return 0;
}
好处是:它很小丶很简单且快速,缺点是:它 在空间的使用没有效率,而且对范围有严格的限制-试着在那边储存一个大於 32767 的数,它就会不高兴!
你也可以在上面的例子看到,最後一对的十进位空间并没有正确保存。
我们该怎麽改呢?
好的,用来储存浮点数(float point number)的标准方式是已知的 IEEE-754 [29]。多数的电脑会在内部使用这个格式做浮点运算,所以在这些例子里,严格说来,不需要做转换。但是如果你想要你的代码具可移植性,就要假设你不需要转换。[换句话说,如果你想要让程序很快,你应该要在不需要做转换的平台上进行最佳化!这就是 htons() 与它的家族使用的方法。]
这边有段代码可以将 float 与 double 编码为 IEEE-754 格式 [30]。[主要的功能,它不会编码 NaN 或 Infinity,不过可以将它改成可以。]
#define pack754_32(f) (pack754((f), 32, 8))
#define pack754_64(f) (pack754((f), 64, 11))
#define unpack754_32(i) (unpack754((i), 32, 8))
#define unpack754_64(i) (unpack754((i), 64, 11))
uint64_t pack754(long double f, unsigned bits, unsigned expbits)
{
long double fnorm;
int shift;
long long sign, exp, significand;
unsigned significandbits = bits - expbits - 1; // -1 for sign bit
if (f == 0.0) return 0; // get this special case out of the way
// 检查正负号并开始正规化
if (f < 0) { sign = 1; fnorm = -f; }
else { sign = 0; fnorm = f; }
// 取得 f 的正规化型式并追踪指数
shift = 0;
while(fnorm >= 2.0) { fnorm /= 2.0; shift++; }
while(fnorm < 1.0) { fnorm *= 2.0; shift--; }
fnorm = fnorm - 1.0;
// 计算有效位数数据的二进制格式(非浮点数)
significand = fnorm * ((1LL<<significandbits) + 0.5f);
// get the biased exponent
exp = shift + ((1<<(expbits-1)) - 1); // shift + bias
// 返回最後的解答
return (sign<<(bits-1)) | (exp<<(bits-expbits-1)) | significand;
}
long double unpack754(uint64_t i, unsigned bits, unsigned expbits)
{
long double result;
long long shift;
unsigned bias;
unsigned significandbits = bits - expbits - 1; // -1 for sign bit
if (i == 0) return 0.0;
// pull the significand
result = (i&((1LL<<significandbits)-1)); // mask
result /= (1LL<<significandbits); // convert back to float
result += 1.0f; // add the one back on
// deal with the exponent
bias = (1<<(expbits-1)) - 1;
shift = ((i>>significandbits)&((1LL<<expbits)-1)) - bias;
while(shift > 0) { result *= 2.0; shift--; }
while(shift < 0) { result /= 2.0; shift++; }
// sign it
result *= (i>>(bits-1))&1? -1.0: 1.0;
return result;
}
我在那里的顶端放一些方便的 macro 用来封装与解除封装 32-bit[或许是 float]与 64-bit[或许是 double]的数字,但是 pack754() 函数可以直接调用,并告知编码几个比特的数据[expbits 的哪几个比特要保留给正规化数值的指数。]
这里是使用范例:
#include <stdio.h>
#include <stdint.h> // 定义 uintN_t 型别
#include <inttypes.h> // 定义 PRIx macros
int main(void)
{
float f = 3.1415926, f2;
double d = 3.14159265358979323, d2;
uint32_t fi;
uint64_t di;
fi = pack754_32(f);
f2 = unpack754_32(fi);
di = pack754_64(d);
d2 = unpack754_64(di);
printf("float before : %.7f\n", f);
printf("float encoded: 0x%08" PRIx32 "\n", fi);
printf("float after : %.7f\n\n", f2);
printf("double before : %.20lf\n", d);
printf("double encoded: 0x%016" PRIx64 "\n", di);
printf("double after : %.20lf\n", d2);
return 0;
}