protobuf的Proto3语法学习 (二)

文章目录

    • 五、proto3语法详解
    • 1. 字段规则
    • 2. 消息类型的定义与使⽤
      • 2.1 定义
      • 2.2 使⽤
      • 2.3 创建通讯录2.0版本
        • 2.3.1 通讯录2.0的写⼊实现
        • 2.3.2 通讯录2.0的读取实现
        • 2.3.3 验证写入是否正确
    • 3. enum类型
      • 3.1 定义规则
      • 3.2 定义enum类型的注意事项
      • 3.3 升级通讯录⾄2.1版本
    • 4. Any类型
      • 4.1 升级通讯录⾄2.2版本
    • 5. oneof类型
      • 5.1 升级通讯录⾄2.3版本
      • 5.2 oneof 模拟实现
    • 6. map类型
      • 6.1升级通讯录⾄2.4版本
    • 7. 默认值
    • 8. 更新消息
      • 8.1 更新规则
      • 8.2 保留字段reserved
        • 8.2.1 创建通讯录3.0版本---验证错误删除字段造成的数据损坏
      • 8.3 未知字段
        • 8.3.1 未知字段从哪获取
        • 8.3.2 升级通讯录3.1版本---验证未知字段
      • 8.4 前后兼容性
    • 9. 选项option
      • 9.1 选项分类
      • 9.2选项的基本用法
      • 9.2 常⽤选项列举

在序列化存储时,read的message文件可以参照write的message编写。

五、proto3语法详解

在语法详解部分,依旧使⽤项⽬推进的⽅式完成教学。这个部分会对通讯录进⾏多次升级,使⽤2.x
表⽰升级的版本,最终将会升级如下内容:

  • 不再打印联系⼈的序列化结果,⽽是将通讯录序列化后并写⼊⽂件中。
  • 从⽂件中将通讯录解析出来,并进⾏打印。
  • 新增联系⼈属性,共包括:姓名、年龄、电话信息、地址、其他联系⽅式、备注。

1. 字段规则

消息的字段可以⽤下⾯⼏种规则来修饰:

  • singular:消息中可以包含该字段零次或⼀次(不超过⼀次)。proto3语法中,字段默认使⽤该
    规则。
  • repeated:消息中可以包含该字段任意多次(包括零次),其中重复值的顺序会被保留。可以理
    解为定义了⼀个数组。

更新contacts.proto, PeopleInfo 消息中新增 phone_numbers 字段,表⽰⼀个联系⼈有多个号码,可将其设置为repeated,写法如下:

syntax = "proto3";
package contacts;
	message PeopleInfo {
	string name = 1;
	int32 age = 2;
	repeated string phone_numbers = 3;// 一个人有1个及以上的手机号码
}

2. 消息类型的定义与使⽤

2.1 定义

在单个.proto⽂件中可以定义多个消息体,且⽀持定义嵌套类型的消息(任意多层)。每个消息体中
的字段编号可以重复。
更新contacts.proto,我们可以将phone_number提取出来,单独成为⼀个消息:

// -------------------------- 嵌套写法 -------------------------
syntax = "proto3";
package contacts;
message PeopleInfo {
	string name = 1;
	int32 age = 2;
	message Phone {
		string number = 1;
	}
	}
// -------------------------- ⾮嵌套写-------------------------
	syntax = "proto3";
	package contacts;
	message Phone {
		string number = 1;
	}
	message PeopleInfo {
		string name = 1;
		int32 age = 2;
}

2.2 使⽤

  • 消息类型可作为字段类型使⽤

contacts.proto

syntax = "proto3";
package contacts;

// 联系⼈
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 contacts;

import "phone.proto"; // 使⽤ importphone.proto ⽂件导⼊进来 !!!

message PeopleInfo {
	string name = 1;
	int32 age = 2;
	
	// 引⼊的⽂件声明了package,使⽤消息时,需要⽤ ‘命名空间.消息类型’ 格式
	
	repeated phone.Phone phone = 3;
}

注:在proto3⽂件中可以导⼊proto2消息类型并使⽤它们,反之亦然。

2.3 创建通讯录2.0版本

通讯录2.x的需求是向⽂件中写⼊通讯录列表,以上我们只是定义了⼀个联系⼈的消息,并不能存放通
讯录列表,所以还需要在完善⼀下contacts.proto(终版通讯录2.0):

syntax = "proto3";
package contacts2;

//import "phone.proto"

message PeopleInfo{
  string name = 1 ; //姓名
  int32 age =2 ; // 年龄
  message Phone{
    string number = 1;
  }
  repeated Phone phone = 3;//电话号码
}
// 通讯录
message Contacts{
  repeated PeopleInfo contacts = 1 ;
}

接着进⾏⼀次编译:

protoc --cpp_out=. contacts.proto

编译后⽣成的 contacts.pb.h contacts.pb.cc 会将在快速上⼿的⽣成⽂件覆盖掉。

contacts.pb.h更新的部分代码展⽰:

// 新增了 PeopleInfo_Phone 类
class PeopleInfo_Phone final : public ::PROTOBUF_NAMESPACE_ID::Message
{
public:
  using ::PROTOBUF_NAMESPACE_ID::Message::CopyFrom;
  void CopyFrom(const PeopleInfo_Phone &from);
  using ::PROTOBUF_NAMESPACE_ID::Message::MergeFrom;
  void MergeFrom(const PeopleInfo_Phone &from)
  {
    PeopleInfo_Phone::MergeImpl(*this, from);
  }
  static ::PROTOBUF_NAMESPACE_ID::StringPiece FullMessageName()
  {
    return "PeopleInfo.Phone";
  }
  // string number = 1;
  void clear_number();
  const std::string &number() const;
  template <typename ArgT0 = const std::string &, typename... ArgT>
  void set_number(ArgT0 &&arg0, ArgT... args);
  std::string *mutable_number();
  PROTOBUF_NODISCARD std::string *release_number();
  void set_allocated_number(std::string *number);
};
// 更新了 PeopleInfo 类
class PeopleInfo final : public ::PROTOBUF_NAMESPACE_ID::Message
{
public:
  using ::PROTOBUF_NAMESPACE_ID::Message::CopyFrom;

  void CopyFrom(const PeopleInfo &from);
  using ::PROTOBUF_NAMESPACE_ID::Message::MergeFrom;
  void MergeFrom(const PeopleInfo &from)
  {
    PeopleInfo::MergeImpl(*this, from);
  }
  static ::PROTOBUF_NAMESPACE_ID::StringPiece FullMessageName()
  {
    return "PeopleInfo";
  }
  typedef PeopleInfo_Phone Phone;
  // repeated .PeopleInfo.Phone phone = 3;
  int phone_size() const;
  void clear_phone();
  ::PeopleInfo_Phone *mutable_phone(int index);
  ::PROTOBUF_NAMESPACE_ID::RepeatedPtrField<::PeopleInfo_Phone> *
  mutable_phone();
  const ::PeopleInfo_Phone &phone(int index) const;
  ::PeopleInfo_Phone *add_phone();
  const ::PROTOBUF_NAMESPACE_ID::RepeatedPtrField<::PeopleInfo_Phone> &
  phone() const;
};
// 新增了 Contacts 类
class Contacts final : public ::PROTOBUF_NAMESPACE_ID::Message
{
public:
  using ::PROTOBUF_NAMESPACE_ID::Message::CopyFrom;
  void CopyFrom(const Contacts &from);
  using ::PROTOBUF_NAMESPACE_ID::Message::MergeFrom;
  void MergeFrom(const Contacts &from)
  {
    Contacts::MergeImpl(*this, from);
  }
  static ::PROTOBUF_NAMESPACE_ID::StringPiece FullMessageName()
  {
    return "Contacts";
  }
  // repeated .PeopleInfo contacts = 1;
  int contacts_size() const;
  void clear_contacts();
  ::PeopleInfo *mutable_contacts(int index);//返回array[index]
  ::PROTOBUF_NAMESPACE_ID::RepeatedPtrField<::PeopleInfo> *
  mutable_contacts();
  const ::PeopleInfo &contacts(int index) const;
  ::PeopleInfo *add_contacts();
  const ::PROTOBUF_NAMESPACE_ID::RepeatedPtrField<::PeopleInfo> &
  contacts() const;
};

上述的例⼦中:

  • 每个字段都有⼀个clear_⽅法,可以将字段重新设置回empty状态。

  • 每个字段都有设置和获取的⽅法,获取⽅法的⽅法名称与⼩写字段名称完全相同。但如果是消息类型的字段,其设置⽅法为mutable_⽅法,返回值为消息类型的指针其实就是数组的元素地址,这类⽅法会为我们开辟好空间,可以直接对这块空间的内容进⾏修改。

  • 对于使⽤repeated修饰的字段,也就是数组类型,pb为我们提供了add_数组名⽅法来新增⼀个值,add_返回的类型可能是新开辟的数组元素地址todo。
    并且提供了数组名_size⽅法来判断数组存放元素的个数。

  • 数组名(index):这样的函数用来获取数组指定元素.

2.3.1 通讯录2.0的写⼊实现

write.cc(通讯录2.0)

#include
#include
#include
#include
using namespace std;
void addPersonInfo(contacts2::Contacts& contacts);
int main()
{

  contacts2::Contacts contacts;
  // 将contacts.bin 文件的数据进行反序列化
  std::ifstream input ("contacts.bin", std::fstream::in | std::fstream::out);
  if(!input){
    cerr<<"contacts.bin 不存在该文件\n";
    return -1;
  }else if(!contacts.ParseFromIstream(&input)){
    cerr<<"contacts.bin 文件序列化失败\n";
    input.close();
    return -1;
  }
  // 添加一个联系人信息
  addPersonInfo(contacts);
  // 序列化通讯录写会到contacts.bin文件里
  ofstream out("contacts.bin",std::ios::out|std::ios::binary);
  if(!out){
    cerr<<"contacts.bin文件打开失败\n";
    return-1;
  }else if(!contacts.SerializePartialToOstream(&out)){
    cerr << "contacts对象序列化失败\n";
    out.close();
    return -1;
  }
  input.close();
  out.close();
  return 0;
}

void addPersonInfo(contacts2::Contacts& contacts)
{
  contacts2::PeopleInfo* p = contacts.add_contacts();
  if(!p){
    cerr << "添加联系人失败\n";
    return ;
  }
  cout<< "------------添加联系人-----------\n";
  cout<< "------------请输出姓名-----------\n";
  string name ;
  getline(cin,name);
  p->set_name(name);
  cout<< "------------请输出年龄-----------\n";
  int age;
  cin>>age;
  p->set_age(age);
  cin.ignore(256,'\n');// 在256个字符前,如果遇到'\n'就跳过'\n'并停下来,忽略256个字符后还没遇到'\n'就停下来.

  int i=1;

  while(true)
  {
    string number;
     cout<< "------------请输出手机号码"<<i<<"---------\n";
     getline(cin,number);
     if(number.empty()){
      cout<<"手机号码读取成功\n";
      break;
     }
    contacts2::PeopleInfo_Phone* phone = p->add_phone();
    phone->set_number(number);
    i++;
  }
}

makefile

all:write read
write:write.cpp contacts.pb.cc
	g++ -o $@ $^ -std=c++11 -lprotobuf 

read:read.cpp contacts.pb.cc
	g++ -o $@ $^ -std=c++11 -lprotobuf 
.PHONY:clean

clean:
	rm -rf write read

make之后,运⾏write

如下,添加了两个联系人信息.

[YYK@VM-8-7-centos proto3]$ ./write
------------添加联系人-----------
------------请输出姓名-----------
yyk
------------请输出年龄-----------
10
------------请输出手机号码1---------
123
------------请输出手机号码2---------

手机号码读取成功

[YYK@VM-8-7-centos proto3]$ ./write
------------添加联系人-----------
------------请输出姓名-----------
bbq
------------请输出年龄-----------
12
------------请输出手机号码1---------
8989
------------请输出手机号码2---------
9999
------------请输出手机号码3---------

手机号码读取成功

查看二进制文件

用来验证写入的数据是否正确, 还可以使用 指令:protoc --decode=contacts2.Contacts contacts.proto < contacts.bin

[YYK@VM-8-7-centos proto3]$ hexdump -C contacts.bin
00000000  0a 0e 0a 03 79 79 6b 10  0a 1a 05 0a 03 31 32 33  |....yyk......123|
00000010  0a 17 0a 03 62 62 71 10  0c 1a 06 0a 04 38 39 38  |....bbq......898|
00000020  39 1a 06 0a 04 39 39 39  39                       |9....9999|
00000029

解释:
	`hexdump`:是Linux下的⼀个⼆进制⽂件查看⼯具,它可以将⼆进制⽂件转换为ASCII、⼋进制、
⼗进制、⼗六进制格式进⾏查看。
	`-C`: 表⽰每个字节显⽰为16进制和相应的ASCII字符

2.3.2 通讯录2.0的读取实现

read.cc(通讯录2.0)

#include 
#include 
#include 
#include 
using namespace std;
void printContacts(const contacts2::Contacts& contacts);
int main()
{
  contacts2::Contacts contacts;
  // 将contacts.bin 文件的数据进行反序列化
  std::ifstream input("contacts.bin", std::fstream::in | std::fstream::out);
  if (!input)
  {
    cerr << "contacts.bin 不存在该文件\n";
    return -1;
  }
  else if (!contacts.ParseFromIstream(&input))
  {
    cerr << "contacts.bin 文件序列化失败\n";
    input.close();
    return -1;
  }
  // 打印contacts
  printContacts(contacts);

  return 0;
}

void printContacts(const contacts2::Contacts& contacts)
{
    for(int i=0;i<contacts.contacts_size();i++)
    {
      const contacts2::PeopleInfo& people = contacts.contacts(i);
      cout<<"----------联系人"<<i<<"----------"<<endl;
      cout<<"联系人姓名:"<<people.name()<<endl;
      cout<<"联系人年龄:"<<people.age()<<endl;
      for(int j=0;j<people.phone_size();j++)
      {
        cout<<"联系手机"<<j+1<<":"<<people.phone(j).number()<<endl;
      }
    }
}

make后运⾏read

[YYK@VM-8-7-centos proto3]$ ./read
----------联系人0----------
联系人姓名:yyk
联系人年龄:10
联系手机1:123
----------联系人1----------
联系人姓名:bbq
联系人年龄:12
联系手机1:8989
联系手机2:9999
2.3.3 验证写入是否正确

方法:

  • 1.通过如上2.3.2 进行打印 – 实现起来麻烦.
  • 2.另⼀种验证⽅法–decode

我们可以⽤ protoc -h 命令来查看ProtoBuf为我们提供的所有命令option。其中ProtoBuf提供
⼀个命令选项 --decode ,表⽰从标准输⼊中读取给定类型的⼆进制消息,并将其以⽂本格式写⼊标准输出。消息类型必须在.proto⽂件或导⼊的⽂件中定义。

如何使用:

[YYK@VM-8-7-centos proto3]$ protoc --decode=contacts2.Contacts contacts.proto < contacts.bin
contacts {
  name: "yyk"
  age: 10
  phone {
    number: "123"
  }
}
contacts {
  name: "bbq"
  age: 12
  phone {
    number: "8989"
  }
  phone {
    number: "9999"
  }
}

指令解释:

  • 如下图指令,
  • protoc : 编译器
  • –decode=contact2.Contacts : 要打印的message,需要带上package
  • contacts.proto : 具体在那个.proto文件里
  • < contacts.bin : protoc编译器需要从标准输入流读取二进制文件

在这里插入图片描述

3. enum类型

3.1 定义规则

语法⽀持我们定义枚举类型并使⽤。在.proto⽂件中枚举类型的书写规范为

命名规则:

  • .proto文件里 message ,enum 命名定义都是 驼峰
  • enum的常量 命名规范是全大写,多个字母使用 ‘_’ 隔开 .

我们可以定义⼀个名为PhoneType的枚举类型,定义如下

enum PhoneType{
	MP = 0 ; //移动电话
	TEL = 1; // 固定电话
}

要注意枚举类型的定义有以下⼏种规则

  • 1.0值常量必须存在,且要作为第⼀个元素。这是为了与proto2的语义兼容:第⼀个元素作为默认
    值,且值为0。
  • 2.枚举类型可以在消息外定义,也可以在消息体内定义(嵌套)。
  • 3.枚举的常量值在32位整数的范围内。但因负值⽆效因⽽不建议使⽤(与编码规则有关)。

3.2 定义enum类型的注意事项

如下代码:

  • 定义了两个具有’相同枚举值名称’ 的 enum , 在编译时会erro:MP已经被使用.
  • PhoneType和PhoneTypeCopy在同一个包里.
  • proto语法规定:同级(同层)的枚举类型,各个枚举类型中的常量不能重名。
  • 解决方法:
    1. 使常量名不相同.
    1. 放到不同的包里.
  • 注意事项:
    1. 在import 导入时,也会出现同层 “相同枚举值名称” 的问题.
// ---------------------- 情况1:同级枚举类型包含相同枚举值名称--------------------
package=Phone

enum PhoneType {
MP = 0; // 移动电话
TEL = 1; // 固定电话
}
enum PhoneTypeCopy {
MP = 0; // 移动电话 // 编译后报错:MP 已经定义
}

// ------------------------解决方法 -----------------------------------
// phone1.proto
==============================================
syntax = "proto3";

package phone1;

import "phone2.proto";

enum PhoneType{  
  MP = 0; // 移动电话
  TEL = 1; // 固定电话
}
==============================================
// phone2.proto
=================================
syntax = "proto3";
package phone;

enum PhoneTypeCopy {
  MP = 0; // 移动电话 // 编译后报错:MP 已经定义
}

==============================================

3.3 升级通讯录⾄2.1版本

write.cc(通讯录2.1)

#include 
#include 
#include 
#include 
using namespace std;
void addPersonInfo(contacts2::Contacts &contacts);
int main()
{

  contacts2::Contacts contacts;
  // 将contacts.bin 文件的数据进行反序列化
  std::ifstream input("contacts.bin", std::fstream::in | std::fstream::out);
  if (!input)
  {
    cerr << "contacts.bin 不存在该文件\n";
    return -1;
  }
  // else if (!contacts.ParseFromIstream(&input))
  // {
  //   cerr << "contacts.bin 文件序列化失败\n";
  //   input.close();
  //   return -1;
  // }
  // 添加一个联系人信息
  addPersonInfo(contacts);
  // 序列化通讯录写会到contacts.bin文件里
  ofstream out("contacts.bin", std::ios::out | std::ios::binary|std::ios::app);
  if (!out)
  {
    cerr << "contacts.bin文件打开失败\n";
    return -1;
  }
  else if (!contacts.SerializePartialToOstream(&out))
  {
    cerr << "contacts对象序列化失败\n";
    out.close();
    return -1;
  }
  input.close();
  out.close();
  return 0;
}

void addPersonInfo(contacts2::Contacts &contacts)
{
  contacts2::PeopleInfo *p = contacts.add_contacts();
  if (!p)
  {
    cerr << "添加联系人失败\n";
    return;
  }
  cout << "------------添加联系人-----------\n";
  cout << "------------请输出姓名-----------\n";
  string name;
  getline(cin, name);
  p->set_name(name);
  cout << "------------请输出年龄-----------\n";
  int age;
  cin >> age;
  p->set_age(age);
  cin.ignore(256, '\n'); // 在256个字符前,如果遇到'\n'就跳过'\n'并停下来,忽略256个字符后还没遇到'\n'就停下来.

  int i = 1;

  while (true)
  {
    string number;
    cout << "------------请输出手机号码" << i << "---------\n";
    getline(cin, number);
    if (number.empty())
    {
      cout << "手机号码读取成功\n";
      break;
    }
    contacts2::PeopleInfo_Phone *phone = p->add_phone();
    phone->set_number(number);
    i++;
    cout << "------------请输入手机号码类型(MP:1 or TEL:2)"
         << "---------\n";
    int type = 0;
    cin >> type;
    cin.ignore(256,'\n');
    switch (type)
    {
    case 1:
    cout<<"type:"<<type<<endl;
      phone->set_type(contacts2::PeopleInfo_Phone_PhoneType::PeopleInfo_Phone_PhoneType_MP);
      break;
    case 2:
        cout<<"type:"<<type<<endl;
      phone->set_type(contacts2::PeopleInfo_Phone_PhoneType::PeopleInfo_Phone_PhoneType_TEL);
      break;
    default:
      cout << "输入手机号码类型有误" << endl;
      break;
    }
  }
}

read.cc(通讯录2.1)

#include 
#include 
#include 
#include 
using namespace std;
void printContacts(const contacts2::Contacts& contacts);
int main()
{
  contacts2::Contacts contacts;
  // 将contacts.bin 文件的数据进行反序列化
  std::ifstream input("contacts.bin", std::fstream::in | std::fstream::out);
  if (!input)
  {
    cerr << "contacts.bin 不存在该文件\n";
    return -1;
  }
  else if (!contacts.ParseFromIstream(&input))
  {
    cerr << "contacts.bin 文件序列化失败\n";
    input.close();
    return -1;
  }
  // 打印contacts
  printContacts(contacts);

  return 0;
}

void printContacts(const contacts2::Contacts& contacts)
{
    for(int i=0;i<contacts.contacts_size();i++)
    {
      const contacts2::PeopleInfo& people = contacts.contacts(i);
      cout<<"----------联系人"<<i<<"----------"<<endl;
      cout<<"联系人姓名:"<<people.name()<<endl;
      cout<<"联系人年龄:"<<people.age()<<endl;
      for(int j=0;j<people.phone_size();j++)
      {
        cout<<"联系手机"<<j+1<<":"<<people.phone(j).number();
        cout<< "    type:("<<people.phone(j).PhoneType_Name(people.phone(j).type())<<")"<<endl;
      }
    }
}

contacts.proto (通讯录2.1)

	syntax = "proto3";
	package contacts2;
	
	//import "phone.proto"
	
	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 Contacts{
	  repeated PeopleInfo contacts = 1 ;
	}

运行结果

[YYK@VM-8-7-centos proto3_2.1_]$ make
g++ -o write write.cpp contacts.pb.cc -std=c++11 -lprotobuf 
[YYK@VM-8-7-centos proto3_2.1_]$ ./write 
------------添加联系人-----------
------------请输出姓名-----------
uu
------------请输出年龄-----------
20
------------请输出手机号码1---------
87654
------------请输入手机号码类型(MP:1 or TEL:2)---------
2
type:2
------------请输出手机号码2---------

手机号码读取成功
[YYK@VM-8-7-centos proto3_2.1_]$ ./read
----------联系人0----------
联系人姓名:yyk
联系人年龄:29
联系手机1:890    type:(MP)
----------联系人1----------
联系人姓名:hh
联系人年龄:19
联系手机1:88899903    type:(TEL)
----------联系人2----------
联系人姓名:uu
联系人年龄:20
联系手机1:87654    type:(TEL)

4. Any类型

字段还可以声明为Any类型,可以理解为泛型类型。使⽤时可以在Any中存储任意消息类型。Any类
型的字段也⽤repeated来修饰。

Any类型是google已经帮我们定义好的类型,在安装ProtoBuf时,其中的include⽬录下查找所有
google已经定义好的.proto⽂件。

/usr/local/protobuf/include/google/protobuf/any.proto 文件

message Any {
  string type_url = 1; 
  bytes value = 2;
}

这是Protocol Buffers中的一个消息类型定义。该Any消息类型用于表示在编译时未知的消息类型,并允许将其包含在其他消息中。具体来说:

  • type_url字段是一个字符串,用于指定序列化后的消息类型URL,例如"proto://example.com/MyMessage"。
  • value字段是一个字节串,用于存储序列化后的消息数据。

使用Any类型可以使消息更加灵活和可扩展,但需要确保各方都能够正确解析和处理任意类型的消息

4.1 升级通讯录⾄2.2版本

通讯录2.2版本会新增联系⼈的地址信息,我们可以使⽤any类型的字段来存储地址信息。

更新contacts.proto(通讯录2.2),更新内容如下:

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

contacts.pb.h更新的部分代码展⽰:

// 新⽣成的 Address 类
class Address final : public ::PROTOBUF_NAMESPACE_ID::Message {
public:
	using ::PROTOBUF_NAMESPACE_ID::Message::CopyFrom;
	void CopyFrom(const Address& from);
	using ::PROTOBUF_NAMESPACE_ID::Message::MergeFrom;
	void MergeFrom( const Address& from) {
	Address::MergeImpl(*this, from);
	}
	// string home_address = 1;
	void clear_home_address();
	const std::string& home_address() const;
	template <typename ArgT0 = const std::string&, typename... ArgT>
	void set_home_address(ArgT0&& arg0, ArgT... args);
	std::string* mutable_home_address();
	PROTOBUF_NODISCARD std::string* release_home_address();
	void set_allocated_home_address(std::string* home_address);
	// string unit_address = 2;
	void clear_unit_address();
	const std::string& unit_address() const;
	template <typename ArgT0 = const std::string&, typename... ArgT>
	void set_unit_address(ArgT0&& arg0, ArgT... args);
	std::string* mutable_unit_address();
	PROTOBUF_NODISCARD std::string* release_unit_address();
	void set_allocated_unit_address(std::string* unit_address);
};


// 更新的 PeopleInfo 类
class PeopleInfo final : public ::PROTOBUF_NAMESPACE_ID::Message {
public:
	// .google.protobuf.Any data = 4;
	bool has_data() const;
	void clear_data();
	const ::PROTOBUF_NAMESPACE_ID::Any& data() const;
	PROTOBUF_NODISCARD ::PROTOBUF_NAMESPACE_ID::Any* release_data();
	::PROTOBUF_NAMESPACE_ID::Any* mutable_data();
	void set_allocated_data(::PROTOBUF_NAMESPACE_ID::Any* data);
};

上述的代码中,对于Any类型字段:

  • 设置和获取:获取⽅法的⽅法名称与⼩写字段名称完全相同。
  • 设置⽅法可以使⽤mutable_⽅法,返回值为Any类型的指针,这类⽅法会为我们开辟好空间,可以直接对这块空间的内容进⾏修改。

之前讲过,我们可以在Any字段中存储任意消息类型,这就要涉及到任意消息类型和Any类型的互转。这部分代码就在Google为我们写好的头⽂件 any.pb.h 中。对any.pb.h 部分代码展⽰:

class PROTOBUF_EXPORT Any final : public ::PROTOBUF_NAMESPACE_ID::Message {
bool PackFrom(const ::PROTOBUF_NAMESPACE_ID::Message& message) {
...
}
bool UnpackTo(::PROTOBUF_NAMESPACE_ID::Message* message) const {
...
}
template<typename T> bool Is() const {
return _impl_._any_metadata_.Is<T>();
}
};
解释:
	使⽤ PackFrom() ⽅法可以将任意消息类型转为 Any 类型。
	使⽤ UnpackTo() ⽅法可以将 Any 类型转回之前设置的任意消息类型。
	使⽤ Is<T>() ⽅法可以⽤来判断存放的消息类型是否为 typename T.

Any类包含:

class Any
{
	string type_url; // 存储的类型名
	vector<bits> value; // 使用字节的存储数据
	
	packfrom();
	uppackfrom();
	Is<T>();
}

如何理解Any:

  • packfrom() : 将任意message对象转换到 Any对象里. 例如message 对象为class student , type_url = “student” , 将student对象存储到value里.
  • uppackfrom() : 将Any对象 转换为 type_url对象,.
  • Is< student > : 判断Any对象是否存放为student类型数据类型. 具体实现应该就是 if(type_url == student.type_name() );

更新write.cc(通讯录2.2)

#include 
#include 
#include 
#include 
using namespace std;
void addPersonInfo(contacts2::Contacts &contacts);
int main()
{

  contacts2::Contacts contacts;
  // 将contacts.bin 文件的数据进行反序列化
  std::ifstream input("contacts.bin", std::fstream::in | std::fstream::out);
  if (!input)
  {
    cerr << "contacts.bin 不存在该文件\n";
    return -1;
  }
  // else if (!contacts.ParseFromIstream(&input))
  // {
  //   cerr << "contacts.bin 文件序列化失败\n";
  //   input.close();
  //   return -1;
  // }
  // 添加一个联系人信息
  addPersonInfo(contacts);
  // 序列化通讯录写会到contacts.bin文件里
  ofstream out("contacts.bin", std::ios::out | std::ios::binary|std::ios::app);
  if (!out)
  {
    cerr << "contacts.bin文件打开失败\n";
    return -1;
  }
  else if (!contacts.SerializePartialToOstream(&out))
  {
    cerr << "contacts对象序列化失败\n";
    out.close();
    return -1;
  }
  input.close();
  out.close();
  return 0;
}

