【Protobuf】Protobuf 语法介绍

Protobuf 语法介绍

在前文中,我们已经对Protobuf 进行了基本的使用,本次这里我们继续通过项目升级的方式来学习Protobuf

本次升级如下内容:

  • 不再打印联系人的序列化结果,而是将通讯录序列化后并写入文件中。

一、 字段规则

消息的字段可以用下面几种规则来修饰:

  • singular :用于表示一个字段在消息中最多出现一次, proto3 语法中,字段默认使用该规则,但是在proto3语法中不可显示加上该规则。

singular 字段在序列化和反序列化中的行为:

  • 序列化时 :如果一个 singular 字段从未被设置过,那么在序列化时,这个字段不会被编码到序列化的二进制流中。这意味着序列化的结果不会包含未设置的 singular 字段。
  • 反序列化时 :如果在反序列化的过程中,序列化数据中没有找到对应的 singular 字段的编码值,那么 protobuf 会使用该字段的默认值来填充该字段。默认值通常是对于标量类型的零值(例如整数的默认值是 0,字符串的默认值是空字符串等)。
  • repeated :消息中可以包含该字段任意多次(包括零次),其中重复值的顺序会被保留,可以理为定义了一个数组。

由于联系人可能有多个号码,于是我们使用数组来进行存储号码,对应的.proto文件如下:

clike 复制代码
syntax = "proto3";
package contacts;

message PeopleInfo 
{
	string name = 1;
	int32 age = 2;
	repeated string phone_numbers = 3;
}

编译这个文件,生成C++ 头/源文件,我们继续看其内部生成的方法,这里我们只关注「数组」的操作方法(其他类型的方法已经在前一章节讲解过了)。

二、消息类型的定义与使用

在单个 .proto 文件中可以定义多个消息体,且支持定义嵌套类型的消息(任意多层),每个消息体中的字段编号可以重复。

  1. 这里我们可以将 phone 提取出来,单独成为一个消息:
clike 复制代码
syntax = "proto3";
package contact;

message Phone
{
	string phone_number = 1;
}

message PeopleInfo
{
	string name = 1;
	int32 age = 2;
	Phone phone = 3;
}

编译这个文件,生成C++ 头/源文件,我们继续看其内部生成的方法,这里我们只关注「自定义对象」的操作方法。

  1. 这里我们还可以将 phone 进行嵌套在People中:
clike 复制代码
syntax = "proto3";
package contact;

message PeopleInfo
{
	string name = 1;
	int32 age = 2;
	message Phone
	{
		string phone_number = 1;
	}
	repeated Phone phone = 3;
}

使用嵌套,生成的操作方法和在外面单独定义phone 是没有区别,唯一的区别在于生成的类名前面会加一个 :父作用域的名称和下划线

  1. 除了这两种方式以外我们还可以将这个phone消息定义在另外一个文件中,然后在本文件中进行导入。
clike 复制代码
// contact2.proto 
syntax = "proto3";
package contact;
// 引入其他的proto文件
import "phone.proto";


message PeopleInfo
{
	string name = 1;
	int32 age = 2;
	// 引入的文件声明了package,使用消息时,需要用 '命名空间.消息类型' 格式
	repeated phone.Phone phone = 3;
}
clike 复制代码
// phone.proto
syntax = "proto3";
package phone;

message Phone
{
	string phone_number = 1;
}

此时我们的编译指令就要需要写两个文件

bash 复制代码
protoc --cpp_out=. contact2.proto phone.proto 

此时会生成两个 头/源文件

总结,上述的例子中:

  • 每个字段都有一个 clear_ 方法,可以将字段重新设置回初始状态。
  • 每个字段都有设置和获取的方法, 获取方法的方法名称与小写字段名称完全相同,pb的内置类型有set 方法, 自定义消息类型没有。
  • 如果是自定义消息类型的字段,其设置方法为 mutable_ 方法,返回值为消息类型的指针,这类方法会为我们开辟好空间,可以直接对这块空间的内容进行修改。
  • 对于使用 repeated 修饰的字段,也就是数组类型,pb 为我们提供了 add_方法来新增一个值,也是会为我们开辟好空间,可以直接对这块空间的内容进行修改。
  • 对于使用 repeated 修饰的字段还会提供了 _size 方法来判断数组存放元素的个数。

1、练习------序列化后并写入文件

这里我们来实现将通讯录序列化后并写入文件中:

  • proto文件
clike 复制代码
syntax = "proto3";
package contact;

// 电话信息
message Phone
{
	string phone_number = 1;
}

// 联系人信息
message PeopleInfo
{
	string name = 1;
	int32 age = 2;
	// 可能有多个电话
	repeated Phone phone = 3;
}

// 通讯录信息
message Contacts
{
	repeated PeopleInfo peopleinfo = 2;
}
  • write源文件
