需要云服务器等云产品来学习Linux可以移步/-->腾讯云<--/官网,轻量型云服务器低至112元/年,新用户首次下单享超低折扣。
目录
一、protobuf的简介
1、protobuf的介绍
2、序列化和反序列化
3、序列化使用的场景
二、protobuf的安装
1、在windows中安装protobuf
2、在Linux中安装protobuf
三、protobuf写法
1、contacts.proto
2、编译命令
3、标量数据类型
4、序列化/反序列化的使用
5、字段规则
6、序列化/反序列化到文件
8、也可以使用--decode命令查看二进制文件。没必要写read.cc
9、枚举类型
10、Any类型
11、oneof类型
12、map类型
13、默认值
14、更新规则
15、常用选项
16、总体代码
一、protobuf的简介
1、protobuf的介绍
ProtoBuf(Protocol Buffers)是一种轻量级的数据序列化协议,由Google开发。它可以将结构化数据序列化为二进制格式,以便在网络上进行传输或存储。ProtoBuf具有高效(比xml更小、更快、更简单)、可扩展(不破坏旧程序的基础上,更新数据结构)、跨平台、支持多种语言(C++、Java、Python)等特点,被广泛应用于分布式系统、数据存储、通信协议等领域。
2、序列化和反序列化
序列化:把对象转换为字节序列的过程 称为对象的序列化。
反序列化:把字节序列恢复为对象的过程 称为对象的反序列化。
常见的序列化和反序列化工具是json、protobuf、xml
3、序列化使用的场景
存储数据:当你想把的内存中的对象状态保存到⼀个⽂件中或者存到数据库中时。
⽹络传输:⽹络直接传输数据,但是⽆法直接传输对象,所以要在传输前序列化,传输完成后反序列化成对象。例如我们之前学习过 socket 编程中发送与接收数据。
二、protobuf的安装
1、在windows中安装protobuf
安装网址:Releases · protocolbuffers/protobuf · GitHub选择相匹配的版本进行下载。
在命令提示符中输入protoc --version查看是否安装成功。
2、在Linux中安装protobuf
首先要安装依赖库:
Ubuntu:
sudo apt-get install autoconf automake libtool curl make g++ unzip -y
CentOs:
sudo yum install autoconf automake libtool curl make gcc-c++ unzip
解压后进入文件夹,依次执行下方命令:
第⼀步执⾏autogen.sh,但如果下载的是具体的某⼀门语⾔,不需要执⾏这⼀步,如果是all,则需要。
./autogen.sh
第⼆步执⾏configure,有两种执⾏⽅式,任选其⼀即可,如下:
1、protobuf默认安装在 /usr/local ⽬录,lib、bin都是分散的
./configure
2、修改安装⽬录,统⼀安装在/usr/local/protobuf下
./configure --prefix=/usr/local/protobuf
第三步:make //很久
第四步:make check //很久
第五步:sudo make install
第六步(可选):当时选择第二种修改目录的安装方式,需要sudo vim /etc/profile并在vim末尾添加:
#(动态库搜索路径) 程序加载运⾏期间查找动态链接库时指定除了系统默认路径之外的其他路径
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/usr/local/protobuf/lib/
#(静态库搜索路径) 程序编译期间查找动态链接库时指定查找共享库的路径
export LIBRARY_PATH=$LIBRARY_PATH:/usr/local/protobuf/lib/
#执⾏程序搜索路径
export PATH=$PATH:/usr/local/protobuf/bin/
#c程序头⽂件搜索路径
export C_INCLUDE_PATH=$C_INCLUDE_PATH:/usr/local/protobuf/include/
#c++程序头⽂件搜索路径
export CPLUS_INCLUDE_PATH=$CPLUS_INCLUDE_PATH:/usr/local/protobuf/include/
#pkg-config 路径
export PKG_CONFIG_PATH=/usr/local/protobuf/lib/pkgconfig/
第七步:source /etc/profile
第八步:protoc --version检查是否安装成功
三、protobuf写法
1、contacts.proto
// 首行:语法指定行
syntax = "proto3";
// 命名空间
package contacts;
// 定义联系人message
message PeopleInfo {string name = 1; // 姓名,这里的1就代表name这个字段,数字必加且不一样,否则编译报错int32 age = 2; // 年龄
}
字段编号1 ~ 536,870,911 (2^29 - 1) ,其中 19000 ~ 19999 不可⽤。
范围为 1 ~ 15 的字段编号需要⼀个字节进⾏编码, 16 ~ 2047 内的数字需要两个字节
进⾏编码。编码后的字节不仅只包含了编号,还包含了字段类型。所以 1 ~ 15 要⽤来标记出现⾮常频
繁的字段,要为将来有可能添加的、频繁出现的字段预留⼀些出来。(这样网络传输的数据就小了)
2、编译命令
protoc --cpp_out=./ contacts.proto
=./(路径) contacts.proto(使用文件)
protoc -I fast_start/ --cpp_out=fast_start/ contacts.proto
-I fast_start/ (编译时可以去这个路径下面找.proto文件,后面的指定的.proto就可以不用加路径了,-I后边可以跟多个搜索路径)
序列化的结果为⼆进制字节序列,而非文本格式。(所以可以使用std::string存储)
3、标量数据类型
4、序列化/反序列化的使用
#include <iostream>
#include <string>
#include "contacts.pb.h"
int main()
{ std::string people_str;// 对⼀个联系⼈的信息使⽤ PB 进⾏序列化,并将结果打印出来。{contacts::PeopleInfo people; // 使用命名空间contacts中的PeopleInfo类创建people对象people.set_name("张三");people.set_age(18);if (!people.SerializeToString(&people_str)){std::cerr << "序列化联系人失败" << std::endl;return -1;}std::cout << "序列化结果为:" << people_str.c_str() << std::endl;}// 对序列化后的内容使⽤ PB 进⾏反序列,解析出联系⼈信息并打印出来{contacts::PeopleInfo people;if (!people.ParseFromString(people_str)){std::cerr << "反序列化联系人失败" << std::endl;return -1;}std::cout << "反序列化结果为:" << std::endl;std::cout << "姓名:" << people.name() << std::endl; // 其实是getName,get省略std::cout << "年龄:" << people.age() << std::endl;}return 0;
}
main:main.cc contacts.pb.ccg++ -o $@ $^ -std=c++11 -lprotobuf
.PHONY:clean
clean:rm -f main
5、字段规则
• singular :消息中可以包含该字段零次或⼀次(不超过⼀次)。 proto3 语法中,字段默认使⽤该
规则。 (多次会被覆盖)
• repeated :消息中可以包含该字段任意多次(包括零次),其中重复值的顺序会被保留。可以理
解为定义了⼀个数组。(repeated修饰的变量需要使用add_变量名进行提取指针,修改内容)
1、可以message里面套massage
2、可以在一个.proto文件中包含另一个.proto文件,这样就可以使用它的message。impoet “phone.proto”,repeated phone.Phone phone =3;(第一个phone是phone.proto中的命名空间)
6、序列化/反序列化到文件
// 首行:语法指定行
syntax = "proto3";
// 命名空间
package phone;message Phone{string number= 1;
}
// 首行:语法指定行
syntax = "proto3";
// 命名空间
package contacts2;
import "phone.proto";
// message Phone{
// string number= 1;
// }
// 定义联系人message,message定义的都是类
message PeopleInfo {string name = 1; // 姓名,这里的1就代表name这个字段,数字必加且不一样,否则编译报错int32 age = 2; // 年龄// 这是一个修饰符,表示该字段可以包含零个或多个元素。它类似于数组或列表,允许多个值。// string是字段的数据类型,表示该字段的每个元素都是一个字符串。// message Phone{ // 也可以嵌套message// string number= 1;// }repeated phone.Phone phone = 3; //repeated string phone_numbers = 3;
}// 定义通讯录message
message Contacts{repeated PeopleInfo contacts = 1;
}
#include <iostream>
#include <fstream>
#include <string>
#include "contacts.pb.h"
using namespace std;
void AddPeopleInfo(contacts2::PeopleInfo* people)
{cout << "新增联系人" << endl;cout << "请输入姓名:";string name;getline(cin, name);people->set_name(name);cout << "请输入年龄:";int age = 0;cin >> age;people->set_age(age);cin.ignore(256, '\n');while(1){cout << "请输入电话:";string number;getline(cin, number);if (number.empty()) { break; }phone::Phone* phone = people->add_phone();phone->set_number(number);}cout << "新增联系人成功" << endl;
}
int main()
{ contacts2::Contacts contacts;// 读取本地已存在的通讯录文件fstream input("contacts.bin", ios::in | ios::binary);if (!input){cout << "创建文件contacts.bin" << std::endl;}else if(!contacts.ParseFromIstream(&input)) // 读取并反序列化{cerr << "反序列化失败" << std::endl;input.close();return -1;}// 在通讯录中添加联系人AddPeopleInfo(contacts.add_contacts());// 将通讯录写入本地文件中fstream output("contacts.bin", ios::out | ios::trunc | ios ::binary);if (!contacts.SerializeToOstream(&output)){cerr << "序列化失败" << std::endl;input.close();output.close();return -1;}cout << "数据写入成功" << endl;input.close();output.close();return 0;
}
#include <iostream>
#include <fstream>
#include <string>
#include "contacts.pb.h"
using namespace std;
void PrintContacts(const contacts2::Contacts& contacts)
{// contacts中就一个数组PeopleInfo contacts,遍历数组打印for (auto& e : contacts.contacts()){cout << "联系人姓名:" << e.name();cout << "联系人年龄:" << e.age();for (auto& p : e.phone()){cout << "联系人电话:" << p.number() << endl;}}
}
int main()
{// 读取本地已存在的contacts.bincontacts2::Contacts contacts;// 读取本地已存在的通讯录文件(因为上个write例子已经创建过了,肯定存在)fstream input("contacts.bin", ios::in | ios::binary);if(!contacts.ParseFromIstream(&input)) // 读取并反序列化{cerr << "反序列化失败" << std::endl;input.close();return -1;}// 打印通讯录列表PrintContacts(contacts);return 0;
}
all:write read
read:read.cc contacts.pb.cc phone.pb.cc g++ -o $@ $^ -std=c++11 -lprotobuf
write:write.cc contacts.pb.cc phone.pb.ccg++ -o $@ $^ -std=c++11 -lprotobuf
.PHONY:clean
clean:rm -f write read
8、也可以使用--decode命令查看二进制文件。没必要写read.cc
[root@localhost proto3语法]# protoc --decode=contacts2.Contacts contacts.proto < contacts.bin
contacts {name: "zhangsan "age: 22phone {number: "1"}phone {number: "1"}
}
--decode=contacts2.Contacts
:- 指定你要解码的数据类型。
contacts2.Contacts
是.proto
文件中定义的message类型。contacts2
是命名空间package,Contacts
是消息类型名称。--decode
参数用于告诉protoc
如何解释二进制数据。contacts.proto
:- 指定用于解码的 Protocol Buffers 文件。这是消息结构的定义文件。
< contacts.bin
:- 使用输入重定向符
<
从contacts.bin
文件读取二进制数据。 如果没有使用< contacts.bin
这种输入重定向方式,protoc
将会从标准输入(stdin)中读取数据。这意味着protoc
等待你在命令行中直接输入二进制数据,或者从其他命令的输出中获取数据。contacts.bin
是通过 Protocol Buffers 序列化后的数据文件,它包含了符合contacts2.Contacts
消息结构的二进制数据。
9、枚举类型
要注意枚举类型的定义有以下几种规则:
1. 0 值常量必须存在,且要作为第⼀个元素。这是为了与 proto2 的语义兼容:第⼀个元素作为默认
值,且值为 0。 (如果字段的枚举值没有设置,默认就是0)
2. 枚举类型可以在消息外定义,也可以在消息体内定义(嵌套)。
3. 枚举的常量值在 32 位整数的范围内。但因负值无效因而不建议使用(与编码规则有关)
4. 在一个作用域中,存在多个enum,需要保证这些enum中的成员常量名不一样。(import引入其他文件的enum时要注意全局作用域中是否存在同名enum常量名(加命名空间package解决))
10、Any类型
字段还可以声明为 Any 类型,可以理解为泛型类型。使⽤时可以在 Any 中存储任意消息类型。Any 类
型的字段也⽤ repeated 来修饰。
Any 类型是 google 已经帮我们定义好的类型,在安装 ProtoBuf 时,其中的 include ⽬录下查找所有
google 已经定义好的 .proto ⽂件。
11、oneof类型
1、如果消息中有很多可选字段, 并且将来同时只有⼀个字段会被设置, 那么就可以使用 oneof 加强这 个行为,也能有节约内存的效果。(如果设置多次,最后一次设置的为准)
2、在oneof中,不能使用repeated修饰。
3、编号和其他message中的编号不能冲突。
12、map类型
语法支持创建⼀个关联映射字段,也就是可以使⽤ map 类型去声明字段类型,格式为:
map<key, value> map_field = N;
要注意的是:
• key 是除了 float 和 bytes 类型以外的任意标量类型。 value 可以是任意类型。
• map 字段不可以⽤ repeated 修饰
• map 中存⼊的元素是⽆序的
13、默认值
反序列化消息时,如果被反序列化的⼆进制序列中不包含某个字段,反序列化对象中相应字段时,就
会设置为该字段的默认值。不同的类型对应的默认值不同:
• 对于字符串,默认值为空字符串。
• 对于字节,默认值为空字节。
• 对于布尔值,默认值为 false。
• 对于数值类型,默认值为 0,double是0.0。
• 对于枚举,默认值是第⼀个定义的枚举值, 必须为 0。
• 对于消息字段,未设置该字段。它的取值是依赖于语⾔。
• 对于设置了 repeated 的字段的默认值是空的( 通常是相应语⾔的⼀个空列表 )。
• 对于 消息字段 、 oneof字段 和 any字段 ,C++ 和 Java 语⾔中都有 has_ ⽅法来检测当前字段
是否被设置。
• 对于标量数据类型,proto3语法并没有提供has_方法(标量数据类型会被设置为默认值)
14、更新规则
禁⽌修改任何已有字段的字段编号。
• 若是移除⽼字段,要保证不再使⽤移除字段的字段编号。正确的做法是保留字段编号(reserved),以确保该编号将不能被重复使⽤。不建议直接删除或注释掉字段。
• 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 类型是不安全的。
15、常用选项
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;
16、总体代码
contacts.proto
// 首行:语法指定行
syntax = "proto3";
// 命名空间
package contacts2;
import "google/protobuf/any.proto";
import "phone.proto";
message Address{string home_address = 1; // 家庭住址string unit_address = 2; // 公司地址}
message PeopleInfo {reserved 100, 101, 150 to 200; // 保留字段,禁止使用该字段reserved "hhh";string name = 1; // 姓名,这里的1就代表name这个字段,数字必加且不一样,否则编译报错int32 age = 2; // 年龄// 这是一个修饰符,表示该字段可以包含零个或多个元素。它类似于数组或列表,允许多个值。// string是字段的数据类型,表示该字段的每个元素都是一个字符串。// message Phone{ // 也可以嵌套message// string number= 1;// }repeated phone.Phone phone = 3; //repeated string phone_numbers = 3;google.protobuf.Any data = 4;oneof other_contact{string qq = 5;string wechat = 6;}map<string, string> remark = 7;
}// 定义通讯录message
message Contacts{repeated PeopleInfo contacts = 1;
}
phone.proto
// 首行:语法指定行
syntax = "proto3";
// 命名空间
package phone;message Phone{enum PhoneType{MP = 0;TEL = 1;}string number = 1;PhoneType type = 2;
}
makefile
all:write read
read:read.cc contacts.pb.cc phone.pb.cc g++ -o $@ $^ -std=c++11 -lprotobuf
write:write.cc contacts.pb.cc phone.pb.ccg++ -o $@ $^ -std=c++11 -lprotobuf
.PHONY:clean
clean:rm -f write read
write.cc
#include <iostream>
#include <fstream>
#include <string>
#include "contacts.pb.h"
using namespace std;
void AddPeopleInfo(contacts2::PeopleInfo* people)
{cout << "新增联系人" << endl;cout << "请输入姓名:";string name;getline(cin, name);people->set_name(name);cout << "请输入年龄:";int age = 0;cin >> age;people->set_age(age);cin.ignore(256, '\n');while(1){cout << "请输入电话:";string number;getline(cin, number);if (number.empty()) { break; }phone::Phone* phone = people->add_phone();phone->set_number(number);cout << "输入电话类型1、固定2移动:";int type;cin >> type;cin.ignore(256, '\n');if (type == 1){phone->set_type(phone::Phone_PhoneType::Phone_PhoneType_MP);}else{phone->set_type(phone::Phone_PhoneType::Phone_PhoneType_TEL);}}contacts2::Address address; // 定义地址对象cout << "请输入联系人的家庭地址:";string home_address;getline(cin, home_address);address.set_home_address(home_address);cout << "请输入联系人的单位地址:";string unit_address;getline(cin, unit_address);address.set_unit_address(unit_address);// 将address对象转换为Anypeople->mutable_data()->PackFrom(address);cout << "请选择添加1、qq或者2、微信:";int input = 1;cin >> input;cin.ignore(256, '\n');cout << "请输入账号:";string account;getline(cin, account);if (1 == input){people->set_qq(account);}else{people->set_wechat(account);}int i = 1;while (1){cout << "请输入第" << i << "个备注的标题:"; string remark_key;getline(cin, remark_key);if (remark_key.empty()) { break; }cout << "请输入第" << i++ << "个备注的内容:"; string remark_value;getline(cin, remark_value);(*people->mutable_remark())[remark_key] = remark_value;}cout << "新增联系人成功" << endl;
}
int main()
{ contacts2::Contacts contacts;// 读取本地已存在的通讯录文件fstream input("contacts.bin", ios::in | ios::binary);if (!input){cout << "创建文件contacts.bin" << std::endl;}else if(!contacts.ParseFromIstream(&input)) // 读取并反序列化{cerr << "反序列化失败" << std::endl;input.close();return -1;}// 在通讯录中添加联系人AddPeopleInfo(contacts.add_contacts()); // add_contacts()这个方法会返回一个指向新添加的元素的指针,使得你可以直接操作该元素。// 将通讯录写入本地文件中fstream output("contacts.bin", ios::out | ios::trunc | ios ::binary);if (!contacts.SerializeToOstream(&output)){cerr << "序列化失败" << std::endl;input.close();output.close();return -1;}cout << "数据写入成功" << endl;input.close();output.close();return 0;
}
read.cc
#include <iostream>
#include <fstream>
#include <string>
#include "contacts.pb.h"
using namespace std;
void PrintContacts(const contacts2::Contacts& contacts)
{// contacts中就一个数组PeopleInfo contacts,遍历数组打印for (auto& e : contacts.contacts()){cout << "联系人姓名:" << e.name() << endl;cout << "联系人年龄:" << e.age() << endl;for (auto& p : e.phone()){cout << "联系人电话:" << p.number() << endl;if (phone::Phone_PhoneType::Phone_PhoneType_MP == p.type()){cout << "联系人电话类型:" << phone::Phone_PhoneType::Phone_PhoneType_MP << endl;}else if (phone::Phone_PhoneType::Phone_PhoneType_TEL == p.type()){cout << "联系人电话类型:" << phone::Phone_PhoneType::Phone_PhoneType_TEL << endl;}}if (e.has_data() && e.data().Is<contacts2::Address>()){contacts2::Address address;e.data().UnpackTo(&address);cout << "家庭地址:" << address.home_address().c_str() << endl;cout << "公司地址:" << address.unit_address().c_str() << endl;}if (e.has_qq()){cout << "联系人的qq是:" << e.qq() << endl;}else if (e.has_wechat()){cout << "联系人的微信是:" << e.wechat() << endl;}if (e.remark_size()){int i = 1;for (auto& re : e.remark()){cout << "第" << i << "个备注的标题:" << re.first << endl;cout << "第" << i++ << "个备注的内容:" << re.second << endl;}}cout << endl;}
}
int main()
{// 读取本地已存在的contacts.bincontacts2::Contacts contacts;// 读取本地已存在的通讯录文件(因为上个write例子已经创建过了,肯定存在)fstream input("contacts.bin", ios::in | ios::binary);if(!contacts.ParseFromIstream(&input)) // 读取并反序列化{cerr << "反序列化失败" << std::endl;input.close();return -1;}// 打印通讯录列表PrintContacts(contacts);return 0;
}