二進制編碼傳輸協(xié)議(轉(zhuǎn)載,僅作記錄)

轉(zhuǎn)載自https://segmentfault.com/a/1190000022356844

二進制編碼傳輸協(xié)議

思考

  1. 何為二進制協(xié)議傳輸,何為文本協(xié)議數(shù)據(jù)傳輸?
  2. 網(wǎng)絡(luò)編程中數(shù)據(jù)協(xié)議的制定方式有哪些?
  3. Protobuf 等二進制數(shù)據(jù)序列化傳輸協(xié)議的機制是什么?

在網(wǎng)絡(luò)編程中,經(jīng)??吹揭髷?shù)據(jù)要以二進制的方式進行傳輸,起初我很不理解,為什么要刻意的說明二進制方式呢?數(shù)據(jù)在底層的傳輸不都是二進制流嗎?而且還引出了 pack/unpack 方法簇。

我們經(jīng)常用到的 rpc,比如 json-rpc 是以 文本方式 傳輸序列化的數(shù)據(jù)的。grpc(protobuf), thrift 都是以 二進制方式 傳輸數(shù)據(jù)的。所以到底何為二進制傳輸呢?

大家可以先想一下日常中發(fā)送請求時經(jīng)常用到的方式: xml, json, formData,他們雖然格式不同,但都有一個特征,自帶描述信息(直白說就是攜帶 參數(shù)名),像 文本 一樣,能很直觀的看到數(shù)據(jù)表征的內(nèi)容。

如果我們事先定義了數(shù)據(jù)中的n~m個字節(jié)位固定作為某參數(shù)的數(shù)據(jù)段,就可以免去 參數(shù)名 所帶來的額外開銷。比如 0 ~ 10 字節(jié)為 account,11 ~ 24 字節(jié)為 passowrd。又因為用戶名或密碼是非定長的,而解析數(shù)據(jù)時又要根據(jù)字節(jié)位精準(zhǔn)的截取,所以我們需要對數(shù)據(jù)項進行打包填充,使其固定字節(jié)長度,而后在服務(wù)端進行解包,pack/unpack便可以實現(xiàn)此功能。

白話

tcp 協(xié)議是日常中最為常見的二進制協(xié)議,協(xié)議體的字節(jié)位都有約定好的表征。

http 在廣義上來說也是二進制模式,使用 \r\n 對協(xié)議進行解包,解析,但 http 攜帶的數(shù)據(jù)通常都是文本模式的,比如 "sqrtcat" 占了 7 個字節(jié),在文本 or 二進制模式下沒什么區(qū)別,但"29",以文本模式發(fā)送需要 2bytes,以二進制模式打包至字符類型,只需要 1bytes。

二進制為何能提高數(shù)據(jù)傳輸效率:

  1. 根據(jù)協(xié)議約定,省去參數(shù)名所占用的字節(jié),縮減了數(shù)據(jù)。
  2. 將數(shù)值類型的數(shù)據(jù)打包至相應(yīng)范圍內(nèi)的二進制,節(jié)省了空間,4bytes能表示 32 位的文本數(shù)值,但文本數(shù)據(jù)值要 32bytes。
  3. 在一定程度上可以起到加密數(shù)據(jù)的作用,如果第三方不知道數(shù)據(jù)協(xié)議,就沒有辦法截取相應(yīng)的字節(jié)為獲取數(shù)據(jù),或得到數(shù)據(jù)的表征。

文本方式傳輸

日常開發(fā),比如發(fā)送一個用戶注冊 http協(xié)議 請求,發(fā)送的數(shù)據(jù)格式分別如下:

<pre class="php hljs language-php" style="box-sizing: border-box; font-family: var(--bs-font-monospace); font-size: 0.875em; direction: ltr; unicode-bidi: bidi-override; display: block; margin-top: 0px !important; margin-bottom: 1.25rem; overflow: auto; color: rgb(36, 41, 46); background: rgb(233, 236, 239); padding: 1rem; max-height: 35rem; line-height: 1.5; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;">$registerData = [
"account" => "sqrtcat",
"password" => "123456"
];</pre>

