Flamingo 網路協議對整型欄位的處理方法介紹
- 2019 年 11 月 20 日
- 筆記
在開源即時通訊軟體 Flamingo,

github 地址:
https://github.com/balloonwj/flamingo,
其通訊協議中有如下程式碼:
//flamingoserver/net/ProtocolStream.cpp //將一個四位元組的整形數值壓縮成1~5個位元組 bool compress_(unsigned int i, char *buf, size_t &len) { len = 0; for (int a = 4; a >= 0; a--) { char c; c = i >> (a * 7) & 0x7f; if (c == 0x00 && len == 0) continue; if (a == 0) c &= 0x7f; else c |= 0x80; buf[len] = c; len++; } if (len == 0) { len++; buf[0] = 0; } //cout << "compress:" << i << endl; //cout << "compress len:" << len << endl; return true; } //將一個1~5個位元組的值還原成四位元組的整形值 bool uncompress_(char *buf, size_t len, unsigned int &i) { i = 0; for (int index = 0; index < (int)len; index++) { char c = *(buf + index); i = i << 7; c &= 0x7f; i |= c; } //cout << "uncompress:" << i << endl; return true; }
(可左右滑動)
不知道讀者是否可以看的明白?
其實這是網路通訊協議中對整型數值處理的常用手段,請聽我慢慢道來。
在實際設計協議時,整型數值(如 int32、int64)在協議內容中出現非常高,以上面介紹的 TLV 格式為例,L 代表每個欄位的長度,假設用一個 int32 類型表示,int32 占 4 個位元組,對於無符號的 int32 類型來說,其可表示的範圍為 0 ~ 4294967295,實際用途中,我們不會用到太長的欄位值,因此可以根據欄位實際的 length 值使用 1 ~ n 個位元組表示這個 int32 值。
在實際處理中,一個位元組(Byte)的共有 8 位(bit),該位元組的最高位我們用來作為標誌位,用於說明一個整型數值是否到此位元組結束,如果某個位元組的最高位為 0 表示該整型值的內容到此位元組結束,最高位為 1 表示表示下一個位元組仍然是該整型值的內容。說有的有點抽象,我們來看一個具體的例子。假設有個在一串位元組流中,存在如下二進位數字表示某個整型值:
第1個位元組 第2個位元組 第3個位元組 第4個位元組 10111011 11110000 01110000 11110111 ...其他省略...
(可左右滑動)
如上圖所示,第一個位元組是 10111011
,其最高位為 1,說明其下一個位元組仍然屬於表示長度的序列,下一個位元組是第二個位元組 11110000
,其最高位仍然是 1,再看第三個位元組的內容 01110000
,第三個位元組的最高位是 0,因此表示這個整數的位元組序列到此就結束了。假定我們壓縮時的順序是低位內容位元組在記憶體地址較小的地方,高位內容在記憶體較大的地方,其值是:
第3個位元組 第2個位元組 第1個位元組 1110000 1110000 0111011 => 11100 00111000 00111011
(可左右滑動)
11100 00111000 00111011
轉換成十進位為 1849403
。
使用上述技巧進行壓縮的整型,由於一個位元組只使用低 7 位(最高位為標誌位,一般稱為「位元組前導位」),一個 int32 的整型共 4 個位元組(4 * 8 = 32)位,因此一個 int32 使用上述方法進行壓縮其長度可能是 1 ~ 5 個位元組。實際協議中,我們基本上很少遇到使用超過 3 個位元組以上長度,因此這種壓縮還是比較實用的(節省空間)。
有了上面的分析,對於一個無符號 int32 的整型的壓縮演算法如下,以下程式碼節選自 POCO C++ 庫,程式碼格式略有調整:
1 //poco-masterFoundationsrcBinaryWriter.cpp 2 //將一個 uint32 壓縮成 1 ~ 5 個位元組的演算法 3 void BinaryWriter::write7BitEncoded(UInt32 value) 4 { 5 do 6 { 7 unsigned char c = (unsigned char) (value & 0x7F); 8 value >>= 7; 9 if (value) 10 c |= 0x80; 11 12 _ostr.write((const char*) &c, 1); 13 } 14 while (value); 15 }
(可左右滑動)
上述程式碼=對一個 uint32_t 整型 value 從低到高每次取 7 bit,判斷下 value 的值在去掉 7 bit 後是否有剩餘(非 0 有則說明有剩餘,程式碼第 8 和 9 行),如果有在當前位元組最高 bit 設置標誌位值為 1,這樣得到一個位元組的值後,放入位元組流容器中 _ostr,位元組流容器的類型只要是連續的記憶體序列即可,如 std::string。
我們假設現在 value 的值是十進位 125678,其二進位是 1 1110 1010 1110 1110
,我們來看一下上述函數執行過程:
第一次循環
十六進位 0x7F 的二進位為 0111 1111
,執行
unsigned char c = (unsigned char) (value & 0x7F);
(可左右滑動)
後 c = 110(十進位),二進位是 0110 1110
,接著將 value 右移 7 bit,看看還有沒有剩餘(與 0 判斷),此時 value 變為 981(十進位),程式碼第 9 行 if 條件為真,說明一個位元組表示不了這個數值,給算出的位元組 c 最高位設置標誌 1(與 0x80 做與運算,0x80 的二進位是 1000 0000
,程式碼第 **10 ** 行),得到第一個位元組值 238(十進位)。
第二次循環
c 開始等於 85(十進位),執行程式碼 7、8 行後,發現 value 的值仍有剩餘,再次在該位元組的高位設置標誌 1,得到第二個位元組值 213(十進位)。
第三次循環
c 開始等於 7,執行程式碼 7、8 行後,發現 value 的值已經沒有剩餘,得到第三個位元組值 7,然後退出循環。
程式執行過程如下圖所示:

在理解了整型的壓縮演算法,其對應的解壓演算法也很容易弄明白了,程式碼如下,同樣節選自 POCO C++ 庫,程式碼格式略有調整:
1 //poco-masterFoundationsrcBinaryReader.cpp 2 //將一個位元組流中 1 ~ 5 個位元組的還原成一個 uint32 整型 3 void BinaryReader::read7BitEncoded(UInt32& value) 4 { 5 char c; 6 value = 0; 7 int s = 0; 8 do 9 { 10 c = 0; 11 _istr.read(&c, 1); 12 UInt32 x = (c & 0x7F); 13 x <<= s; 14 value += x; 15 s += 7; 16 } 17 while (c & 0x80); 18 }
(可左右滑動)
上述程式碼從位元組流容器 _istr 中挨個讀取一個位元組 ,將當前位元組與 0x7F 進行與運算,以取得該位元組的低 7 位內容(程式碼 12 行),然後再將位元組內容與 0x80 進行與運算,以判斷該位元組的最高位是否為 1 進而進一步確定下一個位元組是不是也屬於整型值的內容。
同樣的道理,對於 uint64 位的整型數值,我們可以將其壓縮成 1 ~ 10 個位元組大小的位元組數組,其壓縮和解壓演算法與 uint32 位整型值一樣,這裡就不再貼出來了。