ProtoBuf 的使用 - C++

一、初识ProtoBuf

1.1、序列化概念

序列化和反序列化

  • 序列化:把对象转换为字节序列的过程 称为对象的序列化。
  • 反序列化:把字节序列恢复为对象的过程 称为对象的反序列化。

什么情况下需要序列化

  • 存储数据:当想把的内存中的对象状态保存到⼀个⽂件中或者存到数据库中时。
  • ⽹络传输:⽹络直接传输数据,但是⽆法直接传输对象,所以要在传输前序列化,传输完成后反序列化成对象。

1.2、ProtoBuf 是什么

Protocol Buffers 是 Google 的⼀种语⾔⽆关、平台⽆关、可扩展的序列化结构数据的⽅法,它可⽤ 于(数据)通信协议、数据存储等。

Protocol Buffers 类⽐于 XML,是⼀种灵活,⾼效,⾃动化机制的结构数据序列化⽅法,但是⽐ XML 更⼩、更快、更为简单。

可以定义数据的结构,然后使⽤特殊⽣成的源代码轻松的在各种数据流中使⽤各种语⾔进⾏编写和 读取结构数据。甚⾄可以更新数据结构,⽽不破坏由旧数据结构编译的已部署程序。

简单来讲, ProtoBuf(全称为 Protocol Buffer)是让结构数据序列化的⽅法,其具有以下特点:

  • 语⾔⽆关、平台⽆关:即 ProtoBuf ⽀持 Java、C++、Python 等多种语⾔,⽀持多个平台。
  • ⾼效:即⽐ XML 更⼩、更快、更为简单。
  • 扩展性、兼容性好:可以更新数据结构,⽽不影响和破坏原有的旧程序。

1.3、ProtoBuf 的使⽤特点

  1.  编写 .proto ⽂件,⽬的是为了定义结构对象(message)及属性内容。
  2. 使⽤ protoc 编译器编译 .proto ⽂件,⽣成⼀系列接⼝代码,存放在新⽣成头⽂件和源⽂件中。
  3. 依赖⽣成的接⼝,将编译⽣成的头⽂件包含进我们的代码中,实现对 .proto ⽂件中定义的字段进⾏ 设置和获取,和对 message 对象进⾏序列化和反序列化。

总的来说:ProtoBuf 是需要依赖通过编译⽣成的头⽂件和源⽂件来使⽤的。有了这种代码⽣成机制, 开发⼈员再也不⽤编写那些协议解析的代码了。

二、安装 ProtoBuf

如何安装 ProtoBuf

三、快速上手

在快速上⼿中,会编写第⼀版本的通讯录 1.0。在通讯录 1.0 版本中,将实现:

  • 对⼀个联系⼈的信息使⽤ PB 进⾏序列化,并将结果打印出来。
  • 对序列化后的内容使⽤ PB 进⾏反序列,解析出联系⼈信息并打印出来。
  • 联系⼈包含以下信息: 姓名、年龄。

通过通讯录 1.0,便能了解使⽤ ProtoBuf 初步要掌握的内容,以及体验到 ProtoBuf 的完整使⽤流 程。

3.1、创建 .proto 文件

⽂件规范

  • 创建 .proto ⽂件时,⽂件命名应该使⽤全⼩写字⺟命名,多个字⺟之间⽤ _ 连接。 例如: lower_snake_case.proto 。
  • 书写 .proto ⽂件代码时,应使⽤ 2 个空格的缩进。

为通讯录 1.0 新建⽂件: contacts.proto

添加注释

向⽂件添加注释,可使⽤ // 或者 /* ... */

指定 proto3 语法

Protocol Buffers 语⾔版本3,简称 proto3,是 .proto ⽂件最新的语法版本。proto3 简化了 Protocol Buffers 语⾔,既易于使⽤,⼜可以在更⼴泛的编程语⾔中使⽤。它允许你使⽤ Java,C++,Python 等多种语⾔⽣成 protocol buffer 代码。

在 .proto ⽂件中,要使⽤ syntax = "proto3"; 来指定⽂件语法为 proto3,并且必须写在除去 注释内容的第⼀⾏。 如果没有指定,编译器会使⽤proto2语法。 在通讯录 1.0 的 contacts.proto ⽂件中,可以为⽂件指定 proto3 语法,内容如下:

syntax = "proto3";

package 声明符

package 是⼀个可选的声明符,能表⽰ .proto ⽂件的命名空间,在项⽬中要有唯⼀性。它的作⽤是为 了避免我们定义的消息出现冲突。

在通讯录 1.0 的 contacts.proto ⽂件中,可以声明其命名空间,内容如下:

syntax = "proto3";
package contacts;

定义消息(message)

消息(message): 要定义的结构化对象,可以给这个结构化对象中定义其对应的属性内容。 这⾥再提⼀下为什么要定义消息?

在⽹络传输中,我们需要为传输双⽅定制协议。定制协议说⽩了就是定义结构体或者结构化数据, ⽐如,tcp,udp 报⽂就是结构化的。

再⽐如将数据持久化存储到数据库时,会将⼀系列元数据统⼀⽤对象组织起来,再进⾏存储。

所以 ProtoBuf 就是以 message 的⽅式来⽀持定制协议字段,后期帮助形成类和⽅法来使⽤。在通讯录 1.0 中就需要为 联系⼈定义⼀个 message。

.proto ⽂件中定义⼀个消息类型的格式为:

message 消息类型名{
    ...
}
消息类型命名规范:使⽤驼峰命名法,⾸字⺟⼤写。

为 contacts.proto(通讯录 1.0)新增联系⼈message,内容如下:

syntax = "proto3";
package contacts;

// 定义联系⼈消息
message PeopleInfo {

}

定义消息字段

在 message 中可以定义其属性字段,字段定义格式为:字段类型 字段名 = 字段唯⼀编号;

  • 字段名称命名规范:全⼩写字⺟,多个字⺟之间⽤ _ 连接。
  • 字段类型分为:标量数据类型 和 特殊类型(包括枚举、其他消息类型等)。
  • 字段唯⼀编号:⽤来标识字段,⼀旦开始使⽤就不能够再改变。

该表格展⽰了定义于消息体中的标量数据类型,以及编译 .proto ⽂件之后⾃动⽣成的类中与之对应的 字段类型。在这⾥展⽰了与 C++ 语⾔对应的类型。

.proto Type   NotesC++ Type
doubledouble
floatfloat
int32使⽤变⻓编码[1]。负数的编码效率较低⸺若字段可 能为负值,应使⽤ sint32 代替。int32
int64使⽤变⻓编码[1]。负数的编码效率较低⸺若字段可 能为负值,应使⽤ sint64 代替。int64
uint32使⽤变⻓编码[1]。uint32
uint64使⽤变⻓编码[1]。uint64
sint32使⽤变⻓编码[1]。符号整型。负值的编码效率⾼于 常规的 int32 类型。int32
sint64使⽤变⻓编码[1]。符号整型。负值的编码效率⾼于 常规的 int64 类型。int64
fixed32定⻓ 4 字节。若值常⼤于2^28 则会⽐ uint32 更⾼效。uint32
fixed64定⻓ 8 字节。若值常⼤于2^56 则会⽐ uint64 更⾼效。uint64
sfixed32定⻓ 4 字节。int32
sfixed64int64
bool包含 UTF-8 和 ASCII 编码的字符串,⻓度不能超过 2^32 。bool
string包含 UTF-8 和 ASCII 编码的字符串,⻓度不能超过 2^32 。string
bytes可包含任意的字节序列但⻓度不能超过 2^32 。string

[1] 变⻓编码是指:经过protobuf 编码后,原本4字节或8字节的数可能会被变为其他字节数。

更新 contacts.proto (通讯录 1.0),新增姓名、年龄字段:

syntax = "proto3";
package contacts;

message PeopleInfo {
 string name = 1; 
 int32 age = 2; 
}

在这⾥还要特别讲解⼀下字段唯⼀编号的范围:

1 ~ 536,870,911 (2^29 - 1) ,其中 19000 ~ 19999 不可⽤。

19000 ~ 19999 不可⽤是因为:在 Protobuf 协议的实现中,对这些数进⾏了预留。如果⾮要在.proto ⽂件中使⽤这些预留标识号,例如将 name 字段的编号设置为19000,编译时就会报警:

// 消息中定义了如下编号,代码会告警:
// Field numbers 19,000 through 19,999 are reserved for the protobuf 
implementation
string name = 19000; 

值得⼀提的是,范围为 1 ~ 15 的字段编号需要⼀个字节进⾏编码, 16 ~ 2047 内的数字需要两个字节 进⾏编码。编码后的字节不仅只包含了编号,还包含了字段类型。所以 1 ~ 15 要⽤来标记出现⾮常频 繁的字段,要为将来有可能添加的、频繁出现的字段预留⼀些出来

3.2、编译 contacts.proto ⽂件,⽣成 C++ ⽂件

编译命令

编译命令⾏格式为:

protoc             [--proto_path=IMPORT_PATH]     --cpp_out=DST_DIR path/to/file.proto

protoc             是 Protocol Buffer 提供的命令⾏编译⼯具。
--proto_path       指定 被编译的.proto⽂件所在⽬录,可多次指定。可简写成 -I IMPORT_PATH 。
如不指定该参数,则在当前⽬录进⾏搜索。当某个.proto ⽂件 import 其他 .proto ⽂件时,或需要编译的 .proto ⽂件不在当前⽬录下,这时就要⽤-I来指定搜索⽬
录。

--cpp_out=          指编译后的⽂件为 C++ ⽂件。
OUT_DIR             编译后⽣成⽂件的⽬标路径。
path/to/file.proto  要编译的.proto⽂件。

编译 contacts.proto ⽂件命令如下:

protoc     --cpp_out=.     contacts.proto

编译 contacts.proto ⽂件后会⽣成什么

编译 contacts.proto ⽂件后,会⽣成所选择语⾔的代码,选择的是C++,所以编译后⽣成了两个⽂件: contacts.pb.h contacts.pb.cc 。

对于编译⽣成的 C++ 代码,包含了以下内容 :

  • 对于每个 message ,都会⽣成⼀个对应的消息类。
  • 在消息类中,编译器为每个字段提供了获取和设置⽅法,以及⼀下其他能够操作字段的⽅法。
  • 编辑器会针对于每个 .proto ⽂件⽣成 .h 和 .cc ⽂件,分别⽤来存放类的声明与类的实现。 contacts.pb.h 部分代码展⽰
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";
     }
     // string name = 1;
     void clear_name();
     const std::string& name() const;
     template <typename ArgT0 = const std::string&, typename... ArgT>
     void set_name(ArgT0&& arg0, ArgT... args);
     std::string* mutable_name();
     PROTOBUF_NODISCARD std::string* release_name();
     void set_allocated_name(std::string* name);
     // int32 age = 2;
     void clear_age();
     int32_t age() const;
     void set_age(int32_t value);
};

上述的例⼦中:

  • 每个字段都有设置和获取的⽅法, getter 的名称与⼩写字段完全相同,setter ⽅法以 set_ 开头。
  • 每个字段都有⼀个 clear_ ⽅法,可以将字段重新设置回 empty 状态。

contacts.pb.cc 中的代码就是对类声明⽅法的⼀些实现,在这⾥就不展开了。

之前提到的序列化和反序列化⽅法在哪⾥呢?在消息类的⽗类 MessageLite 中,提供了读写消息实例的⽅法,包括序列化⽅法和反序列化⽅法。

class MessageLite {
public:
     //序列化:
     bool SerializeToOstream(ostream* output) const; // 将序列化后数据写⼊⽂件
流
     bool SerializeToArray(void *data, int size) const;
     bool SerializeToString(string* output) const;
 