formData 31bytes

<pre class="bash hljs language-bash" style="box-sizing: border-box; font-family: var(--bs-font-monospace); font-size: 0.875em; direction: ltr; unicode-bidi: bidi-override; display: block; margin-top: 0px !important; margin-bottom: 1.25rem; overflow: auto; color: rgb(36, 41, 46); background: rgb(233, 236, 239); padding: 1rem; max-height: 35rem; line-height: 1.5; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;">account=sqrtcat&password=123456</pre>

json 41bytes

<pre class="json hljs language-json" style="box-sizing: border-box; font-family: var(--bs-font-monospace); font-size: 0.875em; direction: ltr; unicode-bidi: bidi-override; display: block; margin-top: 0px !important; margin-bottom: 1.25rem; overflow: auto; color: rgb(36, 41, 46); background: rgb(233, 236, 239); padding: 1rem; max-height: 35rem; line-height: 1.5; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;">{"account":"sqrtcat","password":"123456"}</pre>

xml 94bytes

<pre class="xml hljs language-xml" style="box-sizing: border-box; font-family: var(--bs-font-monospace); font-size: 0.875em; direction: ltr; unicode-bidi: bidi-override; display: block; margin-top: 0px !important; margin-bottom: 1.25rem; overflow: auto; color: rgb(36, 41, 46); background: rgb(233, 236, 239); padding: 1rem; max-height: 35rem; line-height: 1.5; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;"><?xml version="1.0" encoding="UTF-8" ?>
<account>sqrtcat</account>
<password>123456</password></pre>

以上三種皆為,我們可以很直觀的在數(shù)據(jù)體重得到各項參數(shù)。

二進制傳輸方式

二進制傳輸,離不開協(xié)議的制定。文本方式傳輸?shù)臄?shù)據(jù)可以自我描述,而二進制方式傳輸?shù)臄?shù)據(jù),需要通過協(xié)議進行解析和讀取。

最簡單的,參數(shù)定長的方式,account 固定為 11 位,password 固定為 14 位,使用 pack將數(shù)據(jù)填充至相應(yīng)的協(xié)議長度,發(fā)送,服務(wù)端按協(xié)議進行字節(jié)長度的截取獲得對應(yīng)的參數(shù)值。

<pre class="php hljs language-php" style="box-sizing: border-box; font-family: var(--bs-font-monospace); font-size: 0.875em; direction: ltr; unicode-bidi: bidi-override; display: block; margin-top: 0px !important; margin-bottom: 1.25rem; overflow: auto; color: rgb(36, 41, 46); background: rgb(233, 236, 239); padding: 1rem; max-height: 35rem; line-height: 1.5; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;"><?php
// binary protocal:
// |-- 11 bytes account --|-- 14 bytes password --|
account = "sqrtcat";password = "123456";

// pack
// A 以空白符對數(shù)據(jù)進行填充 php 解包時會自動 trim 掉
// a 以 0x00 字符對數(shù)據(jù)進行填充 php 解包時會保留 0x00
dataBin = pack("A11A14",account, $password);

// send
echo "data pack to bin len: " . strlen(dataBin) . PHP_EOL; echo "data pack to bin: " .dataBin . PHP_EOL;

// unpack
dataArr = unpack("A11account/A14password",dataBin);
var_dump($dataArr);

// result
data pack to bin len: 25
data pack to bin: sqrtcat 123456
array(2) {
["account"]=>
string(7) "sqrtcat"
["password"]=>
string(6) "123456"
}</pre>

對比文本方式發(fā)送,我們在協(xié)議和二進制傳輸?shù)姆绞较?,只用?25bytes。這就滿足了?并不能夠~,這種簡單協(xié)議的二進制傳輸方式只是在一定場景下發(fā)揮了傳輸效率,在某些場景下可能還不如文本方式。因為嚴格的數(shù)據(jù)定長填充,可能會造成數(shù)據(jù)的冗余,比如 account只有一個字符 s,password 也只有一個字符 1,在此協(xié)議下還是固定 25bytes,文本傳輸反而效率會高一些。