cpp 复制代码
#include <iostream>
#include <fstream>
#include <filesystem>
#include "contact2.pb.h"

using namespace std;
namespace fs = std::filesystem;


void addPeopleInfo(contact::Contacts& mycontact)
{
	contact::PeopleInfo* people = mycontact.add_peopleinfo();

	cout << "-------------新增联系人-------------" << endl;
	// 添加姓名
	string name;
	cout << "请输入联系人姓名:";
	getline(cin, name);
	people->set_name(name);

	// 添加年龄
	int age;
	cout << "请输入联系人年龄:";
	cin >> age;
	// 清除age 输入以后的\n
	cin.ignore();

	// 添加手机号码
	for (int i = 1; ; ++i)
	{
		cout << "请输入联系人电话" << i << "(ctrl + d 退出): ";
		string phone_number;
		if (cin >> phone_number)
		{
			contact::Phone* phone = people->add_phone();
			phone->set_phone_number(phone_number);
		}
		else
		{
			break;
		}
	}
	cout << "\n-----------添加联系人成功-----------" << endl;
}

int main()
{
	GOOGLE_PROTOBUF_VERIFY_VERSION;

	fs::path file_path("contacts.data");
	ofstream ofs;
	if (!fs::exists(file_path))
	{
		// 文件不存在创建文件
		ofs.open(file_path, ios::out | ios::binary);
	}
	else
	{
		// 文件存在打开文件
		ofs.open(file_path, ios::app | ios::binary);
	}

	if (!ofs.is_open())
	{
		perror("文件打开失败!");
		return 1;
	}

	//  到这里文件肯定是已经存在的并且可以正常打开

	// 创建通讯录
	contact::Contacts mycontact;
	ifstream ifs(file_path, ios::in | ios::binary);

	// 文件不为空文件
	if (ifs.peek() != EOF)
	{
		// 读取文件
		if (!mycontact.ParseFromIstream(&ifs))
		{
			cerr << "反序列化失败" << endl;
			return 1;
		}
	}

	// 添加新的联系人
	addPeopleInfo(mycontact);

	// 将通讯录中的数据持久化到文件中
	mycontact.SerializeToOstream(&ofs);

	ofs.close();
	google::protobuf::ShutdownProtobufLibrary();
	cout << "程序退出" << endl;
	return 0;
}
  • Makefile
bash 复制代码
PROTOSRC = contact2.proto
PROTOUT = contact2.pb.h contact2.pb.cc

write.out:write.cpp $(PROTOUT)
	g++ -o $@ $^ -std=c++17 -g -lprotobuf

$(PROTOUT):$(PROTOSRC)
	protoc --cpp_out=. $(PROTOSRC)

.PHONY:clean
clean:
	rm -f write.out contact2.pb*

这里解释一下两段特殊的代码

  • GOOGLE_PROTOBUF_VERIFY_VERSION;
  • GOOGLE_PROTOBUF_VERIFY_VERSION宏,验证没有意外链接到与编译的头文件不兼容的库版本。如果检测到版本不匹配,程序将中止。
  • 注意,每个 .pb.cc文件在启动时都会自动调用此宏。在使用 C++ Protocol Buffer 库之前执行此宏是一种很好的做法,但不是绝对必要的。
  • google::protobuf::ShutdownProtobufLibrary();
  • 在程序结束时调用 ShutdownProtobufLibrary(),为了删除 Protocol Buffer
    库分配的所有全局对象。对于大多数程序来说这是不必要的,因为该过程无论如何都要退出,并且操作系统将负责回收其所有内存。
  • 但是,如果你使用了内存泄漏检查程序,该程序需要释放每个最后对象,或者你正在编写可以由单个进程多次加载和卸载的库,那么你可能希望强制使用Protocol Buffers 来清理所有内容。

运行程序

我们使用hexdump命令来查看这个二进制文件中的内容

2、练习------从文件中反序列化后打印输出

  • read源文件
cpp 复制代码
#include <iostream>
#include <fstream>
#include <filesystem>
#include "contact2.pb.h"

using namespace std;
namespace fs = std::filesystem;


void showContact(contact::Contacts& mycontact)
{
	int sz = mycontact.peopleinfo_size();
	for (int i = 0; i < sz; ++i)
	{
		cout << "---------------联系人" << i + 1 << "-----------------\n";
		const ::contact::PeopleInfo& peopleinfo = mycontact.peopleinfo(i);
		cout << "\t姓名 : " << peopleinfo.name() << endl;
		cout << "\t年龄 : " << peopleinfo.age() << endl;
		for (int j = 0; j < peopleinfo.phone_size(); ++j)
		{
			cout << "\t电话" << j + 1 << " : " << peopleinfo.phone(j).phone_number() << endl;
		}
		cout << "-------------------------END-----------------\n";
	}
}

