Protobuf proto3 语法讲解(1)
个人主页:C++忠实粉丝
欢迎 点赞👍 收藏✨ 留言✉ 加关注💓本文由 C++忠实粉丝 原创Protobuf proto3 语法讲解(1)
收录于专栏[Protobuf的学习与使用]
本专栏旨在分享学习Protobuf的一点学习笔记,欢迎大家在评论区交流讨论💌
目录
1. 字段规则
2. 消息类型的定义与使用
2.1 定义
2.2 使用
2.3 创建通讯录 2.0 版本
2.3.1 通讯录 2.0 的写入实现
2.3.2 通讯录 2.0 的读取实现
3. enum 类型
3.1 定义规则
3.2 定义时注意
3.3 升级通讯录至 2.1 版本
在语法详解部分,依旧使用 项目推进 的方式。这个部分会对之前的通讯录进行多次升级,使用 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;
}
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"; // 使用 import 将 phone.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
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;
}
接着进行一次编译:
protoc --cpp_1 out=. contacts.proto
编译后生成的 contacts.pb.h contacts.pb.cc 会将在快速上手的生成文件覆盖掉。
contacts.pb.h 更新的部分代码展示:
上述例子中:
1. 每个字段都有一个 clear_ 方法,可以将字段重新设置回 empty 状态。
2. 每个字段都有设置和回去的方法,获取方法的方法名称与小写字段名称完全相同。但如果是消息类型的字段,其设置方法为 mutable_ 方法,返回值为消息类型的指针,这类方法会为我们开辟好空间,可以直接对这块空间的内容进行修改。
3. 对于使用 repeated 修饰的字段,也就是数组类型,pb 为我们提供了 add_ 方法来新增一个值,并且提供了 _size 方法来判断数组存放元素的个数。
2.3.1 通讯录 2.0 的写入实现
write.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 宏: 验证没有意外链接到与编译的头文件不兼容的库版
// 本。如果检测到版本不匹配,程序将中止。注意,每个 .pb.cc 文件在启动时都会自动调用此宏。在使
// 用 C++ Protocol Buffer 库之前执行此宏是一种很好的做法,但不是绝对必要的。
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();
// 在程序结束时调用 ShutdownProtobufLibrary(),为了删除 Protocol Buffer 库分配的所
// 有全局对象。对于大多数程序来说这是不必要的,因为该过程无论如何都要退出,并且操作系统将负责
// 回收其所有内存。但是,如果你使用了内存泄漏检查程序,该程序需要释放每个最后对象,或者你正在
// 编写可以由单个进程多次加载和卸载的库,那么你可能希望强制使用 Protocol Buffers 来清理所有内容。
google::protobuf::ShutdownProtobufLibrary();
return 0;
}
Makefile:
write:write.cc contacts.pb.cc
g++ -o $@ $^ -std=c++11 -lprotobuf
.PHONY:clean
clean:
rm -f write
make之后,运行 write:
查看二进制文件:
hexdump:是Linux下的一个二进制文件查看工具,它可以将二进制文件转换为ASCII、八进制、
十进制、十六进制格式进行查看。
-C: 表示每个字节显示为16进制和相应的ASCII字符
2.3.2 通讯录 2.0 的读取实现
read.cc (通讯录 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
make 后运行 read:
另一种验证方法--decode
我们可以用 protoc -h 命令来查看 ProtoBuf 为我们提供的所有命令 option。其中 ProtoBuf 提供
一个命令选项 --decode ,表示从标准输入中读取给定类型的二进制消息,并将其以文本格式写入
标准输出。 消息类型必须在 .proto 文件或导入的文件中定义。
3. enum 类型
3.1 定义规则
语法支持我们定义枚举类型并使用。在.proto文件中枚举类型的书写规范为:
枚举类型名称:
使用驼峰命名法,首字母大写。 例如: MyEnum
常量值名称:
全大写字母,多个字母之间用 _ 连接。例如: ENUM_CONST = 0;
我们可以定义一个名为 PhoneType 的枚举类型,定义如下:
enum PhoneType {
MP = 0; // 移动电话
TEL = 1; // 固定电话
}
要注意枚举类型的定义有一下几种规则:
1. 0值常量必须存在,且要作为第一个元素。这是为了与 proto2 的语义兼容:第一个元素作为默认值,且值为0.
2. 枚举类型可以在消息外定义,也可以在消息体内定义(嵌套)。
3. 枚举的常量值在 32 位整数的范围内。但因负值无效因而不建议使用(与编码规则有关)。
3.2 定义时注意
将两个 ‘具有相同枚举值名称’ 的枚举类型放在单个 .proto 文件下测试时,编译后会报错:某某某常
量已经被定义!所以这里要注意:
• 同级(同层)的枚举类型,各个枚举类型中的常量不能重名。
• 单个 .proto 文件下,最外层枚举类型和嵌套枚举类型,不算同级。
• 多个 .proto 文件下,若一个文件引入了其他文件,且每个文件都未声明 package,每个 proto 文
件中的枚举类型都在最外层,算同级。
• 多个 .proto 文件下,若一个文件引入了其他文件,且每个文件都声明了 package,不算同级。
// ---------------------- 情况1:同级枚举类型包含相同枚举值名称--------------------
enum PhoneType
{
MP = 0; // 移动电话
TEL = 1; // 固定电话
}
enum PhoneTypeCopy
{
MP = 0; // 移动电话 // 编译后报错:MP 已经定义
}
// ---------------------- 情况2:不同级枚举类型包含相同枚举值名称-------------------
-
enum PhoneTypeCopy {
MP = 0; // 移动电话 // 用法正确
}
message Phone
{
string number = 1; // 电话号码
enum PhoneType
{
MP = 0; // 移动电话
TEL = 1; // 固定电话
}
}
// ---------------------- 情况3:多文件下都未声明package--------------------
// phone1.proto
import "phone1.proto" enum PhoneType{
MP = 0; // 移动电话 // 编译后报错:MP 已经定义
TEL = 1; // 固定电话
}
// phone2.proto
enum PhoneTypeCopy
{
MP = 0; // 移动电话
}
// ---------------------- 情况4:多文件下都声明了package--------------------
// phone1.proto
import "phone1.proto" package phone1;
enum PhoneType
{
MP = 0; // 移动电话 // 用法正确
TEL = 1; // 固定电话
}
// phone2.proto
package phone2;
enum PhoneTypeCopy
{
MP = 0; // 移动电话
}
3.3 升级通讯录至 2.1 版本
更新 contacts.proto (通讯录 2.1),新增枚举字段并使用,更新内容如下:
// 通讯录 2.1
syntax = "proto3";
package contacts;
// 联系人
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;
}
• 对于在.proto文件中定义的枚举类型,编译生成的代码中会含有与之对应的枚举类型、校验枚举值是否有效的方法 _IsValid、以及获取枚举值名称的方法 _Name。
• 对于使用了枚举类型的字段,包含设置和获取字段的方法,已经清空字段的方法clear_。
更新 write.cc (通讯录 2.1)
//********** wirte 2.1 */
#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 << "选择此电话类型 (1、移动电话 2、固定电话) : ";
int type;
cin >> type;
cin.ignore(256, '\n');
switch (type)
{
case 1:
phone->set_type(PeopleInfo_Phone_PhoneType::PeopleInfo_Phone_PhoneType_MP);
break;
case 2:
phone->set_type(PeopleInfo_Phone_PhoneType::PeopleInfo_Phone_PhoneType_TEL);
break;
default:
cout << "非法选择,使用默认值!" << endl;
break;
}
}
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;
}
更新 read.cc (通讯录 2.1)
//****************** read 2.1 */
#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();
cout << " (" << phone.PhoneType_Name(phone.type()) << ")" << 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;
}
代码完成后,编译后进行读写验证: