二进制编码传输协议(转载,仅作记录)

转载自https://segmentfault.com/a/1190000022356844

二进制编码传输协议

思考

  1. 何为二进制协议传输,何为文本协议数据传输?
  2. 网络编程中数据协议的制定方式有哪些?
  3. Protobuf 等二进制数据序列化传输协议的机制是什么?

在网络编程中,经常看到要求数据要以二进制的方式进行传输,起初我很不理解,为什么要刻意的说明二进制方式呢?数据在底层的传输不都是二进制流吗?而且还引出了 pack/unpack 方法簇。

我们经常用到的 rpc,比如 json-rpc 是以 文本方式 传输序列化的数据的。grpc(protobuf), thrift 都是以 二进制方式 传输数据的。所以到底何为二进制传输呢?

大家可以先想一下日常中发送请求时经常用到的方式: xml, json, formData,他们虽然格式不同,但都有一个特征,自带描述信息(直白说就是携带 参数名),像 文本 一样,能很直观的看到数据表征的内容。

如果我们事先定义了数据中的n~m个字节位固定作为某参数的数据段,就可以免去 参数名 所带来的额外开销。比如 0 ~ 10 字节为 account11 ~ 24 字节为 passowrd。又因为用户名或密码是非定长的,而解析数据时又要根据字节位精准的截取,所以我们需要对数据项进行打包填充,使其固定字节长度,而后在服务端进行解包,pack/unpack便可以实现此功能。

白话

tcp 协议是日常中最为常见的二进制协议,协议体的字节位都有约定好的表征。

http 在广义上来说也是二进制模式,使用 \r\n 对协议进行解包,解析,但 http 携带的数据通常都是文本模式的,比如 "sqrtcat" 占了 7 个字节,在文本 or 二进制模式下没什么区别,但"29",以文本模式发送需要 2bytes,以二进制模式打包至字符类型,只需要 1bytes。

二进制为何能提高数据传输效率:

  1. 根据协议约定,省去参数名所占用的字节,缩减了数据。
  2. 将数值类型的数据打包至相应范围内的二进制,节省了空间,4bytes能表示 32 位的文本数值,但文本数据值要 32bytes。
  3. 在一定程度上可以起到加密数据的作用,如果第三方不知道数据协议,就没有办法截取相应的字节为获取数据,或得到数据的表征。

文本方式传输

日常开发,比如发送一个用户注册 http协议 请求,发送的数据格式分别如下:

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

以上三种皆为,我们可以很直观的在数据体重得到各项参数。

二进制传输方式

二进制传输,离不开协议的制定。文本方式传输的数据可以自我描述,而二进制方式传输的数据,需要通过协议进行解析和读取。

最简单的,参数定长的方式,account 固定为 11 位,password 固定为 14 位,使用 pack将数据填充至相应的协议长度,发送,服务端按协议进行字节长度的截取获得对应的参数值。

<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 以空白符对数据进行填充 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"
}</pre>

对比文本方式发送,我们在协议和二进制传输的方式下,只用了 25bytes。这就满足了?并不能够~,这种简单协议的二进制传输方式只是在一定场景下发挥了传输效率,在某些场景下可能还不如文本方式。因为严格的数据定长填充,可能会造成数据的冗余,比如 account只有一个字符 spassword 也只有一个字符 1,在此协议下还是固定 25bytes,文本传输反而效率会高一些。

二进制传输败北了?No,是我们协议太简单,不够灵活,没有最大程度上发挥协议+二进制的高效性,可以说,协议下的二进制传输方式,能做到绝对的高效于文本传输,这里我们可以简单的分析和模拟以二进制方式传输的 protobuf 的协议模式。

Protobuf 的二进制传输

我们可以简单分析下 protobuf传输数据的方式:

  1. 定义 IDL,其实就相当于制定了协议体
  2. 生成 proto 文件,得到具体的消息字段的 参数项位参数长度位 映射的消息协议包。
  3. 发送端根据消息协议定义的参数数据类型(主要是变长 or 定长),将数据打包至相应的二进制格式。
  4. 发送数据。
  5. 接收端按消息协议格式对二进制数据进行解析,获得文本数据。

这里原谅我自己造了两个词,参数项位参数长度位,如何理解呢?通过下面模仿 protobuf 的协议示例来理解。

定义消息体的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; // 数据位1 type string name account
string password = 2; // 数据位2 type string name password
tinyint age = 3; // 数据位3 type tinyint name age
}</pre>

注意下面是我自己仿 protobuf 写的一套 php 二进制序列化组件,完整版已放置 github 支持的数据类型还是很全面的protoBin

协议数据类型的二进制格式约定

主要是定义哪些类型是定长,哪些类型是变长,变长类型还需给定长度位的字节数。

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

  • 协议数据类型

  • //| 参数位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 askey => 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);
      }
      }
      }</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
class RegisterRequest extends ProtocolMessage {
public account; publicpassword;
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;
}

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

// 二进制协议
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; echoregisterRequest->getPassword() . PHP_EOL;
echo $registerRequest->getAge() . PHP_EOL;</pre>

数据解析

开始解析数据:

  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 类的数据打包成二进制的方式,要更多的考虑到大量变长数据的场景,如果死板的固定每个数据项的字节数,可能会带来一定的数据冗余

1、解决死板固定字段长度造成的数据填充过多的问题

为每个字段加一个长度位,表征后面多少字节为数据位

<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去表示,那万一数据长度超过 255 了呢,最好有一个约定,定义好某参数的长度位的bytes数。
  2. '123456'占了 6bytes, 如果我打包至定长的短整型,2bytes就可以表示出来,而且短整型就是定长的,我只需要知道我第二个参数是短整型就好,不需要使用长度标识位来记录。

所以,消息协议就应邀而出了。

2、解决长度位固定导致场景受限的问题

我们需要一个协议,突出两点:
1、某个参数的协议结构是怎样的,根据字段类型,分配不同的字段协议,比如变长的字符串,结构要以 paramBytes = lenBytes + dataBytes 的方式,定长的数值型,则以 paramBytes = dataBytes
2、参数项的位序与数据类型的映射关系,要能确定第N个参数的字段协议结构是怎样的,字符串则读取相应的长度字节位,再向后读取长度个字节,获得数据,定长的数值型则直接读取相应的固定的字节数,即可获得数据。

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字节填充字符串空白
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填充到绝对位置</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

$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>
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345

推荐阅读更多精彩内容