int main()
{
	GOOGLE_PROTOBUF_VERIFY_VERSION;
	fs::path file_path("contacts.data");
	if (!fs::exists(file_path))
	{
		cerr << "文件不存在" << endl;
		return 1;
	}
	ifstream ifs(file_path, ios::in | ios::binary);
	if (!ifs.is_open())
	{
		perror("文件打开失败");
		return 1;
	}

	// 反序列化
	contact::Contacts mycontact;
	if (!mycontact.ParseFromIstream(&ifs))
	{
		cerr << "解析失败" << endl;
		return 1;
	}

	showContact(mycontact);
	google::protobuf::ShutdownProtobufLibrary();
	cout << "程序退出" << endl;
	return 0;
}
  • Makefile
bash 复制代码
PROTOSRC = contact2.proto
PROTOUT = contact2.pb.h contact2.pb.cc

all:write.out read.out

write.out:write.cpp $(PROTOUT)
	g++ -o $@ $^ -std=c++17 -g -lprotobuf

read.out:read.cpp $(PROTOUT)
	g++ -o $@ $^ -std=c++17 -g -lprotobuf

$(PROTOUT):$(PROTOSRC)
	protoc --cpp_out=. $(PROTOSRC)

.PHONY:clean
clean:
	rm -f write.out read.out contact2.pb*

执行结果


其实我们也可以使用下面的指令也可以直接查看序列化之后的文件内容:

这个MESSAGE_TYPE 就是我们序列化的对象的在.proto文件中的类型

bash 复制代码
protoc --decode=contact.Contacts contact2.proto  < contacts.data

三、enum 类型

1、 定义规则

语法支持我们定义枚举类型并使用。在.proto文件中枚举类型的书写规范为:

  • 枚举类型名称:使用驼峰命名法,首字母大写。 例如: MyEnum
  • 常量值名称:全大写字母,多个字母之间用 _ 连接。例如: ENUM_CONST = 0;

例如下面的定义:

clike 复制代码
enum Gender
{
	MALE = 0;
	FEMALE = 1;
}

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

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

2、 定义时注意事项

将两个 具有相同枚举值名称 的枚举类型放在单个 .proto 文件下测试时,编译后会报错:某某某常量已经被定义!所以这里要注意:

  • 同级(同层)的枚举类型,各个枚举类型中的常量不能重名。
  • 单个 .proto 文件下,最外层枚举类型和嵌套枚举类型,不算同级。
  • 多个 .proto 文件下,若一个文件引入了其他文件,且每个文件都未声明 package,每个 proto 文件中的枚举类型都在最外层,算同级。
  • 多个 .proto 文件下,若一个文件引入了其他文件,且每个文件都声明了 package,不算同级。

3、查看枚举类的操作方法

我们更新.proto文件,然后进行编译查看protobuf给我们生成的方法有那些:

  • proto文件
clike 复制代码
syntax = "proto3";
package contact;

// 电话信息
message Phone
{
	string phone_number = 1;
}

// 联系人信息
message PeopleInfo
{
	// ----------------------- 新增
	// 性别
	enum Gender
	{
		MALE = 0;
		FEMALE = 1;
		SECRECY = 2;
	}

	string name = 1;
	int32 age = 2;
	// 可能有多个电话
	repeated Phone phone = 3;
	// 联系人的性别
	Gender gender = 4;
}

// 通讯录信息
message Contacts
{
	repeated PeopleInfo peopleinfo = 2;
}

当我们编译完这份代码以后,protobuf会给我们生成两部分代码:

  • 对于在.proto文件中定义的枚举类型,编译生成的代码中会含有与之对应的枚举类型校验枚举值是否有效的方法 _IsValid获取枚举值名称的方法 _Name ,将枚举值名称转化为枚举类型的方法_Parse 。(这些都是全局的方法)。

  • 对于使用了枚举类型的字段,包含设置获取 字段的方法,已经清空字段的方法clear_

4、 实际使用

  • write 文件 (这里我们只更改了addPeopleInfo 这个函数)
cpp 复制代码
void addPeopleInfo(contact::Contacts& mycontact)
{
	contact::PeopleInfo* people = mycontact.add_peopleinfo();

	cout << "-------------新增联系人-------------" << endl;
	// 添加姓名
	string name;
	cout << "请输入联系人姓名:";
	getline(cin, name);
	people->set_name(name);

	// 添加年龄
	int age;
	cout << "请输入联系人年龄:";
	cin >> age;
	// 清除age 输入以后的\n
	cin.ignore();

	// 添加手机号码
	for (int i = 1; ; ++i)
	{
		cout << "请输入联系人电话" << i << "(quit表示退出): ";
		string phone_number;
		cin >> phone_number;
		if (phone_number != "quit")
		{
			contact::Phone* phone = people->add_phone();
			phone->set_phone_number(phone_number);
		}
		else
		{
			break;
		}
	}
	cin.ignore();
	cout << "\n请输入联系人的性别 (0.男性 1.女性 2.保密) : ";
	int sex = 0;
	cin >> sex;
	switch (sex)
	{
	case 0:
		people->set_gender(contact::PeopleInfo_Gender::PeopleInfo_Gender_MALE);
		break;
	case 1:
		people->set_gender(contact::PeopleInfo_Gender::PeopleInfo_Gender_FEMALE);
		break;
	default:
		people->set_gender(contact::PeopleInfo_Gender::PeopleInfo_Gender_SECRECY);
		break;
	}
	cout << "\n-----------添加联系人成功-----------" << endl;
}
  • read 源文件(这里我们只更改了showContact 这个函数)