二進制傳輸敗北了?No,是我們協(xié)議太簡單,不夠靈活,沒有最大程度上發(fā)揮協(xié)議+二進制的高效性,可以說,協(xié)議下的二進制傳輸方式,能做到絕對的高效于文本傳輸,這里我們可以簡單的分析和模擬以二進制方式傳輸?shù)?protobuf 的協(xié)議模式。

Protobuf 的二進制傳輸

我們可以簡單分析下 protobuf傳輸數(shù)據(jù)的方式:

  1. 定義 IDL,其實就相當(dāng)于制定了協(xié)議體
  2. 生成 proto 文件,得到具體的消息字段的 參數(shù)項位參數(shù)長度位 映射的消息協(xié)議包。
  3. 發(fā)送端根據(jù)消息協(xié)議定義的參數(shù)數(shù)據(jù)類型(主要是變長 or 定長),將數(shù)據(jù)打包至相應(yīng)的二進制格式。
  4. 發(fā)送數(shù)據(jù)。
  5. 接收端按消息協(xié)議格式對二進制數(shù)據(jù)進行解析,獲得文本數(shù)據(jù)。

這里原諒我自己造了兩個詞,參數(shù)項位參數(shù)長度位,如何理解呢?通過下面模仿 protobuf 的協(xié)議示例來理解。

定義消息體的IDL

<pre class="protobuf hljs language-protobuf" style="box-sizing: border-box; font-family: var(--bs-font-monospace); font-size: 0.875em; direction: ltr; unicode-bidi: bidi-override; display: block; margin-top: 0px !important; margin-bottom: 1.25rem; overflow: auto; color: rgb(36, 41, 46); background: rgb(233, 236, 239); padding: 1rem; max-height: 35rem; line-height: 1.5; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;">message RegisterRequest {
string account = 1; // 數(shù)據(jù)位1 type string name account
string password = 2; // 數(shù)據(jù)位2 type string name password
tinyint age = 3; // 數(shù)據(jù)位3 type tinyint name age
}</pre>

注意下面是我自己仿 protobuf 寫的一套 php 二進制序列化組件,完整版已放置 github 支持的數(shù)據(jù)類型還是很全面的protoBin。

協(xié)議數(shù)據(jù)類型的二進制格式約定

主要是定義哪些類型是定長,哪些類型是變長,變長類型還需給定長度位的字節(jié)數(shù)。

