二進制編碼傳輸協議

二進制編碼傳輸協議

思考

  1. 何爲二進制協議傳輸,何爲文本協議數據傳輸?
  2. 網絡編程中數據協議的制定方式有哪些?
  3. Protobuf 等二進制數據序列化傳輸協議的機制是什麼?

在網絡編程中,常常看到要求數據要以二進制的方式進行傳輸,起初我很不理解,爲何要刻意的說明二進制方式呢?數據在底層的傳輸不都是二進制流嗎?並且還引出了pack/unpack方法簇。php

咱們常常用到的 rpc,好比json-rpc是以文本方式傳輸序列化的數據的。grpc(protobuf), thrift都是以二進制方式傳輸數據的。那到底何爲二進制傳輸呢?編程

你們能夠先想一下平常中發送請求時常常用到的方式: xml,json,formData,他們雖然格式不一樣,但都有一個特徵,自帶描述信息(直白說就是攜帶參數名),像文本同樣,能很直觀的看到數據表徵的內容。json

若是咱們事先定義了數據中的n~m個字節固定做爲某參數的數據段,就能夠免去參數名所帶來的額外開銷。好比 0 ~ 10 字節爲account,11 ~ 24 字節爲passowrd。又由於用戶名或密碼是非定長的,而解析數據時又要根據字節位精準的截取,因此咱們須要對數據項進行打包填充,使其固定字節長度,然後在服務端進行解包,pack/unpack即可以實現此功能。bash

白話

tcp 協議是平常中最爲常見的二進制協議,協議體的字節位都有約定好的表徵。網絡

http 在廣義上來講也是二進制模式,使用 rn 對協議進行解包,解析,但http攜帶的數據一般都是文本模式的,好比 "sqrtcat" 佔了 7 個字節,在文本or二進制模式下沒什麼區別,但"29",以文本模式發送須要2bytes,以二進制模式打包至字符類型,只須要1bytes。app

二進制爲什麼能提升數據傳輸效率:tcp

  1. 根據協議約定,省去參數名所佔用的字節,縮減了數據。
  2. 將數值類型的數據打包至相應範圍內的二進制,節省了空間,4bytes能表示 32 位的文本數值,但文本數據值要32bytes。
  3. 在必定程度上能夠起到加密數據的做用,若是第三方不知道數據協議,就沒有辦法截取相應的字節爲獲取數據,或獲得數據的表徵。

文本方式傳輸

平常開發,好比發送一個用戶註冊http協議請求,發送的數據格式分別以下:ui

$registerData = [
    "account"  => "sqrtcat",
    "password" => "123456"
];

formData 31bytesthis

account=sqrtcat&password=123456

json 41bytes編碼

{"account":"sqrtcat","password":"123456"}

xml 94bytes

<?xml version="1.0" encoding="UTF-8" ?>
<account>sqrtcat</account>
<password>123456</password>

以上三種皆爲,咱們能夠很直觀的在數據體重獲得各項參數。

二進制傳輸方式

二進制傳輸,離不開協議的制定。文本方式傳輸的數據能夠自我描述,而二進制方式傳輸的數據,須要經過協議進行解析和讀取。

最簡單的,參數定長的方式,account 固定爲 11 位,password 固定爲 14 位,使用 pack將數據填充至相應的協議長度,發送,服務端按協議進行字節長度的截取得到對應的參數值。

<?php
// binary protocal:
// |-- 11 bytes account --|-- 14 bytes password --|
$account = "sqrtcat";
$password = "123456";

// pack
// A 以空白符對數據進行填充 php 解包時會自動 trim 掉
// a 以 0x00 字符對數據進行填充 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"
}

對比文本方式發送,咱們在協議和二進制傳輸的方式下,只用了 25bytes。這就知足了?並不可以~,這種簡單協議的二進制傳輸方式只是在必定場景下發揮了傳輸效率,在某些場景下可能還不如文本方式。由於嚴格的數據定長填充,可能會形成數據的冗餘,好比 account只有一個字符spassword 也只有一個字符1,在此協議下仍是固定25bytes,文本傳輸反而效率會高一些。

