【学习笔记】Protobuf
Protocol Buffers(简称 Protobuf)是由 Google 开发的跨平台、高效的数据序列化协议,用于结构化数据的存储和传输。它类似于 JSON/XML,但具有更高的效率、更小的体积和更强的跨语言支持,广泛应用于微服务、分布式系统和移动应用的数据交互中。
Protobuf 通过二进制编码、自动代码生成和灵活的字段扩展机制,在性能、兼容性和开发效率上实现了多维突破。
主要其中的bytes类型也能支持json格式传输,接收方直接调用json的库直接实现大量数据的传输。在数据量大并且一个一个写字段复杂的情况下,很友好。
一、protobuf实现流程
定义.proto 文件
每个字段需指定唯一编号(1, 2, 3…),用于二进制格式中标识字段,编号在后续版本中不可修改。
syntax = "proto3"; // 指定版本(当前主流为proto3)
package tutorial; // 包名,避免命名冲突
// 消息类型定义
message Person {
int32 id = 1; // 字段编号1,int32类型
string name = 2; // 字段编号2,字符串类型
repeated string emails = 3; // 重复字段(数组)
enum Gender { // 枚举类型
MALE = 0;
FEMALE = 1;
}
Gender gender = 4; // 枚举字段
}
使用 Protobuf 编译器protoc生成目标语言代码,这里用的是C++。
protoc --cpp_out=. example.proto
#include <iostream>
#include <fstream>
#include "example.pb.h" // 假设protoc已生成该头文件
using namespace std;
int main() {
// 序列化(编码)
tutorial::Person person; // tutorial为.proto文件中的package
person.set_id(123);
person.set_name("Alice");
person.add_emails("alice@example.com");
person.set_gender(tutorial::Person::FEMALE); // 枚举类型
// 序列化为二进制数据
string data;
person.SerializeToString(&data);
// 或者输出到文件流
// fstream output("person.bin", ios::out | ios::binary);
// person.SerializeToOstream(&output);
// 反序列化(解码)
tutorial::Person parsedPerson;
parsedPerson.ParseFromString(data);
// 或者从文件流读取
// fstream input("person.bin", ios::in | ios::binary);
// parsedPerson.ParseFromIstream(&input);
cout << "Name: " << parsedPerson.name() << endl; // 输出:Alice
return 0;
}
二、 bytes类型结合json
个人比较喜欢的点:
比较喜欢的一点:
protobuf不仅仅局限于一个变量一个变量的定义与传递,还支持bytes类型,可以将这个类型传递json格式的字符串,在解析的时候利用json格式解析即可,如果数据量很大不愿意一个一个定义字段的情况下,极为方便。
syntax = "proto3";
package example;
message JsonContainer {
bytes json_content = 1; // 存储JSON文本的二进制数据
}
#include <iostream>
#include <fstream>
#include <string>
#include "json_container.pb.h" // 由protoc生成
#include "nlohmann/json.hpp" // JSON解析库(需额外安装)
using json = nlohmann::json; // 简化命名
int main() {
// === 创建JSON数据 ===
json layer_json;
layer_json["Info"]["id"] = 1;
layer_json["Info"]["name"] = "Alice";
layer_json["Info"]["emails"] = "alice@example.com";
// 将JSON转换为字符串
std::string json_str = layer_json.dump(); // 转为JSON文本
std::cout << "原始JSON: " << json_str << std::endl;
// === Protobuf序列化(JSON → bytes) ===
example::JsonContainer container;
container.set_json_content(json_str); // 将JSON文本存入bytes字段
// 序列化为二进制数据
std::string serializedData;
container.SerializeToString(&serializedData);
std::cout << "序列化后大小: " << serializedData.size() << " 字节" << std::endl;
// === Protobuf反序列化(bytes → JSON) ===
example::JsonContainer parsedContainer;
parsedContainer.ParseFromString(serializedData);
// 提取JSON字符串
std::string extractedJson = parsedContainer.json_content();
// 解析JSON
try {
json parsedJson = json::parse(extractedJson);
// 验证解析结果
std::cout << "解析后的JSON:" << std::endl;
std::cout << "ID: " << parsedJson["Info"]["id"] << std::endl;
std::cout << "Name: " << parsedJson["Info"]["name"] << std::endl;
std::cout << "Email: " << parsedJson["Info"]["emails"] << std::endl;
} catch (const json::parse_error& e) {
std::cerr << "JSON解析错误: " << e.what() << std::endl;
return 1;
}
return 0;
}
结果:
原始JSON: {"Info":{"id":1,"name":"Alice","emails":"alice@example.com"}}
序列化后大小: 75 字节
解析后的JSON:
ID: 1
Name: Alice
Email: alice@example.com
json一般用的nlohmann:
json | 介绍 |
---|---|
nlohmann/json | - 头文件仅需包含单一文件 - C++11 兼容 - 简洁的 API(类似 Python) - 自动类型推导 |
这种方式有几个优势:
1、 可以充分利用两者的优势。
(1)protobuf的优势包括数据压缩(序列化为 Protobuf 的bytes后,体积通常比原始 JSON 小 30%~70%,尤其适合带宽受限或存储密集型场景(如物联网、大数据传输))传输效率(Protobuf 的二进制解析速度远快于 JSON(如 C++ 的 Protobuf 库解析效率约为 JSON 库的 3~5 倍),在处理高频数据交互时(如实时通信、API 接口),可显著降低 CPU 开销)。
(2)JSON是文本格式,可读性好,容易解析,结合起来香饽饽。
2、 利用json格式的灵活性,可以随时添加字段而不需要去修改protobuf的配置文件添加字段,对于随时需要添加字段的数据比较方便。
3、 后端服务可使用 Protobuf 进行高效通信(如 C++),而前端或低性能设备可通过解析 JSON 数据减少计算开销。例如:服务端用 Protobuf 序列化 JSON 数据为bytes,通过网络传输;客户端接收到数据后,用 Protobuf 解析出bytes,再转换为 JSON 进行展示。
三、repeated 关键字
repeated 关键字用于声明一个字段为数组类型(或称为 “重复字段”),类似于 C++ 中的 std::vector。它允许消息中包含零个或多个相同类型的值,且这些值会按顺序存储。
在一般的轮巡场景中可以用到这个关键字。
// addressbook.proto
syntax = "proto3";
message Person {
string name = 1;
int32 id = 2;
string email = 3;
}
message AddressBook {
repeated Person people = 1; // 重复字段:存储多个Person对象
}
#include <iostream>
#include "addressbook.pb.h" // 由protoc生成的头文件
int main() {
// 创建AddressBook对象
tutorial::AddressBook address_book;
// 添加第一个Person
tutorial::Person* person1 = address_book.add_people(); // 创建并添加新Person
person1->set_name("Alice");
person1->set_id(1);
person1->set_email("alice@example.com");
// 添加第二个Person
tutorial::Person* person2 = address_book.add_people();
person2->set_name("Bob");
person2->set_id(2);
person2->set_email("bob@example.com");
// 访问重复字段
std::cout << "联系人数量: " << address_book.people_size() << std::endl;
// 遍历所有Person
for (int i = 0; i < address_book.people_size(); i++) {
const tutorial::Person& person = address_book.people(i);
std::cout << "联系人 #" << i + 1 << ":\n";
std::cout << " 姓名: " << person.name() << "\n";
std::cout << " ID: " << person.id() << "\n";
std::cout << " Email: " << person.email() << "\n";
}
// 修改现有元素(例如修改第一个联系人的邮箱)
if (address_book.people_size() > 0) {
address_book.mutable_people(0)->set_email("alice.new@example.com");
std::cout << "修改后第一个联系人的邮箱: "
<< address_book.people(0).email() << std::endl;
}
return 0;
}
注意:
序列化顺序:repeated 字段的元素在序列化时会按添加顺序排列,反序列化时也会保持相同顺序。这也是正常轮巡过程中按照顺序进行轮巡的要求之一了。