大厂神器?Protobuf编码原理和避坑指南!
1 编码原理
2.5 尽量使用小整数
2.6 需要传输负数,试试 sint32 或 sint64
01
1.1 概述
message Student {string name = 1;int32 age = 2;}
funcmain() {student := student.Student{}student.Name = "t"marshal, _ := proto.Marshal(&student)fmt.Println(fmt.Sprintf("%08b", marshal)) // 00001010 00000001 01110100}
由上图所示,每个 byte 第一个bit表示是否结束,0表示结束,所以上面 tag 用两个 byte 表示,并且 protobuf 是小端编码的,需要转成大端方便阅读,所以我们可以知道 tag 去掉每个 byte 第一个 bit 之后,后三位表示类型,是3,其余位是编号表示 16。
所以从上面编码规则我们也可以知道,字段尽可能精简一些,字段尽量不要超过 16 个,这样就可以用一个 byte 表示了。
同时我们也可以知道,protobuf 序列化是不带字段名的,所以如果客户端的 proto 文件只修改了字段名,请求服务端是安全的,服务端继续用根据序列编号还是解出来原来的字段。但是需要注意的是不要修改字段类型。
接下来我们看看类型,protobuf 共定义了 6 种类型,其中两种是废弃的:
| ID | Name | Used For |
| 0 | VARINT | int32, int64, uint32, uint64, sint32, sint64, bool, enum |
| 1 | I64 | fixed64, sfixed64, double |
| 2 | LEN | string, bytes, embedded messages, packed repeated fields |
| 3 | SGROUP | group start (deprecated) |
| 4 | EGROUP | group end (deprecated) |
| 5 | I32 | fixed32, sfixed32, float |
1.2 Varints 编码
0000000000000000 00000000 00000001前 3 个字节都是 0 没有表示任何信息,protobuf 就是将这些 0 都去除了,用 1 byte 表示 1 这个数字,再用 1 byte 表示 tag 的编号和类型,所以占用了 2byte。
比如我们对上面 student 设置 age 等于 150:
funcmain() {student := student.Student{}student.Age = 150marshal, _ := proto.Marshal(&student)fmt.Println(fmt.Sprintf("%08b", marshal)) //00010000 10010110 00000001fmt.Println(fmt.Sprintf("%08b", "a"))}
1.3 ZigZag 编码
所以 Varints 编码负数总共会恒定占用 11 byte,tag 一个byte,值占用 10 byte。
为此 Google Protocol Buffer 定义了 sint32 这种类型,采用 zigzag 编码。将所有整数映射成无符号整数,然后再采用 varint 编码方式编码。例如:
| Signed Original | Encoded As |
| 0 | 0 |
| -1 | 1 |
| 1 | 2 |
| -2 | 3 |
| … | … |
| 0x7fffffff | 0xfffffffe |
| -0x80000000 | 0xffffffff |
(n << 1) ^ (n >> 31) //for 32 bit(n << 1) ^ (n >> 63) //for 64 bit
1.4 embedded messages & repeated
message Lecture {int32 price =1 ;}message Student {repeated int32 scores = 1;Lecture lecture = 2;}
02
2.1 字段编号
需要注意的是范围 1 到 15 中的字段编号需要一个字节进行编码,包括字段编号和字段类型;范围 16 至 2047 中的字段编号需要两个字节。所以你应该保留数字 1 到 15 作为非常频繁出现的消息元素。
因为 Protobuf 的第一个 byte 是用来判断是否结尾,所以单字节表示序列号的时候最高位是零,而最低三位表示类型,所以只剩下 4 位可用了。也就是说,当你的字段数量超过 16 时,就需要用两个以上的字节表示了。
2.2 保留字段
message Foo {reserved2, 15, 9 to 11;reserved"foo", "bar";}
2.3 不要修改字段 tag 编号以及字段类型
2.4 不要使用 required 关键字
required 意味着消息中必须包含这个字段,并且字段的值必须被设置。如果在序列化或者反序列化的过程中,该字段没有被设置,那么 protobuf 库就会抛出一个错误。
如果你在初期定义了一个 required 字段,但是在后来的版本中你想要删除它,那么这就会造成问题,因为旧的代码会期待该字段始终存在。为了确保兼容性,Google 在最新版本的 Protobuf(protobuf 3)中已经不再支持 required 修饰符。
2.5 尽量使用小整数
2.6 如果需要传输负数,可以试试 sint32 或 sint64
因为负数的符号位为 1,并且 Varints 编码对于负数如果是32位的有符号数都会转换成64位无符号来处理,所以 Varints 编码负数总共会恒定占用11 byte,tag 一个 byte,值占用 10 byte。
而 sint32 和 sint64 将所有整数映射成无符号整数,然后再采用 varint 编码方式编码,如果数字比较还是可以节省一定的空间的。如果文章对你有帮助,欢迎转发分享~
📢📢来领开发者专属福利!点击下方图片直达👇
你觉得 Protobuf 比 JSON、XML 好用吗?你在使用 Protobuf 踩过什么样的坑呢??欢迎评论留言补充。我们将选取1则优质的评论,送出腾讯云定制文件袋套装1个(见下图)。5月29日中午12点开奖。