     //反序列化:
     bool ParseFromIstream(istream* input); // 从流中读取数据,再进⾏反序列化
动作
     bool ParseFromArray(const void* data, int size);
     bool ParseFromString(const string& data);
};

注意:

  • 序列化的结果为⼆进制字节序列,⽽⾮⽂本格式
  • 以上三种序列化的⽅法没有本质上的区别,只是序列化后输出的格式不同,可以供不同的应⽤场景 使⽤。
  • 序列化的 API 函数均为const成员函数,因为序列化不会改变类对象的内容, ⽽是将序列化的结果 保存到函数⼊参指定的地址中。
  • 详细 message API 可以参⻅ 完整列表

3.3、序列化与反序列化的使⽤

创建⼀个测试⽂件 main.cc 实现:

  • 对⼀个联系⼈的信息使⽤ PB 进⾏序列化,并将结果打印出来。
  • 对序列化后的内容使⽤ PB 进⾏反序列,解析出联系⼈信息并打印出来。

main.cc

#include <iostream> 
#include "contacts.pb.h" // 引⼊编译⽣成的头⽂件
using namespace std; 
 
int main() { 

 string people_str; 

 {
     // .proto⽂件声明的package,通过protoc编译后,会为编译⽣成的C++代码声明同名的
命名空间
     // 其范围是在.proto ⽂件中定义的内容
     contacts::PeopleInfo people; 
     people.set_age(20); 
     people.set_name("张珊"); 
     // 调⽤序列化⽅法,将序列化后的⼆进制序列存⼊string中
     if (!people.SerializeToString(&people_str)) { 
         cout << "序列化联系⼈失败." << endl; 
     }
     // 打印序列化结果
     cout << "序列化后的 people_str: " << people_str << endl; 
 }
 
 {
     contacts::PeopleInfo people; 
     // 调⽤反序列化⽅法,读取string中存放的⼆进制序列,并反序列化出对象
     if (!people.ParseFromString(people_str)) { 
         cout << "反序列化出联系⼈失败." << endl; 
     } 
     // 打印结果
     cout << "Parse age: " << people.age() << endl; 
     cout << "Parse name: " << people.name() << endl; 
 }
}

代码书写完成后,编译 main.cc,⽣成可执⾏程序 TestProtoBuf :

g++ main.cc contacts.pb.cc -o TestProtoBuf -std=c++11 -lprotobuf
  • -lprotobuf:必加,不然会有链接错误。
  • -std=c++11:必加,使⽤C++11语法。

执⾏ TestProtoBuf ,可以看⻅ people 经过序列化和反序列化后的结果:

由于 ProtoBuf 是把联系⼈对象序列化成了⼆进制序列,这⾥⽤ string 来作为接收⼆进制序列的容器。
所以在终端打印的时候会有换⾏等⼀些乱码显⽰。
所以相对于 xml 和 JSON 来说,因为被编码成⼆进制,破解成本增⼤,ProtoBuf 编码是相对安全的。

⼩结 ProtoBuf 使⽤流程

  1.  编写 .proto ⽂件,⽬的是为了定义结构对象(message)及属性内容。
  2. 使⽤ protoc 编译器编译 .proto ⽂件,⽣成⼀系列接⼝代码,存放在新⽣成头⽂件和源⽂件中。
  3. 依赖⽣成的接⼝,将编译⽣成的头⽂件包含进我们的代码中,实现对 .proto ⽂件中定义的字段进⾏ 设置和获取,和对 message 对象进⾏序列化和反序列化。

总的来说:ProtoBuf 是需要依赖通过编译⽣成的头⽂件和源⽂件来使⽤的。有了这种代码⽣成机制, 开发⼈员再也不⽤编写那些协议解析的代码了。

四、proto3 语法解析

这个部分会对通讯录进⾏多次升级,使⽤ 2.x 表⽰升级的版本,最终将会升级如下内容:

  • 不再打印联系⼈的序列化结果,⽽是将通讯录序列化后并写⼊⽂件中。
  • 从⽂件中将通讯录解析出来,并进⾏打印。
  • 新增联系⼈属性,共包括:姓名、年龄、电话信息、地址、其他联系⽅式、备注。

4.1、字段规则

消息的字段可以⽤下⾯⼏种规则来修饰:

  • singular :消息中可以包含该字段零次或⼀次(不超过⼀次)。 proto3 语法中,字段默认使⽤该 规则。
  • repeated:消息中可以包含该字段任意多次(包括零次),其中重复值的顺序会被保留。可以理 解为定义了⼀个数组。

更新 contacts.proto , PeopleInfo 消息中新增 phone_numbers 字段,表⽰⼀个联系⼈有多个 号码,可将其设置为 repeated,写法如下:

syntax = "proto3";
package contacts;

message PeopleInfo {
     string name = 1; 
     int32 age = 2; 
     repeated string phone_numbers = 3;
}

4.2、消息类型的定义与使⽤

4.2.1、定义

在单个 .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; 
}

4.2.2、使用

  • 消息类型可作为字段类型使⽤

contacts.proto

syntax = "proto3";
package contacts;

// 联系⼈
message PeopleInfo {
 string name = 1; 
 int32 age = 2; 

 message Phone {
     string number = 1; 
 }
 repeated Phone phone = 3; 
}
  • 可导⼊其他 .proto ⽂件的消息并使⽤

例如 Phone 消息定义在 phone.proto ⽂件中:

syntax = "proto3";
package phone;

message Phone {
 string number = 1; 
}

contacts.proto 中的 PeopleInfo 使⽤ Phone 消息:

syntax = "proto3";
package contacts;

import "phone.proto"; // 使⽤ import 将 phone.proto ⽂件导⼊进来 !!!

message PeopleInfo {
 string name = 1; 
 int32 age = 2; 
 
 // 引⼊的⽂件声明了package,使⽤消息时,需要⽤ ‘命名空间.消息类型’ 格式 
 
 repeated phone.Phone phone = 3; 
}

注:在 proto3 ⽂件中可以导⼊ proto2 消息类型并使⽤它们,反之亦然。

4.2.3、创建通讯录 2.0 版本

通讯录 2.x 的需求是向⽂件中写⼊通讯录列表,以上只是定义了⼀个联系⼈的消息,并不能存放通 讯录列表,所以还需要在完善⼀下 contacts.proto (终版通讯录 2.0):

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;
}

接着进⾏⼀次编译:

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 ⽅法来判断数组存放元素的个数。

通讯录 2.0 的写⼊实现
write.cc (通讯录 2.0)
#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 << "-----------添加联系⼈成功-----------" << endl;
}

int main(int argc, char *argv[])
{
// GOOGLE_PROTOBUF_VERIFY_VERSION 宏: 验证没有意外链接到与编译的头⽂件不兼容的库版
本。如果检测到版本不匹配,程序将中⽌。注意,每个 .pb.cc ⽂件在启动时都会⾃动调⽤此宏。在使
⽤ C++ Protocol Buffer 库之前执⾏此宏是⼀种很好的做法,但不是绝对必要的。
 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();
 
 // 在程序结束时调⽤ ShutdownProtobufLibrary(),为了删除 Protocol Buffer 库分配的所
有全局对象。对于⼤多数程序来说这是不必要的,因为该过程⽆论如何都要退出,并且操作系统将负责
回收其所有内存。但是,如果你使⽤了内存泄漏检查程序,该程序需要释放每个最后对象,或者你正在
编写可以由单个进程多次加载和卸载的库,那么你可能希望强制使⽤ Protocol Buffers 来清理所有
内容。
 google::protobuf::ShutdownProtobufLibrary();
 return 0;
}
makefile
write:write.cc contacts.pb.cc
 g++ -o $@ $^ -std=c++11 -lprotobuf

.PHONY:clean
clean:
 rm -f write

make之后,运⾏ write
ketil@8-134-127-49:~/project/protobuf/contacts$ make
g++ -o write write.cc contacts.pb.cc -std=c++11 -lprotobuf
ketil@8-134-127-49:~/project/protobuf/contacts$ ./write contacts.bin
contacts.bin: File not found. Creating a new file.
-------------新增联系⼈-------------
请输⼊联系⼈姓名: 张三
请输⼊联系⼈年龄: 20
请输⼊联系⼈电话1(只输⼊回⻋完成电话新增): 13111111111 
请输⼊联系⼈电话2(只输⼊回⻋完成电话新增): 15111111111
请输⼊联系⼈电话3(只输⼊回⻋完成电话新增): 
-----------添加联系⼈成功-----------

查看⼆进制⽂件

ketil@8-134-127-49:~/project/protobuf/contacts$ hexdump -C contacts.bin 
00000000 0a 28 0a 06 e5 bc a0 e4 b8 89 10 14 1a 0d 0a 0b |.(..............|
00000010 31 33 31 31 31 31 31 31 31 31 31 1a 0d 0a 0b 31 |13111111111....1|
00000020 35 31 31 31 31 31 31 31 31 31 |5111111111|
0000002a

解释:
 hexdump:是Linux下的⼀个⼆进制⽂件查看⼯具,它可以将⼆进制⽂件转换为ASCII、⼋进制、
⼗进制、⼗六进制格式进⾏查看。
 -C: 表⽰每个字节显⽰为16进制和相应的ASCII字符

通讯录 2.0 的读取实现
read.cc (通讯录 2.0)
#include <iostream>
#include <fstream>
#include "contacts.pb.h" 
using namespace std; 
using namespace 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;
}

makefile
all:write read

write:write.cc contacts.pb.cc
 g++ -o $@ $^ -std=c++11 -lprotobuf

read:read.cc contacts.pb.cc
 g++ -o $@ $^ -std=c++11 -lprotobuf

.PHONY:clean
clean:
 rm -f write read

make 后运⾏ read
ketil@8-134-127-49:~/project/protobuf/contacts$ make
g++ -o read read.cc contacts.pb.cc -std=c++11 -lprotobuf
ketil@8-134-127-49:~/project/protobuf/contacts$ ./read contacts.bin 
------------联系⼈1------------
姓名:张三
年龄:20
电话1: 13111111111
电话2: 15111111111

另⼀种验证⽅法--decode

可以⽤ protoc -h 命令来查看 ProtoBuf 为我们提供的所有命令 option。其中 ProtoBuf 提供 ⼀个命令选项 --decode ,表⽰从标准输⼊中读取给定类型的⼆进制消息,并将其以⽂本格式写⼊ 标准输出。 消息类型必须在 .proto ⽂件或导⼊的⽂件中定义。

hyb@139-159-150-152:~/project/protobuf/contacts$ 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"
 }
}

4.3、enum 类型

4.3.1、定义规则

语法⽀持定义枚举类型并使⽤。在.proto⽂件中枚举类型的书写规范为:

  枚举类型名称:

        使⽤驼峰命名法,⾸字⺟⼤写。 例如: MyEnum

  常量值名称:

        全⼤写字⺟,多个字⺟之间⽤ _ 连接。例如: ENUM_CONST = 0;

们可以定义⼀个名为 PhoneType 的枚举类型,定义如下:

enum PhoneType {
 MP = 0; // 移动电话
 TEL = 1; // 固定电话
}

要注意枚举类型的定义有以下⼏种规则:

  1. 0 值常量必须存在,且要作为第⼀个元素。这是为了与 proto2 的语义兼容:第⼀个元素作为默认值,且值为 0。
  2. 枚举类型可以在消息外定义,也可以在消息体内定义(嵌套)。
  3. 枚举的常量值在 32 位整数的范围内。但因负值⽆效因⽽不建议使⽤(与编码规则有关)。

4.3.2、定义时注意

