欢迎各位大佬光临本文章!!!
还请各位大佬提出宝贵的意见,如发现文章错误请联系冰冰,冰冰一定会虚心接受,及时改正。
本系列文章为冰冰学习编程的学习笔记,如果对您也有帮助,还请各位大佬、帅哥、美女点点支持,您的每一分关心都是我坚持的动力。
我的博客地址: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。
1.初识protobuf
1.1protobuf简介
protobuf是Google公司内部的混合语言数据标准,是一种轻便高效的结构化数据存储格式,可以用于序列化与反序列化。他与语言无关,平台无关,可用于即时通讯、数据存储等领域。相比于xml和json,protobuf更加轻便,体量更小,解析速度更快。
protobuf最常用的就是序列化与反序列化操作,那什么是序列化呢?序列化就是将对象转换为字节序列的过程,反序列化就是把字节序列恢复为对象的过程。在网络通信中,我们往往需要将传输的报文进行序列化操作,然后在发送给远端机器,远端接收到后,在通过反序列化操作将内容进行解析,这才真正得到发送的内容。
protobuf作为一种轻量化的序列化工具,他的扩展性以及兼容性更加灵活,我们可以更新数据结构而无需担心影响和破坏原有的旧程序。protobuf最重要的特点就是需要依赖通过编译生成的头文件和源文件来使用。
1.2快速使用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<iostream>
#include"contacts.pb.h"
#include<string>
using namespace std;
int main()
{
string peostr;
{
conntacts::PeopleInfo people;
people.set_name("大兵");
people.set_age(24);
if(!people.SerializeToString(&peostr))
{
cout<<"序列化失败"<<endl;
return 1;
}
cout<<"序列化成功"<<endl;
cout<<"序列化结果:"<<peostr<<endl;
}
{
conntacts::PeopleInfo people;
if(!people.ParseFromString(peostr))
{
cout<<"反序列化失败"<<endl;
return 1;
}
cout<<"反序列化成功"<<endl;
cout<<"反序列化结果:"<<endl
<<"name:"<<people.name()<<endl
<<"age:"<<people.age()<<endl;
}
return 0;
}
(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的使用流程
- 编写 .proto ⽂件,⽬的是为了定义结构对象(message)及属性内容
- 使⽤ protoc 编译器编译 .proto ⽂件,⽣成⼀系列接⼝代码,存放在新⽣成头⽂件和源⽂件中
- 依赖⽣成的接⼝,将编译⽣成的头⽂件包含进我们的代码中,实现对 .proto ⽂件中定义的字段进⾏ 设置和获取,和对 message 对象进⾏序列化和反序列化。
2.protobuf的语法介绍
2.1字段规则与消息类型的定义和使用
(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;//电话 实际为一个数组
}
2.2 protoc命令选项
protoc -h :查看所有选项
protoc –decode=contacts2.Contacts contacts.proto < contacts.bin
查看二进制文件的内容,将其转为我们认识的字符
contacts2.Contacts:contacs2命名空间下的Contacts结构体的输出内容
contacts.proto:该结构体存储在contacts.proto文件中
contacts.bin:输出的二进制内容的文件
2.3 enum类型
枚举类型,使⽤驼峰命名法,⾸字⺟⼤写。里面的常量值为全大写多个字⺟之间⽤ _ 连接。
enum PhoneType {
MP = 0; // 移动电话
TEL = 1; // 固定电话
}
定义特点:
- (1)0 值常量必须存在,且要作为第一个元素。这是为了与 proto2 的语义兼容:第⼀个元素作为默认值,且值为 0
- (2)枚举类型可以在消息外定义,也可以在消息体内定义(嵌套)
- (3)枚举的常量值在 32 位整数的范围内。但因负值无效因而不建议使用(与编码规则有关)。
- (4)将两个 ‘具有相同枚举值名称’ 的枚举类型放在单个 .proto 文件下测试时,编译后会报错
- (5)同级(同层)的枚举类型,各个枚举类型中的常量不能重名
- (6)单个 .proto ⽂件下,最外层枚举类型和嵌套枚举类型,不算同级
- (7)多个 .proto ⽂件下,若一个文件引入了其他文件,且每个文件都未声明 package,每个 proto 文件中的枚举类型都在最外层,算同级
- (8)多个 .proto ⽂件下,若⼀个文件引入了其他文件,且每个文件都声明了 package,不算同级。
2.4 Any类型
字段还可以声明为 Any 类型,可以理解为泛型类型。使用时可以在 Any 中存储任意消息类型。Any 类型的字段也可用 repeated 来修饰。
Any 类型是 google 已经帮我们定义好的类型,在安装 ProtoBuf 时,其中的 include 目录下查找所有google 已经定义好的 .proto 文件。
引入:import "google/protobuf/any.proto"; 使用
- 使用PackFrom() 方法可以将任意消息类型转为 Any 类型
- 使用 UnpackTo() 方法可以将 Any 类型转回之前设置的任意消息类型
- 使用Is()方法可以用来判断存放的消息类型是否为typename T
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>())
{
contacts2::Address addr;
people.data().UnpackTo(&addr);
if(!addr.home_address().empty())
{
cout<<"家庭地址:"<<addr.home_address()<<endl;
}
if(!addr.unit_address().empty())
{
cout<<"单位地址:"<<addr.unit_address()<<endl;
}
}
has_data()方法为检测是否绑定了data类型,Is<Type>()方法为检测绑定的类型是否为Type。
2.5 oneof类型
如果消息中有很多可选字段, 并且将来同时只有一个字段会被设置, 那么就可以使用 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<<"选择错误"<<endl;
}
//读取:
switch (people.other_contact_case())
{
case contacts2::PeopleInfo::OtherContactCase::kQq:
cout<<"qq号码:"<<people.qq()<<endl;
break;
case contacts2::PeopleInfo::OtherContactCase::kWechat:
cout<<"wechat号码:"<<people.wechat()<<endl;
break;
default:
break;
}
2.6 map类型
语法支持创建一个关联映射字段,也就是可以使用 map 类型去声明字段类型,格式为:
map<key_type, value_type> map_field = N
- key_type 是除了 float 和 bytes 类型以外的任意标量类型。 value_type 可以是任意类型
- map 字段不可以用 repeated 修饰
- map 中存入的元素是无序的
//.proto文件中新增map
map<string,string> remake = 7;
//写入map类型的备注
for(int i=1;;i++)
{
cout<<"请输入备注"<<i<<"标签:";
string remark_key;
getline(cin,remark_key);
if(remark_key.empty())
{
break;
}
cout<<"请输入备注"<<i<<"内容:";
string remark_value;
getline(cin,remark_value);
pcont->mutable_remake()->insert({remark_key,remark_value});
}
//读取类型为map的备注信息
if(!people.remake().empty())
{
cout<<"备注信息:"<<endl;
}
for(auto it = people.remake().cbegin();it!=people.remake().cend();it++)
{
cout<<" "<<it->first<<": "<<it->second<<endl;
}
2.7 默认值
反序列化消息时,如果被反序列化的二进制序列中不包含某个字段,当我们反序列化对象中相应字段时,就会设置为该字段的默认值。
对于标量数据类型,proto3语法下没有生成has_方法。
对于消息字段、oneof字段和any字段 ,C++ 和 Java 语言中都有 has_ 方法来检测当前字段是否被设置。
各类型的默认值设置:
类型 | 默认值 |
字符串 | 空字符串 |
字节 | 空字节 |
布尔值 | false |
数值类型 | 0 |
枚举 | 第一个被定义的默认值,必须为0 |
消息字段 | 依赖于语言取值 |
repeated修饰后 | 空 |
3.消息更新
3.1 更新规则
如果现有的消息类型已经不再满⾜我们的需求,例如需要扩展⼀个字段,在不破坏任何现有代码的情况下更新消息类型非常简单。
我们只需要保证更新的字段的名称和编号不要和老字段冲突即可。
对于修改字段则具备以下规则:
- 禁止修改任何已有字段的字段编号。
- int32, uint32, int64, uint64 和 bool 是完全兼容的。可以从这些类型中的一个改为另一个, 而不破坏前后兼容性。若解析出来的数值与相应的类型不匹配,会采用与 C++ 一致的处理方案 (例如,若将 64 位整数当做 32 位进行读取,它将被截断为 32 位)。
- sint32 和 sint64 相互兼容但不与其他的整型兼容。
- string 和 bytes 在合法 UTF-8 字节前提下也是兼容的。
- bytes 包含消息编码版本的情况下,嵌套消息与 bytes 也是兼容的。
-
fixed32 与 sfixed32 兼容, fixed64 与 sfixed64兼容。
-
enum 与 int32,uint32, int64 和 uint64 兼容(注意若值不匹配会被截断)。但要注意当反序列化消息时会根据语言采用不同的处理方案:例如,未识别的 proto3 枚举类型会被保存在消息中,但是当消息反序列化时如何表示是依赖于编程语言的。整型字段总是会保持其的值。
-
oneof: 将一个单独的值更改为新oneof 类型成员之一是安全和二进制兼容的。 若确定没有代码一次性设置多个值那么将多个字段移入一个新 oneof 类型也是可行的。 将任何字段移入已存在的 oneof 类型是不安全的。
3.2 删除规则
对于删除字段,则具备下列规则:
不能直接删除字段。若是移除老字段,要保证不再使用移除字段的字段编号。正确的做法是保留字段编号 (reserved),以确保该编号将不能被重复使用。不建议直接删除或注释掉字段。
reserved关键字,保留字段编号。使用reserved 将指定字段的编号或名称设置为保留项 。当我们再使用这些编号或名称时,protocol buffer 的编译器将会警告这些编号或名称不可用。
reserved可以指定编号也可以指定名称。
reserved 2,10,100 to 200; //保留2,10,100到200的编号
reserved “age”;//保留“age”字段
3.3 未知字段
未知字段:解析结构良好的 protocol buffer 已序列化数据中的未识别字段的表示方式。例如,当旧程序解析带有新字段的数据时,这些新字段就会成为旧程序的未知字段。
本来,proto3 在解析消息时总是会丢弃未知字段,但在 3.5 版本中重新引⼊了对未知字段的保留机制。所以在 3.5 或更高版本中,未知字段在反序列化时会被保留,同时也会包含在序列化的结果中。
例如,我们定义了一个client和一个server两个通信端,并且原本约定的字段类型为名字和年龄,但是后面我们将server端的年龄字段保留,并且新增了生日字段,而client并没有更改,此时我们按照之前的通信方式,client接收到的年龄字段将会是默认值,而生日字段将是未知字段。
如果我们想打印出来未知字段,就需要使用如下代码:
void PrintCon(c_contacts::PeopleContacts& con)
{
for(int i=0;i<con.cont_size();i++)
{
cout<<"-------------联系人"<<i+1<<"--------------------"<<endl;
const c_contacts::PeopleInfo& people = con.cont(i);
cout<<"姓名:"<<people.name()<<endl;
cout<<"年龄:"<<people.age()<<endl;
for(int j=0;j<people.phione_size();j++)
{
const c_contacts::PeopleInfo_Phone& phone = people.phione(j);
cout<<"电话"<<j+1<<":"<<phone.number();
cout<<" ("<<phone.PhoneType_Name(phone.type())<<")"<<endl;
}
//获取未知字段
const Reflection* reflection = PeopleInfo::GetReflection();
const UnknownFieldSet& unknowSet = reflection->GetUnknownFields(people);
for(int j=0;j<unknowSet.field_count();j++)
{
const UnknownField& fild = unknowSet.field(j);
cout<<"未知字段"<<j+1<<":"<<"字段编号:"<<fild.number()<<"类型:"<<fild.type();
switch(fild.type())
{
case UnknownField::Type::TYPE_VARINT:
cout<<" 值:"<<fild.varint()<<endl;
break;
case UnknownField::Type::TYPE_LENGTH_DELIMITED:
cout<<" 值:"<<fild.length_delimited()<<endl;
break;
}
}
}
}
4.option选项
.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; 这⼀⾏会编译报错
}
5.实战使用:网络通信录
代码仓库:https://gitee.com/BingbingSuperEffort/protobuf