cpp 复制代码
void showContact(contact::Contacts& mycontact)
{
	int sz = mycontact.peopleinfo_size();
	for (int i = 0; i < sz; ++i)
	{
		cout << "---------------联系人" << i + 1 << "-----------------\n";
		const ::contact::PeopleInfo& peopleinfo = mycontact.peopleinfo(i);
		cout << "\t姓名 : " << peopleinfo.name() << endl;
		cout << "\t年龄 : " << peopleinfo.age() << endl;
		for (int j = 0; j < peopleinfo.phone_size(); ++j)
		{
			cout << "\t电话" << j + 1 << " : " << peopleinfo.phone(j).phone_number() << endl;
		}
		cout << "联系人的性别" << peopleinfo.Gender_Name(peopleinfo.gender()) << '\n';
		cout << "-------------------------END-----------------\n";
	}
}

编译之后运行,新增一个联系人以后,我们使用read 进行解析,可以看到,第一次我们序列化的结果aclie也带上了枚举类型,这是为什么呢?

实际上这可以用之前的修饰规则来解释,在proto3语法中每个字段默认都是有singular规则修饰的,因此被改规则修饰的字段在序列化的时候如果没有设置值,那么该字段不会被序列化,同理,如果在该字段的反序列化序列中找不到对应字段的编码值,那么该字段会被使用当前类型的默认值来进行填充,而我们的aclie的反序列化数据中是没有枚举数据类型的,因此在反序列化的时候,对于aclie的枚举字段则是用枚举类型的默认值来填充的,而当前枚举类型的默认值就是MALE,因此我们才看到aclie数据在枚举字段显示的是MALE;

四、Any 类型

1、 介绍

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

Any 类型是 google 已经帮我们定义好的类型,在安装 ProtoBuf 时,其中的 include 目录下查找所有google 已经定义好的 .proto 文件,因此当我们们需要使用Any类型时需要导入Any.proto文件

clike 复制代码
import "google/protobuf/any.proto";

例如我的安装目录:

bash 复制代码
ls /usr/local/protobuf/include/google/protobuf/

打开any.proto文件,可以看出Any是一个Message对象

2、查看Any类的操作方法

打开any.pb.h查看any中的的方法:

cpp 复制代码
bool PackFrom(const ::PROTOBUF_NAMESPACE_ID::Message& message);
bool UnpackTo(::PROTOBUF_NAMESPACE_ID::Message* message) const ;
bool Is() const 

接下来我们继续为通讯录中添加联系人的家庭住址

  • proto 文件
clike 复制代码
syntax = "proto3";
package contact;
import "google/protobuf/any.proto";

// 电话信息
message Phone
{
	string phone_number = 1;
}

message Address
{
	string home = 1;
	string company = 2; 
}

// 联系人信息
message PeopleInfo
{
	// 性别
	enum Gender
	{
		MALE = 0;
		FEMALE = 1;
		SECRECY = 2;
	}

	string name = 1;
	int32 age = 2;
	// 可能有多个电话
	repeated Phone phone = 3;
	// 联系人的性别
	Gender gender = 4;
	// 联系人的住址
	google.protobuf.Any data = 5;
}

// 通讯录信息
message Contacts
{
	repeated PeopleInfo peopleinfo = 2;
}

编译然后继续查看protobuf给我们生成的方法有那些:

可以看到,在包含Any类型的类中增加了下面的方法:

3、 实际使用

  • write 文件 (这里我们只更改了addPeopleInfo 这个函数)