void addPersonInfo(contacts2::Contacts &contacts)
{
  contacts2::PeopleInfo *p = contacts.add_contacts();
  if (!p)
  {
    cerr << "添加联系人失败\n";
    return;
  }
  cout << "------------添加联系人-----------\n";
  cout << "------------请输出姓名-----------\n";
  string name;
  getline(cin, name);
  p->set_name(name);
  cout << "------------请输出年龄-----------\n";
  int age;
  cin >> age;
  p->set_age(age);
  cin.ignore(256, '\n'); // 在256个字符前,如果遇到'\n'就跳过'\n'并停下来,忽略256个字符后还没遇到'\n'就停下来.

  int i = 1;

  while (true)
  {
    string number;
    cout << "------------请输出手机号码" << i << "---------\n";
    getline(cin, number);
    if (number.empty())
    {
      cout << "手机号码读取成功\n";
      break;
    }
    contacts2::PeopleInfo_Phone *phone = p->add_phone();
    phone->set_number(number);
    i++;
    cout << "------------请输入手机号码类型(MP:1 or TEL:2)"
         << "---------\n";
    int type = 0;
    cin >> type;
    cin.ignore(256,'\n');
    switch (type)
    {
    case 1:
    cout<<"type:"<<type<<endl;
      phone->set_type(contacts2::PeopleInfo_Phone_PhoneType::PeopleInfo_Phone_PhoneType_MP);
      break;
    case 2:
        cout<<"type:"<<type<<endl;
      phone->set_type(contacts2::PeopleInfo_Phone_PhoneType::PeopleInfo_Phone_PhoneType_TEL);
      break;
    default:
      cout << "输入手机号码类型有误" << endl;
      break;
    }
  }

  contacts2::Address address;
  cout<<"请输入联系人家庭地址"<<endl;
  string home_address;
  getline(cin,home_address);
  address.set_home_address(home_address);
  cout<<"请输入联系人单位地址"<<endl;
  string unit_address;
  getline(cin,unit_address);
  address.set_unit_address(unit_address);
  google::protobuf::Any* data = p->mutable_data();
  data->PackFrom(address);
  cout<<"添加联系人成功"<<endl;
}

更新read.cc(通讯录2.2)

#include 
#include 
#include 
#include 
using namespace std;
void printContacts(const contacts2::Contacts &contacts);
int main()
{
  contacts2::Contacts contacts;
  // 将contacts.bin 文件的数据进行反序列化
  std::ifstream input("contacts.bin", std::fstream::in | std::fstream::out);
  if (!input)
  {
    cerr << "contacts.bin 不存在该文件\n";
    return -1;
  }
  else if (!contacts.ParseFromIstream(&input))
  {
    cerr << "contacts.bin 文件序列化失败\n";
    input.close();
    return -1;
  }
  // 打印contacts
  printContacts(contacts);

  return 0;
}

void printContacts(const contacts2::Contacts &contacts)
{
  for (int i = 0; i < contacts.contacts_size(); i++)
  {
    const contacts2::PeopleInfo &people = contacts.contacts(i);
    cout << "----------联系人" << i << "----------" << endl;
    cout << "联系人姓名:" << people.name() << endl;
    cout << "联系人年龄:" << people.age() << endl;
    for (int j = 0; j < people.phone_size(); j++)
    {
      cout << "联系手机" << j + 1 << ":" << people.phone(j).number();
      cout << "    type:(" << people.phone(j).PhoneType_Name(people.phone(j).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;
      }
    }
  }
}

运行 write read

[YYK@VM-8-7-centos proto3_2.2_]$ ./write
------------添加联系人-----------
------------请输出姓名-----------
Ben
------------请输出年龄-----------
30
------------请输出手机号码1---------
19999999999
------------请输入手机号码类型(MP:1 or TEL:2)---------
2
type:2
------------请输出手机号码2---------

手机号码读取成功
请输入联系人家庭地址
china
请输入联系人单位地址
china
添加联系人成功
bash: __vsc_prompt_cmd_original: command not found
[YYK@VM-8-7-centos proto3_2.2_]$ ./read
----------联系人0----------
联系人姓名:yyk
联系人年龄:29
联系手机1:890    type:(MP)
----------联系人1----------

5. oneof类型

如果消息中有很多可选字段,并且将来同时只有⼀个字段会被设置,那么就可以使⽤ oneof 加强这个⾏为,也能有节约内存的效果。

5.1 升级通讯录⾄2.3版本

通讯录2.3版本想新增联系⼈的其他联系⽅式,⽐如qq或者微信号⼆选⼀,我们就可以使⽤oneof字
段来加强多选⼀这个⾏为。oneof字段定义的格式为: oneof 字段名 { 字段1; 字段2; … }
更新contacts.proto(通讯录2.3),更新内容如下:

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{
    string qq=5;
    string wechat=6;
    //uint32 work_number = 7;
  };
}
// 通讯录
message Contacts{
  repeated PeopleInfo contacts = 1 ;
}

注意:

  • 可选字段中的字段编号,不能与⾮可选字段的编号冲突。
  • 不能在oneof中使⽤repeated字段。
  • 将来在设置oneof字段中值时,如果将oneof中的字段设置多个,那么只会保留最后⼀次设置的成员,之前设置的oneof成员会⾃动清除.

编译 protoc–cpp_out=.contacts.proto

contacts.pb.h更新的部分代码展⽰:

// 更新的 PeopleInfo 类
class PeopleInfo final : public ::PROTOBUF_NAMESPACE_ID::Message {
	enum OtherContactCase {
		kQq = 5,
		kWeixin = 6,
		OTHER_CONTACT_NOT_SET = 0,
	};
	// string qq = 5;
	bool has_qq() const;
	void clear_qq();
	const std::string& qq() const;
	template <typename ArgT0 = const std::string&, typename... ArgT>
	void set_qq(ArgT0&& arg0, ArgT... args);
	std::string* mutable_qq();
	PROTOBUF_NODISCARD std::string* release_qq();
	void set_allocated_qq(std::string* qq);
	
	
	// string weixin = 6;
	bool has_weixin() const;
	void clear_weixin();
	const std::string& weixin() const;
	template <typename ArgT0 = const std::string&, typename... ArgT>
	void set_weixin(ArgT0&& arg0, ArgT... args);
	std::string* mutable_weixin();
	PROTOBUF_NODISCARD std::string* release_weixin();
	void set_allocated_weixin(std::string* weixin);
	void clear_other_contact();
	OtherContactCase other_contact_case() const;
};

上述的代码中,对于oneof字段:

  • 会将oneof中的多个字段定义为⼀个枚举类型,来标识具体字段.
  • 在Protocol Buffers中,oneof实现的基本原理是使用联合体(union)来表示不同字段的内存布局。生成的C++代码中,对于每个oneof定义的字段,都会生成一个相应的枚举类型和一个联合体。
  • 设置和获取:对oneof内的字段进⾏常规的设置和获取即可,但要注意只能设置⼀个。如果设置多个,那么只会保留最后⼀次设置的成员。
  • clear_ : 清空oneof字段。
  • other_contact_case(): 获取当前设置了哪个字段。

更新write.cc(通讯录2.3),更新内容如下:

#include 
#include 
#include 
#include 
using namespace std;
void addPersonInfo(contacts2::Contacts &contacts);
int main()
{

  contacts2::Contacts contacts;
  // 将contacts.bin 文件的数据进行反序列化
  std::ifstream input("contacts.bin", std::fstream::in | std::fstream::out);
  if (!input)
  {
    cerr << "contacts.bin 不存在该文件\n";
    return -1;
  }
  // else if (!contacts.ParseFromIstream(&input))
  // {
  //   cerr << "contacts.bin 文件序列化失败\n";
  //   input.close();
  //   return -1;
  // }
  // 添加一个联系人信息
  addPersonInfo(contacts);
  // 序列化通讯录写会到contacts.bin文件里
  ofstream out("contacts.bin", std::ios::out | std::ios::binary|std::ios::app);
  if (!out)
  {
    cerr << "contacts.bin文件打开失败\n";
    return -1;
  }
  else if (!contacts.SerializePartialToOstream(&out))
  {
    cerr << "contacts对象序列化失败\n";
    out.close();
    return -1;
  }
  input.close();
  out.close();
  return 0;
}

