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 有則說明有剩餘,程式碼第 89 行),如果有在當前位元組最高 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(十進位),執行程式碼 78 行後,發現 value 的值仍有剩餘,再次在該位元組的高位設置標誌 1,得到第二個位元組值 213(十進位)。

第三次循環

c 開始等於 7,執行程式碼 78 行後,發現 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 位整型值一樣,這裡就不再貼出來了。