文章目录
- ProtoBuf
- 5. proto3语法详解
- 5.1 字段规则
- 5.2 消息类型的定义与使用
ProtoBuf
5. proto3语法详解
在语法详解部分,依旧通过项目推进的方式开展教学。此部分会对通讯录多次升级,用 2.x 表示升级的版本,最终将完成以下内容的升级:
(1)不再打印联系人的序列化结果,而是把通讯录序列化后写入文件。
(2)从文件中解析出通讯录,并予以打印。
(3)新增联系人属性,涵盖:姓名、年龄、电话信息、地址、其他联系方式、备注。
5.1 字段规则
消息的字段能够运用以下几种规则加以修饰:
singular :消息中能够包含该字段零次或者一次(不超过一次)。在 proto3 语法里,字段默认采用该规则。
我们在 Protobuf 定义的消息结构中,对于被标记为 singular 规则的字段,在一条消息里,这个字段可以不出现(即零次),也可以出现一次,但最多只能出现一次。
repeated :消息中能够包含该字段任意多次(包含零次),其中重复值的顺序会得以保留。
这意味着在我们定义的消息结构中,对于被标记为 “repeated” 规则的字段,在一条消息里,它可以出现零次、一次或者多次,没有数量上的限制。可以理解为定义了一个数组。
接下来我们就可以使用repeated字段在通讯录中添加我们的电话字段了:
syntax = "proto3";
package contacts;
message PeopleInfo {
string name = 1;
int32 age = 2;
repeated string phone_numbers = 3;
}
protoc编译文件:
protoc --cpp_out=. contacts.proto
查看contacts.pb.h文件,这就是自动生成的有关repeated的函数:
有关repeated类型的函数,下面以phone_numbers为例(自动生成的函数名和我们设置的字段名相关):
获取相关函数:
int phone_numbers_size() const;
:这个函数用于获取 phone_numbers 字段中元素的数量。
const std::string& phone_numbers(int index) const;
:通过指定索引 index,获取 phone_numbers 中对应位置的元素,返回的是常量引用,意味着不能通过这个返回值修改元素。
清空相关函数:
void clear_phone_numbers();
:该函数用于清空 phone_numbers 字段中的所有元素。
修改相关函数:
std::string* mutable_phone_numbers(int index);
:通过索引获取 phone_numbers 中对应位置元素的指针,可通过这个指针修改对应位置的元素。
void set_phone_numbers(int index, const std::string& value);
:通过指定索引和一个字符串常量值,设置 phone_numbers 中对应位置的元素。
void set_phone_numbers(int index, std::string&& value);
:通过指定索引和一个右值引用的字符串,设置对应位置的元素。
void set_phone_numbers(int index, const char* value);
:通过指定索引和一个字符指针,设置对应位置的元素。
void set_phone_numbers(int index, const char* value, size_t size);
:通过指定索引、字符指针以及字符数量,设置对应位置的元素。
添加相关函数:
std::string* add_phone_numbers();
:用于向 phone_numbers 字段添加一个新元素,并返回指向新添加元素的指针,以便进行后续修改。
void add_phone_numbers(const std::string& value);
:向 phone_numbers 字段添加一个字符串常量值。
void add_phone_numbers(std::string&& value);
:向 phone_numbers 字段添加一个右值引用的字符串。
void add_phone_numbers(const char* value);
:向 phone_numbers 字段添加一个通过字符指针表示的字符串。
void add_phone_numbers(const char* value, size_t size);
:向 phone_numbers 字段添加一个通过字符指针和字符数量表示的字符串。
整体获取 / 修改相关函数:
const ::PROTOBUF_NAMESPACE_ID::RepeatedPtrField < std::string> & phone_numbers() const;
:获取整个 phone_numbers 字段的常量引用。
::PROTOBUF_NAMESPACE_ID::RepeatedPtrField < std::string> * mutable_phone_numbers();
:获取整个 phone_numbers 字段的可修改指针。
5.2 消息类型的定义与使用
在单个 .proto 文件中可以定义多个消息体,且支持定义嵌套类型的消息(任意多层)。每个消息体的字段编号可以重复。
单个 .proto 文件嵌套写法:
syntax = "proto3";
package contacts;
// 定义联系⼈消息
message PeopleInfo {
string name = 1; // 姓名
int32 age = 2; // 年龄
// 可以在消息字段中定义消息字段
message Phone{
string number = 1;
}
}
单个 .proto 文件非嵌套写法:
syntax = "proto3";
package contacts;
message Phone{
string number = 1;
}
// 定义联系⼈消息
message PeopleInfo {
string name = 1; // 姓名
int32 age = 2; // 年龄
}
多个 .proto 文件导入头文件写法:
导入其它.proto文件:import path/xxx.proto
,引入的文件声明了package,使用其类型时需要用 命名空间 . 消息类型
的格式。
// 这个是a.proto文件
syntax = "proto3";
package A;
message Phone{
string number = 1;
}
// 这个是contacts.proto文件
syntax = "proto3";
package contacts;
import "a.proto";
// 定义联系⼈消息
message PeopleInfo {
string name = 1; // 姓名
int32 age = 2; // 年龄
// 引用包A,创建Phone类型消息字段,定义为phone
A.Phone phone = 3; // 使用类外定义
}
我们的通讯录程序又得以再次的优化了,在 PeopleInfo 消息中:name:字符串类型,代表姓名。age:32 位整数类型,代表年龄。
嵌套的 Phone 消息,其中包含 number 字段,为字符串类型,代表电话号码。phone:Phone 类型的重复字段,代表电话信息。
在 Contacts 消息中:contacts:PeopleInfo 类型的重复字段,代表通讯录中的联系人信息。
syntax = "proto3";
package contacts;
// 联系⼈
message PeopleInfo {
string name = 1; // 姓名
int32 age = 2; // 年龄
message Phone {
string number = 1; // 电话号码
}
repeated Phone phone = 3; // 电话
}
// 通讯录
message Contacts {
repeated PeopleInfo contacts = 1;
}
编译再次生成我们所需要的 .h 和 .cc文件:
通讯录 2.0 的写入实现:
#include <iostream>
#include <fstream>
#include "contacts.pb.h"
using namespace std;
using namespace contacts;
// 新增联系⼈
void AddPeopleInfo(PeopleInfo *people_info_ptr)
{
cout << "-------------新增联系⼈-------------" << endl;
cout << "请输⼊联系⼈姓名: ";
string name;
getline(cin, name);
people_info_ptr->set_name(name);
cout << "请输⼊联系⼈年龄: ";
int age;
cin >> age;
people_info_ptr->set_age(age);
cin.ignore(256, '\n');
for (int i = 1;; i++)
{
cout << "请输⼊联系⼈电话" << i << "(只输⼊回⻋完成电话新增): ";
string number;
getline(cin, number);
if (number.empty())
{
break;
}
PeopleInfo_Phone *phone = people_info_ptr->add_phone();
phone->set_number(number);
}
cout << "-----------添加联系⼈成功-----------" << endl;
}
int main(int argc, char *argv[])
{
GOOGLE_PROTOBUF_VERIFY_VERSION;
if (argc != 2)
{
cerr << "Usage: " << argv[0] << " CONTACTS_FILE" << endl;
return -1;
}
Contacts contacts;
// 先读取已存在的 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;
}
GOOGLE_PROTOBUF_VERIFY_VERSION
宏用于验证所链接的库版本与编译的头文件是否兼容。若检测到版本不匹配,程序会中止。每个 .pb.cc 文件启动时会自动调用此宏。在使用 C++ Protocol Buffer 库前执行该宏是良好做法。
在程序结束时可调用 ShutdownProtobufLibrary()
来删除 Protocol Buffer 库分配的所有全局对象。对多数程序而言这并非必要,因程序退出时操作系统会回收内存。但如果使用内存泄漏检查程序,或编写可被单个进程多次加载和卸载的库,可能需要强制 Protocol Buffers 清理所有内容。
Makefile:
write:write.cc contacts.pb.cc
g++ -o $@ $^ -std=c++11 -lprotobuf
.PHONY:clean
clean:
rm -f write
接着就可以向我们指定的contacts.bin文件中写入信息了:
查看二进制文件:
hexdump 是 Linux 中的二进制文件查看工具,能把二进制文件转换成 ASCII、八进制、十进制、十六进制格式来查看。其中 -C 选项意味着每个字节会以十六进制形式和对应的 ASCII 字符显示。
decode 是 ProtoBuf 中通过 protoc -h 命令可查看到的一个命令选项 --decode 。它的作用是从标准输入读取给定类型的二进制消息,并将其以文本格式输出到标准输出。而且,该消息的类型必须在 .proto 文件或导入的文件中有定义。
具体用法如下: protoc --decode=MESSAGE_TYPE MESSAGE_TYPE所在文件
protoc --decode=contacts.Contacts contacts.proto < contacts.bin
通讯录 2.0 的读取实现:
#include <iostream>
#include <fstream>
#include "contacts.pb.h"
using namespace std;
using namespace contacts;
//打印联系⼈列表
void PrintfContacts(const Contacts &contacts)
{
for (int i = 0; i < contacts.contacts_size(); ++i)
{
const PeopleInfo &people = contacts.contacts(i);
cout << "------------联系⼈" << i + 1 << "------------" << endl;
cout << "姓名:" << people.name() << endl;
cout << "年龄:" << people.age() << endl;
int j = 1;
for (const PeopleInfo_Phone &phone : people.phone())
{
cout << "电话" << j++ << ": " << phone.number() << endl;
}
}
}
int main(int argc, char *argv[])
{
GOOGLE_PROTOBUF_VERIFY_VERSION;
if (argc != 2)
{
cerr << "Usage: " << argv[0] << "CONTACTS_FILE" << endl;
return -1;
}
// 以⼆进制⽅式读取 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;
}
Makefile:
all:write read
write:write.cc contacts.pb.cc
g++ -o $@ $^ -std=c++11 -lprotobuf
read:read.cc contacts.pb.cc
g++ -o $@ $^ -std=c++11 -lprotobuf
.PHONY:clean
clean:
rm -f write read
我们可以读取到write写入文件的信息了: