Protobuf编码规则详解

1 Message 结构

Message由一系列field组成,每个字段都使用TLV(Tag-Length-Value)结构形式。每个字段都有一个 tag 值,length 表示 value 数据编码后的长度,length 不是必须的,对于固定长度的和使用Varint编码的 value,是没有 length 的。value 是数据本身的内容。
在这里插入图片描述

1.1 tag

  • tag 有 field_number 和 wire_type 两部分组成,组成格式:field_num << 3 | wire_type
  • tag使用Varint编码
  • tag是要占空间的,如果tag>16时,KEY的编码就会占用2个字节了

结构如下图
在这里插入图片描述

1.1.1 字段编号(field_num)

就是.proto文件中定义的字段编号

1.1.2 传输类型(wire_type)

每个字段都有一个对应的字段(传输)类型,如下表:

TypeMeaningUsed ForStructurevalue的字节序value编码格式Length
0Varintint32, int64, uint32, uint64, sint32, sint64, bool, enumTag-Value小端字节序Varint编码变长 无Length值
164-bitfixed64, sfixed64, doubleTag-Value小端字节序非Varint编码固定8字节
2Length-delimitedstring, bytes, embedded messages, packed repeated fieldsTag-Length-Value变长,有Length值
3Start groupgroups (deprecated)
4End groupgroups (deprecated)
532-bitfixed32, sfixed32, floatTag-Value小端字节序非Varint编码固定4字节
  • 消息的二进制格式只使用消息字段的字段编号(field_num)和write_type(根据proto文件定义的类型对应而来)作为Tag的一部分,字段名和声名类型只能在解析端通过引用参考消息类型的定义(即.proto文件)才能确定。
  • 解码的时候解码程序(解码器)读入二进制的字节流,解析出每一个field;如果解码过程中遇到识别不出来的filed_num就直接跳过。这样的机制保证了即使该消息(message)添加了新的字段,也不会影响旧的编/解码程序正常工作。

1.2 字段顺序

字段编号可以在 .proto 文件中以任何顺序使用,编码 / 解码与字段顺序无关。
序列化 message 时,对于如何写入其已知字段或未知字段没有保证的顺序。解析消息不能认为filed_num=1 的消息一定在最前。

1.3 默认值

编码时如果没有对字段设置值,protobuf就不会把该字段编码到消息中。
解析数据时,如果编码的消息不包含特定的字段,则解析将对象中的相应字段将设置为该字段的默认值
不同类型的默认值不同,具体如下:

  • 对于字符串,默认值为null
  • 对于字节,默认值为空字节
  • 对于bool,默认值为false
  • 对于数字类型,默认值为零
  • 对于枚举,默认值是第一个定义的枚举值,该值必须为0。
  • repeated字段默认值是空列表
  • message字段的默认值为空对象

2 编码

2.1 Varint编码

Varint是一种将一个整数序列化为一个或者多个Byte的方法。越小的整数,使用的Bytes越少。Varint规则如下

  • 每个Byte的最高位是标志位(msb, most significant bit)。如果值为1,表示该Bytes后面还有其他Byte;如果该位为0,表示该Byte是最后一个Byte。
  • 每个Byte的低7位是用来存数值的位。
  • Varint方法使用小端字节序(低位在前编码),通常都是大端字节序(高位在前)

2.1.1 Varint编码过程

步骤/值(10进制)65128
1大端字节序二进制(低位在后/右)100000110000000
27位一分隔 (从低开始计数分隔)10000010000001,0000000
3补标志位01000001,后边高位没1了,标志位补000000001(后边高位没1了,标志位补0),10000000(后边高位有1标志位补1)
4翻转变为小端字节序(低位在前/左)0100000110000000 00000001

2.1.2解码过程

Varints 的解码就是对编码的逆操作

10进制数字65128
1pb编码值(小端字节序,低位在前/左),从低位(左)8位一分格0100000110000000 00000001
2去补标志位(最高位)10000010000000 0000001
3翻转为大端字节序(低位在后/右)10000010000001 0000000

2.1.3 存储

