菜鸟笔记
提升您的技术认知

protobuf协议使用详解

一、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();
}