void addPersonInfo(contacts2::Contacts &contacts)
{
  contacts2::PeopleInfo *p = contacts.add_contacts();
  if (!p)
  {
    cerr << "添加联系人失败\n";
    return;
  }
  cout << "------------添加联系人-----------\n";
  cout << "------------请输出姓名: ";
  string name;
  getline(cin, name);
  p->set_name(name);
  cout << "------------请输出年龄: ";
  int age;
  cin >> age;
  p->set_age(age);
  cin.ignore(256, '\n'); // 在256个字符前,如果遇到'\n'就跳过'\n'并停下来,忽略256个字符后还没遇到'\n'就停下来.

  int i = 1;

  while (true)
  {
    string number;
    cout << "------------请输出手机号码" << i << ": ";
    getline(cin, number);
    if (number.empty())
    {
      cout << "手机号码读取成功\n";
      break;
    }
    contacts2::PeopleInfo_Phone *phone = p->add_phone();
    phone->set_number(number);
    i++;
    cout << "------------请输入手机号码类型(MP:1 or TEL:2)"
         << ": ";
    int type = 0;
    cin >> type;
    cin.ignore(256,'\n');
    switch (type)
    {
    case 1:
    cout<<"type:"<<type<<endl;
      phone->set_type(contacts2::PeopleInfo_Phone_PhoneType::PeopleInfo_Phone_PhoneType_MP);
      break;
    case 2:
        cout<<"type:"<<type<<endl;
      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);
  google::protobuf::Any* data = p->mutable_data();
  data->PackFrom(address);

  cout<<"请输入其他联系方式 (1:qq 2.wechat): ";
  int select =1;
  cin>>select;
  cin.ignore(256,'\n');
  if(select == 1){
    cout<<"请输入qq号:";
    string qq_str;
    getline(cin,qq_str);
    p->set_qq(qq_str);
  }else if(select == 2){
    cout<<"请输入wechat:";
    string wechat;
    getline(cin,wechat);
    p->set_wechat(wechat);
  }else{
    cout<<"输入有误"<<endl;
  }
  cout<<"添加联系人成功"<<endl;
}

更新read.cc(通讯录2.3),更新内容如下:

#include 
#include 
#include 
#include 
using namespace std;
void printContacts(const contacts2::Contacts &contacts);
int main()
{
  contacts2::Contacts contacts;
  // 将contacts.bin 文件的数据进行反序列化
  std::ifstream input("contacts.bin", std::fstream::in | std::fstream::out);
  if (!input)
  {
    cerr << "contacts.bin 不存在该文件\n";
    return -1;
  }
  else if (!contacts.ParseFromIstream(&input))
  {
    cerr << "contacts.bin 文件序列化失败\n";
    input.close();
    return -1;
  }
  // 打印contacts
  printContacts(contacts);

  return 0;
}

void printContacts(const contacts2::Contacts &contacts)
{
  for (int i = 0; i < contacts.contacts_size(); i++)
  {
    const contacts2::PeopleInfo &people = contacts.contacts(i);
    cout << "----------联系人" << i << "----------" << endl;
    cout << "联系人姓名:" << people.name() << endl;
    cout << "联系人年龄:" << people.age() << endl;
    for (int j = 0; j < people.phone_size(); j++)
    {
      cout << "联系手机" << j + 1 << ":" << people.phone(j).number();
      cout << "    type:(" << people.phone(j).PhoneType_Name(people.phone(j).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<<"其他联系方式wechat号:"<<people.wechat()<<endl;
        break;      
      case contacts2::PeopleInfo::OtherContactCase::OTHER_CONTACT_NOT_SET:
        cout<<"其他联系方式:无"<<endl;
        break;
    }
  }
}

代码编写完成后,编译后进⾏读写:

[YYK@VM-8-7-centos proto3_2.3_]$ ./write 
------------添加联系人-----------
------------请输出姓名: bbq
------------请输出年龄: 21
------------请输出手机号码1: 123456
------------请输入手机号码类型(MP:1 or TEL:2): 1
------------请输出手机号码2: 
手机号码读取成功
请输入联系人家庭地址:guangz
请输入联系人单位地址:guangz
请输入其他联系方式 (1:qq 2.wechat): 2
请输入wechat:YYKGentleman
添加联系人成功
[YYK@VM-8-7-centos proto3_2.3_]$ ./read
----------联系人0----------
联系人姓名:yyk
联系人年龄:20
联系手机1:289984    type:(MP)
家庭地址:guangd
单位地址:guand
其他联系方式qq号:2658875847
----------联系人1----------
联系人姓名:bbq
联系人年龄:21
联系手机1:123456    type:(MP)
家庭地址:guangz
单位地址:guangz
其他联系方式wechat号:YYKGentleman

5.2 oneof 模拟实现

在Protocol Buffers中,oneof实现的基本原理是使用联合体(union)来表示不同字段的内存布局。生成的C++代码中,对于每个oneof定义的字段,都会生成一个相应的枚举类型和一个联合体。

具体来说,假设我们有如下的message定义

message MyMessage {
  oneof my_field {
    int32 foo = 1;
    string bar = 2;
  }
}

生成的C++代码将包含如下的定义

class MyMessage {
 public:
  enum MyFieldCase {
    kFoo = 1,
    kBar = 2,
    MY_FIELD_NOT_SET = 0
  };

  // Getter and setter for the 'foo' field.
  int32 foo() const;
  void set_foo(int32 value);

  // Getter and setter for the 'bar' field.
  const std::string& bar() const;
  std::string* mutable_bar();

  MyFieldCase my_field_case() const;

 private:
  union MyField {
    MyField() {}
    ~MyField() {}

    int32 foo_;
    std::string bar_;
  } my_field_;

  MyFieldCase my_field_case_;
};

其中,MyMessage类中声明了一个枚举类型MyFieldCase用于表示当前哪个字段被设置,同时还有与每个字段对应的getter和setter方法。联合体MyField中包含了所有可能的字段类型,并且只有一个字段会被赋值。MyFieldCase字段用于标识当前哪个字段被设置。

在序列化和反序列化时,根据MyFieldCase字段的值来确定哪个字段应该被序列化或反序列化。在设置或获取某个字段的值时,会将对应的MyFieldCase字段设置为相应的值。需要注意的是,联合体和枚举类型都是C++中的底层语言特性,在使用时需要谨慎处理可能出现的内存错误和类型安全问题。

6. map类型

语法⽀持创建⼀个关联映射字段,也就是可以使⽤map类型去声明字段类型,格式为:

map<key_type, value_type> map_field = N;

要注意的是

  • key_type 是除了floatbytes类型以外的任意标量类型。 value_type 可以是任意类型。
  • map字段不可以⽤repeated修饰
  • map中存⼊的元素是⽆序的

6.1升级通讯录⾄2.4版本

最后,通讯录2.4版本想新增联系⼈的备注信息,我们可以使⽤map类型的字段来存储备注信息。

更新contacts.proto(通讯录2.4),更新内容如下:

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{
    string qq=5;
    string wechat=6;
    //uint32 work_number = 7;
  };
   map<string,string> remark = 7;
}
// 通讯录
message Contacts{
  repeated PeopleInfo contacts = 1 ;
}

编译 protoc--cpp_out=.contacts.proto

contacts.pb.h更新的部分代码展⽰:

// 更新的 PeopleInfo 类
class PeopleInfo final : public ::PROTOBUF_NAMESPACE_ID::Message {
	// map remark = 7;
	int remark_size() const;
	void clear_remark();
	const ::PROTOBUF_NAMESPACE_ID::Map< std::string, std::string >&
	remark() const;
	::PROTOBUF_NAMESPACE_ID::Map< std::string, std::string >*
	mutable_remark();
};

上述的代码中,对于Map类型的字段:

  • 清空map:clear_⽅法
  • 设置和获取:获取⽅法的⽅法名称与⼩写字段名称完全相同。设置⽅法为mutable_⽅法,返回值为Map类型的指针,这类⽅法会为我们开辟好空间,可以直接对这块空间的内容进⾏修改

更新write.cc(通讯录2.4),更新内容如下:

#include 
#include 
#include 
#include 
using namespace std;
void addPersonInfo(contacts2::Contacts &contacts);
int main()
{

  contacts2::Contacts contacts;
  // 将contacts.bin 文件的数据进行反序列化
  std::ifstream input("contacts.bin", std::fstream::in | std::fstream::out);
  if (!input)
  {
    cerr << "contacts.bin 不存在该文件\n";
    return -1;
  }
  // else if (!contacts.ParseFromIstream(&input))
  // {
  //   cerr << "contacts.bin 文件序列化失败\n";
  //   input.close();
  //   return -1;
  // }
  // 添加一个联系人信息
  addPersonInfo(contacts);
  // 序列化通讯录写会到contacts.bin文件里
  ofstream out("contacts.bin", std::ios::out | std::ios::binary | std::ios::app);
  if (!out)
  {
    cerr << "contacts.bin文件打开失败\n";
    return -1;
  }
  else if (!contacts.SerializePartialToOstream(&out))
  {
    cerr << "contacts对象序列化失败\n";
    out.close();
    return -1;
  }
  input.close();
  out.close();
  return 0;
}

void addPersonInfo(contacts2::Contacts &contacts)
{
  contacts2::PeopleInfo *p = contacts.add_contacts();
  if (!p)
  {
    cerr << "添加联系人失败\n";
    return;
  }
  cout << "------------添加联系人-----------\n";
  cout << "------------请输出姓名: ";
  string name;
  getline(cin, name);
  p->set_name(name);
  cout << "------------请输出年龄: ";
  int age;
  cin >> age;
  p->set_age(age);
  cin.ignore(256, '\n'); // 在256个字符前,如果遇到'\n'就跳过'\n'并停下来,忽略256个字符后还没遇到'\n'就停下来.

  int i = 1;

  while (true)
  {
    string number;
    cout << "------------请输出手机号码" << i << ": ";
    getline(cin, number);
    if (number.empty())
    {
      cout << "手机号码读取成功\n";
      break;
    }
    contacts2::PeopleInfo_Phone *phone = p->add_phone();
    phone->set_number(number);
    i++;
    cout << "------------请输入手机号码类型(MP:1 or TEL:2)"
         << ": ";
    int type = 0;
    cin >> type;
    cin.ignore(256, '\n');
    switch (type)
    {
    case 1:
      cout << "type:" << type << endl;
      phone->set_type(contacts2::PeopleInfo_Phone_PhoneType::PeopleInfo_Phone_PhoneType_MP);
      break;
    case 2:
      cout << "type:" << type << endl;
      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);
  google::protobuf::Any *data = p->mutable_data();
  data->PackFrom(address);

  cout << "请输入其他联系方式 (1:qq 2.wechat): ";
  int select = 1;
  cin >> select;
  cin.ignore(256, '\n');
  if (select == 1)
  {
    cout << "请输入qq号:";
    string qq_str;
    getline(cin, qq_str);
    p->set_qq(qq_str);
  }
  else if (select == 2)
  {
    cout << "请输入wechat:";
    string wechat;
    getline(cin, wechat);
    p->set_wechat(wechat);
  }
  else
  {
    cout << "输入有误" << endl;
  }

  for (int i = 0;; i++)
  {
    cout << "请输入备注" << i << "信息(备注标题):";
    string title;
    getline(cin, title);
    if (title.empty())
    {
      break;
    }
    cout << "请输入备注" << i << "信息(备注内容):";
    string content;
    getline(cin, content);
    p->mutable_remark()->insert({title, content});
  }
  cout << "添加联系人成功" << endl;
}

更新read.cc(通讯录2.4),更新内容如下:

#include 
#include 
#include 
#include 
using namespace std;
void printContacts(const contacts2::Contacts &contacts);
int main()
{
  contacts2::Contacts contacts;
  // 将contacts.bin 文件的数据进行反序列化
  std::ifstream input("contacts.bin", std::fstream::in | std::fstream::out);
  if (!input)
  {
    cerr << "contacts.bin 不存在该文件\n";
    return -1;
  }
  else if (!contacts.ParseFromIstream(&input))
  {
    cerr << "contacts.bin 文件序列化失败\n";
    input.close();
    return -1;
  }
  // 打印contacts
  printContacts(contacts);

  return 0;
}

void printContacts(const contacts2::Contacts &contacts)
{
  for (int i = 0; i < contacts.contacts_size(); i++)
  {
    const contacts2::PeopleInfo &people = contacts.contacts(i);
    cout << "----------联系人" << i << "----------" << endl;
    cout << "联系人姓名:" << people.name() << endl;
    cout << "联系人年龄:" << people.age() << endl;
    for (int j = 0; j < people.phone_size(); j++)
    {
      cout << "联系手机" << j + 1 << ":" << people.phone(j).number();
      cout << "    type:(" << people.phone(j).PhoneType_Name(people.phone(j).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 << "其他联系方式wechat号:" << people.wechat() << endl;
      break;
    case contacts2::PeopleInfo::OtherContactCase::OTHER_CONTACT_NOT_SET:
      cout << "其他联系方式:无" << endl;
      break;
    }

    if (people.remark().empty() != true)
    {
      cout << "备注:" << endl;
    }
    int j = 0;
    for (auto &it : people.remark())
    {
      cout << "\t备注" << j << it.first << " : " << it.second << endl;
      j++;
    }
  }
}

代码编写完成后,编译后进⾏读写:

[YYK@VM-8-7-centos proto3_2.4_]$ ./write
------------添加联系人-----------
------------请输出姓名: rrr
------------请输出年龄: 333
------------请输出手机号码1: 3333333
------------请输入手机号码类型(MP:1 or TEL:2): 1
------------请输出手机号码2: 
手机号码读取成功
请输入联系人家庭地址:ggg
请输入联系人单位地址:ggg
请输入其他联系方式 (1:qq 2.wechat): 1
请输入qq号:3333
请输入备注信息0(备注标题):日期
请输入备注信息0(备注内容):2002/12/23
请输入备注信息1(备注标题):
添加联系人成功
[YYK@VM-8-7-centos proto3_2.4_]$ ./read
----------联系人1---------
联系人姓名:rrr
联系人年龄:333
联系手机1:3333333    type:(MP)
家庭地址:ggg
单位地址:ggg
其他联系方式qq号:3333
备注:
        备注0日期 : 2002/12/23

到此,我们对通讯录2.x要求的任务全部完成。在这个过程中我们将通讯录升级到了2.4版本,同时对ProtoBuf的使⽤也进⼀步熟练了,并且也掌握了ProtoBuf的proto3语法⽀持的⼤部分类型及其使⽤,但只是正常使⽤还是完全不够的。通过接下来的学习,我们就能更进⼀步了解到ProtoBuf深⼊的内容。

7. 默认值

反序列化消息时,如果被反序列化的⼆进制序列中不包含某个字段,反序列化对象中相应字段时,就会设置为该字段的默认值。不同的类型对应的默认值不同

  • 对于字符串,默认值为空字符串。
  • 对于字节,默认值为空字节。
  • 对于布尔值,默认值为false。
  • 对于数值类型,默认值为0。
  • 对于枚举,默认值是第⼀个定义的枚举值,必须为0。
  • 对于消息字段,未设置该字段。它的取值是依赖于语⾔。
  • 对于设置了repeated的字段的默认值是空的(通常是相应语⾔的⼀个空列表)。
  • 对于 消息字段oneof字段 和 any字段 ,C++Java语⾔中都有has_⽅法来检测当前字段是否被设置。

注意事项

  • 在proto3语法里 消息字段是没有提供has_方法的, 影响不大。
  • 没有has_方法可能带来的问题.
  • 例如下图:
  • 有一个消息字段 message{int32 a,b,c;},将消息序列化后,序列化串是没有字段c的内容, 因此在反序列化为在c字段填写默认值0。
  • 此时右边的无法确认 0值是被赋值还是默认值,如果有has方法就能确定,在业务中兼容默认值。

protobuf的Proto3语法学习 (二)_第1张图片

8. 更新消息

8.1 更新规则

如果现有的消息类型已经不再满⾜我们的需求,例如需要扩展⼀个字段,在不破坏任何现有代码的情况下更新消息类型⾮常简单。遵循如下规则即可

  • 1)禁止修改任何已有字段的字段编号。 - - 存储数据也需要对数据编号, 拿取也需要编号获取, 读取和存储需要相同的编号.
    • 在protobuf中,禁止修改任何已有字段的字段编号是因为序列化和反序列化时会使用字段编号进行数据的读取和写入。如果修改了字段编号,那么在使用旧的消息格式进行反序列化时会导致数据读取错误,而使用新的消息格式进行反序列化时也会导致数据写入错误或数据丢失。这样会导致数据不一致,严重影响数据的正确性和可靠性。因此,为了保证数据的稳定性和一致性,禁止修改已有字段的字段编号是必要的。
  • 2)如果在一个protobuf文件中删除了一个旧字段,那么就不能再使用该字段的编号。
    • 为了确保该编号不会被重复使用,正确的做法是在protobuf文件中保留该编号,但不再使用该字段。这可以通过使用"reserved"关键字来实现。因此,不建议直接删除或注释掉字段,而应该使用"reserved"关键字来保留该字段的编号并防止其被重复使用。
  • 3) 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:
  • 4)将⼀个单独的值更改为新oneof类型成员之⼀是安全和⼆进制兼容的
    • 若确定没有代码⼀次性设置多个值那么将多个字段移⼊⼀个新oneof类型也是可⾏的。
    • 将任何字段移⼊已存在的oneof类型是不安全的。