cpp 复制代码
void addPeopleInfo(contact::Contacts& mycontact)
{
	contact::PeopleInfo* people = mycontact.add_peopleinfo();

	cout << "-------------新增联系人-------------" << endl;
	// 添加姓名
	string name;
	cout << "请输入联系人姓名:";
	getline(cin, name);
	people->set_name(name);

	// 添加年龄
	int age;
	cout << "请输入联系人年龄:";
	cin >> age;
	people->set_age(age);
	// 清除age 输入以后的\n
	cin.ignore();

	// 添加手机号码
	for (int i = 1; ; ++i)
	{
		cout << "请输入联系人电话" << i << "(quit表示退出): ";
		string phone_number;
		cin >> phone_number;
		if (phone_number != "quit")
		{
			contact::Phone* phone = people->add_phone();
			phone->set_phone_number(phone_number);
		}
		else
		{
			break;
		}
	}
	cin.ignore();
	cout << "\n请输入联系人的性别 (0.男性 1.女性 2.保密) : ";
	int sex = 0;
	cin >> sex;
	switch (sex)
	{
	case 0:
		people->set_gender(contact::PeopleInfo_Gender::PeopleInfo_Gender_MALE);
		break;
	case 1:
		people->set_gender(contact::PeopleInfo_Gender::PeopleInfo_Gender_FEMALE);
		break;
	default:
		people->set_gender(contact::PeopleInfo_Gender::PeopleInfo_Gender_SECRECY);
		break;
	}
	cin.ignore();
	cout << " 请输入联系人家庭住址 : ";
	string addr;
	getline(cin, addr);
	contact::Address address;
	address.set_home(addr);
	cout << " 请输入联系人公司住址 : ";
	getline(cin, addr);
	address.set_company(addr);
	google::protobuf::Any* data = people->mutable_data();
	data->PackFrom(address);
	cout << "\n-----------添加联系人成功-----------" << endl;
}
  • read 源文件(这里我们只更改了showContact 这个函数)
cpp 复制代码
void showContact(contact::Contacts& mycontact)
{
	int sz = mycontact.peopleinfo_size();
	for (int i = 0; i < sz; ++i)
	{
		cout << "---------------联系人" << i + 1 << "-----------------\n";
		const ::contact::PeopleInfo& peopleinfo = mycontact.peopleinfo(i);
		cout << "\t姓名 : " << peopleinfo.name() << endl;
		cout << "\t年龄 : " << peopleinfo.age() << endl;
		for (int j = 0; j < peopleinfo.phone_size(); ++j)
		{
			cout << "\t电话" << j + 1 << " : " << peopleinfo.phone(j).phone_number() << endl;
		}
		cout << "联系人的性别 : " << peopleinfo.Gender_Name(peopleinfo.gender()) << '\n';
		if (peopleinfo.has_data() && peopleinfo.data().Is<contact::Address>())
		{
			contact::Address address;
			peopleinfo.data().UnpackTo(&address);
			cout << "联系人的家庭地址 : " << address.home() << endl;
			cout << "联系人的公司地址 : " << address.company() << endl;
		}
	}
}

五、 oneof 类型

1、介绍

如果消息中有很多可选字段, 并且将来同时只有一个字段会被设置, 那么就可以使用 oneof 加强这个行为,也能有节约内存的效果,有些类似于C语言中的union联合体。

在 Protobuf 中,oneof 字段在序列化时会记录当前设置的字段类型,这有助于在反序列化时恢复正确的类型。

这里我们新增联系人的其他联系方式,比如qq或者微信号二选一,我们就可以使用 oneof 字段来加强多选一这个行为。
oneof 字段定义的格式为: oneof 字段名 { 字段1; 字段2; ... }

更新 contacts.proto ,更新内容如下:

clike 复制代码
syntax = "proto3";
package contact;
import "google/protobuf/any.proto";

// 电话信息
message Phone
{
	string phone_number = 1;
}

message Address
{
	string home = 1;
	string company = 2; 
}

// 联系人信息
message PeopleInfo
{
	// 性别
	enum Gender
	{
		MALE = 0;
		FEMALE = 1;
		SECRECY = 2;
	}

	string name = 1;
	int32 age = 2;
	// 可能有多个电话
	repeated Phone phone = 3;
	// 联系人的性别
	Gender gender = 4;
	// 联系人的住址
	google.protobuf.Any data = 5;
	// 其他联系方式
	oneof other_contact
	{
		string qq = 6;
		string wechat = 7;
	}
}

// 通讯录信息
message Contacts
{
	repeated PeopleInfo peopleinfo = 2;
}

注意:

  • oneof 中的变量隶属于上一个作用域,所以字段编号不能重复(oneof不会新增一个作用域!)
  • oneof 中的变量不能被repeated修饰

2、查看oneof类的操作方法

编译这个proto文件,查看内部生成的方法:

可以看到,protobuf为我们生成了一个枚举类:表示对应的字段名称

3、 实际使用

  • write 文件 (这里我们只更改了addPeopleInfo 这个函数)