<pre class="php hljs language-php" style="box-sizing: border-box; font-family: var(--bs-font-monospace); font-size: 0.875em; direction: ltr; unicode-bidi: bidi-override; display: block; margin-top: 0px !important; margin-bottom: 1.25rem; overflow: auto; color: rgb(36, 41, 46); background: rgb(233, 236, 239); padding: 1rem; max-height: 35rem; line-height: 1.5; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;"><?php
/**

  • 協(xié)議數(shù)據(jù)類型

  • //| 參數(shù)位1(變長數(shù)據(jù)) | 參數(shù)位2(定長類型) | 參數(shù)位3(變長數(shù)據(jù)) |

  • //| param1Len | param1Data | param3Data | param3Len | param3Data |
    */
    class ProtocolType {
    const TYPE_TINYINT = 'tinyint';
    const TYPE_INT16 = 'int16';
    const TYPE_INT32 = 'int32';
    const TYPE_INT64 = 'int64';
    const TYPE_STRING = 'string';
    const TYPE_TEXT = 'text';

    /**

    • 數(shù)據(jù)類型是否為定長
      */
      const TYPE_FIXED_LEN = [
      self::TYPE_TINYINT => true,
      self::TYPE_INT16 => true,
      self::TYPE_INT32 => true,
      self::TYPE_INT64 => true,
      self::TYPE_STRING => false,
      self::TYPE_TEXT => false,
      ];

    // 定長數(shù)據(jù)類型的字節(jié)數(shù) paramBytes = dataBytes
    const TYPE_FIXED_LEN_BYTES = [
    self::TYPE_TINYINT => 1, // tinyint 固定1字節(jié) 不需要長度表征 追求極致
    self::TYPE_INT16 => 2, // int16 固定2字節(jié) 不需要長度表征 追求極致
    self::TYPE_INT32 => 4, // int32 固定4字節(jié) 不需要長度表征 追求極致
    self::TYPE_INT64 => 8, // int64 固定8字節(jié) 不需要長度表征 追求極致
    ];

    /**

    • 變長數(shù)據(jù)類型長度位字節(jié)數(shù) paramBytes = dataLenBytes . dataBytes
      */
      const TYPE_VARIABLE_LEN_BYTES = [
      self::TYPE_STRING => 1, // string 用 1bytes 表征數(shù)據(jù)長度 0 ~ 255 個字符長度
      self::TYPE_TEXT => 4, // text 用 4bytes 表征數(shù)據(jù)長度 能表征 2 ^ 32 - 1個字符長度 1PB的數(shù)據(jù) 噗
      ];

    /**

    • 數(shù)據(jù)類型對應(yīng)的打包方式
      */
      const TYPE_PACK_SYMBOL = [
      self::TYPE_TINYINT => 'C', // tinyint 固定1字節(jié) 不需要長度表征 追求極致 無符號字節(jié)
      self::TYPE_INT16 => 'n', // int16 固定2字節(jié) 不需要長度表征 追求極致 大端無符號短整形
      self::TYPE_INT32 => 'N', // int32 固定4字節(jié) 不需要長度表征 追求極致 大端無符號整形
      self::TYPE_INT64 => 'J', // int64 固定8字節(jié) 不需要長度表征 追求極致 大端無符號長整形
      self::TYPE_STRING => 'C', // string 用 1bytes 表征數(shù)據(jù)長度 0 ~ 255 個字符長度
      self::TYPE_TEXT => 'N', // text 用 4bytes 表征數(shù)據(jù)長度 能表征 2 ^ 32 - 1個字符長度 1PB的數(shù)據(jù) 噗
      ];

    /**

    • 是否為定長類型
    • @param [type] $type [description]
    • @return boolean [description]
      */
      public static function isFixedLenType(type) { return self::TYPE_FIXED_LEN[type];
      }

    /**

    • 定長獲得字節(jié)數(shù)
    • 變長獲得數(shù)據(jù)長度為字節(jié)數(shù)
    • @param [type] $type [description]
    • @return [type] [description]
      */
      public static function getTypeOrTypeLenBytes(type) { if (self::isFixedLenType(type)) {
      return self::TYPE_FIXED_LEN_BYTES[type]; } else { return self::TYPE_VARIABLE_LEN_BYTES[type];
      }
      }

    /**

    • 打包二進制數(shù)據(jù)

    • @param [type] $data [description]

    • @param [type] $paramType [description]

    • @return [type] [description]
      */
      public static function pack(data,paramType) {
      packSymbol = self::TYPE_PACK_SYMBOL[paramType];
      if (self::isFixedLenType(paramType)) { // 定長類型 直接打包數(shù)據(jù)至相應(yīng)的二進制paramProtocDataBin = pack(packSymbol,data);
      } else {
      // 變長類型 數(shù)據(jù)長度位 + 數(shù)據(jù)位
      paramProtocDataBin = pack(packSymbol, strlen(data)) .data;
      }

      return $paramProtocDataBin;
      }

    /**

    • 解包二進制數(shù)據(jù)

    • @param [type] &$dataBin [description]

    • @param [type] $paramType [description]

    • @return [type] [description]
      */
      public static function unPack(&dataBin,paramType) {
      packSymbol = self::TYPE_PACK_SYMBOL[paramType];

      // 定長數(shù)據(jù)直接讀取對應(yīng)的字節(jié)數(shù)解包
      if (self::isFixedLenType(paramType)) { // 參數(shù)的字節(jié)數(shù)paramBytes = self::TYPE_FIXED_LEN_BYTES[paramType];paramBin = substr(dataBin, 0,paramBytes);
      // 定長類型 直接打包數(shù)據(jù)至相應(yīng)的二進制
      paramData = unpack(packSymbol, paramBin)[1]; } else { // 類型的長度位字節(jié)數(shù)typeLenBytes = self::TYPE_VARIABLE_LEN_BYTES[paramType]; // 數(shù)據(jù)長度位paramLenBytes = substr(dataBin, 0,typeLenBytes);
      // 解析二進制的數(shù)據(jù)長度
      paramDataLen = unpack(packSymbol, paramLenBytes)[1]; // 讀取變長的數(shù)據(jù)內(nèi)容paramData = substr(dataBin,typeLenBytes, paramDataLen); // 參數(shù)項的總字節(jié)數(shù)paramBytes = typeLenBytes +paramDataLen;
      }

      // 剩余待處理的數(shù)據(jù)
      dataBin = substr(dataBin, $paramBytes);

      return $paramData;
      }
      }

