前言
我们依旧使用项目推进的方式去认识proto3语法,这个部分会对通讯录进行多次升级,使用2.x 表示升级的版本,最终将会升级如下内容:
- 不再打印联系人的序列化结果,而是将通讯录序列化后并写入文件中。
- 从文件中将通讯录解析出来,并进行打印。
- 新增联系人属性,共包括:姓名、年龄、电话信息、地址、其他联系方式、备注。
字段规则
消息的字段可以用下面几种规则来修饰:
- singular :消息中可以包含该字段零次或一次(不超过一次)。 proto3 语法中,字段默认使用该规则。(如果设置多了,以最后一次设置的值为主)
- repeated :消息中可以包含该字段任意多次(包括零次),其中重复值的顺序会被保留。可以理解为定义了一个数组。
更新 contacts.proto , PeopleInfo 消息中新增 phone_numbers 字段,表示一个联系人有多个号码,可将其设置为 repeated,写法如下:
// 定义联系人message
message PeopleInfo(){
string name = 1; // 名字
int32 age = 2 ; // 年龄
repeated string phone_numbers = 3;
}
下面还可以定义消息体,比如电话信息包含多个电话、电话号呢类型等,下面定义消息体可以解决这个问题。
消息类型的定义与使用
定义
在单个 .proto 文件中可以定义多个消息体,且支持定义嵌套类型的消息(任意多层)。每个消息体中的字段编号可以重复。
更新 contacts.proto,我们可以将 phone_number 提取出来,单独成为一个消息:
// -------------------------- 嵌套写法 -------------------------
// 定义联系人message
message PeopleInfo(){
string name = 1; // 名字
int32 age = 2 ; // 年龄
message Phone{
string numbers = 1;
}
}
// -------------------------- 非嵌套写法 -------------------------
message Phone{
string numbers = 1;
}
// 定义联系人message
message PeopleInfo(){
string name = 1; // 名字
int32 age = 2 ; // 年龄
}
使用
- 消息类型可作为字段类型使用
contacts.proto
// -------------------------- 嵌套写法 -------------------------
syntax = "proto3";
package contacts2;
// 定义联系人message
message PeopleInfo(){
string name = 1; // 名字
int32 age = 2 ; // 年龄
message Phone{
string numbers = 1;
}
repeated Phone phone = 3; //使用
}
// -------------------------- 非嵌套写法 -------------------------
syntax = "proto3";
package contacts2;
message Phone{
string numbers = 1;
}
// 定义联系人message
message PeopleInfo(){
string name = 1; // 名字
int32 age = 2 ; // 年龄
repeated Phone phone = 3; 、、使用
}
- 可导入其他 .proto 文件的消息并使用
例如 Phone 消息定义在 phone.proto 文件中:
syntax = "proto3";
package phone;
message Phone{
string numbers = 1;
}
contacts.proto 中的 PeopleInfo 使用 Phone 消息:
syntax = "proto3";
package contacts2;
import "phone.proto"; // 使用 import 将 phone.proto 文件导入进来 !!!
// 定义联系人message
message PeopleInfo(){
string name = 1; // 名字
int32 age = 2 ; // 年龄
// 引入的文件声明了package,使用消息时,需要用 ‘命名空间.消息类型’ 格式
repeated phone.Phone phone = 3;
}
注:在 proto3 文件中可以导入 proto2 消息类型并使用它们,反之亦然。
创建通讯录 2.0 版本
通讯录 2.x 的需求是向文件中写入通讯录列表,以上我们只是定义了一个联系人的消息,并不能存放通讯录列表,所以还需要在完善一下 contacts.proto (终版通讯录 2.0):
syntax = "proto3";
package contacts2;
// 定义联系人message
message PeopleInfo{
string name = 1; // 名字
int32 age = 2 ; // 年龄
message Phone{
string numbers = 1;
}
repeated Phone phone = 3; // 电话信息
}
// 通讯录message
message Contacts{
repeated PeopleInfo contacts = 1;
}
接着进行一次编译:
protoc --cpp_out=. contacts.proto
contacts.pb.h 更新的部分代码展示:

上述的例子中:
- 每个字段都有一个 clear_ 方法,可以将字段重新设置回 empty 状态。
- 每个字段都有设置和获取的方法, 获取方法的方法名称与小写字段名称完全相同。但如果是消息类型的字段,其设置方法为 mutable_ 方法,返回值为消息类型的指针,这类方法会为我们开辟好空间,可以直接对这块空间的内容进行修改。
- 对于使用 repeated 修饰的字段,也就是数组类型,pb 为我们提供了 add_ 方法来新增一个值,并且提供了 _size 方法来判断数组存放元素的个数。
通讯录 2.0 的写入实现
write.cc (通讯录 2.0)
#include <iostream>
#include <fstream>
#include <limits> // 用于处理输入缓冲区的清理
// 引入protobuf自动生成的头文件,包含通讯录数据结构定义
#include "contacts.pb.h"
using namespace std;
//@brief 是 Doxygen 的标签,表示 “简要说明”。
//@param 是 Doxygen 的标签,表示 “函数参数说明”。
/**
* @brief 向联系人信息对象中添加数据
* @param people 指向待填充数据的PeopleInfo对象的指针
*/
void AddPeopleInfo(contacts2::PeopleInfo* people){
cout << "-------------新增联系人-------------" << endl;
// 读取联系人姓名
cout << "请输入联系人姓名: ";
string name;
getline(cin, name); // 使用getline可读取包含空格的完整姓名
people->set_name(name); // 调用protobuf生成的setter方法设置姓名
// 读取联系人年龄
cout << "请输入联系人年龄: ";
int age;
cin >> age; // 读取整数年龄
people->set_age(age); // 设置年龄
// 清理输入缓冲区中的残留换行符
// numeric_limits<streamsize>::max()确保清空所有剩余字符
cin.ignore(numeric_limits<streamsize>::max(), '\n');
// 循环读取多个电话号码,直到用户输入空行为止
for(int i=0; ; i++){
cout << "请输入联系人电话: " << i+1 << "(只输入回车完成电话新增): ";
string number;
getline(cin, number); // 读取电话号码
// 如果用户输入空行,则退出循环,结束电话号码输入
if(number.empty()){
break;
}
// 调用protobuf生成的add_phone()方法添加新电话
// 该方法会返回一个指向新创建的Phone对象的指针
contacts2::PeopleInfo_Phone* phone = people->add_phone();
phone->set_numbers(number); // 设置电话号码
}
cout << "-------------添加联系人成功-------------" << endl;
}
int main()
{
// 验证protobuf库版本是否与编译时一致,确保兼容性
GOOGLE_PROTOBUF_VERIFY_VERSION;
// 创建通讯录主对象,用于存储所有联系人信息
contacts2::Contacts contacts;
// 尝试读取本地已存在的通讯录文件
fstream input("contacts.bin", ios::in | ios::binary);
if(!input){
// 文件不存在时提示将创建新文件
cout << "contacts.bin not find, creat new file! " << endl;
}else if(!contacts.ParseFromIstream(&input)){
// 解析文件失败时输出错误信息并退出
cerr << "parse error!" << endl;
input.close();
return -1;
}
// 关闭输入流,避免资源占用
input.close();
// 向通讯录中添加新联系人
// add_contacts()方法会创建一个新的PeopleInfo对象并返回其指针
AddPeopleInfo(contacts.add_contacts());
// 打开文件用于写入更新后的通讯录数据
// ios::out: 输出模式;ios::trunc: 若文件存在则清空;ios::binary: 二进制模式
fstream output("contacts.bin", ios::out | ios::trunc | ios::binary);
if(!contacts.SerializeToOstream(&output)){
// 序列化并写入失败时处理
cerr << "write error!" << endl;
output.close();
return -1;
}
// 提示写入成功并关闭输出流
cout << "write success!" << endl;
output.close();
// 清理protobuf库分配的全局资源,适合在长时间运行的程序中使用
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.0 的读取实现
read.cc (通讯录 2.0)
#include <iostream>
#include <fstream>
// 引入protobuf自动生成的通讯录数据结构定义头文件
#include "contacts.pb.h"
using namespace std;
//@brief 是 Doxygen 的标签,表示 “简要说明”。
//@param 是 Doxygen 的标签,表示 “函数参数说明”。
/**
* @brief 打印通讯录中的所有联系人信息
* @param contacts 常量引用,指向存储所有联系人数据的protobuf对象
* 使用const确保函数不会修改原始数据,提高安全性
*/
void PrintContacts(const contacts2::Contacts& contacts){
// 遍历所有联系人,contacts_size()返回联系人总数
for(int i = 0; i < contacts.contacts_size(); i++){
cout << "--------------------联系人" << i+1 << "--------------------" << endl;
// 通过contacts(i)获取第i个联系人的详细信息(protobuf生成的访问方法)
const contacts2::PeopleInfo& people = contacts.contacts(i);
// 打印联系人基本信息,调用protobuf生成的getter方法(name()、age())
cout << "联系人姓名:" << people.name() << endl;
cout << "联系人年龄:" << people.age() << endl;
// 遍历当前联系人的所有电话号码,phone_size()返回电话号码总数
for(int j= 0; j < people.phone_size(); j++){
// 通过phone(j)获取第j个电话号码对象
const contacts2::PeopleInfo_Phone& phone = people.phone(j);
cout << "联系人电话" << j+1 << ":" << phone.numbers() << endl;
}
}
}
int main(){
// 创建protobuf通讯录对象,用于存储从文件读取的数据
contacts2::Contacts contacts;
// 尝试打开本地通讯录文件(二进制读模式)
fstream input("contacts.bin", ios::in | ios::binary);
// 检查文件是否成功打开
if (!input.is_open()) {
cerr << "无法打开文件 contacts.bin!" << endl;
return -1;
}
// 从文件流中解析protobuf数据(反序列化过程)
else if(!contacts.ParseFromIstream(&input)){
cerr << "解析文件失败! 可能是文件格式错误或损坏" << endl;
input.close(); // 出错时关闭文件流
return -1;
}
// 关闭输入文件流(数据已读取完毕)
input.close();
// 调用打印函数,展示所有联系人信息
PrintContacts(contacts);
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 文件或导入的文件中定义。

protoc --decode=contacts2.Contacts contacts.proto < contacts.bin
这个命令是使用 Protocol Buffers(protobuf)工具protoc来解码二进制数据的指令,各部分含义如下:
protoc:protobuf 的官方命令行工具,用于处理.proto文件(协议定义文件)
--decode=contacts2.Contacts:指定解码模式,并指定要解码成的消息类型
contacts2是消息类型所在的包名(package)
Contacts是具体的消息类型名称
contacts.proto:协议定义文件,其中包含了contacts2.Contacts消息类型的结构定义
< contacts.bin:通过管道(pipe)将contacts.bin文件中的二进制数据作为输入传递给protoc
整个命令的作用是:使用contacts.proto中定义的contacts2.Contacts消息结构,来解码contacts.bin文件中的二进制数据,并将解码后的结果以人类可读的文本形式输出到控制台。
这通常用于调试 protobuf 数据,查看二进制文件中实际存储的内容。执行该命令前需要确保contacts.proto文件存在且正确定义了contacts2.Contacts消息类型。

enum 类型
定义规则
语法支持我们定义枚举类型并使用。在.proto文件中枚举类型的书写规范为:
枚举类型名称: 使用驼峰命名法,首字母大写。 例如: MyEnum
常量值名称: 全大写字母,多个字母之间用 _ 连接。例如:ENUM_CONST = 0;
我们可以定义一个名为 PhoneType 的枚举类型,定义如下:
// -------------------------- 非嵌套写法 -------------------------
syntax = "proto3";
package test_enum;
enum PhoneType{
MP = 0; //移动电话
TEL = 1; //固定电话
}
message Phone{
}
// -------------------------- 嵌套写法 -------------------------
syntax = "proto3";
package test_enum;
message Phone{
//主要这里要看清楚是嵌套再message里面,而不是enum
enum PhoneType{
MP = 0; //移动电话
TEL = 1; //固定电话
}
}
要注意枚举类型的定义有以下几种规则:
- 0 值常量必须存在,且要作为第一个元素。这是为了与 proto2 的语义兼容:第一个元素作为默认值,且值为 0。
- 枚举类型可以在消息外定义,也可以在消息体内定义(嵌套)。
- 枚举的常量值在 32 位整数的范围内。但因负值无效因而不建议使用(与编码规则有关)。
定义时注意
将两个 ‘具有相同枚举值名称’ 的枚举类型放在单个 .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; // 移动电话
}
升级通讯录至 2.1 版本
更新 contacts.proto (通讯录 2.1),新增枚举字段并使用,更新内容如下:
syntax = "proto3";
package contacts2;
// 定义联系人message
message PeopleInfo{
string name = 1; // 名字
int32 age = 2 ; // 年龄
message Phone{
string numbers = 1;
enum PhoneType{
MP = 0; // 移动电话
TEL = 1; // 固定电话
}
PhoneType type = 2; // 类型
}
repeated Phone phone = 3; // 电话信息
}
// 通讯录message
message Contacts{
repeated PeopleInfo contacts = 1;
}
编译
protoc --cpp_out=. contacts.proto
contacts.pb.h 更新的部分代码展示:
// 新生成的 PeopleInfo_Phone_PhoneType 枚举类
enum PeopleInfo_Phone_PhoneType : int {
PeopleInfo_Phone_PhoneType_MP = 0,
PeopleInfo_Phone_PhoneType_TEL = 1,
PeopleInfo_Phone_PhoneType_PeopleInfo_Phone_PhoneType_INT_MIN_SENTINEL_DO_NOT_U
SE_ = std::numeric_limits<int32_t>::min(),
PeopleInfo_Phone_PhoneType_PeopleInfo_Phone_PhoneType_INT_MAX_SENTINEL_DO_NOT_U
SE_ = std::numeric_limits<int32_t>::max()
};
// 更新的 PeopleInfo_Phone 类
class PeopleInfo_Phone final : public ::PROTOBUF_NAMESPACE_ID::Message {
public:
typedef PeopleInfo_Phone_PhoneType PhoneType;
static inline bool PhoneType_IsValid(int value) {
return PeopleInfo_Phone_PhoneType_IsValid(value);
}
template<typename T>
static inline const std::string& PhoneType_Name(T enum_t_value) {
...}
static inline bool PhoneType_Parse(
::PROTOBUF_NAMESPACE_ID::ConstStringParam name, PhoneType* value) {
...}
// .contacts.PeopleInfo.Phone.PhoneType type = 2;
void clear_type();
::contacts::PeopleInfo_Phone_PhoneType type() const;
void set_type(::contacts::PeopleInfo_Phone_PhoneType value);
}
上述的代码中:
- 对于在.proto文件中定义的枚举类型,编译生成的代码中会含有与之对应的枚举类型、校验枚举值是否有效的方法 _IsValid、以及获取枚举值名称的方法 _Name。
- 对于使用了枚举类型的字段,包含设置和获取字段的方法,已经清空字段的方法clear_。
更新 write.cc (通讯录 2.1)
#include <iostream>
#include <fstream>
#include <limits> // 用于处理输入缓冲区的清理
// 引入protobuf自动生成的头文件,包含通讯录数据结构定义
#include "contacts.pb.h"
using namespace std;
//@brief 是 Doxygen 的标签,表示 “简要说明”。
//@param 是 Doxygen 的标签,表示 “函数参数说明”。
/**
* @brief 向联系人信息对象中添加数据
* @param people 指向待填充数据的PeopleInfo对象的指针
*/
void AddPeopleInfo(contacts2::PeopleInfo* people){
cout << "-------------新增联系人-------------" << endl;
// 读取联系人姓名
cout << "请输入联系人姓名: ";
string name;
getline(cin, name); // 使用getline可读取包含空格的完整姓名
people->set_name(name); // 调用protobuf生成的setter方法设置姓名
// 读取联系人年龄
cout << "请输入联系人年龄: ";
int age;
cin >> age; // 读取整数年龄
people->set_age(age); // 设置年龄
// 清理输入缓冲区中的残留换行符
// cin.ignore(256,'\n');
// numeric_limits<streamsize>::max()确保清空所有剩余字符
cin.ignore(numeric_limits<streamsize>::max(), '\n');
// 循环读取多个电话号码,直到用户输入空行为止
for(int i=0; ; i++){
cout << "请输入联系人电话: " << i+1 << "(只输入回车完成电话新增): ";
string number;
getline(cin, number); // 读取电话号码
// 如果用户输入空行,则退出循环,结束电话号码输入
if(number.empty()){
break;
}
// 调用protobuf生成的add_phone()方法添加新电话
// 该方法会返回一个指向新创建的Phone对象的指针
contacts2::PeopleInfo_Phone* phone = people->add_phone();
phone->set_numbers(number); // 设置电话号码
cout << "选择此电话号码类型(1、移动电话 2、固定电话): ";
int type;
cin >> type;
// 清理输入缓冲区中的残留换行符
// cin.ignore(256,'\n');
// numeric_limits<streamsize>::max()确保清空所有剩余字符
cin.ignore(numeric_limits<streamsize>::max(), '\n');
switch (type)
{
case 1:
phone->set_type(contacts2::PeopleInfo_Phone_PhoneType::PeopleInfo_Phone_PhoneType_MP);
break;
case 2:
phone->set_type(contacts2::PeopleInfo_Phone_PhoneType::PeopleInfo_Phone_PhoneType_TEL);
break;
default:
cout << "选择有误!!" << endl;
break;
}
}
cout << "-------------添加联系人成功-------------" << endl;
}
int main()
{
// 验证protobuf库版本是否与编译时一致,确保兼容性
GOOGLE_PROTOBUF_VERIFY_VERSION;
// 创建通讯录主对象,用于存储所有联系人信息
contacts2::Contacts contacts;
// 尝试读取本地已存在的通讯录文件
fstream input("contacts.bin", ios::in | ios::binary);
if(!input){
// 文件不存在时提示将创建新文件
cout << "contacts.bin not find, creat new file! " << endl;
}else if(!contacts.ParseFromIstream(&input)){
// 解析文件失败时输出错误信息并退出
cerr << "parse error!" << endl;
input.close();
return -1;
}
// 关闭输入流,避免资源占用
input.close();
// 向通讯录中添加新联系人
// add_contacts()方法会创建一个新的PeopleInfo对象并返回其指针
AddPeopleInfo(contacts.add_contacts());
// 打开文件用于写入更新后的通讯录数据
// ios::out: 输出模式;ios::trunc: 若文件存在则清空;ios::binary: 二进制模式
fstream output("contacts.bin", ios::out | ios::trunc | ios::binary);
if(!contacts.SerializeToOstream(&output)){
// 序列化并写入失败时处理
cerr << "write error!" << endl;
output.close();
return -1;
}
// 提示写入成功并关闭输出流
cout << "write success!" << endl;
output.close();
// 清理protobuf库分配的全局资源,适合在长时间运行的程序中使用
google::protobuf::ShutdownProtobufLibrary();
return 0;
}
更新 read.cc (通讯录 2.1)
#include <iostream>
#include <fstream>
// 引入protobuf自动生成的通讯录数据结构定义头文件
#include "contacts.pb.h"
using namespace std;
//@brief 是 Doxygen 的标签,表示 “简要说明”。
//@param 是 Doxygen 的标签,表示 “函数参数说明”。
/**
* @brief 打印通讯录中的所有联系人信息
* @param contacts 常量引用,指向存储所有联系人数据的protobuf对象
* 使用const确保函数不会修改原始数据,提高安全性
*/
void PrintContacts(const contacts2::Contacts& contacts){
// 遍历所有联系人,contacts_size()返回联系人总数
for(int i = 0; i < contacts.contacts_size(); i++){
cout << "--------------------联系人" << i+1 << "--------------------" << endl;
// 通过contacts(i)获取第i个联系人的详细信息(protobuf生成的访问方法)
const contacts2::PeopleInfo& people = contacts.contacts(i);
// 打印联系人基本信息,调用protobuf生成的getter方法(name()、age())
cout << "联系人姓名:" << people.name() << endl;
cout << "联系人年龄:" << people.age() << endl;
// 遍历当前联系人的所有电话号码,phone_size()返回电话号码总数
for(int j= 0; j < people.phone_size(); j++){
// 通过phone(j)获取第j个电话号码对象
const contacts2::PeopleInfo_Phone& phone = people.phone(j);
cout << "联系人电话" << j+1 << ":" << phone.numbers();
//希望格式 联系人电话1:12312312312 (MP)
// phone.type()打印的格式是int类型也就是枚举的数字,用 phone.PhoneType_Name()将枚举的数字值转换为对应的名称字符串。
cout << " (" << phone.PhoneType_Name(phone.type()) << ")" << endl;
}
}
}
int main(){
// 创建protobuf通讯录对象,用于存储从文件读取的数据
contacts2::Contacts contacts;
// 尝试打开本地通讯录文件(二进制读模式)
fstream input("contacts.bin", ios::in | ios::binary);
// 检查文件是否成功打开


2851

被折叠的 条评论
为什么被折叠?



