protobuf介绍
protocol buffers 是一种语言无关、平台无关、可扩展的序列化结构数据的方法,它可用于(数据)通信协议、数据存储等。
Protocol Buffers 是一种灵活,高效,自动化机制的结构数据序列化方法-可类比 XML,但是比 XML 更小(3 ~ 10倍)、更快(20 ~ 100倍)、更为简单。
你可以定义数据的结构,然后使用特殊生成的源代码轻松的在各种数据流中使用各种语言进行编写和读取结构数据。你甚至可以更新数据结构,而不破坏由旧数据结构编译的已部署程序。
以上为官网介绍,主要有三大特点:
- 语言无关、平台无关;
- 性能好
- 扩展性好
尤其是在微服务场景中,rpc调用非常频繁,使用合适的序列化协议会大大提高系统的性能。
protobuf编码
和json这类序列化不一样,在protobuf中,key是不直接存在序列化协议中的,而是在本地存好proto文件(文件中定义各类字段的类型和顺序),protobuf只需存储字段编码类型和字段顺序即可。
如上图所示,每个value可以变为tag-[length]-value格式,其中length是可选的。
tag使用Varints编码,由field_number 和 wire_type 两个部分组成:
field_number : proto文件定义的字段序号
wire_type: protoBuf 编码类型
对每个字段field来说,根据类型有以下两种格式:
1. tag-[length]-value,使用Length-delimited 编码类型;
2. tag - value:varint、64-bit、32-bit
Varints编码
主要规则有三点:
- 每个字节第一位是msb(most significant bit),标识是否需要读下一个字节。0代表不需要,1代表需要;
- 存储数字对应的二进制补码;
- 低位字节在前,字节内位顺序不变
为什么这么设计呢?首先看第一个规则,显然这是个利用字节最高位来判断是否需要读下一个字节,从而省去了length,实现了变长的优点。在数字较小时能省较多的空间。如在java中,数字1是4个字节,用varints一个字节即可表示。当然缺点也是有的,即能表示的范围比正常的要小,如int32只能表示2的28次方,大于此值的只能用5位字节来表示了。用补码存储就不说了,计算机用补码统一进行就加法计算,不需要单独考虑符号位。至于低位字节在前,也是因为方便位计算。
while (true)
{
if ((value & ~0x7FL) == 0)
{
buf[locPtr++] = (byte) value;
//其他代码忽略
return this;
}
else
{
buf[locPtr++] = (byte) (((int) value & 0x7F) | 0x80);
value >>>= 7;
}
}
以上是protostuff实现编码的代码,value & ~0x7FL是用来判断是否能用7位表示。如果不能,说明varints至少要2个字节。value & 0x7F表示取最后7个字节,再| 0x80表示最高为置1。(value & 0x7F) |
0x80这步即是取后面的字节,高位置1,然后存储在前面。
那如果负数会如何?因为负数最高位是1,又int32兼容int64,所以补码总是8个字节,加上msb就是十个字节。
ZigZag 编码
ProtoBuf为我们提供了 sint32、sint64 两种类型,来解决负数效率低的问题。当你使用这2种类型的时候protobuf使用zigzag编码。用一句话概括就是:
ZigZag 编码:有符号整数映射到无符号整数,然后再使用 Varints 编码
假设int32 val=-1,映射为1,然后再用varints编码。实际上这种映射是用位运算来计算的。
public static int encodeZigZag32(final int n)
{
return (n << 1) ^ (n >> 31);
}
Length-delimited 类型
除了varints编码,固定编码,只剩Length-delimited了,这个类型是唯一的tag-length-value,有string、bytes、EmbeddedMessage、repeated。编码方式也比较好理解。
总结
protobuf序列化简单明了,效率较高。在实际项目中对于字段的编码类型要谨慎点(选protobuf作为序列化基本上会要求性能了),尤其是string类型,无任何压缩,长度也基本上没有上限。