将两个 ‘具有相同枚举值名称’ 的枚举类型放在单个 .proto ⽂件下测试时,编译后会报错:某某某常 量已经被定义!所以这⾥要注意:

  • 同级(同层)的枚举类型,各个枚举类型中的常量不能重名。
  • 单个 .proto ⽂件下,最外层枚举类型和嵌套枚举类型,不算同级。
  • 多个 .proto ⽂件下,若⼀个⽂件引⼊了其他⽂件,且每个⽂件都未声明 package,每个 proto ⽂ 件中的枚举类型都在最外层,算同级。
  • 多个 .proto ⽂件下,若⼀个⽂件引⼊了其他⽂件,且每个⽂件都声明了 package,不算同级。
// ---------------------- 情况1:同级枚举类型包含相同枚举值名称--------------------
enum PhoneType {
 MP = 0; // 移动电话
 TEL = 1; // 固定电话
}

enum PhoneTypeCopy {
 MP = 0; // 移动电话 // 编译后报错:MP 已经定义
}

// ---------------------- 情况2:不同级枚举类型包含相同枚举值名称-------------------
enum PhoneTypeCopy {
 MP = 0; // 移动电话 // ⽤法正确
}

message Phone {
 string number = 1; // 电话号码
 enum PhoneType {
     MP = 0; // 移动电话
     TEL = 1; // 固定电话
 }
}

// ---------------------- 情况3:多⽂件下都未声明package--------------------
// phone1.proto
import "phone1.proto"
enum PhoneType {
 MP = 0; // 移动电话 // 编译后报错:MP 已经定义
 TEL = 1; // 固定电话
}

// phone2.proto
enum PhoneTypeCopy {
 MP = 0; // 移动电话 
}

// ---------------------- 情况4:多⽂件下都声明了package--------------------
// phone1.proto
import "phone1.proto"
package phone1;
enum PhoneType {
 MP = 0; // 移动电话 // ⽤法正确
 TEL = 1; // 固定电话
}

// phone2.proto
package phone2;
enum PhoneTypeCopy {
 MP = 0; // 移动电话 
}

4.3.3、升级通讯录⾄ 2.1 版本

更新 contacts.proto (通讯录 2.1),新增枚举字段并使⽤,更新内容如下:
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 更新的部分代码展⽰:
// 新⽣成的 PeopleInfo_Phone_PhoneType 枚举类
enum PeopleInfo_Phone_PhoneType : int {
 PeopleInfo_Phone_PhoneType_MP = 0,
 PeopleInfo_Phone_PhoneType_TEL = 1,
 
 PeopleInfo_Phone_PhoneType_PeopleInfo_Phone_PhoneType_INT_MIN_SENTINEL_DO_NOT_U
SE_ = std::numeric_limits<int32_t>::min(),
 
 PeopleInfo_Phone_PhoneType_PeopleInfo_Phone_PhoneType_INT_MAX_SENTINEL_DO_NOT_U
SE_ = std::numeric_limits<int32_t>::max()
};

// 更新的 PeopleInfo_Phone 类
class PeopleInfo_Phone final : public ::PROTOBUF_NAMESPACE_ID::Message {
public:
 typedef PeopleInfo_Phone_PhoneType PhoneType;
 static inline bool PhoneType_IsValid(int value) {
     return PeopleInfo_Phone_PhoneType_IsValid(value);
 }
 template<typename T>
 static inline const std::string& PhoneType_Name(T enum_t_value) {...}
 static inline bool PhoneType_Parse(
 ::PROTOBUF_NAMESPACE_ID::ConstStringParam name, PhoneType* value) {...}

 // .contacts.PeopleInfo.Phone.PhoneType type = 2;
 void clear_type();
 ::contacts::PeopleInfo_Phone_PhoneType type() const;
 void set_type(::contacts::PeopleInfo_Phone_PhoneType value);
};

上述的代码中:

  • 对于在.proto⽂件中定义的枚举类型,编译⽣成的代码中会含有与之对应的枚举类型、校验枚举 值是否有效的⽅法 _IsValid、以及获取枚举值名称的⽅法 _Name。
  • 对于使⽤了枚举类型的字段,包含设置和获取字段的⽅法,已经清空字段的⽅法clear_。

更新 write.cc (通讯录 2.1)
#include <iostream>
#include <fstream>
#include "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::PeopleInfo_Phone_PhoneType_MP);
          break;
      case 2:
          phone->set_type(PeopleInfo_Phone_PhoneType::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;
 
 // 先读取已存在的 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;
}

更新 read.cc (通讯录 2.1)
#include <iostream>
#include <fstream>
#include "contacts.pb.h" 
using namespace std; 
using namespace 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();
         cout << " (" << 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 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;
}

代码完成后,编译后进⾏读写验证:

ketil@8-134-127-49:~/project/protobuf/contacts$ ./write contacts.bin
-------------新增联系⼈-------------
请输⼊联系⼈姓名: 李四
请输⼊联系⼈年龄: 25
请输⼊联系⼈电话1(只输⼊回⻋完成电话新增): 12333
选择此电话类型 (1、移动电话 2、固定电话) : 2
请输⼊联系⼈电话2(只输⼊回⻋完成电话新增): 
-----------添加联系⼈成功-----------
hyb@139-159-150-152:~/project/protobuf/contacts$ ./read contacts.bin 
------------联系⼈1------------
姓名:张三
年龄:20
电话1: 13111111111 (MP) // 这⾥打印出 MP 是因为未设置该字段,导致⽤了枚举的第⼀个
元素作为默认值
电话2: 15111111111 (MP)
------------联系⼈2------------
姓名:李四
年龄:25
电话1: 12333 (TEL)

4.4、Any 类型

字段还可以声明为 Any 类型,可以理解为泛型类型。使⽤时可以在 Any 中存储任意消息类型。Any 类 型的字段也⽤ repeated 来修饰。

Any 类型是 google 已经帮我们定义好的类型,在安装 ProtoBuf 时,其中的 include ⽬录下查找所有 google 已经定义好的 .proto ⽂件。

ketil@8-134-127-49:~/project/protobuf/contacts$ cd
/usr/local/protobuf/include/google/protobuf/
ketil@8-134-127-49:/usr/local/protobuf/include/google/protobuf$ ls
any.h    endian.h map_entry_lite.h    service.h
any.pb.h explicitly_constructed.h     map_field.h 
source_context.pb.h    any.proto extension_set.h      map_field_inl.h 
source_context.proto    api.pb.h extension_set_inl.h  map_field_lite.h 
struct.pb.h    api.proto field_access_listener.h      map.h 
struct.proto    arena.h field_mask.pb.h     map_type_handler.h 
stubs    arena_impl.h     field_mask.proto     message.h 
text_format.h    arenastring.h    generated_enum_reflection.h    message_lite.h 
timestamp.pb.h
arenaz_sampler.h generated_enum_util.h metadata.h 
timestamp.proto
compiler generated_message_bases.h metadata_lite.h 
type.pb.h
descriptor_database.h generated_message_reflection.h parse_context.h 
type.proto
descriptor.h generated_message_tctable_decl.h port_def.inc 
unknown_field_set.h
descriptor.pb.h generated_message_tctable_impl.h port.h 
util
descriptor.proto generated_message_util.h port_undef.inc 
wire_format.h
duration.pb.h has_bits.h reflection.h 
wire_format_lite.h
duration.proto implicit_weak_message.h reflection_internal.h 
wrappers.pb.h
dynamic_message.h inlined_string_field.h reflection_ops.h 
wrappers.proto
empty.pb.h io repeated_field.h
empty.proto map_entry.h repeated_ptr_field.h

4.4.1、升级通讯录⾄ 2.2 版本

通讯录 2.2 版本会新增联系⼈的地址信息,可以使⽤ any 类型的字段来存储地址信息。

更新 contacts.proto (通讯录 2.2),更新内容如下:
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

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 类型字段:

  • 设置和获取:获取⽅法的⽅法名称与⼩写字段名称完全相同。设置⽅法可以使⽤ mutable_ ⽅ 法,返回值为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 <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::PeopleInfo_Phone_PhoneType_MP);
             break;
         case 2:
             phone>set_type(PeopleInfo_Phone_PhoneType::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[])
{
     ...
}

更新 read.cc (通讯录 2.2)
#include <iostream>
#include <fstream>
#include "contacts.pb.h" 
using namespace std; 
using namespace 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();
         cout << " (" << 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[]) {
     ...
}

代码编写完成后,编译后进⾏读写:

ketil@8-134-127-49:~/project/protobuf/contacts$ ./write contacts.bin
-------------新增联系⼈-------------
请输⼊联系⼈姓名: 王五
请输⼊联系⼈年龄: 49
请输⼊联系⼈电话1(只输⼊回⻋完成电话新增): 642
选择此电话类型 (1、移动电话 2、固定电话) : 2
请输⼊联系⼈电话2(只输⼊回⻋完成电话新增): 
请输⼊联系⼈家庭地址: xx省xx市
请输⼊联系⼈单位地址: xx省xx市
-----------添加联系⼈成功-----------

ketil@8-134-127-49:~/project/protobuf/contacts$ ./read contacts.bin 
# 此处省略前两个添加的联系⼈
------------联系⼈3------------
姓名:王五
年龄:49
电话1: 642 (TEL)
家庭地址:xx省xx市
单位地址:xx省xx市

4.5、oneof 类型

如果消息中有很多可选字段, 并且将来同时只有⼀个字段会被设置, 那么就可以使⽤ oneof 加强这 个⾏为,也能有节约内存的效果。

4.5.1、升级通讯录⾄ 2.3 版本

通讯录 2.3 版本想新增联系⼈的其他联系⽅式,⽐如qq或者微信号⼆选⼀,我们就可以使⽤ oneof 字 段来加强多选⼀这个⾏为。oneof 字段定义的格式为: oneof 字段名 { 字段1; 字段2; ... }

更新 contacts.proto (通讯录 2.3),更新内容如下:
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

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 字段:

  • 会将 oneof 中的多个字段定义为⼀个枚举类型。
  • 设置和获取:对 oneof 内的字段进⾏常规的设置和获取即可,但要注意只能设置⼀个。如果设置 多个,那么只会保留最后⼀次设置的成员。
  • 清空oneof字段:clear_ ⽅法
  • 获取当前设置了哪个字段:_case ⽅法

更新 write.cc (通讯录 2.3),更新内容如下:
#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::PeopleInfo_Phone_PhoneType_MP);
             break;
         case 2:
             phone->set_type(PeopleInfo_Phone_PhoneType::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;
 }
 cout << "-----------添加联系⼈成功-----------" << endl;
}

int main(int argc, char *argv[])
{
     ...
}

更新 read.cc (通讯录 2.3),更新内容如下:
#include <iostream>
#include <fstream>
#include "contacts.pb.h" 
using namespace std; 
using namespace 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();
         cout << " (" << 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;
         } 
     }

     /* if (people.has_qq()) {
     } else if (people.has_weixin()) {
     } */

     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[]) {
     ...
}

代码编写完成后,编译后进⾏读写:

ketil@8-134-127-49:~/project/protobuf/contacts$ ./write contacts.bin
-------------新增联系⼈-------------
请输⼊联系⼈姓名: 郭六
请输⼊联系⼈年龄: 38
请输⼊联系⼈电话1(只输⼊回⻋完成电话新增): 171
选择此电话类型 (1、移动电话 2、固定电话) : 1
请输⼊联系⼈电话2(只输⼊回⻋完成电话新增): 
请输⼊联系⼈家庭地址: 北京市
请输⼊联系⼈单位地址: 北京市
选择添加⼀个其他联系⽅式 (1、qq号 2、微信号) : 2
请输⼊微信号: guo_liu
-----------添加联系⼈成功-----------


ketil@8-134-127-49:~/project/protobuf/contacts$ ./read contacts.bin 
# 此处省略前三个添加的联系⼈
------------联系⼈4------------
姓名:郭六
年龄:38
电话1: 171 (MP)
家庭地址:北京市
单位地址:北京市
微信号: guo_liu