一个字节的 Varints 编码有 7 位可以存储数据(最高位为 msb),则可以传输 [ 0 , 2^7 -1] 以此类推,两个字节就是 [ 2 ^7 , 2^14 − 1 ]

2.1.4 小结

Varint 确实是一种紧凑的表示数字的方法。它用一个或多个字节来表示一个数字,值越小的数字使用越少的字节数。这能减少用来表示数字的字节数。比如对于 int32 类型的数字,一般需要 4 个字节来表示。但是采用 Varints,对于很小的 int32 类型的数字,则可以用 1 个字节来表示。当然凡事都有好的也有不好的一面,采用 Varint 表示法,大的数字则需要 5 个字节来表示。从统计的角度来说,一般不会所有的消息中的数字都是大数,因此大多数情况下,采用 Varint 后,可以用更少的字节数来表示数字信息。如果确定传输大的数字,可以考虑fixed32/fixed64 类型

2.2 有符号整数(sint32和sint64)编码的问题与zigzag优化

protocol buffer中 write_type=0 的都使用Varint编码。当数值为负数时,有符号整型(sint32, sint64)和标准整型(int32, int64)有一个重要的差别。如果使用 int32 或 int64 存储负数,那么 Varints 编码后的结果一定是 10 个字节(int32 类型的负数也是占用10个字节)。而如果使用 sint32 或 sint64 存储负数,则会使用效率更高的 ZigZag 编码。
为此 Protobuf 定义了 sint32 和 sint64 这种类型,采用 ZigZag 编码。将所有整数映射成无符号整数,然后再采用 Varints 编码方式编码,这样绝对值小的整数,编码后也会有一个较小的 varint 编码值。ZigZag 映射函数为:
Zigzag(n) = (n << 1) ^ (n >> 31), n 为 sint32 时
Zigzag(n) = (n << 1) ^ (n >> 63), n 为 sint64 时
或简单理解:
正数 Zigzag(n) =2n
负数 Zigzag(n) =2
abs(n)-1

映射结果如下:

Signed OriginalEncoded As
00
-11
12
-23
24
-35
21474836474294967294
-21474836484294967295

3 编码实践

3.1测试数据

message S2
{
    optional int32 s2_1 = 1;
    optional string s2_2 = 2 ;
}
 
enum E1 
{
    E1_0 = 0;
    E1_1 = 1;
    E1_3 = 3;
    E1_5 = 5;
}
 
