
proto3 语法详解
在语法详解部分,依旧使用项目推进的方式来演示。这个部分会对通讯录进行多次升级,使用 2.x 表示升级的版本,最终将会介绍如下内容:
- 不再打印联系人的序列化结果,而是将通讯录序列化后并写入文件中。
- 从文件中将通讯录解析出来,并进行打印。
- 新增联系人属性,共包括:姓名、年龄、电话信息、地址、其他联系方式、备注。
1. 字段规则
消息的字段可以用下面几种规则来修饰:
- singular:消息中可以包含该字段零次或一次(不超过一次)。proto3 语法中,字段默认使用该规则。
- repeated:消息中可以包含该字段任意多次(包括零次),其中重复的顺序会被保留。可以理解为定义了一个 "数组"。
更新contacts.proto,在PeopleInfo消息中新增phone_numbers字段,表示一个联系人有多个号码,可将其设置为repeated,写法如下:
cpp
syntax = "proto3";
package contacts;
message PeopleInfo {
string name = 1;
int32 age = 2;
repeated string phone_numbers = 3;
}
2. 消息类型的定义与使用
2.1 定义
在单个.proto文件中可以定义多个 "消息" 体,且支持定义嵌套类型的消息(任意多层)。每个消息中的字段编号可以重复。(这个 message 就相当于 C++ 的类)
更新contacts.proto,我们可以将phone_number提取出来,单独成为一个消息:
cpp
message Phone {
string number = 1;
}
message PeopleInfo {
string name = 1;
int32 age = 2;
2.2 使用
- 消息类型可作为字段类型使用
contacts.proto
cpp
syntax = "proto3";
package contacts;
message PeopleInfo {
string name = 1;
int32 age = 2;
message Phone {
string number = 1;
}
repeated Phone phone = 3;
}
cpp
package contacts;
import "phone.proto"; // 使用import将phone.proto文件导入进来!!!
message PeopleInfo {
string name = 1;
int32 age = 2;
// 引入文件声明了package,使用消息时,需要用"命名空间.消息类"格式导入
repeated phone.Phone phone = 3;
}
注:在.proto文件中可以导入.proto2消息类型并使用它们,反之亦然。
2.3 创建通讯录 2.0 版本
通讯录 2.0 的需求是向文件中写入通讯录列表,以上我们只是定义了一个联系人的消息,并不能存放通讯录列表,所以还需要完善一下contacts.proto(形成通讯录 2.0):
cpp
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;
}
接着进行一次编译:
bash
protoc --cpp_out=. contacts.proto
编译后生成的contacts.pb.h、contacts.pb.cc会将在快速上手的生成文件覆盖掉。
contacts.pb.h更新的部分代码展示
cpp
void set_allocated_number(std::string* number);
// ==== PeopleInfo ====
class PeopleInfo final : public ::PROTOBUF_NAMESPACE_ID::Message {
public:
using CopyFrom = ::PROTOBUF_NAMESPACE_ID::Message::CopyFrom;
using MergeFrom = ::PROTOBUF_NAMESPACE_ID::Message::MergeFrom;
using Proto = PeopleInfo;
void MergeFrom(const PeopleInfo& from) {
::PROTOBUF_NAMESPACE_ID::internal::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();
::PROTOBUF_NAMESPACE_ID::RepeatedPtrField<PeopleInfo_Phone>*
mutable_phone();
const ::PROTOBUF_NAMESPACE_ID::RepeatedPtrField<PeopleInfo_Phone>&
phone() const;
::PeopleInfo_Phone* mutable_phone(int index);
::PeopleInfo_Phone* add_phone();
const ::PeopleInfo_Phone& phone(int index) const;
};
// ==== Contacts ====
class Contacts final : public ::PROTOBUF_NAMESPACE_ID::Message {
public:
using CopyFrom = ::PROTOBUF_NAMESPACE_ID::Message::CopyFrom;
using MergeFrom = ::PROTOBUF_NAMESPACE_ID::Message::MergeFrom;
using Proto = Contacts;
static ::PROTOBUF_NAMESPACE_ID::StringPiece FullMessageName() {
return "Contacts";
}
// repeated PeopleInfo contacts = 1;
int contacts_size() const;
void clear_contacts();
::PROTOBUF_NAMESPACE_ID::RepeatedPtrField<PeopleInfo>*
mutable_contacts();
const ::PROTOBUF_NAMESPACE_ID::RepeatedPtrField<PeopleInfo>&
contacts() const;
::PeopleInfo* mutable_contacts(int index);
::PeopleInfo* add_contacts();
const ::PeopleInfo& contacts(int index) const;
};
上述的例子中:
- 每个字段都有一个
clear_方法,可以将字段重新设置回empty状态。 - 每个字段都有设置和获取的方法,获取方法的方法名称都与小写字段名完全相同。但如果是消息类型的字段,可以直接对方法返回的内容进行访问,返回值为消息类型的指针,这类方法会为我们开辟好空间,可以直接设置方法的
mutable_方法。 - 对于使用
repeated修饰的字段,也就是数组类型,pb为我们提供了add_方法来新增一个值,并且提供了_size方法来判断数组存放元素的个数。
2.3.1 通讯录 2.0 的写入实现
write.cc(通讯录 2.0)
cpp
#include <iostream>
#include "contacts.pb.h"
#include <fstream>
using namespace std;
using namespace contacts;
// 添加联系人
void AddPeopleInfo(PeopleInfo* people_info_ptr) {
cout << "-------新增联系人-------" << endl;
cout << "请输入联系人姓名:";
string name;
cin >> name;
people_info_ptr->set_name(name);
cout << "请输入联系人年龄:";
int age;
cin >> age;
people_info_ptr->set_age(age);
while (true) {
cout << "请输入联系人电话(只输入回车完成电话新增):";
string number;
cin >> number;
if (number.empty()) {
break;
}
PeopleInfo_Phone* phone_ptr = people_info_ptr->add_phone();
phone_ptr->set_number(number);
}
}
int main(int argc, char* argv[]) {
// 调用GOOGLE_PROTOBUF_VERIFY_VERSION,检查protobuf库版本是否与编译版本兼容。
// 这个宏必须在调用任何其他库函数之前调用,但是在调用ParseFromIstream之后可以安全地释放。
GOOGLE_PROTOBUF_VERIFY_VERSION;
if (argc != 2) {
cerr << "Usage: " << argv[0] << " CONTACTS_FILE" << endl;
return -1;
}
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;
return -1;
}
// 添加一个联系人
AddPeopleInfo(contacts.add_contacts());
// 将序列化后的数据写入文件
fstream output(argv[1], ios::out | ios::trunc | ios::binary);
if (!contacts.SerializeToOstream(&output)) {
cerr << "Failed to write contacts." << endl;
return -1;
}
// 可选:删除所有已分配的protobuf对象。
// 对于大多数程序来说,这不是必要的,因为进程退出时会自动释放内存。
// 但是在某些情况下,比如在长时间运行的服务器进程中,可能需要手动释放内存以避免内存泄漏。
google::protobuf::ShutdownProtobufLibrary();
return 0;
}
makefile
bash
write:write.cc contacts.pb.cc
g++ -o $@ $^ -std=c++11 -lprotobuf
.PHONY:clean
clean:
rm -f write
make之后,运行write
bash
g++ -o write write.cc contacts.pb.cc -std=c++11 -lprotobuf
hb@13-159-156-152:~/project/protobuf/contacts1$ ./write contacts.bin
contacts.bin: File not found. Creating a new file.
-------新增联系人-------
请输入联系人姓名:张三
请输入联系人年龄:20
请输入联系人电话(只输入回车完成电话新增):13111111111
请输入联系人电话(只输入回车完成电话新增):15111111111
请输入联系人电话(只输入回车完成电话新增):
查看二进制文件
cpp
hb@13-159-156-152:~/project/protobuf/contacts1$ xxd contacts.bin
00000000: 0a20 0a06 e5bc a0e4 b889 1014 1a0b 3133 . ............13
00000010: 3131 3131 3131 3131 3131 311a0b 3135 3131 1111111111..1511
00000020: 3131 3131 3131 3131 3131 1111111111
解释:xxd是 Linux 下的一个 "二进制" 文件查看工具,它可以将 "二进制" 文件转换为 ASCII、八进制、十进制、十六进制格式进行查看。
- 十六进制:十六个数字(0~f)进行编码和对应的 ASCII 字符
2.3.2 通讯录 2.0 的读取实现
read.cc(通讯录 2.0)
cpp
#include <iostream>
#include "contacts.pb.h"
#include <fstream>
using namespace std;
using namespace contacts;
// 打印联系人列表
void PrintContacts(const Contacts& contacts) {
for (int i = 0; i < contacts.contacts_size(); ++i) {
const PeopleInfo& people_info = contacts.contacts(i);
cout << "-------联系人" << i+1 << "-------" << endl;
cout << "姓名:" << people_info.name() << endl;
cout << "年龄:" << people_info.age() << endl;
cout << "电话:" << endl;
for (int j = 0; j < people_info.phone_size(); ++j) {
const PeopleInfo_Phone& phone = people_info.phone(j);
cout << " " << 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;
fstream input(argv[1], ios::in | ios::binary);
if (!input) {
cerr << argv[1] << ": File not found." << endl;
return -1;
} else if (!contacts.ParseFromIstream(&input)) {
cerr << "Failed to parse contacts." << endl;
return -1;
}
PrintContacts(contacts);
google::protobuf::ShutdownProtobufLibrary();
return 0;
}
makefile
bash
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
bash
hb@13-159-156-152:~/project/protobuf/contacts1$ make
g++ -o write write.cc contacts.pb.cc -std=c++11 -lprotobuf
g++ -o read read.cc contacts.pb.cc -std=c++11 -lprotobuf
hb@13-159-156-152:~/project/protobuf/contacts1$ ./read contacts.bin
-------联系人1-------
姓名:张三
年龄:20
电话:
13111111111
15111111111
另一种调试工具
我们可以用protoc命令,来查看protobuf为我们提供的所有命令option,执行protoc --help提供一个选项,消息--decode选项,表示从标准输入中读取定义的二进制消息,并将其以文本格式写入标准输出(消歧义:decode在protobuf文件 / 程序的二进制中,是 "解析")。
bash
hb@13-159-156-152:~/project/protobuf/contacts1$ protoc --decode contacts.Contacts contacts.proto < contacts.bin
contacts {
name: "\345\274\240\344\270\211" // 在这里是将utf-8汉字转为八进制格式输出了
age: 20
phone {
number: "13111111111"
}
phone {
number: "15111111111"
}
}
3. enum 类型
3.1 定义规则
在 proto 文件中定义枚举类型并使用,枚举类型的书写规范为:
- 枚举类型名称 :使用驼峰命名法,首字母大写。例如:
PhoneType - 常量值名称 :全大写字母,多个单词之间用
_连接。例如:ENM_CONST = 0;
我们定义一个名为PhoneType的枚举类型,定义如下:
cpp
enum PhoneType {
MP = 0; // 移动电话
TEL = 1; // 固定电话
}
要注意枚举类型的定义有以下几种规则:
- 0 值常量必须存在 ,且要作为第一个元素。这是为了与 proto2 的语义兼容:第一个元素为默认值,且值为 0。
- 枚举类型可以在消息外定义,也可以在消息体内定义(嵌套)。
- 枚举的常量值在 32 位整数的范围内,但因负值无效故而不建议使用(与编码规则有关)。
3.2 定义时注意
将两个 "具有相同枚举值名称" 的枚举类型放在单个.proto 文件下测试时,编译器会报错:某某某常量已被定义。所以这里要注意:
- 同级(同层)的枚举类型,各个枚举类型中的常量不能重名。
- 单个.proto 文件下,最外层枚举类型和嵌套枚举类型,不算同级。
- 多个.proto 文件下,若一个文件引入了其他文件,且每个文件都未声明 package,每个 proto 文件中的枚举类型都在最外层,算同级。
- 多个.proto 文件下,若一个文件引入了其他文件,且每个文件都声明了 package,不算同级。
情况 1:同级枚举类型包含相同枚举名称
cpp
// 同级枚举类型包含相同枚举名称
enum PhoneType {
MP = 0; // 移动电话
TEL = 1; // 固定电话
}
enum PhoneTypeCopy {
MP = 0; // 移动电话 // 编译后报错:MP 已经定义
}
情况 2:不同级枚举类型包含相同枚举名称
cpp
// 不同级枚举类型包含相同枚举名称
enum PhoneType {
MP = 0; // 移动电话 // 用法正确
}
message Phone {
string number = 1; // 电话号码
enum PhoneType {
MP = 0; // 移动电话
TEL = 1; // 固定电话
}
PhoneType type = 2; // 类型
}
情况 3:多文件下未声明 package
cpp
// phone1.proto
import "phone2.proto"
enum PhoneType {
MP = 0; // 移动电话 // 编译后报错:MP 已经定义
TEL = 1; // 固定电话
}
// phone2.proto
enum PhoneTypeCopy {
MP = 0; // 移动电话
}
情况 4:多文件下声明了 package
cpp
// phone1.proto
import "phone2.proto"
package phone1;
enum PhoneType {
MP = 0; // 移动电话 // 用法正确
TEL = 1; // 固定电话
}
// phone2.proto
package phone2;
enum PhoneTypeCopy {
MP = 0; // 移动电话
}
3.3 升级通讯录至 2.1 版本
更新contact.proto(通讯录 2.1),新增枚举字段并使用,更新内容如下:
cpp
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;
}
执行编译命令:protoc -cpp_out=. contacts.proto
contacts.pb.h更新的部分代码展示:
cpp
// 枚举 PeopleInfo_Phone_PhoneType
enum PeopleInfo_Phone_PhoneType {
PeopleInfo_Phone_PhoneType_MP = 0,
PeopleInfo_Phone_PhoneType_TEL = 1,
PeopleInfo_Phone_PhoneType_PeopleInfo_Phone_PhoneType_INT_MIN_SENTINEL_DO_NOT_USE_ = std::numeric_limits<int32_t>::min(),
PeopleInfo_Phone_PhoneType_PeopleInfo_Phone_PhoneType_INT_MAX_SENTINEL_DO_NOT_USE_ = std::numeric_limits<int32_t>::max()
};
// 类 PeopleInfo_Phone
class PeopleInfo_Phone final : public ::PROTOBUF_NAMESPACE_ID::Message {
public:
typedef PeopleInfo_Phone_PhoneType PhoneType;
static bool PhoneType_IsValid(int value) {
return PeopleInfo_Phone_PhoneType_IsValid(value);
}
static const std::string& PhoneType_Name(PhoneType value) { ... }
static bool PhoneType_Parse(const std::string& name, PhoneType* value) { ... }
// 字段 type
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):
cpp
#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;
cin >> name;
people_info_ptr->set_name(name);
cout << "请输入联系人年龄:";
int age;
cin >> age;
people_info_ptr->set_age(age);
cout << "请输入联系人电话(输入-1 结束):" << endl;
while (1) {
cout << "请输入联系人电话:";
string number;
cin >> number;
if (number == "-1") {
break;
}
PeopleInfo::Phone* phone = people_info_ptr->add_phone();
phone->set_number(number);
cout << "请选择电话类型(1.移动电话 2.固定电话):";
int type;
cin >> type;
switch (type) {
case 1:
phone->set_type(PeopleInfo::Phone::PhoneType::MP);
break;
case 2:
phone->set_type(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;
// 先读取已存在的通讯录
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;
return -1;
}
// 添加一个联系人
AddPeopleInfo(contacts.add_contacts());
// 再把通讯录写入到文件中
fstream output(argv[1], ios::out | ios::trunc | ios::binary);
if (!contacts.SerializeToOstream(&output)) {
cerr << "Failed to write contacts." << endl;
return -1;
}
output.close();
input.close();
return 0;
}
更新 read.cc(通讯录 2.1)
cpp
#include <iostream>
#include <fstream>
#include "contacts.pb.h"
using namespace std;
using namespace contacts;
/*
* 打印联系人列表
*/
void PrintContacts(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;
for (int j = 0; j < people.phone_size(); ++j) {
const PeopleInfo::Phone& phone = people.phone(j);
cout << "电话" << j+1 << ":" << phone.number() << "("
<< PeopleInfo::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;
fstream input(argv[1], ios::in | ios::binary);
if (!contacts.ParseFromIstream(&input)) {
cerr << "Failed to parse contacts." << endl;
return -1;
}
// 打印通讯录
PrintContacts(contacts);
input.close();
google::protobuf::ShutdownProtobufLibrary();
return 0;
}
代码完成后,编译后进行读写验证:
bash
# 编译 write 程序
g++ write.cc contacts.pb.cc -o write_contacts -lprotobuf
# 运行 write 程序
./write_contacts test.data
运行输出:
bash
----------添加联系人----------
请输入联系人姓名:李四
请输入联系人年龄:25
请输入联系人电话(输入-1 结束):
请输入联系人电话:123456
请选择电话类型(1.移动电话 2.固定电话):1
请输入联系人电话:-1
----------添加联系人成功----------
bash
# 编译 read 程序
g++ read.cc contacts.pb.cc -o read_contacts -lprotobuf
# 运行 read 程序
./read_contacts test.data
运行输出:
bash
----------联系人1----------
姓名:李四
年龄:25
电话1:123456(MP)
注:这里打印
MP是因为设置了默认值,导致取了枚举的第一个值。
4. Any 类型
字段可以声明为 Any 类型,可理解为泛型类型。使用时可以在 Any 中存储任意消息类型。Any 类型的字段也用 repeated 来修饰。
Any 类型是 Google 已经帮我们定义好的类型,在安装 Protobuf 时,其中的 include 目录下查找所有 Google 已经定义好的 .proto 文件。
bash
hyb@192-159-150-152:~/project/protobuf/contact$ cd /usr/local/protobuf/include/google/protobuf/
hyb@192-159-150-152:/usr/local/protobuf/include/google/protobuf$ ls
any.pb.h descriptor.proto map_field.h
any.pb.cc descriptor.pb.h map_field.hpp
any.proto descriptor.pb.cc map_field.inl
api.pb.h descriptor_set.h map_field_lite.h
api.pb.cc descriptor_set.inl map.h
api.proto field_access_listener.h map_type_handler.h
struct.pb.h field_mask.pb.h message.h
struct.proto field_mask.pb.cc message_lite.h
arena.h field_mask.proto metadata.h
arena_impl.h generated_enum_reflection.h metadata_lite.h
arena_port.h generated_enum_util.h parse_context.h
test_arith.h generated_message_bases.h port_def.inc
test_arith.pb.h generated_message_reflection.h port.h
test_arith.proto generated_message_table_driven.h port_undef.inc
test_simple.h generated_message_table_driven_impl.h reflection.h
test_simple.pb.h generated_message_util.h reflection_internal.h
test_simple.proto has_bits.h reflection_ops.h
compiler implicit_weak_message.h repeated_field.h
type.pb.h inline_string_field.h repeated_ptr_field.h
descriptor_database.h io
descriptor.h map_entry.h
unknown_field_set.h
util
duration.pb.h
duration.pb.cc
duration.proto
wire_format.h
wire_format_lite.h
wrappers.pb.h
wrappers.pb.cc
wrappers.proto
dynamic_message.h
empty.pb.h
empty.pb.cc
empty.proto
4.1 升级通讯录至 2.2 版本
通讯录 2.2 版本会新增联系人的地址信息,我们可以使用 any 类型的字段来存储地址信息。
更新 contact.proto(通讯录 2.2),更新内容如下:
cpp
syntax = "proto3";
package contacts;
import "google/protobuf/any.proto"; // 引入 any.proto 文件
// 地址
message Address {
string home_address = 1; // 家庭地址
string unit_address = 2; // 单位地址
}
// 联系人
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; // 电话
google.protobuf.Any data = 4;
}
// 通讯录
message Contacts {
repeated PeopleInfo contacts = 1;
}
执行编译命令:protoc -cpp_out=. contacts.proto
4.2 contacts.pb.h更新的部分代码展示:
cpp
// 枚举 Address
class Address final : public ::PROTOBUF_NAMESPACE_ID::Message {
public:
using ::PROTOBUF_NAMESPACE_ID::Message::CopyFrom;
void CopyFrom(const Address& from);
using ::PROTOBUF_NAMESPACE_ID::Message::MergeFrom;
void MergeFrom(const Address& from);
Address();
~Address();
// string home_address = 1;
void clear_home_address();
const std::string& home_address() const;
template <typename Arg0 = const std::string&, typename... ArgT>
void set_home_address(Arg0&& arg0, ArgT&&... args);
void set_allocated_home_address(std::string* home_address);
PROTOBUF_NODISCARD std::string* release_home_address();
// string unit_address = 2;
void clear_unit_address();
const std::string& unit_address() const;
template <typename Arg0 = const std::string&, typename... ArgT>
void set_unit_address(Arg0&& arg0, ArgT&&... args);
void set_allocated_unit_address(std::string* unit_address);
PROTOBUF_NODISCARD std::string* release_unit_address();
};
// 类 PeopleInfo
class PeopleInfo final : public ::PROTOBUF_NAMESPACE_ID::Message {
public:
// google.protobuf.Any data = 4;
bool has_data() const;
void clear_data();
const ::PROTOBUF_NAMESPACE_ID::Any& data() const;
PROTOBUF_NODISCARD ::PROTOBUF_NAMESPACE_ID::Any* release_data();
::PROTOBUF_NAMESPACE_ID::Any* mutable_data();
void set_allocated_data(::PROTOBUF_NAMESPACE_ID::Any* data);
};
上述代码中:对于 Any 类型字段:
- 设置和获取 :获取方法的方法名称与小写字段名完全相同;设置方法可以使用
mutable_方法,返回值为 Any 类型的指针,这类方法会为我们开辟好空间,可以直接对这块空间的内容进行修改。
之前讲过,我们可以在 Any 字段中存储任意消息类型,这就涉及到任意消息类型和 Any 类型的互转。这部分代码就在 Google 为我们写好的头文件 any.pb.h 中。对 any.pb.h 部分代码展示:
cpp
class PROTOBUF_EXPORT Any final : public ::PROTOBUF_NAMESPACE_ID::Message {
bool PackFrom(const ::PROTOBUF_NAMESPACE_ID::Message& message) {
...
}
bool UnpackTo(::PROTOBUF_NAMESPACE_ID::Message* message) const {
...
}
template<typename T> bool Is() const {
return GetTypeName().empty() || any_metadata_.Is<T>();
}
};
解释:
- 使用
PackFrom()方法可以将任意消息类型转为 Any 类型。 - 使用
UnpackTo()方法可以将 Any 类型转回之前设置的任意消息类型。 - 使用
Is<T>()方法可以用来判断所存放的消息类型是否为typename T。
更新 write.cc(通讯录 2.2)
cpp
#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;
cin >> name;
people_info_ptr->set_name(name);
cout << "请输入联系人年龄:";
int age;
cin >> age;
people_info_ptr->set_age(age);
for (int i = 1; ; i++) {
cout << "请输入联系人电话" << i << "(输入-1 结束):";
string number;
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;
switch (type) {
case 1:
phone->set_type(PeopleInfo::Phone::PhoneType::MP);
break;
case 2:
phone->set_type(PeopleInfo::Phone::PhoneType::TEL);
break;
default:
cout << "非法选项,使用默认值!" << endl;
break;
}
}
Address address;
cout << "请输入联系人家庭地址:";
string home_address;
getline(cin, home_address);
address.set_home_address(home_address);
cout << "请输入联系人单位地址:";
string unit_address;
getline(cin, unit_address);
address.set_unit_address(unit_address);
google::protobuf::Any* data = people_info_ptr->mutable_data();
data->PackFrom(address);
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;
// 先读取已存在的通讯录
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;
return -1;
}
// 添加一个联系人
AddPeopleInfo(contacts.add_contacts());
// 再把通讯录写入到文件中
fstream output(argv[1], ios::out | ios::trunc | ios::binary);
if (!contacts.SerializeToOstream(&output)) {
cerr << "Failed to write contacts." << endl;
return -1;
}
output.close();
input.close();
return 0;
}
更新 read.cc(通讯录 2.2)
cpp
#include <iostream>
#include <fstream>
#include "contacts.pb.h"
using namespace std;
using namespace contacts;
/*
* 打印联系人列表
*/
void PrintContacts(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;
for (int j = 0; j < people.phone_size(); ++j) {
const PeopleInfo::Phone& phone = people.phone(j);
cout << "电话" << j+1 << ":" << phone.number() << "("
<< PeopleInfo::Phone::PhoneType_Name(phone.type())
<< ")" << endl;
}
if (people.has_data() && people.data().Is<Address>()) {
Address address;
people.data().UnpackTo(&address);
if (!address.home_address().empty()) {
cout << "家庭地址:" << address.home_address() << endl;
}
if (!address.unit_address().empty()) {
cout << "单位地址:" << address.unit_address() << 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;
fstream input(argv[1], ios::in | ios::binary);
if (!contacts.ParseFromIstream(&input)) {
cerr << "Failed to parse contacts." << endl;
return -1;
}
// 打印通讯录
PrintContacts(contacts);
input.close();
google::protobuf::ShutdownProtobufLibrary();
return 0;
}
代码完成后,编译后进行读写验证:
bash
# 编译 write 程序
g++ write.cc contacts.pb.cc -o write_contacts -lprotobuf
# 运行 write 程序
./write_contacts test.data
运行输出:
bash
----------添加联系人----------
请输入联系人姓名:王五
请输入联系人年龄:40
请输入联系人电话1(输入-1 结束):123456
请选择电话类型(1.移动电话 2.固定电话):1
请输入联系人电话2(输入-1 结束):654321
请选择电话类型(1.移动电话 2.固定电话):2
请输入联系人电话3(输入-1 结束):-1
请输入联系人家庭地址:陕西省西安市
请输入联系人单位地址:陕西省西安市
----------添加联系人成功----------
bash
# 编译 read 程序
g++ read.cc contacts.pb.cc -o read_contacts -lprotobuf
# 运行 read 程序
./read_contacts test.data
运行输出:
bash
----------联系人1----------
姓名:王五
年龄:40
电话1:123456(MP)
电话2:654321(TEL)
家庭地址:陕西省西安市
单位地址:陕西省西安市
5. oneof 类型
如果消息中有很多可选字段,并且将来同时只有一个字段会被设置,那么就可以使用 oneof 加强这个行为,也能有节约内存的效果。
5.1 升级通讯录至 2.3 版本
通讯录 2.3 版本新增联系人的其他联系方式(比如 QQ 或者微信号二选一),我们就可以使用 oneof 字段来加强 "多选一" 这个行为。oneof字段定义的格式为:oneof 字段名 { 字段1; 字段2; ... }
更新contacts.proto(通讯录 2.3),更新内容如下:
cpp
syntax = "proto3";
package contacts;
import "google/protobuf/any.proto"; // 引入 any.proto 文件
// 地址
message Address {
string home_address = 1; // 家庭地址
string unit_address = 2; // 单位地址
}
// 联系人
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; // 电话
google.protobuf.Any data = 4;
oneof other_contact { // 其他联系方式:多选一
string qq = 5;
string weixin = 6;
}
}
// 通讯录
message Contacts {
repeated PeopleInfo contacts = 1;
}
注意:
- 可选字段中的字段编号,不能与非可选字段的编号冲突。
- 不能在
oneof中使用repeated字段。 - 将来在设置
oneof字段中值时,如果将oneof中的字段设置多个,那么只会保留最后一次设置的成员,之前设置的oneof成员会自动清除。
执行编译命令:protoc -cpp_out=. contacts.proto
5.2 contacts.pb.h更新的部分代码展示:
cpp
// 类 PeopleInfo
class PeopleInfo final : public ::PROTOBUF_NAMESPACE_ID::Message {
public:
enum OtherContactCase {
kQq = 5,
kWeixin = 6,
OTHER_CONTACT_NOT_SET = 0,
};
// string qq = 5;
bool has_qq() const;
void clear_qq();
const std::string& qq() const;
template <typename Arg0 = const std::string&, typename... ArgT>
void set_qq(Arg0&& arg0, ArgT&&... args);
std::string* mutable_qq();
PROTOBUF_NODISCARD std::string* release_qq();
void set_allocated_qq(std::string* qq);
// string weixin = 6;
bool has_weixin() const;
void clear_weixin();
const std::string& weixin() const;
template <typename Arg0 = const std::string&, typename... ArgT>
void set_weixin(Arg0&& arg0, ArgT&&... args);
std::string* mutable_weixin();
PROTOBUF_NODISCARD std::string* release_weixin();
void set_allocated_weixin(std::string* weixin);
void clear_other_contact();
OtherContactCase other_contact_case() const;
};
上述代码中:对于oneof字段:
- 会将
oneof中的多个字段定义为一个枚举类型。 - 设置和获取:对
oneof内的字段进行常规的设置和获取即可,但要注意只能设置一个。如果设置多个,那么只会保留最后一次设置的成员。 - 清空
oneof字段:clear_方法。 - 获取当前设置了哪个字段:
_case方法。
更新 write.cc(通讯录 2.3),更新内容如下:
cpp
#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::MP);
break;
case 2:
phone->set_type(PeopleInfo::Phone::PhoneType::TEL);
break;
default:
cout << "非法选项,使用默认值!" << endl;
break;
}
}
Address address;
cout << "请输入联系人家庭地址:";
string home_address;
getline(cin, home_address);
address.set_home_address(home_address);
cout << "请输入联系人单位地址:";
string unit_address;
getline(cin, unit_address);
address.set_unit_address(unit_address);
google::protobuf::Any* data = people_info_ptr->mutable_data();
data->PackFrom(address);
cout << "请选择一个其他联系方式(1.QQ号 2.微信号):";
int other_contact;
cin >> other_contact;
cin.ignore(256, '\n');
if (other_contact == 1) {
cout << "请输入QQ号:";
string qq;
getline(cin, qq);
people_info_ptr->set_qq(qq);
} else if (other_contact == 2) {
cout << "请输入微信号:";
string weixin;
getline(cin, weixin);
people_info_ptr->set_weixin(weixin);
} else {
cout << "非法选择,该设置失效!" << endl;
}
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;
// 先读取已存在的通讯录
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;
return -1;
}
// 添加一个联系人
AddPeopleInfo(contacts.add_contacts());
// 再把通讯录写入到文件中
fstream output(argv[1], ios::out | ios::trunc | ios::binary);
if (!contacts.SerializeToOstream(&output)) {
cerr << "Failed to write contacts." << endl;
return -1;
}
output.close();
input.close();
return 0;
}
更新 read.cc(通讯录 2.3),更新内容如下:
cpp
#include <iostream>
#include <fstream>
#include "contacts.pb.h"
using namespace std;
using namespace contacts;
/*
* 打印联系人列表
*/
void PrintContacts(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;
for (int j = 0; j < people.phone_size(); ++j) {
const PeopleInfo::Phone& phone = people.phone(j);
cout << "电话" << j+1 << ":" << phone.number() << "("
<< PeopleInfo::Phone::PhoneType_Name(phone.type())
<< ")" << endl;
}
if (people.has_data() && people.data().Is<Address>()) {
Address address;
people.data().UnpackTo(&address);
if (!address.home_address().empty()) {
cout << "家庭地址:" << address.home_address() << endl;
}
if (!address.unit_address().empty()) {
cout << "单位地址:" << address.unit_address() << endl;
}
}
switch (people.other_contact_case()) {
case PeopleInfo::OtherContactCase::kQq:
cout << "QQ号:" << people.qq() << endl;
break;
case PeopleInfo::OtherContactCase::kWeixin:
cout << "微信号:" << people.weixin() << endl;
break;
case PeopleInfo::OtherContactCase::OTHER_CONTACT_NOT_SET:
break;
}
}
}
int main(int argc, char* argv[])
{
GOOGLE_PROTOBUF_VERIFY_VERSION;
if (argc != 2) {
cerr << "Usage: " << argv[0] << " CONTACTS_FILE" << endl;
return -1;
}
// 读取通讯录
Contacts contacts;
fstream input(argv[1], ios::in | ios::binary);
if (!contacts.ParseFromIstream(&input)) {
cerr << "Failed to parse contacts." << endl;
return -1;
}
// 打印通讯录
PrintContacts(contacts);
input.close();
google::protobuf::ShutdownProtobufLibrary();
return 0;
}
代码完成后,编译后进行读写验证:
bash
# 编译 write 程序
g++ write.cc contacts.pb.cc -o write_contacts -lprotobuf
# 运行 write 程序
./write_contacts test.data
运行输出:
bash
----------新增联系人----------
请输入联系人姓名:赵六
请输入联系人年龄:38
请输入联系人电话1(只输入回车完成电话新增):171
请选择电话类型(1.移动电话 2.固定电话):1
请输入联系人电话2(只输入回车完成电话新增):
请输入联系人家庭地址:北京市
请输入联系人单位地址:北京市
请选择一个其他联系方式(1.QQ号 2.微信号):2
请输入微信号:guo_liu
----------添加联系人成功----------
cpp
# 编译 read 程序
g++ read.cc contacts.pb.cc -o read_contacts -lprotobuf
# 运行 read 程序
./read_contacts test.data
运行输出:
bash
----------联系人1----------
姓名:赵六
年龄:38
电话1:171(MP)
家庭地址:北京市
单位地址:北京市
微信号:guo_liu
6. map 类型
语法支持创建一个关联映射字段,也就是可以使用 map 类型去声明字段类型,格式为:map<key_type, value_type> map_field = N;
要注意的是:
key_type是除了 float 和 bytes 类型以外的任意标量类型。value_type可以是任意类型。- map 字段不可以用
repeated修饰。 - map 中存入的元素是无序的。
6.1 升级通讯录至 2.4 版本
最后,通讯录 2.4 版本想新增联系人的备注信息,我们可以使用 map 类型的字段来存储备注信息。
更新contact.proto(通讯录 2.4),更新内容如下:
cpp
syntax = "proto3";
package contacts;
import "google/protobuf/any.proto"; // 引入 any.proto 文件
// 地址
message Address {
string home_address = 1; // 家庭地址
string unit_address = 2; // 单位地址
}
// 联系人
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; // 电话
google.protobuf.Any data = 4;
oneof other_contact { // 其他联系方式:多选一
string qq = 5;
string weixin = 6;
}
map<string, string> remark = 7; // 备注
}
// 通讯录
message Contacts {
repeated PeopleInfo contacts = 1;
}
执行编译命令:protoc -cpp_out=. contacts.proto
6.2 contacts.pb.h更新的部分代码展示:
cpp
// 类 PeopleInfo
class PeopleInfo final : public ::PROTOBUF_NAMESPACE_ID::Message {
public:
// map<string, string> remark = 7;
int remark_size() const;
void clear_remark();
const ::PROTOBUF_NAMESPACE_ID::Map<std::string, std::string>&
remark() const;
::PROTOBUF_NAMESPACE_ID::Map<std::string, std::string>*
mutable_remark();
};
上述代码中:对于 Map 类型的字段:
- 清空 map:
clear_方法。 - 设置和获取:获取方法的方法名称与小写字段名完全相同;设置方法为
mutable_方法,返回值为 Map 类型的指针,这类方法会为我们开辟好空间,可以直接对这块空间的内容进行修改。
更新 write.cc(通讯录 2.4),更新内容如下:
cpp
#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::MP);
break;
case 2:
phone->set_type(PeopleInfo::Phone::PhoneType::TEL);
break;
default:
cout << "非法选择,使用默认值!" << endl;
break;
}
}
Address address;
cout << "请输入联系人家庭地址:";
string home_address;
getline(cin, home_address);
address.set_home_address(home_address);
cout << "请输入联系人单位地址:";
string unit_address;
getline(cin, unit_address);
address.set_unit_address(unit_address);
google::protobuf::Any* data = people_info_ptr->mutable_data();
data->PackFrom(address);
cout << "选择添加一个其他联系方式(1、qq号 2、微信号):";
int other_contact;
cin >> other_contact;
cin.ignore(256, '\n');
if (1 == other_contact) {
cout << "请输入qq号:";
string qq;
getline(cin, qq);
people_info_ptr->set_qq(qq);
} else if (2 == other_contact) {
cout << "请输入微信号:";
string weixin;
getline(cin, weixin);
people_info_ptr->set_weixin(weixin);
} else {
cout << "非法选择,该设置失效!" << endl;
}
for (int i = 1; ; i++) {
cout << "请输入备注" << i << "标题(只输入回车完成备注新增):";
string remark_key;
getline(cin, remark_key);
if (remark_key.empty()) {
break;
}
cout << "请输入备注" << i << "内容:";
string remark_value;
getline(cin, remark_value);
people_info_ptr->mutable_remark()->insert({remark_key, remark_value});
}
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;
// 先读取已存在的通讯录
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;
return -1;
}
// 添加一个联系人
AddPeopleInfo(contacts.add_contacts());
// 再把通讯录写入到文件中
fstream output(argv[1], ios::out | ios::trunc | ios::binary);
if (!contacts.SerializeToOstream(&output)) {
cerr << "Failed to write contacts." << endl;
return -1;
}
output.close();
input.close();
return 0;
}
更新 read.cc(通讯录 2.4),更新内容如下:
cpp
#include <iostream>
#include <fstream>
#include "contacts.pb.h"
using namespace std;
using namespace contacts;
/*
* 打印联系人列表
*/
void PrintContacts(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 auto& phone : people.phone()) {
cout << "电话" << j++ << ":" << phone.number() << "("
<< PeopleInfo::Phone::PhoneType_Name(phone.type())
<< ")" << endl;
}
if (people.has_data() && people.data().Is<Address>()) {
Address address;
people.data().UnpackTo(&address);
if (!address.home_address().empty()) {
cout << "家庭地址:" << address.home_address() << endl;
}
if (!address.unit_address().empty()) {
cout << "单位地址:" << address.unit_address() << endl;
}
}
switch (people.other_contact_case()) {
case PeopleInfo::OtherContactCase::kQq:
cout << "qq号:" << people.qq() << endl;
break;
case PeopleInfo::OtherContactCase::kWeixin:
cout << "微信号:" << people.weixin() << endl;
break;
case PeopleInfo::OtherContactCase::OTHER_CONTACT_NOT_SET:
break;
}
if (people.remark_size() > 0) {
cout << "备注信息:" << endl;
for (auto it = people.remark().cbegin(); it != people.remark().cend(); ++it) {
cout << "\t" << it->first << ":" << it->second << 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;
fstream input(argv[1], ios::in | ios::binary);
if (!contacts.ParseFromIstream(&input)) {
cerr << "Failed to parse contacts." << endl;
return -1;
}
// 打印通讯录
PrintContacts(contacts);
input.close();
google::protobuf::ShutdownProtobufLibrary();
return 0;
}
代码完成后,编译后进行读写验证:
bash
# 编译 write 程序
g++ write.cc contacts.pb.cc -o write_contacts -lprotobuf
# 运行 write 程序
./write_contacts test.data
运行输出:
bash
----------新增联系人----------
请输入联系人姓名:赵七
请输入联系人年龄:28
请输入联系人电话1(只输入回车完成电话新增):123123
选择此电话类型(1、移动电话 2、固定电话):2
请输入联系人电话2(只输入回车完成电话新增):
请输入联系人家庭地址:海南海口
请输入联系人单位地址:海南海口
选择添加一个其他联系方式(1、qq号 2、微信号):1
请输入qq号:123123
请输入备注1标题(只输入回车完成备注新增):日程
请输入备注1内容:10月1一起出去玩
请输入备注2标题(只输入回车完成备注新增):
----------添加联系人成功----------
bash
# 编译 read 程序
g++ read.cc contacts.pb.cc -o read_contacts -lprotobuf
# 运行 read 程序
./read_contacts test.data
运行输出:
bash
----------联系人1----------
姓名:赵七
年龄:28
电话1:123123(TEL)
家庭地址:海南海口
单位地址:海南海口
qq号:123123
备注信息:
日程:10月1一起出去玩
到这里,我们的通讯录 2.x 就更新了全部内容啦。在这个过程中我们将通讯录升级到了 2.4 版本,同时对 Protobuf 的内容又更深的了解了一些。不过,也只是正常使用还是完全不够的。通过接下来的学习,我们就能更进一步了解到 Protobuf 深入的内容。
7. 默认值
反序列化消息时,如果被反序列化的二进制序列中不包含某个字段,反序列化对象中相应字段时,就会设置为该字段的默认值。不同的类型对应的默认值不同:
- 对于字符串,默认值为空字符串。
- 对于字节,默认值为空字节。
- 对于布尔值,默认值为
false。 - 对于数值类型,默认值为 0。(对于 double 类型,就是 0.0)
- 对于枚举,默认值是第一个定义的枚举值,必须为 0。
- 对于消息字段,未设置该字段。它的取值是依赖于语言。
- 对于设置了
repeated的字段的默认值是空的(通常是相应语言的一个空列表)。 - 对于消息字段 、oneof 字段 和any 字段 ,C++ 和 Java 语言中都有
has_方法来检测当前字段是否被设置。
需要注意的是:
proto3 对于标量数据类型,并没有生成 has_ 方法!
这就会造成一个问题:

我们不确定在这个二进制序列消息中,有没有一个字段是 c = 0?
8. 更新消息
8.1 更新规则
如果现有的消息类型已经不满足我们的需求,例如想要扩展一个字段,在不破坏任何现有代码的情况下更新消息类型非常简单。遵循如下规则即可:
不要修改已存在字段的字段编号。
如果删除了某个字段,不要再使用该字段的字段编号。可以给该字段的字段编号进行(reserved),以保证该字段不会被重新使用。不过建议删除/注释字段,保留字段编号。
保留字段的概念就在下面!
int32、uint32、int64、uint64和bool是完全兼容的。可以从这些类型中的一个改为另一个(例如,将int32类型改成int64类型),它们的解析方式是一样的。
仅当 "大数值类型→小数值类型" 且数值超出小类型取值范围时,才会发生截断(不安全);同范围 / 小→大类型转换不会截断(安全),bool 与整数互转也遵循此规则 。
sint32和sint64相互兼容(截断),但与其他的整数类型不兼容。
string和bytes是兼容的(如果bytes是有效的 UTF-8 字节)。
因为 Protobuf 中 string 类型的语义是 "UTF-8 编码的字符串" ,所以只有当 bytes 存储的二进制数据是合法的 UTF-8 字节序列 时,bytes 转 string 才能正确解析为字符串;反之,string 转 bytes 时,会自动将 UTF-8 字符串编码为对应的字节流(天然是合法的 bytes)。
具体来说:
-
当 bytes → string 时:Protobuf 会把 bytes 的二进制数据当作 UTF-8 编码来解码 。如果 bytes 不是有效的 UTF-8(比如包含乱码字节),解码会失败(不同语言表现不同:C++ 可能返回乱码,Java 可能抛异常)。示例:bytes 存的是 "abc" 的 UTF-8 字节 [0x61, 0x62, 0x63],转 string 后是 "abc";但如果 bytes 存的是无效 UTF-8 字节 [0xFF],转 string 会解析失败。
-
当 string → bytes 时:Protobuf 会把 string 的 UTF-8 编码直接转成字节流,所以得到的 bytes 天然是合法的 UTF-8 字节,不存在兼容性问题。
简言之,string 和 bytes 的兼容是基于 "string 是 UTF-8 编码的字节序列" 这一语义 ,所以要求 bytes 必须是有效的 UTF-8,才能和 string 互相正确解析。
fixed32与sfixed32兼容,fixed64与sfixed64兼容。
enum与int32、uint32、int64和uint64兼容(注意值不匹配会被截断)。但要注意当反序列化消息时,根据语言采用不同的处理方式:例如,未知的proto3枚举类型会保存在消息中,但当被序列化后,该值会被转换为对应的整型数值;但如果被识别为其类型,会使用其值。
enum 的本质是 "带名字的整数" --- Protobuf 里的 enum 底层就是32 位整数 (默认基于 int32 实现)
proto3 不会丢弃 "未知的 enum 值",而是以整数形式保留,不同语言处理方式略有差异 。
- "未知的 proto3 枚举类型会保存在消息中"
假设服务 A 的 proto 定义:
cpp
enum PhoneType {
MP = 0;
TEL = 1;
}
服务 B 的 proto 新增了枚举值:
cpp
enum PhoneType {
MP = 0;
TEL = 1;
WORK = 2; // 服务A不认识的新值
}
当服务 B 把 WORK(值为 2)序列化发给服务 A 时:
- 服务 A 不认识 "2" 对应的枚举名,但不会丢弃这个值,而是把 "2" 以整数形式保存在消息的 "未知字段" 里;(后面马上就会谈到!)
- 服务 A 再把这个消息序列化发出去时,"2" 会被原样保留(不会丢失)。
- "序列化后转换为对应的整型数值;被识别为其类型则用其值"
- 场景 1(服务 A 解析服务 B 的消息):服务 A 不认识 WORK=2,所以解析时只会拿到整数 2(而非枚举名),但这个 2 会被保留;
- 场景 2(服务 B 解析自己的消息):服务 B 认识 WORK=2,所以解析时会直接拿到枚举名 WORK(而非单纯的整数 2);
- 场景 3(服务 A 把含 2 的消息发给服务 B):服务 B 能识别 2 对应 WORK,所以会解析为枚举名 WORK。
oneof:
- 将非 oneof 字段更新为 oneof 类型成员之一是安全和二进制兼容的。(原本独立的普通字段(不在任何 oneof 里),把它移到一个新创建的 oneof 里作为成员,对旧代码(没更新 proto 的代码)完全无影响,二进制数据也能正常解析。)
旧 proto(v1) :
cpp
message PeopleInfo {
string name = 1;
string qq = 2; // 普通字段,非oneof
}
新 proto(v2) :
cpp
message PeopleInfo {
string name = 1;
oneof other_contact { // 新增oneof
string qq = 2; // 把普通字段移进新oneof
}
}
为什么安全呢?
- 旧代码(基于 v1):依然能正常读写 qq 字段,感知不到 oneof 的存在,二进制数据解析和原来完全一致;
- 新代码(基于 v2):把 qq 当作 oneof 成员使用,设置 / 读取逻辑也完全兼容旧数据(旧数据里的 qq 值会被新代码识别为 oneof 的当前值);
- 二进制层面:qq 的字段编号还是 2,编码 / 解码规则没变,完全兼容。
- 若肯定没有代码设置该字段值,将该字段移入一个新 oneof 类型也是可行的。(如果某个普通字段从未被任何代码赋值过(字段值一直是默认值,比如 string 默认空、int 默认 0),把它移到新 oneof 里也没问题;但如果该字段已有真实值,这么做就可能丢数据。)
旧 proto(v1) :
cpp
message PeopleInfo {
string name = 1;
string weixin = 3; // 普通字段,从未被任何代码设置过值
}
新 proto(v2) :
cpp
message PeopleInfo {
string name = 1;
oneof other_contact {
string qq = 2;
string weixin = 3; // 移进新oneof(从未被赋值过)
}
}
为什么可行?
因为该字段原本就没有真实值(只有默认值),移进 oneof 后,旧代码读取时依然是默认值,新代码也不会读到无效数据;但如果 weixin 字段已有真实值(比如旧代码设置过 weixin="abc"),移进 oneof 后,旧代码仍能读,但新代码若设置了 qq,会清空 weixin,导致旧数据丢失。
- 将同字段移入已存在的 oneof 类型是不安全的。(如果一个普通字段,要移到已经存在的 oneof 里(这个 oneof 已有其他字段,且可能被设置过值),会导致数据冲突、丢失,二进制兼容被破坏。)
旧 proto(v1) :
cpp
message PeopleInfo {
string name = 1;
string qq = 2; // 普通字段
oneof other_contact {
string weixin = 3; // 已存在的oneof,可能被设置过值
}
}
新 proto(v2) :
cpp
message PeopleInfo {
string name = 1;
oneof other_contact {
string weixin = 3;
string qq = 2; // 把普通字段移进已存在的oneof
}
}
为什么不安全?
假设旧代码同时设置了 qq="123" 和 weixin="456":
- 旧 proto(v1):两个字段是独立的,序列化后会同时保存 qq=123 和 weixin=456;
- 新 proto(v2):qq 和 weixin 互斥,反序列化时只会保留最后设置的那个值(比如先设 qq、后设 weixin,就只剩 weixin),导致另一个值丢失;
- 更严重的是:旧代码写的 "同时有 qq 和 weixin 值" 的二进制数据,被新代码解析后会丢失一个值,数据一致性被破坏,这就是 "不安全" 的核心。
8.2 保留字reserved
如果通过删除或注释字段来更新消息类型,未来的用户在添加新字段时,有可能会使用以前已经使用,但已经被删除或注释掉的字段编号。将使用.proto 的旧版本的程序序列发消息时,就会出现解析错误等。我们会发现解决该问题的一种方法是:使用reserved将字段的编号或名称设置为保留项。当我们使用这些编号或名称时,protocol buffer 的编译器将会把这些编号或名称不可用。举个例子:
cpp
message Message {
reserved 16, 17;
reserved 100 to 199;
reserved "foo", "bar";
// string foo = 16; // 被删除,字段编号和名称被reserved
// string bar = 17; // 被删除,字段编号和名称被reserved
int32 field1 = 1; // field1 foo2
int32 field2 = 2; // field2 bar2
int32 field3 = 182; // field3 name
}
reserved 后可跟 字段编号 、字段编号范围 、字段名称,但有严格的语法限制:
| 类型 | 格式示例 | 说明 |
|---|---|---|
| 单个字段编号 | reserved 2; |
保留编号 2,不能再用于任何字段 |
| 编号范围 | reserved 10 to 20; |
保留 10~20(含)的所有编号,范围用 to(小写) |
| 多个编号 / 范围 | reserved 5, 8 to 15; |
同时保留单个编号 5 + 范围 8~15 |
| 字段名称 | reserved "name", "age"; |
保留名称 "name" 和 "age",不能再用作字段名 |
注意:
-
编号和名称不能写在同一条 reserved 语句里 :必须分开写!
❌ 错误:reserved 2, "age";(同时写编号和名称)
✅ 正确:cppreserved 2; // 单独写编号 reserved "age"; // 单独写名称 -
编号范围的
to必须小写 :不能用TO/To,否则编译器报错。 -
编号必须是正整数:且符合 Protobuf 字段编号规则(1~2²⁹-1,其中 19000~19999 是保留段)。
-
名称必须用双引号包裹:单引号、无引号都不行,且区分大小写(比如 "Age" 和 "age" 是两个不同名称)。
-
reserved 作用域是当前 message:只约束当前消息内的字段,嵌套消息 / 其他消息不受影响。
8.2.1 创建通讯 3.0 版本 ------ 验证删除字段造成的数据损坏
现有两个服务,他们各自使用一份通讯录.proto 文件,内容约定好了是一模一样的。
- 服务(
service):负责序列化通讯录对象,并写入文件中。 - 服务 2(
client):负责读取文件中的数据,解析并打印。
服务之间的通讯.proto 文件是一致的。现在,服务 1 删除了某个字段,并新增了一个字段,新增的字段使用了被删除字段的字段编号。并将新的序列化对象写进了文件。但 client 并没有更新自己的 proto 文件。根据推论,可能会出现数据损坏的现象,接下来就让我们来验证这个推论。
新增两个目录:service、client,分别存放两个服务的代码。
service 目录下新增contact.proto(通讯录 3.0)
cpp
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;
}
client 目录下新增contact.proto(通讯录 3.0)
cpp
syntax = "proto3";
package contacts;
// 联系人
message PeopleInfo {
string name = 1; // 姓名
int32 age = 2; // 年龄
int32 birthday = 4; // 出生日期(新增字段)
message Phone {
string number = 1; // 电话号码
repeated Phone phone = 3; // 电话
}
}
message Contacts {
repeated PeopleInfo contacts = 1;
}
分别对两个文件进行编译,自行操作。
继续对service目录下新增service.cc(通讯录 3.0),负责向文件中写通讯录消息,内容如下:
cpp
#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 << "(输入-1完成电话新增):";
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;
return -1;
}
input.close();
// 添加一个联系人
AddPeopleInfo(contacts.add_contacts());
// 再把通讯录写入到文件中
fstream output(argv[1], ios::out | ios::trunc | ios::binary);
if (!contacts.SerializeToOstream(&output)) {
cerr << "Failed to write contacts." << endl;
return -1;
}
output.close();
google::protobuf::ShutdownProtobufLibrary();
return 0;
}
service 目录下新增Makefile
bash
service:service.cc contacts.pb.cc
g++ $^ -o $@ -lprotobuf
.PHONY:clean
clean:
rm -f service
client 目录下新增client.cc(通讯录 3.0),负责读出文件中的通讯录消息,内容如下:
cpp
#include <iostream>
#include <fstream>
#include "contacts.pb.h"
using namespace std;
using namespace contacts;
// 打印联系人列表
void PrintContacts(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;
for (const auto& phone : people.phone()) {
cout << "电话:" << 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;
fstream input(argv[1], ios::in | ios::binary);
if (!contacts.ParseFromIstream(&input)) {
cerr << "Failed to parse contacts." << endl;
return -1;
}
input.close();
// 打印通讯录
PrintContacts(contacts);
google::protobuf::ShutdownProtobufLibrary();
return 0;
}
client 目录下新增Makefile
cpp
client:client.cc contacts.pb.cc
g++ $^ -o $@ -lprotobuf
.PHONY:clean
clean:
rm -f client
代码编写完成后,进行一次读写(读写前的编译过程省略,自行操作)。
bash
hyb@192-159-150-152:~/project/protobuf/update/service$ ./service ../contacts.bin
----------新增联系人----------
请输入联系人姓名:张珊
请输入联系人年龄:22
请输入联系人电话1(输入-1完成电话新增):131
请输入联系人电话2(输入-1完成电话新增):
----------添加联系人成功----------
hyb@192-159-150-152:~/project/protobuf/update/client$ ./client ../contacts.bin
----------联系人1----------
姓名:张珊
年龄:22
电话:131
确定无误后,对service目录下的contact.proto文件进行更新:删除age字段,新增birthday字段,新增的字段使用被删除字段的字段编号。更新后的contact.proto(通讯录 3.0)内容如下:
cpp
syntax = "proto3";
package contacts;
// 联系人
message PeopleInfo {
string name = 1; // 姓名
// int32 age = 2; // 已删除
int32 birthday = 2; // 出生日期
message Phone {
string number = 1; // 电话号码
}
repeated Phone phone = 3; // 电话
}
message Contacts {
repeated PeopleInfo contacts = 1;
}
编辑proto文件后,还需要更新一下对应的service.cc(通讯录 3.0):
cpp
#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 birthday;
getline(cin, birthday);
people_info_ptr->set_birthday(birthday);
cin.ignore(256, '\n');
for (int i = 1; ; i++) {
cout << "请输入联系人电话" << i << "(输入-1完成电话新增):";
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[])
{
// ...(main函数内容与之前一致,省略)
}
我们对client相关的代码都不进行更新。再进行一次读写(对service.cc编译过程,自行操作)。
cpp
hyb@192-159-150-152:~/project/protobuf/update/service$ ./service ../contacts.bin
----------新增联系人----------
请输入联系人姓名:李四
请输入联系人出生日期:1990-1-1
请输入联系人电话1(输入-1完成电话新增):151
请输入联系人电话2(输入-1完成电话新增):
----------添加联系人成功----------
hyb@192-159-150-152:~/project/protobuf/update/client$ ./client ../contacts.bin
----------联系人1----------
姓名:张珊
年龄:22
电话:131
----------联系人2----------
姓名:李四
年龄:199011
电话:151
上面的结果中,我们发现 "李四" 的年龄变成了 "199011",这明显是不正确的。这是因为service给 "出生日期" 字段设置的值 "1990-1-1" 被client识别成了 "年龄" 字段的值,所以出现了乱码或者错误的字段值。要避免这个问题就需要不要使用被删除的字段编号,不要重复使用字段编号。
正确的service目录下的contact.proto写法应该(给被删除字段编号)被设置为:
cpp
syntax = "proto3";
package contacts;
// 联系人
message PeopleInfo {
reserved 2;
string name = 1; // 姓名
int32 birthday = 4; // 出生日期
message Phone {
string number = 1; // 电话号码
}
repeated Phone phone = 3; // 电话
}
message Contacts {
repeated PeopleInfo contacts = 1;
}
proto文件修改后,还需要更新一下对应的service.cc程序,再进行一次读写,这样就不会出现上述的错误了。
cpp
hyb@192-159-150-152:~/project/protobuf/update/service$ ./service ../contacts.bin
----------新增联系人----------
请输入联系人姓名:王五
请输入联系人出生日期:1112
请输入联系人电话1(输入-1完成电话新增):110
请输入联系人电话2(输入-1完成电话新增):
----------添加联系人成功----------
hyb@192-159-150-152:~/project/protobuf/update/client$ ./client ../contacts.bin
----------联系人1----------
姓名:张珊
年龄:22
电话:131
----------联系人2----------
姓名:李四
年龄:199011
电话:151
----------联系人3----------
姓名:王五
年龄:0
电话:110
这样一来,"王五" 的 "年龄" 字段使用了proto3的默认值 "0",这就是我们想要的结果。这也印证了我们的推论。
根据上面的例子,我们可能还有一个疑问:如果使用了reserved 2 了那么service给王五设置的生日"1112",client就没有办法读到了吗?
答案是可以的,继续学习下面的知识我们就可以知道啦。
8.3 未知字段
在通讯录 3.0 版本中,我们向service目录下的contacts.proto新增了'生日'字段,但对于client相关的代码并没有任何改动。验证后发现 新代码序列化的消息(service)也可以被旧代码(client)解析。并且这里要说的是,新增的'生日'字段在旧程序(client)中其实并没有丢失,而是会作为旧程序的未知字段。
- 未知字段:解析结构良好的 protocol buffer 已序列化数据中的未识别字段的表示方式。例如,当旧程序解析带有新字段的数据时,这些新字段就会成为旧程序的未知字段。
- 本来,
proto3在解析消息时总是会丢弃未知字段,但在 3.5 版本中重新引入了对未知字段的保留机制。所以在 3.5 或更高版本中,未知字段在反序列化时会被保留,同时也会包含在序列化的结果中。(也就是3.5版本之前会被丢弃,之后会被保留)
8.3.1 未知字段从哪获取
了解相关类关系图

针对未知字段从哪里获取,不再是像我们之前定义的常规字一样的使用 Get 方式来获取未知字段,而是通过一些复杂的手段才可以拿得到的!
其实上图中,UnknownField 就是表示未知字段的类!
MessageLite 类介绍(了解)
-
MessageLite从名字看是轻量级的message,仅仅提供序列化、反序列化功能。 -
类定义在 google 提供的
message_lite.h中。
Message 类介绍(了解)
-
我们自己定义的
message都是继承自Message。 -
Message最重要的两个接口GetDescriptor()/GetReflection(),可以获取该类型对应的Descriptor对象指针和Reflection对象指针。 -
类定义在 google 提供的
message.h中。
cpp
const Descriptor* GetDescriptor() const;
const Reflection* GetReflection() const;
Descriptor 类介绍(了解)
-
Descriptor是对 google 的message类型定义的描述,包括message的名字、所有字段的描述、原始的.proto文件内容。 -
类定义在 google 提供的
descriptor.h中。
cpp
class PROTOBUF_EXPORT Descriptor : private internal::SymbolBase {
public:
const string& name() const;
int field_count() const;
const FieldDescriptor* field(int index) const;
const FieldDescriptor* FindFieldByNumber(int number) const;
const FieldDescriptor* FindFieldByName(const string& name) const;
const FieldDescriptor* FindFieldByCamelCaseName(const string& name) const;
const FieldDescriptor* field(int index) const;
const FieldDescriptor* FindFieldByNumber(int number) const;
const EnumDescriptor* enum_type(int index) const;
const EnumDescriptor* FindEnumTypeByName(const string& name) const;
const EnumValueDescriptor* FindEnumValueByName(const string& name) const;
};
Reflection 类介绍(了解)
-
Reflection接口类,主要提供动态访问消息字段的接口,对消息对象的自动写主要通过这类完成。 -
提供方法动态访问 / 修改
message中的字段,对每种类型,Reflection都提供了一个单独的接口用于读 / 写对应的值。 -
对所有不同的标量类型
FieldDescriptor::TYPE_*,需要使用不同的Get*()/Set*()/Add*()接口; -
对
repeated类型字段,需要使用GetRepeated*()/SetRepeated*()接口,不可以和Repeated类型接口混用; -
message对象可以通过它自身的reflection(message.GetReflection()) 来操作类中包含了访问 / 修改未知字段的方法。 -
类定义在 google 提供的
message.h中。
也就是读写字段的相关方法!
cpp
class PROTOBUF_EXPORT Reflection final {
public:
const UnknownFieldSet& GetUnknownFields(const Message& message) const;
void SetUnknownFields(Message* message, const UnknownFieldSet& fields);
bool HasField(const Message& message, const FieldDescriptor* field) const;
void ClearField(Message* message, const FieldDescriptor* field) const;
void SetField(Message* message, const FieldDescriptor* field,
const Message& value) const;
bool HasOneof(const Message& message, const OneofDescriptor* oneof_descriptor) const;
void ClearOneof(Message* message, const OneofDescriptor* oneof_descriptor) const;
const FieldDescriptor* GetOneofFieldDescriptor(
const Message& message, const OneofDescriptor* oneof_descriptor) const;
// Singular field getters
int32 GetInt32(const Message& message, const FieldDescriptor* field) const;
int64 GetInt64(const Message& message, const FieldDescriptor* field) const;
uint32 GetUInt32(const Message& message, const FieldDescriptor* field) const;
uint64 GetUInt64(const Message& message, const FieldDescriptor* field) const;
float GetFloat(const Message& message, const FieldDescriptor* field) const;
double GetDouble(const Message& message, const FieldDescriptor* field) const;
bool GetBool(const Message& message, const FieldDescriptor* field) const;
std::string GetString(const Message& message, const FieldDescriptor* field) const;
const EnumValueDescriptor* GetEnum(const Message& message, const FieldDescriptor* field) const;
int GetEnumValue(const Message& message, const FieldDescriptor* field) const;
const Message& GetMessage(const Message& message, const FieldDescriptor* field,
const MessageFactory* factory = nullptr) const;
// Singular field mutators
void SetInt32(Message* message, const FieldDescriptor* field,
int32 value) const;
void SetInt64(Message* message, const FieldDescriptor* field,
int64 value) const;
void SetUInt32(Message* message, const FieldDescriptor* field,
uint32 value) const;
void SetUInt64(Message* message, const FieldDescriptor* field,
uint64 value) const;
void SetFloat(Message* message, const FieldDescriptor* field,
float value) const;
void SetDouble(Message* message, const FieldDescriptor* field,
double value) const;
void SetBool(Message* message, const FieldDescriptor* field,
bool value) const;
void SetString(Message* message, const FieldDescriptor* field,
const std::string& value) const;
void SetEnum(Message* message, const FieldDescriptor* field,
const EnumValueDescriptor* value) const;
void SetEnumValue(Message* message, const FieldDescriptor* field,
int value) const;
Message* MutableMessage(Message* message, const FieldDescriptor* field,
const MessageFactory* factory = nullptr) const;
Message* ReleaseMessage(Message* message, const FieldDescriptor* field,
const MessageFactory* factory = nullptr) const;
// Repeated field getters
int FieldSize(const Message& message, const FieldDescriptor* field) const;
int32 GetRepeatedInt32(const Message& message, const FieldDescriptor* field,
int index) const;
int64 GetRepeatedInt64(const Message& message, const FieldDescriptor* field,
int index) const;
uint32 GetRepeatedUInt32(const Message& message, const FieldDescriptor* field,
int index) const;
uint64 GetRepeatedUInt64(const Message& message, const FieldDescriptor* field,
int index) const;
float GetRepeatedFloat(const Message& message, const FieldDescriptor* field,
int index) const;
double GetRepeatedDouble(const Message& message, const FieldDescriptor* field,
int index) const;
bool GetRepeatedBool(const Message& message, const FieldDescriptor* field,
int index) const;
std::string GetRepeatedString(const Message& message, const FieldDescriptor* field,
int index) const;
const EnumValueDescriptor* GetRepeatedEnum(const Message& message, const FieldDescriptor* field,
int index) const;
int GetRepeatedEnumValue(const Message& message, const FieldDescriptor* field,
int index) const;
const Message& GetRepeatedMessage(const Message& message,
const FieldDescriptor* field,
int index) const;
const std::string& GetRepeatedStringReference(const Message& message,
const FieldDescriptor* field,
int index,
std::string* scratch) const;
// Repeated field mutators
void SetRepeatedInt32(Message* message, const FieldDescriptor* field,
int index, int32 value) const;
void SetRepeatedInt64(Message* message, const FieldDescriptor* field,
int index, int64 value) const;
void SetRepeatedUInt32(Message* message, const FieldDescriptor* field,
int index, uint32 value) const;
void SetRepeatedUInt64(Message* message, const FieldDescriptor* field,
int index, uint64 value) const;
void SetRepeatedFloat(Message* message, const FieldDescriptor* field,
int index, float value) const;
void SetRepeatedDouble(Message* message, const FieldDescriptor* field,
int index, double value) const;
void SetRepeatedBool(Message* message, const FieldDescriptor* field,
int index, bool value) const;
void SetRepeatedString(Message* message, const FieldDescriptor* field,
int index, const std::string& value) const;
void SetRepeatedEnum(Message* message, const FieldDescriptor* field,
int index, const EnumValueDescriptor* value) const;
void SetRepeatedEnumValue(Message* message, const FieldDescriptor* field,
int index, int value) const;
Message* MutableRepeatedMessage(Message* message, const FieldDescriptor* field,
int index) const;
// Repeated field adders
void AddInt32(Message* message, const FieldDescriptor* field,
int32 value) const;
void AddInt64(Message* message, const FieldDescriptor* field,
int64 value) const;
void AddUInt32(Message* message, const FieldDescriptor* field,
uint32 value) const;
void AddUInt64(Message* message, const FieldDescriptor* field,
uint64 value) const;
void AddFloat(Message* message, const FieldDescriptor* field,
float value) const;
void AddDouble(Message* message, const FieldDescriptor* field,
double value) const;
void AddBool(Message* message, const FieldDescriptor* field,
bool value) const;
void AddString(Message* message, const FieldDescriptor* field,
const std::string& value) const;
void AddEnum(Message* message, const FieldDescriptor* field,
const EnumValueDescriptor* value) const;
void AddEnumValue(Message* message, const FieldDescriptor* field,
int value) const;
Message* AddMessage(Message* message, const FieldDescriptor* field,
const MessageFactory* factory = nullptr) const;
const FieldDescriptor* FindKnownExtensionByName(
const std::string& name) const;
const FieldDescriptor* FindKnownExtensionByNumber(int number) const;
bool SupportsUnknownEnumValues() const;
};
UnknownFieldSet 类介绍(了解)
const UnknownFieldSet& GetUnknownFields(const Message& message) const; 由此返回!
-
UnknownFieldSet包含在解析消息但型但未由其类型定义的所有字段。 -
若要将
UnknownFieldSet附加到任何消息,请调用Reflection::SetUnknownFields()。 -
类定义在
unknown_field_set.h中。
cpp
class PROTOBUF_EXPORT UnknownFieldSet {
public:
UnknownFieldSet();
~UnknownFieldSet();
void Clear();
void MergeFrom(const UnknownFieldSet& other);
int field_count() const;
const UnknownField& field(int index) const;
UnknownField* mutable_field(int index);
// Add fields
void AddVarint(int number, uint64 value);
void AddFixed32(int number, uint32 value);
void AddFixed64(int number, uint64 value);
void AddLengthDelimited(int number, const std::string& value);
void AddGroup(int number, UnknownFieldSet* value);
UnknownField* AddField(int number);
// Parsing/serialization
bool ParseFromCodedStream(CodedInputStream* input);
bool ParsePartialFromCodedStream(CodedInputStream* input);
bool ParseFromZeroCopyStream(ZeroCopyInputStream* input);
bool ParsePartialFromZeroCopyStream(ZeroCopyInputStream* input);
bool ParseFromString(const std::string& data) {
return ParseFromArray(data.data(), static_cast<int>(data.size()));
}
bool ParseFromArray(const void* data, int size);
bool SerializeToString(std::string* output) const;
bool SerializeToCodedStream(CodedOutputStream* output) const;
bool SerializePartialToCodedStream(CodedOutputStream* output) const;
};
UnknownField 类介绍(重要)
-
表示一个未知的字段。通过上者 field(int) 方式获取!
-
类定义在
unknown_field_set.h中。
cpp
class PROTOBUF_EXPORT UnknownField {
public:
enum Type {
TYPE_VARINT,
TYPE_FIXED32,
TYPE_FIXED64,
TYPE_LENGTH_DELIMITED,
TYPE_GROUP
};
int number() const; // 返回位置字段的编号
int type() const; // 返回枚举常量
// Accessors for the value of the field, depending on type.
// 和上面的 type() 返回是一一对应的!!!注意!!!
uint64 varint() const;
uint32 fixed32() const;
uint64 fixed64() const;
const std::string& length_delimited() const;
const UnknownFieldSet& group() const;
// Mutators
// 和上面的 type() 返回是一一对应的!!!注意!!!
void set_varint(uint64 value);
void set_fixed32(uint32 value);
void set_fixed64(uint64 value);
void set_length_delimited(const std::string& value);
std::string* mutable_length_delimited();
};
8.3.2 升级通讯录至 3.1 版本 ------ 验证未知字段
更新 client.cc(通讯录 3.1),在这个版本中,需要打印出未知字段的内容。更新的代码如下:
cpp
#include <iostream>
#include <fstream>
#include "google/protobuf/unknown_field_set.h"
#include "contacts.pb.h"
using namespace std;
using namespace contacts;
using namespace google::protobuf;
// 打印联系人列表
void PrintContacts(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;
for (const auto& phone : people.phone()) {
cout << "电话:" << phone.number() << endl;
}
// 获取未知字段
const Reflection* reflection = people::GetReflection(); // 静态方法
const UnknownFieldSet& unknown_fields = reflection->GetUnknownFields(people);
cout << "未知字段数量:" << unknown_fields.field_count() << endl;
for (int j = 0; j < unknown_fields.field_count(); ++j) {
const UnknownField& unknown_field = unknown_fields.field(j);
cout << "未知字段编号:" << unknown_field.number() << endl;
cout << "未知字段类型:" << unknown_field.type() << endl;
switch (unknown_field.type()) {
case UnknownField::TYPE_VARINT:
cout << "未知字段值:" << unknown_field.varint() << endl;
break;
case UnknownField::TYPE_LENGTH_DELIMITED:
cout << "未知字段值:" << unknown_field.length_delimited() << endl;
break;
default:
break;
}
}
}
}
int main(int argc, char* argv[])
{
GOOGLE_PROTOBUF_VERIFY_VERSION;
if (argc != 2) {
cerr << "Usage: " << argv[0] << " CONTACTS_FILE" << endl;
return -1;
}
// 读取通讯录
Contacts contacts;
fstream input(argv[1], ios::in | ios::binary);
if (!contacts.ParseFromIstream(&input)) {
cerr << "Failed to parse contacts." << endl;
return -1;
}
input.close();
// 打印通讯录
PrintContacts(contacts);
google::protobuf::ShutdownProtobufLibrary();
return 0;
}
其他文件不用做任何修改,重新对 client.cc 进行一次编译,得到如下结果:
cpp
hyb@192-159-150-152:~/project/protobuf/update/client$ ./client ../contacts.bin
----------联系人1----------
姓名:张珊
年龄:22
电话:131
未知字段数量:0
----------联系人2----------
姓名:李四
年龄:199011
电话:151
未知字段数量:1
未知字段编号:2
未知字段类型:3
未知字段值:1990-1-1
----------联系人3----------
姓名:王五
年龄:0
电话:110
未知字段数量:1
未知字段编号:4
未知字段类型:3
未知字段值:1980-1-1
类型方向:在介绍 UnknownField 中提到了其中包含了未知字段的具体类型:
cpp
enum Type {
TYPE_VARINT,
TYPE_FIXED32,
TYPE_FIXED64,
TYPE_LENGTH_DELIMITED,
TYPE_GROUP
};
类型 3 对应的是 TYPE_LENGTH_DELIMITED。
8.4 前后兼容性
根据上述例子可以得出,pb 是具有向前兼容的。为了叙述方便,把增加了 "生日" 属性的 service 称为 "新模块";未做变动的 client 称为 "老模块"。
- 向前兼容:老模块能够正确识别新模块生成或发出的协议。这时新增加的 "生日" 属性会被当作未知字段(pb 3.5 版本及之后)。
- 向后兼容:新模块也能够正确识别老模块生成或发出的协议。
前后兼容的作用:当我们维护一个很庞大的分布式系统时,由于你无法同时升级所有模块,为了保证在升级过程中,整个系统能够尽可能不受影响,就需要尽量保证通讯协议的 "向后兼容" 或 "向前兼容"。
9. 选项 option
.proto 文件中可以声明许多选项,使用 option 标注。选项能影响 proto 编译器的某些处理方式。
9.1 选项分类
选项的完整列表在google/protobuf/descriptor.proto中定义。部分代码:
cpp
syntax = "proto2"; // descriptor.proto 使用 proto2 语法版本
message FileOptions { ... } // 文件选项 定义在 FileOptions 消息中
message MessageOptions { ... } // 消息类型选项 定义在 MessageOptions 消息中
message FieldOptions { ... } // 消息字段选项 定义在 FieldOptions 消息中
message OneofOptions { ... } // oneof字段选项 定义在 OneofOptions 消息中
message EnumOptions { ... } // 枚举类型选项 定义在 EnumOptions 消息中
message EnumValueOptions { ... } // 枚举值选项 定义在 EnumValueOptions 消息中
message ServiceOptions { ... } // 服务选项 定义在 ServiceOptions 消息中
message MethodOptions { ... } // 服务方法选项 定义在 MethodOptions 消息中
...
由此可见,选项分为 文件级、消息级、字段级 等等,但并没有一种选项能作用于所有的类型。
9.2 常用选项列举
optimize_for :该选项为文件选项,可以设置 protoc 编译器的优化级别,分别为 SPEED、CODE_SIZE、LITE_RUNTIME。受该选项影响,设置不同的优化级别,编译.proto 文件后生成的代码内容不同。
- SPEED :protoc 编译器将生成的代码是高度优化的,代码运行效率高,但是由此生成的代码编译后会占用更多的空间。SPEED 是默认选项。
- CODE_SIZE :proto 编译器将生成最少的类,会占用更少的空间,是依赖基于反射的代码来实现序列化、反序列化和各种其他操作。但和 SPEED 恰恰相反,它的代码运行效率较低。这种方式适合用在包含大量的.proto 文件,但并不盲目追求速度的应用中。
- LITE_RUNTIME :生成的代码执行效率高,同时生成代码编译后的所占用的空间也是非常少。这是以牺牲 Protocol Buffer 提供的反射功能为代价的,仅仅提供 encoding + 序列化功能,所以我们在链接 BP 库时仅需链接 libprotobuf-lite,而非 libprotobuf。这种模式通常用于资源有限的平台,例如移动手机平台中。
bash
option optimize_for = LITE_RUNTIME;
- allow_alias :允许将相同的常数值分配给不同的枚举常量,用来定义别名。该选项为枚举选项。举个例子:
cpp
enum PhoneType {
option allow_alias = true;
MP = 0;
TEL = 1;
LANDLINE = 1; // 若不加 option allow_alias = true; 这一行会编译报错
}
对 TEL 起别名 --- LANDLINE
9.3 设置自定义选项
Protobuf 允许自定义选项并使用。该功能大部分场景用不到,在这里不拓展讲解。有兴趣可以参考:https://developers.google.cn/protocol-buffers/docs/proto?hl=zh-cn#customoptions