个人主页:Zfox_
系列专栏:ProtoBuf
在语法详解部分,依旧使⽤项⽬推进的⽅式完成讲解。这个部分会对通讯录进⾏多次升级,使⽤2.x表⽰升级的版本,最终将会升级如下内容:
消息的字段可以⽤下⾯⼏种规则来修饰:
更新contacts.proto, PeopleInfo 消息中新增 phone_numbers 字段,表⽰⼀个联系⼈有多个号码,可将其设置为repeated,写法如下:
yntax = "proto3";
package contacts;
message PeopleInfo {
string name = 1;
int32 age = 2;
repeated string phone_numbers = 3;
}
在单个.proto⽂件中可以定义多个消息体,且⽀持定义嵌套类型的消息(任意多层)。每个消息体中的字段编号可以重复。
更新contacts.proto,我们可以将phone_number提取出来,单独成为⼀个消息:
// -------------------------- 嵌套写法 -------------------------
syntax = "proto3";
package contacts;
message PeopleInfo {
string name = 1;
int32 age = 2;
message Phone {
string number = 1;
}
}
// -------------------------- ⾮嵌套写法 -------------------------
syntax = "proto3";
package contacts;
message Phone {
string number = 1;
}
message PeopleInfo {
string name = 1;
int32 age = 2;
}
contacts.proto
syntax = "proto3";
package contacts2;
// 定义联系人message
message PeopleInfo {
string name = 1; // 姓名
int32 age = 2; // 年龄
message Phone {
string number = 1;
}
repeated Phone phone = 3; // 电话信息
}
// 通讯录message
message Contacts {
repeated PeopleInfo contacts = 1;
}
例如Phone消息定义在phone.proto⽂件中:
syntax = "proto3";
package phone;
message Phone {
string number = 1;
}
contacts.proto中的 PeopleInfo 使⽤ Phone 消息
syntax = "proto3";
package contacts;
import "phone.proto"; // 使⽤ import 将 phone.proto ⽂件导⼊进来 !!!
message PeopleInfo {
string name = 1;
int32 age = 2;
// 引⼊的⽂件声明了package,使⽤消息时,需要⽤ ‘命名空间.消息类型’ 格式
repeated phone.Phone phone = 3;
}
注:在proto3⽂件中可以导⼊proto2消息类型并使⽤它们,反之亦然。
通讯录2.x的需求是向⽂件中写⼊通讯录列表,以上我们只是定义了⼀个联系⼈的消息,并不能存放通讯录列表,所以还需要在完善⼀下contacts.proto(终版通讯录2.0):
syntax = "proto3";
package contacts2;
// 定义联系人message
message PeopleInfo {
string name = 1; // 姓名
int32 age = 2; // 年龄
message Phone {
string number = 1;
}
repeated Phone phone = 3; // 电话信息
}
// 通讯录message
message Contacts {
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 类
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 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⽅法来判断数组存放元素的个数。
write.cc
#include
#include
#include
#include "contacts.pb.h"
void AddPeopleInfo(contacts2::PeopleInfo * people)
{
std::cout << "--------------------新增联系人--------------------" << std::endl;
std::cout << "请输入联系人的姓名: ";
std::string name;
std::getline(std::cin, name);
people->set_name(name);
std::cout << "请输入联系人的年龄: ";
int age;
std::cin >> age;
people->set_age(age);
std::cin.ignore(256, '\n');
for(int i = 0; ; i++) {
std::cout << "请输入联系人的电话 " << i + 1 << " " << "(只输⼊回⻋完成电话新增): ";
std::string number;
std::getline(std::cin, number);
if(number.empty()) break;
contacts2::PeopleInfo_Phone *phone = people->add_phone();
phone->set_number(number);
}
std::cout << "-------------------添加联系人成功------------------" << std::endl;
}
int main()
{
contacts2::Contacts contacts;
// 读取本地已存在的通讯录文件
std::fstream input("contacts.bin", std::ios::in | std::ios::binary);
if (!input.is_open())
{
std::cout << "contacts.bin not exists, create new file" << std::endl;
}
else
{
if (!contacts.ParseFromIstream(&input))
{
std::cerr << "ParseFromIstream Failed!" << std::endl;
input.close();
return -1;
}
}
// 向通讯录中添加一个联系人 返回开辟好空间的 peopleinfo对象
AddPeopleInfo(contacts.add_contacts());
// 将通讯录写入本地文件中
std::fstream output("contacts.bin", std::ios::out | std::ios::trunc | std::ios::binary);
if(!contacts.SerializeToOstream(&output)) {
std::cerr << "SerializeToOstream Failed!" << std::endl;
input.close();
output.close();
return -1;
}
std::cout << "SerializeToOstream Success" << std::endl;
input.close();
output.close();
return 0;
}
read.cc
#include
#include
#include
#include "contacts.pb.h"
void PrintContacts(contacts2::Contacts &contacts)
{
for (int i = 0; i < contacts.contacts_size(); i++)
{
std::cout << "------------------联系人" << i + 1 << "------------------" << std::endl;
const contacts2::PeopleInfo &people = contacts.contacts(i);
std::cout << "联系人姓名:" << people.name() << std::endl;
std::cout << "联系人年龄: " << people.age() << std::endl;
for (int j = 0; j < people.phone_size(); j++)
{
const contacts2::PeopleInfo_Phone &phone = people.phone(j);
std::cout << "联系人电话: " << j + 1 << " " << phone.number() << std::endl;
}
}
}
int main()
{
contacts2::Contacts contacts;
// 读取本地已存在的通讯录文件
std::fstream intput("contacts.bin", std::ios::in | std::ios::binary);
if (!contacts.ParseFromIstream(&intput))
{
std::cout << "ParseFromIstream Failed!" << std::endl;
intput.close();
return -1;
}
// 打印通讯录列表
PrintContacts(contacts);
return 0;
}
另⼀种验证⽅法–decode
我们可以⽤ protoc -h 命令来查看ProtoBuf为我们提供的所有命令option。其中ProtoBuf提供⼀个命令选项 --decode ,表⽰从标准输⼊中读取给定类型的⼆进制消息,并将其以⽂本格式写⼊标准输出。消息类型必须在.proto⽂件或导⼊的⽂件中定义。
# protoc --decode=contacts2.Contacts contacts.proto < contacts.bin
contacts {
name: "khj"
age: 19
phone {
number: "12312412"
}
phone {
number: "1231231234"
}
}
语法⽀持我们定义枚举类型并使⽤。在 .proto ⽂件中枚举类型的书写规范为:
我们可以定义⼀个名为PhoneType的枚举类型,定义如下:
enum PhoneType {
MP = 0; // 移动电话
TEL = 1; // 固定电话
}
要注意枚举类型的定义有以下⼏种规则:
将两个‘具有相同枚举值名称’的枚举类型放在单个.proto⽂件下测试时,编译后会报错:某某某常量已经被定义!所以这⾥要注意:
更新contacts.proto(通讯录2.1),新增枚举字段并使⽤,更新内容如下:
syntax = "proto3";
package contacts2;
// 定义联系人message
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
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);
};
上述的代码中:
更新write.cc(通讯录2.1)
#include
#include
#include
#include "contacts.pb.h"
void AddPeopleInfo(contacts2::PeopleInfo *people)
{
std::cout << "--------------------新增联系人--------------------" << std::endl;
std::cout << "请输入联系人的姓名: ";
std::string name;
std::getline(std::cin, name);
people->set_name(name);
std::cout << "请输入联系人的年龄: ";
int age;
std::cin >> age;
people->set_age(age);
std::cin.ignore(256, '\n');
for (int i = 0;; i++)
{
std::cout << "请输入联系人的电话 " << i + 1 << " " << "(只输⼊回⻋完成电话新增): ";
std::string number;
std::getline(std::cin, number);
if (number.empty())
break;
contacts2::PeopleInfo_Phone *phone = people->add_phone();
phone->set_number(number);
std::cout << "请输入该电话的类型 (1. 移动电话 2. 固定电话): ";
int type;
std::cin >> type;
std::cin.ignore(256, '\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:
std::cout << "选择有误! " << std::endl;
break;
}
}
std::cout << "-------------------添加联系人成功------------------" << std::endl;
}
int main()
{
contacts2::Contacts contacts;
// 读取本地已存在的通讯录文件
std::fstream input("contacts.bin", std::ios::in | std::ios::binary);
if (!input.is_open())
{
std::cout << "contacts.bin not exists, create new file" << std::endl;
}
else
{
if (!contacts.ParseFromIstream(&input))
{
std::cerr << "ParseFromIstream Failed!" << std::endl;
input.close();
return -1;
}
}
// 向通讯录中添加一个联系人 返回开辟好空间的 peopleinfo对象
AddPeopleInfo(contacts.add_contacts());
// 将通讯录写入本地文件中
std::fstream output("contacts.bin", std::ios::out | std::ios::trunc | std::ios::binary);
if (!contacts.SerializeToOstream(&output))
{
std::cerr << "SerializeToOstream Failed!" << std::endl;
input.close();
output.close();
return -1;
}
std::cout << "SerializeToOstream Success" << std::endl;
input.close();
output.close();
return 0;
}
更新read.cc(通讯录2.1)
#include
#include
#include
#include "contacts.pb.h"
void PrintContacts(contacts2::Contacts &contacts)
{
for (int i = 0; i < contacts.contacts_size(); i++)
{
std::cout << "------------------联系人" << i + 1 << "------------------" << std::endl;
const contacts2::PeopleInfo &people = contacts.contacts(i);
std::cout << "联系人姓名:" << people.name() << std::endl;
std::cout << "联系人年龄: " << people.age() << std::endl;
for (int j = 0; j < people.phone_size(); j++)
{
const contacts2::PeopleInfo_Phone &phone = people.phone(j);
std::cout << "联系人电话: " << j + 1 << " " << phone.number();
std::cout << " (" << phone.PhoneType_Name(phone.type()) << ")" << std::endl;
}
}
}
int main()
{
contacts2::Contacts contacts;
// 读取本地已存在的通讯录文件
std::fstream intput("contacts.bin", std::ios::in | std::ios::binary);
if (!contacts.ParseFromIstream(&intput))
{
std::cout << "ParseFromIstream Failed!" << std::endl;
intput.close();
return -1;
}
// 打印通讯录列表
PrintContacts(contacts);
return 0;
}
代码完成后,编译后进⾏读写验证:
@139-159-150-152:~/project/protobuf/contacts$ ./write contacts.bin
-------------新增联系⼈-------------
请输⼊联系⼈姓名: 李四
请输⼊联系⼈年龄: 25
请输⼊联系⼈电话1(只输⼊回⻋完成电话新增): 12333
选择此电话类型 (1、移动电话 2、固定电话) : 2
请输⼊联系⼈电话2(只输⼊回⻋完成电话新增):
-----------添加联系⼈成功-----------
@139-159-150-152:~/project/protobuf/contacts$ ./read contacts.bin
------------联系⼈1------------
姓名:张三
年龄:20
电话1: 13111111111 (MP) // 这⾥打印出 MP 是因为未设置该字段,导致⽤了枚举的第⼀个
元素作为默认值
电话2: 15111111111 (MP)
------------联系⼈2------------
姓名:李四
年龄:25
电话1: 12333 (TEL)
字段还可以声明为Any类型,可以理解为泛型类型。使⽤时可以在Any中存储任意消息类型。Any类型的字段也⽤repeated来修饰。
Any类型是google已经帮我们定义好的类型,在安装ProtoBuf时,其中的include⽬录下查找所有 google已经定义好的.proto⽂件。
通讯录2.2版本会新增联系⼈的地址信息,我们可以使⽤any类型的字段来存储地址信息。
更新contacts.proto(通讯录2.2),更新内容如下:
syntax = "proto3";
package contacts2;
import "google/protobuf/any.proto";
message Address {
string home_address = 1; // 家庭住址
string unit_address = 2; // 单位住址
}
// 定义联系人message
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
message Contacts {
repeated PeopleInfo contacts = 1;
}
编译
protoc–cpp_out=.contacts.proto
contacts.pb.h更新的部分代码展⽰:
// 新⽣成的 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::MergeImpl(*this, from);
}
// string home_address = 1;
void clear_home_address();
const std::string &home_address() const;
template <typename ArgT0 = const std::string &, typename... ArgT>
void set_home_address(ArgT0 &&arg0, ArgT... args);
std::string *mutable_home_address();
PROTOBUF_NODISCARD std::string *release_home_address();
void set_allocated_home_address(std::string *home_address);
// string unit_address = 2;
void clear_unit_address();
const std::string &unit_address() const;
template <typename ArgT0 = const std::string &, typename... ArgT>
void set_unit_address(ArgT0 &&arg0, ArgT... args);
std::string *mutable_unit_address();
PROTOBUF_NODISCARD std::string *release_unit_address();
void set_allocated_unit_address(std::string *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类型字段:
之前讲过,我们可以在Any字段中存储任意消息类型,这就要涉及到任意消息类型和Any类型的互转。这部分代码就在Google 为我们写好的头⽂件 any.pb.h 中。对 any.pb.h 部分代码展⽰:
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 _impl_._any_metadata_.Is<T>();
}
};
解释:
使⽤ PackFrom() ⽅法可以将任意消息类型转为 Any 类型。
使⽤ UnpackTo() ⽅法可以将 Any 类型转回之前设置的任意消息类型。
使⽤ Is() ⽅法可以⽤来判断存放的消息类型是否为 typename T。
更新write.cc(通讯录2.2)
#include
#include
#include
#include "contacts.pb.h"
void AddPeopleInfo(contacts2::PeopleInfo *people)
{
std::cout << "--------------------新增联系人--------------------" << std::endl;
std::cout << "请输入联系人的姓名: ";
std::string name;
std::getline(std::cin, name);
people->set_name(name);
std::cout << "请输入联系人的年龄: ";
int age;
std::cin >> age;
people->set_age(age);
std::cin.ignore(256, '\n');
for (int i = 0;; i++)
{
std::cout << "请输入联系人的电话 " << i + 1 << " " << "(只输⼊回⻋完成电话新增): ";
std::string number;
std::getline(std::cin, number);
if (number.empty())
break;
contacts2::PeopleInfo_Phone *phone = people->add_phone();
phone->set_number(number);
std::cout << "请输入该电话的类型 (1. 移动电话 2. 固定电话): ";
int type;
std::cin >> type;
std::cin.ignore(256, '\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:
std::cout << "选择有误! " << std::endl;
break;
}
}
contacts2::Address address;
std::cout << "请输入联系人家庭地址: " << std::endl;
std::string home_address;
std::getline(std::cin, home_address);
address.set_home_address(home_address);
std::cout << "请输入联系人的单位地址: " << std::endl;
std::string unit_address;
std::getline(std::cin, unit_address);
address.set_unit_address(unit_address);
// Address -> Any
people->mutable_data()->PackFrom(address);
std::cout << "-------------------添加联系人成功------------------" << std::endl;
}
int main()
{
contacts2::Contacts contacts;
// 读取本地已存在的通讯录文件
std::fstream input("contacts.bin", std::ios::in | std::ios::binary);
if (!input.is_open())
{
std::cout << "contacts.bin not exists, create new file" << std::endl;
}
else
{
if (!contacts.ParseFromIstream(&input))
{
std::cerr << "ParseFromIstream Failed!" << std::endl;
input.close();
return -1;
}
}
// 向通讯录中添加一个联系人 返回开辟好空间的 peopleinfo对象
AddPeopleInfo(contacts.add_contacts());
// 将通讯录写入本地文件中
std::fstream output("contacts.bin", std::ios::out | std::ios::trunc | std::ios::binary);
if (!contacts.SerializeToOstream(&output))
{
std::cerr << "SerializeToOstream Failed!" << std::endl;
input.close();
output.close();
return -1;
}
std::cout << "SerializeToOstream Success" << std::endl;
input.close();
output.close();
return 0;
}
更新read.cc(通讯录2.2)
#include
#include
#include
#include "contacts.pb.h"
void PrintContacts(contacts2::Contacts &contacts)
{
for (int i = 0; i < contacts.contacts_size(); i++)
{
std::cout << "------------------联系人" << i + 1 << "------------------" << std::endl;
const contacts2::PeopleInfo &people = contacts.contacts(i);
std::cout << "联系人姓名:" << people.name() << std::endl;
std::cout << "联系人年龄: " << people.age() << std::endl;
for (int j = 0; j < people.phone_size(); j++)
{
const contacts2::PeopleInfo_Phone &phone = people.phone(j);
std::cout << "联系人电话: " << j + 1 << " " << phone.number();
std::cout << " (" << phone.PhoneType_Name(phone.type()) << ")" << std::endl;
}
if(people.has_data() && people.data().Is<contacts2::Address>()) {
contacts2::Address address;
people.data().UnpackTo(&address);
if(!address.home_address().empty()) {
std::cout << "联系人的家庭住址为:" << address.home_address() << std::endl;
}
if(!address.unit_address().empty()) {
std::cout << "联系人的家庭住址为:" << address.unit_address() << std::endl;
}
}
}
}
int main()
{
contacts2::Contacts contacts;
// 读取本地已存在的通讯录文件
std::fstream intput("contacts.bin", std::ios::in | std::ios::binary);
if (!contacts.ParseFromIstream(&intput))
{
std::cout << "ParseFromIstream Failed!" << std::endl;
intput.close();
return -1;
}
// 打印通讯录列表
PrintContacts(contacts);
return 0;
}
代码编写完成后,编译后进⾏读写:
139-159-150-152:~/project/protobuf/contacts$ ./write contacts.bin
-------------新增联系⼈-------------
请输⼊联系⼈姓名: 王五
请输⼊联系⼈年龄: 49
请输⼊联系⼈电话1(只输⼊回⻋完成电话新增): 642
选择此电话类型 (1、移动电话 2、固定电话) : 2
请输⼊联系⼈电话2(只输⼊回⻋完成电话新增):
请输⼊联系⼈家庭地址: 陕西省西安市
请输⼊联系⼈单位地址: 陕西省西安市
-----------添加联系⼈成功-----------
139-159-150-152:~/project/protobuf/contacts$ ./read contacts.bin
# 此处省略前两个添加的联系⼈
------------联系⼈3------------
姓名:王五
年龄:49
电话1: 642 (TEL)
家庭地址:陕西省西安市
单位地址:陕西省西安市
如果消息中有很多可选字段,并且将来同时只有⼀个字段会被设置,那么就可以使⽤ oneof 加强这个⾏为,也能有节约内存的效果。
通讯录2.3版本想新增联系⼈的其他联系⽅式,⽐如qq或者微信号⼆选⼀,我们就可以使⽤oneof字段来加强多选⼀这个⾏为。oneof字段定义的格式为: oneof 字段名 { 字段1; 字段2; … } 更新contacts.proto(通讯录2.3),更新内容如下:
syntax = "proto3";
package contacts2;
import "google/protobuf/any.proto";
message Address {
string home_address = 1; // 家庭住址
string unit_address = 2; // 单位住址
}
// 定义联系人message
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 {
// 不能使用repeated
string qq = 5;
string wechat = 6;
}
}
// 通讯录message
message Contacts {
repeated PeopleInfo contacts = 1;
}
注意:
编译
protoc–cpp_out=.contacts.proto
contacts.pb.h更新的部分代码展⽰:
// 更新的 PeopleInfo 类
class PeopleInfo final : public ::PROTOBUF_NAMESPACE_ID::Message
{
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 ArgT0 = const std::string &, typename... ArgT>
void set_qq(ArgT0 &&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 ArgT0 = const std::string &, typename... ArgT>
void set_weixin(ArgT0 &&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字段:
更新write.cc(通讯录2.3),更新内容如下:
#include
#include
#include
#include "contacts.pb.h"
void AddPeopleInfo(contacts2::PeopleInfo *people)
{
std::cout << "--------------------新增联系人--------------------" << std::endl;
std::cout << "请输入联系人的姓名: ";
std::string name;
std::getline(std::cin, name);
people->set_name(name);
std::cout << "请输入联系人的年龄: ";
int age;
std::cin >> age;
people->set_age(age);
std::cin.ignore(256, '\n');
for (int i = 0;; i++)
{
std::cout << "请输入联系人的电话 " << i + 1 << " " << "(只输⼊回⻋完成电话新增): ";
std::string number;
std::getline(std::cin, number);
if (number.empty())
break;
contacts2::PeopleInfo_Phone *phone = people->add_phone();
phone->set_number(number);
std::cout << "请输入该电话的类型 (1. 移动电话 2. 固定电话): ";
int type;
std::cin >> type;
std::cin.ignore(256, '\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:
std::cout << "选择有误! " << std::endl;
break;
}
}
contacts2::Address address;
std::cout << "请输入联系人家庭地址: " << std::endl;
std::string home_address;
std::getline(std::cin, home_address);
address.set_home_address(home_address);
std::cout << "请输入联系人的单位地址: " << std::endl;
std::string unit_address;
std::getline(std::cin, unit_address);
address.set_unit_address(unit_address);
// Address -> Any
people->mutable_data()->PackFrom(address);
std::cout << "请选择要添加的其他联系方式: (1. qq 2. wechat)";
int other_contact;
std::cin >> other_contact;
std::cin.ignore(256, '\n');
if(1 == other_contact) {
std::cout << "请输入联系人的QQ号:";
std::string qq;
std::getline(std::cin, qq);
people->set_qq(qq);
} else if(2 == other_contact) {
std::cout << "请输入联系的人 wechat:";
std::string wechat;
std::getline(std::cin, wechat);
people->set_wechat(wechat);
} else {
std::cout << "选择有误" << std::endl;
}
std::cout << "-------------------添加联系人成功------------------" << std::endl;
}
int main()
{
contacts2::Contacts contacts;
// 读取本地已存在的通讯录文件
std::fstream input("contacts.bin", std::ios::in | std::ios::binary);
if (!input.is_open())
{
std::cout << "contacts.bin not exists, create new file" << std::endl;
}
else
{
if (!contacts.ParseFromIstream(&input))
{
std::cerr << "ParseFromIstream Failed!" << std::endl;
input.close();
return -1;
}
}
// 向通讯录中添加一个联系人 返回开辟好空间的 peopleinfo对象
AddPeopleInfo(contacts.add_contacts());
// 将通讯录写入本地文件中
std::fstream output("contacts.bin", std::ios::out | std::ios::trunc | std::ios::binary);
if (!contacts.SerializeToOstream(&output))
{
std::cerr << "SerializeToOstream Failed!" << std::endl;
input.close();
output.close();
return -1;
}
std::cout << "SerializeToOstream Success" << std::endl;
input.close();
output.close();
return 0;
}
更新read.cc(通讯录2.3),更新内容如下:
#include
#include
#include
#include "contacts.pb.h"
void PrintContacts(contacts2::Contacts &contacts)
{
for (int i = 0; i < contacts.contacts_size(); i++)
{
std::cout << "------------------联系人" << i + 1 << "------------------" << std::endl;
const contacts2::PeopleInfo &people = contacts.contacts(i);
std::cout << "联系人姓名:" << people.name() << std::endl;
std::cout << "联系人年龄: " << people.age() << std::endl;
for (int j = 0; j < people.phone_size(); j++)
{
const contacts2::PeopleInfo_Phone &phone = people.phone(j);
std::cout << "联系人电话: " << j + 1 << " " << phone.number();
std::cout << " (" << phone.PhoneType_Name(phone.type()) << ")" << std::endl;
}
if (people.has_data() && people.data().Is<contacts2::Address>())
{
contacts2::Address address;
people.data().UnpackTo(&address);
if (!address.home_address().empty())
{
std::cout << "联系人的家庭住址为:" << address.home_address() << std::endl;
}
if (!address.unit_address().empty())
{
std::cout << "联系人的家庭住址为:" << address.unit_address() << std::endl;
}
}
switch (people.other_contact_case())
{
case contacts2::PeopleInfo::kQq:
std::cout << "联系人 qq: " << people.qq() << std::endl;
break;
case contacts2::PeopleInfo::kWechat:
std::cout << "联系人 wechat: " << people.wechat() << std::endl;
break;
default:
break;
}
}
}
int main()
{
contacts2::Contacts contacts;
// 读取本地已存在的通讯录文件
std::fstream intput("contacts.bin", std::ios::in | std::ios::binary);
if (!contacts.ParseFromIstream(&intput))
{
std::cout << "ParseFromIstream Failed!" << std::endl;
intput.close();
return -1;
}
// 打印通讯录列表
PrintContacts(contacts);
return 0;
}
语法⽀持创建⼀个关联映射字段,也就是可以使⽤map类型去声明字段类型,格式为: map
要注意的是:
最后,通讯录2.4版本想新增联系⼈的备注信息,我们可以使⽤map类型的字段来存储备注信息。
更新contacts.proto(通讯录2.4),更新内容如下:
syntax = "proto3";
package contacts2;
import "google/protobuf/any.proto";
message Address {
string home_address = 1; // 家庭住址
string unit_address = 2; // 单位住址
}
// 定义联系人message
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 {
// 不能使用repeated
string qq = 5;
string wechat = 6;
}
map<string, string> remark = 7; // 备注
}
// 通讯录message
message Contacts {
repeated PeopleInfo contacts = 1;
}
编译
protoc–cpp_out=.contacts.proto
contacts.pb.h更新的部分代码展⽰:
// 更新的 PeopleInfo 类
class PeopleInfo final : public ::PROTOBUF_NAMESPACE_ID::Message
{
// map 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类型的字段:
更新write.cc(通讯录2.4),更新内容如下:
#include
#include
#include
#include "contacts.pb.h"
void AddPeopleInfo(contacts2::PeopleInfo *people)
{
std::cout << "--------------------新增联系人--------------------" << std::endl;
std::cout << "请输入联系人的姓名: ";
std::string name;
std::getline(std::cin, name);
people->set_name(name);
std::cout << "请输入联系人的年龄: ";
int age;
std::cin >> age;
people->set_age(age);
std::cin.ignore(256, '\n');
for (int i = 0;; i++)
{
std::cout << "请输入联系人的电话 " << i + 1 << " " << "(只输⼊回⻋完成电话新增): ";
std::string number;
std::getline(std::cin, number);
if (number.empty())
break;
contacts2::PeopleInfo_Phone *phone = people->add_phone();
phone->set_number(number);
std::cout << "请输入该电话的类型 (1. 移动电话 2. 固定电话): ";
int type;
std::cin >> type;
std::cin.ignore(256, '\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:
std::cout << "选择有误! " << std::endl;
break;
}
}
contacts2::Address address;
std::cout << "请输入联系人家庭地址: " << std::endl;
std::string home_address;
std::getline(std::cin, home_address);
address.set_home_address(home_address);
std::cout << "请输入联系人的单位地址: " << std::endl;
std::string unit_address;
std::getline(std::cin, unit_address);
address.set_unit_address(unit_address);
// Address -> Any
people->mutable_data()->PackFrom(address);
std::cout << "请选择要添加的其他联系方式: (1. qq 2. wechat)";
int other_contact;
std::cin >> other_contact;
std::cin.ignore(256, '\n');
if(1 == other_contact) {
std::cout << "请输入联系人的QQ号:";
std::string qq;
std::getline(std::cin, qq);
people->set_qq(qq);
} else if(2 == other_contact) {
std::cout << "请输入联系的人 wechat:";
std::string wechat;
std::getline(std::cin, wechat);
people->set_wechat(wechat);
} else {
std::cout << "选择有误" << std::endl;
}
for(int i = 0; ; i++) {
std::cout << "请输入备注" << i + 1 << "标题:(只输入会车完成备注新增)";
std::string remark_key;
std::getline(std::cin, remark_key);
if(remark_key.empty()) {
break;
}
std::cout << "请输入备注内容: ";
std::string remark_value;
std::getline(std::cin, remark_value);
people->mutable_remark()->insert({remark_key, remark_value});
}
std::cout << "-------------------添加联系人成功------------------" << std::endl;
}
int main()
{
contacts2::Contacts contacts;
// 读取本地已存在的通讯录文件
std::fstream input("contacts.bin", std::ios::in | std::ios::binary);
if (!input.is_open())
{
std::cout << "contacts.bin not exists, create new file" << std::endl;
}
else
{
if (!contacts.ParseFromIstream(&input))
{
std::cerr << "ParseFromIstream Failed!" << std::endl;
input.close();
return -1;
}
}
// 向通讯录中添加一个联系人 返回开辟好空间的 peopleinfo对象
AddPeopleInfo(contacts.add_contacts());
// 将通讯录写入本地文件中
std::fstream output("contacts.bin", std::ios::out | std::ios::trunc | std::ios::binary);
if (!contacts.SerializeToOstream(&output))
{
std::cerr << "SerializeToOstream Failed!" << std::endl;
input.close();
output.close();
return -1;
}
std::cout << "SerializeToOstream Success" << std::endl;
input.close();
output.close();
return 0;
}
更新read.cc(通讯录2.4),更新内容如下:
#include
#include
#include
#include "contacts.pb.h"
void PrintContacts(contacts2::Contacts &contacts)
{
for (int i = 0; i < contacts.contacts_size(); i++)
{
std::cout << "------------------联系人" << i + 1 << "------------------" << std::endl;
const contacts2::PeopleInfo &people = contacts.contacts(i);
std::cout << "联系人姓名:" << people.name() << std::endl;
std::cout << "联系人年龄: " << people.age() << std::endl;
for (int j = 0; j < people.phone_size(); j++)
{
const contacts2::PeopleInfo_Phone &phone = people.phone(j);
std::cout << "联系人电话: " << j + 1 << " " << phone.number();
std::cout << " (" << phone.PhoneType_Name(phone.type()) << ")" << std::endl;
}
if (people.has_data() && people.data().Is<contacts2::Address>())
{
contacts2::Address address;
people.data().UnpackTo(&address);
if (!address.home_address().empty())
{
std::cout << "联系人的家庭住址为:" << address.home_address() << std::endl;
}
if (!address.unit_address().empty())
{
std::cout << "联系人的家庭住址为:" << address.unit_address() << std::endl;
}
}
switch (people.other_contact_case())
{
case contacts2::PeopleInfo::kQq:
std::cout << "联系人 qq: " << people.qq() << std::endl;
break;
case contacts2::PeopleInfo::kWechat:
std::cout << "联系人 wechat: " << people.wechat() << std::endl;
break;
default:
break;
}
if (people.remark_size())
{
std::cout << "备注信息: " << std::endl;
for (auto it = people.remark().cbegin(); it != people.remark().cend(); it++)
{
std::cout << " " << it->first << ": " << it->second << std::endl;
}
}
}
}
int main()
{
contacts2::Contacts contacts;
// 读取本地已存在的通讯录文件
std::fstream intput("contacts.bin", std::ios::in | std::ios::binary);
if (!contacts.ParseFromIstream(&intput))
{
std::cout << "ParseFromIstream Failed!" << std::endl;
intput.close();
return -1;
}
// 打印通讯录列表
PrintContacts(contacts);
return 0;
}
到此,我们对通讯录2.x要求的任务全部完成。在这个过程中我们将通讯录升级到了2.4版本,同时对 ProtoBuf的使⽤也进⼀步熟练了,并且也掌握了ProtoBuf的proto3语法⽀持的⼤部分类型及其使⽤,但只是正常使⽤还是完全不够的。通过接下来的学习,我们就能更进⼀步了解到ProtoBuf深⼊的内容。
反序列化消息时,如果被反序列化的⼆进制序列中不包含某个字段 (比如我要反序列化年龄,但是没有这个字段) ,反序列化对象中相应字段时,就会设置为该字段的默认值。不同的类型对应的默认值不同:
如果现有的消息类型已经不再满⾜我们的需求,例如需要扩展⼀个字段,在不破坏任何现有代码的情况下更新消息类型⾮常简单。遵循如下规则即可:
如果通过删除或注释掉字段来更新消息类型,未来的⽤⼾在添加新字段时,有可能会使⽤以前已经存在,但已经被删除或注释掉的字段编号。将来使⽤该.proto的旧版本时的程序会引发很多问题:数据损坏、隐私错误等等。
确保不会发⽣这种情况的⼀种⽅法是:使⽤ reserved 将指定字段的编号或名称设置为保留项。当我们再使⽤这些编号或名称时,protocolbuffer的编译器将会警告这些编号或名称不可⽤。举个例⼦:
message Message {
// 设置保留项
reserved 100, 101, 200 to 299;
reserved "field3", "field4";
// 注意:不要在⼀⾏ reserved 声明中同时声明字段编号和名称。
// reserved 102, "field5";
// 设置保留项之后,下⾯代码会告警
int32 field1 = 100; //告警:Field 'field1' uses reserved number 100
int32 field2 = 101; //告警:Field 'field2' uses reserved number 101
int32 field3 = 102; //告警:Field name 'field3' is reserved
int32 field4 = 103; //告警:Field name 'field4' is reserved
}
现模拟有两个服务,他们各⾃使⽤⼀份通讯录.proto⽂件,内容约定好了是⼀模⼀样的。
⼀段时间后,service更新了⾃⼰的.proto⽂件,更新内容为:删除了某个字段,并新增了⼀个字段,新增的字段使⽤了被删除字段的字段编号。并将新的序列化对象写进了⽂件。
但client并没有更新⾃⼰的.proto⽂件。根据结论,可能会出现数据损坏的现象,接下来就让我们来验证下这个结论。
新建两个⽬录:service、client。分别存放两个服务的代码。
service⽬录下新增contacts.proto(通讯录3.0)
syntax = "proto3";
package s_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⽬录下新增contacts.proto(通讯录3.0)
syntax = "proto3";
package c_contacts;
// 联系⼈
message PeopleInfo {
string name = 1; // 姓名
int32 age = 2; // 年龄
message Phone {
string number = 1; // 电话号码
}
repeated Phone phone = 3; // 电话
}
// 通讯录
message Contacts {
repeated PeopleInfo contacts = 1;
}
分别对两个⽂件进⾏编译,可⾃⾏操作。
继续对service⽬录下新增service.cc(通讯录3.0),负责向⽂件中写通讯录消息,内容如下:
#include
#include
#include "contacts.pb.h"
using namespace std;
using namespace s_contacts;
/**
* 新增联系⼈
*/
void AddPeopleInfo(PeopleInfo *people_info_ptr)
{
cout << "-------------新增联系⼈-------------" << endl;
cout << "请输⼊联系⼈姓名: ";
string name;
getline(cin, name);
people_info_ptr->set_name(name);
cout << "请输⼊联系⼈年龄: ";
int age;
cin >> age;
people_info_ptr->set_age(age);
cin.ignore(256, '\n');
for (int i = 1;; i++)
{
cout << "请输⼊联系⼈电话" << i << "(只输⼊回⻋完成电话新增): ";
string number;
getline(cin, number);
if (number.empty())
{
break;
}
PeopleInfo_Phone *phone = people_info_ptr->add_phone();
phone->set_number(number);
}
cout << "-----------添加联系⼈成功-----------" << endl;
}
int main(int argc, char *argv[])
{
GOOGLE_PROTOBUF_VERIFY_VERSION;
if (argc != 2)
{
cerr << "Usage: " << argv[0] << " CONTACTS_FILE" << endl;
return -1;
}
Contacts contacts;
// 先读取已存在的 contacts
fstream input(argv[1], ios::in | ios::binary);
if (!input)
{
cout << argv[1] << ": File not found. Creating a new file." << endl;
}
else if (!contacts.ParseFromIstream(&input))
{
cerr << "Failed to parse contacts." << endl;
input.close();
return -1;
}
// 新增⼀个联系⼈
AddPeopleInfo(contacts.add_contacts());
// 向磁盘⽂件写⼊新的 contacts
fstream output(argv[1], ios::out | ios::trunc | ios::binary);
if (!contacts.SerializeToOstream(&output))
{
cerr << "Failed to write contacts." << endl;
input.close();
output.close();
return -1;
}
input.close();
output.close();
google::protobuf::ShutdownProtobufLibrary();
return 0;
}
client⽬录下新增client.cc(通讯录3.0),负责向读出⽂件中的通讯录消息,内容如下:
#include
#include
#include "contacts.pb.h"
using namespace std;
using namespace c_contacts;
/**
* 打印联系⼈列表
*/
void PrintfContacts(const Contacts &contacts)
{
for (int i = 0; i < contacts.contacts_size(); ++i)
{
const PeopleInfo &people = contacts.contacts(i);
cout << "------------联系⼈" << i + 1 << "------------" << endl;
cout << "姓名:" << people.name() << endl;
cout << "年龄:" << people.age() << endl;
int j = 1;
for (const PeopleInfo_Phone &phone : people.phone())
{
cout << "电话" << j++ << ": " << phone.number() << endl;
}
}
}
int main(int argc, char *argv[])
{
GOOGLE_PROTOBUF_VERIFY_VERSION;
if (argc != 2)
{
cerr << "Usage: " << argv[0] << "CONTACTS_FILE" << endl;
return -1;
}
// 以⼆进制⽅式读取 contacts
Contacts contacts;
fstream input(argv[1], ios::in | ios::binary);
if (!contacts.ParseFromIstream(&input))
{
cerr << "Failed to parse contacts." << endl;
input.close();
return -1;
}
// 打印 contacts
PrintfContacts(contacts);
input.close();
google::protobuf::ShutdownProtobufLibrary();
return 0;
}
代码编写完成后,进⾏⼀次读写(读写前的编译过程省略,⾃⾏操作)
~/project/protobuf/update/service$ ./service
../contacts.bin
../contacts.bin: File not found. Creating a new file.
-------------新增联系⼈-------------
请输⼊联系⼈姓名: 张珊
请输⼊联系⼈年龄: 34
请输⼊联系⼈电话1(只输⼊回⻋完成电话新增): 131
请输⼊联系⼈电话2(只输⼊回⻋完成电话新增):
-----------添加联系⼈成功-----------
~/project/protobuf/update/client$ ./client ../contacts.bin
------------联系⼈1------------
姓名:张珊
年龄:34
电话1: 131
确认⽆误后,对service⽬录下的contacts.proto⽂件进⾏更新:除age字段,新增birthday字段,新增的字段使⽤被删除字段的字段编号。
更新后的contacts.proto(通讯录3.0)内容如下:
syntax = "proto3";
package s_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):
cout << "请输⼊联系⼈生日: ";
int birthday;
cin >> birthday;
people_info_ptr->set_birthday(birthday);
cin.ignore(256, '\n');
我们对client相关的代码保持原样,不进⾏更新。
再进⾏⼀次读写(对service.cc编译过程省略,⾃⾏操作)。
root@VM-12-8-ubuntu:~/job_skill/protobuf/update/service# ./service ../contacts.bin
-------------新增联系⼈-------------
请输⼊联系⼈姓名: 李四
请输⼊联系⼈生日: 24
请输⼊联系⼈电话1(只输⼊回⻋完成电话新增): 123
请输⼊联系⼈电话2(只输⼊回⻋完成电话新增):
-----------添加联系⼈成功-----------
root@VM-12-8-ubuntu:~/job_skill/protobuf/update/client# ./client ../contacts.bin
------------联系⼈1------------
姓名:张三
年龄:20
电话1: 123123
------------联系⼈2------------
姓名:李四
年龄:24
电话1: 123
这时问题便出现了,我们发现输⼊的⽣⽇,在反序列化时,被设置到了使⽤了相同字段编号的年龄上!!所以得出结论:若是移除⽼字段,要保证不再使⽤移除字段的字段编号
,不建议直接删除或注释掉字段。
那么正确的做法是保留字段编号(reserved),以确保该编号将不能被重复使⽤。
正确service⽬录下的contacts.proto写法如下(终版通讯录3.0)。
syntax = "proto3";
package s_contacts;
// 联系⼈
message PeopleInfo {
reserved 2, 10, 11, 100 to 200;
reserved "age";
string name = 1; // 姓名
// int32 age = 2; // 年龄
int32 birthday = 4;
message Phone {
string number = 1; // 电话号码
}
repeated Phone phone = 3; // 电话
}
// 通讯录
message Contacts {
repeated PeopleInfo contacts = 1;
}
编译.proto⽂件后,还需要重新编译下service.cc,让service程序保持使⽤新⽣成的pbC++⽂件。
root@VM-12-8-ubuntu:~/job_skill/protobuf/update/service# ./service ../contacts.bin
-------------新增联系⼈-------------
请输⼊联系⼈姓名: 王五
请输⼊联系⼈生日: 1020
请输⼊联系⼈电话1(只输⼊回⻋完成电话新增): 123123
请输⼊联系⼈电话2(只输⼊回⻋完成电话新增):
-----------添加联系⼈成功-----------
root@VM-12-8-ubuntu:~/job_skill/protobuf/update/client# ./client ../contacts.bin
------------联系⼈1------------
姓名:张三
年龄:20
电话1: 123123
------------联系⼈2------------
姓名:李四
年龄:24
电话1: 123
------------联系⼈3------------
姓名:王五
年龄:0
电话1: 123123
根据实验结果,发现‘王五’的年龄为0,这是由于新增时未设置年龄,通过client程序反序列化时,给年龄字段设置了默认值0。这个结果显然是我们想看到的。
还要解释⼀下‘李四’的年龄依旧使⽤了之前设置的⽣⽇字段‘1221’,这是因为在新增‘李四’的时候,⽣⽇字段的字段编号依旧为2,并且已经被序列化到⽂件中了。最后再读取的时候,字段编号依旧为2。
还要再说⼀下的是:因为使⽤了reserved关键字,ProtoBuf 在编译阶段就拒绝了我们使⽤已经保留的字段编号。到此实验结束,也印证了我们的结论。
根据以上的例⼦,有的同学可能还有⼀个疑问:如果使⽤了 reserved 2 了,那么service给‘王五’设置的⽣⽇‘1112’,client就没法读到了吗?答案是可以的。继续学习下⾯的未知字段即可揭晓答案。
在通讯录3.0版本中,我们向service⽬录下的contacts.proto新增了‘⽣⽇’字段,但对于client相关的代码并没有任何改动。验证后发现新代码序列化的消息(service)也可以被旧代码(client)解析。并且这⾥要说的是,新增的‘⽣⽇’字段在旧程序(client)中其实并没有丢失,⽽是会作为旧程序的未知字段。
仅仅提供序列化、反序列化功能
GetDescriptor
/ GetReflection
,可以获取该类型对应的 Descriptor 对象指针和Reflection对象指针。//google::protobuf::Message 部分代码展⽰
const Descriptor* GetDescriptor() const;
const Reflection* GetReflection() const;
class PROTOBUF_EXPORT Descriptor : private internal::SymbolBase
{
string& name () const
int field_count() const;
const FieldDescriptor* field(int index) const;
const FieldDescriptor* FindFieldByNumber(int number) const;
const FieldDescriptor* FindFieldByName(const std::string& name) const;
const FieldDescriptor* FindFieldByLowercaseName(
const std::string& lowercase_name) const;
const FieldDescriptor* FindFieldByCamelcaseName(
const std::string& camelcase_name) const;
int enum_type_count() const;
const EnumDescriptor* enum_type(int index) const;
const EnumDescriptor* FindEnumTypeByName(const std::string& name) const;
const EnumValueDescriptor* FindEnumValueByName(const std::string& name)
const;
}
// 部分代码展⽰
class PROTOBUF_EXPORT Reflection final
{
const UnknownFieldSet &GetUnknownFields(const Message &message) const;
UnknownFieldSet *MutableUnknownFields(Message *message) const;
bool HasField(const Message &message, const FieldDescriptor *field) const;
int FieldSize(const Message &message, const FieldDescriptor *field) const;
void ClearField(Message *message, const FieldDescriptor *field) 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 ------------------------------------------
// These get the value of a non-repeated field. They return the default
// value for fields that aren't set.
int32_t GetInt32(const Message &message, const FieldDescriptor *field) const;
int64_t GetInt64(const Message &message, const FieldDescriptor *field) const;
uint32_t GetUInt32(const Message &message,
const FieldDescriptor *field) const;
uint64_t 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,
MessageFactory *factory = nullptr) const;
// Singular field mutators -----------------------------------------
// These mutate the value of a non-repeated field.
void SetInt32(Message *message, const FieldDescriptor *field,
int32_t value) const;
void SetInt64(Message *message, const FieldDescriptor *field,
int64_t value) const;
void SetUInt32(Message *message, const FieldDescriptor *field,
uint32_t value) const;
void SetUInt64(Message *message, const FieldDescriptor *field,
uint64_t 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,
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,
MessageFactory *factory = nullptr) const;
PROTOBUF_NODISCARD Message *ReleaseMessage(
Message *message, const FieldDescriptor *field,
MessageFactory *factory = nullptr) const;
// Repeated field getters ------------------------------------------
// These get the value of one element of a repeated field.
int32_t GetRepeatedInt32(const Message &message, const FieldDescriptor *field,
int index) const;
int64_t GetRepeatedInt64(const Message &message, const FieldDescriptor *field,
int index) const;
uint32_t GetRepeatedUInt32(const Message &message,
const FieldDescriptor *field, int index) const;
uint64_t 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 -----------------------------------------
// These mutate the value of one element of a repeated field.
void SetRepeatedInt32(Message *message, const FieldDescriptor *field,
int index, int32_t value) const;
void SetRepeatedInt64(Message *message, const FieldDescriptor *field,
int index, int64_t value) const;
void SetRepeatedUInt32(Message *message, const FieldDescriptor *field,
int index, uint32_t value) const;
void SetRepeatedUInt64(Message *message, const FieldDescriptor *field,
int index, uint64_t 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, 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 -------------------------------------------
// These add an element to a repeated field.
void AddInt32(Message *message, const FieldDescriptor *field,
int32_t value) const;
void AddInt64(Message *message, const FieldDescriptor *field,
int64_t value) const;
void AddUInt32(Message *message, const FieldDescriptor *field,
uint32_t value) const;
void AddUInt64(Message *message, const FieldDescriptor *field,
uint64_t 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,
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,
MessageFactory *factory = nullptr) const;
const FieldDescriptor *FindKnownExtensionByName(
const std::string &name) const;
const FieldDescriptor *FindKnownExtensionByNumber(int number) const;
bool SupportsUnknownEnumValues() const;
};
class PROTOBUF_EXPORT UnknownFieldSet
{
inline void Clear();
void ClearAndFreeMemory();
inline bool empty() const;
inline int field_count() const;
inline const UnknownField &field(int index) const;
inline UnknownField *mutable_field(int index);
// Adding fields ---------------------------------------------------
void AddVarint(int number, uint64_t value);
void AddFixed32(int number, uint32_t value);
void AddFixed64(int number, uint64_t value);
void AddLengthDelimited(int number, const std::string &value);
std::string *AddLengthDelimited(int number);
UnknownFieldSet *AddGroup(int number);
// Parsing helpers -------------------------------------------------
// These work exactly like the similarly-named methods of Message.
bool MergeFromCodedStream(io::CodedInputStream *input);
bool ParseFromCodedStream(io::CodedInputStream *input);
bool ParseFromZeroCopyStream(io::ZeroCopyInputStream *input);
bool ParseFromArray(const void *data, int size);
inline bool ParseFromString(const std::string &data)
{
return ParseFromArray(data.data(), static_cast<int>(data.size()));
}
// Serialization.
bool SerializeToString(std::string *output) const;
bool SerializeToCodedStream(io::CodedOutputStream *output) const;
static const UnknownFieldSet &default_instance();
};
class PROTOBUF_EXPORT UnknownField
{
public:
enum Type
{
TYPE_VARINT,
TYPE_FIXED32,
TYPE_FIXED64,
TYPE_LENGTH_DELIMITED,
TYPE_GROUP
};
inline int number() const;
inline Type type() const;
// Accessors -------------------------------------------------------
// Each method works only for UnknownFields of the corresponding type.
inline uint64_t varint() const;
inline uint32_t fixed32() const;
inline uint64_t fixed64() const;
inline const std::string &length_delimited() const;
inline const UnknownFieldSet &group() const;
inline void set_varint(uint64_t value);
inline void set_fixed32(uint32_t value);
inline void set_fixed64(uint64_t value);
inline void set_length_delimited(const std::string &value);
inline std::string *mutable_length_delimited();
inline UnknownFieldSet *mutable_group();
};
更新client.cc(通讯录3.1),在这个版本中,需要打印出未知字段的内容。更新的代码如下:
#include
#include
#include "contacts.pb.h"
using namespace std;
using namespace c_contacts;
using namespace google::protobuf;
/**
* 打印联系⼈列表
*/
void PrintfContacts(const Contacts &contacts)
{
for (int i = 0; i < contacts.contacts_size(); ++i)
{
const PeopleInfo &people = contacts.contacts(i);
cout << "------------联系⼈" << i + 1 << "------------" << endl;
cout << "姓名:" << people.name() << endl;
cout << "年龄:" << people.age() << endl;
int j = 1;
for (const PeopleInfo_Phone &phone : people.phone())
{
cout << "电话" << j++ << ": " << phone.number() << endl;
}
// 获取到 reflection 对象
const Reflection* reflection = PeopleInfo::GetReflection();
// 通过 reflection 获取到未知字段的集合
const UnknownFieldSet& set = reflection->GetUnknownFields(people);
// 遍历集合
for(int j = 0; j < set.field_count(); j++) {
// 获取到单个未知字段
const UnknownField& unknown_field = set.field(j);
std::cout << "未知字段" << j + 1 << ": " << " 编号:" << unknown_field.number();
switch (unknown_field.type())
{
case UnknownField::Type::TYPE_VARINT:
std::cout << " 值: " << unknown_field.varint() << std::endl;
break;
case UnknownField::Type::TYPE_LENGTH_DELIMITED:
std::cout << " 值: " << unknown_field.length_delimited() << std::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 contacts;
fstream input(argv[1], ios::in | ios::binary);
if (!contacts.ParseFromIstream(&input))
{
cerr << "Failed to parse contacts." << endl;
input.close();
return -1;
}
// 打印 contacts
PrintfContacts(contacts);
input.close();
google::protobuf::ShutdownProtobufLibrary();
return 0;
}
其他⽂件均不⽤做任何修改,重新编译client.cc,进⾏⼀次读操作可得如下结果:
root@VM-12-8-ubuntu:~/job_skill/protobuf/update/client# ./client ../contacts.bin
------------联系⼈1------------
姓名:张三
年龄:20
电话1: 123123
------------联系⼈2------------
姓名:李四
年龄:24
电话1: 123
------------联系⼈3------------
姓名:王五
年龄:0
电话1: 123123
未知字段1: 编号:4 值: 1020
根据上述的例⼦可以得出,pb是具有向前兼容的。为了叙述⽅便,把增加了“⽣⽇”属性的service称为“新模块”;未做变动的client称为“⽼模块”。
前后兼容的作⽤:当我们维护⼀个很庞⼤的分布式系统时,由于你⽆法同时升级所有模块,为了保证在升级过程中,整个系统能够尽可能不受影响,就需要尽量保证通讯协议的“向后兼容”或“向前兼容”。
.proto⽂件中可以声明许多选项,使⽤ option 标注。选项能影响proto编译器的某些处理⽅式。
选项的完整列表在 google/protobuf/descriptor.proto 中定义。部分代码:
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 消息中
...
由此可⻅,选项分为 ⽂件级、消息级、字段级 等等,但并没有⼀种选项能作⽤于所有的类型。
optimize_for:该选项为⽂件选项,可以设置protoc编译器的优化级别,分别为 SPEED 、CODE_SIZE 、 LITE_RUNTIME 。受该选项影响,设置不同的优化级别,编译.proto⽂件后⽣成的代码内容不同。
syntax = "proto3";
option optimize_for = LITE_RUNTIME;
message PeopleInfo {
enum PhoneType {
option allow_alias = true;
MP = 0;
TEL = 1;
LANDLINE = 1; // 若不加 option allow_alias = true; 这⼀⾏会编译报错
}
string name = 1;
}
allow_alias:允许将相同的常量值分配给不同的枚举常量,⽤来定义别名。该选项为枚举选项。
ProtoBuf允许⾃定义选项并使⽤。该功能⼤部分场景⽤不到,在这⾥不拓展讲解。
有兴趣可以参考:https://developers.google.cn/protocol-buffers/docs/proto?hl=zhcn#customoptions
以上就是我对 ProtoBuf:proto3 语法详解
的理解, 觉得这篇博客对你有帮助的,可以点赞收藏关注支持一波~