doc: 阅读protobuf encoding文档

This commit is contained in:
asahi
2025-03-03 00:15:49 +08:00
parent 89a8616cfc
commit bb028ceafc

View File

@@ -40,6 +40,17 @@
- [optimize\_for (file option)](#optimize_for-file-option) - [optimize\_for (file option)](#optimize_for-file-option)
- [deprecated (field option)](#deprecated-field-option) - [deprecated (field option)](#deprecated-field-option)
- [生成代码](#生成代码) - [生成代码](#生成代码)
- [Encoding](#encoding)
- [Simple Message](#simple-message)
- [Base 128 varints](#base-128-varints)
- [varint编码原理](#varint编码原理)
- [消息结构](#消息结构)
- [wire type](#wire-type)
- [More Integer Types](#more-integer-types)
- [Boolean \& Enum](#boolean--enum)
- [Length-Delimited Records](#length-delimited-records)
- [Sub messages](#sub-messages)
- [Optional \& Repeated](#optional--repeated)
# protobuf # protobuf
@@ -464,4 +475,124 @@ protoc --proto_path=IMPORT_PATH --cpp_out=DST_DIR --java_out=DST_DIR --python_ou
在指定输入时也可以指定多个proto文件。 在指定输入时也可以指定多个proto文件。
## Encoding
默认protobuf会将消息序列化为wire formatwire format定义了如何将消息发送到wire中并定义了消息占用的空间大小。
### Simple Message
如下是一个简单的消息定义示例:
```proto
message Test1 {
optional int32 a = 1;
}
```
在application code中可以创建Test1消息实例并将a设置为150之后将消息序列化到outputstream中。`被序列化之后消息的hex内容为`
```
08 96 01
```
如果使用protoscope tool来转储这些字节可以得到`1:150`的结果。
### Base 128 varints
可变宽度整数varints为wire format的基础。通过varints可以将unsigned 64bit整数编码为1~10个字节。
> varint可以对固定长度8字节的64bit unsigned整数进行编码当64bit整数越小varint花费的字节数越少。
>
> varint中每个字节的载荷为7位取值范围为0~127故而对64bit整数进行编码时最多可能需要花费10个字节。`(7 * 9 = 63 < 64)`
>
> 而对于7位载荷能够代表的数字例如1则varint只需要1个字节即能对其进行编码。
#### varint编码原理
对于varint中的每个字节都含有一个`continuation bit`代表下一字节是否为varint的一部分`continuation bit`是字节的`MSB`。
> 故而在varint编码产生的结果中最后一个字节的MSB都是0而其他字节的MSB都是1这样可以对varint进行分隔。
构成varint的每个字节除了MSB之外的7bit构成了载荷将所有字节的7bit拼接在一起即是varint代表的整数。
例如整数1其编码为varint后的长度为1字节内容为`01`,其中`MSB`为0代表varint只由当前字节构成7位载荷为`0000001`代表该varint的值为1。
对于整数150其16进制代表为`0x96`一个字节能表示的最大数为127故而150需要两个字节来表示表示的载荷为`1001 0110`将其分割为7位并由两个字节来表示之后在大端环境下编码后字节内容为`1 0000001 0 0010110`,即`0x8116`;在小端环境下,编码后字节内容为`1 0010110 0 0000001`,即`0x9601`。
> 网络传输默认使用大端字节序,故而网络传输字节内容为`0x9601`
### 消息结构
protocol buffer message由一系列的key/value pairs构成message的二进制数据将`field number`作为keyfield的类型以及field name只有在decode时参照message type的定义才能得知。
当消息被编码后每个key/value pair都会被转化为一条记录记录中包含`field number, wire type, payload`。其中,`wire type`会告知parser后续payload的大小。
> 对于old parser通过wire type可以得知payload的大小故而old parser可以跳过unknown field。
#### wire type
wire type故而被称为`tag-length-value`,即`TLV`。
有6中wire type`varinti64, len, sgroup, egroup, i32`。
<table><thead><tr><th>ID</th><th>Name</th><th>Used For</th></tr></thead><tbody><tr><td>0</td><td>VARINT</td><td>int32, int64, uint32, uint64, sint32, sint64, bool, enum</td></tr><tr><td>1</td><td>I64</td><td>fixed64, sfixed64, double</td></tr><tr><td>2</td><td>LEN</td><td>string, bytes, embedded messages, packed repeated fields</td></tr><tr><td>3</td><td>SGROUP</td><td>group start (deprecated)</td></tr><tr><td>4</td><td>EGROUP</td><td>group end (deprecated)</td></tr><tr><td>5</td><td>I32</td><td>fixed32, sfixed32, float</td></tr></tbody></table>
对于一条记录(field key/value pair), 其`tag`被编码为了一个varint被编码值的计算公式如下
```
(field_number << 3) | wire_type
```
故而在对varint类型的tag进行decode操作后其结果最低三位代表`wire type`其他部分代表field number。
故而可以stream中永远以`varint`数字开头代表field的`tag`例如stream中的第一个字节为`08`
```
0000 1000
```
其去掉符号位后,载荷为`0001 000`后三位代表wire type值为`0`的wire type代表varint即field value的类型为varint。前4位代表field number故而field number的值为`0001`,即`1`。
故而,`08`tag 代表field number为1并且field value的类型为varing的field。
### More Integer Types
#### Boolean & Enum
bool类型和enum类型的编码方式和`int32`一致bool值通常会被编码为`00`或`01`。
#### Length-Delimited Records
`length prefixed`为wire format另一个要点。wire type中值为2的类型为`LEN`,其在`tag`后有一个varint类型的动态长度动态长度之后跟随载荷。
示例如下:
```proto
message Test2 {
optional string b = 2;
}
```
如果新建一个`Test2`的实例,并且将`b`设置为`testing`,那么其编码后的结果为
```
12 07 [74 65 73 74 69 6e 67]
```
解析如下:
- 第一个varint值`12`代表tag其载荷`0010 010`代表field number为`2`并且wire type为`2`,即`LEN`。
- wire type为`LEN`代表tag后跟随一个varint表示field值的长度而`07`其载荷表示的值为7代表field value的长度为7`testing`字符串长度正好为7
- 字符串`testing`的utf8编码为`0x74657374696e67`,其正好和后续内容一致
#### Sub messages
对于sub messages类型的记录其仍使用`LEN` wire type在`tag`和`length varint`之后跟随的是sub message编码之后的二进制内容。
#### Optional & Repeated
对于otpional场景编码时如果field没有被设置只需要跳过其即可。
对于repeated场景普通not packedrepeated fields会为field中的每一个元素单独发送一个record示例如下
```proto
message Test4 {
optional string d = 4;
repeated int32 e = 5;
}
```
如果构造一个Test4实例d为`hello`并且e为`1, 2, 3`,那么,其可以按照如下方式被编码:
```
4: {"hello"}
5: 1
5: 2
5: 3
```
其中e的多条record顺序并不需要在一起可以乱序排放例如
```
5: 1
5: 2
4: {"hello"}
5: 3
```