/**

  • 協(xié)議消息體
    /
    class ProtocolMessage {
    /
    *

    • 二進制協(xié)議流
    • @var [type]
      */
      public $dataBin;

    /**

    • [paramName1, paramName2, paramName3]
    • @var array
      */
      public static $paramNameMapping = [];

    /**

    • paramName => ProtocolType
    • @var array
      */
      public static $paramProtocolTypeMapping = [];

    /**

    • 獲取參數(shù)的協(xié)議數(shù)據(jù)類型
    • @param [type] $param [description]
    • @return [type] [description]
      */
      public static function getParamType(param) { return static::paramProtocolTypeMapping[$param];
      }

    /**

    • 按參數(shù)位序依次打包

    • @return [type] [description]
      */
      public function packToBinStream() {
      // 按參數(shù)位序
      foreach (static::paramNameMapping askey => paramName) {this->dataBin .= this->{paramName . 'Bin'};
      }

      return $this->dataBin;
      }

    /**

    • 按參數(shù)位序一次解包
    • @param [type] $dataBin [description]
    • @return [type] [description]
      */
      public function unpackFromBinStream(dataBin) { foreach (static::paramNameMapping as key =>paramName) {
      paramType = static::getParamType(paramName);
      this->{paramName} = ProtocolType::unPack(dataBin,paramType);
      }
      }
      }</pre>

得到消息協(xié)議包

<pre class="php hljs language-php" style="box-sizing: border-box; font-family: var(--bs-font-monospace); font-size: 0.875em; direction: ltr; unicode-bidi: bidi-override; display: block; margin-top: 0px !important; margin-bottom: 1.25rem; overflow: auto; color: rgb(36, 41, 46); background: rgb(233, 236, 239); padding: 1rem; max-height: 35rem; line-height: 1.5; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;"><?php
class RegisterRequest extends ProtocolMessage {
public account; publicpassword;
public $age;

// 參數(shù)項位序 accoutBin PaaswordBin ageBin
public static $paramNameMapping = [
    0 => 'account',
    1 => 'password',
    2 => 'age',
];

// 參數(shù)類型
public static $paramProtocolTypeMapping = [
    'account'  => ProtocolType::TYPE_STRING,
    'password' => ProtocolType::TYPE_STRING,
    'age'      => ProtocolType::TYPE_TINYINT,
];

public function setAccount($account) {
    $paramType        = static::getParamType('account');
    $this->accountBin = ProtocolType::pack($account, $paramType);
}

public function getAccount() {
    return $this->account;
}

public function setPassword($password) {
    $paramType         = static::getParamType('password');
    $this->passwordBin = ProtocolType::pack($password, $paramType);
}

public function getPassword() {
    return $this->password;
}

public function setAge($age) {
    $paramType    = static::getParamType('age');
    $this->ageBin = ProtocolType::pack($age, $paramType);
}

public function getAge() {
    return $this->age;
}

}
</pre>

