一、protobuf协议详解
在protobuf中,协议是由一系列的消息(message)
组成的,如下所示:
systax = "proto3"; //表明使用proto3语法;如果你没有指定这个,编译器会使用proto2语法;这个指定语法行必须是文件的非空非注释的第一个行
package School; //包名,类似于模块
message Student { //消息,类似于类
required string name = 1 [default="张三"];
optional int32 chinese = 2 [default=0];
optional int32 math = 3 [default=0];
optional int32 english = 4 [default=0];
}
message Teacher {
required strint name = 1;
optional string class = 2;
optional string object = 3;
}
message HengShuiZhongXue {
repeated Student student = 1; //message内可以嵌套message
repeated Teacher teachar = 2;
}
字段格式:
限定修饰符① | 数据类型② | 字段名称③ = 字段编码值④ | 字段默认值⑤
①. 限定修饰符 required
| optional
| repeated
required:表示是一个必须字段
,必须相对于发送方,在发送消息之前必须设置该字段的值,对于接收方,必须能够识别该字段的意思。发送之前没有设置required字段或者无法识别required字段都会引发编解码异常,导致消息被丢弃。
optional:表示是一个可选字段
,可选对于发送方,在发送消息时,可以有选择性的设置或者不设置该字段的值。对于接收方,如果能够识别可选字段就进行相应的处理,如果无法识别,则忽略该字段,消息中的其它字段正常处理。
repeated:表示该字段可以包含0~n个元素
, 其特性和optional一样,但是每一次可以包含多个值,可以看作是一个数组
。
②. 数据类型
Protobuf定义了一套基本数据类型,几乎都可以映射到C++\Java等语言的基础数据类型。
protobuf 数据结构 | 描述 | 打包 | C++语言映射 |
---|---|---|---|
bool | 布尔类型 | 1字节 | bool |
double | 64浮点数 | N | double |
float | 32浮点数 | N | float |
int32 | 32位整数 | N | int |
uint32 | 无符号32位整数 | N | unsigned int |
int64 | 64位整数 | N | __int64 |
uint64 | 64位无整数 | N | unsigned __int64 |
sint32 | 32位整数,处理负数效率更高 | N | int32 |
sint64 | 64位整数,处理负数效率更高 | N | __int64 |
fixed32 | 32位无符号整数 | 4 | unsigned int32 |
fixed64 | 64位无符号整数 | 8 | unsigned __int64 |
sfixed32 | 32位整数,能以更高的效率处理负数 | 4 | unsigned int32 |
sfixed64 | 64位整数 | 8 | unsigned __int64 |
string | 只能处理ASCII字符 | N | std::string |
bytes | 用于处理多字节的语言字符,如中文 | N | std::string |
enum | 可以包含一个用户自定义的枚举类型uint32 | N(uint32) | enum |
message | 可以包含一个用户自定义的消息类型 | N | object of class |
N:表示打包的字节并不是固定的,而是根据数据的大小或者长度决定的
③. 字段名称
字段名称的命名与C、C++、Java等语言的变量命名方式几乎是相同的:字母、数字和下划线组成
protobuf建议字段的命名采用以下划线分割的驼峰式,例如:first_name 而不是firstName
④. 字段编码值
有了该值,通信双方才能互相识别对方的字段。当然相同的编码值,其限定修饰符和数据类型必须相同,编码值的取值范围为1~2^32
其中1~
15的编码时间和空间效率都是最高的,编码值越大,其编码的时间和空间效率就越低(相对于1~
15), protobuf 建议把经常要传递的值把其字段编码设置为1-15之间的值。
消息中的字段的编码值无需连续,只要是合法的,并且不能在同一个消息中有字段包含相同的编码值。
建议:项目投入运营以后涉及到版本升级时的新增消息字段全部使用optional或者repeated,尽量不实用required。如果使用了required,需要全网统一升级,如果使用optional或者repeated可以平滑升级。
⑤. 字段默认值
当在传递数据时,对于required数据类型,如果用户没有设置值,则使用默认值传递到对端。 对于optional字段,如果没有接收到optional字段,则设置为默认值。
对于strings,默认是一个空string
对于bytes,默认是一个空的bytes
对于bools,默认是false
对于数值类型,默认是0
二、使用message
//Demo.proto 协议格式文件
syntax='proto3'
package=Demo
message Data {
optional int32 x = 1;
optional string str = 2;
repeated int32 d = 3;
}
2.1、类成员变量的访问
- 获取成员变量:直接采用使用成员变量名(全部为小写);
- 设置成员变量:使用成员变量名前加
set_
的方法
//使用message
#include <Demo.pb.h>
#include <QDebug>
Demo::Data data;
data.set_x(20); //设置成员变量
qDebug()<<data.x(); //获取成员变量
对于普通成员变量(required和optional)
- 提供
has_
方法判断变量值是否被设置; - 提供
clear_
方法清除设置的变量值 ;
//使用message
#include <Demo.pb.h>
#include <QDebug>
Demo::Data data;
data.set_x(20); //设置成员变量
qDebug()<<data.has_x(); //判断变量值是否被设置
data.clear_x(); //清除x设置的变量值
对于string类型
- 提供了多种
set_
方法,其参数不同; - 提供了一个
mutable_
方法,返回变量值的可修改指针 ;
//使用message
#include <Demo.pb.h>
#include <QDebug>
Demo::Data data;
data.set_str(20); //设置成员变量
std::string* mutable_str(); //返回str变量值的可修改指针
对于repeated变量
_size
方法:返回变量的长度;- 通过下脚标访问其中的数据成员组;
- 通过下脚标返回其中的成员的
mutable_
的方法 _add
方法:增加一个成员
//使用message
#include <Demo.pb.h>
#include <QDebug>
Demo::Data data;
for(int i=0; i<10; i++)
{
data.d_add(i); //向d中添加成员
}
for(int i=0; i<data.d_size(); i++)
printf("%d\t",data.d(i)); //通过下脚标访问数据成员组
三、序列化和反序列化
3.1、序列化和反序列化有什么用?
序列化和反序列化主要用在保存数据结构上,保存数据很简单,各种形式都可以,例如txt,但是如果想把数据恢复成原先的数据结构就没那么简单了。
例如:下面是一个学生的结构体
struct student {
QString name;
QString class;
long int stu_id;
float Chinese;
float Math;
float English;
}
假如把三年一班的学生记录为一个结构体数组struct student Class_3_1[max_length];
;
把它存为.txt文件
,如果想把它恢复成struct student Class_3_1[max_length];
就得自己解析文件,特别麻烦。
如果采用序列化可以把数据结构序列化为二进制数据进行存储,反序列化可以把存储的二进制数据再次恢复成之前的数据结构,很方便使用。
3.2、序列化
//文件后缀可以自定
fileName = QFileDialog::getSaveFileName(0, QObject::tr("protobuf序列化"),currentPath,QObject::tr("TestData(*.TD)"));
if (!fileName.isEmpty())
{
if (!fileName.endsWith(".TD"))
{
fileName += ".TD";
}
QFile file(fileName);
if(file.open(QIODevice::WriteOnly))
{
// data 是一个 Demo::Data message对象
int nLength = data->ByteSize();
char* pbuf = new char[nLength];
data->SerializePartialToArray(pbuf,nLength); //序列化
if(nLength == file.write(pbuf,nLength))
{
qDebug()<<"SAVE_SUCESS";
}
else
{
qDebug()<<"SAVE_FAIL";
}
}
file.close();
}
3.3、反序列化
fileName = QFileDialog::getOpenFileName(0, QObject::tr("读取参考曲线数据 "),sCurPath,QObject::tr("ReferenceLine(*.RL)"));
Demo::Data data_1;
if (!fileName.isEmpty())
{
QFile file(fileName);
if(file.open(QIODevice::ReadOnly))
{
QByteArray array_para = file.readAll();
int nLen = array_para.length();
// data 是一个 Demo::Data message对象
if(!data_1->ParsePartialFromArray(array_para.data(),nLen)) //反序列化
{
qDebug()<<"LOAD_FAIL";
}
else
{
qDebug()<<"LOAD_SUCCESS";
int X = data_1->x();
QString STR = QString::fromUtf8(data_1->str().data());
QVector<int> D;
for(int i=0; i<data_1->d_size(); i++)
{
D.push_back(data_1->d(i));
}
}
}
file.close();
}