clike 复制代码
void addPeopleInfo(contact::Contacts& mycontact)
{
	contact::PeopleInfo* people = mycontact.add_peopleinfo();

	cout << "-------------新增联系人-------------" << endl;
	// 添加姓名
	string name;
	cout << "请输入联系人姓名:";
	getline(cin, name);
	people->set_name(name);

	// 添加年龄
	int age;
	cout << "请输入联系人年龄:";
	cin >> age;
	people->set_age(age);
	// 清除age 输入以后的\n
	cin.ignore();

	// 添加手机号码
	for (int i = 1; ; ++i)
	{
		cout << "请输入联系人电话" << i << "(quit表示退出): ";
		string phone_number;
		cin >> phone_number;
		if (phone_number != "quit")
		{
			contact::Phone* phone = people->add_phone();
			phone->set_phone_number(phone_number);
		}
		else
		{
			break;
		}
	}
	cin.ignore();
	cout << "\n请输入联系人的性别 (0.男性 1.女性 2.保密) : ";
	int sex = 0;
	cin >> sex;
	switch (sex)
	{
	case 0:
		people->set_gender(contact::PeopleInfo_Gender::PeopleInfo_Gender_MALE);
		break;
	case 1:
		people->set_gender(contact::PeopleInfo_Gender::PeopleInfo_Gender_FEMALE);
		break;
	default:
		people->set_gender(contact::PeopleInfo_Gender::PeopleInfo_Gender_SECRECY);
		break;
	}
	cin.ignore();
	cout << " 请输入联系人家庭住址 : ";
	string addr;
	getline(cin, addr);
	contact::Address address;
	address.set_home(addr);
	cout << " 请输入联系人公司住址 : ";
	getline(cin, addr);
	address.set_company(addr);
	google::protobuf::Any* data = people->mutable_data();
	data->PackFrom(address);
flag:
	cout << "请输入其他的联系方式(0.qq 1.微信): ";
	int type = 0;
	cin >> type;
	cin.ignore();
	string number;
	switch (type)
	{
	case 0:
		cout << "请输入qq号码 : ";
		cin >> number;
		people->set_qq(number);
		break;
	case 1:
		cout << "请输入微信号码 : ";
		cin >> number;
		people->set_wechat(number);
		break;
	default:
		cout << "您的输入有误, 请重新输入\n";
		goto flag;
		break;
	}
	cout << "\n-----------添加联系人成功-----------" << endl;
}
  • read 源文件(这里我们只更改了showContact 这个函数)
clike 复制代码
void showContact(contact::Contacts& mycontact)
{
	int sz = mycontact.peopleinfo_size();
	for (int i = 0; i < sz; ++i)
	{
		cout << "---------------联系人" << i + 1 << "-----------------\n";
		const ::contact::PeopleInfo& peopleinfo = mycontact.peopleinfo(i);
		cout << "\t姓名 : " << peopleinfo.name() << endl;
		cout << "\t年龄 : " << peopleinfo.age() << endl;
		for (int j = 0; j < peopleinfo.phone_size(); ++j)
		{
			cout << "\t电话" << j + 1 << " : " << peopleinfo.phone(j).phone_number() << endl;
		}
		cout << "联系人的性别 : " << peopleinfo.Gender_Name(peopleinfo.gender()) << '\n';
		if (peopleinfo.has_data() && peopleinfo.data().Is<contact::Address>())
		{
			contact::Address address;
			peopleinfo.data().UnpackTo(&address);
			cout << "联系人的家庭地址 : " << address.home() << endl;
			cout << "联系人的公司地址 : " << address.company() << endl;
		}
		// 可以这样判断,但是如果种类很多代码不够简洁,这里我们采用 switch case
		// if (peopleinfo.has_qq())
		// {}
		// else if (peopleinfo.has_wechat())
		// {}

		switch (peopleinfo.other_contact_case())
		{
		case contact::PeopleInfo::kQq:
			cout << "qq号码 : " << peopleinfo.qq();
			break;
		case contact::PeopleInfo::kWechat:
			cout << "微信号码 : " << peopleinfo.wechat();
			break;
		default:
			break;
		}
		cout << "\n";
	}
}

六、map 类型

1、介绍

map 类型语法支持创建一个关联映射字段,也就是可以使用 map 类型去声明字段类型,格式为:map<key_type, value_type> map_field = N;

要注意的是:

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

利用这个特性我们在通讯录中新增联系人的备注信息,我们可以使用 map 类型的字段来存储备注信息。

  • proto文件如下:
clike 复制代码
syntax = "proto3";
package contact;
import "google/protobuf/any.proto";

// 电话信息
message Phone
{
	string phone_number = 1;
}

message Address
{
	string home = 1;
	string company = 2; 
}

// 联系人信息
message PeopleInfo
{
	// 性别
	enum Gender
	{
		MALE = 0;
		FEMALE = 1;
		SECRECY = 2;
	}

	string name = 1;
	int32 age = 2;
	// 可能有多个电话
	repeated Phone phone = 3;
	// 联系人的性别
	Gender gender = 4;
	// 联系人的住址
	google.protobuf.Any data = 5;
	// 其他联系方式
	oneof other_contact
	{
		string qq = 6;
		string wechat = 7;
	}
	// 备注信息
	map<string, string> remark = 8;
}

