在网络编程中,常常看到要求数据要以二进制的方式进行传输,起初我很不理解,为何要刻意的说明二进制方式呢?数据在底层的传输不都是二进制流吗?并且还引出了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));