4.6、map 类型

语法⽀持创建⼀个关联映射字段,也就是可以使⽤ map 类型去声明字段类型,格式为:

map<key_type, value_type​​​​​​​> map_field = N;

要注意的是:

  • key_type 是除了 float 和 bytes 类型以外的任意标量类型。 value_type 可以是任意类型。
  • map 字段不可以⽤ repeated 修饰
  • map 中存⼊的元素是⽆序的

4.6.1、升级通讯录⾄ 2.4 版本

最后,通讯录 2.4 版本想新增联系⼈的备注信息,我们可以使⽤ map 类型的字段来存储备注信息。

更新 contacts.proto (通讯录 2.4),更新内容如下:
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

contacts.pb.h 更新的部分代码展⽰:
// 更新的 PeopleInfo 类
class PeopleInfo final : public ::PROTOBUF_NAMESPACE_ID::Message {
 // 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),更新内容如下:
#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::PeopleInfo_Phone_PhoneType_MP);
         break;
     case 2:
         phone->set_type(PeopleInfo_Phone_PhoneType::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[])
{
 ...
}
更新 read.cc (通讯录 2.4),更新内容如下:
#include <iostream>
#include <fstream>
#include "contacts.pb.h" 
using namespace std; 
using namespace 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();
         cout << " (" << 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;
         } 
     }
     /* if (people.has_qq()) {
     } else if (people.has_weixin()) {} */

     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()) {
         cout << "备注信息: " << endl;
     }
     for (auto it = people.remark().cbegin(); it != people.remark().cend(); 
++it){ 
         cout << " " << it->first << ": " << it->second << endl; 
     } 
 }
}

int main(int argc, char* argv[]) {
     ...
}

代码编写完成后,编译后进⾏读写:

ketil@8-134-127-49:~/project/protobuf/contacts$ ./write contacts.bin
-------------新增联系⼈-------------
请输⼊联系⼈姓名: 胡七
请输⼊联系⼈年龄: 28
请输⼊联系⼈电话1(只输⼊回⻋完成电话新增): 110
选择此电话类型 (1、移动电话 2、固定电话) : 2
请输⼊联系⼈电话2(只输⼊回⻋完成电话新增): 
请输⼊联系⼈家庭地址: 海南海⼝
请输⼊联系⼈单位地址: 海南海⼝
选择添加⼀个其他联系⽅式 (1、qq号 2、微信号) : 1
请输⼊qq号: 123123123
请输⼊备注1标题 (只输⼊回⻋完成备注新增): ⽇程
请输⼊备注1内容: 10⽉1⼀起出去玩
请输⼊备注2标题 (只输⼊回⻋完成备注新增): 
-----------添加联系⼈成功-----------


ketil@8-134-127-49:~/project/protobuf/contacts$ ./read contacts.bin 
# 此处省略前四个添加的联系⼈
------------联系⼈5------------
姓名:胡七
年龄:28
电话1: 110 (TEL)
家庭地址:海南海⼝
单位地址:海南海⼝
qq号: 123123123
备注信息: 
 ⽇程: 10⽉1⼀起出去玩

到此,对通讯录 2.x 要求的任务全部完成。在这个过程中我们将通讯录升级到了 2.4 版本,同时对 ProtoBuf 的使⽤也进⼀步熟练了,并且也掌握了 ProtoBuf 的 proto3 语法⽀持的⼤部分类型及其使 ⽤,但只是正常使⽤还是完全不够的。通过接下来的学习,我们就能更进⼀步了解到 ProtoBuf 深⼊的内容。

4.7、默认值

反序列化消息时,如果被反序列化的⼆进制序列中不包含某个字段,反序列化对象中相应字段时,就 会设置为该字段的默认值。不同的类型对应的默认值不同:

  • 对于字符串,默认值为空字符串。
  • 对于字节,默认值为空字节。
  • 对于布尔值,默认值为 false。
  • 对于数值类型,默认值为 0。
  • 对于枚举,默认值是第⼀个定义的枚举值, 必须为 0。
  • 对于消息字段,未设置该字段。它的取值是依赖于语⾔。
  • 对于设置了 repeated 的字段的默认值是空的( 通常是相应语⾔的⼀个空列表 )。
  • 对于消息字段 、 oneof字段 和 any字段 ,C++ 和 Java 语⾔中都有 has_ ⽅法来检测当前字段 是否被设置。

4.8、更新信息

4.8.1、更新规则

如果现有的消息类型已经不再满⾜需求,例如需要扩展⼀个字段,在不破坏任何现有代码的情 况下更新消息类型⾮常简单。遵循如下规则即可:

  • 禁⽌修改任何已有字段的字段编号。
  • 若是移除⽼字段,要保证不再使⽤移除字段的字段编号。正确的做法是保留字段编号 (reserved),以确保该编号将不能被重复使⽤。不建议直接删除或注释掉字段。
  • int32, uint32, int64, uint64 和 bool 是完全兼容的。可以从这些类型中的⼀个改为另⼀个, ⽽不破坏前后兼容性。若解析出来的数值与相应的类型不匹配,会采⽤与 C++ ⼀致的处理⽅案 (例如,若将 64 位整数当做 32 位进⾏读取,它将被截断为 32 位)。
  • sint32 和 sint64 相互兼容但不与其他的整型兼容。
  • string 和 bytes 在合法 UTF-8 字节前提下也是兼容的。
  • bytes 包含消息编码版本的情况下,嵌套消息与 bytes 也是兼容的。
  • fixed32 与 sfixed32 兼容, fixed64 与 sfixed64兼容。
  • enum 与 int32,uint32, int64 和 uint64 兼容(注意若值不匹配会被截断)。但要注意当反序 列化消息时会根据语⾔采⽤不同的处理⽅案:例如,未识别的 proto3 枚举类型会被保存在消息 中,但是当消息反序列化时如何表⽰是依赖于编程语⾔的。整型字段总是会保持其的值。
  • oneof:
    • 将⼀个单独的值更改为 新 oneof 类型成员之⼀是安全和⼆进制兼容的。
    • 若确定没有代码⼀次性设置多个值那么将多个字段移⼊⼀个新 oneof 类型也是可⾏的。
    • 将任何字段移⼊已存在的 oneof 类型是不安全的。

4.8.2、保留字段 reserved

如果通过 删除 或 注释掉 字段来更新消息类型,未来的⽤⼾在添加新字段时,有可能会使⽤以前已经 存在,但已经被删除或注释掉的字段编号。将来使⽤该 .proto 的旧版本时的程序会引发很多问题:数据损坏、隐私错误等等。

确保不会发⽣这种情况的⼀种⽅法是:使⽤ reserved 将指定字段的编号或名称设置为保留项 。当 我们再使⽤这些编号或名称时,protocol buffer 的编译器将会警告这些编号或名称不可⽤。举个例 ⼦:

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
}

创建通讯录 3.0 版本---验证 错误删除字段 造成的数据损坏

现模拟有两个服务,他们各⾃使⽤⼀份通讯录 .proto ⽂件,内容约定好了是⼀模⼀样的。

        服务1(service):负责序列化通讯录对象,并写⼊⽂件中。

        服务2(client):负责读取⽂件中的数据,解析并打印出来。

⼀段时间后,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 <iostream>
#include <fstream>
#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;
}

service ⽬录下新增 makefile
service:service.cc contacts.pb.cc
 g++ -o $@ $^ -std=c++11 -lprotobuf

.PHONY:clean
clean:
 rm -f service

client ⽬录下新增 client.cc (通讯录 3.0),负责向读出⽂件中的通讯录消息,内容如下:
#include <iostream>
#include <fstream>
#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;
}

client ⽬录下新增 makefile
client:client.cc contacts.pb.cc
 g++ -o $@ $^ -std=c++11 -lprotobuf

.PHONY:clean
clean:
 rm -f client

代码编写完成后,进⾏⼀次读写

ketil@8-134-127-49:~/project/protobuf/update/service$ ./service
../contacts.bin
../contacts.bin: File not found. Creating a new file.
-------------新增联系⼈-------------
请输⼊联系⼈姓名: 张珊
请输⼊联系⼈年龄: 34
请输⼊联系⼈电话1(只输⼊回⻋完成电话新增): 131
请输⼊联系⼈电话2(只输⼊回⻋完成电话新增): 
-----------添加联系⼈成功-----------


hyb@139-159-150-152:~/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):
#include <iostream>
#include <fstream>
#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'); */
 cout << "请输⼊联系⼈⽣⽇: ";
 int birthday;
 cin >> birthday;
 people_info_ptr->set_birthday(birthday);
 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[]) {...}

对 client 相关的代码保持原样,不进⾏更新。 再进⾏⼀次读写(对 service.cc 编译过程省略,⾃⾏操作)。

ketil@8-134-127-49:~/project/protobuf/update/service$ ./service
../contacts.bin
-------------新增联系⼈-------------
请输⼊联系⼈姓名: 李四
请输⼊联系⼈⽣⽇: 1221
请输⼊联系⼈电话1(只输⼊回⻋完成电话新增): 151
请输⼊联系⼈电话2(只输⼊回⻋完成电话新增): 
-----------添加联系⼈成功-----------


ketil@8-134-127-49:~/project/protobuf/update/client$ ./client ../contacts.bin
------------联系⼈1------------
姓名:张珊
年龄:34
电话1: 131
------------联系⼈2------------
姓名:李四
年龄:1221
电话1: 151

若是移除⽼字段,要保证不再使⽤移除字段的字段编号,不建议直接删除或注 释掉字段。

那么正确的做法是保留字段编号(reserved),以确保该编号将不能被重复使⽤。

正确 service ⽬录下的 contacts.proto 写法如下(终版通讯录 3.0)。
syntax = "proto3";
package s_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,让 service 程序保持使⽤新⽣成的 pb C++⽂件。

ketil@8-134-127-49:~/project/protobuf/update/service$ ./service
../contacts.bin
-------------新增联系⼈-------------
请输⼊联系⼈姓名: 王五
请输⼊联系⼈⽣⽇: 1112
请输⼊联系⼈电话1(只输⼊回⻋完成电话新增): 110
请输⼊联系⼈电话2(只输⼊回⻋完成电话新增): 
-----------添加联系⼈成功-----------


ketil@8-134-127-49:~/project/protobuf/update/client$ ./client ../contacts.bin
------------联系⼈1------------
姓名:张珊
年龄:34
电话1: 131
------------联系⼈2------------
姓名:李四
年龄:1221
电话1: 151
------------联系⼈3------------
姓名:王五
年龄:0
电话1: 110

根据实验结果,发现 ‘王五’ 的年龄为 0,这是由于新增时未设置年龄,通过 client 程序反序列化 时,给年龄字段设置了默认值 0。这个结果显然是想看到的。

还要解释⼀下 ‘李四’ 的年龄依旧使⽤了之前设置的⽣⽇字段 ‘1221’,这是因为在新增 ‘李四’ 的时候,⽣⽇字段的字段编号依旧为 2,并且已经被序列化到⽂件中了。最后再读取的时候,字段编号 依旧为 2。

还要再说⼀下的是:因为使⽤了 reserved 关键字,ProtoBuf在编译阶段就拒绝了我们使⽤已经保留 的字段编号。到此实验结束,也印证了我们的结论。

根据以上的例⼦,有的同学可能还有⼀个疑问:如果使⽤了 reserved 2 了,那么 service 给 ‘王 五’ 设置的⽣⽇ ‘1112’,client 就没法读到了吗? 答案是可以的。继续学习下⾯的未知字段即可揭 晓答案。

4.8.3、未知字段