打包至二進制

<pre class="php hljs language-php" style="box-sizing: border-box; font-family: var(--bs-font-monospace); font-size: 0.875em; direction: ltr; unicode-bidi: bidi-override; display: block; margin-top: 0px !important; margin-bottom: 1.25rem; overflow: auto; color: rgb(36, 41, 46); background: rgb(233, 236, 239); padding: 1rem; max-height: 35rem; line-height: 1.5; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;"><?php
$data = [
'account' => 'sqrtcat',
'password' => '123456',
'age' => 29,
];

// 文本表單
var_dump(http_build_query(data)); // 文本json var_dump(json_encode(data));

// 二進制協(xié)議
registerRequest = new RegisterRequest();registerRequest->setAccount('sqrtcat');
registerRequest->setPassword('123456');registerRequest->setAge(29);
dataBin =registerRequest->packToBinStream();
var_dump($dataBin);

// 解析二進制協(xié)議
registerRequest->unpackFromBinStream(dataBin);

echo registerRequest->getAccount() . PHP_EOL; echoregisterRequest->getPassword() . PHP_EOL;
echo $registerRequest->getAge() . PHP_EOL;</pre>

數(shù)據(jù)解析

開始解析數(shù)據(jù):

  1. 按協(xié)議約定,第一個參數(shù)項位是 account, 類型是 string,用 1byte 表示數(shù)據(jù)長度,讀取 1byte 獲取 account 的長度,再讀取相應(yīng)的長度,獲得 account 的數(shù)據(jù)內(nèi)容,參數(shù)項1解析完成。
  2. 按協(xié)議約定,第二個參數(shù)項位是 password,類型是 string,用 1byte 表示數(shù)據(jù)長度,讀取 1byte 獲取 password 的長度,再讀取相應(yīng)的長度,獲得 password 的數(shù)據(jù)內(nèi)容,參數(shù)項2解析完成。
  3. 按協(xié)議約定,第三個參數(shù)項位是 age,類型是 tinyint,固定1byte,讀取 1byte 獲得 age 的數(shù)據(jù)內(nèi)容,參數(shù)項3解析完成。

大概的機制就是這樣,所以我們發(fā)送端和接收端都需要載入 protobuf 生成的數(shù)據(jù)協(xié)議包,用來解析和映射。

protobuf 類的數(shù)據(jù)打包成二進制的方式,要更多的考慮到大量變長數(shù)據(jù)的場景,如果死板的固定每個數(shù)據(jù)項的字節(jié)數(shù),可能會帶來一定的數(shù)據(jù)冗余

1、解決死板固定字段長度造成的數(shù)據(jù)填充過多的問題

為每個字段加一個長度位,表征后面多少字節(jié)為數(shù)據(jù)位

<pre class="protobuf hljs language-protobuf" style="box-sizing: border-box; font-family: var(--bs-font-monospace); font-size: 0.875em; direction: ltr; unicode-bidi: bidi-override; display: block; margin-top: 0px !important; margin-bottom: 1.25rem; overflow: auto; color: rgb(36, 41, 46); background: rgb(233, 236, 239); padding: 1rem; max-height: 35rem; line-height: 1.5; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;">|1byteLen | account | 1byteLen| password |

| 7 | account | 6 | password |
|0000 0111|s|q|r|t|c|a|t|0000 0110|1|2|3|4|5|6|</pre>

但還是不夠完美:

  1. 長度位不夠靈活,例子中固定用1bytes去表示,那萬一數(shù)據(jù)長度超過 255 了呢,最好有一個約定,定義好某參數(shù)的長度位的bytes數(shù)。
  2. '123456'占了 6bytes, 如果我打包至定長的短整型,2bytes就可以表示出來,而且短整型就是定長的,我只需要知道我第二個參數(shù)是短整型就好,不需要使用長度標(biāo)識位來記錄。

