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