在通讯录 3.0 版本中,我们向 service ⽬录下的 contacts.proto 新增了‘⽣⽇’字段,但对于 client 相 关的代码并没有任何改动。验证后发现 新代码序列化的消息(service)也可以被旧代码(client)解 析。并且这⾥要说的是,新增的 ‘⽣⽇’字段在旧程序(client)中其实并没有丢失,⽽是会作为旧程 序的未知字段

  • 未知字段:解析结构良好的 protocol buffer 已序列化数据中的未识别字段的表⽰⽅式。例如,当 旧程序解析带有新字段的数据时,这些新字段就会成为旧程序的未知字段。
  • 本来,proto3 在解析消息时总是会丢弃未知字段,但在 3.5 版本中重新引⼊了对未知字段的保留机 制。所以在 3.5 或更⾼版本中,未知字段在反序列化时会被保留,同时也会包含在序列化的结果 中。

未知字段从哪获取

了解相关类关系图

 MessageLite 类介绍(了解)
  • MessageLite 从名字看是轻量级的 message,仅仅提供序列化、反序列化功能。
  • 类定义在 google 提供的 message_lite.h 中。

Message 类介绍(了解)
  • ⾃定义的message类,都是继承⾃Message。
  • Message 最重要的两个接⼝ GetDescriptor/GetReflection,可以获取该类型对应的Descriptor对象 指针 和 Reflection 对象指针。
  • 类定义在 google 提供的 message.h 中。
//google::protobuf::Message 部分代码展⽰
const Descriptor* GetDescriptor() const;
const Reflection* GetReflection() const;

Descriptor 类介绍(了解)
  • Descriptor:是对message类型定义的描述,包括message的名字、所有字段的描述、原始的 proto⽂件内容等。
  • 类定义在 google 提供的 descriptor.h 中。
// 部分代码展⽰
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;
}

Reflection 类介绍(了解)
  • Reflection接⼝类,主要提供了动态读写消息字段的接⼝,对消息对象的⾃动读写主要通过该类完成。
  • 提供⽅法来动态访问/修改message中的字段,对每种类型,Reflection都提供了⼀个单独的接⼝⽤于读写字段对应的值。
    • 针对所有不同的field类型 FieldDescriptor::TYPE_* ,需要使⽤不同的 Get*()/Set* ()/Add*() 接⼝;
    • repeated类型需要使⽤ GetRepeated*()/SetRepeated*() 接⼝,不可以和⾮repeated 类型接⼝混⽤;
    • message对象只可以被由它⾃⾝的 reflection(message.GetReflection()) 来操作;
  • 类中还包含了访问/修改未知字段的⽅法。
  • 类定义在 google 提供的 message.h 中。
// 部分代码展⽰
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;
};

UnknownFieldSet 类介绍(重要)
  • UnknownFieldSet 包含在分析消息时遇到但未由其类型定义的所有字段。
  • 若要将 UnknownFieldSet 附加到任何消息,请调⽤ Reflection::GetUnknownFields()。
  • 类定义在 unknown_field_set.h 中。
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();
};

UnknownField 类介绍(重要)
  • 表⽰未知字段集中的⼀个字段。
  • 类定义在 unknown_field_set.h 中。
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();
};

