深入剖析protobuf
Posted 歪鼻子
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了深入剖析protobuf相关的知识,希望对你有一定的参考价值。
1.语法
简介什么的就不提了,最简单的就语法开始吧(基于protobuf2,没办法,公司就用这个版本编码)
// 定义一个名字为xxx的消息体
message xxx {
字段规则 类型 名称 = 字段编号;
// 字段规则:required -> 字段只能也必须出现 1 次
// 字段规则:optional -> 字段可出现 0 次或1次
// 字段规则:repeated -> 字段可出现任意多次(包括 0)
// 类型:int32、int64、sint32、sint64、string、32-bit ....
// 字段编号:0 ~ 536870911(除去 19000 到 19999 之间的数字)
}
例子
syntax = "proto2";
message waibizi{
required string name = 1;
required int32 age = 2;
optional string phone = 3;
// 嵌套使用
message company{
required string company_name = 1;
required string company_address = 2;
}
repeated company waibizi_company = 4;
}
生产go代码
语法:
protoc --go_out=$DST_DIR $SRC_DIR
解释:
$DST_DIR是输出到哪里,$SRC_DIR是.proto后缀文件
例子:
protoc --go_out=. test.proto
2.编码结构
对于protobuf我们最常见的莫过于字段,而protobuf的优秀莫过于对于字段的编码控制,我们先简单看一看字段的数据结构吧
对于字段的,一般有两种表现形式:「Tag - Length - Value」、「Tag - Value」
那是什么因素决定是什么构造的呢?别急~,接下来就是介绍这个了,主要是因为tag,先看看tag的结构吧
tag由子弹编号field_number和编码类型wire_type组成,tag的整体采用的是Varints编码
后三位有一个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、embedde messages、packed repeated fields |
3 | Start group | groups(deprecated) |
4 | End group | groups(deprecated) |
5 | 32-bit | fixed32、sfixed32、float |
「Tag - Length - Value」:编码类型表中 Type = 2 即 Length-delimited 编码类型将使用这种结构,
「Tag - Value」:编码类型表中 Varint、64-bit、32-bit 使用这种结构。
谈谈Varint编码
Varint可以说是一种编码方式,也可以说是一种压缩算法,这个算法来自于现实当中的一个现象:「越小的数字,越经常使用」
如果不进行压缩的话,那我们平时传输一个32位整形的数字1是怎么传输的呢?
32位的数字1: 00000000 00000000 00000000 00000001
我们可以看到,这一堆需要传输的数据当中,有很多位数都是需要填零补充的,所以就会导致了我们需要传输很多的无效数据,而Varints就是为了解决这个问题的,接下来我们来看看Varints的编码方式
-
除了最后一个字节,varint中的每个字节的最高位设为1,表示后面还有字节出现 -
每个字节的低7位看成是一个组(group),这个组和他相邻的下一个7位组共同存储某个整形的“组合表示”,最低有效组在前面。
下面举个例子说明一下
-
1个字节,假设数字是 00000001
则经过Base 128 Varint编码后,还是原来的样子 (0#0000001)
-
2个字节,这次我们来个解码,假设经过Base 128 Varint编码后的01串是 1#0101100 0#0000010
根据定义,第1个字节的最高位是1,说明后面还是有数据。我们往后看,第2个字节的最高位是0,好,说明这个数编码后是使用2字节的了。
取第1个字节的低7位,为:0101100
取第2个字节的低7位,为:0000010
我们再来看这句:最低有效组在前面
然后倒过来组合起来,即为原01串:
0000010 0101100
PS:这里的倒装方式涉及到编码方式的大小端
-
3个字节,我们来看看怎么对 01 1010110 1100011进行编码
a.从低位到高位,取7位为一个组(不足7位前面补0),这里为
1100011
1010110
0000001
b.反转组装 1#100011 1#010110 0#000001
c.除了在最后一个字节补0,其他字节补1,即为:11100011 11010110 00000001
可以发现采用Varint编码的最大表示的数为2^28,有得必有失,毕竟Varint的「压缩的依据是基于一个现实:越小的数字,越经常使用」
回顾一下wire_type,就可以看到protobuf在选择的时候也是将小的数字采用了Varint进行编码
但是Varint对于负数的编码却存在缺陷
int32 val = -1
原码:1000 ... 0001 // 注意这里是 8 个字节
补码:1111 ... 1111 // 注意这里是 8 个字节
再次复习 Varints 编码:对补码取 7 bit 一组,低位放在前面。
上述补码 8 个字节共 64 bit,可分 9 组且这 9 组均为 1,这 9 组的 msb 均为 1(因为还有最后一组)
最后剩下一个 bit 的 1,用 0 补齐作为最后一组放在最后,最后得到 Varints 编码
Varints 编码:1#1111111 ... 0#000 0001 (FF FF FF FF FF FF FF FF FF 01)
可以看到。。。。。一个-1采用Varint居然要十个字节去存储,这时候我们的Protobuf怎么可以容忍这种事情的出现,提供了另外了一种编码方式,但是声明的数据类型需要采用sint32、sint64,这种编码方式就是:ZigZag 编码
ZigZag 编码
对于负数来说,因为最高位符号位始终为1,使用varint编码就很浪费空间,zigzag编码就是解决负数的问题的,同时其对正数也没有很大的影响。
int类型zigzag变换的代码表示为(n << 1) ^ (n >> 31)
-
左移1位可以消去符号位,低位补0 -
按位异或 -
对于正数来说,最低位符号位为0,其他位不变 -
对于负数,最低位符号位为1,其他位按位取反
-1的二进制补码表示为11111111 11111111 11111111 11111111
,zigzag变换后00000000 00000000 00000000 00000001
,再用varint编码,是不是很小了。
1的二进制表示为00000000 00000000 00000000 00000001
,zigzag变换后00000000 00000000 00000000 00000010
,再用varint编码,依然很小。
-2的二进制表示为11111111 11111111 11111111 11111110
,zigzag变换后 00000000 00000000 00000000 00000011
,再用varint编码,依然很小。
「编码步骤:」
-
zigzag =>> varint
「解码:」
解码步骤和编码步骤相反,先读取varint,再执行ZigZag解码
以上是关于深入剖析protobuf的主要内容,如果未能解决你的问题,请参考以下文章