在语法详解部分,依旧使用 项目推进 的方式完成教学。这个部分会对通讯录进行多次升级,使用 2.x 表示升级的版本,最终将会升级如下内容:
消息的字段可以用下面几种规则来修饰:
singular:消息中可以包含该字段 零次或一次。 在 proto3 语法中,字段默认使用该规则。
repeated:消息中可以包含该字段 任意多次(包括零次),其中重复值的顺序会被保留。可以理解为定义了一个数组。
下面我们更新之前写的 contacts.proto 文件,在 PeopleInfo 消息类中新增 Phone 类型的字段并且将其设置为 repeated 规则,表示一个联系人有多个号码,写法如下:
syntax = "proto3";
package contacts1;
// 定义电话号码信息
message Phone {
string number = 1; // 不同信息类的字段编号是可以相同的
}
message PeopleInfo {
string name = 1;
int32 age = 2;
repeated Phone phone = 3; // 设置为repeated规则,表示可以有多个电话号码
} 在 单个 .proto 文件中可以定义多个消息体,且 支持定义任意多层的「嵌套」类型消息体。
并且每个消息体中的字段编号可以重复。
syntax = "proto3";
package contacts1;
// -------------------------- 1. 嵌套写法 -------------------------
message PeopleInfo {
string name = 1;
int32 age = 2;
message Phone {
string number = 1; // 不同信息类的字段编号是可以相同的
}
}
// -------------------------- 2. ⾮嵌套写法 -------------------------
message Phone {
string number = 1; // 不同信息类的字段编号是可以相同的
}
message PeopleInfo {
string name = 1;
int32 age = 2;
}syntax = "proto3";
package contacts1;
// -------------------------- 1. 嵌套写法 -------------------------
message PeopleInfo {
string name = 1;
int32 age = 2;
message Phone {
string number = 1;
}
repeated Phone phone = 3; // 在PeopleInfo需要注意编号
}
// -------------------------- 2. ⾮嵌套写法 -------------------------
message Phone {
string number = 1;
}
message PeopleInfo {
string name = 1;
int32 age = 2;
repeated Phone phone = 3; // 在PeopleInfo需要注意编号
} 通常情况下,使用 import 关键字在一个 .proto 文件中引入另一个 .proto 文件,这样可以将消息类型分割为多个文件,使代码更加模块化和可维护。
下面是注意事项:
命名空间.消息类型 格式进行定义使用。proto3 文件中可以导入 proto2 消息类型并使用它们,反之亦然。 假设现在 Phone 消息类是定义在别的 proto 文件中,如下面的 phone.proto 文件:
syntax = "proto3";
package phone;
message Phone {
string number = 1;
} 然后我们在 contacts.proto 中的 PeopleInfo 使用 Phone 消息类:
syntax = "proto3";
package contacts1;
import "phone.proto"; // 引入Phone文件
message PeopleInfo {
string name = 1;
int32 age = 2;
// 若引入的⽂件声明了package,则在使⽤该消息类时,需要⽤ “命名空间.消息类型” 格式定义
repeated phone.Phone phone = 3;
} 下面我们先来完成「前言」中提到的前两个要求,也就是结合文件操作实现通讯录的读写、序列化和反序列化操作,其中我们定义第三个要求中的姓名、年龄、电话号码字段,其它的字段得等我们后面讲「特殊类型」才能进行定义!
所以通讯录 2.x 的需求是向文件中写入通讯录列表,以上我们只是定义了一个联系人的消息,并不能存放通讯录列表,所以还需要再完善一下 contacts.proto:
syntax = "proto3";
package contacts2;
// 联系人
message PeopleInfo {
string name = 1; // 姓名
int32 age = 2; // 年龄
message Phone {
string number = 1; // 电话号码
}
repeated Phone phone = 3;
}
// 通讯录
message Contacts {
repeated PeopleInfo people = 1;
} 通过我们前面学的编译语法进行编译:
protoc --cpp_out=. contacts.proto 编译之后就能得到 contacts.pb.h 以及 contacts.pb.cc 文件,我们打开 contacts.pb.h 文件可以看到三个很眼熟的类:

