protobuf学习
protobuf学习
示例
设置的.proto文件结构
-
示例
syntax = "proto2"; package tutorial; message Person { required int32 id = 1; required string name = 2; } -
syntax = "proto2";可选,指定protobuf编译版本为proto2,如果写必须写在第一非空非注释行 -
package tutorial;可选,指定包名为tutorial,在cpp中等价声明了一个命名空间 -
message定义一个消息类型,类似与cpp定义一个类 -
required必需的(protobuf3中已弃用,少用),optional可选择的
编译.proto
- 示例
test.proto# protoc --version # 查看protobuf版本 protoc --cpp_out=. test.proto # 生成c++配置文件到当前目录 protocprotobuf指令--cpp_out=.生成cpp语言的配置文件,生成目录为当前目录test.proto自定义protobuf格式协议文件- 最后会生成
test.pb.cc和test.pb.h文件,需要使用时在cpp文件内引用test.pb.h,在编译cpp代码时间需要链接test.pb.cc -lprotobuf
在对应cpp语言中使用
-
示例
#include "test.pb.h" #include <iostream> #include <string> using namespace std; using namespace tutorial; // proto结构 // message Person { // required int32 id = 1; // required string name = 2; // } int main() { string data; // 存储序列化的消息 // 客户端发送请求 { Person pro; pro.set_id(11); pro.set_name("Luo"); cout << "设置的id : " << pro.id() << endl; cout << "设置的名字 : " << pro.name() << endl; pro.SerializeToString(&data); // 序列化到string格式, Serialize序列化 } // 服务器端接收请求 { Person pro2; pro2.ParseFromString(data); // 从string格式解析, Parse解析 cout << "客户端发送过来的id: " << pro2.id() << endl; cout << "客户端发送过来的name: " << pro2.name() << endl; } } -
编译使用
g++ -o test.out test.cpp test.pb.cc -lprotobuf ./test.out
Bash 128 Varints
可变宽整数(varints),它们允许使用一到十个字节之间的任何位置编码无符号 64 位整数(64/7=9.1.. => 最多需要10字节),小值使用较少的字节
varint 中的每个字节都有一个延续位,指示其后的字节是否是 varint 的一部分。这是字节的最高有效位 (MSB)(有时也称为符号位)。较低的 7 位是有效负载;生成的整数是通过将构成字节的 7 位有效负载附加在一起来构建的
-
例如
150的编码为0x96 0x01150 的二进制 : 10010110 0000001 0010110 // 7位有效的大端序 0010110 0000001 // 7位有效的小端序 10010110 00000001 // 加入msb位(后续有数据 延续位设置1,后续没有数据 延续位设置0) 得到的编码结果为 : 0x96 0x01 -
解码过程
10010110 00000001 // 编码的数据 ^msb ^msb // msb为1代表后续字节依然是这个数字的一部分,msb为0表示这个数字的结束 0010110 0000001 // 去掉延续位 0000001 0010110 // 转换为大端序 00000010010110 // 连接 128+16+4+2=150 // 解释为150
Length-delimited
Length-delimited 编码的过程如下:
- 计算数据的长度:首先计算要编码的数据的字节长度。
- 编码长度:将计算出的长度使用 Varint 编码方式编码。
- 附加数据:将编码后的长度附加到数据前面。
消息结构
协议缓冲区消息是一系列键值对。消息的二进制版本仅使用字段的编号作为键 - 每个字段的名称和声明的类型只能在解码端通过引用消息类型的定义(即 .proto 文件)来确定。Protoscope 无权访问此信息,因此它只能提供字段编号。
当消息被编码时,每个键值对都转换为一个记录,该记录由字段编号、线路类型和有效负载组成。线路类型告诉解析器其后的有效负载有多大。这允许旧的解析器跳过他们不理解的新字段。这种类型的方案有时称为 Tag-Length-Value,或 TLV。
有六种线路类型:VARINT、I64、LEN、SGROUP、EGROUP 和 I32
| ID | 名称 | 用途 |
|---|---|---|
| 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 | 组开始 (已弃用) |
| 4 | EGROUP | 组结束 (已弃用) |
| 5 | I32 | fixed32, sfixed32, float |
LEN(Length-delimited):
-
embedded messages: 嵌套消息。在序列化时,先将嵌套消息独立序列化,然后计算其长度,并使用 Length-delimited 编码
-
packed repeated fields:打包的重复字段。使用 Length-delimited 编码将多个值打包到一个单一的字节流中。
- 使用
在 .proto 文件中,可以通过在字段定义中添加 packed = true 来启用 packed 选项。例如:syntax = "proto2"; message MyMessage { repeated int32 values = 1 [packed = true]; }
- 使用
编码示例
-
示例信息定义:
syntax = "proto2"; // 使用 proto2 语法 message Person { required int32 id = 1; // 字段编号 1,Varint 编码 optional string name = 2; // 字段编号 2,Length-delimited 编码 repeated float scores = 3; // 字段编号 3,32-bit fixed 编码 } -
消息实例
- 字段
id=42,name="Alice",scores=[97.5,88.0]
Person person; person.set_id(42); person.set_name("Alice"); person.add_scores(97.5); person.add_scores(88.0); - 字段
-
编码过程
- 二进制结构(简化表示)
Tag(1, Varint) -> 0x08 Value(Varint 42) -> 0x2A Tag(2, LEN) -> 0x12 Length(5) -> 0x05 Value("Alice") -> 0x41 0x6C 0x69 0x63 0x65 Tag(3, I32) -> 0x1D field_number = 3, wire_type = 5, Tag = (3<<3) | 5 = 29 -> Varint编码位0x1D Value(97.5) -> 0x00 0x00 0xC3 0x42 97.5的IEEE 754 32-bit 浮点数: 0x42C30000 : 小端字节序(00 00 B0 42) Tag(3, I32) -> 0x1D Value(80.0) -> 0x00 0x00 0xB0 0x42 80.0的IEEE 754 32-bit 浮点数: 0x42B00000 : 小端字典序(00 00 B0 42) -
关键点总结
- 计算Tag:
- 始终是 (field_number << 3) | wire_type 的 Varint 编码。
- 例如 id=1 -> 0x08,name=2 -> 0x12
- 值编码
- int32: 直接Varint编码(如 42 -> 0x2A)
- string: 长度(Varint) + UTF-8 字节(如"Alice" -> "0x05 0x41 0x6c 0x69 0x63 0x65")
- float: 固定4字节小端序(如97.5 -> 0x00 0x00 0xC3 0x42)
- 计算Tag:
-
解码时的依赖
- 必须通过 .proto 文件才能正确解码:
- 知道 fielid_number=1 是 int32 (字节 Varint 解码)
- 知道 fielid_number=3 是 float (读取4字节小端序)
如果没有 schema, 只能看到
- 0x08 0x2A -> Tag=8(字段编号=1, wire_type=0), 值是42(但不知道是int32还是sint32)
编码示例补充 -- 嵌套消息类型
在 Protocol Buffers 中,嵌套消息类型(如 Person1 嵌套在 Person2 中)的编码方式与字符串或字节数组类似,采用 Length-delimited(wire_type=2) 的编码规则。
-
定义嵌套消息
syntax = "proto2"; message Person1 { required int32 id = 1; optional string name = 2; } message Person2 { required Person1 person = 1; // 嵌套消息 } -
消息实例
person1{id=42,name="Alice"},person2{person1} -
编码过程
- 先编码嵌套的
Person1- 编码
person1.id = 42->0x08 0x2A - 编码
person1.name = "Alice"->0x12 0x05 0x41 0x6C 0x69 0x63 0x65 - 合并 Person1 编码结果:
0x08 0x2A 0x12 0x05 0x41 0x6C 0x69 0x63 0x65
- 编码
- 编码
person2.person(嵌套字段)- 计算Tag:
- field_number = 1, wire_type = 2(Length-delimited)
- Tag: (1 << 3) | 2 = 10 ->
0x0A
- 编码 Person1 的二进制长度:
- Person1 编码后占8字节 -> Varint 编码8 ->
0x08
- Person1 编码后占8字节 -> Varint 编码8 ->
- 拼接 Person1 的二进制数据:
- 结果
0x0A 0x08 0x08 0x2A 0x41 0x6C 0x69 0x65
- 结果
- 计算Tag:
- 先编码嵌套的
-
最终的二进制编码
0x0A 0x08 0x08 0x2A 0x41 0x6C 0x69 0x65
分解:
0x0A:Person2.person的Tag(字段编号=1, wire_type=2)0x08: 嵌套的Person1数据长度(8字节)0x08 0x2A 0x41 0x6C 0x69 0x65:Person1的编码数据
-
解码过程
- 读取
Person2的 Tag0x0A:- 解析出
field_number=1,wire_type=2(Length-delimited)
- 解析出
- 读取长度
0x08:- 知道接下来的 8 字节是
Person1的数据。
- 知道接下来的 8 字节是
- 解析嵌套的
Person1:- 读取
0x08 0x2A->id=42。 - 读取
0x12 0x05 0x41 0x6C 0x69 0x63 0x65-> name="Alice"。
- 读取
- 重建
Person2对象:- 将解码后的
Person1赋值给Person2.person。
- 将解码后的
- 读取
浙公网安备 33010602011771号