1. Protobuf 是什么?
Protocol Buffers(Protobuf) 是 Google 开发的一种 二进制序列化格式 ,用于高效地 序列化结构化数据 。
它包含三部分:
- .proto 文件:定义数据的结构(消息格式)
- 编译器:将 .proto 文件生成对应语言的代码(如 Java、Python、C++ 等)
- 序列化/反序列化库:将对象与二进制数据相互转换
示例 .proto 文件:
protobuf
message Person {
string name = 1;
int32 id = 2;
repeated string emails = 3;
}
2. Protobuf 能做什么?
- 数据序列化:将结构化数据转换为紧凑的二进制格式
- 网络通信:常用于 gRPC、微服务间通信、游戏网络协议等
- 数据存储:将数据以高效格式保存到文件或数据库
- 配置文件:某些系统用 Protobuf 格式存储配置
3. 相比 JSON 的优势
| 特性 | Protobuf | JSON |
|---|---|---|
| 数据大小 | 二进制,体积小(比 JSON 小 3-10 倍) | 文本,体积大 |
| 解析速度 | 快(直接解码为二进制,无需词法分析) | 慢(需要解析字符串) |
| 类型安全 | 强类型(通过 .proto 定义,编译时检查) | 弱类型(运行时可能出错) |
| 模式演进 | 支持向前/向后兼容(字段编号机制) | 需要手动处理兼容性 |
| 人类可读 | 否(二进制不可直接阅读) | 是(文本格式) |
| 浏览器支持 | 需要额外解码 | 原生支持(JavaScript) |
4. 核心优势详解
(1)更小的数据体积
protobuf
// Protobuf 二进制示例(示意):
[0A 04 4A 6F 68 6E 10 9C 01] // 9字节
// 对应:name="John", id=156
// JSON 同等数据:
{"name":"John","id":156} // 22字节 + 空格/换行符
- Protobuf 省略了字段名(用字段编号替代),使用变长整数编码
(2)更快的解析速度
- Protobuf:直接按字段编号映射到内存,O(n) 线性解析
- JSON:需要解析括号、引号、逗号,复杂度更高
(3)模式演进能力
protobuf
// 旧版本
message Person {
string name = 1;
int32 id = 2;
}
// 新版本(添加字段,旧代码仍可解析)
message Person {
string name = 1;
int32 id = 2;
string email = 3; // 新增字段
}
- 通过字段编号(如
=1、=2)实现兼容性 - 可以删除/添加字段,只要不重用字段编号
5. 何时选择 Protobuf?
✅ 适合场景:
- 微服务/RPC 通信(如 gRPC)
- 高并发网络协议(游戏、金融交易)
- 物联网设备(带宽有限)
- 需要高性能序列化的大数据系统
❌ 不适合场景:
- 需要人类可读/可编辑的配置文件
- 浏览器直接调用的 API(通常用 JSON/REST)
- 简单前端-后端通信(JSON 更简单直接)
6. 代码示例对比
JSON 使用(Python):
python
import json
data = {"name": "John", "id": 156}
json_str = json.dumps(data) # 序列化
parsed = json.loads(json_str) # 反序列化
Protobuf 使用(Python):
python
# 1. 先编译 person.proto -> person_pb2.py
# 2. 使用生成的类
from person_pb2 import Person
person = Person(name="John", id=156)
binary_data = person.SerializeToString() # 序列化(二进制)
new_person = Person()
new_person.ParseFromString(binary_data) # 反序列化
总结
| 选择依据 | 推荐 |
|---|---|
| 需要性能/带宽优化 | ✅ Protobuf |
| 需要人类可读性 | ✅ JSON |
| 前后端直接通信 | ✅ JSON |
| 内部服务间通信 | ✅ Protobuf |
| 快速原型开发 | ✅ JSON |
| 强类型/合约优先 | ✅ Protobuf |
现代常见搭配:
- 对外 API:JSON/REST(兼容性好)
- 内部服务通信:Protobuf/gRPC(高性能)
- 配置文件:YAML/JSON(易读)
- 持久化存储:Protobuf/Parquet(高效压缩)
Protobuf 在 C++ 中应用广泛,尤其是在网络通信和数据持久化场景。这里为你提供一个从 编写协议文件 、生成代码 ,到 发送 和 接收数据 的完整示例。
🧾 第一步:编写协议定义文件
首先,你需要创建一个 .proto 文件来定义数据的结构。假设我们创建一个 person.proto 文件:
protobuf
// person.proto
syntax = "proto3";
package example;
message Person {
string name = 1;
int32 id = 2;
string email = 3;
}
文件说明
syntax = "proto3":指定使用 proto3 语法。package example:定义包名,在 C++ 中会生成对应的命名空间。message Person:定义了包含三个字段(姓名、ID、邮箱)的数据结构。
🔨 第二步:生成 C++ 代码
使用 Protocol Buffers 的编译器 protoc 来生成 C++ 的类文件。在命令行中执行:
bash
protoc --cpp_out=. person.proto
执行后,你会得到两个文件:
person.pb.h:头文件,包含生成的类声明。person.pb.cc:源文件,包含类的实现。
你需要将这两个文件添加到你的 C++ 项目中。
📤 第三步:序列化与发送数据(发送方)
以下是在 C++ 程序中创建 Person 对象、填充数据并将其序列化到字符串或文件的示例代码:
cpp
// sender.cpp - 序列化并发送数据
#include <iostream>
#include <fstream>
#include "person.pb.h"
int main() {
// 1. 创建 Person 对象并设置字段
example::Person person;
person.set_name("张三");
person.set_id(1001);
person.set_email("zhangsan@example.com");
// 2. 序列化到字符串
std::string serialized_str;
if (person.SerializeToString(&serialized_str)) {
std::cout << "序列化成功,字节数: " << serialized_str.size() << std::endl;
// 在实际应用中,这里可以通过网络发送 serialized_str
}
// 3. 或者,序列化到文件(用于存储)
std::fstream output("./person_data.bin",
std::ios::out | std::ios::trunc | std::ios::binary);
if (person.SerializeToOstream(&output)) {
std::cout << "数据已写入文件." << std::endl;
}
output.close();
return 0;
}
代码说明
- 使用了生成的
example::Person类。 set_xxx()方法用于设置字段值。SerializeToString()将对象序列化为二进制字符串,便于网络传输。SerializeToOstream()将对象序列化后写入文件流,便于本地存储。
📥 第四步:反序列化与读取数据(接收方)
接收方(或读取方)拿到序列化后的二进制数据,可以按如下方式还原为对象:
cpp
// receiver.cpp - 接收并反序列化数据
#include <iostream>
#include <fstream>
#include "person.pb.h"
int main() {
// 假设我们从文件读取序列化的数据
std::fstream input("./person_data.bin", std::ios::in | std::ios::binary);
example::Person person;
if (person.ParseFromIstream(&input)) { // 从流中解析
std::cout << "反序列化成功!" << std::endl;
std::cout << "姓名: " << person.name() << std::endl;
std::cout << "ID: " << person.id() << std::endl;
std::cout << "邮箱: " << person.email() << std::endl;
} else {
std::cerr << "解析文件失败." << std::endl;
}
input.close();
// 如果是通过网络接收的字符串数据,可以这样解析:
// std::string received_data = ...; // 从网络接收的数据
// if (person.ParseFromString(received_data)) { ... }
return 0;
}
代码说明
ParseFromIstream()从文件流中反序列化数据。ParseFromString()从字符串中反序列化数据,对应网络传输场景。xxx()方法(如person.name())用于读取字段值。
💡 核心概念与关键点
在使用上述代码时,有几个关键点需要理解:
1. 消息嵌套
Protobuf 支持消息的嵌套,这类似于 C++ 中的结构体包含。例如,可以在 person.proto 中定义一个地址消息,然后在 Person 消息中包含它:
protobuf
message Address {
string city = 1;
string street = 2;
}
message Person {
string name = 1;
int32 id = 2;
Address addr = 3; // 嵌套消息
}
在 C++ 代码中,可以通过 mutable_addr() 获得可修改的 Address 指针来设置其字段。
2. 重复字段
repeated 关键字用于定义数组或列表。对于重复字段,Protobuf 提供了像 add_xxx() 这样的方法来动态添加元素。
3. 构建工具集成
在真实项目中,通常使用 CMake 等构建工具来管理 Protobuf 的编译和链接。你需要确保:
- 项目能找到
protobuf库(例如,通过find_package(Protobuf REQUIRED))。 - 将生成的
.pb.cc文件加入编译源文件列表。 - 将可执行文件链接到
protobuf库(例如,target_link_libraries(your_target protobuf::libprotobuf))。
🔍 主流应用场景
了解其核心用法后,你可以更好地将 Protobuf 应用到以下常见场景中:
1. 高性能网络通信 (gRPC)
这是 Protobuf 最经典的应用。gRPC 框架默认使用 Protobuf 作为接口定义语言(IDL)和数据序列化格式。你首先需要定义服务(service)和消息(message),然后 protoc 编译器配合 gRPC 插件可以生成完整的客户端和服务端骨架代码,极大简化了开发。
2. 进程间通信 (IPC)
在复杂的系统中,不同模块或进程之间需要交换结构化数据。Protobuf 凭借其高效的序列化能力和清晰的数据契约,非常适合作为 IPC 的通信协议。一些中间件(如 ROS 2、eCAL 等)也直接集成了对 Protobuf 的支持。
3. 配置文件与数据持久化
与 JSON 或 XML 等文本格式相比,用 Protobuf 二进制格式存储配置或用户数据,体积更小、解析更快。虽然牺牲了直接可读性,但可以通过一个小的解码工具来查看内容,适合对性能和存储空间有要求的场景。
4. 游戏开发与嵌入式系统
在对实时性要求极高的领域(如游戏网络同步、机器人控制),Protobuf 高效的数据编码能减少延迟和带宽占用。一些机器人领域的开源库(如 WPILib)也提供了对 Protobuf 的专门封装。
总的来说,Protobuf 在 C++ 中的应用是一个从定义契约 、工具生成 到集成使用的标准化流程。它通过将数据结构与代码分离,并自动生成健壮的序列化代码,帮助开发者构建高效、可扩展且跨平台的通信和数据交换系统。
如果你能告诉我你主要想了解哪个应用场景(比如网络通信、数据存储,或者在某个特定框架里的使用),我可以提供更具体的指导和代码片段。