protobuf 编码

Protobuf 内部采用 Varint 编码来压缩数据,因此效率比 Json、XML 等要高。注意:Protobuf 采用 little-endian 模式。

1、Varint

Varint 是一种紧凑的数字表示方法,用一个或多个字节表示一个数字。值越小的数字,占用的字节数越少。Varint 的每个 byte 的最高位(MSB - Most Significant Bit)有特殊含义:若最高位为 1,则表示后续的 byte 也是该数字的一部分,如果为 0,则结束。

例如:数字 300
-> 1010 1100 0000 0010
-> 010 1100 000 0010 (丢弃每个字节的 MSB 位)
-> 000 0010 010 1100 (protobuf 为小端序)
-> 100101100
-> 256 + 32 + 8 + 4 = 300

2、Protobuf 消息结构

protobuf 消息是 key-value 序列。序列化后的二进制消息使用字段的序号作为 key。
消息解码时,解析器能够跳过无法识别的字段,因而在消息中添加了新字段后,未升级的旧程序也能兼容继续使用。为此,在 wire-format 消息中,每个 key 实际上是两个值:proto文件中的字段编号及 wire type(即:key = number + wire_type)。wire type 表明 value 的长度。在多数实现中,这种 key 也称为 tag。
每种数据类型都有对应的 wire type:

TypeMeaningUsed For
0Varintint32, int64, uint32, uint64, sint32, sint64, bool, enum
164-bitfixed64, sfixed64, double
2Length-delimitedstring, bytes, embedded messages, packed repeated fields
3Start groupgroups (deprecated)
4End groupgroups (deprecated)
532-bitfixed32, sfixed32, float

消息中 key 的值为:(field_number << 3) | wire_type,即:key 的后三位存储的是 wire type(因此最多 8 种,目前定义了 6 种)。

消息流中,第一个数字始终是 varint key,假设为 0x08:
  -> 000 1000 (去除 MSB 位)
后三位为 000,即:wire type 为 0;再右移三位,得到字段序号 1。由此可知当前为字段 1,且值为 varint 格式。
假设 0x08 后面数字为 0x96 0x01,则:
 -> 0x96 0x01 = 1001 0110 0000 0001
        -> 001 0110 000 0001 (丢弃 MSB)
        -> 000 0001 001 0110 (little-endian)
        -> 10010110
        -> 128 + 16 + 4 + 2 = 150

3、更多数值类型
1)signed integers
signed int 类型(sint32、sint64)和标准的 int 类型(int32、int64)在处理负数时有很大区别。若使用 int32 或 int64 来表示负数,实际上被视为一个非常大的无符号数,则 int32 需要 5 个字节,int64 则需要 10 个字节。
若使用 signed int 类型,则使用 ZigZag 编码来生成 varint。

ZigZag 编码将有符号整数映射为无符号整数,以便具有较小绝对值的数字也具有较小的 varint 编码值。通过将正整数和负整数来回 “zig-zag” 实现,因此 -1 编码为 1,1 编码为 2,-2 编码为 3,…

Signed OriginalEncoded As
00
-11
12
-23
21474836474294967294
-21474836484294967295

即:任意 sint32 整数 n 编码为 (n << 1) ^ (n >> 31),其中,当 n 为整数时 (n >> 31) 为 0,n 为负数时,(n >> 31) 为 1;sint64 则为 (n << 1) ^ (n >> 63)

2)non-varint 数字
non-varint 类型很简单: double 和 fixed64 的 wire type 为1;float 和 fixed32 的 wire type 为 5。

3)strings
wire type 为 2(length-delimited),该值的表示方法是: varint编码的长度值+指定数量的字节数据。例如:

message Test2 {
    optional string b = 2;
}

赋值为 “testing”,编码后的内容为
  12 07 74 65 73 74 69 6e 67
  key 为 0x12 -> 001 0010 (丢弃 MSB)
             -> 低三位 010,即: wire type 为 2
             -> 右移三位: 0010,即: 字段值为 2
字符串长度为: 0x07(varint 编码)
        -> 000 0111 (丢弃 MSB)
       -> 即:字符串长度为 7Bytes

4)embedded message(嵌套的消息类型)
embedded message 的处理方式与字符串完全相同(wire type 为 2)。例如:

message Test1 {
  optional int32 a = 1;
}
message Test3 {
  optional Test1 c = 3;
}

设置 Test1 中 a 字段的值为 150,则序列化后的数据为: 1a 03 08 96 01
    08 96 01 为 Test1 序列化后的表示。
    key 为 1a -> 0001 1010
            -> 后三位为 010,即: wire type 为 2
            -> 右移三位 0011,即: key 的编号为 3
因 wire type 为 2,因此后面的 03 表示字符串的长度为 3 个字节。

5)Optional 及 Repeated 元素
proto3 中,repeated 字段使用 packed 编码。proto3 中,未指定字段描述符的字段即为 optional(proto2中需使用 optional 字段描述符)。

6)packed repeated 字段
若 repeated 字段没有任何实际元素,则不会出现在编码后的消息中,否则该字段的所有元素都将打包为 wire type 为 2 的单个 key-value。该字段内部每个元素的编码方式与普通字段相同,不同之处在于 value 前面没有 key。例如:

message Test4 {
  repeated int32 d = 4 [packed=true];// proto3 无需 [packed=true]
}

假设字段 d 中包含数值:3、270、86942,则编码后的消息为:
    22 -> 0010 0010 -> 字段 4,wire type 为 2
    06 -> 由 wire type 可知为 string 的长度 (6bytes)
    03 -> 第一个元素值 (varint 3)
    8E 02 -> 第二个元素值 1000 1110 0000 0010
        -> 000 1110 000 0010 (丢弃 MSB)
       -> 000 0010 000 1110 (little-endian)
        -> 100001110 = 270
    9E A7 05 -> 第三个元素 (varint 86942)

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值