前言
作为一名 C++ 开发者,你是否曾为以下问题烦恼过?
- 不同服务之间数据传输格式混乱
- JSON 序列化/反序列化性能瓶颈
- 协议字段频繁变更导致的兼容性问题
- 手写解析代码繁琐且容易出错
本文介绍 Google Protocol Buffers(Protobuf)在 C++ 中的实战应用。涵盖序列化概念、Windows/Linux 环境搭建、proto 文件编写及编译流程。详解 Proto3 语法特性包括字段规则、嵌套消息、枚举、oneof 及 map。通过通讯录系统案例演示版本兼容性与 Any 类型使用。对比 Protobuf 与 JSON 性能差异,并提供字段编号规则、兼容性处理及网络编程示例。适合 C++ 开发者快速掌握高效数据传输方案。

作为一名 C++ 开发者,你是否曾为以下问题烦恼过?
今天我要介绍的 Google Protocol Buffers(简称 Protobuf)就是解决这些问题的利器!下面让我用最通俗易懂的方式,带你从零开始掌握 Protobuf。
序列化:把内存中的对象转换为字节序列的过程 反序列化:把字节序列恢复为对象的过程
简单来说,就像你要把一本书寄给朋友:
| 特性 | JSON | XML | Protobuf |
|---|---|---|---|
| 格式 | 文本 | 文本 | 二进制 |
| 可读性 | 好 | 好 | 差(需反序列化) |
| 大小 | 中等 | 大 | 小 |
| 速度 | 中等 | 慢 | 快 |
| 通用性 | 通用 | 通用 | Google 生态 |
简单理解:Protobuf 就像快递的真空压缩包装,体积小、速度快,但需要专门工具才能打开。
# 1. 下载 protoc-xx.x-win64.zip
# 从 https://github.com/protocolbuffers/protobuf/releases 下载
# 2. 解压到 D:\protobuf
# 目录结构:
# D:\protobuf\bin\protoc.exe
# D:\protobuf\include\google\protobuf\...
# 3. 添加环境变量
# 将 D:\protobuf\bin 添加到系统 Path
# 4. 验证安装
protoc --version
# 1. 安装依赖
sudo apt-get install autoconf automake libtool curl make g++ unzip -y
# 2. 下载并解压
wget https://github.com/protocolbuffers/protobuf/releases/download/v21.11/protobuf-all-21.11.zip
unzip protobuf-all-21.11.zip
cd protobuf-21.11
# 3. 编译安装
./autogen.sh
./configure
make
sudo make install
# 4. 验证安装
protoc --version
# CMakeLists.txt 示例
cmake_minimum_required(VERSION 3.10)
project(MyProtobufDemo)
# 查找 Protobuf
find_package(Protobuf REQUIRED)
# 包含目录
include_directories(${Protobuf_INCLUDE_DIRS})
# 添加可执行文件
add_executable(demo main.cpp contacts.pb.cc)
# 链接库
target_link_libraries(demo ${Protobuf_LIBRARIES})
创建 contacts.proto:
// 指定使用 proto3 语法
syntax = "proto3";
// 包名,相当于 C++ 的命名空间
package contacts;
// 定义联系人消息
message PeopleInfo {
string name = 1; // 姓名
int32 age = 2; // 年龄
}
字段格式:字段类型 字段名 = 字段编号;
# 生成 C++ 头文件和源文件
protoc --cpp_out=. contacts.proto
# 生成的文件:
# contacts.pb.h - 类声明
# contacts.pb.cc - 类实现
创建 main.cpp:
#include <iostream>
#include <string>
#include "contacts.pb.h"
int main() {
// 1. 创建联系人对象
contacts::PeopleInfo people;
people.set_name("张三");
people.set_age(20);
// 2. 序列化为字符串
std::string serialized_str;
if (!people.SerializeToString(&serialized_str)) {
std::cerr << "序列化失败!" << std::endl;
return -1;
}
std::cout << "序列化成功!字节数:" << serialized_str.size() << std::endl;
// 3. 反序列化
contacts::PeopleInfo new_people;
if (!new_people.ParseFromString(serialized_str)) {
std::cerr << "反序列化失败!" << std::endl;
return -1;
}
// 4. 输出结果
std::cout << "姓名:" << new_people.name() << std::endl;
std::cout << "年龄:" << new_people.age() << std::endl;
return 0;
}
# 编译
g++ main.cpp contacts.pb.cc -o demo -std=c++11 -lprotobuf
# 运行
./demo
输出结果:
序列化成功!字节数:7
姓名:张三
年龄:20
| .proto 类型 | C++ 类型 | 说明 |
|---|---|---|
| string | std::string | UTF-8 字符串 |
| int32 | int32_t | 32 位整数 |
| int64 | int64_t | 64 位整数 |
| bool | bool | 布尔值 |
| double | double | 双精度浮点数 |
| float | float | 单精度浮点数 |
| bytes | std::string | 字节序列 |
// singular: 0 或 1 个(proto3 默认)
string name = 1;
// repeated: 多个(类似数组)
repeated string phone_numbers = 2;
// optional: 可选字段(proto3 需要显式声明)
optional string email = 3;
message PeopleInfo {
string name = 1;
int32 age = 2;
// 内嵌消息
message Phone {
string number = 1;
string type = 2; // "home", "work", "mobile"
}
repeated Phone phones = 3;
}
C++ 中使用:
contacts::PeopleInfo person;
contacts::PeopleInfo_Phone* phone = person.add_phones();
phone->set_number("13800138000");
phone->set_type("mobile");
message Phone {
string number = 1;
enum PhoneType {
MP = 0; // 移动电话
TEL = 1; // 固定电话
}
PhoneType type = 2;
}
// phone.proto
syntax = "proto3";
package phone;
message Phone {
string number = 1;
}
// contacts.proto
syntax = "proto3";
package contacts;
import "phone.proto"; // 导入其他 proto
message PeopleInfo {
string name = 1;
repeated phone.Phone phones = 2; // 使用导入的类型
}
让我们通过一个完整的通讯录项目来深入学习。
contacts_v1.proto:
syntax = "proto3";
package contacts_v1;
// 联系人
message PeopleInfo {
string name = 1;
int32 age = 2;
}
// 通讯录
message Contacts {
repeated PeopleInfo contacts = 1;
}
write_contacts.cpp:
#include <iostream>
#include <fstream>
#include "contacts_v1.pb.h"
void AddPeople(contacts_v1::PeopleInfo* person) {
std::cout << "请输入姓名:";
std::string name;
std::getline(std::cin, name);
person->set_name(name);
std::cout << "请输入年龄:";
int age;
std::cin >> age;
person->set_age(age);
std::cin.ignore(256, '\n');
}
int main(int argc, char* argv[]) {
if (argc != 2) {
std::cerr << "Usage: " << argv[0] << " CONTACTS_FILE" << std::endl;
return -1;
}
contacts_v1::Contacts contacts;
// 读取已有通讯录
std::fstream input(argv[1], std::ios::in | std::ios::binary);
if (input) {
if (!contacts.ParseFromIstream(&input)) {
std::cerr << "解析通讯录失败!" << std::endl;
return -1;
}
}
input.close();
// 添加新联系人
AddPeople(contacts.add_contacts());
// 写入文件
std::fstream output(argv[1], std::ios::out | std::ios::trunc | std::ios::binary);
if (!contacts.SerializeToOstream(&output)) {
std::cerr << "写入通讯录失败!" << std::endl;
return -1;
}
output.close();
std::cout << "联系人添加成功!" << std::endl;
return 0;
}
让我们为通讯录添加更多功能:
contacts_v2.proto:
syntax = "proto3";
package contacts_v2;
message PeopleInfo {
string name = 1;
int32 age = 2;
message Phone {
string number = 1;
enum PhoneType {
MP = 0; // 移动电话
TEL = 1; // 固定电话
}
PhoneType type = 2;
}
repeated Phone phones = 3;
// 使用 oneof 实现多选一
oneof other_contact {
string qq = 4;
string wechat = 5;
}
// 使用 map 存储备注
map<string, string> remarks = 6;
}
高级特性解析:
contacts_v3.proto:
syntax = "proto3";
package contacts_v3;
import "google/protobuf/any.proto";
message Address {
string home = 1;
string office = 2;
}
message PeopleInfo {
string name = 1;
int32 age = 2;
// 使用 Any 存储任意类型
google.protobuf.Any extra_info = 3;
}
使用 Any 类型:
// 设置 Any 类型
contacts_v3::PeopleInfo person;
contacts_v3::Address address;
address.set_home("北京市");
address.set_office("海淀区");
google::protobuf::Any* any_data = person.mutable_extra_info();
any_data->PackFrom(address);
// 读取 Any 类型
if (person.has_extra_info() && person.extra_info().Is<contacts_v3::Address>()) {
contacts_v3::Address unpacked_addr;
person.extra_info().UnpackTo(&unpacked_addr);
std::cout << "家庭地址:" << unpacked_addr.home() << std::endl;
}
让我们通过实际测试看看 Protobuf 的优势:
compare_performance.cpp:
#include <iostream>
#include <chrono>
#include <json/json.h>
#include "contacts.pb.h"
// 测试 10000 次序列化/反序列化
const int TEST_COUNT = 10000;
void TestProtobuf() {
contacts::PeopleInfo person;
person.set_name("张三");
person.set_age(25);
auto start = std::chrono::high_resolution_clock::now();
std::string buffer;
for (int i = 0; i < TEST_COUNT; ++i) {
person.SerializeToString(&buffer);
contacts::PeopleInfo new_person;
new_person.ParseFromString(buffer);
}
auto end = std::chrono::high_resolution_clock::now();
auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
std::cout << "Protobuf 测试 " << TEST_COUNT << " 次耗时:" << duration.count() << "ms" << std::endl;
std::cout << "序列化大小:" << buffer.size() << " 字节" << std::endl;
}
void TestJSON() {
Json::Value person;
person["name"] = "张三";
person["age"] = 25;
Json::StreamWriterBuilder writer;
auto start = std::chrono::high_resolution_clock::now();
for (int i = 0; i < TEST_COUNT; ++i) {
std::string json_str = Json::writeString(writer, person);
Json::Value parsed;
Json::Reader().parse(json_str, parsed);
}
auto end = std::chrono::high_resolution_clock::now();
auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
std::cout << "JSON 测试 " << TEST_COUNT << " 次耗时:" << duration.count() << "ms" << std::endl;
std::cout << "序列化大小:" << Json::writeString(writer, person).size() << " 字节" << std::endl;
}
int main() {
std::cout << "=== 性能对比测试 ===" << std::endl;
TestProtobuf();
TestJSON();
return 0;
}
测试结果(仅供参考):
=== 性能对比测试 ===
Protobuf 测试 10000 次耗时:45ms
序列化大小:12 字节
JSON 测试 10000 次耗时:120ms
序列化大小:25 字节
可以看到 Protobuf 在性能和空间上都有明显优势!
reserved 关键字message MyMessage {
// 保留已删除的字段编号
reserved 2, 15, 9 to 11;
reserved "old_field";
string new_field = 16;
}
// 错误:修改字段类型
// int32 age = 2; // 原来 string age = 2;
// 错误!
// 正确:添加新字段
int32 age = 2;
string birthday = 3; // 新增
// 优化速度(默认)
option optimize_for = SPEED;
// 优化代码大小
option optimize_for = CODE_SIZE;
// 精简运行时(移动设备)
option optimize_for = LITE_RUNTIME;
让我们看看如何在网络编程中使用 Protobuf:
server.cpp(简化版):
#include <iostream>
#include <thread>
#include <vector>
#include "contacts.pb.h"
class ContactServer {
private:
contacts::Contacts contacts_;
public:
void AddContact(const contacts::PeopleInfo& person) {
*contacts_.add_contacts() = person;
SaveToFile();
}
std::string SerializeContacts() {
std::string data;
contacts_.SerializeToString(&data);
return data;
}
private:
void SaveToFile() {
std::ofstream file("contacts.dat", std::ios::binary);
contacts_.SerializeToOstream(&file);
}
};
// 网络处理线程
void HandleClient(int client_fd, ContactServer& server) {
char buffer[1024];
ssize_t n = read(client_fd, buffer, sizeof(buffer));
if (n > 0) {
contacts::PeopleInfo person;
if (person.ParseFromArray(buffer, n)) {
server.AddContact(person);
std::cout << "收到新联系人:" << person.name() << std::endl;
}
}
close(client_fd);
}
使用 Protobuf:
使用 JSON:
reserved,不要重用编号通过本文的学习,你应该已经掌握了:
✅ 基础概念:理解序列化和 Protobuf 的作用 ✅ 环境搭建:在不同平台安装配置 Protobuf ✅ 语法掌握:熟悉 proto3 的各种语法特性 ✅ 实战开发:完成通讯录项目的各个版本 ✅ 性能优化:了解 Protobuf 的性能优势 ✅ 最佳实践:掌握开发中的注意事项
Protobuf 作为现代分布式系统的核心技术之一,掌握它对于 C++ 开发者来说至关重要。希望这篇教程能帮助你在 Protobuf 的学习道路上顺利前行!

微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML 转 Markdown 互为补充。 在线工具,Markdown 转 HTML在线工具,online
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML 转 Markdown在线工具,online
通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online
将JSON字符串修饰为友好的可读格式。 在线工具,JSON美化和格式化在线工具,online