【ProtoBuf 语法详解】字段规则|消息类型
文章目录
1. 字段规则
消息的字段可以用下面几种规则来修饰:
- singular: 消息中可以包含该字段零次或一次(不超过一次)。在 proto3 语法中,字段默认使用该规则。
- repeated: 消息中可以包含该字段任意多次(包括零次),其中重复值的顺序会被保留。可以理解为定义了一个数组。
下面我们更新 contacts.proto,并在 PeopleInfo 消息中新增 phone_numbers 字段,表示一个联系人有多个号码,可将其设置为 repeated。
写法如下:
syntax = "proto3";
package contacts;
// 定义联系人message
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
message PeopleInfo {
string name = 1;
int32 age = 2;
message Phone {
string number = 1;
}
}
非嵌套写法:
syntax = "proto3";
package contacts;
message Phone {
string number = 1;
}
// 定义联系人message
message PeopleInfo {
string name = 1;
int32 age = 2;
}
2.2 使用
- 消息类型可以作为字段类型使用
在 contacts.proto 文件中:
syntax = "proto3";
package contacts;
// 定义联系人message
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
message PeopleInfo
{
string name = 1;
int32 age = 2;
// 引入的文件声明了 package,使用消息时,需要用 命名空间.消息类型 格式
repeated phone.Phone phone = 3;
}
注意:在 proto3 文件中可以导入 proto2 消息类型并使用它们,反之亦然。
3. 创建通讯录 2.0 版本
通讯录 2.x 的需求是向文件中写入通讯录列表,以上我们只是定义了一个联系人的消息,并不能存放通讯录列表,所以还需要在完善一下 contacts.proto(终版通讯录 2.0):
syntax = "proto3";
package contacts;
// 定义联系人message
message PeopleInfo
{
string name = 1; // 姓名
int32 age = 2; // 年龄
message Phone {
string number = 1; // 电话号码
}
repeated Phone phone = 3; // 电话
}
// 通讯录
message ContactBook
{
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_Phone 类
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 Contactsfinal : 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);
::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_方法来新增一个值,并且提供了_size方法来判断数组存放元素的个数。
4. 通讯录 2.0 的写入实现
write.cc 代码如下:
#include <iostream>
#include <fstream>
#include "contacts.pb.h"
using namespace std;
// 新增联系人
void AddPeopleInfo(contacts::PeopleInfo *people)
{
cout << "-------------新增联系人-------------" << endl;
cout << "请输入联系人姓名:";
string name;
getline(cin, name);
people->set_name(name);
cout << "请输入联系人年龄:";
int age;
cin >> age;
people->set_age(age);
cin.ignore(256, '\n');
for (int i = 0;; i++)
{
cout << "请输入联系人电话" << i + 1 << "(只要输入回车完成电话新增):";
string number;
getline(cin, number);
if (number.empty())
{
break;
}
contacts::PeopleInfo_Phone *phone = people->add_phone();
phone->set_number(number);
}
cout << "-----------添加联系人成功-----------" << endl;
}
int main()
{
GOOGLE_PROTOBUF_VERIFY_VERSION;
contacts::ContactBook contacts;
// 读取本地已存在的通讯录文件
fstream input("contacts.bin", ios::in | ios::binary);
if (!input)
{
cout << "contacts.bin not find, create new file!" << endl;
}
else if (!contacts.ParseFromIstream(&input))
{
cerr << "parse error!" << endl;
input.close();
return -1;
}
// 向通讯录中添加一个联系人
AddPeopleInfo(contacts.add_contacts());
// 将通讯录写入本地文件中
fstream output("contacts.bin", ios::out | ios::trunc | ios::binary);
if (!contacts.SerializeToOstream(&output))
{
cerr << "write error!" << endl;
input.close();
output.close();
return -1;
}
cout << "write success!" << endl;
input.close();
output.close();
google::protobuf::ShutdownProtobufLibrary();
return 0;
}
其中 main 函数里面两个宏的作用如下:
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
然后在终端执行 make 命令,运行可执行文件,如下图所示:

然后可以使用 hexdump 命令来查看二进制文件:
hexdump -C contacts.bin
结果如下:

5. 通讯录 2.0 的读取实现
read.cc 代码如下:
#include <iostream>
#include <fstream>
#include "contacts.pb.h"
using namespace std;
using namespace contacts; // 把命名空间展开
// 打印联系人列表
void PrintContacts(ContactBook& 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;
for (int j = 0; j < people.phone_size() ; j++)
{
const PeopleInfo_Phone& phone = people.phone(j);
cout << "联系人电话" << j+1 << ":" << phone.number() << endl;
}
}
}
int main()
{
GOOGLE_PROTOBUF_VERIFY_VERSION;
ContactBook contacts;
// 读取本地已存在的通讯录文件(以二进制方式读取)
fstream input("contacts.bin", ios::in | ios::binary);
if (!contacts.ParseFromIstream(&input))
{
cerr << "parse error!" << endl;
input.close();
return -1;
}
// 打印通讯录列表
PrintContacts(contacts);
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 文件,结果如下:

6. decode 验证方法
我们可以用 protoc -h 命令来查看 ProtoBuf 为我们提供的所有命令 option。
其中 ProtoBuf 提供一个命令选项 --decode,表示从标准输入中读取给定类型的二进制消息,并将其以文本格式写入标准输出。消息类型必须在 .proto 文件或导入的文件中定义。
命令如下:
protoc --decode=contacts.ContactBook contacts.proto < contacts.bin
结果如下:

更多推荐


所有评论(0)