ProtoBuf使用手册(入门)
目录
- ProtoBuffer使用入门
- Protobuf概念
- 一、Protobuf 的核心定位:为什么需要它?
- 二、Protobuf 的核心工作原理
- 三、Protobuf 的核心应用场景
- 四、Protobuf 的关键概念补充
- Protobuf (.proto) 文件语法规则
- 0. 数据类型
- (1)标量类型
- (2)复合类型
- (3)特殊类型
- (4)通用注意事项
- 1. 版本声明
- 2. 包声明
- 3. 消息定义(Message)
- 4. 字段类型
- 5. 服务定义(Service)
- 生成代码的使用方法
- 1. 编译 .proto 文件
- 2. 核心类与函数
- (1)对象创建与字段设置
- (2)序列化与反序列化
- (3)字段访问与判断
- 3. 核心函数参数详解
- 注意事项
ProtoBuffer使用入门
Protobuf概念
Protobuf(全称为 Protocol Buffers)是由 Google 开发的一种轻便、高效的结构化数据序列化协议,核心作用是实现数据的结构化存储与跨平台、跨语言的高效传输,本质是一套 “数据格式约定 + 编解码工具链”,解决了传统数据交换格式(如 XML、JSON)在 “体积” 和 “解析效率” 上的痛点。
一、Protobuf 的核心定位:为什么需要它?
在分布式系统(如微服务、跨语言通信)或数据存储场景中,需要将内存中的结构化数据(如 “用户信息”“订单数据”)转为可传输 / 存储的二进制格式,或从二进制格式恢复为内存数据 —— 这个过程就是 “序列化” 与 “反序列化”。
Protobuf 相比 XML、JSON 的核心优势:
特性 | Protobuf | JSON | XML |
---|---|---|---|
数据体积 | 极小(二进制压缩,无冗余字符) | 较大(文本格式,含引号 / 逗号) | 最大(标签冗余多) |
解析效率 | 极高(预编译生成代码,直接操作内存) | 中等(需解析文本结构) | 较低(标签嵌套解析复杂) |
跨语言支持 | 原生支持(C++/Java/Python/Go 等) | 依赖第三方库 | 依赖第三方库 |
类型安全性 | 强类型(.proto 定义字段类型) | 弱类型(值类型需动态判断) | 弱类型(需解析标签类型) |
兼容性(版本迭代) | 天然支持(字段编号不变即可兼容) | 需手动处理(如新增字段兼容) | 需手动处理(标签新增兼容) |
二、Protobuf 的核心工作原理
Protobuf 的工作流程围绕 .proto 文件 和 编解码工具链 展开,核心是 “先定义结构,再生成代码,最后编解码”:
-
定义数据结构(.proto 文件)通过专门的语法(如
proto3
)在.proto
文件中描述数据的结构(类似 “数据蓝图”),例如定义 “用户” 包含 “姓名、ID、爱好” 等字段:syntax = "proto3"; // 声明使用 proto3 语法 package user; // 避免命名冲突(生成代码时作为命名空间)message Person { // 定义数据结构(类似类/结构体)string name = 1; // 字段:类型 名称 = 字段编号(核心!用于编解码)int32 id = 2; // 字段编号:1-15 占1字节,常用字段优先用repeated string hobbies = 3; // repeated:表示可重复(类似数组) }
-
编译生成代码(protoc 工具)使用 Google 提供的
protoc
编译器,根据.proto
文件生成对应编程语言的 “编解码代码”(如 C++ 的.h
/.cc
、Java 的.java
)。例:生成 C++ 代码的命令:protoc --cpp_out=./ ./person.proto # --cpp_out:指定输出目录;后面跟 .proto 文件路径
生成的代码中包含:
- 对应
.proto
中message
的类(如 C++ 中的user::Person
类); - 字段的访问 / 修改函数(如
set_name()
、name()
、add_hobbies()
); - 序列化(转二进制)/ 反序列化(读二进制)函数(如
SerializeToString()
、ParseFromString()
)。
- 对应
-
编解码与使用在业务代码中引入生成的代码,直接通过类的方法实现 “内存数据 ↔ 二进制数据” 的转换,无需手动处理编解码细节:
- 序列化:将内存中的
Person
对象转为二进制字符串(可传输 / 存储); - 反序列化:将二进制字符串恢复为内存中的
Person
对象(可直接访问字段)。
- 序列化:将内存中的
三、Protobuf 的核心应用场景
Protobuf 因 “小体积、高效率、强兼容” 的特性,广泛用于以下场景:
- 跨语言 / 跨平台通信:如微服务间的 RPC 调用(如 gRPC 框架默认使用 Protobuf 作为数据格式)、客户端与服务器的通信(如游戏客户端与服务端的数据交互);
- 数据存储:如日志存储(二进制格式比文本格式节省磁盘空间)、分布式系统中的数据同步;
- 大数据传输:如实时数据流(如 Flink/Spark 处理的结构化数据)、物联网设备的传感器数据(设备资源有限,需高效传输)。
四、Protobuf 的关键概念补充
-
字段编号(Field Number)
.proto
中每个字段的= 1
/= 2
是核心标识,而非 “字段顺序”:- 编解码时,Protobuf 不依赖字段名,只依赖字段编号(因此字段名修改不影响兼容性,编号不能改);
- 编号范围:1-536870911(1-15 占 1 字节,16-2047 占 2 字节,常用字段优先用小编号以节省体积)。
- 19000 ~19999不可⽤, 因为:在Protobuf协议的实现中,对这些数进⾏了预留。如果⾮要在.proto ⽂件中使⽤这些预留标识号,例如将name字段的编号设置为19000,编译时就会报警
-
proto3 与 proto2 的区别目前主流使用
proto3
,相比proto2
简化了语法:proto3
移除了required
(必填)和optional
(可选)关键字,所有字段默认 “可选”;proto3
支持更多语言(如 Go、Rust),且默认值规则更统一(如int32
默认 0,string
默认空串);proto3
支持map
类型(如map<int32, string> id_to_name = 4
),proto2
需手动模拟。
-
二进制格式的不可读性Protobuf 序列化后是二进制数据,无法直接用文本编辑器查看(与 JSON/XML 不同),需用专门工具(如
protoc --decode
)解析:# 从二进制文件 person.bin 解析出数据(需指定 .proto 定义) protoc --decode=user.Person ./person.proto < ./person.bin
Protobuf (.proto) 文件语法规则
0. 数据类型
(1)标量类型
.proto 类型 | C++ 对应类型 | Java 对应类型 | 特点与用途 | 注意事项 |
---|---|---|---|---|
double | double | double | 64 位浮点数 | 精度较高,适合需要大范围数值的场景(如坐标、科学计算) |
float | float | float | 32 位浮点数 | 精度较低但占用空间小,适合存储传感器数据等对精度要求不高的场景 |
int32 | int32 | int | 32 位整数(变长编码) | 负数会编码为 10 字节,不适合存储负数(建议用sint32 ) |
int64 | int64 | long | 64 位整数(变长编码) | 负数会编码为 10 字节,不适合存储负数(建议用sint64 ) |
uint32 | uint32 | int | 32 位无符号整数(变长编码) | 仅用于非负整数,编码效率高于int32 (无符号时) |
uint64 | uint64 | long | 64 位无符号整数(变长编码) | 仅用于非负整数,编码效率高于int64 (无符号时) |
sint32 | int32 | int | 32 位有符号整数(变长编码, ZigZag 编码) | 专门优化负数存储,负数编码后体积比int32 小(推荐存储可能为负的整数) |
sint64 | int64 | long | 64 位有符号整数(变长编码, ZigZag 编码) | 专门优化负数存储,负数编码后体积比int64 小(推荐存储可能为负的整数) |
fixed32 | uint32 | int | 32 位无符号整数(固定 4 字节) | 数值较大时(>2^28)编码效率高于uint32 |
fixed64 | uint64 | long | 64 位无符号整数(固定 8 字节) | 数值较大时(>2^56)编码效率高于uint64 |
sfixed32 | int32 | int | 32 位有符号整数(固定 4 字节) | 适合存储绝对值较大的负数(无需变长编码开销) |
sfixed64 | int64 | long | 64 位有符号整数(固定 8 字节) | 适合存储绝对值较大的负数(无需变长编码开销) |
bool | bool | boolean | 布尔值(true/false) | 编码后占 1 字节,无默认值(未设置时视为false ) |
string | std::string | String | UTF-8 编码的文本(长度不超过 2^32) | 必须是合法的 UTF-8 字符,适合存储短文本(长文本建议用bytes ) |
bytes | std::string | ByteString | 原始字节序列(长度不超过 2^32) | 适合存储二进制数据(如图片、压缩数据)或非 UTF-8 文本 |
(2)复合类型
类型 | 定义示例 | 特点与用途 | 注意事项 |
---|---|---|---|
枚举(enum) | protobuf enum Status { OK = 0; ERROR = 1; } | 有限的离散值集合 | 1. 首元素必须为 0(作为默认值);2. 可通过 option allow_alias = true; 允许枚举值别名;3. 枚举值范围:0~2^32-1 |
消息(message) | protobuf message Person { string name = 1; } | 自定义结构化数据(可嵌套) | 1. 可嵌套定义(如 message A { message B {} } );2. 可作为其他消息的字段类型;3. 支持递归引用(需用 import "google/protobuf/any.proto"; ) |
repeated 字段 | protobuf repeated int32 ids = 1; | 动态数组(类似 List/Vector) | 1. 编码效率:proto3 中采用 “长度前缀” 编码,比 proto2 更高效;2. 空数组与未设置等价,size() 为 0 |
map 类型 | protobuf map<int32, string> id_name = 1; | 键值对集合(类似字典) | 1. 键类型:除 float /double /bytes 外的标量类型;2. 无序存储,迭代顺序不保证;3. 不可用 repeated 修饰 |
(3)特殊类型
类型 | 定义示例 | 特点与用途 | 注意事项 |
---|---|---|---|
Any | protobuf import "google/protobuf/any.proto"; message Data { google.protobuf.Any value = 1; } | 动态类型(可存储任意消息) | 1. 需要导入 any.proto ;2. 需通过 PackFrom() /UnpackTo() 序列化 / 反序列化;3. 适合存储不确定类型的数据 |
Oneof | protobuf message Result { oneof value { int32 num = 1; string str = 2; } } | 多个字段中最多一个被设置(节省空间) | 1. 设置新字段会自动清除之前的字段;2. 不能包含 repeated 字段;3. 可通过 which_value() 判断哪个字段被设置 |
(4)通用注意事项
- 字段编号(Field Number)
- 范围:1~536870911(1~15 占 1 字节,16~2047 占 2 字节,建议常用字段用小编号);
- 一旦使用,不可修改或重复使用(影响兼容性);
- 保留字段:用
reserved 1, 2;
或reserved "name";
防止未来误用已废弃的编号 / 名称。
- 默认值(Default Values)
- proto3 中所有字段默认 “可选”,未设置时使用默认值:
- 数值类型:0;布尔值:false;字符串 /bytes:空;枚举:第一个值(0);消息:默认实例(所有字段为默认值)。
has_xxx()
函数在 proto3 中对标量类型无效(默认值不视为 “被设置”)。
- proto3 中所有字段默认 “可选”,未设置时使用默认值:
- 兼容性
- 新增字段:不影响旧版本解析(旧版本会忽略新增字段);
- 删除字段:建议标记为
reserved
避免复用编号; - 类型变更:仅允许兼容类型(如
int32
↔int64
兼容,int32
→string
不兼容)。
- 编码效率
- 变长类型(如
int32
/sint32
)适合小数值,固定长度类型(如fixed32
)适合大数值; - 负数优先用
sint32
/sint64
(ZigZag 编码更高效); - 避免嵌套过深的消息(影响解析效率)。
- 变长类型(如
- 命名规范
- 消息 / 枚举名:帕斯卡命名法(如
PersonInfo
); - 字段 / 枚举值:蛇形命名法(如
user_name
、error_code
); - 包名:小写字母 + 下划线(如
my_project
)。
- 消息 / 枚举名:帕斯卡命名法(如
1. 版本声明
必须指定 protobuf 版本(通常使用 proto3):
syntax = "proto3"; // 声明使用 proto3 语法
2. 包声明
类似c++中的namespace命名空间,用来避免命名冲突:
package myproject; // 包名,生成代码时会作为命名空间
3. 消息定义(Message)
数据结构的核心,类似类或结构体:
message Person {string name = 1; // 字段名 = 字段编号(1-15 占1字节,建议常用字段用)int32 id = 2; // 字段类型 + 名称 + 编号bool is_student = 3;repeated string hobbies = 4; // repeated 表示可重复(类似数组)
}
4. 字段类型
-
标量类型:
int32
/int64
、float
/double
、bool
、string
、bytes
-
枚举(Enum):
enum Gender {UNKNOWN = 0; // 枚举默认值必须为 0MALE = 1;FEMALE = 2; }
-
嵌套消息:
message Student {Person info = 1; // 嵌套使用其他消息类型int32 grade = 2; }
5. 服务定义(Service)
用于 RPC 通信:
service UserService {rpc GetUserInfo(UserRequest) returns (UserResponse);
}
生成代码的使用方法
1. 编译 .proto 文件
使用 protoc 编译器生成 C++ 代码:
protoc --cpp_out=./ ./person.proto
会生成两个文件:
person.pb.h
:头文件,包含类定义person.pb.cc
:实现文件
2. 核心类与函数
假设生成的类为 Person
(对应 .proto 中的 message):
(1)对象创建与字段设置
#include "person.pb.h"
using namespace myproject; // 对应 package 声明int main() {Person person;// 设置字段(生成的 setter 函数)person.set_name("Alice"); // string 类型person.set_id(1001); // int32 类型person.set_is_student(true); // bool 类型// repeated 字段(类似 vector)person.add_hobbies("reading");person.add_hobbies("sports");// 获取 repeated 字段数量int hobby_count = person.hobbies_size();// 获取指定索引的元素std::string first_hobby = person.hobbies(0);
}
(2)序列化与反序列化
-
序列化:将对象转为二进制数据
std::string data; person.SerializeToString(&data); // 序列化为字符串 // 或写入文件 std::fstream output("person.bin", std::ios::out | std::ios::binary); person.SerializeToOstream(&output);
-
反序列化:从二进制数据恢复对象
Person new_person; new_person.ParseFromString(data); // 从字符串解析 // 或从文件读取 std::fstream input("person.bin", std::ios::in | std::ios::binary); new_person.ParseFromIstream(&input);
(3)字段访问与判断
// 判断字段是否被设置(proto3 中默认值不视为"被设置")
if (person.has_id()) {int id = person.id(); // 获取字段值
}// 清除字段值
person.clear_name();
3. 核心函数参数详解
函数 | 作用 | 参数说明 |
---|---|---|
set_xxx(value) | 设置字段值 | value :与字段类型匹配的值 |
xxx() | 获取字段值 | 无参数,返回字段类型的值 |
has_xxx() | 判断字段是否被显式设置 | 无参数,返回 bool |
clear_xxx() | 清除字段值 | 无参数 |
add_xxx(value) | 向 repeated 字段添加元素 | value :元素值 |
xxx_size() | 获取 repeated 字段元素数量 | 无参数,返回 int |
xxx(index) | 获取 repeated 字段指定元素 | index :元素索引(0 开始) |
SerializeToString() | 序列化为字符串 | 输出参数:std::string* 用于存储结果 |
ParseFromString() | 从字符串反序列化 | 输入参数:const std::string& 二进制数据 |
SerializeToOstream() | 序列化到输出流 | 输入参数:std::ostream* (如文件流) |
ParseFromIstream() | 从输入流反序列化 | 输入参数:std::istream* (如文件流) |
注意事项
- 字段编号一旦确定,不应修改(影响兼容性)
- proto3 移除了 proto2 中的
required
和optional
关键字 - 生成的代码需链接 protobuf 库(编译时加
-lprotobuf
) - 二进制格式不兼容不同版本的 .proto 定义,需做好版本管理