【C++ 脚手架】Protobuf 的介绍与使用
Protobuf 的介绍与使用
Protocol Buffers(简称 protobuf)是 Google 开发的一种语言中立、平台中立、可扩展的序列化结构数据的方法。它常用于:
-
跨语⾔服务:由于 Protobuf ⽀持多种编程语⾔,它⾮常适合构建跨语⾔的 RPC(远程过程调⽤)服务。
-
数据序列化:用于网络通信、数据存储等场景,将结构化数据序列化为字节流。
Protobuf 定义了⼀种接⼝描述语⾔(IDL),⽤于描述数据结构,然后可以⾃动⽣成各种编程语⾔的代码来操作这些数据结构。
使用流程
-
在
.proto文件中定义数据结果(消息类型)。 -
使用 protoc 编译器生成目标语言的代码。
-
在你的程序中引用生成的代码,进行序列化与反序列化。
一、Protobuf 安装
sudo apt install protobuf-compiler libprotobuf-dev
#如果需要使⽤grpc相关编译
sudo apt install protobuf-compiler-grpc
# 验证是否安装成功
protoc --version
二、Protobuf 基本语法
2.1 基本结构
-
消息:Protobuf 中的基本数据结构单元,类似于 C++ 中的结构体或 Java 中的类。
-
字段:消息中的数据项,每个字段都有⼀个 唯⼀的字段编号 和 数据类型。
- 编号 1 ~ 15 占用 1 个字节,16 ~ 2047 的占用 2 个字节,常用字段应使用较小的编号
- 19000 ~ 19999 不可用,在 Protobuf 协议实现中,对这些数进行了预留
-
枚举:用于定义一组命名的常数。
-
服务:在 RPC 场景中定义服务接口。
2.2 数据类型
| .proto Type | Notes | C++ Type |
|---|---|---|
| double | double | |
| float | float | |
| int32 | 使用变长编码[1]。负数的编码效率较低——若字段可能为负值,应使用 sint32 代替。 | int32 |
| int64 | 使用变长编码[1]。负数的编码效率较低——若字段可能为负值,应使用 sint64 代替。 | int64 |
| uint32 | 使用变长编码[1]。 | uint32 |
| uint64 | 使用变长编码[1]。 | uint64 |
| sint32 | 使用变长编码[1]。符号整型。负值的编码效率高于常规的 int32 类型。 | int32 |
| sint64 | 使用变长编码[1]。符号整型。负值的编码效率高于常规的 int64 类型。 | int64 |
| fixed32 | 定长 4 字节。若值常大于 (2^{28}) 则会比 uint32 更高效。 | uint32 |
| fixed64 | 定长 8 字节。若值常大于 (2^{56}) 则会比 uint64 更高效。 | uint64 |
| sfixed32 | 定长 4 字节。 | int32 |
| sfixed64 | 定长 8 字节。 | int64 |
| bool | bool | |
| string | 包含 UTF-8 和 ASCII 编码的字符串,长度不能超过 (2^{32})。 | string |
| bytes | 可包含任意的字节序列但长度不能超过 (2^{32})。 | string |
[1] 变长编码是指:经过 protobuf 编码后,原本 4 字节或 8 字节的数可能会被变为其他字节数。
2.3 定义 .proto 文件
syntax = "proto3"; // 使用 proto3 语法
package example; // 生成 C++ 代码后,example 变成命名空间,防止命名冲突
// 默认情况protoc命令并不会针对service服务⽣成对应rpc代码
// 需要开启选项才会进⾏⽣成
option cc_generic_services = true;
// 枚举类型
enum PhoneType {
MOBILE = 0; // 枚举类型的编号必须从0开始
HOME = 1;
WORK = 2;
}
// 定义消息类型
message Student {
string name = 1; // 字段编号 1
int32 id = 2; // 字段编号 2
// 嵌套消息
message PhoneNumber {
string number = 1;
PhoneType type = 2;
}
repeated PhoneNumber phones = 3; // repeated 表示数组
map<string , float> scores = 4; // hash类型(键值对)
}
// RPC 远程调用的相关接口
message PersonRequest {
int32 id = 1;
}
message PersonResponse {
Student stu = 1;
}
// 定义一个 RPC 服务
service PersonService {
rpc getPerson(PersonRequest) returns (PersonResponse);
}
2.4 编译命令
protoc [--proto_path=IMPORT_PATH] --cpp_out=DST_DIR path/to/file.proto
-
protoc编译工具 -
--proto_path指定被编译的.proto文件所在的目录,可多次指定,可简写为-I -
IMPORT_PATH如果不指定该参数,则在当前目录搜索,当某个.proto文件import其他.proto文件或编译的.proto文件不在当前目录下,这时就需要用-I指定搜索目录 -
--cpp_out指编译后的⽂件为 C++ ⽂件 -
DST_DIR编译后⽣成⽂件的⽬标路径 -
path/to/file.proto要编译的.proto⽂件
2.5 编译后生成的结构
person.pb.h
namespace person {
enum PhoneType : int {
MOBILE = 0,
HOME = 1,
WORK = 2,
// 用于枚举类型的边界检查
PhoneType_INT_MIN_SENTINEL_DO_NOT_USE_ =
::std::numeric_limits<::int32_t>::min(),
PhoneType_INT_MAX_SENTINEL_DO_NOT_USE_ =
::std::numeric_limits<::int32_t>::max(),
};
// string name = 1;
void clear_name() ; // 清除 name 字段的值
const ::std::string& name() const; // 获取 name 字段的值(只读)
template <typename Arg_ = const ::std::string&, typename... Args_>
void set_name(Arg_&& arg, Args_... args); // 设置 name 字段的值(支持多种参数类型)
::std::string* PROTOBUF_NONNULL mutable_name(); // 获取 name 字段的可修改指针
// int32 id = 2;
void clear_id() ;
::int32_t id() const;
void set_id(::int32_t value);
// map<string, float> scores = 4;
int scores_size() const; // 获取 scores map 的元素个数
void clear_scores() ; // 清空 scores map 中的所有元素
// 获取 scores map 的只读引用
// 返回: 常量 map 引用,可用于遍历和查找
// 示例: for (const auto& pair : scores()) { ... }
// scores()["math"] = 99.8
const ::google::protobuf::Map<::std::string, float>& scores() const;
// 获取 scores map 的可修改指针
// 返回: 可修改的 map 指针,用于添加、删除或修改元素
// 示例: (*mutable_scores())["math"] = 95.5;
::google::protobuf::Map<::std::string, float>* PROTOBUF_NONNULL mutable_scores();
// repeated .person.Student.PhoneNumber phones = 3;
int phones_size() const; // 获取 phones 列表的元素个数
void clear_phones() ;
// 获取指定索引位置的 PhoneNumber 对象的可修改指针
// 参数: index - 索引位置(0 到 phones_size()-1)
// 返回: 可修改的 PhoneNumber 指针
// 示例: mutable_phones(0)->set_number("123456");
::person::Student_PhoneNumber* PROTOBUF_NONNULL mutable_phones(int index);
::google::protobuf::RepeatedPtrField<::person::Student_PhoneNumber>* PROTOBUF_NONNULL mutable_phones();
// 取指定索引位置的 PhoneNumber 对象的只读引用
const ::person::Student_PhoneNumber& phones(int index) const;
// 在 phones 列表末尾添加一个新的 PhoneNumber 对象
// 返回: 新添加的 PhoneNumber 对象的可修改指针
// 示例: add_phones()->set_number("987654");
::person::Student_PhoneNumber* PROTOBUF_NONNULL add_phones();
// 获取整个 phones 列表的只读引用
const ::google::protobuf::RepeatedPtrField<::person::Student_PhoneNumber>& phones() const;
class PersonService : public ::google::protobuf::Service {
public:
virtual void getPerson(::google::protobuf::RpcController* PROTOBUF_NULLABLE controller,
const ::person::PersonRequest* PROTOBUF_NONNULL request,
::person::PersonResponse* PROTOBUF_NONNULL response,
::google::protobuf::Closure* PROTOBUF_NULLABLE done);
}
class PersonService_Stub final : public PersonService {
public:
void getPerson(::google::protobuf::RpcController* PROTOBUF_NULLABLE controller,
const ::person::PersonRequest* PROTOBUF_NONNULL request,
::person::PersonResponse* PROTOBUF_NONNULL response,
::google::protobuf::Closure* PROTOBUF_NULLABLE done) override;
}
}
-
repeated作用于不同字段的类型时:-
基础类型(int32, string, float, bool 等):可以直接使用
add_字段名(value)添加元素 -
复合类型(message,enum):需要使用
add_字段名()返回指针,然后设置
-
2.6 序列化和反序列化
每个 protobuf message 编译后生成的类都继承自 google::protobuf::Message(或 MessageLite),提供了丰富的通用接口。
namespace google {
namespace protobuf {
class PROTOBUF_EXPORT Message : public MessageLite {}
class PROTOBUF_EXPORT MessageLite {
public:
// 序列化方法
bool SerializeToString(std::string* output) const; // 序列化到 string 中
bool SerializeToArray(void* data, int size) const; // 序列化到 C 风格数组中
bool SerializeToOstream(std::ostream* output) const; // 序列化到C++流中
std::string SerializeAsString() const; // 返回空 string 表⽰出错
// 反序列化方法
bool ParseFromString(const std::string& data); // 从 string 中反序列化
bool ParseFromArray(const void* data, int size); // 从 C 风格数组中反序列化
bool ParseFromIstream(std::istream* input); // 从 C++ 流中反序列化
}
}
}
2.7 基本使用
目录结构
|-test.cc
|-person.pb.cc
|-person.pb.h
|-person.proto
|-makefile
person.proto
syntax = "proto3";
package person;
option cc_generic_services = true;
enum PhoneType {
MOBILE = 0;
HOME = 1;
WORK = 2;
}
message Student {
string name = 1;
int32 id = 2;
message PhoneNumber {
string number = 1;
PhoneType type = 2;
}
repeated PhoneNumber phones = 3;
map<string , float> scores = 4;
}
message PersonRequest {
int32 id = 1;
}
message PersonResponse {
Student stu = 1;
}
service PersonService {
rpc getPerson(PersonRequest) returns (PersonResponse);
}
test.cc
#include "person.pb.h"
#include <iostream>
std::string serialize_test() {
person::Student stu;
stu.set_id(1001);
stu.set_name("zhangsan");
auto phone1 = stu.add_phones();
phone1->set_number("123456789");
phone1->set_type(person::PhoneType::HOME);
auto scores = stu.mutable_scores();
(*scores)["math"] = 95;
(*scores)["english"] = 85.5;
scores->insert({"chinese", 100});
std::string str = stu.SerializeAsString();
std::cout << str << std::endl;
return str;
}
void unserialize_test(const std::string& str) {
person::Student stu;
bool ret = stu.ParseFromString(str);
if (ret == false) {
std::cout << "unserialize failed" << std::endl;
return;
}
std::cout << stu.id() << std::endl;
std::cout << stu.name() << std::endl;
for (int i = 0; i < stu.phones().size(); ++i) {
std::cout << stu.phones(i).number() << " " << stu.phones(i).type() << std::endl;
}
auto it = stu.scores().find("math");
if (it != stu.scores().end()) {
std::cout << it->second << std::endl; // 数学成绩
}
for (auto it: stu.scores()) {
std::cout << it.first << " " << it.second << std::endl;
}
}
int main() {
std::cout << "序列化结果:" << std::endl;
std::string res = serialize_test();
std::cout << res << std::endl;
std::cout << "反序列化结果:" << std::endl;
unserialize_test(res);
return 0;
}
Makefile
main: test.cc person.pb.cc
g++ -std=c++17 -o $@ $^ -lprotobuf -lpthread
.PHONY:clean
clean:
rm -f test.cc
三、补充操作
3.1 map
protobuf 中 message 消息结构中⽀持 map 字段的定义,其字段类型对应的是 protobuf 中的Map类(注意并⾮C++STL中 的 map,虽然他们的基本操作都差不多,但是类型是不同的)。
namespace google {
namespace protobuf {
template <typename Key, typename T>
struct MapPair {
MapPair(const Key& other_first, const T& other_second)
};
template <typename Key, typename T>
class Map {
public:
using iterator = ....;
using const_iterator = ....;
iterator begin();
iterator end();
void clear();
size_type size()
bool empty()
iterator find(const TrivialKey& k)
std::pair<iterator, bool> insert(const KeyValuePair& kv)
iterator operator[](const TrivialKey& k)
void erase(iterator it)
};
}
}
3.2 Json 与 Protobuf 的相互转换
#include <google/protobuf/util/json_util.h>
// 核心转换函数
namespace google::protobuf::util {
class PROTOBUF_EXPORT Status {
public:
bool ok();
StringPiece message();
string ToString();
};
struct JsonParseOptions {
//解析过程中是否忽略未知的JSON字段
bool ignore_unknown_fields;
//是否在解析枚举类型时不区分⼤⼩写
bool case_insensitive_enum_parsing;
};
// JSON 字符串 -> Protobuf Message
Status JsonStringToMessage(
const std::string& json,
Message* message,
const JsonParseOptions& options = JsonParseOptions()
);
struct JsonPrintOptions {
//是否添加空⽩字符让json更易读
bool add_whitespace;
//是否总是输出基本类型的字段。默认值的基本类型字段在JSON输出中会被省略
bool always_print_primitive_fields;
//是否总是将枚举类型作为整数打印。默认情况下,枚举值会被渲染为字符串
bool always_print_enums_as_ints;
//是否保留 pb 字段名称,true则使⽤原始名⽽不是根据 JSON 命名约定进⾏转换
bool preserve_proto_field_names;
//默认均为false
};
// Protobuf Message -> JSON 字符串
Status MessageToJsonString(
const Message& message,
std::string* json,
const JsonPrintOptions& options = JsonPrintOptions()
);
}
案例
#include "person.pb.h"
#include <iostream>
#include <google/protobuf/util/json_util.h>
void protobufToJsonTest() {
person::Student stu;
stu.set_id(100);
stu.set_name("zhangsan");
auto phone1 = stu.add_phones();
phone1->set_number("123456789");
phone1->set_type(person::PhoneType::HOME);
auto scores = stu.mutable_scores();
(*scores)["math"] = 95;
(*scores)["english"] = 85.5;
scores->insert({"chinese", 100});
std::string json;
google::protobuf::util::JsonPrintOptions options;
options.add_whitespace = true;
google::protobuf::util::Status res = google::protobuf::util::MessageToJsonString(stu , &json , options);
if(!res.ok()) {
std::cout << "message to json failed: " << res.ToString() << std::endl;
return;
}
std::cout << json << std::endl;
}
void jsonToProtobufTest() {
std::string json = R"(
{
"name": "ccc",
"id": 10000,
"phones": [
{
"number": "13620775555",
"type": 1
}
],
"scores": {
"math": 98,
"chinese": 100
}
}
)";
person::Student stu;
google::protobuf::util::Status res = google::protobuf::util::JsonStringToMessage(json , &stu);
if(!res.ok()) {
std::cout << "json to protubuf failed: " << res.ToString() << std::endl;
return;
}
std::cout << stu.id() << std::endl;
std::cout << stu.name() << std::endl;
for (int i = 0; i < stu.phones().size(); ++i) {
std::cout << stu.phones(i).number() << " " << stu.phones(i).type() << std::endl;
}
for (auto it: stu.scores()) {
std::cout << it.first << " " << it.second << std::endl;
}
}
int main() {
std::cout << "protobufToJsonTest: " << std::endl;
protobufToJsonTest();
std::cout << "jsonToProtobufTest: " << std::endl;
jsonToProtobufTest();
return 0;
}
输出结果:
protobufToJsonTest:
{
"name": "zhangsan",
"id": 100,
"phones": [
{
"number": "123456789",
"type": "HOME"
}
],
"scores": {
"chinese": 100,
"math": 95,
"english": 85.5
}
}
jsonToProtobufTest:
10000
ccc
13620775555 1
math 98
chinese 100
更多推荐




所有评论(0)