本文介绍protobuf的编码方式。源地址
一个简单的消息
message Test1 { required int32 a = 1; }
我们创建一个Test1
消息,给a
赋值150,序列化之后存储,我们得到如下的3个byte数据(16进制表示):
08 96 01
非常的简洁,那么这是如何得到的呢?下面我们开始分析
Varints编码
首先介绍Varints编码,它是一种对int型变量的变长编码,使用1个或者多个byte表示一个整数。在这种编码方式下,小的整数需要更少的字节表示。
Varints使用Litte-Endian(小端)字节序(低位在前,高位在后)。下面是Varints的两个基本规则:
- (1)每个字节的最高位是标志位,0表示该字节是最后一个字节,1表示该字节后面还有其他字节
- (2)每个字节的低7位存储数值的位
举例来说,整数 1 经过编码后是:
0000 0001
只有一个字节,可以看出,Varints编码去除了全0无意义的高位字节。
另一个例子,整数300,编码后的结果:
1010 1100 0000 0010
我们稍微分析一下,首先,每个字节的最高位是标志位,去掉
1010 11000000 0010 010 1100 000 0010
Varints使用Litte-Endian字节序,我们要将高位与地位调换
000 0010 010 1100 → 000 0010 ++ 010 1100 → 100101100 → 256 + 32 + 8 + 4 = 300
如此,我们便得到了结果,下面我们分析一下编码的过程是怎样的:
300 = 100101100 从右向左,每7位分为一组,不足7位补零 000 0010,010 1100 使用Litte-Endian字节序 010 1100,000 0010 按照规则(1) 补0或补1 1010 1100,0000,0010
消息结构
protobuf的消息就是一系列的key-value对,key在编码时使用field(就是变量=后面那个数),而不使用变量名。
value的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和type,格式为:(field_number << 3) | type。也就是说,后三个bit存储的是type。本文开头的例子,存储的第一个字节是08,二进制为
0000 1000
type=000=0,field_number=0000 1=1,符合a的field_number,a是整形,type=1。后面两个字节
96 01 = 1001 0110 0000 0001 → 000 0001 ++ 001 0110 (drop the msb and reverse the groups of 7 bits) → 10010110 → 2 + 4 + 16 + 128 = 150
正是给a赋的值150。到此我们已经理解了本文开头的简单示例。
非整数变量
非整数变量比较简单,double和fixed64的type是1,float和fixed32的type是5,因为长度固定,得知type就知道怎么取value。
对于type=2,value存储的是 长度+value值,举个例子:
message Test2 { required string b = 2; }
给b赋值"testing",编码结果如下:
12 07 74 65 73 74 69 6e 67
红色部分是UTF-8编码的"testing",key=0×12 → field_number = 2, type = 2 ; 0×07是长度,共有7个字节。
protobuf 编码的基础知识大概就是这样,更多的内容可以参考官方文档。学习了这些具体的编码方式,我们才会更深刻的理解,为啥protobuf比 XML 小 3 到 10 倍,快 20 到 100 倍。