8.2 保留字段reserved

如果通过删除或注释掉字段来更新消息类型,未来的⽤⼾在添加新字段时,有可能会使⽤以前已经存在,但已经被删除或注释掉的字段编号。将来使⽤该.proto的旧版本时的程序会引发很多问题:数据损坏、隐私错误等等。

确保不会发⽣这种情况的⼀种⽅法是:使⽤ reserved 将指定字段的编号或名称设置为保留项。当我们再使⽤这些编号或名称时,protocolbuffer的编译器将会警告这些编号或名称不可⽤。举个例⼦:

message Message {
	// 设置保留项
	reserved 100, 101, 200 to 299; // 保留100,101, 200~299的字段
	reserved "field3", "field4"; // 保留字段名
	// 注意:不要在⼀⾏ reserved 声明中同时声明字段编号和名称。
	// reserved 102, "field5"; //erro
	// 设置保留项之后,下⾯代码会告警
	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
}
8.2.1 创建通讯录3.0版本—验证错误删除字段造成的数据损坏

现模拟有两个服务,他们各⾃使⽤⼀份通讯录.proto⽂件,内容约定好了是⼀模⼀样的。

  • 服务1(service):负责序列化通讯录对象,并写⼊⽂件中。
  • 服务2(client):负责读取⽂件中的数据,解析并打印出来。

案例1分析:

第一步: 准备些文件, client和server有相同的contacts.proto .


protobuf的Proto3语法学习 (二)_第2张图片

第二步: 此时 Server修改proto文件


>Server/contacts.proto
syntax = "proto3";
package contacts2;

message PeopleInfo{
  //int32 age =1 ; // 年龄
  int32 birthday = 1;//生日
  string name = 2 ; //姓名
}
// 通讯录
message Contacts{
  repeated PeopleInfo contacts = 1 ;
}

Client/contacts.proto

syntax = "proto3";
package contacts2;

message PeopleInfo{
  int32 age =1 ; // 年龄
  string name = 2 ; //姓名
}
// 通讯录
message Contacts{
  repeated PeopleInfo contacts = 1 ;
}

第三步: Server编译并写入一个联系人


protobuf的Proto3语法学习 (二)_第3张图片

第四 步: 可以发现client获取年龄时却获取到了生日,因为在两个proto文件里,生日字段和年龄字段编号相同.

案例2分析:

现模拟有两个服务,他们各⾃使⽤⼀份通讯录.proto⽂件,内容约定好了是⼀模⼀样的。

  • 服务1(service):负责序列化通讯录对象,并写⼊⽂件中。
  • 服务2(client):负责读取⽂件中的数据,解析并打印出来。

第一步: 准备些文件, client和server有相同的contacts.proto .

Server/contacts.proto

syntax = "proto3";
package contacts2;

message PeopleInfo{
  int32 age =1 ; // 年龄
  string name = 2 ; //姓名
}
// 通讯录
message Contacts{
  repeated PeopleInfo contacts = 1 ;
}

第二步: 编译并写入一个联系人

[YYK@VM-8-7-centos server]$ ./write
------------添加联系人-----------
------------请输出姓名: yyk
------------请输出年龄: 20
添加联系人成功

[YYK@VM-8-7-centos client]$ ./read
----------联系人0----------
联系人姓名:yyk
联系人年龄:20

第三步: 更改Server的proto文件

syntax = "proto3";
package contacts2;

message PeopleInfo{
  int32 age =1 ; // 年龄

  string name = 2 ; //姓名

  int32 birthday =3 ; // 添加生日字段
}
// 通讯录
message Contacts{
  repeated PeopleInfo contacts = 1 ;
}

