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

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

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

思考

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

在網(wǎng)絡(luò)編程中遮婶,經(jīng)郴人椋看到要求數(shù)據(jù)要以二進(jìn)制的方式進(jìn)行傳輸,起初我很不理解旗扑,為什么要刻意的說明二進(jìn)制方式呢蹦骑?數(shù)據(jù)在底層的傳輸不都是二進(jìn)制流嗎?而且還引出了 pack/unpack 方法簇臀防。

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

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

如果我們事先定義了數(shù)據(jù)中的n~m個(gè)字節(jié)位固定作為某參數(shù)的數(shù)據(jù)段,就可以免去 參數(shù)名 所帶來的額外開銷脱吱。比如 0 ~ 10 字節(jié)為 account荷腊,11 ~ 24 字節(jié)為 passowrd。又因?yàn)橛脩裘蛎艽a是非定長的急凰,而解析數(shù)據(jù)時(shí)又要根據(jù)字節(jié)位精準(zhǔn)的截取女仰,所以我們需要對數(shù)據(jù)項(xiàng)進(jìn)行打包填充猜年,使其固定字節(jié)長度,而后在服務(wù)端進(jìn)行解包疾忍,pack/unpack便可以實(shí)現(xiàn)此功能乔外。

白話

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

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

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

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

文本方式傳輸

日常開發(fā)影钉,比如發(fā)送一個(gè)用戶注冊 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ù)體重得到各項(xiàng)參數(shù)平委。

二進(jìn)制傳輸方式

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

最簡單的蜡塌,參數(shù)定長的方式,account 固定為 11 位勿负,password 固定為 14 位馏艾,使用 pack將數(shù)據(jù)填充至相應(yīng)的協(xié)議長度,發(fā)送,服務(wù)端按協(xié)議進(jìn)行字節(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ù)進(jìn)行填充 php 解包時(shí)會(huì)自動(dòng) trim 掉
// a 以 0x00 字符對數(shù)據(jù)進(jìn)行填充 php 解包時(shí)會(huì)保留 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é)議和二進(jìn)制傳輸?shù)姆绞较拢挥昧?25bytes房资。這就滿足了蜕劝?并不能夠~,這種簡單協(xié)議的二進(jìn)制傳輸方式只是在一定場景下發(fā)揮了傳輸效率轰异,在某些場景下可能還不如文本方式岖沛。因?yàn)閲?yán)格的數(shù)據(jù)定長填充,可能會(huì)造成數(shù)據(jù)的冗余搭独,比如 account只有一個(gè)字符 s婴削,password 也只有一個(gè)字符 1,在此協(xié)議下還是固定 25bytes戳稽,文本傳輸反而效率會(huì)高一些馆蠕。

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

Protobuf 的二進(jìn)制傳輸

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

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

