Protubuf 原理
30 March, 2020
protobuf 的 message 中有很多字段,每个字段的格式为: 修饰符 字段类型 字段名 = 域号; 在序列化时,protobuf 按照 TLV 的格式序列化每一个字段,T 即 Tag,也叫 Key;V 是该字段对应的值 v 省略。 序列化后的 Value 是按原样保存到字符串或者文件中,Key 按照一定的转换条件保存起来,序列化后的 message 中字段后面的域号与字段类型来转换。转换公式如下:
(field_number << 3) | wire_type
wire_type 与类型的对应关系表:
| wire_type | meaning | | --------- | ------------- | --------------------------------------------------------- | | 0 | Vaint | int32、int64、uint32、uint64、sint32、sint64、bool、enum | | 1 | 64-bit | fixed、sfixed64、double | | 2 | Length-delimi | string、bytes、embedded、messages、packed repeated fields | | 3 | Start group | Groups(deprecated) | | 4 | End group | Groups(deprecated) | | 5 | 32-bit | fixed32、sfixed32、float |
As you can see, each field in the message definition has a unique numbered tag. These tags are used to identify your fields in the message binary format, and should not be changed once your message type is in use. Note that tags with values in the range 1 through 15 take one byte to encode. Tags in the range 16 through 2047 take two bytes. So you should reserve the tags 1 through 15 for very frequently occurring message elements. Remember to leave some room for frequently occurring elements that might be added in the future.
上面一段话是来自 Google Protobuf Documents,上面有几个信息需要注意的地方: protobuf 协议使用二进制格式表示 Key 字段;对 value 而言,不同的类型采用的编码方式也不同,如果是整型,采用二进制表示;如果是字符,会直接原样写入文件或者字符串(即不编码)。由于刚开始接触 protobuf 协议,我也在学习中,下面我会给出一个例子,对于其他一些类型的编码方式,可以仿照这个例子自己实验一下。 (这个例子主要是讲述 Key 的编码方式)
上面说过,对于 message 中的每一个域,都对应一个域号。protobuf 规定:
- 如果域号在[1,15]范围内,会使用一个字节表示 Key;
- 如果域号大于等于 16,会使用两个字节表示 Key;
Key 编码过后,该字节的第一个比特位表示之后的一个字节是否与当前这个字节有关:
- 如果第一个比特位为 1,表示有关,即连续两个字节都是 Key 的编码;
- 如果第一个比特位为 0,表示 Key 的编码只有当前一个字节,后面的字节是 Length 或者 Value;
结合公式 (field_number << 3)| wire_type ,如果域号大于等于 16,两个字节共 16 位,去掉移位的 3 位,去掉两个字节中第一个比特位,总共 16 个比特位只有 16-5==11 个比特位用来表示 Key,所以 Key 的域号要小于 2^11== 2048
Protobuf Example
message Person {
required string id = 1;
required name = 2;
required addr = 3;
required test = 1000;
}
使用 protoc 编译后,生成两个文件:
protoc -I=. –cpp_out=. person.proto
建立一个 Person 对象: 属性为
id = 111
name = China
addr = Asia
test = ttttt
# 打印出序列化后的结果为:
'\n\003\061\061\061\022\005China\032\004\Asia\302\005ttttt'
‘\n’是 id 字段的 Key,后面的\003(八进制)表示 id 字段的值长度为 3
key 的域号不超过 15 的序列化解析:
因为 id 字段的域号为 1,是小于 15 的,所以 id 字段的 Key 序列化要占 1 个字节的空间,00000001 左移 3 位变成 00001000,因为 string 的 wire_type 值是 2,所以 00001000 再或上 2,变成 00001010,就是十进制的 10,即字符’\n’。下面的字段如果域号不超过 15,解析同 id 字段。 后面连续 3 个’\61’(八进制)即字符’1’; 同样\022\005 是 name 字段的 key 和 value 长度,后面是 name 字段的值; \032\004 是 addr 字段的 key 和 value 长度;
最后,\302>\005 是 test 字段的 Key 和 Value 长度;
key 的域号大于 15 的序列化解析:
由于 CSDN 编辑器不支持 CSS 格式,没有办法标记下面的解析内容的颜色,只有放一个图片上去了 ^_^; 下面图片中的\76 就是\302 后面的‘>’字符的八进制表示,\302 与>共同组成最后一个字段的 Key 的表示(因为最后一个字段 test 的域号 1000 大于 15,所以需要占两个字节表示 Key)