目录
- [proto 3 语法详解](#proto 3 语法详解)
- 字段规则
- 消息类型的定义与使用
- 创建通讯录2.0版本
- enum类型
- 升级通讯录至2.1版本
- Any类型
- 升级通讯录至2.2版本
- oneof类型
- 升级通讯录至2.3版本
- map类型
- 升级通讯录至2.4版本
- 默认值
- 更新消息
- 保留字段reserved
- 创建通讯录3.0版本
- 未知字段
- 升级通讯录3.1版本
- 前后兼容性
- 选项option
proto 3 语法详解
在语法详解部分, 依旧使用 项目推进 的方式完成教学, 这个部分会对通讯录进行多次升级, 使用 2.x表示升级的版本, 最终版将会升级为如下内容:
- 不再打印联系人的序列化结果, 而是将通讯录序列化后并写入文件中.
- 从文件中将通讯录解析出来, 并进行打印
- 新增联系人属性, 共包括: 姓名, 年龄, 电话信息, 地址, 其他联系方式, 备注.
字段规则
除了常规字段, 我们能否还可以给字段设置其他规则, 比如我想要一个数组类型的字段.
- singular: 消息中可以包含该字段零次或一次, proto3语法中, 字段默认使用该规则
- repeated: 消息中可以包含该字段任意多次(包括0次), 其中重复值的顺序会被保留. 可以理解为定义了一个数组.
更新contacts.proto,
PeopleInfo
消息中新增phone
字段, 表示一个联系人有多个号码, 可将其设置为repeated, 写法如下:
消息类型的定义与使用
1.在单个.proto文件中可以定义多个消息体 , 且支持定义嵌套类型的消息(任意多层) , 每个消息体中的字段编号可以重复
2.消息类型可作为字段类型使用
3.可导入其他.proto文件的消息并使用
创建通讯录2.0版本
下面我们来实现通讯录2.0版本, 向文件中写入通讯录列表
进行编译生成
再次查看生成的.h文件
write.cc通讯录2.0的写入实现
#include <iostream>
#include <fstream>
#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;
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;
// 读取本地已存在的通讯录文件
fstream input("contacts.bin", ios::in | ios::binary);
if (!input.is_open()) {
cout << "contacts.bin not find, create new file!" << endl;
} else if (!contacts.ParseFromIstream(&input)) {
cerr << "parse error!" << endl;
input.close();
return -1;
}
// 向通讯录中添加一个联系人
AddPeopleInfo(contacts.add_contacts());
// 将通讯录写入本地文件中
fstream output("contacts.bin", ios::out | ios::trunc | ios::binary);
if (!contacts.SerializeToOstream(&output)) {
cerr << "write error!" << endl;
input.close();
output.close();
return -1;
}
cout << "write success!" << endl;
input.close();
output.close();
return 0;
}
运行:
介绍在Linux查看二进制文件的工具: hexdump: 可以将二进制文件进行转换, 转换为对应的ASCII值, 以及八进制, 十进制, 十六进制的格式.
read.cc通讯录2.0的读取实现
#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()
{
contacts2::Contacts contacts;
// 读取本地已存在的通讯录文件
fstream input("contacts.bin", ios::in | ios::binary);
if (!contacts.ParseFromIstream(&input)) {
cerr << "parse error!" << endl;
input.close();
return -1;
}
// 打印通讯录列表
PrintContacts(contacts);
return 0;
}
运行:
我们也可以从标准输入中读取给定类型的二进制消息, 可以将其内容以文本的格式输出到标准输出上.
有什么用呢? 我们之前存的contacts.bin存的是二进制序列, 想要看的话就需要写一个read.cc, 再通过打印的方式来查看内容是什么. 我们现在可以使用--decode命令, 使用一行命令就可以进行二进制消息的打印.
enum类型
proto3语法还支持我们定义枚举类型的字段, 在.proto文件中枚举类型的书写规范为:
枚举类型名称: 使用驼峰命名法, 首字母大写. 例如: MyEnum
常量值名称: 全大写字母, 多个字母之间用 _ 连接. 例如: ENUM_CONST = 0;
我们可以定义一个PhoneType的枚举类型, 定义如下:
要注意枚举的定义有以下几种规则:
1.0值常量必须存在, 且要作为第一个元素 . 这是为了与proto2的语义兼容: 第一个元素作为默认值, 且值为0.
2.枚举类型可以在消息外定义, 也可以在消息体内定义(嵌套) .
3.枚举的常量值在32位整数的范围内 . 但因负值无效因而不建议使用(与编码规则有关).
4.同级的枚举类型, 如果有相同的常量名称, 是会报错的 .
什么是同级(同层)的枚举类型?
单个.proto文件下, 最外层枚举类型和嵌套枚举类型, 不算同级.
不会编译报错
多个.proto文件下, 若一个文件引入了其他文件, 且每个文件都未声明package, 每个proto文件中的枚举类型都在最外层, 算同级
多个.proto文件下, 若一个文件引入了其他文件, 且每个文件都声明了package, 不算同级.
此时就可以正常生成了
升级通讯录至2.1版本
更新contacts.proto, 新增枚举字段并使用, 更新内容如下:
进行编译
查看对应.h文件
更新write.cc(通讯录2.1)
#include <iostream>
#include <fstream>
#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;
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;
break;
}
}
cout << "-----------添加联系人成功-----------------" << endl;
}
int main()
{
contacts2::Contacts contacts;
// 读取本地已存在的通讯录文件
fstream input("contacts.bin", ios::in | ios::binary);
if (!input.is_open()) {
cout << "contacts.bin not find, create new file!" << endl;
} else if (!contacts.ParseFromIstream(&input)) {
cerr << "parse error!" << endl;
input.close();
return -1;
}
// 向通讯录中添加一个联系人
AddPeopleInfo(contacts.add_contacts());
// 将通讯录写入本地文件中
fstream output("contacts.bin", ios::out | ios::trunc | ios::binary);
if (!contacts.SerializeToOstream(&output)) {
cerr << "write error!" << endl;
input.close();
output.close();
return -1;
}
cout << "write success!" << endl;
input.close();
output.close();
return 0;
}
更新read.cc(通讯录2.1)
#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()
{
contacts2::Contacts contacts;
// 读取本地已存在的通讯录文件
fstream input("contacts.bin", ios::in | ios::binary);
if (!contacts.ParseFromIstream(&input)) {
cerr << "parse error!" << endl;
input.close();
return -1;
}
// 打印通讯录列表
PrintContacts(contacts);
return 0;
}
运行结果:
Any类型
proto3语法还支持我们定义Any的字段, 可以理解为泛型类型, 使用时可以在Any中存储任意消息类型. Any类型的字段也可以用repeated来修饰.
Any类型是google已经帮我们定义好的类型, 在安装Protobuf时, 其中的include目录下已经定义好了.proto文件.
进行cat查看该文件
都是我们熟悉的语法, 说明我们要学的Any就是一个message消息体
升级通讯录至2.2版本
通讯录2.2版本会新增联系人的地址信息, 我们可以使用any类型的字段来存储地址信息.
更新contacts.proto(通讯录2.2), 更新内容如下:
进行编译
查看生成的.h文件
如何将我们的any类型去存放通讯录的地址信息?
打开any.proto文件编译后的Any.h文件
更新write.cc(通讯录2.2)
#include <iostream>
#include <fstream>
#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;
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;
break;
}
}
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);
people->mutable_data()->PackFrom(address);
cout << "-----------添加联系人成功-----------------" << endl;
}
int main()
{
contacts2::Contacts contacts;
// 读取本地已存在的通讯录文件
fstream input("contacts.bin", ios::in | ios::binary);
if (!input.is_open()) {
cout << "contacts.bin not find, create new file!" << endl;
} else if (!contacts.ParseFromIstream(&input)) {
cerr << "parse error!" << endl;
input.close();
return -1;
}
// 向通讯录中添加一个联系人
AddPeopleInfo(contacts.add_contacts());
// 将通讯录写入本地文件中
fstream output("contacts.bin", ios::out | ios::trunc | ios::binary);
if (!contacts.SerializeToOstream(&output)) {
cerr << "write error!" << endl;
input.close();
output.close();
return -1;
}
cout << "write success!" << endl;
input.close();
output.close();
return 0;
}
更新read.cc(通讯录2.2)
#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()
{
contacts2::Contacts contacts;
// 读取本地已存在的通讯录文件
fstream input("contacts.bin", ios::in | ios::binary);
if (!contacts.ParseFromIstream(&input)) {
cerr << "parse error!" << endl;
input.close();
return -1;
}
// 打印通讯录列表
PrintContacts(contacts);
return 0;
}
编译运行:
为什么用Any类型, 而不用void* 类型代替呢? 如果大家经历过, 记忆应该能更深刻一点.不知道你们有没有调试过崩溃过的程序, 调试出来了就是因为使用了类型转换错误的, 所以没有类型检查, 是很不安全的事. 当然原因不止这一点, 还有一些原因(可以AI搜一下), 但我讲的这一点是比较主要的原因.
oneof类型
如果消息中有很多可选字段, 并且将来同时只有一个字段会被设置, 那么就可以使用oneof加强这个行为, 也能有节约内存的效果.
升级通讯录至2.3版本
通讯录2.3版本想新增联系人的其他联系方式, 比如qq或者微信号二选一, 我们就可以使用oneof字段来加强多选一这个行为.
更新contacts.proto, 更新内容如下:
注意:
1.可选字段中的字段编号, 不能与非可选字段的编号冲突.
2.不能在oneof中使用repeated字段.
3.将来在设置oneof字段的时候, 如果你set了多个字段, 那么只会保留最后一次set的成员.
进行编译后查看.h文件
更新write.cc, 更内容如下:
#include <iostream>
#include <fstream>
#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;
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;
break;
}
}
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);
people->mutable_data()->PackFrom(address);
cout << "请选择要添加的其他联系方式(1. qq 2. 微信号):";
int other_contact;
cin >> other_contact;
cin.ignore(256, '\n');
if (1 == other_contact) {
cout << "请输入联系人qq号: ";
string qq;
getline(cin, qq);
people->set_qq(qq);
} else if (2 == other_contact) {
cout << "请输入联系人微信号: ";
string wechat;
getline(cin, wechat);
people->set_wechat(wechat);
} else {
cout << "选择有误, 未成功设置其他联系方式! " << endl;
}
cout << "-----------添加联系人成功-----------------" << endl;
}
int main()
{
contacts2::Contacts contacts;
// 读取本地已存在的通讯录文件
fstream input("contacts.bin", ios::in | ios::binary);
if (!input.is_open()) {
cout << "contacts.bin not find, create new file!" << endl;
} else if (!contacts.ParseFromIstream(&input)) {
cerr << "parse error!" << endl;
input.close();
return -1;
}
// 向通讯录中添加一个联系人
AddPeopleInfo(contacts.add_contacts());
// 将通讯录写入本地文件中
fstream output("contacts.bin", ios::out | ios::trunc | ios::binary);
if (!contacts.SerializeToOstream(&output)) {
cerr << "write error!" << endl;
input.close();
output.close();
return -1;
}
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()
{
contacts2::Contacts contacts;
// 读取本地已存在的通讯录文件
fstream input("contacts.bin", ios::in | ios::binary);
if (!contacts.ParseFromIstream(&input)) {
cerr << "parse error!" << endl;
input.close();
return -1;
}
// 打印通讯录列表
PrintContacts(contacts);
return 0;
}
编译运行结果:
map类型
语法支持创建一个关联映射字段, 也就是可以使用map类型去声明字段类型
要注意的是:
1.key_type是除了float和bytes类型以外的任意标量类型, value_type可以是任意类型.
2.map字段不可以用repeated修饰
3.map中存入的元素是无序的
升级通讯录至2.4版本
通讯录2.4版本想新增联系人的备注信息, 我们可以使用map类型字段来存储备注信息
更新contacts.proto, 更新内容如下:
进行编译后查看.h文件
更新write.cc, 更新内容如下:
#include <iostream>
#include <fstream>
#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;
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;
break;
}
}
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);
people->mutable_data()->PackFrom(address);
cout << "请选择要添加的其他联系方式(1. qq 2. 微信号):";
int other_contact;
cin >> other_contact;
cin.ignore(256, '\n');
if (1 == other_contact) {
cout << "请输入联系人qq号: ";
string qq;
getline(cin, qq);
people->set_qq(qq);
} else if (2 == other_contact) {
cout << "请输入联系人微信号: ";
string wechat;
getline(cin, wechat);
people->set_wechat(wechat);
} else {
cout << "选择有误, 未成功设置其他联系方式! " << endl;
}
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;
// 读取本地已存在的通讯录文件
fstream input("contacts.bin", ios::in | ios::binary);
if (!input.is_open()) {
cout << "contacts.bin not find, create new file!" << endl;
} else if (!contacts.ParseFromIstream(&input)) {
cerr << "parse error!" << endl;
input.close();
return -1;
}
// 向通讯录中添加一个联系人
AddPeopleInfo(contacts.add_contacts());
// 将通讯录写入本地文件中
fstream output("contacts.bin", ios::out | ios::trunc | ios::binary);
if (!contacts.SerializeToOstream(&output)) {
cerr << "write error!" << endl;
input.close();
output.close();
return -1;
}
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()
{
contacts2::Contacts contacts;
// 读取本地已存在的通讯录文件
fstream input("contacts.bin", ios::in | ios::binary);
if (!contacts.ParseFromIstream(&input)) {
cerr << "parse error!" << endl;
input.close();
return -1;
}
// 打印通讯录列表
PrintContacts(contacts);
return 0;
}
编译运行结果:
至此, 我们2.4版本就正式完成了所有的需求.
默认值
反序列化消息时, 如果被反序列化的二进制序列中不包含某个字段, 反序列化对象中相应字段时, 就会设置为该字段的默认值. 不同的类型对应的默认值不同:
- 对于字符串, 默认值为空字符串
- 对于字节, 默认值为空字节
- 对于布尔值, 默认值为false
- 对于数值类型, 默认值为0
- 对于枚举, 默认值是第一个定义的枚举值, 必须为0
- 对于消息字段, 如果未设置该字段, 它的取值是依赖于语言
- 对于设置了repeated的字段的默认值是空的(通常是相应语言的一个空列表)
- 对于消息字段, oneof字段和any字段, C++和java语言中都有hash_方法来检测当前字段是否被设置.
更新消息
反序列化消息时, 如果现有的消息类型已经不再满足我们的需求, 例如需要扩展一个字段, 在不破坏任何现有代码的情况下更新消息类型非常简单. 遵循如下规则即可:
- 精致修改任何已有字段的字段编号.
- 若是移除老字段, 要保证不再使用移除字段的字段编号. 正确的做法是保留字段编号(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类型是不安全的。
保留字段reserved
如果通过 删除 或 注释掉 字段来更新消息类型, 未来的用户在添加新字段时, 有可能会使用以前已经存在, 但已经被删除或注释掉的字段编号, 将来使用该.proto的旧版本时的程序会引发很多问题: 数据损坏, 隐私错误等等.
确保不会发生这种情况的一种方法是: 使用reserved将指定字段的编号或名称设置为保留项. 当我们再使用这些编号或名称时, protobuffer的编译器将会警告这些编号或名称不可用. 举个例子:
message Message {
// 设置保留项
reserved 100, 101, 200 to 299;
reserved "field3", "field4";
// 注意:不要在⼀⾏ reserved 声明中同时声明字段编号和名称。
// reserved 102, "field5";
// 设置保留项之后,下⾯代码会告警
int32 field1 = 100; //告警:Field 'field1' uses reserved number 100
int32 field2 = 101; //告警:Field 'field2' uses reserved number 101
int32 field3 = 102; //告警:Field name 'field3' is reserved
int32 field4 = 103; //告警:Field name 'field4' is reserved
}
创建通讯录3.0版本
验证 错误字段 造成的数据损坏
现模拟有两个服务, 他们各自使用一份通讯录.proto文件, 内容约定好了是一模一样的. 唯一不同的是package的命名上
服务1: service 负责序列化通讯录对象, 并写入文件中.
服务2: client: 负责读取文件中的数据, 解析并打印出来.
一段时间后, service更新了自己的.proto文件, 更新了某个字段, 并新增了一个字段, 新增的字段使用了被删除字段的字段编号, 并将新的序列化对象写进了文件, 但Client并没有更新自己的.proto文件. 根据结论, 可能会出现数据损坏的现象, 接下来就让我们来验证下这个结论.
新增两个目录: service, client. 分别存放两个服务代码.
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;//: 验证没有意外链接到与编译的头⽂件不兼容的库版
//本。如果检测到版本不匹配,程序将中⽌。注意,每个 .pb.cc ⽂件在启动时都会⾃动调⽤此宏。在使
//⽤ C++ Protocol Buffer 库之前执⾏此宏是⼀种很好的做法,但不是绝对必要的。
if (argc != 2)
{
cerr << "Usage: " << argv[0] << " CONTACTS_FILE" << endl;
return -1;
}
Contacts contacts;
// 先读取已存在的 contacts
fstream 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());
// 向磁盘⽂件写⼊新的 contacts
fstream 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;
}
service ⽬录下新增 makefile
service:service.cc contacts.pb.cc
g++ -o $@ $^ -std=c++11 -lprotobuf
.PHONY:clean
clean:
rm -f service
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;//: 验证没有意外链接到与编译的头⽂件不兼容的库版
//本。如果检测到版本不匹配,程序将中⽌。注意,每个 .pb.cc ⽂件在启动时都会⾃动调⽤此宏。在使
//⽤ C++ Protocol Buffer 库之前执⾏此宏是⼀种很好的做法,但不是绝对必要的。
if (argc != 2) {
cerr << "Usage: " << argv[0] << "CONTACTS_FILE" << endl;
return -1;
}
// 以⼆进制⽅式读取 contacts
Contacts contacts;
fstream input(argv[1], ios::in | ios::binary);
if (!contacts.ParseFromIstream(&input)) {
cerr << "Failed to parse contacts." << endl;
input.close();
return -1;
}
// 打印 contacts
PrintfContacts(contacts);
input.close();
google::protobuf::ShutdownProtobufLibrary();
return 0;
}
client ⽬录下新增 makefile
client:client.cc contacts.pb.cc
g++ -o $@ $^ -std=c++11 -lprotobuf
.PHONY:clean
clean:
rm -f client
编译运行结果:
接下来对service目录下的contacts.proto文件进行更新: 删除age字段, 新增birthday字段, 新增的字段使用被删除字段的字段编号.
更新后的contacts.proto内容如下:
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;
}
编译文件.proto后, 还需要更新一下对应的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(int argc, char *argv[])
{
GOOGLE_PROTOBUF_VERIFY_VERSION;//: 验证没有意外链接到与编译的头⽂件不兼容的库版
//本。如果检测到版本不匹配,程序将中⽌。注意,每个 .pb.cc ⽂件在启动时都会⾃动调⽤此宏。在使
//⽤ C++ Protocol Buffer 库之前执⾏此宏是⼀种很好的做法,但不是绝对必要的。
if (argc != 2)
{
cerr << "Usage: " << argv[0] << " CONTACTS_FILE" << endl;
return -1;
}
Contacts contacts;
// 先读取已存在的 contacts
fstream 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());
// 向磁盘⽂件写⼊新的 contacts
fstream 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;
}
编译运行:
这时问题便出现了, 我们发现输入的生日, 在反序列化时, 被设置到了使用了相同字段编号的年龄上, 所以得出结论: 若是移除老字段, 要保证不再使用移除字段的字段编号, 不建议直接删除或注释掉字段, 如果直接删除或注释掉, 则会导致后面的开发人员不知道这个字段是否被删除 , 那么正确的做法是保留字段编号(reserved), 以确保该编号将不能被重复使用
正确service目录下的contacts.proto写法如下:
syntax ="proto3";
package s_contacts;
//联系人
message PeopleInfo {
reserved 2;
string name = 1; //姓名
// 删除年龄字段
// int32 age = 2; //年龄
int32 birthday = 4; //生日
message Phone{
string number = 1; //电话号码
}
repeated Phone phone = 3; //电话
}
//通讯录
message Contacts {
repeated PeopleInfo contacts = 1;
}
不要忘记更新完contacts.proto要进行编译并重新编译程序, 运行结果:
发现tianqi的年龄为0, 这是由于新增时未设置年龄, 通过client程序反序列化时, 给年龄字段设置了默认值0.
根据以上例子, 有的同学可能还有一个疑问: 如果使用了reserved 2了, 那么service给tianqi设置的生日1210, client就没法读到了吗? 答案是可以的, 继续学习下面的位置字段即可揭晓答案.
未知字段
在通讯录3.0版本中, 我们想service目录下的contacts.proto新增了'生日'字段, 但对于client的代码并没有任何改动, 新增的'生日'字段在旧程序(client)中其实并没有丢失, 而是会作为旧程序的未知字段
本来, proto3在解析消息时总是会丢弃未知字段, 但在3.5版本中重新引入了对未知字段的保留机制. 所以在3.5或更高版本中, 未知字段在反序列化时会被保留, 同时也会包含在序列化的结果中.
未知字段和自定义字段之间的关系图:
升级通讯录3.1版本
验证未知字段
更新client.cc, 在这个版本中, 需要打印出未知字段的内容. 更新的代码如下:
#include <iostream>
#include <fstream>
#include <google/protobuf/unknown_field_set.h>
#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& unknowSet = reflection->GetUnknownFields(people);
for (int j = 0; j < unknowSet.field_count(); ++j) {
const UnknownField& unknow_field = unknowSet.field(j);
cout << "未知字段" << j + 1 << ":"
<< " 字段编号: " << unknow_field.number()
<< " 类型:" << unknow_field.type();
switch (unknow_field.type()) {
case UnknownField::Type::TYPE_VARINT:
cout << " 值: " << unknow_field.varint() << endl;
break;
case UnknownField::Type::TYPE_LENGTH_DELIMITED:
cout << " 值: " << unknow_field.length_delimited() << endl;
break;
}
}
}
}
int main(int argc, char* argv[]) {
GOOGLE_PROTOBUF_VERIFY_VERSION;//: 验证没有意外链接到与编译的头⽂件不兼容的库版
//本。如果检测到版本不匹配,程序将中⽌。注意,每个 .pb.cc ⽂件在启动时都会⾃动调⽤此宏。在使
//⽤ C++ Protocol Buffer 库之前执⾏此宏是⼀种很好的做法,但不是绝对必要的。
if (argc != 2) {
cerr << "Usage: " << argv[0] << "CONTACTS_FILE" << endl;
return -1;
}
// 以⼆进制⽅式读取 contacts
Contacts contacts;
fstream input(argv[1], ios::in | ios::binary);
if (!contacts.ParseFromIstream(&input)) {
cerr << "Failed to parse contacts." << endl;
input.close();
return -1;
}
// 打印 contacts
PrintfContacts(contacts);
input.close();
google::protobuf::ShutdownProtobufLibrary();
return 0;
}
编译运行结果:
前后兼容性
根据上述的例子可以得出, pb是具有向前兼容的, 为了叙述方便, 把新增了 "生日" 属性的service称为 "新模块"; 未做变动的client称为 "老模块"
- 向前兼容: 老模块能够正确识别新模块生成或发出的协议. 这时, 新增加的 "生日" 属性会被当做位置字段(pb 3.5版本及之后).
- 向后兼容: 新模块也能够正确识别老模块生成或发出的协议.
前后兼容的作用: 当我们维护一个很庞大的分布式系统时, 由于你无法同时升级所有模块, 为了保证在升级过程中, 整个系统能够尽可能不受影响, 就需要保证通讯协议的 "向前兼容" 或 "向后兼容"
选项option
.proto文件中可以声明许多选项, 使用 option标注. 选项能影响proto编译器的某些处理方式
举个例子:
进行编译
查看.h文件
自定义类型继承的Message
我们加上这个option选项
进行编译, 查看.h文件
自定义类型继承的MessageLite, 这是什么意思呢? 请继续看后面
option分为文件级, 消息级, 字段级 等等, 但并没有一种选项能作用于所有的类型
常用选项列举
optimize_for : 该选项为文件选项, 可以设置protoc编译器的优化级别, 分别为SPEED, CODE_SIZE, LITE_RUNTIME, 受该选项的影响, 设置不同的优化级别, 编译.proto文件后生成的代码内容不同
- SPEED: protoc编译器将生成的代码是高度优化的, 代码运行效率高 , 但是由此生成的代码编译后会占用更多的空间 , SPEED是默认选项, 所以我们之前写的代码都是SPEED级别的
- CODE_SIZE: proto编译器将生成最少的类, 会占用更少的空间 , 但和SPEED恰恰相反, 它的代码运行效率较低 , 这种方式适用在包含大量的.proto文件, 但并不盲目追求速度的应用中.
- LITE_RUNTIME: 生成的代码执行效率高 , 同时生成代码编译后的所占用空间也是非常少 , 这是以牺牲protobuf提供的反射功能为代价的(之前的图片可以看到, 自定义类是先继承message类, 再继承messagelite类. 所以这就没有message类所提供的能力, 比如说reflection, description), 仅仅提供encoding + 序列化功能, 所以我们在连接BP库时仅需链接libprotobuf-lite, 而非libprotobuf, 这种模式通常用于资源有限的平台, 例如移动手机平台中
allow_alias: 允许将相同的常量值分配给不同的枚举常量, 用来定义别名, 该选项为枚举选项.举个例子:
ProtoBuf允许自定义选项并使用, 该功能大部分场景用不到, 在这里不扩展讲解.