二進制傳輸敗北了?No,是咱們協議太簡單,不夠靈活,沒有最大程度上發揮協議+二進制的高效性,能夠說,協議下的二進制傳輸方式,能作到絕對的高效於文本傳輸,咱們能夠簡單的分析和模擬以二進制方式傳輸的protobuf的協議模式。

Protobuf 的二進制傳輸

咱們能夠簡單分析下 protobuf傳輸數據的方式:

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

這裏原諒我本身造了兩個詞,參數項位參數長度位,如何理解呢?經過下面模仿 protobuf 的協議示例來理解。

定義消息體的IDL

message RegisterRequest {
    string account = 1; // 數據位1 type string name account 
    string password = 2; // 數據位2 type string name password
    tinyint age = 3; // 數據位3 type tinyint name age
}

協議數據類型的二進制格式約定

主要是定義哪些類型是定長,哪些類型是變長,變長類型還需給定長度位的字節數。

<?php
/**
 * 協議數據類型
 * //|    參數位1(變長數據)      |  參數位2(定長類型) |      參數位3(變長數據)     |
 * //| 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';

    /**
     * 數據類型是否爲定長
     */
    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,
    ];

    // 定長數據類型的字節數 paramBytes = dataBytes
    const TYPE_FIXED_LEN_BYTES = [
        self::TYPE_TINYINT => 1, // tinyint 固定1字節 不須要長度表徵 追求極致
        self::TYPE_INT16   => 2, // int16 固定2字節 不須要長度表徵 追求極致
        self::TYPE_INT32   => 4, // int32 固定4字節 不須要長度表徵 追求極致
        self::TYPE_INT64   => 8, // int64 固定8字節 不須要長度表徵 追求極致
    ];

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

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

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

    /**
     * 定長得到字節數
     * 變長得到數據長度爲字節數
     * @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];
        }
    }

    /**
     * 打包二進制數據
     * @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)) {
            // 定長類型 直接打包數據至相應的二進制
            $paramProtocDataBin = pack($packSymbol, $data);
        } else {
            // 變長類型 數據長度位 + 數據位
            $paramProtocDataBin = pack($packSymbol, strlen($data)) . $data;
        }

        return $paramProtocDataBin;
    }

    /**
     * 解包二進制數據
     * @param  [type] &$dataBin  [description]
     * @param  [type] $paramType [description]
     * @return [type]            [description]
     */
    public static function unPack(&$dataBin, $paramType)
    {
        $packSymbol = self::TYPE_PACK_SYMBOL[$paramType];

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

        // 剩餘待處理的數據
        $dataBin = substr($dataBin, $paramBytes);

        return $paramData;
    }
}

/**
 * 協議消息體
 */
class ProtocolMessage
{
    /**
     * 二進制協議流
     * @var [type]
     */
    public $dataBin;

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

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

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

    /**
     * 按參數位序依次打包
     * @return [type] [description]
     */
    public function packToBinStream()
    {
        // 按參數位序
        foreach (static::$paramNameMapping as $key => $paramName) {
            $this->dataBin .= $this->{$paramName . 'Bin'};
        }

        return $this->dataBin;
    }

