Protocol Buffer
Protocol Buffer, 简称 ProtoBuf
由 Google 开发,是一种开源跨平台的序列化协议
Proto2 Diff Proto3
Proto3是Proto2的简化版本
默认值
Proto3不允许自定义默认值, 在Proto3中的所有字段都具有一致的零默认值
必填字段
Proto3删除了required
字段的支持
编码
varints
可变宽整数, 使用 1 到 10个字节之间的长度对无符号64位整数进行编码,较小的值可以使用较少的字节
在varint中,每个字节都有一个标志位,表示它后面的字节是否时varint的一部分
这个字节是MSB(Most Significant Bit / 最高有效位), 低 7 位是有效载荷
0000 0001
^ msb
1000 0011 0000 0001 // 131
^ msb ^ msb
那么是如何得出是 131 的呢
1000 0011 0000 0001 // 原始输入
000 0011 000 0001 // 去除MSB
000 0001 000 0011 // 按小端排列
10000011 // 连接
128 + 2 + 1 = 131 // 解释为十进制
字段编号和类型
当对消息进行编码时,每个键值对会变为一个记录
其中包含字段编号(field number 简称 fn)、类型(wire type 简称 wt)和有效负载
其中字段编号和类型 使用 varint 格式编码, 最低三位表示类型
Wire Type | 解析 | 对应数据类型 |
---|---|---|
0 | varint 变长整型 | int32, int64, uint32, uint64, sint32, sint64, bool, enum |
1 | 固定8字节 | fixed64, sfixed64, double |
2 | 后面需要跟随一个长度 | string, bytes, required字段, 嵌套类型 |
3 | 废弃 | |
4 | 废弃 | |
5 | 固定4字节 | fixed32, sfixed32, float |
// 消息定义
message example {
int32 i32 = 1;
fixed64 f64 = 2;
string str = 3;
sint32 s32 = 4;
}
// 假设序列化时的对象信息
{
i32: 257,
f64: 1,
s32: -2,
b: Buffer.from([0xaa, 0xbb, 0xcc, 0xdd]),
}
//编码后 <Buffer 08 81 02 11 01 00 00 00 00 00 00 00 1a 04 aa bb cc dd 20 03>
首先分析第一个字节 0x08 二进制 0000 1000
去除MSB, 000 1000
后三位表示类型 wt = 000 = 0, 表示数据类型为变长整形
剩余字段表示字段编号 fn = 0001 = 1
第二个字节 0x81 => 1000 0001
MSB为1, 表示后面的数据是其的一部分
第三字节 0x02 => 0000 0010
MSB为0, 表示是数据最后一个字节
按照小端排列并连接起来得到
000 0010 000 0001 => 257
即前三个字节表示, 字段编号1, 数据类型为变长整形
对应到消息定义的类型为int32, 对应数据为 257
接着分析第四个字节 0x11 => 0001 0001
fn = 2, wt = 1
即有效载荷为 01 00 00 00 00 00 00 00
根据小端排序后 00 00 00 00 00 00 00 01
对应到消息定义的类型为fixed64, 对应数据为 1
接着分析第十三个字节 0x1a => 0001 1010
fn = 3, wt = 2
当 wt = 2 时, 后面紧跟一个 varint 编码的数值表示有效载荷的长度
第十四字节 0x04 , 表示后面的有效载荷的长度为4, 为 0xaa, 0xbb, 0xcc, 0xdd
接着分析第十九个字节 0x20 => 0010 0000
fn = 4, wt = 0
第二十个字节为 0x03 => 0000 0011
对应到消息定义的类型为s32, sintN
使用了ZigZag编码,而不是二进制补码的来编码负整数
总结
- 当小负数时应该选择 sint32, sint64 相比 int32 和 int64 在负数时更具效率, 因为使用了 ZigZag 编码
- 当数字大于 2^56 那么定长8字节的 fixed64 比 uint64 更高效, 因为 uint64 每个字节只有7位有效负荷
- 当数字大于 2^28 那么定长4字节的 fixed32 比 uint32 更高效, 原因一样