ProtoBuf序列化技术详解与实战指南
初识序列化
序列化概念
序列化和反序列化
- 序列化:把对象转换为字节序列的过程 称为对象的序列化
- 反序列化:把字节序列恢复为对象的过程 称为对象的反序列化
那什么情况需要序列化?
- 储存数据:当你想把内存中的对象状态保存到一个文件中或者存到数据库中
- 网络传输:网络直接传输数据,但是无法直接传送对象,所以要在传输前序列化,传输完成后反序列化对象。
ProtoBuf是什么
Protocol Buffers 是 Google 的⼀种语言无关、平台⽆关、可扩展的序列化结构数据的⽅法,它可用于(数据)通信协议、数据存储等。
Protocol Buffers 类⽐于 XML,是⼀种灵活,⾼效,⾃动化机制的结构数据序列化⽅法,但是比XML 更小、更快、更为简单。
你可以定义数据的结构,然后使⽤特殊⽣成的源代码轻松的在各种数据流中使⽤各种语⾔进⾏编写和读取结构数据。你甚⾄可以更新数据结构,⽽不破坏由旧数据结构编译的已部署程序。
总结来说,ProtoBuf是让结构数据序列化的方法,它的有以下特点:
- 语言无关、平台无关,ProtoBuf支持多种语言,多个平台
- 高效:比XML更小、更快、更简单
- 扩展性、兼容性好:你可以更新数据结构,而不想影响和破坏原有的旧程序。
使用特点
- 编写.proto文件,目的是为了定义结构对象以及属性内容
- 是以哦那个protoc编译器编译.proto文件,生成一系列接口代码,存放在新生成头文件和源文件中
- 依赖生成的接口,将编译生成的头文件包含进我们的代码中,实现对.proto文件中定义的字段进行设置和获取,和对message对象进行序列化和反序列化。
总的来说,ProtoBuf是需要依赖编译生成的头文件和源文件来使用的。
ProtoBuf的安装
ProtoBuf在window下的安装
下载ProtoBuf编译器
下载地址:ProtoBuf
可以不下载最新版本,具体下载根据自己的电脑情况选择。
下载之后将压缩包解压到本地⽬录下。解压后的⽂件内包含 bin、include⽂件,以及⼀个readme.txt。
配置环境变量
把解压后⽂件中的bin⽬录配置到系统环境变量的Path中去
检查时候配置成功
打开cmd,输入protoc --version
ProtoBuf在Linux(Ubuntu)下的安装
下载ProtoBuf
下载 ProtoBuf 前⼀定要安装依赖库:autoconf automake libtool curl make g++ unzip
如未安装,安装命令如下:
sudo apt-get install autoconf automake libtool curl make g++ unzip -y
ProtoBuf 下载地址:ProtoBuf
- 如果要在C++下使用ProtoBuf,可以选择 cpp.zip;
- 如果要在Java中使用ProtoBuf,可以选择 java.zip;
- ……
- 如果希望支持所有语言,选择 all.zip
这里选择:all.zip
下载命令:wget https://github.com/protocolbuffers/protobuf/releases/download/v21.12/protobuf-all-21.12.zip
下载完成后,解压zip包:unzip protobuf-all-21.12.zip
在进入这个目录:cd protobuf-21.12/
安装ProtoBuf
# 第⼀步执⾏autogen.sh,但如果下载的是具体的某⼀⻔语⾔,不需要执⾏这⼀步。
./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
如果在 make check 出现错误,例如
出现以上错误的原因是test的模块⾥⾯有⾮常多的测试⽤例,这些⽤例对服务器环境要求特别严格,需要增⼤下swap分区,具体操作可参考:csdn
执⾏ make check 后 ,出现以下内容就可以执⾏ sudo make install 。
到此,需要你回忆⼀下在执⾏configure时,如果当时选择了第⼀种执⾏⽅式,也就是./configure ,那么到这就可以正常使⽤protobuf了。如果选择了第⼆种执⾏⽅式,即修改了安装⽬录,那么还需要在/etc/profile 中添加⼀些内容:
sudo vim /etc/profile# 添加内容如下:
#(动态库搜索路径) 程序加载运⾏期间查找动态链接库时指定除了系统默认路径之外的其他路径
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/
最后一步,重新执行/etc/profile文件:
source /etc/profile
检查时候安装成功
protoc --version
快速上手
设计一个通讯录,实现
- 对一个联系人的信息使用PB进行序列化,并将结果打印出来
- 对序列化之后的内容使用PB进行反序列化,解析出联系人的信息并打印出来
- 联系人包含:姓名、年龄
创建.proto文件
文件规范
- 创建.proto文件时,文件命名应该使用全小写字母命名,多字母之间使用_连接。
- 书写.proto文件代码时,应使用2个空格的缩进
添加注释
向文件添加注释,可使用//或者/**/
指定proto3语法
Protocol Buffers语言版本3,简称proto3,是.proto文件最新的语法版本。proto3简化了Protocol Buffers语言,既易于使用,又可以在更广泛的编程语言中使用。它允许你使用C++,Java,python等多种语言生成Protocol Buffers代码
内容如下:
syntax = "proto3";
package声明符
package是一个可选的声明符,能表示.proto文件的命名空间,在项目中要有唯一性。它的作用就是为了避免我们定义的消息出现冲突。
syntax = "proto3";
package contacts;
定义消息(message)
消息(mesage):要定义的结构化对象,我们可以给这个结构化对象中定义其对应的属性内容。
// 首行:语法指定行
syntax = "proto3";
package contacts;//定义联系人message
message PeopleInfo{}
定义消息字段
在message中我们可以定义其属性字段,字段定义格式为:字段类型 字段名=字段唯一编号
- 字段名称命名规范:全小写字母,多个字母之间使用_连接
- 字段类型分为:标量数据类型和特殊数据类型(包括枚举,其他消息类型等)
- 字段唯一编号:用来标识字段,一旦开始使用就不能够再改变。
.proto Type | Notes | C++ Type |
---|---|---|
double | double | |
float | float | |
int32 | 使用变长编码1。负数的编码效率较低,若字段可能为负值,应使用sint32代替 | int32 |
int64 | 使用变长编码1。负数的编码效率较低,若字段可能为负值,应使用sint64代替 | int64 |
uint64 | 使用变长编码1 | uint64 |
sint32 | 使用变长编码1 | int32 |
sint64 | 使用变长编码1。负数的编码效率较低,若字段可能为负值,应使用int32代替 | 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 编码的字符串,⻓度不能超过 | string |
bytes | 2^32 。可包含任意的字节序列但⻓度不能超过 2^32 。 | string |
syntax = "proto3";
package contacts;//定义联系人message
message PeopleInfo{string name=1; // 姓名int32 age = 2; // 年龄
}
字段唯一编号
1 ~ 536,870,911 (2^29 - 1) ,其中 19000 ~ 19999 不可用
19000 ~ 19999 不可⽤是因为:在 Protobuf 协议的实现中,对这些数进⾏了预留。如果⾮要在.proto⽂件中使⽤这些预留标识号,例如将 name 字段的编号设置为19000,编译时就会报警。
范围为1~15的字段编号只需要一个字节进行编码,16~2047内的数字需要两个字节进行编码。编码后的字节不仅包含了编号,还包含了字段类型。所以1~15要用来标记出现非常频繁的字段,要为将来有可能添加、频繁出现的字段预留一些出来。
编译contacts.proto文件,生成C++文件
编译命令
protoc [--proto_path=IMPORT_PATH] --cpp_out=DST_DIR path/to/file.protoprotoc 是 Protocol Buffer 提供的命令⾏编译⼯具。
--proto_path=IMPORT_PATH 指定 被编译的.proto⽂件所在⽬录,可多次指定。可简写成 -I 。如不指定该参数,则在当前⽬录进⾏搜索。当某个.proto ⽂件 import其他.proto ⽂件时,或需要编译的 .proto ⽂件不在当前⽬录下,这时就要⽤-I来指定搜索录。
--cpp_out= 指编译后的⽂件为 C++ ⽂件。
OUT_DIR 编译后⽣成⽂件的⽬标路径。
path/to/file.proto 要编译的.proto⽂件
编译contacts.proto文件后会生成什么
编译 contacts.proto ⽂件后,会⽣成所选择语⾔的代码,我们选择的是C++,所以编译后⽣成了两个⽂件:contacts.pb.h contacts.pb.cc
对于每个message,都会生成一个对应的消息类。
- 对于每个message,都会生成一个对应的消息类
- 在消息类中,编译器为每个字段提供了获取和设置方法,以及其他能够操作字段的方法
- 编译器会针对每个.proto文件生成.h和.cpp文件,分别用来存放类的声明和实现。
在消息类的⽗类MessageLite 中,提供了读写消息实例的⽅法,包括序列化⽅法和反序列化⽅法。
注意:
- 序列化的结果为二进制字节序列,而非文本格式
- 三种序列化的方法没有本质上的区别,只是序列化后输出的格式不同,可以提供给不同的应用场景。
- 序列化的API函数均为const成员函数,因为序列化不会改变类对象的内容,而是将序列化的结果保存在函数入参指定的地址中
序列化和反序列化的使用
创建一个测试文件main.cc
#include "contacts.pb.h"
#include <iostream>
#include <string>int main()
{std::string people_str;{ // 对一个联系人进行序列化contacts::PeopleInfo people;people.set_name("张三");people.set_age(20);if (!people.SerializeToString(&people_str)){std::cerr << "失败的序列化" << std::endl;return -1;}std::cout << "序列化成功,结果为" << people_str << std::endl;}// 进行反序列化{contacts::PeopleInfo peopleInfo;if(!peopleInfo.ParseFromString(people_str)){std::cerr << "失败的反序列化" << std::endl;return -2;}std::cout << "反序列化成功" << std::endl;std::cout<<peopleInfo.name()<<std::endl;std::cout<<peopleInfo.age()<<std::endl;}return 0;
}
编译指令:
g++ main.cc contacts.pb.cc -o TestProtoBuf -std=c++11 -lprotobuf
- -lprotobuf:必加,不然有链接错误
- -std=c++11:必加,因为使用到了C++11的语法
运行结果:
./test 序列化成功,结果为
张三
反序列化成功
张三
20
由于 ProtoBuf 是把联系⼈对象序列化成了⼆进制序列,这⾥⽤ string 来作为接收⼆进制序列的容器。
所以在终端打印的时候会有换⾏等⼀些乱码显⽰。
所以相对于 xml 和 JSON 来说,因为被编码成⼆进制,破解成本增⼤,ProtoBuf 编码是相对安全的。
proto3语法详解
升级之前的要求
- 不在打印联系人的序列化结果,而是将通讯录序列化写入到文件中
- 从文件中将通讯录解析出来,并进行打印
- 新增联系人的属性,共包括:姓名、年龄、电话信息、地址、其他联系方式,备注
字段规则
消息的字段可以下面几种规则来修饰:
- sigular:消息中可以包含该字段零次或一次。proto3语法中,字段默认使用该规则
- repeated:消息中可以包含该字段任意多次,其中重复值的顺序会被保留。可以理解为定义了一个数组。
更新contacts.proto,People消息中新增了phone_numbers字段,表示一个联系人有多个号码,可将其设置为repeated,写法如下:
syntax="proto3";package contacts2;message PeopleInfo{string name = 1;// 姓名int32 age = 2; // 年龄message Phone{string number = 1;}repeated Phone phone = 3; // 电话信息
}
消息类型的定义和使用
定义
在单个.proto文件中可以定义多个消息体,且支持定义嵌套类型的消息。每个消息体中的字段编号可以重复。
// 嵌套写法
syntax="proto3";package contacts2;message PeopleInfo{string name = 1;// 姓名int32 age = 2; // 年龄message Phone{string number = 1;}repeated Phone phone = 3; // 电话信息
}//非嵌套写法
syntax="proto3";package contacts2;message Phone{string number = 1;
}message PeopleInfo{string name = 1;// 姓名int32 age = 2; // 年龄repeated Phone phone = 3; // 电话信息
}
使用
- 消息类型可作为字段类型使用
syntax="proto3";package contacts2;message PeopleInfo{string name = 1;// 姓名int32 age = 2; // 年龄message Phone{string number = 1;}repeated Phone phone = 3; // 电话信息
}
- 可导入其他.proto文件的消息来使用来使用
例如Phone消息定义在phone.proto文件中
syntax="proto3";package phone;message Phone{string number = 1;
}
contacts.proto中的PeopleInfo使用Phone消息
syntax = "proto3";package contacts2;import "phone.proto";message PeopleInfo{string name = 1;int32 age = 2;repeated phone.Phone phone = 3;
}
创建通讯录2.0版本
通讯录2.x的需求是向文件中写入通讯录列表,以上我们只是定义了一个联系人的消息,并不能存放通讯录列表,所以还需要完善一下contacts.proto:
syntax="proto3";package contacts2;message PeopleInfo{string name = 1;// 姓名int32 age = 2; // 年龄message Phone{string number = 1;}repeated Phone phone = 3; // 电话信息
}// 通讯录message
message Contacts{repeated PeopleInfo contacts=1;}
接下来进行编译
protoc --cpp_out=. contacts.proto
实现写入
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;cin>>name;people->set_name(name);cout<<"请输入联系人年龄:";int age;cin>>age;people->set_age(age);cin.ignore(256,'\n');for(int i=0;;++i){cout<<"请输入联系人的电话"<<i+1<<"(只输入回车完成电话新增)";string number;getline(cin,number);if(number.empty())break;contacts2::PeopleInfo_Phone* phone = people->add_phone();phone->set_number(number);}cout<<"-----------添加联系⼈成功-----------"<<endl;
}int main()
{contacts2::Contacts contacts;// 0 .读取本地已存在的通讯录文件fstream input("contacts.bin", ios::in | ios::binary);if (!input){cerr << "contacts.bin not find, create new file!" << endl;}else if (!contacts.ParseFromIstream(&input)){cerr << "parse error!" << endl;input.close();exit(2);}// 1. 向通讯录中添加联系人AddPeopleInfo(contacts.add_contacts());// 2. 将通讯录写入本地文件中(这里使用覆盖写)fstream output("contacts.bin", ios::out | ios::binary | ios::trunc);if (!contacts.SerializeToOstream(&output)){cerr << "write error!" << endl;input.close();output.close();exit(2);}cout << "write success!" << endl;input.close();output.close();return 0;
}
makefile
write:write.cc contacts.pb.ccg++ $^ -o $@ -std=c++11 -lprotobuf.PHONY:clean
clean:rm -rf write read
make之后,运行write
查看二进制文件
hexdump -C contacts.bin
实现读取
read.cc
#include <iostream>
#include <fstream>
#include "contacts.pb.h"
using namespace std;void PrintContacts(contacts2::Contacts& contacts)
{for(int i=0;i<contacts.contacts_size();++i){cout<<"-----------联系人"<<i+1<<"-----------"<<endl;const contacts2::PeopleInfo& people = contacts.contacts(i);cout<<"联系人姓名:"<<people.name()<<endl;cout<<"联系人年龄:"<<people.age()<<endl;for(int j=0;j<people.phone_size();++j){const contacts2::PeopleInfo_Phone&phone = people.phone(j);cout<<"电话"<<j+1<<":"<<phone.number()<<endl;}}
}
int main()
{// 1. 读取本地文件中通讯录contacts2::Contacts contacts;fstream input("contacts.bin", ios::in | ios::binary);if (!input){cerr << "contacts.bin not find, create new file!" << endl;}else if (!contacts.ParseFromIstream(&input)){cerr << "parse error!" << endl;input.close();exit(2);}// 2. 打印通讯录列表PrintContacts(contacts);return 0;
}
makefile
all:write read
read:read.cc contacts.pb.ccg++ $^ -o $@ -std=c++11 -lprotobufwrite:write.cc contacts.pb.ccg++ $^ -o $@ -std=c++11 -lprotobuf.PHONY:clean
clean:rm -rf write read
之后运行read
介绍一下–decode
我们首先使用protoc -h
命令来查看ProtoBuf为我们提供的所有命令option。其中ProtoBuf提供了一个命令选项 --decode
,表示从标准输入中读取给定类型的二进制消息,并将其以文本格式写入标准输出。消息类型必须在.proto文件中或者导入的文件中定义。
enum类型
定义规则
语法支持我们定义枚举类型并使用。在.proto文件中枚举类型的书写规范为:
枚举类型名称:
使⽤驼峰命名法,⾸字⺟⼤写。 例如: MyEnum
常量值名称:
全⼤写字⺟,多个字⺟之间⽤ _ 连接。例如: ENUM_CONST = 0;
我们可以定义一个名为PhoneType的枚举类型,定义如下:
enum PhoneType{MP = 0; // 移动电话TEL = 1; 固定电话
}
需注意枚举类型的定义有几种规则:
- 0值常量必须存在。这是为了与proto2的语义兼容;第一个元素作为默认值,且值为0
- 枚举类型可以在消息外定义,也可以在消息体内定义(嵌套)
- 枚举的常量值在32位整数的范围内。但因负值无效因而不建议使用(与编码规则有关)
定义时注意
将两个‘具有相同枚举值名称’的枚举类型放在单个.proto文件下测试时,编译后会报错;xxxx常量已经被定义!所以这里要注意:
- 同级的枚举类型,各个枚举类型中的常量不能重
- 单个.proto文件下,最外层的枚举类型和嵌套枚举类型不算同级
- 多个.proto文件下,若一个文件引入了其他文件,且每个文件都没有声明package,每个proto文件中的枚举类型都在最外层,算同级
- 多个.proto文件下,若一个文件引入了其他文件,且每个文件都声明了package,不算同级
升级通讯录至2.1版本
更新contacts.proto,新增枚举字段并使用,更新内容如下:
syntax="proto3";package contacts2;message PeopleInfo{string name = 1;// 姓名int32 age = 2; // 年龄message Phone{string number = 1;enum PhoneType{MP=0; // 移动电话TEL=1; // 固定电话}PhoneType type = 2;}repeated Phone phone = 3; // 电话信息
}// 通讯录message
message Contacts{repeated PeopleInfo contacts=1;}
编译
protoc --cpp_out=. contacts.proto
更新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;cin>>name;people->set_name(name);cout<<"请输入联系人年龄:";int age;cin>>age;people->set_age(age);cin.ignore(256,'\n');for(int i=0;;++i){cout<<"请输入联系人的电话"<<i+1<<"(只输入回车完成电话新增)";string number;getline(cin,number);if(number.empty())break;contacts2::PeopleInfo_Phone* phone = people->add_phone();phone->set_number(number);cout<<"请输入电话类型(1.移动电话 2.固定电话)";int type;cin>>type;cin.ignore(256,'\n');switch(type){case 1:phone->set_type(contacts2::PeopleInfo_Phone_PhoneType::PeopleInfo_Phone_PhoneType_MP);break;case 2:phone->set_type(contacts2::PeopleInfo_Phone_PhoneType::PeopleInfo_Phone_PhoneType_TEL);break;default:cout<<"选择有误"<<endl;}}cout<<"-----------添加联系⼈成功-----------"<<endl;
}int main()
{contacts2::Contacts contacts;// 0 .读取本地已存在的通讯录文件fstream input("contacts.bin", ios::in | ios::binary);if (!input){cerr << "contacts.bin not find, create new file!" << endl;}else if (!contacts.ParseFromIstream(&input)){cerr << "parse error!" << endl;input.close();exit(2);}// 1. 向通讯录中添加联系人AddPeopleInfo(contacts.add_contacts());// 2. 将通讯录写入本地文件中(这里使用覆盖写)fstream output("contacts.bin", ios::out | ios::binary | ios::trunc);if (!contacts.SerializeToOstream(&output)){cerr << "write error!" << endl;input.close();output.close();exit(2);}cout << "write success!" << endl;input.close();output.close();return 0;
}
更新read.cc
#include <iostream>
#include <fstream>
#include "contacts.pb.h"
using namespace std;void PrintContacts(contacts2::Contacts& contacts)
{for(int i=0;i<contacts.contacts_size();++i){cout<<"-----------联系人"<<i+1<<"-----------"<<endl;const contacts2::PeopleInfo& people = contacts.contacts(i);cout<<"联系人姓名:"<<people.name()<<endl;cout<<"联系人年龄:"<<people.age()<<endl;for(int j=0;j<people.phone_size();++j){const contacts2::PeopleInfo_Phone&phone = people.phone(j);cout<<"电话"<<j+1<<":"<<phone.number();cout<<"("<<phone.PhoneType_Name(phone.type())<<")"<<endl;}}
}
int main()
{// 1. 读取本地文件中通讯录contacts2::Contacts contacts;fstream input("contacts.bin", ios::in | ios::binary);if (!input){cerr << "contacts.bin not find, create new file!" << endl;}else if (!contacts.ParseFromIstream(&input)){cerr << "parse error!" << endl;input.close();exit(2);}// 2. 打印通讯录列表PrintContacts(contacts);return 0;
}
编译代码,然后运行
Any类型
字段还可以声明为Any类型,可以理解为泛型类型。使用时可以在Any中存储任意消息类型。Any类型的字段也可以使用repeated来修饰。
Any类型是Google已经帮我们定义好的类型,在安装ProtoBuf的时候,其中的include目录下查找所有的google已经定义好的.proto文件。
升级通讯录至2.2版本
通讯录2.2版本会新增联系人的地址信息,我们可以使用Any类型的字段来存储地址信息。更新contacts.proto
syntax="proto3";
package contacts2;import "google/protobuf/any.proto";message Address{string home_address = 1; // 家庭地址string unit_address = 2; // 单位地址
}message PeopleInfo{string name = 1;// 姓名int32 age = 2; // 年龄message Phone{string number = 1;enum PhoneType{MP=0; // 移动电话TEL=1; // 固定电话}PhoneType type = 2;}repeated Phone phone = 3; // 电话信息google.protobuf.Any data = 4; }// 通讯录message
message Contacts{repeated PeopleInfo contacts=1; // 联系人数组}
编译
protoc --cpp_out=. contacts.proto
之前讲过,我们可以在Any字段中存储任意消息类型,这就要设计到任意消息类型和Any类型之间的互相转换。这部分代码就在google为我们写好的头文件any.pb.h中。
更新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;cin>>name;people->set_name(name);cout<<"请输入联系人年龄:";int age;cin>>age;people->set_age(age);cin.ignore(256,'\n');for(int i=0;;++i){cout<<"请输入联系人的电话"<<i+1<<"(只输入回车完成电话新增)";string number;getline(cin,number);if(number.empty())break;contacts2::PeopleInfo_Phone* phone = people->add_phone();phone->set_number(number);cout<<"请输入电话类型(1.移动电话 2.固定电话)";int type;cin>>type;cin.ignore(256,'\n');switch(type){case 1:phone->set_type(contacts2::PeopleInfo_Phone_PhoneType::PeopleInfo_Phone_PhoneType_MP);break;case 2:phone->set_type(contacts2::PeopleInfo_Phone_PhoneType::PeopleInfo_Phone_PhoneType_TEL);break;default:cout<<"选择有误"<<endl;}}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<<"-----------添加联系人成功-----------"<<endl;
}int main()
{contacts2::Contacts contacts;// 0 .读取本地已存在的通讯录文件fstream input("contacts.bin", ios::in | ios::binary);if (!input){cerr << "contacts.bin not find, create new file!" << endl;}else if (!contacts.ParseFromIstream(&input)){cerr << "parse error!" << endl;input.close();exit(2);}// 1. 向通讯录中添加联系人AddPeopleInfo(contacts.add_contacts());// 2. 将通讯录写入本地文件中(这里使用覆盖写)fstream output("contacts.bin", ios::out | ios::binary | ios::trunc);if (!contacts.SerializeToOstream(&output)){cerr << "write error!" << endl;input.close();output.close();exit(2);}cout << "write success!" << endl;input.close();output.close();return 0;
}
更新read.cc
#include <iostream>
#include <fstream>
#include "contacts.pb.h"
using namespace std;void PrintContacts(contacts2::Contacts& contacts)
{for(int i=0;i<contacts.contacts_size();++i){cout<<"-----------联系人"<<i+1<<"-----------"<<endl;const contacts2::PeopleInfo& people = contacts.contacts(i);cout<<"联系人姓名:"<<people.name()<<endl;cout<<"联系人年龄:"<<people.age()<<endl;for(int j=0;j<people.phone_size();++j){const contacts2::PeopleInfo_Phone&phone = people.phone(j);cout<<"电话"<<j+1<<":"<<phone.number();cout<<"("<<phone.PhoneType_Name(phone.type())<<")"<<endl;}if(people.has_data()&&people.data().Is<contacts2::Address>()){contacts2::Address address;people.data().UnpackTo(&address);if(!address.home_address().empty()){cout<<"联系人家庭住址:"<<address.home_address()<<endl;}if(!address.unit_address().empty()){cout<<"联系人单位地址:"<<address.unit_address()<<endl;}}}
}
int main()
{// 1. 读取本地文件中通讯录contacts2::Contacts contacts;fstream input("contacts.bin", ios::in | ios::binary);if (!input){cerr << "contacts.bin not find, create new file!" << endl;}else if (!contacts.ParseFromIstream(&input)){cerr << "parse error!" << endl;input.close();exit(2);}// 2. 打印通讯录列表PrintContacts(contacts);return 0;
}
编译之后,运行
oneof类型
如果消息中有很多的可选字段,并且将来同事只有一个字段被设置,那么就可以使用oneof
加强这个行为,也能有节约内存的效果
升级通讯录
通讯录2.3版本新增联系人的其他联系方式,比如qq或者微信二选一,我们就可以使用oneof这段来加强多选一这种行为。oneof字段的定义的格式为:
one 字段名 {字段1; 字段2;...}
更新contacts.proto
syntax="proto3";
package contacts2;import "google/protobuf/any.proto";message Address{string home_address = 1; // 家庭地址string unit_address = 2; // 单位地址
}message PeopleInfo{string name = 1;// 姓名int32 age = 2; // 年龄message Phone{string number = 1;enum PhoneType{MP=0; // 移动电话TEL=1; // 固定电话}PhoneType type = 2;}repeated Phone phone = 3; // 电话信息google.protobuf.Any data = 4; oneof other_contact{// 这里面不可以使用repeatedstring qq = 5;string wechat = 6;}
}// 通讯录message
message Contacts{repeated PeopleInfo contacts=1; // 联系人数组}
- 可选字段中的字段编号,不能与非可选的编号冲突
- 不能在oneof中使用repeated字段
- 将来在设置oneof字段中值时,如果将oneof中的字段设置多个,那么只会保留最后一次设置的成员,之前的oneof成员被自动清除
编译
protoc --cpp_out=. contacts.proto
更新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;cin>>name;people->set_name(name);cout<<"请输入联系人年龄:";int age;cin>>age;people->set_age(age);cin.ignore(256,'\n');for(int i=0;;++i){cout<<"请输入联系人的电话"<<i+1<<"(只输入回车完成电话新增)";string number;getline(cin,number);if(number.empty())break;contacts2::PeopleInfo_Phone* phone = people->add_phone();phone->set_number(number);cout<<"请输入电话类型(1.移动电话 2.固定电话)";int type;cin>>type;cin.ignore(256,'\n');switch(type){case 1:phone->set_type(contacts2::PeopleInfo_Phone_PhoneType::PeopleInfo_Phone_PhoneType_MP);break;case 2:phone->set_type(contacts2::PeopleInfo_Phone_PhoneType::PeopleInfo_Phone_PhoneType_TEL);break;default:cout<<"选择有误"<<endl;}}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<<"-----------添加联系人成功-----------"<<endl;cout<<"请选择要添加的其他联系方式:(1. qq 2. wechat)";int other_contact;cin>>other_contact;cin.ignore(256,'\n');if(other_contact==1){cout<<"请输入QQ号:";string qq;getline(cin,qq);people->set_qq(qq);}else if(other_contact==2){cout<<"请输入微信号:";string wechat;getline(cin,wechat);people->set_wechat(wechat);}else{cout<<"选择有误,未成功设置其他联系方式";}
}int main()
{contacts2::Contacts contacts;// 0 .读取本地已存在的通讯录文件fstream input("contacts.bin", ios::in | ios::binary);if (!input){cerr << "contacts.bin not find, create new file!" << endl;}else if (!contacts.ParseFromIstream(&input)){cerr << "parse error!" << endl;input.close();exit(2);}// 1. 向通讯录中添加联系人AddPeopleInfo(contacts.add_contacts());// 2. 将通讯录写入本地文件中(这里使用覆盖写)fstream output("contacts.bin", ios::out | ios::binary | ios::trunc);if (!contacts.SerializeToOstream(&output)){cerr << "write error!" << endl;input.close();output.close();exit(2);}cout << "write success!" << endl;input.close();output.close();return 0;
}
更新read.cc
#include <iostream>
#include <fstream>
#include "contacts.pb.h"
using namespace std;void PrintContacts(contacts2::Contacts &contacts)
{for (int i = 0; i < contacts.contacts_size(); ++i){cout << "-----------联系人" << i + 1 << "-----------" << endl;const contacts2::PeopleInfo &people = contacts.contacts(i);cout << "联系人姓名:" << people.name() << endl;cout << "联系人年龄:" << people.age() << endl;for (int j = 0; j < people.phone_size(); ++j){const contacts2::PeopleInfo_Phone &phone = people.phone(j);cout << "电话" << j + 1 << ":" << phone.number();cout << "(" << phone.PhoneType_Name(phone.type()) << ")" << endl;}if (people.has_data() && people.data().Is<contacts2::Address>()){contacts2::Address address;people.data().UnpackTo(&address);if (!address.home_address().empty()){cout << "联系人家庭住址:" << address.home_address() << endl;}if (!address.unit_address().empty()){cout << "联系人单位地址:" << address.unit_address() << endl;}}switch(people.other_contact_case()){case contacts2::PeopleInfo::OtherContactCase::kQq:cout<<"联系人QQ号:"<<people.qq()<<endl;break;case contacts2::PeopleInfo::OtherContactCase::kWechat:cout<<"联系人微信号:"<<people.wechat()<<endl;break;default:break;}}
}
int main()
{// 1. 读取本地文件中通讯录contacts2::Contacts contacts;fstream input("contacts.bin", ios::in | ios::binary);if (!input){cerr << "contacts.bin not find, create new file!" << endl;}else if (!contacts.ParseFromIstream(&input)){cerr << "parse error!" << endl;input.close();exit(2);}// 2. 打印通讯录列表PrintContacts(contacts);return 0;
}
编译之后,运行
map类型
语法支持创建一个关联映射字段,也就是可以使用map类型去声明字段类型,格式为:
map<key_type,value_type> map_field = N;
要注意的是:
- key_type是除了float和bytes类型以外的任意标量类型。value_type可以是任意类型
- map字段不能使用repeated修饰
- map中存放的数据是无序的
升级通讯录
通讯录2.4版本新增联系人的备注信息,我们可以使用map类型的字段来存储备注信息。
更新contacts.proto
syntax="proto3";
package contacts2;import "google/protobuf/any.proto";message Address{string home_address = 1; // 家庭地址string unit_address = 2; // 单位地址
}message PeopleInfo{string name = 1;// 姓名int32 age = 2; // 年龄message Phone{string number = 1;enum PhoneType{MP=0; // 移动电话TEL=1; // 固定电话}PhoneType type = 2;}repeated Phone phone = 3; // 电话信息google.protobuf.Any data = 4; oneof other_contact{// 这里面不可以使用repeatedstring qq = 5;string wechat = 6;}map<string,string> remark = 7; // 备注信息 无序的
}// 通讯录message
message Contacts{repeated PeopleInfo contacts=1; // 联系人数组}
编译:
protoc --cpp_out=. contacts.proto
更新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;cin>>name;people->set_name(name);cout<<"请输入联系人年龄:";int age;cin>>age;people->set_age(age);cin.ignore(256,'\n');for(int i=0;;++i){cout<<"请输入联系人的电话"<<i+1<<"(只输入回车完成电话新增)";string number;getline(cin,number);if(number.empty())break;contacts2::PeopleInfo_Phone* phone = people->add_phone();phone->set_number(number);cout<<"请输入电话类型(1.移动电话 2.固定电话)";int type;cin>>type;cin.ignore(256,'\n');switch(type){case 1:phone->set_type(contacts2::PeopleInfo_Phone_PhoneType::PeopleInfo_Phone_PhoneType_MP);break;case 2:phone->set_type(contacts2::PeopleInfo_Phone_PhoneType::PeopleInfo_Phone_PhoneType_TEL);break;default:cout<<"选择有误"<<endl;}}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. wechat)";int other_contact;cin>>other_contact;cin.ignore(256,'\n');if(other_contact==1){cout<<"请输入QQ号:";string qq;getline(cin,qq);people->set_qq(qq);}else if(other_contact==2){cout<<"请输入微信号:";string wechat;getline(cin,wechat);people->set_wechat(wechat);}else{cout<<"选择有误,未成功设置其他联系方式";}for(int i=0;;++i){cout<<"请输入备注"<<i+1<<"标题";string remark_key;getline(cin,remark_key);if(remark_key.empty()){break;}cout<<"请输入备注"<<i+1<<"内容:";string remark_value;getline(cin,remark_value);people->mutable_remark()->insert({remark_key,remark_value});}cout<<"-----------添加联系人成功-----------"<<endl;
}int main()
{contacts2::Contacts contacts;// 0 .读取本地已存在的通讯录文件fstream input("contacts.bin", ios::in | ios::binary);if (!input){cerr << "contacts.bin not find, create new file!" << endl;}else if (!contacts.ParseFromIstream(&input)){cerr << "parse error!" << endl;input.close();exit(2);}// 1. 向通讯录中添加联系人AddPeopleInfo(contacts.add_contacts());// 2. 将通讯录写入本地文件中(这里使用覆盖写)fstream output("contacts.bin", ios::out | ios::binary | ios::trunc);if (!contacts.SerializeToOstream(&output)){cerr << "write error!" << endl;input.close();output.close();exit(2);}cout << "write success!" << endl;input.close();output.close();return 0;
}
更新read.cc
#include <iostream>
#include <fstream>
#include "contacts.pb.h"
using namespace std;void PrintContacts(contacts2::Contacts &contacts)
{for (int i = 0; i < contacts.contacts_size(); ++i){cout << "-----------联系人" << i + 1 << "-----------" << endl;const contacts2::PeopleInfo &people = contacts.contacts(i);cout << "联系人姓名:" << people.name() << endl;cout << "联系人年龄:" << people.age() << endl;for (int j = 0; j < people.phone_size(); ++j){const contacts2::PeopleInfo_Phone &phone = people.phone(j);cout << "电话" << j + 1 << ":" << phone.number();cout << "(" << phone.PhoneType_Name(phone.type()) << ")" << endl;}if (people.has_data() && people.data().Is<contacts2::Address>()){contacts2::Address address;people.data().UnpackTo(&address);if (!address.home_address().empty()){cout << "联系人家庭住址:" << address.home_address() << endl;}if (!address.unit_address().empty()){cout << "联系人单位地址:" << address.unit_address() << endl;}}switch(people.other_contact_case()){case contacts2::PeopleInfo::OtherContactCase::kQq:cout<<"联系人QQ号:"<<people.qq()<<endl;break;case contacts2::PeopleInfo::OtherContactCase::kWechat:cout<<"联系人微信号:"<<people.wechat()<<endl;break;default:break;}if(people.remark_size()){cout<<"备注信息:"<<endl;}for(auto it = people.remark().cbegin();it!=people.remark().cend();it++){cout<<" "<<(it->first)<<": "<<(it->second)<<endl;}}
}
int main()
{// 1. 读取本地文件中通讯录contacts2::Contacts contacts;fstream input("contacts.bin", ios::in | ios::binary);if (!input){cerr << "contacts.bin not find, create new file!" << endl;}else if (!contacts.ParseFromIstream(&input)){cerr << "parse error!" << endl;input.close();exit(2);}// 2. 打印通讯录列表PrintContacts(contacts);return 0;
}
默认值
反序列化消息时,如果被反序列化的二进制序列中不包含某个字段,反序列化对象中相应字段时,就会设置为该字段的默认值。不同的类型对应的默认值不同:
- 对于字符串,默认值是空字符串
- 对于字节,默认值是空字节
- 对于布尔值,默认值是false
- 对于数值类型,默认值是0
- 对于枚举类型,默认值是第一个定义的枚举值,必须为0
- 对于消息字段,未设置该字段,它的取值是依赖于语言
- 对于设置了repeated的字段的默认值是空的
- 对于消息字段、oneof字段按到any字段,C++和Java语言中都有has+方法来检测当前字段是否被设置
更新消息
更新规则
如果现有的消息类型已经不能满足我们的需求,例如需要扩展一个字段,在不破坏任何现有代码的情况下更新消息类型非常简单。通常如下规则即可:
-
禁止修改任何已有字段的字段编号
-
若是移除老字段,要保证不再使用移除字段的字段编号。正确的做法是保留字段编号,以确保该编号将不能被重复使用。不建议直接删除或注释掉字段。
-
int32,uint32,int64,uint64和bool是完全兼容的。可以从这些类型中的一个改成另一个,而不破坏前后兼容性。
-
sint32和sint64相互兼容但不与其他的整型兼容
-
string和bytes在合法UTF-8字节前提下也是兼容的
-
bytes包含的消息编码版本的情况下,嵌套消息和bytes也是兼容的
-
fixed32和sfixed32兼容,fixed64和sfixed64兼容
-
enum与int32,uint32,int64和uint64兼容。但要注意当反序列化消息时会根据语言采用不同的处理方案;例如,未识别的proto3枚举类型会被保存在消息中,但是当消息发序列化时如何表示是以来编程语言的。整型字段总是保持其的值。
-
oneof:
- 将一个单独的值更改为新的oneof类型成员之一是安全和二进制兼容的
- 若确定没有代码一次性设置多个值那么将多个字段移入一个新的oneof类型也是可行的
- 将任何字段移入已存在的oneof类型是不安全的
保留字段reserved
如果通过 删除 或 注释 字段来更新消息类型,未来的用户在添加新字段时,有可能会使用以前已经存在,但是已经被删除了的字段编号。将来使用该.proto的旧版本时就会引发很多问题
确保不会出现这种情况的一种方法就是:使用
reserved
将指定字段的编号或名称设置为保留项。当我们再使用这些编号或名称的时候,ProtoBuf的编译器将会警告这些编号或名称不可用。
更新通讯录
先模拟有两个服务,它们各自使用一份通讯录.proto文件,内容约定好是一模一样的。
服务1(service):负责序列化通讯录对象,并写入文件中
服务2(client):负责读取文件中的数据,解析并打印出来
⼀段时间后,service 更新了⾃⼰的 .proto ⽂件,更新内容为:删除了某个字段,并新增了⼀个字段,
新增的字段使⽤了被删除字段的字段编号。并将新的序列化对象写进了⽂件。
但 client 并没有更新⾃⼰的 .proto ⽂件。根据结论,可能会出现数据损坏的现象,接下来就让我们来验证下这个结论
service目录下新增contacts.proto
syntax = "proto3";
package s_contacts;
// 联系⼈
message PeopleInfo {string name = 1; // 姓名int32 age = 2; // 年龄message Phone {string number = 1; // 电话号码}repeated Phone phone = 3; // 电话
}
// 通讯录
message Contacts {repeated PeopleInfo contacts = 1;
}
client目录下新增contacts.proto
syntax = "proto3";
package c_contacts;
// 联系⼈
message PeopleInfo {string name = 1; // 姓名int32 age = 2; // 年龄message Phone {string number = 1; // 电话号码}repeated Phone phone = 3; // 电话
}
// 通讯录
message Contacts {
repeated PeopleInfo contacts = 1;
分别对两个文件进行编译.
service目录下新增service.cc,负责向文件中写入通讯录消息,内容如下
#include <iostream>
#include <fstream>
#include "contacts.pb.h"
using namespace std;
using namespace s_contacts;
/*** 新增联系⼈*/
void AddPeopleInfo(PeopleInfo *people_info_ptr)
{cout << "-------------新增联系⼈-------------" << endl;cout << "请输⼊联系⼈姓名: ";string name;getline(cin, name);people_info_ptr->set_name(name);cout << "请输⼊联系⼈年龄: ";int age;cin >> age;people_info_ptr->set_age(age);cin.ignore(256, '\n');for (int i = 1;; i++){cout << "请输⼊联系⼈电话" << i << "(只输⼊回⻋完成电话新增): ";string number;getline(cin, number);if (number.empty()){break;}PeopleInfo_Phone *phone = people_info_ptr->add_phone();phone->set_number(number);}cout << "-----------添加联系⼈成功-----------" << endl;
}
int main(int argc, char *argv[])
{GOOGLE_PROTOBUF_VERIFY_VERSION;if (argc != 2){cerr << "Usage: " << argv[0] << " CONTACTS_FILE" << endl;return -1;}Contacts contacts;// 先读取已存在的 contactsfstream input(argv[1], ios::in | ios::binary);if (!input){cout << argv[1] << ": File not found. Creating a new file." << endl;}else if (!contacts.ParseFromIstream(&input)){cerr << "Failed to parse contacts." << endl;input.close();return -1;}// 新增⼀个联系⼈AddPeopleInfo(contacts.add_contacts());// 向磁盘⽂件写⼊新的 contactsfstream output(argv[1], ios::out | ios::trunc | ios::binary);if (!contacts.SerializeToOstream(&output)){cerr << "Failed to write contacts." << endl;input.close();output.close();return -1;}input.close();output.close();google::protobuf::ShutdownProtobufLibrary();return 0;
}
client目录下新增client.cc,负责读出文件中的通讯录消息
#include <iostream>
#include <fstream>
#include "contacts.pb.h"
using namespace std;
using namespace c_contacts;
/*** 打印联系⼈列表*/
void PrintfContacts(const Contacts &contacts)
{for (int i = 0; i < contacts.contacts_size(); ++i){const PeopleInfo &people = contacts.contacts(i);cout << "------------联系⼈" << i + 1 << "------------" << endl;cout << "姓名:" << people.name() << endl;cout << "年龄:" << people.age() << endl;int j = 1;for (const PeopleInfo_Phone &phone : people.phone()){cout << "电话" << j++ << ": " << phone.number() << endl;}}
}
int main(int argc, char *argv[])
{GOOGLE_PROTOBUF_VERIFY_VERSION;if (argc != 2){cerr << "Usage: " << argv[0] << "CONTACTS_FILE" << endl;return -1;}// 以⼆进制⽅式读取 contactsContacts contacts;fstream input(argv[1], ios::in | ios::binary);if (!contacts.ParseFromIstream(&input)){cerr << "Failed to parse contacts." << endl;input.close();return -1;}// 打印 contactsPrintfContacts(contacts);input.close();google::protobuf::ShutdownProtobufLibrary();return 0;
}
自行尝试,发现无误后,对service目录下的contacts.proto文件进行更新:删除age字段,新增birthday字段,新增的字段使用被删除字段的字段编号
syntax = "proto3";
package s_contacts;// 联系⼈
message PeopleInfo {string name = 1; // 姓名// int32 age = 2; // 年龄int32 birthday = 2;message Phone {string number = 1; // 电话号码}repeated Phone phone = 3; // 电话
}// 通讯录
message Contacts {repeated PeopleInfo contacts = 1;
}
更新一下对应的service.cc
#include <iostream>
#include <fstream>
#include "contacts.pb.h"
using namespace std;
using namespace s_contacts;/*** 新增联系⼈ */
void AddPeopleInfo(PeopleInfo *people_info_ptr) {cout << "-------------新增联系⼈-------------" << endl;cout << "请输⼊联系⼈姓名: ";string name;getline(cin, name);people_info_ptr->set_name(name);// cout << "请输⼊联系⼈年龄: ";// int age;// cin >> age;// people_info_ptr->set_age(age);// cin.ignore(256, '\n'); cout << "请输⼊联系⼈生日: ";int birthday;cin >> birthday;people_info_ptr->set_birthday(birthday);cin.ignore(256, '\n'); for(int i = 1; ; i++) {cout << "请输⼊联系⼈电话" << i << "(只输⼊回⻋完成电话新增): ";string number;getline(cin, number);if (number.empty()) {break;}PeopleInfo_Phone* phone = people_info_ptr->add_phone();phone->set_number(number);}cout << "-----------添加联系⼈成功-----------" << endl;
}int main() {Contacts contacts; // 先读取已存在的 contactsfstream input("../contacts.bin", ios::in | ios::binary);if (!input) {cout << "contacts.bin not found. Creating a new file." << endl;} else if (!contacts.ParseFromIstream(&input)) {cerr << "Failed to parse contacts." << endl;input.close();return -1;}// 新增⼀个联系⼈ AddPeopleInfo(contacts.add_contacts());// 向磁盘⽂件写⼊新的 contactsfstream output("../contacts.bin", ios::out | ios::trunc | ios::binary);if (!contacts.SerializeToOstream(&output)) {cerr << "Failed to write contacts." << endl;input.close();output.close();return -1;}input.close();output.close();return 0;
}
运行后,发现,输入的生日,在反序列化的时候,被设置到了使用相同字段的年龄上!所以得出结论,要保证不再使用移除字段的字段编号,不建议直接删除或注释掉字段。
正确的的做法是保留字段编号,确保该编号将不能被重复使用
syntax = "proto3";
package s_contacts;// 联系⼈
message PeopleInfo {string name = 1; // 姓名// int32 age = 2; // 年龄int32 birthday = 4;message Phone {string number = 1; // 电话号码}repeated Phone phone = 3; // 电话
}// 通讯录
message Contacts {repeated PeopleInfo contacts = 1;
}
根据实验结果得出,由于新增时未设置年龄,通过client程序反序列化时,给年龄字段设置了默认值0。
未知字段
未知字段从哪里获取
了解相关类关系图
MessageLite类介绍
- MessageLite从名字看是轻量级的message,仅仅提供序列化和反序列化功能。
- 类定义在google提供的message_lite.h中
Message类介绍
-
我们自定义的message类,都是继承自Message
-
Message最重要的两个接口GetDescriptor/GetReflection,可以获取该类型对应的Descriptor对象指针和Reflection对象指针
-
类定义在google提供的message.h中
Descriptor类介绍
- Descriptor:是对message类型定义的描述,包括message的名字、所有字段的描述、原始的proto文件内容等
- 类定义在google提供的descriptor.h中
Reflection类介绍
- Reflection接口类,主要提供了动态读写消息字段的接口,对消息对象的自动读写主要通过该类完成
- 提供方法来供台访问/修改message中的字段,对每个类型,Reflection都踢动了一个单独的接口用于读写字段对应的值
- 针对所有不同的field类型FieldDescriptor::TYPE_*,需要使用不同的Get*()/Set*()/Add*()接口
- repeated类型需要使用GetRepeated*()/SetRepeated*()接口,不可以和非repeated类型接口混用
- message对象只可以由它自身的reflection来操作
- 类中还包含了访问、修改未知字段的方法
- 类定义在google提供的message.h中
UnknownFieldSet类介绍
- UnknownFieldSet包含在分析消息时遇到但未有类型定义的所有字段
- 若要将UnknownFiledSet附加到任何消息,请调用Reflection::GetUnknownFields()。
- 类定义在Unknown_field_set.h中。
升级通讯录
更新client.cc
#include <iostream>
#include <fstream>
#include "contacts.pb.h"
using namespace std;
using namespace c_contacts;
using namespace google::protobuf;/*** 打印联系⼈列表*/
void PrintfContacts(const Contacts& contacts) { for (int i = 0; i < contacts.contacts_size(); ++i) { const PeopleInfo& people = contacts.contacts(i);cout << "------------联系人" << i+1 << "------------" << endl;cout << "联系人姓名:" << people.name() << endl;cout << "联系人年龄:" << people.age() << endl;int j = 1;for (const PeopleInfo_Phone& phone : people.phone()) {cout << "联系人电话" << j++ << ": " << phone.number() << endl;}const Reflection* reflection = PeopleInfo::GetReflection();const UnknownFieldSet& set = reflection->GetUnknownFields(people);for (int j = 0; j < set.field_count(); j++) {const UnknownField& unknown_field = set.field(j);cout << "未知字段" << j+1 << ": "<< " 编号:" << unknown_field.number();switch(unknown_field.type()) {case UnknownField::Type::TYPE_VARINT:cout << " 值1:" << unknown_field.varint() << endl;break;case UnknownField::Type::TYPE_LENGTH_DELIMITED:cout << " 值2:" << unknown_field.length_delimited() << endl;break;// case ...}}}
}int main() {Contacts contacts;// 先读取已存在的 contactsfstream input("../contacts.bin", ios::in | ios::binary);if (!contacts.ParseFromIstream(&input)) {cerr << "Failed to parse contacts." << endl;input.close();return -1;}// 打印 contactsPrintfContacts(contacts);input.close();return 0;
}
前后兼容性
根据上述的例子可以得出,pb是具有向前兼容的。为了叙述方便,把增加了生日属性的service成为新模块,未做变动的client称为老模块
- 向前兼容:老模块能够正确识别新模块生成或发出的协议。这时新增加的生日属性会被当做未知字段。
- 向后兼容:新模块也能正确识别老模块生成或发出的协议
前后兼容的作用:当我们维护庞大的分布式系统的时候,由于你无法同时升级所有的模块,为了保证在升级的过程中,整个系统的尽可能不受影响,就需要尽量保证通讯协议的向后兼容和向前兼容。
选项option
.proto文件可以声明许多选项,使用oprtion标准。选项可以影响proto编译器的某些处理方式。
选项分类
选项的完整列表在google/protobuf/descriptor.proto中定义。
message FileOptions { ... } // ⽂件选项 定义在 FileOptions 消息中
message MessageOptions { ... } // 消息类型选项 定义在 MessageOptions 消息中
message FieldOptions { ... } // 消息字段选项 定义在 FieldOptions 消息中
message OneofOptions { ... } // oneof字段选项 定义在 OneofOptions 消息中
message EnumOptions { ... } // 枚举类型选项 定义在 EnumOptions 消息中
message EnumValueOptions { .. } // 枚举值选项 定义在 EnumValueOptions 消息中
message ServiceOptions { ... } // 服务选项 定义在 ServiceOptions 消息中
message MethodOptions { ... } // 服务⽅法选项 定义在 MethodOptions 消息中
常见选项列举
- optimize_for:该选项为文件选项,可以设置protoc编译器的优化级别,分别为SPEED,CODE_SIZE,LITE_RUNTIME。受该选项影响,设置不同的优化级别,编译.proto文件后生成的代码内容不同。
- SPEED:protoc编译器将生成的代码是高度优化的,代码运行效率高,但是生成的代码编译后湖占用更多的空间。SPEED是默认选项。
- CODE_SIZE:proto编译器将生成最少得类,占用更少的空间,是依赖基于反射的代码来实现序列化、反序列化和各种操作。但和SPEED恰恰相反,它的代码运行效率较低。这种方式适合用在包含大量的.proto文件,但不盲目追求速度的应用中。
- LITE_RUNTIME:生成的代码执行效率高,同时生成代码编译后所占用的空间也是非常少。这时以牺牲ProtoBuf提供的反射功能为代价的,仅仅提供了encoding和序列化功能所以在连接BP库时只需要链接libprotobuf-lite,而非libprotobuf。这种模式同时用于资源有限的平台
- allow_alias:允许相同的常量值分配给不同的枚举常量,用来定义别名。该选项为枚举选项。
通讯录实现–网络版
环境搭建
httplib库:一个开元的库,是⼀个c++封装的http库,使⽤这个库可以在linux、windows平台下完成http客⼾端、http服务端的搭建。使⽤起来⾮常⽅便,只需要包含头⽂件httplib.h 即可。编译程序时,需要带上 -lpthread 选项。
httplib
约定双端交互接口
client
#include <iostream>
#include <string>#include "httplib.h"
#include "ContactsException.h"
#include "add_contact.pb.h"using std::cin;
using std::cout;
using std::endl;
using namespace httplib;#define CONTACTS_HOST "xxx"
#define CONTACTS_POST xxxx
void menu()
{std::cout << "-----------------------------------------------------" << std::endl<< "--------------- 请选择对通讯录的操作 ----------------" << std::endl<< "------------------ 1、新增联系⼈ --------------------" << std::endl<< "------------------ 2、删除联系⼈ --------------------" << std::endl<< "------------------ 3、查看联系⼈列表 ----------------" << std::endl<< "------------------ 4、查看联系⼈详细信息 ------------" << std::endl<< "------------------ 0、退出 --------------------------" << std::endl<< "-----------------------------------------------------" << std::endl;
}
void buildAddContactRequest(add_contact::AddContactRequest* req)
{cout<<"-------------新增联系人-------------"<<endl;cout<<"请输入联系人姓名:";std::string name;cin>>name;req->set_name(name);cout<<"请输入联系人年龄:";int age;cin>>age;req->set_age(age);cin.ignore(256,'\n');for(int i=0;;++i){cout<<"请输入联系人的电话"<<i+1<<"(只输入回车完成电话新增)";std::string number;std::getline(cin,number);if(number.empty())break;add_contact::AddContactRequest_Phone* phone = req->add_phone();phone->set_number(number);cout<<"请输入电话类型(1.移动电话 2.固定电话)";int type;cin>>type;cin.ignore(256,'\n');switch(type){case 1:phone->set_type(add_contact::AddContactRequest_Phone_PhoneType::AddContactRequest_Phone_PhoneType_MP);break;case 2:phone->set_type(add_contact::AddContactRequest_Phone_PhoneType::AddContactRequest_Phone_PhoneType_TEL);break;default:cout<<"选择有误"<<endl;}}cout<<"-----------添加联系人成功-----------"<<endl;
}
void addContact()
{Client cli(CONTACTS_HOST,CONTACTS_POST);// 1. 构造reqadd_contact::AddContactRequest req;buildAddContactRequest(&req);// 2. 序列化reqstd::string req_str;if(!req.SerializeToString(&req_str)){throw(ContactsException("request 序列化失败"));}// 3. 发起post调用Result res = cli.Post("/contacts/add",req_str,"application/protobuf");if(!res){std::string err_desc="application/protobuf 链接失败!错误信息"+httplib::to_string(res.error());throw(ContactsException(err_desc));}// 4. 反序列化respadd_contact::AddContactResponse resp;bool parse = resp.ParseFromString(res->body);if(res->status!=200&&!parse){std::string err_desc="/contacts/add 调用失败"+std::to_string(res->status)+res->reason;throw(ContactsException(err_desc));}else if(res->status!=200){std::string err_desc="/contacts/add 调用失败"+std::to_string(res->status)+"错误原因:"+res->reason;throw(ContactsException(err_desc));}else if(!resp.success()){std::string err_desc="/contacts/add 结果异常,异常原因:"+resp.error_desc();throw(ContactsException(err_desc));}// 5. 结果打印cout<<"新增联系人ID成功,联系人ID:"<<resp.uid()<<endl;
}
int main()
{enum OPETION{QUIT = 0,ADD,DEL,FINDALL,FINDONE,};while (true){menu();cout << "--->请选择:";int choose;cin >> choose;cin.ignore(256, '\n');try{switch (choose){case OPETION::QUIT:cout << "--->程序退出" << endl;return 0;case OPETION::ADD:addContact();break;default:cout << "选择有误,请重新选择" << endl;break;}}catch (const ContactsException &e){cout << "--->操作通讯录时发生异常" << endl<< "--->异常信息为:" << e.what() << endl;}}return 0;
}// int main()
// {
// Client cli(CONTACTS_HOST,CONTACTS_POST);
// Result res1 = cli.Post("/test-post");
// if(res1->status==200)
// {
// cout<<"调用post成功"<<endl;
// }
// Result res2 = cli.Get("/test-get");
// if(res2->status==200)
// {
// cout<<"调用get方法成功"<<endl;
// }// return 0;
// }
service
#include <iostream>#include "httplib.h"
#include "add_contact.pb.h"
#include "ContactsException.h"using namespace std;
using namespace httplib;void printContact(add_contact::AddContactRequest &req)
{cout << "联系人姓名:" << req.name() << endl;cout << "联系人年龄:" << req.age() << endl;for (int j = 0; j < req.phone_size(); ++j){const add_contact::AddContactRequest_Phone &phone = req.phone(j);cout << "联系人电话" << j + 1 << ":" << phone.number();cout << "(" << phone.PhoneType_Name(phone.type()) << ")" << endl;}
}
static unsigned int random_char()
{std::random_device rd;std::mt19937 gen(rd());std::uniform_int_distribution<>dis(0,255);return dis(gen);
}
static std::string generate_hex(const unsigned int len)
{std::stringstream ss;for (auto i = 0; i < len; ++i){const auto rc = random_char();std::stringstream hexstream;hexstream << std::hex << rc;auto hex = hexstream.str();ss << (hex.length() < 2 ? '0' + hex : hex);}return ss.str();
}
int main()
{cout << "-------------服务启动-------------" << endl;Server server;server.Post("/contacts/add", [](const Request &req, Response &res){cout << "接收到post请求!" << endl;// 反序列化 request:req.bodyadd_contact::AddContactRequest request;add_contact::AddContactResponse response;try{if (!request.ParseFromString(req.body)){throw(ContactsException("AddContactRequest 反序列化失败"));}// 新增联系人,持久化存储 (打印)printContact(request);// 构造一个responseresponse.set_success(true);response.set_uid(generate_hex(10));//res.bodystring response_str;if(!response.SerializeToString(&response_str)){throw(ContactsException("AddContactResponse 序列化失败"));}res.status=200;res.body=response_str;res.set_header("Content-Type","application/protobuf");}catch(const ContactsException& e){res.status=500;response.set_success(false);response.set_error_desc(e.what());string response_str;if(response.SerializeToString(&response_str)){res.body = response_str;res.set_header("Content-Type","application/protobuf");}cout<<"/contacts/add发生异常,异常信息:"<<e.what()<<endl;} });server.Get("/test-get", [](const Request &req, Response &res){cout<<"接收到get请求!"<<endl;res.status=200; });// 绑定8123端口,并且将端口号对外开放server.listen("0.0.0.0", 8123);return 0;
}
// int main()
// {
// cout << "-------------服务启动-------------" << endl;
// Server server;
// server.Post("/test-post", [](const Request &req, Response &res){
// cout<<"接收到post请求!"<<endl;
// res.status=200; });
// server.Get("/test-get", [](const Request &req, Response &res){
// cout<<"接收到get请求!"<<endl;
// res.status=200; });
// // 绑定8123端口,并且将端口号对外开放
// server.listen("0.0.0.0", 8123);
// return 0;
// }
ContactsException.h
#pragma once
#include <string>
class ContactsException
{
private:std::string message;
public:ContactsException(std::string str = "A problem"):message(str){}std::string what()const{return message;}
};
总结
序列化协议 | 通用性 | 格式 | 可读性 | 序列化大小 | 序列化性能 | 使用场景 |
---|---|---|---|---|---|---|
JSON | 通用 | 文本格式 | 好 | 轻量(使用键值对方式,压缩了一定的数据空间) | 中 | web项⽬。因为浏览 |
XML | 通⽤ | 文本格式 | 好 | 重量(数据冗余,因为需要成对的闭合标签) | 低 | XML 作为⼀种扩展标记语言,衍生出了HTML,RDF、RDFS,他强调数据结构化的能力和可读性 |
ProtoBuf | 独⽴(Protobuf只是Google公司内部的⼯具) | 二进制 | 差(只能反序列化后得到真正可读的数据) | 轻量(⽐JSON更轻量,传输起来带宽和速度会有优化) | 高 | 适合⾼性能,对响应速度有要求的数据传输场景。Protobuf⽐XML、JSON 更⼩、更快。 |
string response_str;if(response.SerializeToString(&response_str)){res.body = response_str;res.set_header("Content-Type","application/protobuf");}cout<<"/contacts/add发生异常,异常信息:"<<e.what()<<endl;} });
server.Get("/test-get", [](const Request &req, Response &res){cout<<"接收到get请求!"<<endl;res.status=200; });
// 绑定8123端口,并且将端口号对外开放
server.listen("0.0.0.0", 8123);
return 0;
}
// int main()
// {
// cout << “-------------服务启动-------------” << endl;
// Server server;
// server.Post(“/test-post”, [](const Request &req, Response &res){
// cout<<“接收到post请求!”<<endl;
// res.status=200; });
// server.Get(“/test-get”, [](const Request &req, Response &res){
// cout<<“接收到get请求!”<<endl;
// res.status=200; });
// // 绑定8123端口,并且将端口号对外开放
// server.listen(“0.0.0.0”, 8123);
// return 0;
// }
**ContactsException.h**
#pragma once
#include
class ContactsException
{
private:
std::string message;
public:
ContactsException(std::string str = “A problem”):
message(str)
{}
std::string what()const
{
return message;
}
};
# 总结| 序列化协议 | 通用性 | 格式 | 可读性 | 序列化大小 | 序列化性能 | 使用场景 |
| ---------- | ---------------------------------------- | -------- | -------------------------------------- | ------------------------------------------------ | ---------- | ------------------------------------------------------------ |
| JSON | 通用 | 文本格式 | 好 | 轻量(使用键值对方式,压缩了一定的数据空间) | 中 | web项⽬。因为浏览 |
| XML | 通⽤ | 文本格式 | 好 | 重量(数据冗余,因为需要成对的闭合标签) | 低 | XML 作为⼀种扩展标记语言,衍生出了HTML,RDF、RDFS,他强调数据结构化的能力和可读性 |
| ProtoBuf | 独⽴(Protobuf只是Google公司内部的⼯具) | 二进制 | 差(只能反序列化后得到真正可读的数据) | 轻量(⽐JSON更轻量,传输起来带宽和速度会有优化) | 高 | 适合⾼性能,对响应速度有要求的数据传输场景。Protobuf⽐XML、JSON 更⼩、更快。 |
变长编码:经过protobuf编码之后,原来4或8字节的数可能会被变为其他字节数 ↩︎ ↩︎ ↩︎ ↩︎ ↩︎