第四步: 编译并录入一个联系人

[YYK@VM-8-7-centos server]$ protoc --cpp_out=. contacts.proto

[YYK@VM-8-7-centos server]$ g++ -o write write.cpp contacts.pb.cc -std=c++11 -lprotobuf ;./write
------------添加联系人-----------
------------请输出姓名: bbq
------------请输出年龄: 22
------------请输出生日: 20021223
添加联系人成功

第五步: 执行client

[YYK@VM-8-7-centos client]$ ./read
----------联系人0----------
联系人姓名:yyk
联系人年龄:20
----------联系人1----------
联系人姓名:bbq
联系人年龄:22

第六步: 可以发现 由于client没有改变proto文件,所以读取不了birthday字段的数据.

8.3 未知字段

在通讯录3.0版本中,我们向service⽬录下的contacts.proto新增了‘⽣⽇’字段,但对于client相关的代码并没有任何改动。验证后发现新代码序列化的消息(service)也可以被旧代码(client)解析。并且这⾥要说的是,新增的‘⽣⽇’字段在旧程序(client)中其实并没有丢失,⽽是会作为旧程序的未知字段。

  • 未知字段:解析结构良好的protocolbuffer已序列化数据中的未识别字段的表⽰⽅式。例如,当旧程序解析带有新字段的数据时,这些新字段就会成为旧程序的未知字段。
  • 本来,proto3在解析消息时总是会丢弃未知字段,但在3.5版本中重新引⼊了对未知字段的保留机制。所以在3.5或更⾼版本中,未知字段在反序列化时会被保留,同时也会包含在序列化的结果中。
8.3.1 未知字段从哪获取

了解相关类关系图

protobuf的Proto3语法学习 (二)_第4张图片
MessageLite类介绍(了解)

  • MessageLite从名字看是轻量级的message,仅仅提供序列化、反序列化功能。
  • 类定义在google提供的message_lite.h中。
    Message类介绍(了解)
  • 我们⾃定义的message类,都是继承⾃Message。
  • Message最重要的两个接⼝GetDescriptor/GetReflection,可以获取该类型对应的Descriptor对象
    指针和Reflection对象指针。
  • 类定义在google提供的message.h中。
//google::protobuf::Message 部分代码展⽰
const Descriptor* GetDescriptor() const;
const Reflection* GetReflection() const;

Descriptor类介绍(了解)

  • Descriptor:是对message类型定义的描述,包括message的名字、所有字段的描述、原始的
    proto⽂件内容等。
  • 类定义在google提供的descriptor.h中。
// 部分代码展⽰
class PROTOBUF_EXPORT Descriptor : private internal::SymbolBase {
	string& name () const
	int field_count() const;
	const FieldDescriptor* field(int index) const;
	const FieldDescriptor* FindFieldByNumber(int number) const;
	const FieldDescriptor* FindFieldByName(const std::string& name) const;
	const FieldDescriptor* FindFieldByLowercaseName(
	const std::string& lowercase_name) const;
	const FieldDescriptor* FindFieldByCamelcaseName(
	const std::string& camelcase_name) const;
	int enum_type_count() const;
	const EnumDescriptor* enum_type(int index) const;
	const EnumDescriptor* FindEnumTypeByName(const std::string& name) const;
	const EnumValueDescriptor* FindEnumValueByName(const std::string& name)
	const;
}

Reflection类介绍(了解)

  • Reflection接⼝类,主要提供了动态读写消息字段的接⼝,对消息对象的⾃动读写主要通过该类完
    成。
  • 提供⽅法来动态访问/修改message中的字段,对每种类型,Reflection都提供了⼀个单独的接⼝⽤
    于读写字段对应的值。
    • 针对所有不同的field类型 FieldDescriptor::TYPE_* ,需要使⽤不同的 Get*()/Set*
      ()/Add*() 接⼝;
    • repeated类型需要使⽤ GetRepeated*()/SetRepeated*() 接⼝,不可以和⾮repeated
      类型接⼝混⽤;
    • message对象只可以被由它⾃⾝的 reflection(message.GetReflection()) 来操
      作;
  • 类中还包含了访问/修改未知字段的⽅法。
  • 类定义在google提供的message.h中。
//google::protobuf::Message 部分代码展⽰
const Descriptor* GetDescriptor() const;
const Reflection* GetReflection() const;

Descriptor类介绍(了解)

  • Descriptor:是对message类型定义的描述,包括message的名字、所有字段的描述、原始的
    proto⽂件内容等。
  • 类定义在google提供的descriptor.h中。
// 部分代码展⽰
class PROTOBUF_EXPORT Descriptor : private internal::SymbolBase {
string& name () const
int field_count() const;
const FieldDescriptor* field(int index) const;
const FieldDescriptor* FindFieldByNumber(int number) const;
const FieldDescriptor* FindFieldByName(const std::string& name) const;
const FieldDescriptor* FindFieldByLowercaseName(
const std::string& lowercase_name) const;
const FieldDescriptor* FindFieldByCamelcaseName(
const std::string& camelcase_name) const;
int enum_type_count() const;
const EnumDescriptor* enum_type(int index) const;
const EnumDescriptor* FindEnumTypeByName(const std::string& name) const;
const EnumValueDescriptor* FindEnumValueByName(const std::string& name)
const;
}

FieldDescriptor类的介绍(了解)

在protobuf3中,FieldDescriptor表示了一个字段的描述信息,包括字段的名称、编号、类型、是否为重复字段、默认值等。

一个FieldDescriptor对象可以从MessageDescriptor中获取,表示了一个Message中的一个字段。

FieldDescriptor有如下属性:
`

name:字段的名称
full_name:字段的完整名称,包括Message的名称和字段的名称
index:字段在Message中的编号
number:字段的编号
type:字段的类型,可以是double、float、int32、int64、uint32、uint64、sint32、sint64、fixed32、fixed64、sfixed32、sfixed64、bool、string、bytes、enum、message其中之一
label:字段的标签,可以是optional、required、repeated其中之一
default_value:字段的默认值,根据类型不同而不同

`
例如,对于以下的protobuf定义:

message Person { string name = 1; int32 age = 2; repeated string email = 3; }

我们可以通过以下代码获取对应的FieldDescriptor:

person_desc = Person.DESCRIPTOR name_desc = person_desc.fields_by_name[‘name’] age_desc = person_desc.fields_by_name[‘age’] email_desc = person_desc.fields_by_name[‘email’]

Reflection类介绍(了解)

在 protobuf3 中,Reflection 是一种机制,用于在运行时动态地操作消息类型,包括访问/修改字段值、获取字段名和类型、查询消息是否存在字段等。

Reflection 的主要接口是 Message 类的 GetReflection() 方法,通过该方法可以获取一个 MessageReflection 对象。MessageReflection 对象提供了一组方法,用于访问消息类型的所有字段。

例如,通过 MessageReflection 的 GetField() 方法可以获取消息类型中指定字段的值,如下所示:

google::protobuf::MessageReflection* reflection = message->GetReflection();
const google::protobuf::FieldDescriptor* field = reflection->FindFieldByName("field_name");
if (field != NULL) {
    switch (field->type()) {
        case google::protobuf::FieldDescriptor::TYPE_INT32:
            int32_t value = reflection->GetInt32(*message, field);
            break;
        case google::protobuf::FieldDescriptor::TYPE_STRING:
            std::string value = reflection->GetString(*message, field);
            break;
        // handle other types
        // ...
    }
}

通过 MessageReflection 的 MutableField() 方法可以修改消息类型中指定字段的值,如下所示:

google::protobuf::MessageReflection* reflection = message->GetReflection();
const google::protobuf::FieldDescriptor* field = reflection->FindFieldByName("field_name");
if (field != NULL) {
    switch (field->type()) {
        case google::protobuf::FieldDescriptor::TYPE_INT32:
            reflection->SetInt32(message, field, 42);
            break;
        case google::protobuf::FieldDescriptor::TYPE_STRING:
            reflection->SetString(message, field, "hello");
            break;
        // handle other types
        // ...
    }
}

除了访问/修改字段值之外,Reflection 还提供了一些其他的方法,例如:

GetDescriptor():获取消息类型的描述符;
GetRepeatedFieldSize():获取重复字段的大小;
AddMessage():向消息类型的重复字段中添加一个新的消息;
ClearField():清空消息类型的指定字段的值。

UnknownFieldSet类介绍(重要)
• UnknownFieldSet包含在分析消息时遇到但未由其类型定义的所有字段。
• 若要将UnknownFieldSet附加到任何消息,请调⽤Reflection::GetUnknownFields()。
• 类定义在unknown_field_set.h中。

class PROTOBUF_EXPORT UnknownFieldSet {
	inline void Clear();
	void ClearAndFreeMemory();
	inline bool empty() const;
	inline int field_count() const;
	inline const UnknownField& field(int index) const;
	
	inline UnknownField* mutable_field(int index);
	// Adding fields ---------------------------------------------------
	void AddVarint(int number, uint64_t value);
	void AddFixed32(int number, uint32_t value);
	void AddFixed64(int number, uint64_t value);
	void AddLengthDelimited(int number, const std::string& value);
	std::string* AddLengthDelimited(int number);
	UnknownFieldSet* AddGroup(int number);
	// Parsing helpers -------------------------------------------------
	// These work exactly like the similarly-named methods of Message.
	bool MergeFromCodedStream(io::CodedInputStream* input);
	bool ParseFromCodedStream(io::CodedInputStream* input);
	bool ParseFromZeroCopyStream(io::ZeroCopyInputStream* input);
	bool ParseFromArray(const void* data, int size);
	inline bool ParseFromString(const std::string& data) {
	return ParseFromArray(data.data(), static_cast<int>(data.size()));
	}
	// Serialization.
	bool SerializeToString(std::string* output) const;
	bool SerializeToCodedStream(io::CodedOutputStream* output) const;
	static const UnknownFieldSet& default_instance();
};

