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:
Type | Meaning | Used For |
---|---|---|
0 | Varint | int32, int64, uint32, uint64, sint32, sint64, bool, enum |
1 | 64-bit | fixed64, sfixed64, double |
2 | Length-delimited | string, bytes, embedded messages, packed repeated fields |
3 | Start group | groups (deprecated) |
4 | End group | groups (deprecated) |
5 | 32-bit | fixed32, 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 Original | Encoded As |
---|---|
0 | 0 |
-1 | 1 |
1 | 2 |
-2 | 3 |
2147483647 | 4294967294 |
-2147483648 | 4294967295 |
即:任意 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)