// 通讯录信息
message Contacts
{
	repeated PeopleInfo peopleinfo = 2;
}

2、查看Map类的操作方法

  • Map类的操作方法与unordered_map类似这里我们不再讲解

  • 在包含是Map字段的消息中会生成下面的的操作方法,(前面我们已经介绍过这里我们不在介绍)

3、 实际使用

  • write 文件 (这里我们只更改了addPeopleInfo 这个函数)
cpp 复制代码
void addPeopleInfo(contact::Contacts& mycontact)
{
	contact::PeopleInfo* people = mycontact.add_peopleinfo();

	cout << "-------------新增联系人-------------" << endl;
	// 添加姓名
	string name;
	cout << "请输入联系人姓名:";
	getline(cin, name);
	people->set_name(name);

	// 添加年龄
	int age;
	cout << "请输入联系人年龄:";
	cin >> age;
	people->set_age(age);
	// 清除age 输入以后的\n
	cin.ignore();

	// 添加手机号码
	for (int i = 1; ; ++i)
	{
		cout << "请输入联系人电话" << i << "(quit表示退出): ";
		string phone_number;
		cin >> phone_number;
		if (phone_number != "quit")
		{
			contact::Phone* phone = people->add_phone();
			phone->set_phone_number(phone_number);
		}
		else
		{
			break;
		}
	}
	cin.ignore();
	cout << "\n请输入联系人的性别 (0.男性 1.女性 2.保密) : ";
	int sex = 0;
	cin >> sex;
	switch (sex)
	{
	case 0:
		people->set_gender(contact::PeopleInfo_Gender::PeopleInfo_Gender_MALE);
		break;
	case 1:
		people->set_gender(contact::PeopleInfo_Gender::PeopleInfo_Gender_FEMALE);
		break;
	default:
		people->set_gender(contact::PeopleInfo_Gender::PeopleInfo_Gender_SECRECY);
		break;
	}
	cin.ignore();
	cout << " 请输入联系人家庭住址 : ";
	string addr;
	getline(cin, addr);
	contact::Address address;
	address.set_home(addr);
	cout << " 请输入联系人公司住址 : ";
	getline(cin, addr);
	address.set_company(addr);
	google::protobuf::Any* data = people->mutable_data();
	data->PackFrom(address);
flag:
	cout << "请输入其他的联系方式(0.qq 1.微信): ";
	int type = 0;
	cin >> type;
	cin.ignore();
	string number;
	switch (type)
	{
	case 0:
		cout << "请输入qq号码 : ";
		cin >> number;
		people->set_qq(number);
		break;
	case 1:
		cout << "请输入微信号码 : ";
		cin >> number;
		people->set_wechat(number);
		break;
	default:
		cout << "您的输入有误, 请重新输入\n";
		goto flag;
		break;
	}
	cin.ignore();
	cout << "请输入备注 : ";
	string remark;
	getline(cin, remark);
	cout << "请输入备注信息 : ";
	string content;
	getline(cin, remark);
	google::protobuf::Map<string, string>* hash = people->mutable_remark();
	hash->insert({remark, content});

	cout << "\n-----------添加联系人-----------" << endl;
}
  • read文件 (这里我们只更改了addPeopleInfo 这个函数)
cpp 复制代码
void showContact(contact::Contacts& mycontact)
{
	int sz = mycontact.peopleinfo_size();
	for (int i = 0; i < sz; ++i)
	{
		cout << "---------------联系人" << i + 1 << "-----------------\n";
		const ::contact::PeopleInfo& peopleinfo = mycontact.peopleinfo(i);
		cout << "\t姓名 : " << peopleinfo.name() << endl;
		cout << "\t年龄 : " << peopleinfo.age() << endl;
		for (int j = 0; j < peopleinfo.phone_size(); ++j)
		{
			cout << "\t电话" << j + 1 << " : " << peopleinfo.phone(j).phone_number() << endl;
		}
		cout << "联系人的性别 : " << peopleinfo.Gender_Name(peopleinfo.gender()) << '\n';
		if (peopleinfo.has_data() && peopleinfo.data().Is<contact::Address>())
		{
			contact::Address address;
			peopleinfo.data().UnpackTo(&address);
			cout << "联系人的家庭地址 : " << address.home() << endl;
			cout << "联系人的公司地址 : " << address.company() << endl;
		}
		// 可以这样判断,但是如果种类很多代码不够简洁,这里我们采用 switch case
		// if (peopleinfo.has_qq())
		// {}
		// else if (peopleinfo.has_wechat())
		// {}

		switch (peopleinfo.other_contact_case())
		{
		case contact::PeopleInfo::kQq:
			cout << "qq号码 : " << peopleinfo.qq();
			break;
		case contact::PeopleInfo::kWechat:
			cout << "微信号码 : " << peopleinfo.wechat();
			break;
		default:
			break;
		}
		cout << "\n";
		auto& hash = peopleinfo.remark();
		for (auto it = hash.begin(); it != hash.end(); ++i)
		{
			cout << "联系人备注 : " << it->first << endl;
			cout << "联系人备注信息 : " << it->second << endl;
		}
	}
}

