一、Google Protobuf项目简介
Google Protobuf(Protocol Buffers)是一种高效的、跨多语言的开源序列化实现方案,它支持在多种操作系统平台上定义结构化数据。
Protocol Buffers支持C++、C#、Dart、Go、Java、Kotlin、Objective-C、Python和Ruby等编程语言, Protocol Buffers序列化后的数据体积比JSON/XML小3-10倍,读写效率更高。
在C++开发场景中,Protobuf通过.proto文件定义消息体,这些消息体由基本数据类型和复合数据类型组成,所有数据类型在被序列化时会自动优化存储大小和性能,确保跨平台兼容性。
Google Protobuf在大型软件项目中的应用场景:
1.gRPC框架的默认序列化协议,实现服务的跨语言调用。
2.Netflix的微服务通信和配置分发,实现每秒处理百万级服务调用。
3.Uber的实时位置追踪和派单系统,高峰期支持每秒20万订单处理。
4.物联网边缘计算,在低功耗设备上的高效数据传输。
5.AI模型部署,TensorFlow Serving的预测请求编码。
Protobuf序列化过程的核心设计:
1.TLV编码结构(TAG-LENGTH-VALUE)
Tag:字段的唯一标识(字段编号+数据类型)。
Length:仅变长类型(如string、bytes)需要,固定长度类型(如int32)可以省略。
Value:具体的数据值,编码方式由数据类型决定。
+—–+——–+——-+
| Tag | Length | Value |
+—–+——–+——-+
2.Varint压缩(整数类型优化)
以每个字节最高位MSB为标志位:
MSB=1:表示后续字节仍属于当前数据。
MSB=0:表示当前字节是最后一个。
3.ZigZag编码
解决Varint对负数压缩效率低的问题。
二、Protobuf开发环境搭建
从github下载指定版本的源码进行编译和安装。
wget https://github.com/protocolbuffers/protobuf/archive/v3.15.8.tar.gz –no-check-certificate
./autogen.sh
./configure –prefix=/usr/local
make && make install
更新库缓存
ldconfig
输入以下shell命令行,检查是否安装成功:
查看 Protobuf 库的版本
protoc –version
查看 Protobuf 库的头文件
ls -al /usr/local/include/google/protobuf
查看 Protobuf 库的动态库
ls -al /usr/local/lib/libproto*
三、Protobuf语法详解
定义一个查询指定字段的搜索请求消息体格式,proto文件样例如下:
proto2语法:
syntax = “proto2”;
message SearchRequest {
optional string query = 1;
optional int32 page_number = 2;
optional int32 results_per_page = 3;
}
proto3语法:
syntax = “proto3”;
message SearchRequest {
string query = 1;
int32 page_number = 2;
int32 results_per_page = 3;
}
proto文件中可以添加C/C++/Java语言’//’行尾风格的注释,也可以添加’/…/’风格的多行注释。
/*
- SearchRequest represents a search query
- with pagination options to
- indicate which results to include in the response.
*/
message SearchRequest {
string query = 1; // Which page number do we want?
int32 page_number = 2; // Number of results to return per page.
int32 results_per_page = 3;
}
Proto3语法中的数据类型:
a.基本数据类型:
1.整型
int32, int64:32位和64位有符号整数,对应C++的int32_t和int64_t。
uint32, uint64:32位和64位无符号整数,对应C++的uint32_t和uint64_t。
2.浮点型
float:单精度浮点数,对应C++的float。
double:双精度浮点数,对应C++的double。
3.其他
bool:布尔值,对应C++的bool。
string:UTF-8字符串,对应C++的std::string。
bytes:二进制数据,对应C++的std::string,用于存储原始字节。
b.复合数据类型:
1.message类型
自定义消息体,可以嵌套其他消息或基本类型,在C++开发场景,.proto文件中的每一个message消息体都对应一个C++类。
message User {
string name = 1;
int32 age = 2;
repeated string emails = 3; // 重复字段,表示列表
}
2.enum枚举类型
定义一组命名常量。
enum Status {
UNKNOWN = 0;
ACTIVE = 1;
INACTIVE = 2;
}
3.repeated类型
表示有重复值,字段的多个重复值会按照顺序保留,比如列表或数组,对应C++的std::vector。
例如,”repeated int32 scores” 生成”std::vector”。
4.map键值对类型
键值对映射,对应C++的std::map类型。
例如,”map phone_book”。
5.oneof类型
互斥字段组,确保同一时间只有一个字段被设置。
oneof contact_method {
string email = 1;
string phone = 2;
}
Protobuf支持导入第三方.proto文件
我们可以通过import关键字来导入其他.proto文件中定义的消息体,样例如下:
// Located in my_project/protos/main.proto
import “common/timestamp.proto”;
proto文件结构如下:
my_project/
├── protos/
│ ├── main.proto
│ └── common/
│ └── timestamp.proto
Protobuf支持命名空间管理
用package关键字声明一个包的名称,可以防止不同的消息类型有命名冲突。对于C++编程场景,产生的类会被包装在C++的命名空间中。
例如,以下样例中的Open会被封装在foo::bar命名空间中:
package foo.bar;
message Open { … }
四、Protobuf代码生成
Protobuf代码生成基于IDL(接口描述语言)定义的数据结构,通过protoc编译器将.proto文件转换为目标语言的类文件。
Protobuf代码生成的优点如下:
跨语言支持:同一份.proto文件可同时生成Java/C++/Python/Go等代码。
二进制格式紧凑:生成的数据结构比XML/JSON小3-10倍,更加节省空间。
强类型约束:支持编译时类型检查,避免代码运行时错误。
版本兼容:通过字段编号机制实现向前/向后兼容。
安装完Protobuf开发环境以后,可以使用protoc命令来生成序列化/反序列化代码。
protoc –proto_path=IMPORT_PATH
–cpp_out=DST_DIR –java_out=DST_DIR –python_out=DST_DIR
path/to/file.proto
命令行参数:
IMPORT_PATH:.proto文件所在的具体目录。如果忽略该值,则使用当前目录,-I=IMPORT_PATH是它的简化形式。
DST_DIR:生成的C++\Java\Python代码,生成的C++代码文件有pb.h和pb.cc两种后缀。
C++开发场景使用以下简化命令即可:
protoc -I=$SRC_DIR –cpp_out=$DST_DIR $SRC_DIR/file.proto
五、Protobuf常用API接口
- Message消息类的基本接口
Protobuf中所有消息类都继承自google::protobuf::Message
序列化接口:
// 序列化到输出流
bool SerializeToOstream(std::ostream* output) const;
// 序列化到字符串
bool SerializeToString(std::string* output) const;
// 序列化到字节数组
bool SerializeToArray(void* data, int size) const;
反序列化接口:
// 从输入流反序列化
bool ParseFromIstream(std::istream* input);
// 从字符串反序列化
bool ParseFromString(const std::string& data);
// 从字节数组反序列化
bool ParseFromArray(const void* data, int size);
消息管理接口:
// 深拷贝
void CopyFrom(const Message& from);
// 合并消息(重复字段合并,单字段覆盖)
void MergeFrom(const Message& from);
// 清空所有字段
void Clear();
- 字段访问接口
基本字段:
// 字符串字段
const std::string& name() const; // 获取
void set_name(const std::string& value); // 设置
// 整数字段
int32 id() const; //获取
void set_id(int32 value); //设置
重复字段(repeated):
// 获取字段数量
int phones_size() const;
// 通过索引访问
const std::string& phones(int index) const;
void set_phones(int index, const std::string& value);
// 添加元素
void add_phones(const std::string& value);
// 获取整个列表(只读)
const ::google::protobuf::RepeatedPtrField& phones() const;
// 获取可修改的列表
::google::protobuf::RepeatedPtrField* mutable_phones();
映射字段(map):
// 获取只读映射
const ::google::protobuf::Map& scores() const;
// 获取可修改的映射
::google::protobuf::Map* mutable_scores();
// 直接操作(示例)
(*person.mutable_scores())[“数学”] = 95;
3.调试工具接口
// 将消息转换为可读文本
std::string debug_str = person.DebugString();
include
using google::protobuf::util::JsonStringToMessage;
using google::protobuf::util::MessageToJsonString;
// Protobuf转JSON
std::string json_output;
MessageToJsonString(person, &json_output);
// JSON转Protobuf
Person parsed_person;
JsonStringToMessage(json_output, &parsed_person);
// Protobuf转文本格式打印
std::string text_format;
google::protobuf::TextFormat::PrintToString(person, &text_format);
C++代码样例:
Demo1:序列化 & 反序列化
// 序列化到字符串
std::string serialized_data;
person.SerializeToString(&serialized_data);
// 从字符串反序列化
Person new_person;
new_person.ParseFromString(serialized_data);
// 文件序列化 (需包含)
std::fstream output(“data.bin”, std::ios::out | std::ios::binary);
person.SerializeToOstream(&output);
// 文件反序列化
std::fstream input(“data.bin”, std::ios::in | std::ios::binary);
new_person.ParseFromIstream(&input);
Demo2:文本格式打印
// 获取调试字符串
std::cout << person.DebugString() << std::endl;
// 文本格式序列化
std::string text_format;
google::protobuf::TextFormat::PrintToString(person, &text_format);
// 从文本格式解析
Person text_person;
google::protobuf::TextFormat::ParseFromString(text_format, &text_person);
当用protocolbuffer编译器来运行.proto文件时,编译器将生成所选择语言的代码,这些代码提供了专门的接口用来处理.proto文件中定义的消息字段,包括get/set字段值、序列化到字节流、从字节流中反序列化等。
例如:
edition = “2023”;
package tutorial;
message Person {
string name = 1;
int32 id = 2;
string email = 3;
enum PhoneType {
PHONE_TYPE_UNSPECIFIED = 0;
PHONE_TYPE_MOBILE = 1;
PHONE_TYPE_HOME = 2;
PHONE_TYPE_WORK = 3;
}
message PhoneNumber {
string number = 1;
PhoneType type = 2;
}
repeated PhoneNumber phones = 4;
}
message AddressBook {
repeated Person people = 1;
}
生成的C++代码中,调用专门接口处理消息体:
// name
bool has_name() const; // Only for explicit presence
void clear_name();
const ::std::string& name() const;
void set_name(const ::std::string& value);
::std::string* mutable_name();
// id
bool has_id() const;
void clear_id();
int32_t id() const;
void set_id(int32_t value);
// email
bool has_email() const;
void clear_email();
const ::std::string& email() const;
void set_email(const ::std::string& value);
::std::string* mutable_email();
// phones
int phones_size() const;
void clear_phones();
const ::google::protobuf::RepeatedPtrField< ::tutorial::Person_PhoneNumber >& phones() const;
::google::protobuf::RepeatedPtrField< ::tutorial::Person_PhoneNumber >* mutable_phones();
const ::tutorial::Person_PhoneNumber& phones(int index) const;
::tutorial::Person_PhoneNumber* mutable_phones(int index);
::tutorial::Person_PhoneNumber* add_phones();
六、C++编码实战
Demo1:序列化/反序列化
proto文件定义:
Person.proto文件
syntax = “proto3”;
message Person {
string name = 1;
int32 id = 2;
repeated string phones = 3; // 字符串列表
map scores = 4; // 键值映射
oneof payment_method { // 互斥字段
string credit_card = 5;
string paypal = 6;
}
}
完整C++代码实现:
数据类型处理:
repeated字段:使用add_phones()添加列表元素
map字段:通过mutable_scores()获取可修改的映射指针
资源管理:
GOOGLE_PROTOBUF_VERIFY_VERSION初始化库
ShutdownProtobufLibrary()防止内存泄漏
include
include
include “Person.pb.h”
// 序列化到文件
void serializeToFile(const Person& person, const std::string& filename) {
std::fstream output(filename, std::ios::out | std::ios::binary);
if (!person.SerializeToOstream(&output)) {
std::cerr << “序列化失败!” << std::endl;
}
}
// 从文件反序列化
Person deserializeFromFile(const std::string& filename) {
Person person;
std::fstream input(filename, std::ios::in | std::ios::binary);
if (!person.ParseFromIstream(&input)) {
std::cerr << “反序列化失败!” << std::endl;
}
return person;
}
// 打印Person对象
void printPerson(const Person& person) {
std::cout << “\n=== 反序列化结果验证 ===” << std::endl;
std::cout << “姓名: ” << person.name() << std::endl;
std::cout << “ID: ” << person.id() << std::endl;
// 打印phones列表
std::cout << "电话: ";
for (int i = 0; i < person.phones_size(); ++i) {
std::cout << person.phones(i) << (i < person.phones_size()-1 ? ", " : "");
}
// 打印scores映射
std::cout << "\n分数: ";
for (constauto& [subject, score] : person.scores()) {
std::cout << subject << ":" << score << " ";
}
// 打印oneof字段
std::cout << "\n支付方式: ";
switch (person.payment_method_case()) {
case Person::kCreditCard:
std::cout << "信用卡: " << person.credit_card();
break;
case Person::kPaypal:
std::cout << "PayPal: " << person.paypal();
break;
default:
std::cout << "未设置";
}
std::cout << "\n========================\n" << std::endl;
}
int main() {
GOOGLE_PROTOBUF_VERIFY_VERSION; // 初始化Protobuf库
// 步骤1: 创建并填充Person对象
Person person;
person.set_name("Tim");
person.set_id(1001);
// 添加列表元素
person.add_phones("13800138000");
person.add_phones("010-12345678");
// 添加映射元素
auto* scores = person.mutable_scores();
(*scores)["数学"] = 95;
(*scores)["英语"] = 88;
// 设置oneof字段 (二选一)
person.set_credit_card("6226-8888-6666"); // 设置信用卡
// person.set_paypal("zhangsan@example.com"); // 或设置PayPal
// 步骤2: 序列化到文件
conststd::string filename = "person_data.bin";
serializeToFile(person, filename);
std::cout << "✅ 序列化完成,数据已保存至: " << filename << std::endl;
// 步骤3: 从文件反序列化
Person restored_person = deserializeFromFile(filename);
std::cout << "✅ 反序列化完成,开始验证数据..." << std::endl;
// 步骤4: 验证反序列化结果
printPerson(restored_person);
google::protobuf::ShutdownProtobufLibrary(); // 清理资源
return0;
}
生成C++代码文件:
protoc –cpp_out=. Person.proto
编译运行:
g++ -std=c++11 demo.cpp Person.pb.cc -lprotobuf -pthread -o demo
运行结果:
Demo2:服务器/客户端通信
通信流程图:
proto文件定义:
data.proto文件
syntax = “proto3”;
message Request {
int32 id = 1;
string name = 2;
repeated double values = 3;
enum Operation {
ADD = 0;
MULTIPLY = 1;
}
Operation op = 4;
}
message Response {
int32 id = 1;
double result = 2;
string status = 3;
}
客户端C++代码实现:
include
include
include
include “data.pb.h”
int main() {
// 创建请求
Request request;
request.set_id(1001);
request.set_name(“Calculation”);
request.add_values(2.5);
request.add_values(3.7);
request.add_values(1.8);
request.set_op(Request::MULTIPLY);
// 序列化到文件
std::ofstream outfile("request.bin", std::ios::binary);
if (!request.SerializeToOstream(&outfile)) {
std::cerr << "序列化失败" << std::endl;
return1;
}
outfile.close();
std::cout << "客户端: 请求已序列化到文件\n";
// 等待服务器响应
std::cout << "客户端: 等待响应...\n";
sleep(3);
// 读取响应
Response response;
std::ifstream infile("response.bin", std::ios::binary);
if (!response.ParseFromIstream(&infile)) {
std::cerr << "反序列化失败" << std::endl;
return1;
}
infile.close();
// 显示结果
std::cout << "客户端: 收到响应\n"
<< "ID: " << response.id() << "\n"
<< "结果: " << response.result() << "\n"
<< "状态: " << response.status() << std::endl;
return0;
}
服务器端C++代码实现:
include
include
include
include “data.pb.h”
int main() {
while (true) {
// 检查请求文件
if (access(“request.bin”, F_OK) == 0) {
// 读取请求
Request request;
std::ifstream infile(“request.bin”, std::ios::binary);
if (!request.ParseFromIstream(&infile)) {
std::cerr << “请求反序列化失败” << std::endl;
return1;
}
infile.close();
// 删除请求文件
remove("request.bin");
std::cout << "服务器: 收到请求\n"
<< "ID: " << request.id() << "\n"
<< "操作: " << Request::Operation_Name(request.op()) << "\n"
<< "值: ";
for (double v : request.values()) {
std::cout << v << " ";
}
std::cout << std::endl;
// 处理请求
Response response;
response.set_id(request.id());
double result = 0.0;
switch (request.op()) {
case Request::ADD:
for (double v : request.values()) result += v;
response.set_status("SUCCESS");
break;
case Request::MULTIPLY:
result = 1.0;
for (double v : request.values()) result *= v;
response.set_status("SUCCESS");
break;
default:
response.set_status("INVALID_OPERATION");
}
response.set_result(result);
// 序列化响应
std::ofstream outfile("response.bin", std::ios::binary);
if (!response.SerializeToOstream(&outfile)) {
std::cerr << "响应序列化失败" << std::endl;
return1;
}
outfile.close();
std::cout << "服务器: 响应已发送\n";
}
sleep(1); // 每秒检查一次
}
return0;
}
生成C++代码文件:
protoc –cpp_out=. data.proto
编译运行:
编译客户端
g++ client.cpp data.pb.cc -o client -lprotobuf -pthread
编译服务器
g++ server.cpp data.pb.cc -o server -lprotobuf -pthread
运行结果:
客户端打印:
服务器端打印:
参考阅读:
声明:来自程序员与背包客,仅代表创作者观点。链接:https://eyangzhen.com/6494.html