欢迎各位大佬光临本文章!!!
还请各位大佬提出宝贵的意见,如发现文章错误请联系冰冰,冰冰一定会虚心接受,及时改正。
本系列文章为冰冰学习编程的学习笔记,如果对您也有帮助,还请各位大佬、帅哥、美女点点支持,您的每一分关心都是我坚持的动力。
我的博客地址:bingbing~bang的博客_CSDN博客https://blog.csdn.net/bingbing_bang?type=blog
我的gitee:冰冰棒 (BingbingSuperEffort) - Gitee.comhttps://gitee.com/BingbingSuperEffort
前言
1.初识protobuf
1.1protobuf简介
1.2快速使用protobuf
2.protobuf的语法介绍
2.1 字段规则与消息类型的定义和使用
2.2 protoc命令选项
2.3 enum类型
2.4 Any类型
2.5 oneof类型
2.6 map类型
2.7 默认值
3.消息更新
3.1 更新规则
3.2 删除规则
3.3 未知字段
4.option选项
5.实战使用:网络通信录
序列化与反序列化是我们在通讯传输和文件保存时常用的手段。尤其是在网络传输协议中,字符串与各种格式之间的转换也需要这种手段。protobuf,json,xml都是常用的集成库,json我们已经了解过了,而protobuf作为Google公司的产品,其具备的优点更是数不胜数。下面我们一起了解一下protobuf。
protobuf是Google公司内部的混合语言数据标准,是一种轻便高效的结构化数据存储格式,可以用于序列化与反序列化。他与语言无关,平台无关,可用于即时通讯、数据存储等领域。相比于xml和json,protobuf更加轻便,体量更小,解析速度更快。
protobuf最常用的就是序列化与反序列化操作,那什么是序列化呢?序列化就是将对象转换为字节序列的过程,反序列化就是把字节序列恢复为对象的过程。在网络通信中,我们往往需要将传输的报文进行序列化操作,然后在发送给远端机器,远端接收到后,在通过反序列化操作将内容进行解析,这才真正得到发送的内容。
protobuf作为一种轻量化的序列化工具,他的扩展性以及兼容性更加灵活,我们可以更新数据结构而无需担心影响和破坏原有的旧程序。protobuf最重要的特点就是需要依赖通过编译生成的头文件和源文件来使用。
首先在使用protobuf之前我们需要先安装protobuf,大家可自行搜索查找安装流程,这里不再赘述。
我们需要创建一个后缀为.proto的文件,在该文件中进行protobuf的编写逻辑。文件命名应该使用全小写字母命名,多个字母之间使用下划线 “_” 进行连接。例如first_pro.proto。
(1)指定语法
进入文件后,我们需要指定protobuf的语法,我们常用的语法为proto3。proto3 简化了 Protocol Buffers 语⾔,既易于使⽤,⼜可以在更⼴泛的编程语⾔中使⽤。它允许你使⽤ Java,C++,Python 等多种语⾔⽣成 protocol buffer 代码。因此我们需要在首行添加如下代码:
syntax = "proto3";
(2)package声明符
package 是⼀个可选的声明符,能表示 .proto ⽂件的命名空间,在项⽬中要有唯⼀性。它的作⽤是为 了避免我们定义的消息出现冲突。在编译完成后,相当于C++中的命名空间。
(3)定义消息及消息字段
消息(message): 要定义的结构化对象,我们可以给这个结构化对象中定义其对应的属性内容。实际上就是编译生成后的class。.proto文件的定义消息的格式如下:
syntax = "proto3";
package conntacts;
//定义信息
message PeopleInfo
{
string name = 1;//姓名,=1 表示字段编号
int32 age = 2;//年龄
}
命名规范则采用驼峰法命名,首字母大写。
name和age是我们在message中定义的属性字段,字段定义格式为:字段类型 字段名称 = 字段唯一编号;
字段名称命名规范:全⼩写字⺟,多个字⺟之间⽤ _ 连接。
字段类型分为:标量数据类型和特殊类型(包括枚举、其他消息类型等)。
字段唯⼀编号:⽤来标识字段,⼀旦开始使⽤就不能够再改变。
实际上,属性字段就对应的C++语言的成员变量,只不过后面不再是定义的默认值,而是定义的编号,用来在生成的.h文件中进行标识这些成员。
标量数据类型与C++类型对应表:
.protoc 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 |
(4)编译命令
protoc -I path --cpp_out=DST_DIR path/to/file.proto
1.protoc 为编译命令
2.-I 指定被编译的.proto文件所在目录,可多次指定,当前指定为path
3.--cpp_out:表示编译生成C++文件
4.= 后面加生成文件的路径
执行编译命令后,会在指定的文件目录中出现.h和.cc的C++文件,而我们在.proto文件中定义的message将会生成对应的类,而类中的操作方法则定义在生成的.h文件和.cc文件中。
例如上文中定义的message,在生成的.h文件中就有如下方法:
而对于序列化和反序列化的代码,则位于MessageLite类中,该类为message的父类。
class MessageLite {
public:
//序列化:
bool SerializeToOstream(ostream* output) const; // 将序列化后数据写⼊⽂件流
bool SerializeToArray(void *data, int size) const;
bool SerializeToString(string* output) const;
//反序列化:
bool ParseFromIstream(istream* input); // 从流中读取数据,再进⾏反序列化动作
bool ParseFromArray(const void* data, int size);
bool ParseFromString(const string& data);
};
序列化的结果为二进制数据,并非文本数据。 并且序列化的 API 函数均为const成员函数,因为序列化不会改变类对象的内容,而是将序列化的结果保存到函数入参指定的地址中。
(5)序列化与反序列化的使用
下面的代码我们使用先前创建的message并且创建一个对象设置对应的信息,然后调用SerializeToString函数将其序列化为字符串。随后在将字符串内容调用ParseFromString函数反序列化出来。
#include
#include"contacts.pb.h"
#include
using namespace std;
int main()
{
string peostr;
{
conntacts::PeopleInfo people;
people.set_name("大兵");
people.set_age(24);
if(!people.SerializeToString(&peostr))
{
cout<<"序列化失败"<
(6)编译链接库
正如C++引入其他库一样,在使用.proto文件生成的.h里面的函数时,我们也需要连接protobuf提供的第三方库protobuf。这里要注意,在连接protobuf库时,一定要增加-std=c++11,因为protobuf中使用了部分c++11的语法。
g++ main.cc contacts.pb.cc -o TestProtoBuf -std=c++11 -lprotobuf
总结:protobuf的使用流程
(1)singular:消息中可以包含该字段零次或一次(不超过一次)。 Proto3语法中,字段默认使用该规则。
(2)repeated:消息中可以包含该字段任意多次(包括零次),其中重复值的顺序会被保留。可以理解为定义了一个数组。
注:Proto3语法支持嵌套定义message,支持多个message定义在同一个文件,不同message中的字段编号并不冲突。
当我们想引入其他定义的.proto文件时,需要使用import引入。
import “phone.proto” //引入其他的 .proto文件
syntax = "proto3";
package contacts2;
import "google/protobuf/any.proto";//引入其他文件
message PeopleInfo
{
string name = 1; //姓名,=1 表示字段编号
int32 age = 2; //年龄
message Phone
{
string number=1;
}
repeated Phone phione=3;//电话 实际为一个数组
}
protoc -h :查看所有选项
protoc –decode=contacts2.Contacts contacts.proto < contacts.bin
查看二进制文件的内容,将其转为我们认识的字符
contacts2.Contacts:contacs2命名空间下的Contacts结构体的输出内容
contacts.proto:该结构体存储在contacts.proto文件中
contacts.bin:输出的二进制内容的文件
枚举类型,使⽤驼峰命名法,⾸字⺟⼤写。里面的常量值为全大写多个字⺟之间⽤ _ 连接。
enum PhoneType {
MP = 0; // 移动电话
TEL = 1; // 固定电话
}
定义特点:
字段还可以声明为 Any 类型,可以理解为泛型类型。使用时可以在 Any 中存储任意消息类型。Any 类型的字段也可用 repeated 来修饰。
Any 类型是 google 已经帮我们定义好的类型,在安装 ProtoBuf 时,其中的 include 目录下查找所有google 已经定义好的 .proto 文件。
引入:import "google/protobuf/any.proto"; 使用
message Address{
string home_address =1;
string unit_address =2;
}
message PeopleInfo
{
string name = 1; //姓名,=1 表示字段编号
int32 age = 2; //年龄
message Phone
{
string number=1;
enum PhoneType
{
MP=0;//移动电话
TEL=1;//固定电话
}
PhoneType type =2;
}
repeated Phone phione=3;//电话
google.protobuf.Any data = 4;//添加any类型
}
在输入处进行any类型的绑定
contacts2::Address address;//定义Address类
cout<<"请输入联系人家庭地址:";
string homeadd;
getline(cin,homeadd);
address.set_home_address(homeadd);
cout<<"请输入联系人单位地址:";
string unitadd;
getline(cin,unitadd);
address.set_unit_address(unitadd);
google::protobuf::Any* data = pcont->mutable_data();
data->PackFrom(address);//将Any类型绑定为Address类型
在读取处进行数据读取
if(people.has_data()&&people.data().Is())
{
contacts2::Address addr;
people.data().UnpackTo(&addr);
if(!addr.home_address().empty())
{
cout<<"家庭地址:"<
has_data()方法为检测是否绑定了data类型,Is
如果消息中有很多可选字段, 并且将来同时只有一个字段会被设置, 那么就可以使用 oneof 加强这个行为,也能有节约内存的效果。
我们不能使用repeated,只会保留最后设置的内容,并且字段编号不能与其他字段重复。
//.proto文件中message新增oneof修饰的字段
oneof other_contact{
string qq=5;
string wechat=6;
}
//写入:
cout<<"请选择联系方式:1.qq 2.wechat";
int other_contact;
cin>>other_contact;
cin.ignore(256,'\n');
if(1==other_contact)
{
cout<<"请输入qq: ";
string qq;
getline(cin,qq);
pcont->set_qq(qq);
}
else if(2==other_contact)
{
cout<<"请输入wechat: ";
string wechat;
getline(cin,wechat);
pcont->set_wechat(wechat);
}
else{
cout<<"选择错误"<
语法支持创建一个关联映射字段,也就是可以使用 map 类型去声明字段类型,格式为:
map
map_field = N
- key_type 是除了 float 和 bytes 类型以外的任意标量类型。 value_type 可以是任意类型
- map 字段不可以用 repeated 修饰
- map 中存入的元素是无序的
//.proto文件中新增map
map remake = 7;
//写入map类型的备注
for(int i=1;;i++)
{
cout<<"请输入备注"<mutable_remake()->insert({remark_key,remark_value});
}
//读取类型为map的备注信息
if(!people.remake().empty())
{
cout<<"备注信息:"<first<<": "<second<
反序列化消息时,如果被反序列化的二进制序列中不包含某个字段,当我们反序列化对象中相应字段时,就会设置为该字段的默认值。
对于标量数据类型,proto3语法下没有生成has_方法。
对于消息字段、oneof字段和any字段 ,C++ 和 Java 语言中都有 has_ 方法来检测当前字段是否被设置。
各类型的默认值设置:
类型 | 默认值 |
字符串 | 空字符串 |
字节 | 空字节 |
布尔值 | false |
数值类型 | 0 |
枚举 | 第一个被定义的默认值,必须为0 |
消息字段 | 依赖于语言取值 |
repeated修饰后 | 空 |
如果现有的消息类型已经不再满⾜我们的需求,例如需要扩展⼀个字段,在不破坏任何现有代码的情况下更新消息类型非常简单。
我们只需要保证更新的字段的名称和编号不要和老字段冲突即可。
对于修改字段则具备以下规则:
fixed32 与 sfixed32 兼容, fixed64 与 sfixed64兼容。
enum 与 int32,uint32, int64 和 uint64 兼容(注意若值不匹配会被截断)。但要注意当反序列化消息时会根据语言采用不同的处理方案:例如,未识别的 proto3 枚举类型会被保存在消息中,但是当消息反序列化时如何表示是依赖于编程语言的。整型字段总是会保持其的值。
oneof: 将一个单独的值更改为新oneof 类型成员之一是安全和二进制兼容的。 若确定没有代码一次性设置多个值那么将多个字段移入一个新 oneof 类型也是可行的。 将任何字段移入已存在的 oneof 类型是不安全的。
对于删除字段,则具备下列规则:
不能直接删除字段。若是移除老字段,要保证不再使用移除字段的字段编号。正确的做法是保留字段编号 (reserved),以确保该编号将不能被重复使用。不建议直接删除或注释掉字段。
reserved关键字,保留字段编号。使用reserved 将指定字段的编号或名称设置为保留项 。当我们再使用这些编号或名称时,protocol buffer 的编译器将会警告这些编号或名称不可用。
reserved可以指定编号也可以指定名称。
reserved 2,10,100 to 200; //保留2,10,100到200的编号
reserved “age”;//保留“age”字段
未知字段:解析结构良好的 protocol buffer 已序列化数据中的未识别字段的表示方式。例如,当旧程序解析带有新字段的数据时,这些新字段就会成为旧程序的未知字段。
本来,proto3 在解析消息时总是会丢弃未知字段,但在 3.5 版本中重新引⼊了对未知字段的保留机制。所以在 3.5 或更高版本中,未知字段在反序列化时会被保留,同时也会包含在序列化的结果中。
例如,我们定义了一个client和一个server两个通信端,并且原本约定的字段类型为名字和年龄,但是后面我们将server端的年龄字段保留,并且新增了生日字段,而client并没有更改,此时我们按照之前的通信方式,client接收到的年龄字段将会是默认值,而生日字段将是未知字段。
如果我们想打印出来未知字段,就需要使用如下代码:
void PrintCon(c_contacts::PeopleContacts& con)
{
for(int i=0;iGetUnknownFields(people);
for(int j=0;j
.proto 文件中可以声明许多选项,使用option 标注。选项能影响 proto 编译器的某些处理方式。
选项的完整列表在google/protobuf/descriptor.proto中定义。选项分为文件级、消息级、字段级等等, 但并没有一种选项能作用于所有的类型。
optimize_for : 该选项为⽂件选项,可以设置 protoc 编译器的优化级别,分别为 SPEED 、 CODE_SIZE 、 LITE_RUNTIME 。受该选项影响,设置不同的优化级别,编译 .proto ⽂件后生成的代码内容不同。
SPEED : protoc 编译器将⽣成的代码是⾼度优化的,代码运⾏效率⾼,但是由此⽣成的代码 编译后会占⽤更多的空间。 SPEED 是默认选项。
CODE_SIZE : proto 编译器将⽣成最少的类,会占⽤更少的空间,是依赖基于反射的代码来 实现序列化、反序列化和各种其他操作。但和 SPEED 恰恰相反,它的代码运⾏效率较低。这种⽅式适合⽤在包含⼤量的.proto⽂件,但并不盲⽬追求速度的应⽤中。
LITE_RUNTIME : ⽣成的代码执⾏效率⾼,同时⽣成代码编译后的所占⽤的空间也是⾮常 少。这是以牺牲Protocol Buffer提供的反射功能为代价的,仅仅提供 encoding+序列化功能, 所以我们在链接 BP 库时仅需链接libprotobuf-lite,⽽⾮libprotobuf。这种模式通常⽤于资源 有限的平台,例如移动⼿机平台中。
option optimize_for = LITE_RUNTIME;
allow_alias : 允许将相同的常量值分配给不同的枚举常量,⽤来定义别名。该选项为枚举选项。 举个例⼦:
enum PhoneType {
option allow_alias = true;
MP = 0;
TEL = 1;
LANDLINE = 1; // 若不加 option allow_alias = true; 这⼀⾏会编译报错
}
代码仓库:https://gitee.com/BingbingSuperEffort/protobuf