在網絡編程中,常常看到要求數據要以二進制的方式進行傳輸,起初我很不理解,爲何要刻意的說明二進制方式呢?數據在底層的傳輸不都是二進制流嗎?並且還引出了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
平常開發,好比發送一個用戶註冊http協議
請求,發送的數據格式分別以下:ui
$registerData = [ "account" => "sqrtcat", "password" => "123456" ];
formData 31bytes
this
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
只有一個字符s
,password
也只有一個字符1
,在此協議下仍是固定25bytes,文本傳輸反而效率會高一些。
二進制傳輸敗北了?No,是咱們協議太簡單,不夠靈活,沒有最大程度上發揮協議+二進制的高效性,能夠說,協議下的二進制傳輸方式,能作到絕對的高效於文本傳輸,咱們能夠簡單的分析和模擬以二進制方式傳輸的protobuf
的協議模式。
咱們能夠簡單分析下 protobuf
傳輸數據的方式:
參數項位
和 參數長度位
映射的消息協議包。這裏原諒我本身造了兩個詞,參數項位
和參數長度位
,如何理解呢?經過下面模仿 protobuf 的協議示例來理解。
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;
開始解析數據:
大概的機制就是這樣,因此咱們發送端和接收端都須要載入 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|
但仍是不夠完美:
因此,消息協議就應邀而出了。
咱們須要一個協議,突出兩點:
一、某個參數的協議結構是怎樣的,根據字段類型,分配不一樣的字段協議,好比變長的字符串,結構要以 paramBytes = lenBytes + dataBytes
的方式,定長的數值型,則以 paramBytes = dataBytes
。
二、參數項的位序與數據類型的映射關係,要能肯定第N個參數的字段協議結構是怎樣的,字符串則讀取相應的長度字節位,再向後讀取長度個字節,得到數據,定長的數值型則直接讀取相應的固定的字節數,便可得到數據。
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));