一、ProtoBuf是什么
ProtoBuf是Google开发的一种轻量高效的结构化数据交换格式,和JSON、XML干的是同一件事------存数据、传数据,但它比JSON更小、更快、更简单。
它的玩法很特别:先用一种IDL语言(接口描述语言)定义数据结构,然后用编译器自动生成各种编程语言的代码,C++、Java、Go、Python通通支持。
ProtoBuf的用途
- 数据序列化:网络通信、数据存储,把结构体转成二进制字节流传输
- 跨语言RPC服务:gRPC就是基于ProtoBuf的,多语言服务之间通信全靠它
- 版本兼容:字段加加减减都不影响旧代码,向后兼容性拉满
安装
Ubuntu一条命令搞定:
bash
sudo apt install protobuf-compiler libprotobuf-dev
二、Proto文件语法
基本组成
ProtoBuf里有几个核心概念,我们逐个拆分理解一下:
- 消息(message):基本数据结构单元,就相当于C++里的结构体
- 字段:消息里的每个数据项,都有唯一的编号(标签号)和类型
- 枚举(enum):定义一组命名常量,和C++枚举差不多
- 服务(service):RPC场景下定义服务接口的
语法示例
先看一个完整的proto文件例子,名叫example.proto:
protobuf
syntax = "proto3"; // 指定语法版本,用proto3
package example; // 命名空间,生成C++代码后对应namespace
// 要生成service的RPC代码必须开这个选项
option cc_generic_services = true;
// 定义一个消息Person,就像C++的struct
message Person {
string name = 1; // 字符串类型,标签号1
int32 age = 2; // 32位整数,标签号2
string email = 3; // 字符串,标签号3
repeated string skills = 4; // repeated相当于数组,标签号4
map<string, float> scores = 5; // map类型,标签号5
}
// RPC请求消息
message HelloRequest {
string name = 1;
}
// RPC响应消息
message HelloReply {
string message = 1;
}
// 定义RPC服务
service Greeter {
rpc SayHello (HelloRequest) returns (HelloReply) {}
}
关键字拆解
- syntax = "proto3":指定用proto3语法,现在基本都用这个版本
- package example :包名,生成C++代码后就是
namespace example - message :定义消息结构体,每个字段后面的数字是标签号,序列化的时候靠这个编号识别字段,不是按顺序的哦
- repeated:可重复字段,就相当于数组/vector,可以有多个值
- map:键值对,就相当于C++的map
- service / rpc:定义RPC服务接口和方法
三、编译生成代码
写好proto文件后,用protoc编译器生成C++代码:
bash
protoc --cpp_out=./ example.proto
参数说明:
--cpp_out=./:生成C++代码,输出到当前目录-I或--proto_path:指定proto文件所在的路径
编译完会生成两个文件:
example.pb.h------ 头文件,类的声明example.pb.cc------ 源文件,类的实现
生成的代码长啥样
生成的Person类,每个字段都会对应生成get/set方法,我们来看看:
cpp
namespace example { // 和proto里的package对应
class Person : public ::google::protobuf::Message {
public:
// ===== 普通字符串字段name =====
void clear_name();
const std::string& name() const; // 获取值
void set_name(const std::string& value); // 设置值
std::string* mutable_name(); // 获取指针用来修改
// ===== 普通整数字段age =====
void clear_age();
int32_t age() const;
void set_age(int32_t value);
// ===== repeated数组字段skills =====
int skills_size() const; // 获取元素个数
void clear_skills();
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); // 追加元素(直接传值)
// ===== map字段scores =====
int scores_size() const;
void clear_scores();
const ::google::protobuf::Map<std::string, float>& scores() const;
::google::protobuf::Map<std::string, float>* mutable_scores();
};
}
是不是很有规律?每个字段都有clear_xxx()、xxx()(get)、set_xxx()(set)、mutable_xxx()(拿指针改)。
repeated字段多了xxx_size()、add_xxx();map字段用mutable_xxx()拿指针后操作,和STL的map用法差不多~
四、核心接口介绍
1. Message基类 ------ 序列化反序列化
所有生成的消息类都继承自MessageLite,自带序列化反序列化方法:
cpp
// 反序列化:从字符串解析
bool ParseFromString(const std::string& data);
// 反序列化:从内存块解析
bool ParseFromArray(const void* data, int size);
// 序列化:写入字符串
bool SerializeToString(std::string* output) const;
// 序列化:直接返回字符串
std::string SerializeAsString() const;
// 序列化:写入内存块
bool SerializeToArray(void* data, int size) const;
2. Map类
ProtoBuf的map不是STL的map,但用法几乎一模一样:
cpp
iterator begin();
iterator end();
size_type size();
bool empty();
void clear();
iterator find(const Key& k);
insert(键值对);
operator[](key);
erase(iterator);
3. JSON转换工具
ProtoBuf还自带和JSON互转的工具,调试的时候超好用!
cpp
// Proto转JSON
util::Status MessageToJsonString(const Message& message, std::string* output);
// JSON转Proto
util::Status JsonStringToMessage(StringPiece input, Message* message);
还有配置选项可以控制格式,比如要不要加缩进、要不要输出默认值等等~
五、序列化 & 反序列化
目录结构
|-- makefile
|-- example.proto
`-- example.cc
1. proto文件 example.proto
protobuf
syntax = "proto3";
package example;
option cc_generic_services = true;
// 定义Person消息
message Person {
string name = 1;
int32 age = 2;
string email = 3;
repeated string skills = 4; // 数组
map<string, float> scores = 5; // 映射表
}
2. 测试代码 example.cc
cpp
#include <iostream>
#include "example.pb.h"
int main(int argc, char* argv[])
{
// ========== 第一步:构造数据 ==========
example::Person person;
// 普通字段直接set
person.set_name("Jack");
person.set_age(28);
person.set_email("qq.com");
// repeated数组字段用add追加
person.add_skills("C++");
person.add_skills("Java");
person.add_skills("Python");
// map字段要先拿mutable指针再操作
auto score_map = person.mutable_scores();
score_map->insert({"Ch", 88});
score_map->insert({"Mt", 77});
score_map->insert(google::protobuf::MapPair<std::string, float>("En", 99));
// ========== 第二步:序列化 ==========
std::string pstr = person.SerializeAsString();
if (pstr.empty()) {
std::cout << "SerializeAsString Failed!\n";
return -1;
}
std::cout << "序列化成功,数据长度: " << pstr.size() << " 字节\n" << std::endl;
// ========== 第三步:反序列化 ==========
example::Person tmp;
bool ret = tmp.ParseFromString(pstr);
if (ret == false) {
std::cout << "ParseFromString Failed!\n";
return -1;
}
// ========== 第四步:读取验证 ==========
// 普通字段直接get
std::cout << "name: " << tmp.name() << std::endl;
std::cout << "age: " << tmp.age() << std::endl;
std::cout << "email: " << tmp.email() << std::endl;
// repeated字段:先拿size,再按下标遍历
int sz = tmp.skills_size();
for (int i = 0; i < sz; i++) {
std::cout << "skills: " << tmp.skills(i) << std::endl;
}
// map字段:迭代器遍历,和STL一样
auto beg = tmp.scores().begin();
auto end = tmp.scores().end();
for (; beg != end; ++beg) {
std::cout << beg->first << " = " << beg->second << std::endl;
}
// map查找操作
auto it = tmp.scores().find("Ch");
if (it != tmp.scores().end()) {
std::cout << "Ch: " << it->second << std::endl;
}
return 0;
}
3. Makefile
makefile
all: example
example: example.cc example.pb.cc
g++ -std=c++17 $^ -o $@ -lprotobuf
# 自动编译proto文件生成pb.h和pb.cc
example.pb.cc : example.proto
protoc --cpp_out=./ $^
运行结果
name: Jack
age: 28
email: qq.com
skills: C++
skills: Java
skills: Python
En = 99
Ch = 88
Mt = 77
Ch: 88
六、Proto与JSON互相转换
调试的时候二进制看不清内容?ProtoBuf自带JSON转换功能,直接转成JSON字符串打印出来,一目了然!
代码示例
cpp
#include <iostream>
#include <google/protobuf/util/json_util.h>
#include "example.pb.h"
// Proto消息 转 JSON字符串
std::string p2j(google::protobuf::Message &msg) {
google::protobuf::util::JsonOptions opts;
opts.add_whitespace = true; // 加空白字符,格式化输出更好看
std::string json_str;
auto ret = google::protobuf::util::MessageToJsonString(msg, &json_str);
if (ret.ok() == false) {
std::cout << ret.ToString() << std::endl;
return "";
}
std::cout << json_str << std::endl;
return json_str;
}
// JSON字符串 转 Proto消息
void j2p(const std::string &json_str) {
example::Person person;
auto ret = google::protobuf::util::JsonStringToMessage(json_str, &person);
if (ret.ok() == false) {
std::cout << ret.ToString() << std::endl;
return;
}
// 读取数据
if (person.has_name())
std::cout << "person.name:" << person.name() << std::endl;
if (person.has_age())
std::cout << "person.age:" << person.age() << std::endl;
if (person.has_email())
std::cout << "person.email:" << person.email() << std::endl;
int sz = person.skills_size();
for (int i = 0; i < sz; i++)
std::cout << "person.skills:" << person.skills(i) << std::endl;
for (const auto &f : person.scores())
std::cout << f.first << " = " << f.second << std::endl;
}
int main(int argc, char* argv[])
{
example::Person person;
person.set_name("Jack");
person.set_age(0);
person.set_email("qq.com");
person.add_skills("C++");
person.add_skills("Java");
person.add_skills("Python");
auto score_map = person.mutable_scores();
score_map->insert({"Ch", 88});
score_map->insert({"Mt", 77});
score_map->insert({"En", 99});
std::cout << "---------- Proto转JSON ----------\n";
std::string str = p2j(person);
std::cout << "\n---------- JSON转Proto ----------\n";
j2p(str);
return 0;
}
注意:要用has_xxx()判断字段存不存在,proto3里需要给字段加optional关键字才会生成has方法,编译的时候还要加--experimental_allow_proto3_optional参数哦~
运行结果
---------- Proto转JSON ----------
{
"name": "Jack",
"age": 0,
"email": "qq.com",
"skills": [
"C++",
"Java",
"Python"
],
"scores": {
"En": 99,
"Ch": 88,
"Mt": 77
}
}
---------- JSON转Proto ----------
person.name:Jack
person.age:0
person.email:qq.com
person.skills:C++
person.skills:Java
person.skills:Python
Ch = 88
En = 99
Mt = 77
总结
ProtoBuf核心知识点梳理一下:
四步走流程:
- 写
.proto文件,用IDL语法定义消息结构 - 用
protoc编译器生成C++代码(.pb.h和.pb.cc) - 代码里用生成的类构造数据,
SerializeAsString()序列化 - 接收端
ParseFromString()反序列化,读取数据
字段操作规律:
- 普通字段:
set_xxx()设置,xxx()读取 - repeated数组:
add_xxx()追加,xxx_size()长度,xxx(i)按下标取 - map映射:
mutable_xxx()拿指针,然后和STL map一样用 - 想修改就用
mutable_xxx()拿指针,想只读就直接xxx()
常用API:
- 序列化:
SerializeAsString()/SerializeToString() - 反序列化:
ParseFromString()/ParseFromArray() - JSON互转:
MessageToJsonString()/JsonStringToMessage()