這里原諒我自己造了兩個(gè)詞急膀,參數(shù)項(xiàng)位參數(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 二進(jìn)制序列化組件,完整版已放置 github 支持的數(shù)據(jù)類型還是很全面的protoBin聘殖。

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

主要是定義哪些類型是定長晨雳,哪些類型是變長行瑞,變長類型還需給定長度位的字節(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 個(gè)字符長度
      self::TYPE_TEXT => 4, // text 用 4bytes 表征數(shù)據(jù)長度 能表征 2 ^ 32 - 1個(gè)字符長度 1PB的數(shù)據(jù) 噗
      ];

    /**

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

    /**

    • 打包二進(jìn)制數(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)的二進(jìn)制paramProtocDataBin = pack(packSymbol,data);
      } else {
      // 變長類型 數(shù)據(jù)長度位 + 數(shù)據(jù)位
      paramProtocDataBin = pack(packSymbol, strlen(data)) .data;
      }

      return $paramProtocDataBin;
      }

    /**

    • 解包二進(jìn)制數(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)的二進(jìn)制
      paramData = unpack(packSymbol, paramBin)[1]; } else { // 類型的長度位字節(jié)數(shù)typeLenBytes = self::TYPE_VARIABLE_LEN_BYTES[paramType]; // 數(shù)據(jù)長度位paramLenBytes = substr(dataBin, 0,typeLenBytes);
      // 解析二進(jìn)制的數(shù)據(jù)長度
      paramDataLen = unpack(packSymbol, paramLenBytes)[1]; // 讀取變長的數(shù)據(jù)內(nèi)容paramData = substr(dataBin,typeLenBytes, paramDataLen); // 參數(shù)項(xiàng)的總字節(jié)數(shù)paramBytes = typeLenBytes +paramDataLen;
      }

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

      return $paramData;
      }
      }

/**

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

    • 二進(jìn)制協(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ù)項(xiàng)位序 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>

打包至二進(jìn)制

<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));

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

// 解析二進(jìn)制協(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é)議約定悍募,第一個(gè)參數(shù)項(xiàng)位是 account, 類型是 string蘑辑,用 1byte 表示數(shù)據(jù)長度,讀取 1byte 獲取 account 的長度坠宴,再讀取相應(yīng)的長度洋魂,獲得 account 的數(shù)據(jù)內(nèi)容,參數(shù)項(xiàng)1解析完成喜鼓。
  2. 按協(xié)議約定副砍,第二個(gè)參數(shù)項(xiàng)位是 password,類型是 string庄岖,用 1byte 表示數(shù)據(jù)長度豁翎,讀取 1byte 獲取 password 的長度,再讀取相應(yīng)的長度隅忿,獲得 password 的數(shù)據(jù)內(nèi)容心剥,參數(shù)項(xiàng)2解析完成。
  3. 按協(xié)議約定背桐,第三個(gè)參數(shù)項(xiàng)位是 age优烧,類型是 tinyint,固定1byte链峭,讀取 1byte 獲得 age 的數(shù)據(jù)內(nèi)容畦娄,參數(shù)項(xiàng)3解析完成。

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

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

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

為每個(gè)字段加一個(gè)長度位颓鲜,表征后面多少字節(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 了呢,最好有一個(gè)約定蚊丐,定義好某參數(shù)的長度位的bytes數(shù)熙参。
  2. '123456'占了 6bytes, 如果我打包至定長的短整型,2bytes就可以表示出來麦备,而且短整型就是定長的孽椰,我只需要知道我第二個(gè)參數(shù)是短整型就好昭娩,不需要使用長度標(biāo)識(shí)位來記錄。

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

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

我們需要一個(gè)協(xié)議锐涯,突出兩點(diǎn):
1磕诊、某個(gè)參數(shù)的協(xié)議結(jié)構(gòu)是怎樣的,根據(jù)字段類型纹腌,分配不同的字段協(xié)議霎终,比如變長的字符串,結(jié)構(gòu)要以 paramBytes = lenBytes + dataBytes 的方式升薯,定長的數(shù)值型莱褒,則以 paramBytes = dataBytes
2涎劈、參數(shù)項(xiàng)的位序與數(shù)據(jù)類型的映射關(guān)系广凸,要能確定第N個(gè)參數(shù)的字段協(xié)議結(jié)構(gòu)是怎樣的,字符串則讀取相應(yīng)的長度字節(jié)位蛛枚,再向后讀取長度個(gè)字節(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 十六進(jìn)制字符串,低位在前
H 十六進(jìn)制字符串白筹,高位在前
c 有符號(hào)字符 -128 ~ 127
C 無符號(hào)字符 0 ~ 255
s 有符號(hào)短整型(16位智末,主機(jī)字節(jié)序)
S 無符號(hào)短整型(16位,主機(jī)字節(jié)序)
n 無符號(hào)短整型(16位徒河,大端字節(jié)序)
v 無符號(hào)短整型(16位系馆,小端字節(jié)序)
i 有符號(hào)整型(機(jī)器相關(guān)大小字節(jié)序)
I 無符號(hào)整型(機(jī)器相關(guān)大小字節(jié)序)
l 有符號(hào)整型(32位,主機(jī)字節(jié)序) -2147483648 ~ 2147483647
L 無符號(hào)整型(32位顽照,主機(jī)字節(jié)序) 0 ~ 4294967296
N 無符號(hào)整型(32位由蘑,大端字節(jié)序)
V 無符號(hào)整型(32位,小端字節(jié)序)
q 有符號(hào)長整型(64位代兵,主機(jī)字節(jié)序)
Q 無符號(hào)長整型(64位尼酿,主機(jī)字節(jié)序) 0 ~ 18446744073709551616
J 無符號(hào)長整型(64位,大端字節(jié)序)
P 無符號(hào)長整型(64位植影,小端字節(jié)序)
f 單精度浮點(diǎn)型(機(jī)器相關(guān)大小)
d 雙精度浮點(diǎn)型(機(jī)器相關(guān)大小)
x NUL字節(jié)
X 回退一字節(jié)
Z 以NUL字節(jié)填充字符串空白(new in PHP 5.5)
@ NUL填充到絕對位置</pre>

二進(jìn)制數(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)系作者
  • 序言:七十年代末裳擎,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子思币,更是在濱河造成了極大的恐慌鹿响,老刑警劉巖羡微,帶你破解...
    沈念sama閱讀 207,113評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異惶我,居然都是意外死亡妈倔,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,644評論 2 381
  • 文/潘曉璐 我一進(jìn)店門绸贡,熙熙樓的掌柜王于貴愁眉苦臉地迎上來盯蝴,“玉大人,你說我怎么就攤上這事恃轩〗嵬荩” “怎么了?”我有些...
    開封第一講書人閱讀 153,340評論 0 344
  • 文/不壞的土叔 我叫張陵叉跛,是天一觀的道長松忍。 經(jīng)常有香客問我,道長筷厘,這世上最難降的妖魔是什么鸣峭? 我笑而不...
    開封第一講書人閱讀 55,449評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮酥艳,結(jié)果婚禮上摊溶,老公的妹妹穿的比我還像新娘。我一直安慰自己充石,他們只是感情好莫换,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,445評論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著骤铃,像睡著了一般拉岁。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上惰爬,一...
    開封第一講書人閱讀 49,166評論 1 284
  • 那天喊暖,我揣著相機(jī)與錄音,去河邊找鬼撕瞧。 笑死陵叽,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的丛版。 我是一名探鬼主播巩掺,決...
    沈念sama閱讀 38,442評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼页畦!你這毒婦竟也來了胖替?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,105評論 0 261
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎刊殉,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體州胳,經(jīng)...
    沈念sama閱讀 43,601評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡记焊,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,066評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了栓撞。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片遍膜。...
    茶點(diǎn)故事閱讀 38,161評論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖瓤湘,靈堂內(nèi)的尸體忽然破棺而出瓢颅,到底是詐尸還是另有隱情,我是刑警寧澤弛说,帶...
    沈念sama閱讀 33,792評論 4 323
  • 正文 年R本政府宣布挽懦,位于F島的核電站,受9級(jí)特大地震影響木人,放射性物質(zhì)發(fā)生泄漏信柿。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,351評論 3 307
  • 文/蒙蒙 一醒第、第九天 我趴在偏房一處隱蔽的房頂上張望渔嚷。 院中可真熱鬧,春花似錦稠曼、人聲如沸形病。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,352評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽漠吻。三九已至,卻和暖如春蝗岖,著一層夾襖步出監(jiān)牢的瞬間侥猩,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,584評論 1 261
  • 我被黑心中介騙來泰國打工抵赢, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留欺劳,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,618評論 2 355
  • 正文 我出身青樓铅鲤,卻偏偏與公主長得像划提,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子邢享,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,916評論 2 344

推薦閱讀更多精彩內(nèi)容