message  S3
{
    optional int32 s3_1 = 1;      //设置为0x88
    optional int32 s3_2 = 2;      //设置为0x8888
    optional uint32 s3_3 = 3;     //设置为0xE8E8E8
    optional uint32 s3_4 = 4;     //设置为0xE8E8E8E8
    optional int64 s3_5 = 5;      //设置为0x8888
    optional int64 s3_6 = 6;      //设置为0xE8E8E8E8
    optional uint64 s3_7 = 7;     //设置为0xE8E8E8E8
    optional uint64 s3_8 = 8;     //设置为0xE8E8E8E8E8E8E8E8
    optional sint32 s3_9 = 9;     //设置为0x8888
    optional sint32 s3_10 = 10;   //设置为-0x8888
    optional sint64 s3_64 = 64;   //注意这个tag id  设置为0xE8E8E8E8
    optional sint64 s3_65 = 65;   //注意这个tag id  设置为-0xE8E8E8E8
    optional E1 s3_11 = 11;       //设置为E1_5
    optional bool s3_12 = 12;     //设置为true
    optional float s3_13 = 13;    //设置 float,设置为88.888
    optional fixed32 s3_14 = 14;  //设置为 0x8888
    optional sfixed32 s3_15 = 15; //设置为 -0x8888
    optional double s3_16 = 16;   //设置 double,设置为8888.8888
    optional fixed64 s3_17 = 17;  //设置为 0x8888888888
    optional sfixed64 s3_18 = 18; //设置为 -0x8888888888
    optional string s3_19 = 19;   //设置为 "I love you,C++!"
    optional bytes s3_20 = 20;    //设置为 "I hate you,C++!"
    repeated int32 s3_21 = 21 [packed = false];    //设置为3, 270, and 86942, 用google文档的例子
    repeated int32 s3_22 = 22 [packed = true]; //设置为3, 270, and 86942
    repeated string s3_23 = 23;   //设置为"love","hate","C++"
    optional S2 s3_24 = 24;       //设置为 0x1,"love"
    repeated S2 s3_25 = 25;       //设置为 0x16,"love"  and 0x16,"hate"
    repeated fixed32 s3_26 = 26 [packed = false];  //设置为1,2,3
    optional int32 s3_27 = 27;    //不设置
}
分类说明定义FieldNumWriteType编码后(16进制)
VALUE用VARINT表示optional int32100x88 08 88 01
optional int32200x888810 88 91 02
optional uint32300xE8E8E818 e8 d1 a3 07
optional uint32400xE8E8E8E820 e8 d1 a3 c7 0e
optional int64500x888828 88 91 02
optional int64600xE8E8E8E830 e8 d1 a3 c7 0e
optional int64700xE8E8E8E838 e8 d1 a3 c7 0e
optional int64800xE8E8E8E8E8E8E8E840 e8 d1 a3 c7 8e 9d ba f4 e8 01
optional sint32900x888848 90 a2 04
optional sint32100-0x888850 8f a2 04
optional E1(enum)110E1_558 05
optional bool120true60 01
VALUE固定4个字节
FIXED32(5)
optional float13588.8886d a8 c6 b1 42
optional fixed321450x888875 88 88 00 00
optional sfixed321550x88887d 88 88 00 00
VALUE固定8个字节
FIXED64(1)
optional double1618888.888881 01 58 ca 32 c4 71 5c c1 40
optional fixed321710x888888888889 01 88 88 88 88 88 00 00 00
optional sfixed32181-0x888888888891 01 78 77 77 77 77 ff ff ff
string, bytes
embedded messages
packed repeated fields
LENGTH_DELIMITED(2)
optional string192"I love you,C++!"9a 01 of 49 20 6c 6f 76 65 20 79 6f 75 2c 43 2b 2b 21
optional bytes202"I love you,C++!"9a 01 of 49 20 6c 6f 76 65 20 79 6f 75 2c 43 2b 2b 21
optional repeated int32
[packed=false]
210 3,270,86942a8 01 03
a8 01 8e 02
a8 01 9e a7 05
optional repeated int32
[packed=true]
2223,270,86942b2 01 06 03 8e 02 9e a7 05
optional repeated string232"love","hate","C++"ba 01 04 6c 6f 76 65
ba 01 04 68 61 74 65
ba 01 03 43 2b 2b
optional S2(message)2420x1,"love" c2 01 08 08 01 12 04 6c 6f 76 65
repeated S2252S2{0x16,"love"} ,S2{0x16,"hate"} ca 01 08 08 16 12 04 6c 6f 76 65
ca 01 08 08 16 12 04 68 61 74 65
repeated fixed32[packed=false]2651,2,3d5 0101 00 00 00
d5 0102 00 00 00
d5 0103 00 00 00
可选没有设置optional int3227.没有设置没有数据

所有tag 转换过程都一样:值->二进制->分组->加标志位->翻转->转16进制

3.2 write_type=0 编码

Filed 为Tag-Value结构,没有Length,
Tag,Value 都采用 Varint编码(小端字节序)

3.2.1 int32, int64, uint32, uint64 编码

这四种类型的值直接对其做Varint编码

转换过程:值->二进制->分组->加标志位->翻转->转16进制

  • s3_1
    • tag :1<<3|0-> 0001000-> 0000 1000-> 0000 1000-> 0000 1000->->0x08
    • value: 0x88-> 10001000->000 0001,000 1000->0000 0001,1000 1000->1000 1000 ,0000 0001->88 01
  • s3_2
    • tag :2<<3|0->00010000->001 0000->0001 0000->0001 0000->10
    • value: 0x8888->1000100010001000->000 0010 ,001 0001 ,000 1000->0000 0010 ,1001 0001 ,1000 1000->1000 1000,1001 0001,0000 0010 ->88,91,02
  • s3_3
    • tag :3<<3|0->00011000->001 1000->0001 1000->0001 1000->18
    • value: 0xE8E8E8->111010001110100011101000->000 0111,010 0011 ,101 0001 ,110 1000->0000 0111,1010 0011 ,1101 0001 ,1110 1000->1110 1000,1101 0001,1010 0011,0000 0111-> E8 D1 A3 07
  • s3_4
    • tag :4<<3|0->00100000->010 0000->0010 0000->0010 0000->20
    • value:0xE8E8E8E8->000 1110,100 0111,010 0011,101 0001 ,110 1000->0000 1110,1100 0111,1010 0011,1101 0001 ,1110 1000->1110 1000,1101 0001,1010 0011,1100 0111,0000 1110->E8 D1 A3 C7 0E

