目录
[一、什么是 Protocol Buffers?](#一、什么是 Protocol Buffers?)
[2.1 安装 Protobuf 编译器](#2.1 安装 Protobuf 编译器)
[2.2 定义 .proto 文件](#2.2 定义 .proto 文件)
[2.3 编译生成 C++ 代码](#2.3 编译生成 C++ 代码)
[三、Message 接口详解](#三、Message 接口详解)
[3.1 基础字段操作接口](#3.1 基础字段操作接口)
[3.2 Repeated 字段(数组)操作](#3.2 Repeated 字段(数组)操作)
[3.3 Map 字段(字典)操作](#3.3 Map 字段(字典)操作)
[4.1 序列化接口](#4.1 序列化接口)
[4.2 反序列化接口](#4.2 反序列化接口)
[4.3 完整示例](#4.3 完整示例)
[4.4 其他实用接口](#4.4 其他实用接口)
[五、Protobuf 与 JSON 互转](#五、Protobuf 与 JSON 互转)
[5.1 转换选项配置](#5.1 转换选项配置)
[5.2 Proto 转 JSON](#5.2 Proto 转 JSON)
[5.3 JSON 转 Proto](#5.3 JSON 转 Proto)
[5.4 完整示例](#5.4 完整示例)
[六、RPC 服务接口(Service)](#六、RPC 服务接口(Service))
[6.1 生成的接口类](#6.1 生成的接口类)
[6.2 使用示例](#6.2 使用示例)
[7.1 字段编号管理](#7.1 字段编号管理)
[7.2 使用技巧](#7.2 使用技巧)
[7.3 版本兼容性](#7.3 版本兼容性)
[7.4 程序启动与退出](#7.4 程序启动与退出)
一、什么是 Protocol Buffers?
Protocol Buffers(简称 Protobuf)是 Google 开发的一种轻便、高效的结构化数据存储/交换格式,类似于 XML 或 JSON,但比它们更小、更快、更简单。
Protobuf 使用接口描述语言(IDL)定义数据结构,然后自动生成各种编程语言的代码来操作这些数据结构。适合:
数据序列化:网络通信、数据存储等场景
跨语言服务:构建跨语言的 RPC(远程过程调用)服务
数据兼容性:通过版本控制机制实现向后兼容
二、环境安装与代码生成
2.1 安装 Protobuf 编译器
bash
# Ubuntu/Debian 系统
sudo apt install protobuf-compiler libprotobuf-dev
# 如果需要使用 gRPC 相关编译
sudo apt install protobuf-compiler-grpc
2.2 定义 .proto 文件
创建一个 example.proto 文件:
cpp
syntax = "proto3"; // 指定使用 proto3 语法
package example; // 定义命名空间
// 默认情况下 protoc 不会针对 service 生成 RPC 代码
// 需要开启此选项才会生成
option cc_generic_services = true;
// 定义消息结构
message Person
{
string name = 1; // 普通字段
int32 age = 2; // 整型字段
string email = 3; // 字符串字段
repeated string skills = 4; // 重复字段(数组)
map<string, float> scores = 5; // 映射字段(字典)
}
// 定义 RPC 服务
service Greeter
{
rpc SayHello (HelloRequest) returns (HelloReply) {}
}
message HelloRequest
{
string name = 1;
}
message HelloReply
{
string message = 1;
}
2.3 编译生成 C++ 代码
bash
# 基本编译命令
protoc --cpp_out=./ example.proto
# 指定搜索路径
protoc -I=protobuf --cpp_out=./ protobuf/example.proto
# proto3 中使用 optional 需要添加实验性选项
protoc --cpp_out=./ --experimental_allow_proto3_optional example.proto
编译后会生成两个文件:
example.pb.h:头文件,包含类定义
example.pb.cc:实现文件
三、Message 接口详解
每个在 proto 中定义的 message,编译后都会生成一个继承自
google::protobuf::Message的派生类。这个类提供了丰富的接口来操作消息字段。
3.1 基础字段操作接口
对于每个字段,Protobuf 都会生成一套标准的访问接口:
| 操作类型 | 接口命名规则 | 示例(name 字段) |
|---|---|---|
| 获取值 | 字段名() | name() |
| 设置值 | set_字段名() | set_name("Jack") |
| 获取可变引用 | mutable_字段名() | mutable_name() |
| 清空字段 | clear_字段名() | clear_name() |
| 检查是否存在 | has_字段名() | has_name()(仅 optional) |
代码示例:
cpp
#include "example.pb.h"
#include <iostream>
int main()
{
example::Person person;
// 设置普通字段
person.set_name("Jack");
person.set_age(28);
person.set_email("jack@example.com");
// 获取字段值
std::cout << "Name: " << person.name() << std::endl;
std::cout << "Age: " << person.age() << std::endl;
// 修改字符串(获取可变指针)
std::string* name_ptr = person.mutable_name();
*name_ptr = "Jackson";
// 清空字段
person.clear_email();
return 0;
}
3.2 Repeated 字段(数组)操作
对于标记为
repeated的字段,生成的接口类似于 C++ 的std::vector:
cpp
// 获取元素数量
int skills_size() const;
// 清空所有元素
void clear_skills();
// 通过索引获取元素(const 版本)
const std::string& skills(int index) const;
// 通过索引获取可变元素
std::string* mutable_skills(int index);
// 设置指定索引的元素
void set_skills(int index, const std::string& value);
// 添加新元素,返回指针用于修改
std::string* add_skills();
// 添加新元素(拷贝)
void add_skills(const std::string& value);
使用示例:
cpp
// 添加技能
person.add_skills("C++");
person.add_skills("Java");
person.add_skills("Python");
// 遍历 repeated 字段
int sz = person.skills_size();
for (int i = 0; i < sz; i++)
{
std::cout << "Skill " << i << ": " << person.skills(i) << std::endl;
}
// 修改指定元素
*person.mutable_skills(0) = "C++17";
3.3 Map 字段(字典)操作
Map 字段对应
google::protobuf::Map类,接口与 C++ STL 的std::map类似:
cpp
// 获取元素数量
int scores_size() const;
// 清空 map
void clear_scores();
// 获取 const map 引用
const ::google::protobuf::Map<std::string, float>& scores() const;
// 获取可变 map 指针
::google::protobuf::Map<std::string, float>* mutable_scores();
使用示例:
cpp
// 获取 map 指针并插入数据
auto score_map = person.mutable_scores();
score_map->insert({"Chinese", 88.5});
score_map->insert({"Math", 77.5});
score_map->insert({"English", 99.0});
// 使用 MapPair 插入(另一种方式)
score_map->insert(google::protobuf::MapPair<std::string, float>("Physics", 85.0));
// 遍历 map(const 版本)
for (const auto& pair : person.scores())
{
std::cout << pair.first << ": " << pair.second << std::endl;
}
// 查找元素
auto it = person.scores().find("Chinese");
if (it != person.scores().end())
{
std::cout << "Chinese score: " << it->second << std::endl;
}
四、序列化与反序列化接口
这是 Protobuf 最核心的功能,所有消息类都继承自
MessageLite,提供了多种序列化/反序列化方式
4.1 序列化接口
cpp
// 序列化为 std::string
bool SerializeToString(std::string* output) const;
std::string SerializeAsString() const; // 直接返回 string,失败返回空
// 序列化到字符数组
bool SerializeToArray(void* data, int size) const;
// 序列化到输出流
bool SerializeToOstream(std::ostream* output) const;
4.2 反序列化接口
cpp
// 从 std::string 解析
bool ParseFromString(const std::string& data);
// 从字符数组解析
bool ParseFromArray(const void* data, int size);
// 从输入流解析
bool ParseFromIstream(std::istream* input);
4.3 完整示例
cpp
#include <iostream>
#include "example.pb.h"
int main()
{
// ========== 序列化 ==========
example::Person person;
person.set_name("Jack");
person.set_age(28);
person.set_email("jack@example.com");
person.add_skills("C++");
person.add_skills("Python");
auto scores = person.mutable_scores();
scores->insert({"Algorithm", 95.0});
scores->insert({"DataStructure", 88.0});
// 序列化为字符串
std::string serialized = person.SerializeAsString();
if (serialized.empty())
{
std::cerr << "Serialization failed!" << std::endl;
return -1;
}
std::cout << "Serialized size: " << serialized.size() << " bytes" << std::endl;
// ========== 反序列化 ==========
example::Person restored;
bool success = restored.ParseFromString(serialized);
if (!success)
{
std::cerr << "Deserialization failed!" << std::endl;
return -1;
}
// 验证数据
std::cout << "\nRestored data:" << std::endl;
std::cout << "Name: " << restored.name() << std::endl;
std::cout << "Age: " << restored.age() << std::endl;
return 0;
}
4.4 其他接口
cpp
// 清空所有字段
void Clear();
// 获取类型名称
std::string GetTypeName() const;
// 获取字段描述符(用于反射)
const Descriptor* GetDescriptor();
// 获取反射接口
const Reflection* GetReflection();
// 交换两个消息的内容
void swap(Message& other);
void CopyFrom(const Message& from); // 深拷贝
五、Protobuf 与 JSON 互转
Protobuf 提供了与 JSON 格式互转的实用工具,位于
google::protobuf::util命名空间:
5.1 转换选项配置
cpp
// JSON 解析选项
struct JsonParseOptions
{
bool ignore_unknown_fields; // 是否忽略未知的 JSON 字段
bool case_insensitive_enum_parsing; // 解析枚举时是否不区分大小写
};
// JSON 输出选项
struct JsonPrintOptions
{
bool add_whitespace; // 是否添加空白字符让 JSON 更易读
bool always_print_primitive_fields; // 是否总是输出基本类型字段
bool always_print_enums_as_ints; // 是否将枚举作为整数输出
bool preserve_proto_field_names; // 是否保留原始字段名(不使用驼峰命名)
};
5.2 Proto 转 JSON
cpp
#include <google/protobuf/util/json_util.h>
std::string ProtoToJson(const google::protobuf::Message& msg)
{
google::protobuf::util::JsonOptions options;
options.add_whitespace = true; // 格式化输出
options.always_print_primitive_fields = true; // 输出所有字段
std::string json_output;
auto status = google::protobuf::util::MessageToJsonString(msg, &json_output, options);
if (!status.ok())
{
std::cerr << "Conversion failed: " << status.ToString() << std::endl;
return "";
}
return json_output;
}
5.3 JSON 转 Proto
cpp
bool JsonToProto(const std::string& json_str, google::protobuf::Message* msg)
{
google::protobuf::util::JsonParseOptions options;
options.ignore_unknown_fields = true; // 忽略未知字段
auto status = google::protobuf::util::JsonStringToMessage(json_str, msg, options);
if (!status.ok())
{
std::cerr << "Parse failed: " << status.ToString() << std::endl;
return false;
}
return true;
}
5.4 完整示例
cpp
#include <iostream>
#include <google/protobuf/util/json_util.h>
#include "example.pb.h"
int main()
{
// 创建并填充消息
example::Person person;
person.set_name("Jack");
person.set_age(0); // 测试默认值输出
person.set_email("jack@example.com");
person.add_skills("C++");
person.add_skills("Java");
auto scores = person.mutable_scores();
scores->insert({"Math", 88.0});
// Proto -> JSON
std::cout << "=== Proto to JSON ===" << std::endl;
std::string json_str = ProtoToJson(person);
std::cout << json_str << std::endl;
// JSON -> Proto
std::cout << "\n=== JSON to Proto ===" << std::endl;
example::Person restored;
if (JsonToProto(json_str, &restored))
{
// 使用 has_ 接口检查 optional 字段(proto3 需要开启 optional 选项)
if (restored.has_name())
{
std::cout << "name: " << restored.name() << std::endl;
}
if (restored.has_age())
{
std::cout << "age: " << restored.age() << std::endl;
}
}
return 0;
}
输出示例:
cpp
{
"name": "Jack",
"age": 0,
"email": "jack@example.com",
"skills": ["C++", "Java"],
"scores": {
"Math": 88
}
}
六、RPC 服务接口(Service)
当 proto 文件中定义了
service时,Protobuf 会生成对应的 RPC 接口类:
6.1 生成的接口类
cpp
// 服务端接口(纯虚类,需要继承实现)
class Greeter : public ::google::protobuf::Service
{
public:
virtual void SayHello(
::google::protobuf::RpcController* controller,
const ::example::HelloRequest* request,
::example::HelloReply* response,
::google::protobuf::Closure* done) = 0;
};
// 客户端 Stub(用于发起 RPC 调用)
class Greeter_Stub : public Greeter
{
public:
Greeter_Stub(::google::protobuf::RpcChannel* channel);
void SayHello(::google::protobuf::RpcController* controller,
const ::example::HelloRequest* request,
::example::HelloReply* response,
::google::protobuf::Closure* done) override;
};
6.2 使用示例
cpp
// 服务端实现
class GreeterServiceImpl : public example::Greeter
{
public:
void SayHello(::google::protobuf::RpcController* controller,
const ::example::HelloRequest* request,
::example::HelloReply* response,
::google::protobuf::Closure* done) override
{
std::string greeting = "Hello, " + request->name();
response->set_message(greeting);
done->Run(); // 通知 RPC 框架调用完成
}
};
// 客户端调用
void CallService(::google::protobuf::RpcChannel* channel)
{
example::Greeter_Stub stub(channel);
example::HelloRequest request;
request.set_name("World");
example::HelloReply response;
::google::protobuf::RpcController* controller = ...;
::google::protobuf::Closure* done = ...;
stub.SayHello(controller, &request, &response, done);
}
七、注意事项
7.1 字段编号管理
不要重用字段编号 :删除字段后,应将其编号标记为
reserved不要随意更改字段类型:可能影响兼容性
预留字段编号:为未来扩展预留空间
cpp
message User
{
string name = 1;
int32 age = 2;
reserved 3 to 10; // 预留编号
reserved "foo", "bar"; // 预留字段名
string email = 11;
}
7.2 使用技巧
重用消息对象:消息对象被清除后会保留内存空间供重用,减少内存分配压力
避免频繁拷贝 :使用
Swap()或mutable_接口避免不必要的拷贝
7.3 版本兼容性
不要更改已有字段的编号
不要删除
required字段(proto3 已移除 required)可以添加新的
optional或repeated字段,但必须使用新的编号使用
optional关键字明确标识可选字段,便于通过has_接口检查
7.4 程序启动与退出
cpp
#include <google/protobuf/stubs/common.h>
int main()
{
// 验证链接的库版本与编译的头文件版本兼容
GOOGLE_PROTOBUF_VERIFY_VERSION;
// ... 你的代码 ...
// 可选:清理所有全局对象(仅在内存泄漏检测或动态库场景需要)
google::protobuf::ShutdownProtobufLibrary();
return 0;
}
参考资源:
感谢阅读,本文如有错漏之处,烦请斧正。