protobuf3编解码详解

Base 128 Varints

Varints是一种使用一个或多个字节表示整型数据的方法。其中数值本身越小,其所占用的字节数越少。

在Varint中,除了最后一个字节之外,其余每个字节中都包含一个MSB(最高位),字节中的其余七位将用于存储数据本身。在解码的时候,如果读到的字节的 MSB 是 1 话,则表示还有后序字节,一直读到 MSB 为 0 的字节为止。

通常而言,整数数值都是由字节表示,其中每个字节为8位,即Base 256。然而在Protocol Buffer的编码中,最高位成为了msb,只有后面的7位存储实际的数据,因此我们称其为Base 128。

将数据转换成Varints编码有如下五个步骤:

  1. 将数据转换为二进制。
  2. 从 LSB 到 MSB 每七位分一组,不足七位高位补零。
  3. 最左边一组高位补0,其他组高位补1。
  4. 转换成十六进制。
  5. 从 LSB 到 MSB 输出结果(即小端的表示方法)。

举个例子,整数624485 可以作如下编码:

 MSB ------------------ LSB
       10011000011101100101  <1> 二进制 20bit
  0100110  0001110  1100101  <2> 从 LSB 到 MSB 每七位分一组,不足七位高位补零
 00100110 10001110 11100101  <3> 最左边一组高位补零,其他组高位补一
     0x26     0x8E     0xE5  <4> 转换成十六进制
 → 0xE5 0x8E 0x26            <5> 从 LSB 到 MSB 输出结果

编码方式

protobuf3每个字段编码后从逻辑上分为四个部分:

<tag> <type> [<length>] <data>

其中<tag>表示字段(或变量)的唯一标识符<type>是该字段的类型,<length>表示字段的长度,<data>是经过Varints编码(字符串用UTF-8 编码)的数据。

<tag><type>共占1个字节,但是<tag>超过 16 时,就需要用两个以上的字节表示了,<length>是可选的。

protobuf3定义了4种类型的type:

0 	VarInt 	表示int32,int64,uint32,uint64,sint32,sint64,bool,enum
1 	64-bit 	表示fixed64,sfixed64,double
2 	Length-delimited 表示string,bytes,embedded messages,repeated字段
5 	32-bit 	表示fixed32,sfixed32,float

其中 3 和 4 表示的类型已经废弃,不多讨论。因为类型比较少,所以 protubuf3在编码type的时候只用了3比特,实际传输的时候是以 (tag<<3)|type 的方式传输的,即<tag><type>共占1个字节。但是,单字节的最高位是零,而最低3位表示类型,所以只剩下 4 位可用了,因此<tag>超过 16 时,就需要用两个以上的字节表示了。

使用<tag>的优点是不用重复传输字段名,但也是它的缺点。因为没有字段名,所以编码和解码的代码必须持有一份字段名和 tag 的映射关系,这是在生成代码的时候自动完成的。也就是说,没有 proto 文件,你是没法对 Protocol Buffers 数据进行解码的。

正数的编码

message Foo {
  int32  foo = 1;
}

Foo 的 foo 字段取值为 1 的话,则对应的编码是:0x08 0x01。foo 的类型是 int32,对应的 type 取 0。而它的 tag 又是 1,所以第一个字节是 (1<<3)|0 = 0x08,第二个字节是数字 1 的 VarInts 编码,即 0x01。

  7       0 7      0
 +-----+---+--------+
 |00001|000|00000001|
 +-----+---+--------+
   tag  type  data

字符串的编码

message Foo {
  string bar = 2;
}

Foo 的 bar 字段取值为 吕 的话,则对应的编码是:0x12 0x03 0xe5 0x90 0x95。bar 的类型是 string,对应的 type 取 2。而它的 tag 又是 2,所以第一个字节是 (2<<3)|2 = 0x12,第二个字节表示字符串的长度为 3,再后面 3 个字节是汉字吕 UTF-8 编码。

  7       0 7      0 25        0
 +-----+---+--------+===========+
 |00010|010|00000011|0xe50x90x95|
 +-----+---+--------+===========+
   tag  type length    utf-8

嵌套消息的编码

 message Baz {
   int32 b = 1;
 }
 message Bar {
   repeated int32 a = 1;
              Baz b = 2;
 }

如果我们让 Bar 的 a 字段取 [1,2,3],让 b 字段取 {4},则对应的编码为 0x0a 0x03 0x01 0x02 0x03 0x12 0x02 0x08 0x04。这段数据可以拆成两部分:

1. 0x0a 0x03 0x01 0x02 0x03
2. 0x12 0x02 0x08 0x04

先说第一部分。因为 a 的类型为 repeated int32,所以对应 type 取 2;又 a 的 tag 为 1,所以第一个字节应该是 (1<<3|2) = 0x0a。第二个字节表示数组长度,所以是 0x03,接下来三个字节分别是 1, 2, 3 的 VarInts 编码。

再说第二部分。因为 b 的类型为 Bar,所以对应的 type 也是 2;又 b 的 tag 为 2,所以第一个字节应该是(2<<3|2) = 0x12。第二个字节表示 message 的长度,所以是 0x02,接下来两个字节表示 Baz 的编码,Baz中 b 的类型是 int32,对应的 type 取 0。而它的 tag 又是 1,所以第一个字节是 (1<<3)|0 = 0x08,第二个字节是数字 4 的 VarInts 编码,即 0x04。

  7       0 7      0 25        0 7       0 7      0 7       0 7      0
 +-----+---+--------+===========+-----+---+--------+=====+===+========+
 |00001|010|00000011|0x010x02x03|00010|010|00000010|00001|000|00000100|
 +-----+---+--------+===========+-----+---+--------+=====+===+========+
   tag  type length    utf-8      tag  type length    tag type   data
                                                   |<----- baz.b ---->|
 |<---------- Bar.a ----------->|<-------------- Bar.b -------------->|

单从数据来看,我们无法区分 string,repeated 和 message。要想解析这类数据,必须依赖 proto 定义。

负数的编码——ZigZag

VarInts 不太适合表示负数。因为负数在计算机使用补码表示,转成 unit64 是一个很大的数。当你使用 VarInts 表示的时候,-1 居然要占用 10 个字节!为此,Protocol Buffers 引入了 sint32 和 sint64 两种类型,在编码的时候先将数字转化成 ZigZag 编码。ZigZag 思想也很简单,就是用正数来表示负数,映射规则如下:

(n << 1) ^ (n >> 31)    //sint32
(n << 1> ^ (n >> 63)   //sint64

protobuf3在实现上述位移操作时均采用的算术位移,因此对于(n >> 31)和(n >> 63)而言,如果n为负值位移后的结果就是-1,否则就是0。

ZigZag对照表如下:

   Original  ZigZag
     0			0
    -1			1
     1			2
 	-2			3
 2147483647	4294967294
-2147483648	4294967295
  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值