3.2.2 sint32 sint64 编码

sint32 sint64 需要首先做一个zigzag 转化,转化后对值做Varint编码

tag 转换过程:值->二进制->分组->加标志位->翻转->转16进制
value转换过程:值->zigzag 转化->二进制->分组->加标志位->翻转->转16进制

  • s3_9
    • tag: 9<<3|0= 01001000-> 100 1000->0100 1000->0100 1000->48
    • value 0x8888-> 0x11110->00010001000100010000->000 0100,010 0010,001 0000->0000 0100,1010 0010,1001 0000->1001 0000 ,1010 0010,0000 0100->90 A2 04
  • s3_10
    • tag: 10<<3|0=01010000->101 0000->0101 0000->0101 0000->50
    • value 0x8888-> 0x1110F->00010001000100001111->000 0100,010 0010,000 1111->0000 0100,1010 0010,1000 1111->1000 1111,1000 1111,1010 0010,0000 0100->8F 8F A2 04

3.2.3 enum 编码

enum 是对其对应的数值做Varint编码

tag 转换过程:值->二进制->分组->加标志位->翻转->转16进制
value转换过程:值->对应数值->二进制->分组->加标志位->翻转->转16进制

  • s3_11
    • tag: 11<<3|0= 01011000-> 101 1000->0101 1000->0101 1000->58
    • value E1_5->0x5->0101-> 000 0101->0000 0101->0000 0101->05

3.2.4 bool编码

**bool,是对其对应的数值做Varint编码 ,true:1,false:0 **

value转换过程:值->对应数值->二进制->分组->加标志位->翻转->转16进制

  • s3_12
    • value true->0x1->0001-> 000 0001->0000 0001->0000 0001->01
    • value false->0x0->0000-> 000 0000->0000 0000->0000 0000->00

3.3 write_type=5 编码

Filed 为Tag-Value结构,没有Length,
Tag,采用 Varint编码(小端字节序)
Value 长度为固定定4个字节,小端字节序(非Varint编码),编码过程只是转成小端字节序即可

3.3.1 float 编码

**

编码过程只是转成小端字节序即可非Varint编码),固定长度4字节
**

  • s3_13
    • tag:13<<3|5->01101101-> 0110 1101 -> 0110 1101-> 0110 1101 ->6D
    • value:88.888f->1000010101100011100011010101000->100 0010,1011 0001,1100 0110 ,1010 1000->1010 1000,1100 0110,1011 0001,100 0010->A8 C6 B1 42

3.3.2 fixed32编码