没错,它们就是我们在 proto 中的消息体,protobuf 会自动将它们转化为对应语言的类!
然后我们再观察一下三个类内一些常用的接口:
// 新增了 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 people = 1;
int people_size() const;
void clear_people();
::PeopleInfo* mutable_people(int index);
::PROTOBUF_NAMESPACE_ID::RepeatedPtrField< ::PeopleInfo >* mutable_people();
const ::PeopleInfo& people(int index) const;
::PeopleInfo* add_people();
const ::PROTOBUF_NAMESPACE_ID::RepeatedPtrField< ::PeopleInfo >& people() const;从上述的例子中可以看到:
clear_ 方法,可以将字段重新设置回 empty 状态。mutable_ 方法,返回值为消息类型的指针,这类方法会为我们开辟好空间,可以直接对这块空间的内容进行修改。repeated 修饰的字段,也就是数组类型,protobuf 为我们提供了 add_ 方法来新增一个值,并且提供了 _size 方法来查看数组存放元素的个数。下面我们通过实现以下需求,来学习对这些头文件中函数的使用:
首先是 write.cc 文件,来实现第一个需求,首先就是要读取已有的通讯录,然后再添加联系人,最后写入文件中,这用到 fstream 对象来操作比较方便,细节都在下面代码注释中:
// write.cc
#include "contacts.pb.h"
#include <iostream>
#include <fstream>
using namespace std;
void addPeople(::contacts2::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'); // 去除缓冲区中的回车(而上面的getline则不需要,因为getline会清除缓冲区的回车)
for(int i = 1; ; i++)
{
cout << "请输入联系人电话" << i << "(只输入回车则退出):";
string number;
getline(cin, number);
if(number.empty())
break;
auto phone = people->add_phone();
phone->set_number(number);
}
cout << "-----------添加联系⼈成功-----------" << endl;
}
int main()
{
contacts2::Contacts contacts;
// 1. 读取本地已存在的通讯录
fstream input("./contacts.bin", ios::in | ios::binary); // protobuf是二进制操作
if(!input.is_open())
cout << "contacts.bin not found, create new file!" << endl;
else if(!contacts.ParseFromIstream(&input))
{
// 这个操作建议学起来!
// 直接通过ParseFromIstream()获取文件流进行反序列化,还能判断是否成功,一步到位!
cerr << "parse contacts.bin error!" << endl;
input.close();
return -1;
}
input.close();
// 2. 向通讯录中添加一个联系人
addPeople(contacts.add_people());
// 3. 将通讯录写入本地文件中
fstream output("./contacts.bin", ios::out | ios::binary | ios::trunc); // 记得要覆盖写
if(!contacts.SerializeToOstream(&output))
{
// 上面的操作,这里同理!
cerr << "write contacts.bin error!" << endl;
output.close();
return -1;
}
cout << "write success" << endl;
output.close();
return 0;
}
// makefile文件:
write:write.cc contacts.pb.cc
g++ -o $@ $^ -std=c++11 -lprotobuf
.PHONY:clean
clean:
rm -f write
这里介绍一个 hexdump 工具,可以将 .bin 文件中的二进制数据转化为十六进制和 ASCII 码方便我们观察,命令如下所示:
hexdump -C 二进制文件
然后就是 read.cc 文件,负责获取通讯录序列化之后打印出各个联系人的信息:
// read.cc
#include "contacts.pb.h"
#include <iostream>
#include <fstream>
using namespace std;
void printContacts(contacts2::Contacts& contacts)
{
for(int i = 0; i < contacts.people_size(); ++i)
{
cout << "------------联系⼈" << i + 1 << "------------" << endl;
cout << "名称:" << contacts.people(i).name() << endl;
cout << "年龄:" << contacts.people(i).age() << endl;
for(int j = 0 ; j < contacts.people(i).phone_size(); ++j)
cout << "手机号码" << j + 1 << ":" << contacts.people(i).phone(j).number() << endl;
cout << "------------------------------" << endl;
}
}
int main()
{
contacts2::Contacts contacts;
// 1. 读取本地已存在的通讯录(默认存在,故不需要判断是否存在)
fstream input("./contacts.bin", ios::in | ios::binary); // protobuf是二进制操作
if(!contacts.ParseFromIstream(&input))
{
// 这个操作建议学起来!
// 直接通过ParseFromIstream()获取文件流进行反序列化,还能判断是否成功,一步到位!
cerr << "parse contacts.bin error!" << endl;
input.close();
return -1;
}
input.close();
// 2. 打印通讯录列表
printContacts(contacts);
return 0;
}
其实我们可以不用像上面那么麻烦专门写个程序查看当前序列化的内容,可以直接使用 protoc -h 命令来查看 ProtoBuf 为我们提供的所有命令选项。
其中 ProtoBuf 提供一个 命令选项 --decode ,表示从标准输入中读取给定类型的二进制消息,并将其以文本格式写入标准输出。 注意消息类型必须在 .proto 文件或导入的文件中定义!

非常方便是吧!