跳到主要内容
Protobuf C++ 实战入门指南 | 极客日志
C++
Protobuf C++ 实战入门指南 介绍 Google Protocol Buffers(Protobuf)在 C++ 中的实战应用。涵盖序列化概念、Windows/Linux 环境搭建、proto 文件编写及编译流程。详解 Proto3 语法特性包括字段规则、嵌套消息、枚举、oneof 及 map。通过通讯录系统案例演示版本兼容性与 Any 类型使用。对比 Protobuf 与 JSON 性能差异,并提供字段编号规则、兼容性处理及网络编程示例。适合 C++ 开发者快速掌握高效数据传输方案。
前言
作为一名 C++ 开发者,你是否曾为以下问题烦恼过?
不同服务之间数据传输格式混乱
JSON 序列化/反序列化性能瓶颈
协议字段频繁变更导致的兼容性问题
手写解析代码繁琐且容易出错
今天我要介绍的 Google Protocol Buffers(简称 Protobuf)就是解决这些问题的利器!下面让我用最通俗易懂的方式,带你从零开始掌握 Protobuf。
一、什么是 Protobuf?
1.1 序列化的概念
序列化 :把内存中的对象转换为字节序列的过程
反序列化 :把字节序列恢复为对象的过程
简单来说,就像你要把一本书寄给朋友:
序列化 = 把书打包成包裹
反序列化 = 朋友收到包裹后拆开阅读
1.2 为什么需要序列化?
存储数据 :将对象保存到文件或数据库
网络传输 :通过网络发送对象数据
分布式系统 :不同服务间的数据交换
1.3 Protobuf vs JSON vs XML
特性 JSON XML Protobuf 格式 文本 文本 二进制 可读性 好 好 差(需反序列化) 大小 中等 大 小 速度 中等 慢 快 通用性 通用 通用 Google 生态
简单理解 :Protobuf 就像快递的真空压缩包装,体积小、速度快,但需要专门工具才能打开。
二、环境安装(C++ 版)
2.1 Windows 安装
protoc --version
2.2 Linux 安装
apt-get install autoconf automake libtool curl make g++ unzip -y
wget https://github.com/protocolbuffers/protobuf/releases/download/v21.11/protobuf-all-21.11.zip
unzip protobuf-all-21.11.zip
protobuf-21.11
./autogen.sh
./configure
make
make install
protoc --version
sudo
cd
sudo
2.3 C++ 项目配置 # 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})
三、第一个 Protobuf 程序
3.1 创建 .proto 文件 // 指定使用 proto3 语法
syntax = "proto3";
// 包名,相当于 C++ 的命名空间
package contacts;
// 定义联系人消息
message PeopleInfo {
string name = 1; // 姓名
int32 age = 2; // 年龄
}
3.2 编译生成 C++ 代码
protoc --cpp_out=. contacts.proto
3.3 编写 C++ 测试程序 #include <iostream>
#include <string>
#include "contacts.pb.h"
int main () {
contacts::PeopleInfo people;
people.set_name ("张三" );
people.set_age (20 );
std::string serialized_str;
if (!people.SerializeToString (&serialized_str)) {
std::cerr << "序列化失败!" << std::endl;
return -1 ;
}
std::cout << "序列化成功!字节数:" << serialized_str.size () << std::endl;
contacts::PeopleInfo new_people;
if (!new_people.ParseFromString (serialized_str)) {
std::cerr << "反序列化失败!" << std::endl;
return -1 ;
}
std::cout << "姓名:" << new_people.name () << std::endl;
std::cout << "年龄:" << new_people.age () << std::endl;
return 0 ;
}
3.4 编译运行
g++ main.cpp contacts.pb.cc -o demo -std=c++11 -lprotobuf
./demo
四、Proto3 语法详解
4.1 字段规则
基本字段类型 .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;
4.2 嵌套消息 message PeopleInfo {
string name = 1;
int32 age = 2;
// 内嵌消息
message Phone {
string number = 1;
string type = 2; // "home", "work", "mobile"
}
repeated Phone phones = 3;
}
contacts::PeopleInfo person;
contacts::PeopleInfo_Phone* phone = person.add_phones ();
phone->set_number ("13800138000" );
phone->set_type ("mobile" );
4.3 枚举类型 message Phone {
string number = 1;
enum PhoneType {
MP = 0; // 移动电话
TEL = 1; // 固定电话
}
PhoneType type = 2;
}
4.4 导入其他 proto 文件 // 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; // 使用导入的类型
}
五、实战项目:通讯录系统
5.1 版本 1.0:基础通讯录 syntax = "proto3";
package contacts_v1;
// 联系人
message PeopleInfo {
string name = 1;
int32 age = 2;
}
// 通讯录
message Contacts {
repeated PeopleInfo contacts = 1;
}
#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 ;
}
5.2 版本 2.0:增强功能 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;
}
oneof 字段 :多个字段中只能设置一个
map 字段 :键值对存储
枚举类型 :类型安全的选项
5.3 版本 3.0:Any 类型和未知字段 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;
}
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);
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 的优势:
#include <iostream>
#include <chrono>
#include <json/json.h>
#include "contacts.pb.h"
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 在性能和空间上都有明显优势!
七、最佳实践和注意事项
7.1 字段编号规则
编号范围 : 1-536,870,911(但 1-15 更高效)
保留编号 : 不要修改已使用的字段编号
删除字段 : 使用 reserved 关键字
message MyMessage {
// 保留已删除的字段编号
reserved 2, 15, 9 to 11;
reserved "old_field";
string new_field = 16;
}
7.2 版本兼容性 // 错误:修改字段类型
// int32 age = 2; // 原来 string age = 2;
// 错误!
// 正确:添加新字段
int32 age = 2;
string birthday = 3; // 新增
7.3 编译优化选项 // 优化速度(默认)
option optimize_for = SPEED;
// 优化代码大小
option optimize_for = CODE_SIZE;
// 精简运行时(移动设备)
option optimize_for = LITE_RUNTIME;
八、网络应用示例 让我们看看如何在网络编程中使用 Protobuf:
#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);
}
九、常见问题解答
Q1: Protobuf 和 JSON 如何选择?
高性能要求的场景
内部服务通信
移动端应用
需要向前/向后兼容
需要人类可读的配置文件
Web API 接口
简单的数据交换
第三方接口对接
Q2: Protobuf 有哪些缺点?
可读性差 :二进制格式无法直接阅读
需要编译 :需要 proto 编译器
灵活性差 :相比 JSON 动态性较差
生态限制 :主要在 Google 生态中流行
Q3: 如何处理字段变更?
添加字段 :直接添加,使用新编号
删除字段 :标记为 reserved,不要重用编号
修改字段 :创建新字段,逐步迁移
十、总结 ✅ 基础概念 :理解序列化和 Protobuf 的作用
✅ 环境搭建 :在不同平台安装配置 Protobuf
✅ 语法掌握 :熟悉 proto3 的各种语法特性
✅ 实战开发 :完成通讯录项目的各个版本
✅ 性能优化 :了解 Protobuf 的性能优势
✅ 最佳实践 :掌握开发中的注意事项
Protobuf 作为现代分布式系统的核心技术之一,掌握它对于 C++ 开发者来说至关重要。希望这篇教程能帮助你在 Protobuf 的学习道路上顺利前行!
相关免费在线工具 Base64 字符串编码/解码 将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
Base64 文件转换器 将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
Markdown转HTML 将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
HTML转Markdown 将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online
JSON 压缩 通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online
JSON美化和格式化 将JSON字符串修饰为友好的可读格式。 在线工具,JSON美化和格式化在线工具,online