这个用于处理32位整形的数值 和int32的区别就是

  • fixed32 长度为固定定4个字节,小端字节序(非Varint编码
  • int32 变长,小端字节序(Varint编码
  • s3_14
    • value:0x8888>1000100010001000->0000 0000 0000 0000 1000 1000 1000 1000->1000 1000 1000 1000 0000 0000 0000 0000->88 88 00 00

3.3.3 sfixed32编码

和fixed32 编码完全一样(不明白为什么弄了两个类型)

3.3 write_type=1 编码

Filed 为Tag-Value结构,没有Length,
Tag,采用 Varint编码(小端字节序)
Value 长度为固定定8个字节,小端字节序(非Varint编码),编码过程只是转成小端字节序即可

3.3.1 double编码

编码过程只是转成小端字节序即可(非Varint编码),固定长度8字节

3.3.2 fixed64编码

这个用于处理64位整形的数值 和int64的区别就是

  • fixed64 长度为固定定8个字节,小端字节序(非Varint编码
  • int64 变长,小端字节序(Varint编码

3.3.3 sfixed64编码

和fixed64编码完全一样(不明白为什么弄了两个类型)

3.4 write_type=2 编码

消息结构
在这里插入图片描述

Field含有Length

3.4.1 string编码

字段类型为 string 类型,字段值采用 UTF-8 编码,value 编码为正常字符串编码

  • s3_19
    • tag:19<<3|2-> 10011010->1 ,001 1010->0000 0001 ,1001 1010->1001 1010,0000 0001->9A 01
    • length: 15->1111->0f
    • value 正常字符串编码

3.4.2 repeated [packed] 原始数字类型 编码

  • repeated字段, proto2 默认 [packed=false],proto3中 默认 [packed=true]
  • 只有原始数字类型(使用varint,32位或64位)的repeated 字段才可以声明为 [packed=true]。
  • [packed=false] 时,编码后的 message 有一个或者多个包含相同tag(filed_num 和 **write_type 为对应的数值类型write_type**)的 field。这些 filed 不需要连续的出现。他们可能与其他的字段交错出现。尽管它们是无序的,但是在解析后它们是需要有序的。
  • [packed=true] 的repeated 字段所有的元素会被打包到单一一个 field 对中,且它的 wire_type=2,长度为所有数值编码后的长度之和。value编码按其定义类型的write_type编码,无缝排列到一起。
    在这里插入图片描述
  • s3_21[packed=false] repeated int32
    • 三个值,在Message中会有三个完整的field,三个field 的tag 都相同,write_type(因值类型为int32,所以write_type=0)
    • tag: 21<<3|0->10101000->1,0101000->0000 0001,10101000->1010 1000,0000 0001->A8 01
    • value1:3->0000 0011->03
    • value2:270->000100001110-> 0 0010 ,000 1110->0000 0010 ,1000 1110->1000 1110,0000 0010->8E 02
    • value3:86942->00010101001110011110->00 0101,010 0111,001 1110->0000 0101,1010 0111,1001 1110->1001 1110,1010 0111,0000 0101->9E A7 05
  • s3_22[packed=true] repeated int32
    • 三个值放在一个字段,value紧密排列(value的编码方式 因值类型为int32,所以是varint编码),‘write_type=2’
    • tag: 22<<3|2->10110010->1,011 0010->0000 0001,1011 0010->1011 0010,0000 0001->B2 01
    • length->6->0110->06 (长度值为value 实际编码后长度)
    • value
      • 3->0000 0011->03
      • 240->8E 02
      • 86942->9E A7 05
  • s3_23 repeated string
    • 三个值,在Message中会有三个完整的field,三个field 的tag 都相同,因值类型为String,所以write_type=2,并有length

3.4.3 嵌套Message编码

嵌套Message就是value又是一个Message,外层消息存储采用 TLV 存储,外层write_type=2 ,它的 value 又是一个 TLV 存储。整个编码结构如下图所示
在这里插入图片描述
s3_24

  • 嵌套自定义类型 的write_type=2
  • tag: 24<<3|2->11000010->1,100 0010=0000 0001, 1100 0010->1100 0010,0000 0001->C2 01
  • length-> (嵌套数据编码后实际长度)8->08
  • value:
    • s2_1:
      • write_type=0
      • tag: 1<<3|0=>1000->08
      • value:0x1->0001->01
    • s2_2:
      • write_type=2
      • tag: 2<<3|2=00010010->12
      • length:4->0100->04
      • -value: 6c 6f 76 65

3.4.2 repeated Message

  • [packed=false] 只能为false
  • write_type=2
  • 消息结构如下图,repeated Message 相当于 嵌套 多个Message并且这些Tag都一样(field_num ,write_type=2 都相同)
    在这里插入图片描述

3.4.2 optional 不设置

不设置 就不会出现在Message中

4 使用建议

  • 版本兼容宝典:字段只新增,不删除,任何时候tag不要变动(即不要修改类型和field_num)
  • 如果要出现负数,不要使用int32,int64,而应该使用sint32,sint64。
  • repeated的原始数字类型(使用varint,32位或64位)的重复字段可以声明为 [packed=true ]减小占用空间,但有低版本不兼容的风险
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值