升级通讯录 3.1 版本---验证未知字段
更新 client.cc (通讯录 3.1),在这个版本中,需要打印出未知字段的内容。更新的代码如下:
#include <iostream>
#include <fstream>
#include <google/protobuf/unknown_field_set.h>
#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;
     }
     // 打印未知字段
     const Reflection* reflection = PeopleInfo::GetReflection();
     const UnknownFieldSet& unknowSet = reflection->GetUnknownFields(people);
     for (int j = 0; j < unknowSet.field_count(); j++) {
         const UnknownField& unknow_field = unknowSet.field(j);
         cout << "未知字段" << j+1 << ":" 
         << " 字段编号: " << unknow_field.number() 
         << " 类型: "<< unknow_field.type();
         switch (unknow_field.type()) {
             case UnknownField::Type::TYPE_VARINT: 
                 cout << " 值: " << unknow_field.varint() << endl;
                 break;
             case UnknownField::Type::TYPE_LENGTH_DELIMITED:
                 cout << " 值: " << unknow_field.length_delimited() << endl;
                 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,进⾏⼀次读操作可得如下结果:

ketil@8-134-127-49:~/project/protobuf/update/client$ ./client ../contacts.bin 
------------联系⼈1------------
姓名:张珊
年龄:34
电话1: 131
------------联系⼈2------------
姓名:李四
年龄:1221
电话1: 151
------------联系⼈3------------
姓名:王五
年龄:0
电话1: 110
未知字段1: 字段编号: 4 类型: 0 值: 1112


类型为何为 0 ?在介绍 UnknownField 类中讲到了类中包含了未知字段的⼏种类型:
enum Type {
 TYPE_VARINT,
 TYPE_FIXED32,
 TYPE_FIXED64,
 TYPE_LENGTH_DELIMITED,
 TYPE_GROUP
};
类型为 0,即为 TYPE_VARINT

前后兼容性

根据上述的例⼦可以得出,pb是具有向前兼容的。为了叙述⽅便,把增加了“⽣⽇”属性的 service 称为“新模块”;未做变动的 client 称为 “⽼模块”。

  • 向前兼容:⽼模块能够正确识别新模块⽣成或发出的协议。这时新增加的“⽣⽇”属性会被当作未 知字段(pb 3.5版本及之后)。
  • 向后兼容:新模块也能够正确识别⽼模块⽣成或发出的协议。

前后兼容的作⽤:当我们维护⼀个很庞⼤的分布式系统时,由于你⽆法同时 升级所有 模块,为了保证 在升级过程中,整个系统能够尽可能不受影响,就需要尽量保证通讯协议的“向后兼容”或“向前兼容”。

4.9、选项 option

.proto ⽂件中可以声明许多选项,使⽤ option 标注。选项能影响 proto 编译器的某些处理⽅式。

4.9.1、选项分类

选项的完整列表在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 消息中

...

由此可⻅,选项分为 ⽂件级、消息级、字段级 等等, 但并没有⼀种选项能作⽤于所有的类型。

4.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。这种模式通常⽤于资源 有限的平台,例如移动⼿机平台中。
option optimize_for = LITE_RUNTIME;
  • allow_alias : 允许将相同的常量值分配给不同的枚举常量,⽤来定义别名。该选项为枚举选项。 举个例⼦:
enum PhoneType {
 option allow_alias = true;
 MP = 0;
 TEL = 1;
 LANDLINE = 1; // 若不加 option allow_alias = true; 这⼀⾏会编译报错
}

4.9.3、设置⾃定义选项

ProtoBuf 允许⾃定义选项并使⽤。该功能⼤部分场景⽤不到,在这⾥不拓展讲解。

有兴趣可以参考: https://developers.google.cn/protocol-buffers/docs/proto?hl=zhcn#customoptions

五、通讯录 4.0 实现---⽹络版

main.cc

void menu() {
 std::cout << "-----------------------------------------------------" << 
std::endl
 << "--------------- 请选择对通讯录的操作 ----------------" << 
std::endl
 << "------------------ 1、新增联系⼈ --------------------" << 
std::endl 
 << "------------------ 2、删除联系⼈ --------------------" << 
std::endl
 << "------------------ 3、查看联系⼈列表 ----------------" << 
std::endl 
 << "------------------ 4、查看联系⼈详细信息 ------------" << 
std::endl
 << "------------------ 0、退出 --------------------------" << 
std::endl
 << "-----------------------------------------------------" << 
std::endl;
}
int main() {
     enum OPERATE {ADD=1, DEL, FIND_ALL, FIND_ONE};
     ContactsServer contactsServer;
     while (true) {
         menu();
         std::cout << "---> 请选择:";
         int choose;
         std::cin >> choose;
         std::cin.ignore(256, '\n');
         try {
             switch (choose) {
                 case OPERATE::ADD:
                     contactsServer.addContact();
                     break;
                 case OPERATE::DEL:
                     contactsServer.delContact();
                     break;
                 case OPERATE::FIND_ALL:
                     contactsServer.findContacts();
                     break;
                 case OPERATE::FIND_ONE:
                     contactsServer.findContact();
                     break;
                 case 0:
                     std::cout << "---> 程序已退出" << std::endl;
                     return 0;
                 default:
                     std::cout << "---> ⽆此选项,请重新选择!" << std::endl;
                     break;
             }
         } catch (const ContactException& e) {
             std::cerr << "---> 操作通讯录时发现异常!!!" << std::endl
             << "---> 异常信息:" << e.what() << std::endl;
         } catch (const std::exception& e) {
             std::cerr << "---> 操作通讯录时发现异常!!!" << std::endl
             << "---> 异常信息:" << e.what() << std::endl;
         }
     }
}

ContactException.h:定义异常类

// ⾃定义异常类
class ContactException
{
private:
     std::string message;
public:
     ContactException(std::string str = "A problem") : message{str} {}
     std::string what() const { return message; }
};

ContactsServer.h:客⼾端通讯录服务定义

class ContactsServer
{
public:
     void addContact();
     void delContact();
     void findContacts();
     void findContact();
private:
     void buildAddContactRequest(add_contact_req::AddContactRequest* req);
     void printFindOneContactResponse(find_one_contact_resp::FindOneContactResponse& 
     resp);
     void printFindAllContactsResponse(find_all_contacts_resp::FindAllContactsResponse& 
    resp);
};

ContactsServer.cc:客⼾端通讯录服务实现

#define CONTACTS_IP "43.138.218.166"
#define CONTACTS_PORT 8123
void ContactsServer::addContact() {
     httplib::Client cli(CONTACTS_IP, CONTACTS_PORT);
     // 构建 request 请求
     add_contact_req::AddContactRequest req;
     buildAddContactRequest(&req);
     // 序列化 request
     std::string req_str;
     if (!req.SerializeToString(&req_str)) {
         throw ContactException("AddContactRequest序列化失败!");
     }
     // 发起 post 请求
     auto res = cli.Post("/contacts/add", req_str, "application/protobuf");
     if (!res) {
         std::string err_desc;
         err_desc.append("/contacts/add 链接错误!错误信息:")
         .append(httplib::to_string(res.error()));
         throw ContactException(err_desc);
     }
     // 反序列化 response
     add_contact_resp::AddContactResponse resp;
     bool parse = resp.ParseFromString(res->body);
     // 处理异常
     if (res->status != 200 && !parse) {
         std::string err_desc;
         err_desc.append("post '/contacts/add/' 失败:")
         .append(std::to_string(res->status))
         .append("(").append(res->reason)
         .append(")");
         throw ContactException(err_desc);
     }
     else if (res->status != 200) {
         // 处理服务异常
         std::string err_desc;
         err_desc.append("post '/contacts/add/' 失败 ")
         .append(std::to_string(res->status))
         .append("(").append(res->reason)
         .append(") 错误原因:")
         .append(resp.base_resp().error_desc());
         throw ContactException(err_desc);
     }
     else if (!resp.base_resp().success()) {
         // 处理结果异常
         std::string err_desc;
         err_desc.append("post '/contacts/add/' 结果异常:")
         .append("异常原因:")
         .append(resp.base_resp().error_desc());
         throw ContactException(err_desc);
     }
     // 正常返回,打印结果
     std::cout << "---> 新增联系⼈成功,联系⼈ID:" << resp.uid() << std::endl;
}


void ContactsServer::delContact() {
     httplib::Client cli(CONTACTS_IP, CONTACTS_PORT);
     // 构建 request 请求
     del_contact_req::DelContactRequest req;
     std::cout << "请输⼊要删除的联系⼈id: ";
     std::string uid;
     getline(std::cin, uid);
     req.set_uid(uid);
     // 序列化 request
     std::string req_str;
     if (!req.SerializeToString(&req_str)) {
         throw ContactException("DelContactRequest序列化失败!");
     }
     // 发起 post 请求
     auto res = cli.Post("/contacts/del", req_str, "application/protobuf");
     if (!res) {
         std::string err_desc;
         err_desc.append("/contacts/del 链接错误!错误信息:")
         .append(httplib::to_string(res.error()));
         throw ContactException(err_desc);
     }
     // 反序列化 response
     del_contact_resp::DelContactResponse resp;
     bool parse = resp.ParseFromString(res->body);
     // 处理异常
     if (res->status != 200 && !parse) {
         std::string err_desc;
         err_desc.append("post '/contacts/del' 失败:")
         .append(std::to_string(res->status))
         .append("(").append(res->reason)
         .append(")");
         throw ContactException(err_desc);
     }
     else if (res->status != 200) {
         std::string err_desc;
         err_desc.append("post '/contacts/del' 失败 ")
         .append(std::to_string(res->status))
         .append("(").append(res->reason)
         .append(") 错误原因:")
         .append(resp.base_resp().error_desc());
         throw ContactException(err_desc);
     }
     else if (!resp.base_resp().success()) {
         // 结果异常
         std::string err_desc;
         err_desc.append("post '/contacts/del' 结果异常:")
         .append("异常原因:")
         .append(resp.base_resp().error_desc());
         throw ContactException(err_desc);
     }
     // 正常返回,打印结果
     std::cout << "---> 成功删除联系⼈,被删除的联系⼈ID为:" << resp.uid() << 
     std::endl;
}


void ContactsServer::findContacts() {
     httplib::Client cli(CONTACTS_IP, CONTACTS_PORT);
     // 发起 get 请求
     auto res = cli.Get("/contacts/find-all");
     if (!res) {
         std::string err_desc;
         err_desc.append("/contacts/find-all 链接错误!错误信息:")
         .append(httplib::to_string(res.error()));
         throw ContactException(err_desc);
     }
     // 反序列化 response
     find_all_contacts_resp::FindAllContactsResponse resp;
     bool parse = resp.ParseFromString(res->body);
     // 处理异常
     if (res->status != 200 && !parse) {
         std::string err_desc;
         err_desc.append("get '/contacts/find-all' 失败:")
         .append(std::to_string(res->status))
         .append("(").append(res->reason)
         .append(")");
         throw ContactException(err_desc);
     }
     else if (res->status != 200) {
         // 服务端异常
         std::string err_desc;
         err_desc.append("post '/contacts/find-all' 失败 ")
         .append(std::to_string(res->status))
         .append("(").append(res->reason)
         .append(") 错误原因:")
         .append(resp.base_resp().error_desc());
         throw ContactException(err_desc);
     }
     else if (!resp.base_resp().success()) {
         // 结果异常
         std::string err_desc;
         err_desc.append("post '/contacts/find-all' 结果异常:")
         .append("异常原因:")
         .append(resp.base_resp().error_desc());
         throw ContactException(err_desc);
     }
     // 正常返回,打印结果
     printFindAllContactsResponse(resp);
}


void ContactsServer::findContact() {
     httplib::Client cli(CONTACTS_IP, CONTACTS_PORT);
     // 构建 request 请求
     find_one_contact_req::FindOneContactRequest req;
     std::cout << "请输⼊要查询的联系⼈id: ";
     std::string uid;
     getline(std::cin, uid);
     req.set_uid(uid);
     // 序列化 request
     std::string req_str;
     if (!req.SerializeToString(&req_str)) {
         throw ContactException("FindOneContactRequest序列化失败!");
     }
     // 发起 post 请求
     auto res = cli.Post("/contacts/find-one", req_str, "application/protobuf");
     if (!res) {
         std::string err_desc;
         err_desc.append("/contacts/find-one 链接错误!错误信息:")
         .append(httplib::to_string(res.error()));
         throw ContactException(err_desc);
     }
     // 反序列化 response
     find_one_contact_resp::FindOneContactResponse resp;
     bool parse = resp.ParseFromString(res->body);
     // 处理异常
     if (res->status != 200 && !parse) {
         std::string err_desc;
         err_desc.append("post '/contacts/find-one' 失败:")
         .append(std::to_string(res->status))
         .append("(").append(res->reason)
         .append(")");
         throw ContactException(err_desc);
     }
     else if (res->status != 200) {
         std::string err_desc;
         err_desc.append("post '/contacts/find-one' 失败 ")
         .append(std::to_string(res->status))
         .append("(").append(res->reason)
         .append(") 错误原因:")
         .append(resp.base_resp().error_desc());
         throw ContactException(err_desc);
     }
     else if (!resp.base_resp().success()) {
         // 结果异常
         std::string err_desc;
         err_desc.append("post '/contacts/find-one' 结果异常:")
         .append("异常原因:")
         .append(resp.base_resp().error_desc());
         throw ContactException(err_desc);
     }
     // 正常返回,打印结果
     std::cout << "---> 查询到联系⼈ID为:" << resp.uid() << " 的信息:" << 
     std::endl;
     printFindOneContactResponse(resp);
}


void ContactsServer::printFindAllContactsResponse(
find_all_contacts_resp::FindAllContactsResponse& resp) {
     if (0 == resp.contacts_size()) {
         std::cout << "还未添加任何联系⼈" << std::endl;
         return;
     }
     for (auto contact : resp.contacts()) {
         std::cout << "联系⼈姓名: " << contact.name() << " 联系⼈ID:" << 
         contact.uid() << std::endl;
     }
}


void
ContactsServer::buildAddContactRequest(add_contact_req::AddContactRequest* req)
 {
     std::cout << "请输⼊联系⼈姓名: ";
     std::string name;
     getline(std::cin, name);
     req->set_name(name);
     std::cout << "请输⼊联系⼈年龄: ";
     int age;
     std::cin >> age;
     req->set_age(age);
     std::cin.ignore(256, '\n'); 
     for(int i = 1; ; i++) {
         std::cout << "请输⼊联系⼈电话" << i << "(只输⼊回⻋完成电话新增): ";
         std::string number;
         getline(std::cin, number);
         if (number.empty()) {
             break;
         }
         add_contact_req::AddContactRequest_Phone* phone = req->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( 
                 add_contact_req::AddContactRequest_Phone_PhoneType::
                 AddContactRequest_Phone_PhoneType_MP);
                 break;
             case 2:
                 phone->set_type( 
                 add_contact_req::AddContactRequest_Phone_PhoneType::
                 AddContactRequest_Phone_PhoneType_TEL);
                 break;
             default:
                 std::cout << "----⾮法选择,使⽤默认值!" << std::endl;
                 break;
         }    
     }
     for(int i = 1; ; i++) {
         std::cout << "请输⼊备注" << i << "标题 (只输⼊回⻋完成备注新增): ";
         std::string remark_key;
         getline(std::cin, remark_key);
         if (remark_key.empty()) {
             break;
         }
         std::cout << "请输⼊备注" << i << "内容: ";
         std::string remark_value;
         getline(std::cin, remark_value);
         req->mutable_remark()->insert({remark_key, remark_value}); 
     }
}


void ContactsServer::printFindOneContactResponse(
 find_one_contact_resp::FindOneContactResponse& 
resp) {
     std::cout << "姓名:" << resp.name() << std::endl;
     std::cout << "年龄:" << resp.age() << std::endl;
     for (auto& phone : resp.phone()) {
         int j = 1;
         std::cout << "电话" << j++ << ": " << phone.number();
         std::cout << " (" << phone.PhoneType_Name(phone.type()) << ")" << 
         std::endl;
     }
     if (resp.remark_size()) {
         std::cout << "备注信息: " << std::endl;
     }
     for (auto it = resp.remark().cbegin(); it != resp.remark().cend(); ++it) {
         std::cout << " " << it->first << ": " << it->second << std::endl;
     }
}

服务端代码实现

服务端存储通讯录结构定义:contacts.proto

syntax = "proto3";
package contacts;

// 联系⼈
message PeopleInfo {
     string uid = 1; // 联系⼈ID
     string name = 2; // 姓名
     int32 age = 3; // 年龄

     message Phone {
         string number = 1; // 电话号码
         enum PhoneType {
             MP = 0; // 移动电话
             TEL = 1; // 固定电话
         }
     PhoneType type = 2; // 类型
     }
 
     repeated Phone phone = 4; // 电话
     map<string, string> remark = 5; // 备注
}

// 通讯录
message Contacts {
    map<string, PeopleInfo> contacts = 1;
}

main.cc

using std::cout;
using std::endl;
using std::cerr;
using namespace httplib;
int main() {
 
     cout << "---> 服务启动..." << endl;
     Server srv; // 创建服务端对象
     ContactsServer contactsServer;
     srv.Post("/contacts/add", [contactsServer](const Request& req, Response& res) {
         add_contact_req::AddContactRequest request;
         add_contact_resp::AddContactResponse response;
         try {
             // 反序列化 request
             if (!request.ParseFromString(req.body)) {
                 throw ContactException("Parse AddContactRequest error!");
             }
             // 新增联系⼈
             contactsServer.add(request, &response);
             // 序列化 resp
             std::string response_str;
             if (!response.SerializeToString(&response_str)) {
                 throw ContactException("Serialize AddContactResponse error");
             }
             res.body = response_str;
             res.set_header("Content-Type", "application/protobuf");
             res.status = 200;
         } catch (ContactException &e) {
             cerr << "---> /contacts/add 发现异常!!!" << endl
             << "---> 异常信息:" << e.what() << endl;
             res.status = 500;
             base_response::BaseResponse* baseResponse = 
             response.mutable_base_resp();
             baseResponse->set_success(false);
             baseResponse->set_error_desc(e.what());
             std::string response_str;
             if (response.SerializeToString(&response_str)) {
                 res.body = response_str;
                 res.set_header("Content-Type", "application/protobuf");
             }
         }
     }); 
     srv.Post("/contacts/del", [contactsServer](const Request& req, Response& res) {
         del_contact_req::DelContactRequest request;
         del_contact_resp::DelContactResponse response;
         try {
             // 反序列化 request
             if (!request.ParseFromString(req.body)) {
                 throw ContactException("Parse DelContactRequest error!");
             }
             // 删除联系⼈
             contactsServer.del(request, &response);
             // 序列化 response
             std::string response_str;
             if (!response.SerializeToString(&response_str)) {
                 throw ContactException("Serialize DelContactResponse error");
             }
             res.body = response_str;
             res.set_header("Content-Type", "application/protobuf");
             res.status = 200;
         } catch (ContactException &e) {
             cerr << "---> /contacts/del 发现异常!!!" << endl
             << "---> 异常信息:" << e.what() << endl;
             res.status = 500;
             base_response::BaseResponse* baseResponse = 
             response.mutable_base_resp();
             baseResponse->set_success(false);
             baseResponse->set_error_desc(e.what());
             std::string response_str;
             if (response.SerializeToString(&response_str)) {
                 res.body = response_str;
                 res.set_header("Content-Type", "application/protobuf");
             }
         }
     }); 

     srv.Post("/contacts/find-one", [contactsServer](const Request& req, Response& res)         
     {
         find_one_contact_req::FindOneContactRequest request;
         find_one_contact_resp::FindOneContactResponse response;
         try {
             // 反序列化 request
             if (!request.ParseFromString(req.body)) {
                 throw ContactException("Parse FindOneContactRequest error!");
             }
             // 查询联系⼈详细信息
             contactsServer.findOne(request, &response);
             // 序列化 response
             std::string response_str;
             if (!response.SerializeToString(&response_str)) {
                 throw ContactException("Serialize FindOneContactResponse error");
             }
             res.body = response_str;
             res.set_header("Content-Type", "application/protobuf");
             res.status = 200;
         } catch (ContactException &e) {
             cerr << "---> /contacts/find-one 发现异常!!!" << endl
             << "---> 异常信息:" << e.what() << endl;
             res.status = 500;
             base_response::BaseResponse* baseResponse = 
             response.mutable_base_resp();
             baseResponse->set_success(false);
             baseResponse->set_error_desc(e.what());
             std::string response_str;
             if (response.SerializeToString(&response_str)) {
                 res.body = response_str;
                 res.set_header("Content-Type", "application/protobuf");
             }
         }
     }); 

     srv.Get("/contacts/find-all", [contactsServer](const Request& req, Response& res) {
         find_all_contacts_resp::FindAllContactsResponse response;
         try {
             // 查询所有联系⼈
             contactsServer.findAll(&response);
             // 序列化 response
             std::string response_str;
             if (!response.SerializeToString(&response_str)) {
                 throw ContactException("Serialize FindAllContactsResponse error");
             }
             res.body = response_str;
             res.set_header("Content-Type", "application/protobuf");
             res.status = 200;
         } catch (ContactException &e) {
             cerr << "---> /contacts/find-all 发现异常!!!" << endl
             << "---> 异常信息:" << e.what() << endl;
             res.status = 500;
             base_response::BaseResponse* baseResponse = 
             response.mutable_base_resp();
             baseResponse->set_success(false);
             baseResponse->set_error_desc(e.what());
             std::string response_str;
             if (response.SerializeToString(&response_str)) {
                 res.body = response_str;
                 res.set_header("Content-Type", "application/protobuf");
             }
         }
     });

     srv.listen("0.0.0.0", 8123); 
}

ContactException.h:定义异常类

// ⾃定义异常类
class ContactException
{
private:
     std::string message;
public:
     ContactException(std::string str = "A problem") : message{str} {}
     std::string what() const { return message; }
};

ContactsServer.h:通讯录服务定义

using namespace httplib;
class ContactsServer {
public:
 ContactsMapper contactsMapper;
public:
 void add(add_contact_req::AddContactRequest& request,
 add_contact_resp::AddContactResponse* response) const;

 void del(del_contact_req::DelContactRequest& request,
 del_contact_resp::DelContactResponse* response) const;

 void findOne(find_one_contact_req::FindOneContactRequest request, 
 find_one_contact_resp::FindOneContactResponse* response) const;

 void findAll(find_all_contacts_resp::FindAllContactsResponse* rsp) const;
private:
 void printAddContactRequest(add_contact_req::AddContactRequest& request) const;

 void buildPeopleInfo(contacts::PeopleInfo* people, add_contact_req::AddContactRequest& request) const;

 void buildFindOneContactResponse(const contacts::PeopleInfo& people, 
 find_one_contact_resp::FindOneContactResponse* response) const;

 void buildFindAllContactsResponse(contacts::Contacts& contacts, 
 find_all_contacts_resp::FindAllContactsResponse* rsp) const;

};

ContactsServer.cc:通讯录服务实现

using std::cout;
using std::endl;
void ContactsServer::add(add_contact_req::AddContactRequest& request, 
 add_contact_resp::AddContactResponse* response) const
{
     // 打印⽇志
     printAddContactRequest(request); 
     // 先读取已存在的 contacts
     contacts::Contacts contacts;
     contactsMapper.selectContacts(&contacts);
     // 转换为存⼊⽂件的消息对象 
     google::protobuf::Map<std::string, contacts::PeopleInfo>* map_contacts = 
contacts.mutable_contacts();
     contacts::PeopleInfo people;
     buildPeopleInfo(&people, request); 
     map_contacts->insert({people.uid(), people}); 
     // 向磁盘⽂件写⼊新的 contacts
     contactsMapper.insertContacts(contacts);
     response->set_uid(people.uid());
     response->mutable_base_resp()->set_success(true);
    // 打印⽇志
     cout << "---> (ContactsServer::add) Success to write contacts." << endl;
}

void ContactsServer::del(del_contact_req::DelContactRequest& request,
 del_contact_resp::DelContactResponse* response) const
{
     // 打印⽇志
     cout << "---> (ContactsServer::del) DelContactRequest: uid: " << 
request.uid() << endl; 
     // 先读取已存在的 contacts
     contacts::Contacts contacts;
     contactsMapper.selectContacts(&contacts);
     // 不含uid直接返回
     if (contacts.contacts().find(request.uid()) == contacts.contacts().end()){ 
         cout << "---> (ContactsServer::del) not find uid: " << request.uid() 
<< endl; 
         response->set_uid(request.uid());
         response->mutable_base_resp()->set_success(false);
         response->mutable_base_resp()->set_error_desc("not find uid");
         return;
     }
     // 删除⽤⼾
     contacts.mutable_contacts()->erase(request.uid());
     // 向磁盘⽂件写⼊新的 contacts
     contactsMapper.insertContacts(contacts);
     // 构造resp
     response->set_uid(request.uid());
     response->mutable_base_resp()->set_success(true);
     // 打印⽇志
     cout << "---> (ContactsServer::del) Success to del contact, uid: " << 
     request.uid() << endl;
}

void ContactsServer::findOne(find_one_contact_req::FindOneContactRequest 
request, find_one_contact_resp::FindOneContactResponse* response) const {
     // 打印⽇志
     cout << "---> (ContactsServer::findOne) FindOneContactRequest: uid: " << 
     request.uid() << endl; 
     // 获取通讯录
     contacts::Contacts contacts;
     contactsMapper.selectContacts(&contacts);
     // 转换resp消息对象
     const google::protobuf::Map<std::string, contacts::PeopleInfo>& map_contacts = contacts.contacts();
     auto it = map_contacts.find(request.uid());
     // 查找的联系⼈不存在
     if (it == map_contacts.end()) {
         cout << "---> (ContactsServer::findOne) not find uid: " << 
request.uid() << endl;
         response->mutable_base_resp()->set_success(false);
         response->mutable_base_resp()->set_error_desc("uid not exist");
         return;
     }
     // 构建resp
     buildFindOneContactResponse(it->second, response);
     // 打印⽇志
     cout << "---> (ContactsServer::findOne) find uid: " << request.uid() << endl;
}


void ContactsServer::findAll(find_all_contacts_resp::FindAllContactsResponse* 
rsp) const {
     // 打印⽇志
     cout << "---> (ContactsServer::findAll) " << endl; 
     // 获取通讯录
     contacts::Contacts contacts;
     contactsMapper.selectContacts(&contacts);
     // 转换resp消息对象
     buildFindAllContactsResponse(contacts, rsp);
}

void ContactsServer::buildFindAllContactsResponse(contacts::Contacts& contacts, 
find_all_contacts_resp::FindAllContactsResponse* rsp) const {
     if (nullptr == rsp) {
         return;
     }
     rsp->mutable_base_resp()->set_success(true);

     for (auto it = contacts.contacts().cbegin(); it != 
contacts.contacts().cend(); ++it) {
         find_all_contacts_resp::PeopleInfo* people = rsp->add_contacts();
         people->set_uid(it->first);
         people->set_name(it->second.name());
     }
}


void ContactsServer::buildFindOneContactResponse(const contacts::PeopleInfo& people, 
 find_one_contact_resp::FindOneContactResponse* response) const {
     if (nullptr == response) {
         return;
     }
     response->mutable_base_resp()->set_success(true);
     response->set_uid(people.uid());
     response->set_name(people.name());
     response->set_age(people.age());
     for (auto& phone : people.phone()) {
         find_one_contact_resp::FindOneContactResponse_Phone* resp_phone = 
         response->add_phone();
         resp_phone->set_number(phone.number());
         switch (phone.type()) {
             case contacts::PeopleInfo_Phone_PhoneType::PeopleInfo_Phone_PhoneType_MP:
                 resp_phone->set_type(find_one_contact_resp::FindOneContactResponse_Phone_PhoneType::FindOneContactResponse_Phone_PhoneType_MP);
                 break;
             case contacts::PeopleInfo_Phone_PhoneType::PeopleInfo_Phone_PhoneType_TEL:
                 resp_phone->set_type(find_one_contact_resp::FindOneContactResponse_
Phone_PhoneType::FindOneContactResponse_Phone_PhoneType_TEL);
                 break;
             default:
                 break;
         }
     }
     Utils::map_copy(response->mutable_remark(), people.remark());
}


void ContactsServer::printAddContactRequest(add_contact_req::AddContactRequest& 
request) const {
     cout << "---> (ContactsServer::add) AddContactRequest:" << endl;
     cout << "姓名:" << request.name() << endl;
     cout << "年龄:" << request.age() << endl;
     for (auto& phone : request.phone()) {
         int j = 1;
         cout << "电话" << j++ << ": " << phone.number();
         cout << " (" << phone.PhoneType_Name(phone.type()) << ")" << endl;
     }
     if (request.remark_size()) {
         cout << "备注信息: " << endl;
     }
     for (auto it = request.remark().cbegin(); it != request.remark().cend(); 
++it) { 
         cout << " " << it->first << ": " << it->second << endl; 
     } 
}


void ContactsServer::buildPeopleInfo(contacts::PeopleInfo* people, 
add_contact_req::AddContactRequest& request) const {
     std::string uid = Utils::generate_hex(10);
     people->set_uid(uid);
     people->set_name(request.name());
     people->set_age(request.age());
     for (auto& phone : request.phone()) {
         contacts::PeopleInfo_Phone* peo_phone = people->add_phone();
         peo_phone->set_number(phone.number());
         switch (phone.type()) {
             case add_contact_req::AddContactRequest_Phone_PhoneType::AddContactRequest_
Phone_PhoneType_MP:
                 peo_phone->set_type(contacts::PeopleInfo_Phone_PhoneType::PeopleInfo_
Phone_PhoneType_MP);
                 break;
             case add_contact_req::AddContactRequest_Phone_PhoneType::AddContactRequest_
Phone_PhoneType_TEL:
                 peo_phone->set_type(contacts::PeopleInfo_Phone_PhoneType::PeopleInfo_
Phone_PhoneType_TEL);
                 break;
             default:
                 break;
         }
     }
     Utils::map_copy(people->mutable_remark(), request.remark());
}


Utils.h:定义工具类

#include <sstream>
#include <random>
#include <google/protobuf/map.h>
class Utils
{
public:
     static unsigned int random_char() {
         // ⽤于随机数引擎获得随机种⼦
         std::random_device rd; 
         // mt19937是c++11新特性,它是⼀种随机数算法,⽤法与rand()函数类似,但是mt19937具有速度快,周期⻓的特点
         // 作⽤是⽣成伪随机数
         std::mt19937 gen(rd()); 
         // 随机⽣成⼀个整数i 范围[0, 255]
         std::uniform_int_distribution<> dis(0, 255);
         return dis(gen);
     }

     // ⽣成 UUID (通⽤唯⼀标识符)
     static std::string generate_hex(const unsigned int len) {
         std::stringstream ss;
         // ⽣成 len 个16进制随机数,将其拼接⽽成
         for (auto i = 0; i < len; i++) {
             const auto rc = random_char();
             std::stringstream hexstream;
             hexstream << std::hex << rc;
             auto hex = hexstream.str();
             ss << (hex.length() < 2 ? '0' + hex : hex);
         }
         return ss.str();
     }

     static void map_copy(google::protobuf::Map<std::string, std::string>* 
target, const google::protobuf::Map<std::string, std::string>& source) {
         if (nullptr == target) {
             std::cout << "map_copy warning, target is nullptr!" << std::endl;
             return;
         }
         for (auto it = source.cbegin(); it != source.cend(); ++it) { 
             target->insert({it->first, it->second});
         } 
     }
};

ContactsMapper.h:持久化存储通讯录⽅法定义

class ContactsMapper {
public:
     void selectContacts(contacts::Contacts* contacts) const;
     void insertContacts(contacts::Contacts& contacts) const;
};

ContactsMapper.cc:持久化存储通讯录⽅法实现

注:本应该存⼊数据库中,在这⾥为了简化流程,将通讯录存⼊本地⽂件

#define TEXT_NAME "contacts.bin"

using std::ios;
using std::cout;
using std::endl;

// 本应该存⼊数据库中,在这⾥为了简化流程,将通讯录存⼊本地⽂件
void ContactsMapper::selectContacts(contacts::Contacts* contacts) const{
     std::fstream input(TEXT_NAME, ios::in | ios::binary);
     if (!input) {
         cout << "---> (ContactsMapper::selectContacts) " << TEXT_NAME << ": 
File not found. Creating a new file." << endl;
     }
     else if (!contacts->ParseFromIstream(&input)) {
         input.close();
         throw ContactException("(ContactsMapper::selectContacts) Failed to parse contacts.");
     }
     input.close();
}


void ContactsMapper::insertContacts(contacts::Contacts& contacts) const {
     std::fstream output(TEXT_NAME, ios::out | ios::trunc | ios::binary);
     if (!contacts.SerializeToOstream(&output)) {
         output.close();
         throw ContactException("(ContactsMapper::insertContacts) Failed to 
write contacts.");
     }
     output.close();
}

六、总结

6.1、序列化能⼒对⽐验证

在这⾥分别使⽤ PB 与 JSON 的序列化与反序列化能⼒, 对值完全相同的⼀份结构化数据进⾏不同次数的性能测试。

为了可读性,下⾯这⼀份⽂本使⽤ JSON 格式展⽰了需要被进⾏测试的结构化数据内容:

{
     "age" : 20,
     "name" : "张珊",
     "phone" : 
     [
         {
             "number" : "110112119",
             "type" : 0
         },
         {
             "number" : "110112119",
             "type" : 0
         },
         {
             "number" : "110112119",
             "type" : 0
         },
         {
             "number" : "110112119",
             "type" : 0
         },
         {
             "number" : "110112119",
             "type" : 0
         }
     ],
     "qq" : "95991122",
     "address" : 
     {
         "home_address" : "陕西省西安市⻓安区",
         "unit_address" : "陕西省西安市雁塔区"
     },
     "remark" : 
     {
         "key1" : "value1",
         "key2" : "value2",
         "key3" : "value3",
         "key4" : "value4",
         "key5" : "value5"
     }
}

开始进⾏测试代码编写,在新的⽬录下新建 contacts.proto⽂件,内容如下:

syntax = "proto3";
package compare_serialization;

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; // 备注
}

使⽤ protoc 命令编译⽂件后,新建性能测试⽂件 compare.cc,分别对相同的结构化数据进⾏ 100 、 1000 、 10000 、 100000 次的序列化与反序列化,分别获取其耗时与序列化后的⼤⼩。 内容如下:

#include <iostream>
#include <sys/time.h>
#include <jsoncpp/json/json.h>
#include "contacts.pb.h"
using namespace std; 
using namespace compare_serialization;
using namespace google::protobuf;
#define TEST_COUNT 100
void createPeopleInfoFromPb(PeopleInfo *people_info_ptr);

void createPeopleInfoFromJson(Json::Value& root);

int main(int argc, char *argv[])
{
     struct timeval t_start,t_end;
     double time_used;
     int count;
     string pb_str, json_str;
 // ------------------------------Protobuf 序列化------------------------------
------
     {
         PeopleInfo pb_people;
         createPeopleInfoFromPb(&pb_people);
         count = TEST_COUNT;
         gettimeofday(&t_start, NULL);
         // 序列化count次
         while ((count--) > 0) {
             pb_people.SerializeToString(&pb_str);
         }
         gettimeofday(&t_end, NULL);
         time_used=1000000*(t_end.tv_sec - t_start.tv_sec) + t_end.tv_usec - 
t_start.tv_usec;
         cout << TEST_COUNT << "次 [pb序列化]耗时:" << time_used/1000 << "ms."
 << " 序列化后的⼤⼩:" << pb_str.length() << endl;
     }
 
 // ------------------------------Protobuf 反序列化----------------------------
--------
     {
         PeopleInfo pb_people;
         count = TEST_COUNT;
         gettimeofday(&t_start, NULL);
         // 反序列化count次
         while ((count--) > 0) {
             pb_people.ParseFromString(pb_str);
         }
         gettimeofday(&t_end, NULL);
         time_used=1000000*(t_end.tv_sec - t_start.tv_sec) + t_end.tv_usec - 
t_start.tv_usec;
         cout << TEST_COUNT << "次 [pb反序列化]耗时:" << time_used / 1000 << "ms."
<< endl;
     }
 // ------------------------------JSON 序列化----------------------------------
--
     {
         Json::Value json_people; 
         createPeopleInfoFromJson(json_people); 
         Json::StreamWriterBuilder builder; 
         count = TEST_COUNT;
         gettimeofday(&t_start, NULL);
         // 序列化count次
         while ((count--) > 0) {
             json_str = Json::writeString(builder, json_people); 
         }
         gettimeofday(&t_end, NULL);
         // 打印序列化结果
         // cout << "json: " << endl << json_str << endl;
         time_used=1000000*(t_end.tv_sec - t_start.tv_sec) + t_end.tv_usec - 
t_start.tv_usec;
         cout << TEST_COUNT << "次 [json序列化]耗时:" << time_used/1000 << "ms."
 << " 序列化后的⼤⼩:" << json_str.length() << endl;
 
     }
 // ------------------------------JSON 反序列化--------------------------------
----
     {
         Json::CharReaderBuilder builder;
         unique_ptr<Json::CharReader> reader(builder.newCharReader());
         Json::Value json_people;
         count = TEST_COUNT;
         gettimeofday(&t_start, NULL);
         // 反序列化count次
         while ((count--) > 0) {
             reader->parse(json_str.c_str(), json_str.c_str() + json_str.length(), 
&json_people, nullptr);
         }
         gettimeofday(&t_end, NULL);
         time_used=1000000*(t_end.tv_sec - t_start.tv_sec) + t_end.tv_usec - 
t_start.tv_usec;
         cout << TEST_COUNT << "次 [json反序列化]耗时:" << time_used/1000 << "ms."
<< endl; 
     }
     return 0;
 }

/** 
 * 构造pb对象
 */
void createPeopleInfoFromPb(PeopleInfo *people_info_ptr)
{
     people_info_ptr->set_name("张珊");
     people_info_ptr->set_age(20);
     people_info_ptr->set_qq("95991122");
     for(int i = 0; i < 5; i++) {
         PeopleInfo_Phone* phone = people_info_ptr->add_phone();
         phone->set_number("110112119");
         phone->set_type(PeopleInfo_Phone_PhoneType::PeopleInfo_Phone_PhoneType_MP);
     }
     Address address;
     address.set_home_address("陕西省西安市⻓安区");
     address.set_unit_address("陕西省西安市雁塔区");
     google::protobuf::Any * data = people_info_ptr->mutable_data();
     data->PackFrom(address);
 
     people_info_ptr->mutable_remark()->insert({"key1", "value1"}); 
     people_info_ptr->mutable_remark()->insert({"key2", "value2"}); 
     people_info_ptr->mutable_remark()->insert({"key3", "value3"}); 
     people_info_ptr->mutable_remark()->insert({"key4", "value4"}); 
     people_info_ptr->mutable_remark()->insert({"key5", "value5"}); 
}


/** 
 * 构造json对象
 */
void createPeopleInfoFromJson(Json::Value& root) {
     root["name"] = "张珊"; 
     root["age"] = 20; 
     root["qq"] = "95991122"; 
 
     for(int i = 0; i < 5; i++) {
         Json::Value phone;
         phone["number"] = "110112119";
         phone["type"] = 0;
         root["phone"].append(phone);
     }
     Json::Value address;
     address["home_address"] = "陕西省西安市⻓安区";
     address["unit_address"] = "陕西省西安市雁塔区";
     root["address"] = address;
     Json::Value remark;
     remark["key1"] = "value1";
     remark["key2"] = "value2";
     remark["key3"] = "value3";
     remark["key4"] = "value4";
     remark["key5"] = "value5";
     root["remark"] = remark;
}

Makefile

compare:compare.cc contacts.pb.cc
 g++ -o $@ $^ -std=c++11 -lprotobuf -ljsoncpp

.PHONY:clean
clean:
 rm -f compare

测试结果如下:

100次 [pb序列化]耗时:0.342ms. 序列化后的⼤⼩:278
100次 [pb反序列化]耗时:0.435ms.
100次 [json序列化]耗时:1.306ms. 序列化后的⼤⼩:567
100次 [json反序列化]耗时:0.926ms.

1000次 [pb序列化]耗时:3.59ms. 序列化后的⼤⼩:278
1000次 [pb反序列化]耗时:5.069ms.
1000次 [json序列化]耗时:11.582ms. 序列化后的⼤⼩:567
1000次 [json反序列化]耗时:9.289ms.

10000次 [pb序列化]耗时:34.386ms. 序列化后的⼤⼩:278
10000次 [pb反序列化]耗时:45.96ms.
10000次 [json序列化]耗时:115.76ms. 序列化后的⼤⼩:567
10000次 [json反序列化]耗时:91.046ms.

100000次 [pb序列化]耗时:349.937ms. 序列化后的⼤⼩:278
100000次 [pb反序列化]耗时:428.366ms.
100000次 [json序列化]耗时:1150.54ms. 序列化后的⼤⼩:567
100000次 [json反序列化]耗时:904.58ms.时:5.069ms.

由实验结果可得:

  • 编解码性能:ProtoBuf 的编码解码性能,⽐ JSON ⾼出 2-4 倍。
  • 内存占⽤:ProtoBuf 的内存278,⽽JSON到达567,ProtoBuf的内存占⽤只有JSON的1/2。

注:以上结论的数据只是根据该项实验得出。因为受不同的字段类型、字段个数等影响,测出的数据 会有所差异。

该实验有很多可待优化的地⽅。但其实这种粗略的测试,也能看出来 ProtoBuf 的优势。

6.2、总结

序列化协议通⽤性格式可读性序列化⼤⼩序列化性能适⽤场景
JSON通⽤ (json、 xml已成为多种 ⾏业标准的编 写⼯具)⽂本格式轻量(使 ⽤键值对 ⽅式,压 缩了⼀定 的数据空 间)web项⽬。因为浏览 器对于json数据⽀持 ⾮常好,有很多内建 的函数⽀持。
XML通⽤⽂本格式重量(数 据冗余, 因为需要 成对的闭 合标签)XML 作为⼀种扩展标 记语⾔,衍⽣出了 HTML、RDF/RDFS, 它强调数据结构化的 能⼒和可读性。
ProtoBuf独⽴ (Protobuf只 是Google公司 内部的⼯具)⼆进制格式差(只能 反序列化 后得到真 正可读的 数据)轻量(⽐ JSON更轻 量,传输 起来带宽 和速度会 有优化)适合⾼性能,对响应 速度有要求的数据传 输场景。Protobuf⽐ XML、JSON 更⼩、 更快。

⼩结:

  1.  XML、JSON、ProtoBuf 都具有数据结构化数据序列化的能⼒。
  2. XML、JSON 更注重数据结构化,关注可读性和语义表达能⼒。ProtoBuf 更注重数据序列化,关注 效率、空间、速度,可读性差,语义表达能⼒不⾜,为保证极致的效率,会舍弃⼀部分元信息。
  3. ProtoBuf 的应⽤场景更为明确,XML、JSON 的应⽤场景更为丰富。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值