所以,消息協(xié)議就應(yīng)邀而出了。

2、解決長度位固定導(dǎo)致場景受限的問題

我們需要一個協(xié)議,突出兩點:
1、某個參數(shù)的協(xié)議結(jié)構(gòu)是怎樣的,根據(jù)字段類型,分配不同的字段協(xié)議,比如變長的字符串,結(jié)構(gòu)要以 paramBytes = lenBytes + dataBytes 的方式,定長的數(shù)值型,則以 paramBytes = dataBytes
2、參數(shù)項的位序與數(shù)據(jù)類型的映射關(guān)系,要能確定第N個參數(shù)的字段協(xié)議結(jié)構(gòu)是怎樣的,字符串則讀取相應(yīng)的長度字節(jié)位,再向后讀取長度個字節(jié),獲得數(shù)據(jù),定長的數(shù)值型則直接讀取相應(yīng)的固定的字節(jié)數(shù),即可獲得數(shù)據(jù)。

pack/unpack

<pre class="hljs language-scss" style="box-sizing: border-box; font-family: var(--bs-font-monospace); font-size: 0.875em; direction: ltr; unicode-bidi: bidi-override; display: block; margin-top: 0px !important; margin-bottom: 1.25rem; overflow: auto; color: rgb(36, 41, 46); background: rgb(233, 236, 239); padding: 1rem; max-height: 35rem; line-height: 1.5; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;">a 以NUL字節(jié)填充字符串空白
A 以SPACE(空格)填充字符串
h 十六進制字符串,低位在前
H 十六進制字符串,高位在前
c 有符號字符 -128 ~ 127
C 無符號字符 0 ~ 255
s 有符號短整型(16位,主機字節(jié)序)
S 無符號短整型(16位,主機字節(jié)序)
n 無符號短整型(16位,大端字節(jié)序)
v 無符號短整型(16位,小端字節(jié)序)
i 有符號整型(機器相關(guān)大小字節(jié)序)
I 無符號整型(機器相關(guān)大小字節(jié)序)
l 有符號整型(32位,主機字節(jié)序) -2147483648 ~ 2147483647
L 無符號整型(32位,主機字節(jié)序) 0 ~ 4294967296
N 無符號整型(32位,大端字節(jié)序)
V 無符號整型(32位,小端字節(jié)序)
q 有符號長整型(64位,主機字節(jié)序)
Q 無符號長整型(64位,主機字節(jié)序) 0 ~ 18446744073709551616
J 無符號長整型(64位,大端字節(jié)序)
P 無符號長整型(64位,小端字節(jié)序)
f 單精度浮點型(機器相關(guān)大小)
d 雙精度浮點型(機器相關(guān)大小)
x NUL字節(jié)
X 回退一字節(jié)
Z 以NUL字節(jié)填充字符串空白(new in PHP 5.5)
@ NUL填充到絕對位置</pre>

二進制數(shù)據(jù)壓縮

<pre class="php hljs language-php" style="box-sizing: border-box; font-family: var(--bs-font-monospace); font-size: 0.875em; direction: ltr; unicode-bidi: bidi-override; display: block; margin-top: 0px !important; margin-bottom: 1.25rem; overflow: auto; color: rgb(36, 41, 46); background: rgb(233, 236, 239); padding: 1rem; max-height: 35rem; line-height: 1.5; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;"><?php

$raw = "69984567982132123122231";

echo "raw data: " . raw . PHP_EOL; echo "raw len:" . strlen(raw) . PHP_EOL;

$segmentRaw = [];

while (true) {
$offset = 3;

if (strlen($raw) < 3) {
    $segmentRaw[] = $raw;
    break;
}

$rawEle = substr($raw, 0, $offset);

if (intval($rawEle) > 255) {
    $offset = 2;
    $rawEle = substr($raw, 0, $offset);
}

$segmentRaw[] = $rawEle;</pre>
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容