    /**
     * 按參數位序一次解包
     * @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);
        }
    }
}

獲得消息協議包

<?php
class RegisterRequest extends ProtocolMessage
{
    public $account;
    public $password;
    public $age;

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

    // 參數類型
    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;
    }
}

打包至二進制

<?php
$data = [
    'account'  => 'sqrtcat',
    'password' => '123456',
    'age'      => 29,
];

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

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

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

echo $registerRequest->getAccount() . PHP_EOL;
echo $registerRequest->getPassword() . PHP_EOL;
echo $registerRequest->getAge() . PHP_EOL;

數據解析

開始解析數據:

  1. 按協議約定,第一個參數項位是 account, 類型是 string,用 1byte 表示數據長度,讀取 1byte 獲取 account 的長度,再讀取相應的長度,得到 account 的數據內容,參數項1解析完成。
  2. 按協議約定,第二個參數項位是 password,類型是 string,用 1byte 表示數據長度,讀取 1byte 獲取 password 的長度,再讀取相應的長度,得到 password 的數據內容,參數項2解析完成。
  3. 按協議約定,第三個參數項位是 age,類型是 tinyint,固定1byte,讀取 1byte 得到 age 的數據內容,參數項3解析完成。

大概的機制就是這樣,因此咱們發送端和接收端都須要載入 protobuf 生成的數據協議包,用來解析和映射。

protobuf類的數據打包成二進制的方式,要更多的考慮到大量變長數據的場景,若是死板的固定每一個數據項的字節數,可能會帶來必定的數據冗餘

一、解決死板固定字段長度形成的數據填充過多的問題

爲每一個字段加一個長度位,表徵後面多少字節爲數據位

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

但仍是不夠完美:

  1. 長度位不夠靈活,例子中固定用1bytes去表示,那萬一數據長度超過 255 了呢,最好有一個約定,定義好某參數的長度位的bytes數。
  2. '123456'佔了 6bytes, 若是我打包至定長的短整型,2bytes就能夠表示出來,並且短整型就是定長的,我只須要知道我第二個參數是短整型就好,不須要使用長度標識位來記錄。

因此,消息協議就應邀而出了。

二、解決長度位固定致使場景受限的問題

咱們須要一個協議,突出兩點:
一、某個參數的協議結構是怎樣的,根據字段類型,分配不一樣的字段協議,好比變長的字符串,結構要以 paramBytes = lenBytes + dataBytes 的方式,定長的數值型,則以 paramBytes = dataBytes
二、參數項的位序與數據類型的映射關係,要能肯定第N個參數的字段協議結構是怎樣的,字符串則讀取相應的長度字節位,再向後讀取長度個字節,得到數據,定長的數值型則直接讀取相應的固定的字節數,便可得到數據。

pack/unpack

a    以NUL字節填充字符串空白
A    以SPACE(空格)填充字符串
h    十六進制字符串,低位在前
H    十六進制字符串,高位在前
c    有符號字符 -128 ~ 127
C    無符號字符 0 ~ 255
s    有符號短整型(16位,主機字節序)
S    無符號短整型(16位,主機字節序)
n    無符號短整型(16位,大端字節序)
v    無符號短整型(16位,小端字節序)
i    有符號整型(機器相關大小字節序)
I    無符號整型(機器相關大小字節序)
l    有符號整型(32位,主機字節序) -2147483648 ~ 2147483647
L    無符號整型(32位,主機字節序) 0 ~ 4294967296
N    無符號整型(32位,大端字節序) 
V    無符號整型(32位,小端字節序)
q    有符號長整型(64位,主機字節序)
Q    無符號長整型(64位,主機字節序) 0 ~ 18446744073709551616
J    無符號長整型(64位,大端字節序)
P    無符號長整型(64位,小端字節序)
f    單精度浮點型(機器相關大小)
d    雙精度浮點型(機器相關大小)
x    NUL字節
X    回退一字節
Z    以NUL字節填充字符串空白(new in PHP 5.5)
@    NUL填充到絕對位置

二進制數據壓縮

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

    $raw = substr($raw, $offset);
}

// c 有符號字符打包 -128 ~ 127
// C 無符號字符打包   0  ~ 255
$rawBin = pack("C*", ...$segmentRaw);

echo "transfer data: " . $rawBin . PHP_EOL;
echo "transfer len: " . strlen($rawBin) . PHP_EOL;

echo "unpack: " . implode("", unpack("C*", $rawBin));
相關文章
相關標籤/搜索