深入剖析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」

深入剖析protobuf

那是什么因素决定是什么构造的呢?别急~,接下来就是介绍这个了,主要是因为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的主要内容,如果未能解决你的问题,请参考以下文章

深入protoBuf

LinkedList源代码深入剖析

ArrayList源代码深入剖析

深入剖析python小数据池,代码块

分享《深入浅出深度学习:原理剖析与python实践》PDF+源代码

python小数据池,代码块深入剖析