1、什么是 protocol
protobuf(Protocol Buffers )是 Google 的开源项目,是 Google 的 中立于语言、平台 ,可扩展的用于 序列化结构化数据 的解决方案。
简单的说,protobuf 是用来对数据进行 序列化 和反序列化。支持的开发语言包括 C++、Java、Python、Objective-C、C#、JavaNano、JavaScript、Ruby、Go、PHP 等。
序列化 (Serialization):将数据结构或对象转换成二进制串的过程。
反序列化(Deserialization):将在序列化过程中所生成的二进制串转换成数据结构或者对象的过程。
2、数据为啥要序列化
存储或者传输数据时,需要将当前数据对象转换成字节流便于 网络传输或者存储。
当我们需要再次使用这些数据时,需要将接收到的或者读取的字节流进行 反序列化,重建我们的数据对象。
我们常使用的一个场景是:在分布式处理任务时,主服务器(master)会将每个子任务分发到每个子节点 (works) 进行单独计算,每个子节点的数据都是临时存储在其内存当中,计算完毕后需要 将数据传回 master 上,master 再进行综合处理(合并,数据持久化等操作)。
这个将数据传回 master 上,就需要将这些数据进行序列化,借助 protobuf,将数据转换成二进制串,传回 master 后,再进行反序列化,将 worker 传过来的数据转换成开发语言(如 C++)的数据结构,再将这些反序列化后数据在 master 上进行一些其他的操作。
3、怎么使用 protobuf
假设有以下数据,需要将其序列化:
struct Student
{
Student():m_age(0), m_gender(0), m_scores(0){}
Student(std::string name, int age, int gender, int scores):m_name(name),m_age(age),m_gender(gender), m_scores(scores){}
std::string m_name;
int m_age;
int m_gender; // 0 female; 1 male
int m_scores;
};
std::map<int, std::vector<Student> > work_students; // 班级 - 学生
需要将 work_students
这里面的数据序列化后传输到 master 上。
第一步:编写 proto 文件(studentResult.proto)
syntax="proto2"; // 指定使用 proto 语法版本
package StMessage; // 指定 package 名字,以防止不同项目名字冲突
message Student // 定义消息
{
required string name = 1; // 定义第一个字段 格式: 字段修饰符 数据类型 字段名 = 唯一标识符
required int32 age = 2;
required int32 gender = 3;
required int32 scores = 4;
}
message studentResult // 这个是核心消息,从这里开始
{
required int32 classid = 1;
repeated Student students = 2;
}
- syntax,支持 proto2 和 proto3,二者语法有差异,我们常使用 proto2,如有空,也可去了解下 proto3。
- package, .proto 文件以一个 package 声明开始。这个声明是为了防止不同项目之间的命名冲突。对应到 C++ 中去,你用这个.proto 文件生成的类将被放置在一个与 package 名相同的 命名空间 中。
- message,定义一个消息,一个消息就是某些类型的 字段 的集合,对应着 C++ 里面的数据。
字段:
修饰符
- required 必须提供字段值,否则对应的消息就会被认为是“未初始化的”。
- repeated:字段会重复 N 次(N 可以为 0)。重复的值的顺序将被保存在 protocol buffer 中。你只要将重复的字段视为动态大小的数组就可以了。
- optional:字段值指定与否都可以。如果没有指定一个 optional 的字段值,它就会使用默认值(0)。对简单类型来说,你可以指定你自己的默认值。
数据类型
- 基本数据类型
proto 文件消息类型 C++ 类型 说明 double double float float int32 int32 使用可变长编码方式,负数时不够高效,应该使用 sint32 int64 int64 同上 uint32 uint32 使用可变长编码方式 uint64 uint64 同上 bool bool string string 一个字符串必须是 utf-8 编码或者 7-bit 的 ascii 编码的文本 bytes string 可能包含任意顺序的字节数据 fixed32 uint32 总是 4 个字节,如果数值总是比 2^28 大的话,这个类型会比 uint32 高效 fixed64 uint64 总是 8 个字节,如果数值总是比 2^56 大的话,这个类型会比 uint64 高效 sfixed32 int32 总是 4 个字节 sfixed64 int64 总是 8 个字节 自定义数据类型
可以使用自定义的 message 作为另一个 message 的某些字段的数据类型。
例如上述在 studentResult message 中,使用了自定义的数据类型,这个数据类型其实就是我们在上面定义的一个 message Student。
唯一标识符
- 在每一项后面的、类似于“= 1”,“= 2”的标志指出了该字段在二进制编码中使用的唯一“标识(tag)”。标识号 1-15 编码所需的字节数比更大的标识号使用的字节数要少 1 个,所以,如果你想寻求优化,可以为经常使用或者重复的项采用 1~15 的标识(tag),其他经常使用的 optional 项采用≥16 的标识(tag)。在重复的字段中,每一项都要求重编码标识号(tag number),所以重复的字段特别适用于这种优化情况。
【注意】:字段名最好全用小写字母,不要使用大写字母,因为在编译后的 pb.h 文件中,字段名全部会转成小写,可能在 C++ 中使用错误,找不到这个名字。
第二步:将我们的核心 message 加到总的 proto 文件里
这里我们总的 proto 文件叫 attached_data.proto,与分 proto 文件在同级目录里。
syntax="proto2";
package protocol;
import "studentResult.proto"
message AttachedData
{
#...
optional StMessage.studentResult st_result = 6; // 格式:修饰符 package 名. 要使用的核心消息 字段名 = 标识符
}
第三步:编译 proto 文件(studentResult.proto)
在定义好 proto 文件后,需要将其编译,用于生成.pb.h 文件(proto 文件中自定义类的头文件)和 .pb.cc(proto 文件中自定义类的实现文件)。
这样我们才能在 C++ 中使用相关的接口,进行数据的处理。
在 studentResult.proto 的同级目录下,编写一个 shell 文件,用于编译 proto:
当然,我们现在已经有这个 shell 文件,就不用再去写了,直接用 sh 运行它即可。
如果没有这个 shell 文件,就需要我们去编写它
#!/bin/bash
export ORIG_LD_LIBRARY_PATH=$LD_LIBRARY_PATH
proto34_path='/home/proto3.4_env'
export LD_LIBRARY_PATH=$proto34_path/lib:$ORIG_LD_LIBRARY_PATH
$proto34_path/bin/protoc --cpp_out=./3.4/ *.proto #重要的是这一行
proto37_path='/home/proto3.7_env'
export LD_LIBRARY_PATH=$proto37_path/lib:$ORIG_LD_LIBRARY_PATH
$proto37_path/bin/protoc --cpp_out=./3.7/ *.proto #重要的是这一行
[protoc 程序] –cpp_out=[输出路径] [指定需要编译的 proto 文件]
编译完毕后,生成的 pb 文件可以在编译时指定的路径中找到,注意这个文件不要去编辑它。
4、编写序列化函数
准备好 pb 文件后,就可以在 C++ 中使用它了,序列化 C++ 数据:还是以 std::map<int, std::vector<Student> > class_students;
为例
写一个序列化函数:
#include "studentResult.pb.h" // 添加对应的 pb 头文件
namespace StMessage // 这个写在实现函数的头文件里。这里就不做分离了
{
class studentResult;
}
void StResultManager::serialize(StMessage::studentResult* st_result)
{
for(auto pit=work_students.begin(); pit!=work_students.end(); ++pit)
{
st_result->set_classid(pit->first);
const std::vector<Student>& students = pit->second;
for(size_t i=0; i<students.size(); ++i)
{
StMessage::Student* st = st_result->add_students();
st->set_name(students[i].m_name);
st->set_age(students[i].m_age);
st->set_gender(students[i].m_gender);
st->set_scores(students[i].m_scores);
}
}
}
StMessage::studentResult
StMessage 是 proto 里的 package 名(在 C++ 里就成了命名空间),studentResult 是 proto 里的核心 message 名,也就是最外层的那个 message。
在进行数据序列化的时候,添加数据有几种接口(函数):
set_开头的:对应 required 修饰符。
set__字段名()
add_开头的:对应 repeated 修饰符。
add__字段名()
mutable_开头的:对应 required 修饰符以及数据类型为自定义 message 类型的。还可对应 optional 修饰符的字段。
常用的就这三个,基本就能满足使用了。
这个函数在 work 上调用!
5、编写反序列化函数
反序列化函数在 master 上调用。
#include "studentResult.pb.h" // 添加对应的 pb 头文件
std::map<int, std::vector<Student> > master_datas; // 将反序列化的数据存储在这里
void StResultManager::deserialize(const StMessage::studentResult* st_result)
{
const int calssid = st_result->classid()
for(int i=0;i<st_result->students_size(); ++i)
{
const StMessage::Student& stt = st_result->students(i);
const Student& st = Student(stt.name(), stt.age(), stt.gender(), stt.scores());
master_datas[calssid].push_back(st);
}
}
这样就完成了数据的传输。
当然如有兴趣,可以阅读.pb.h 文件,看看从 proto 定义的 message 是怎么对应到 C++ 函数的。一般来讲,无需纠结这个。
7、参考
Protocol Buffers C++ 入门教程 - 腾讯云开发者社区 - 腾讯云 (tencent.com)
Proto3:C++ 基本使用 - 落雷 - 博客园 (cnblogs.com)
protobuf 使用系列详解三:补充四种 API 的使用:mutable_、ParseFrom*、set_allocated__谢白羽的博客 -CSDN 博客_mutable proto
protobuf 中的嵌套消息的使用 主要对 set_allocated_和 mutable_的使用 - DoubleLi - 博客园 (cnblogs.com)
欢迎各位看官及技术大佬前来交流指导呀,可以邮件至 jqiange@yeah.net