初识 ProtoBuf
目录
- 1. ProtoBuf 概述
- 2. 安装 ProtoBuf
- 3. ProtoBuf 快速上手
- 3.1 步骤1:创建 .proto 文件
- 3.2 步骤2:编译 contacts.proto 文件,生成 C++ 文件
- 3.3 步骤3:序列化与反序列化的使用
- 4. ProtoBuf 总结
1. ProtoBuf 概述
(1)序列化概念:
- 序列化和反序列化:
- 序列化:把对象转换为字节序列的过程 称为对象的序列化。
- 反序列化:把字节序列恢复为对象的过程 称为对象的反序列化。
- 什么情况下需要序列化:
- 存储数据:当你想把的内存中的对象状态保存到⼀个文件中或者存到数据库中时。
- 网络传输:网络直接传输数据,但是无法直接传输对象,所以要在传输前序列化,传输完成后反序列化成对象。例如我们之前学习过 socket 编程中发送与接收数据。
- 如何实现序列化:xml、json、 protobuf。
(2)ProtoBuf 是什么:
-
我们先来看看官方给出的答案是什么:
- Protocol buffers are Google’s language-neutral, platform-neutral, extensible mechanism for serializing structured data ‒ think XML, but smaller, faster, and simpler.
- You define how you want your data to be structured once, then you can use special generated source code to easily write and read your structured data to and from a variety of data streams and using a variety of languages.
-
翻译过来的意思就是:
- Protocol Buffers 是 Google 的⼀种语言无关、平台无关、可扩展的序列化结构数据的方法,它可用于(数据)通信协议、数据存储等。
- Protocol Buffers 类比于 XML,是⼀种灵活,高效,自动化机制的结构数据序列化方法,但是比 XML 更小、更快、更为简单。
- 你可以定义数据的结构,然后使用特殊生成的源代码轻松的在各种数据流中使用各种语言进行编写和读取结构数据。你甚至可以更新数据结构,而不破坏由旧数据结构编译的已部署程序。
-
简单来讲, ProtoBuf(全称为 Protocol Buffer)是让结构数据序列化的方法,其具有以下特点:
- 语言无关、平台无关:即 ProtoBuf 支持 Java、C++、Python 等多种语言,支持多个平台。
- 高效:即比 XML 更小、更快、更为简单。
- 扩展性、兼容性好:你可以更新数据结构,而不影响和破坏原有的旧程序。
(3)ProtoBuf 的使用特点:
- 具体逻辑:
- 编写 .proto 文件,目的是为了定义结构对象(message)及属性内容。
- 使用 protoc 编译器编译 .proto 文件,生成⼀系列接口代码,存放在新生成头文件和源文件中。
- 依赖生成的接口,将编译生成的头文件包含进我们的代码中,实现对 .proto 文件中定义的字段进行设置和获取,和对 message 对象进行序列化和反序列化。
- 总的来说:ProtoBuf 是需要依赖通过编译生成的头文件和源文件来使用的。有了这种代码生成机制,开发人员再也不用吭哧吭哧地编写那些协议解析的代码了(伽马这种活是典型的吃力不讨好)。
(4)总结:
2. 安装 ProtoBuf
(1)安装的过程见博客:https://blog.csdn.net/m0_65558082/article/details/148235303?spm=1001.2014.3001.5502
(2)学习思路:
- 对 ProtoBuf 的完整学习,将使用 项目推进 的方式来学习:即对于 ProtoBuf 知识内容的展开,会对一个项目进行一个版本一个版本的升级去学习 ProtoBuf 对应的知识点。
- 后面将会实现一个通讯录项目。对通讯录大家应该都不陌生,一般,通讯录中包含了一批的联系人,每个联系人又会有很多的属性,例如姓名、电话等等。
随着对通讯录项目的升级,我们对 ProtoBuf 的学习与使用就越深入。
3. ProtoBuf 快速上手
(1)在快速上手中,会编写第⼀版本的通讯录 1.0。在通讯录 1.0 版本中,将实现:
- 对⼀个联系人的信息使用 PB 进行序列化,并将结果打印出来。
- 对序列化后的内容使用 PB 进行反序列,解析出联系人信息并打印出来。
- 联系人包含以下信息: 姓名、年龄。
- 通过通讯录 1.0,我们便能了解使用 ProtoBuf 初步要掌握的内容,以及体验到 ProtoBuf 的完整使用流程。
3.1 步骤1:创建 .proto 文件
(1)文件规范:
- 创建 .proto 文件时,文件命名应该使用全小写字母命名,多个字母之间用 _ 连接。 例如:
lower_snake_case.proto
- 书写 .proto 文件代码时,应使用 2 个空格的缩进。
- 我们为通讯录 1.0 新建文件: contacts.proto。
(2)添加注释:
- 向文件添加注释,可使用 // 或者 /* … */ 。
(3)指定 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";
(4)package 声明符:
- package 是⼀个可选的声明符,能表示 .proto 文件的命名空间,在项目中要有唯一性。它的作用是为了避免我们定义的消息出现冲突。
- 在通讯录 1.0 的 contacts.proto 文件中,可以声明其命名空间,内容如下:
syntax = "proto3";
package contacts;
(5)定义消息(message):
- 消息(message):要定义的结构化对象,我们可以给这个结构化对象中定义其对应的属性内容。
- 这里再提⼀下为什么要定义消息?
- 在网络传输中,我们需要为传输双方定制协议。定制协议说白了就是定义结构体或者结构化数据,比如,tcp,udp 报文就是结构化的。
- 再比如将数据持久化存储到数据库时,会将⼀系列元数据统⼀用对象组织起来,再进行存储。
- 所以 ProtoBuf 就是以 message 的方式来支持我们定制协议字段,后期帮助我们形成类和方法来使用。在通讯录 1.0 中我们就需要为 联系人 定义⼀个 message。
- .proto 文件中定义⼀个消息类型的格式为:
message 消息类型名{}// 消息类型命名规范:使⽤驼峰命名法,⾸字⺟⼤写。
- 为 contacts.proto(通讯录 1.0)新增联系人message,内容如下:
syntax = "proto3";
package contacts;// 定义联系⼈消息
message PeopleInfo {}
(6)定义消息字段:
- 在 message 中我们可以定义其属性字段,字段定义格式为:字段类型 字段名 = 字段唯⼀编号;
- 字段名称命名规范:全小写母,多个字母之间用 _ 连接。
- 字段类型分为:标量数据类型 和 特殊类型(包括枚举、其他消息类型等)。
- 字段唯一编号:用来标识字段,一旦开始使用就不能够再改变。
- 该表格展示了定义于消息体中的标量数据类型,以及编译 .proto 文件之后自动生成的类中与之对应的字段类型。在这里展示了与 C++ 语言对应的类型。
.proto Type | Notes | C++ Type |
---|---|---|
double | double | |
float | float | |
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 |
sfixed64 | 定长 8 字节。 | int64 |
bool | bool | |
string | 包含 UTF-8 和 ASCII 编码的字符串,长度不能超过 2^32 。 | string |
bytes | 可包含任意的字节序列但长度不能超过 2^32 。 | string |
- [1] 变长编码是指:经过protobuf 编码后,原本4字节或8字节的数可能会被变为其他字节数。
(7)更新 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 步骤2:编译 contacts.proto 文件,生成 C++ 文件
(1)编译命令行格式为:
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文件。
(2)编译 contacts.proto 文件命令如下:
protoc --cpp_out=. contacts.proto
(3)编译 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 中的代码就是对类声明方法的⼀些实现,在这里就不展开了。
(4)到这里可能就有疑惑了,那之前提到的序列化和反序列化方法在哪里呢?
- 在消息类的父类 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);
}
(5)注意:
- 序列化的结果为二进制字节序列,而非文本格式。
- 以上三种序列化的方法没有本质上的区别,只是序列化后输出的格式不同,可以供不同的应用场景使用。
- 序列化的 API 函数均为const成员函数,因为序列化不会改变类对象的内容, 而是将序列化的结果保存到函数⼊参指定的地址中。
- 详细 message API 可以参见 完整列表。
3.3 步骤3:序列化与反序列化的使用
(1)创建一个测试文件 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;}
}
(2)代码书写完成后,编译 main.cc,生成可执行程序 TestProtoBuf :
g++ main.cc contacts.pb.cc -o TestProtoBuf -std=c++11 -lprotobuf
- -lprotobuf:必加,不然会有链接错误。
- -std=c++11:必加,使用C++11语法。
(3)执行 TestProtoBuf ,可以看见 people 经过序列化和反序列化后的结果:
[xiaomaker@xiaomaker-virtual-machine:protobuf]$ ./TestProtoBuf
序列化后的 people_str:
张珊
Parse age: 20
Parse name: 张珊
- 由于 ProtoBuf 是把联系⼈对象序列化成了二进制序列,这里用 string 来作为接收二进制序列的容器。所以在终端打印的时候会有换行等⼀些乱码显示。
- 所以相对于 xml 和 JSON 来说,因为被编码成⼆进制,破解成本增大,ProtoBuf 编码是相对安全的。
4. ProtoBuf 总结
(1)小结 ProtoBuf 使用流程:
- 编写 .proto 文件,目的是为了定义结构对象(message)及属性内容。
- 使用 protoc 编译器编译 .proto 文件,生成一系列接口代码,存放在新生成头文件和源文件中。
- 依赖生成的接口,将编译生成的头文件包含进我们的代码中,实现对 .proto 文件中定义的字段进行设置和获取,和对 message 对象进行序列化和反序列化。
(2)总的来说:
- ProtoBuf 是需要依赖通过编译生成的头文件和源文件来使用的。有了这种代码生成机制,开发人员再也不用吭哧吭哧地编写那些协议解析的代码了(干这种活是典型的吃力不讨好)。