UnknownField类介绍(重要)

• 表⽰未知字段集中的⼀个字段。
• 类定义在unknown_field_set.h中。

class PROTOBUF_EXPORT UnknownField {
public:
	enum Type {
	TYPE_VARINT,
	TYPE_FIXED32,
	TYPE_FIXED64,
	TYPE_LENGTH_DELIMITED,
	TYPE_GROUP
	};
	inline int number() const;
	inline Type type() const;
	// Each method works only for UnknownFields of the corresponding type.
	inline uint64_t varint() const;
	inline uint32_t fixed32() const;
	inline uint64_t fixed64() const;
	inline const std::string& length_delimited() const;
	inline const UnknownFieldSet& group() const;
	inline void set_varint(uint64_t value);
	inline void set_fixed32(uint32_t value);
	inline void set_fixed64(uint64_t value);
	inline void set_length_delimited(const std::string& value);
	inline std::string* mutable_length_delimited();
	inline UnknownFieldSet* mutable_group();
};
8.3.2 升级通讯录3.1版本—验证未知字段

需求:

  • 根据#### 8.2.1 案例2, clinet能够打印完未知字段

protobuf的Proto3语法学习 (二)_第5张图片

更新client.cc(通讯录3.1),在这个版本中,需要打印出未知字段的内容。更新的代码如下:

#include 
#include 
#include 
#include 
using namespace std;
void printContacts(const contacts2::Contacts &contacts);
int main()
{
  contacts2::Contacts contacts;
  // 将contacts.bin 文件的数据进行反序列化
  std::ifstream input("../contacts.bin", std::fstream::in | std::fstream::out);
  if (!input)
  {
    cerr << "contacts.bin 不存在该文件\n";
    return -1;
  }
  else if (!contacts.ParseFromIstream(&input))
  {
    cerr << "contacts.bin 文件序列化失败\n";
    input.close();
    return -1;
  }
  // 打印contacts
  printContacts(contacts);

  return 0;
}

void printContacts(const contacts2::Contacts &contacts)
{
  for (int i = 0; i < contacts.contacts_size(); i++)
  {
    const contacts2::PeopleInfo &people = contacts.contacts(i);
    cout << "----------联系人" << i << "----------" << endl;
    cout << "联系人姓名:" << people.name() << endl;
    cout << "联系人年龄:" << people.age() << endl;

    const google::protobuf::Reflection* reflection = contacts2::PeopleInfo::GetReflection();
    const google::protobuf::UnknownFieldSet &fields=reflection->GetUnknownFields(people);

    for(int j=0;j<fields.field_count();j++)
    {
      const google::protobuf::UnknownField & filed = fields.field(j);
      cout<<"未知字段"<<j+1<<":";
      cout<<"\t字段编号:"<<filed.number();
      cout<<"\t类型:" << filed.type();
      switch(filed.type())
      {
        case google::protobuf::UnknownField::Type::TYPE_VARINT :
        {
          cout<<"\t值:"<<filed.varint()<<endl;
          break;
        }
        case google::protobuf::UnknownField::Type::TYPE_LENGTH_DELIMITED :
        {
          cout<<"\t值:"<<filed.length_delimited()<<endl;
          break;
        }
        default:
        break;
          // 等等 
      }
    }

  }
  
}



编译运行 client ,结果

[YYK@VM-8-7-centos client]$ g++ -o read read.cpp contacts.pb.cc -std=c++11 -lprotobuf;./read
----------联系人0----------
联系人姓名:yyk
联系人年龄:20
----------联系人1----------
联系人姓名:bbq
联系人年龄:20021223
----------联系人2----------
联系人姓名:bbq
联系人年龄:22
未知字段1:      字段编号:3      类型:0:20021223

8.4 前后兼容性

根据上述的例⼦可以得出,pb是具有向前兼容的。为了叙述⽅便,把增加了“⽣⽇”属性的service称为“新模块”;未做变动的client称为“⽼模块”。

  • 向前兼容:⽼模块能够正确识别新模块⽣成或发出的协议。这时新增加的“⽣⽇”属性会被当作未
    知字段(pb3.5版本及之后)。
  • 向后兼容:新模块也能够正确识别⽼模块⽣成或发出的协议。
  • 前后兼容的作⽤:当我们维护⼀个很庞⼤的分布式系统时,由于你⽆法同时升级所有模块,为了保证在升级过程中,整个系统能够尽可能不受影响,就需要尽量保证通讯协议的“向后兼容”或“向前兼容”。

9. 选项option

.proto⽂件中可以声明许多选项,使⽤ option 标注。选项能影响proto编译器的某些处理⽅式。

proto3 中的可选字段是什么意思

在proto3中,所有的字段都是可选的,即使你不显式地标记它们为可选的。这意味着,如果你没有为该字段提供值,它将被设置为默认值。如果你希望指定某个字段的默认值,可以在消息定义中使用[default = value]语法在prot3中已经不支持显示默认值了, 之前的其中value是你希望该字段默认设置的值。如果你不指定默认值,则proto3将为该字段设置一个合理的默认值,例如0、false、空字符串或空数组,具体取决于字段的类型。

proto3 中 option 和optional的区别

在proto3中,option是一个关键字,用于设置message或field的选项,例如设置默认值、序列化等选项。而optional是在proto2中使用的关键字,用于定义可选的field,被proto3废弃。

在proto3中,默认情况下所有的field都是可选的,不需要使用optional关键字来指定。如果一个field不被设置值,那么在解析时会返回该field对应类型的默认值,例如int32类型的默认值为0,bool类型的默认值为false。

因此,option和optional在proto3中已经没有区别了。

9.1 选项分类

pb已经写好了可选字段,分别在这些message里.,选项的完整列表在google/protobuf/descriptor.proto中定义。部分代码:

syntax = "proto2"; // descriptor.proto 使⽤ proto2 语法版本
message FileOptions {// ⽂件选项 定义在 FileOptions 消息中
 
  optional string name = 1;
  optional int32 number = 3;
  optional Label label = 4;
  optional Type type = 5;
  optional string type_name = 6;
	
	... 


} 
message MessageOptions { ... } // 消息类型选项 定义在 MessageOptions 消息中
message FieldOptions { ... } // 消息字段选项 定义在 FieldOptions 消息中
message OneofOptions { ... } // oneof字段选项 定义在 OneofOptions 消息中
message EnumOptions { ... } // 枚举类型选项 定义在 EnumOptions 消息中
message EnumValueOptions { .. } // 枚举值选项 定义在 EnumValueOptions 消息中
message ServiceOptions { ... } // 服务选项 定义在 ServiceOptions 消息中
message MethodOptions { ... } // 服务⽅法选项 定义在 MethodOptions 消息中
...

由此可⻅,选项分为 ⽂件级、消息级、字段级 等等,但并没有⼀种选项能作⽤于所有的类型。

9.2选项的基本用法

案例一

syntax = "proto3";


import "google/protobuf/descriptor.proto";

extend google.protobuf.FieldOptions {
  optional string my_option = 51234;
}


message MyMessage {
  option (my_option) = "hello shuaige" ;

  optional int32 age = 1;
}

如上代码解释:

  • extend : 在google.protobuf.FieldOptions这个message添加一个 my_option 的可选字段.
  • option (my_option) = “hello shuaige” : "( )"括号代表扩展的字段, option关键字设置 (my_option) 的值为 “hello shuaige”.

想要获取my_option,如下c++所示 :

#include"test.pb.h"
#include
#include
using namespace std;

int main()
{
    
  string value = MyMessage::descriptor()->options().GetExtension(my_option);
  cout<<value<<endl;
  return 0;
}

运行结果

g++ -o test test.cpp test.pb.cc -std=c++11 -lprotobuf;./test

hello shuaige

9.2 常⽤选项列举

  1. 选项optimize_for
    • 在 Protocol Buffers 中,optimize_for 是一个可选的文件级别选项,用于指定编译器如何生成序列化和反序列化代码以优化性能和文件大小。
    • optimize_for 可以有三个值:
      • SPEED:优化为序列化和反序列化速度最快,但可能会使生成的文件大小较大。
        
      •  CODE_SIZE:优化为生成较小的代码,可能会牺牲一些序列化和反序列化的速度。
        
      • LITE_RUNTIME:仅生成最小限度的运行时代码,可能会使序列化和反序列化的速度更快,并且生成的文件更小。
        
  • 例如,以下是在 .proto 文件中指定 optimize_for 的示例:

    syntax = "proto3";
    package example;
    
    option optimize_for = LITE_RUNTIME;
    
    message Person {
      string name = 1;
      int32 age = 2;
    }
    
    
  • 这个示例将 optimize_for 设置为 LITE_RUNTIME,以生成最小限度的运行时代码。

  • optimize_for 可选字段定义在descriptor.proto 文件里,如下部分代码

    message FileOptions {
    
      enum OptimizeMode {
        SPEED = 1;         // Generate complete code for parsing, serialization,
                           // etc.
        CODE_SIZE = 2;     // Use ReflectionOps to implement these methods.
        LITE_RUNTIME = 3;  // Generate code using MessageLite and the lite runtime.
      }
      optional OptimizeMode optimize_for = 9 [default = SPEED];
    
    
    }
    
    
  1. allow_alias:允许将相同的常量值分配给不同的枚举常量,⽤来定义别名。该选项为枚举选项。
    • 举个例⼦:

      enum PhoneType {
      	option allow_alias = true;
      	MP = 0;
      	TEL = 1;
      	LANDLINE = 1; // 若不加 option allow_alias = true; 这⼀⾏会编译报错
      }
      

你可能感兴趣的:(序列化工具,学习)