diff --git a/protobuf/protobuf.md b/protobuf/protobuf.md index db216bc..9980d93 100644 --- a/protobuf/protobuf.md +++ b/protobuf/protobuf.md @@ -40,6 +40,17 @@ - [optimize\_for (file option)](#optimize_for-file-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 @@ -464,4 +475,124 @@ protoc --proto_path=IMPORT_PATH --cpp_out=DST_DIR --java_out=DST_DIR --python_ou 在指定输入时,也可以指定多个proto文件。 +## Encoding +默认,protobuf会将消息序列化为wire format,wire 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`作为key,field的类型以及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,`varint,i64, len, sgroup, egroup, i32`。 + +
IDNameUsed For
0VARINTint32, int64, uint32, uint64, sint32, sint64, bool, enum
1I64fixed64, sfixed64, double
2LENstring, bytes, embedded messages, packed repeated fields
3SGROUPgroup start (deprecated)
4EGROUPgroup end (deprecated)
5I32fixed32, sfixed32, float
+ +对于一条记录(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 packed)repeated 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 +``` + + +