七、总结

至此,我们的Protobuf 语法已经介绍完毕,我们现在来总结一下protobuf给我们生成的方法:

  • 内置标量类型
cpp 复制代码
xxx();                  //获取字段(const 对象)
set_xxx();              //设置字段
clear_xxx();            //清除字段
mutable_xxx();          //获取字段的地址(诸如string、bytes等类型才会生成,其它内置类型不会);
  • 数组
cpp 复制代码
xxx_size();               //获取数组元素个数;
xxx(index);               //获取第index个元素;
mutable_xxx(index);	      //获取第index个元素的;
clear_xxx();              //清理数组
add_xxx();                //新增一个元素(返回地址以便于修改)
  • message自定义类型
cpp 复制代码
has_xxx();                 //判断该字段是否被设置
xxx();                     //获取该字段(const 对象)
mutable_xxx();             //获取该字段的地址;
clear_xxx();               //清理该字段
release_xxx();			   //释放所有权给外部进行管理,自己不再进行管理
  • enum枚举字段
cpp 复制代码
// 注意:其中XXX为枚举类型名, yyy为字段名

// 三个全局方法:
XXX_IsValid(values);               //判断values是不是枚举常量
XXX_Name(values);                  //将枚举常量values变为字符串
XXX_Parse(StringName, values);     //将枚举字符串变为枚举值

// 包含枚举字段的消息内部生成的方法:
clear_yyy();                       //清理设置的枚举值
yyy();	                           //获取设置的枚举值
set_yyy();                         //设置枚举值
yyy_Name(values);                  //将枚举常量values变为字符串
yyy_Parse(StringName, values);     //将枚举字符串变为枚举值
  • Any字段
cpp 复制代码
// Any类内部方法:
PackFrom(mes);         //将mes设置给Any字段
UnpackTo(mes);         //将Any字段还原成mes对象
template< class T>
bool Is();             //判断挡墙Any字段中是不是存的T类型的值;

// 包含Any字段的消息内部生成的方法:
has_xxx();             //Any字段是否被设置
clear_xxx();           //清理
xxx();                 //获取Any字段
mutable_xxx();         //获取any字段地址
release_xxx() 		   //释放所有权给外部进行管理,自己不再进行管理
  • oneof字段
cpp 复制代码
// 注意:其中XXX为枚举类型名, yyy为字段名

// 包含oneof字段的消息内部生成的方法:
has_yyy();
yyy();
clear_yyy();
set_yyy();

clear_XXX();    //清理oneof字段里放的值
XXX_case();     //获取oneof字段使用的那个字段的枚举类型
  • map字段
cpp 复制代码
// 包含map字段的类内部生成的字段 
clear_xxx();
xxx_size();
xxx();
mutable_xxx();
  • 常用序列化和反序列化方法:
cpp 复制代码
// 常用序列化方法:
bool SerializeToOstream(ostream * output) const;     //将序列化结果放入流里面(标准流、文件流、字符串流);
bool SerializeToArray(void *data, int size) const;   //将序列化结果放入字节流里面
bool SerializeToString(string* output) const;        //将序列化结果放入字符串里面

// 常用反序列化方法:
bool ParseFromIstream(istream* input);               //从流里面读取反序列化结果;
bool ParseFromArray(const void* data, int size);     //从字节流里面读取反序列化结果;
bool ParseFromString(const string& data);            //从字符串中读取反序列化结果
相关推荐
半盏茶香17 分钟前
【C语言】分支和循环详解(下)猜数字游戏
c语言·开发语言·c++·算法·游戏
小堇不是码农24 分钟前
在VScode中配置C_C++环境
c语言·c++·vscode
Jack黄从零学c++26 分钟前
C++ 的异常处理详解
c++·经验分享
捕鲸叉6 小时前
创建线程时传递参数给线程
开发语言·c++·算法
A charmer6 小时前
【C++】vector 类深度解析:探索动态数组的奥秘
开发语言·c++·算法
Peter_chq6 小时前
【操作系统】基于环形队列的生产消费模型
linux·c语言·开发语言·c++·后端
青花瓷8 小时前
C++__XCode工程中Debug版本库向Release版本库的切换
c++·xcode
幺零九零零9 小时前
【C++】socket套接字编程
linux·服务器·网络·c++
捕鲸叉9 小时前
MVC(Model-View-Controller)模式概述
开发语言·c++·设计模式
Dola_Pan10 小时前
C++算法和竞赛:哈希算法、动态规划DP算法、贪心算法、博弈算法
c++·算法·哈希算法