下面举两个例子:
socket
编程中发送与接收数据。Json
XML
ProtoBuf
从官方得到的解释是这样子的:
Protocol Buffers
是 Google
的⼀种语⾔⽆关、平台⽆关、可扩展的序列化结构数据的⽅法,它可⽤于(数据)通信协议、数据存储等。Protocol Buffers
类⽐于 XML
,是⼀种灵活,⾼效,⾃动化机制的结构数据序列化⽅法,但是⽐ XML
更⼩、更快、更为简单。简单来讲, ProtoBuf
是让结构数据序列化的方法,其具有以下特点:
ProtoBuf
⽀持 Java
、C++
、Python
等多种语⾔,⽀持多个平台。XML
更⼩、更快、更为简单。.proto
⽂件,⽬的是为了定义结构对象(message
)及属性内容。protoc
编译器编译 .proto
⽂件,⽣成⼀系列接⼝代码,存放在新⽣成的头⽂件和源⽂件中。.proto
⽂件中定义的字段进⾏设置和获取,和对 message
对象进⾏序列化和反序列化。 简单的说:ProtoBuf
是需要依赖通过编译生成的头文件和源文件来使用的。有了这种代码生成机制,开发人员再也不用吭哧吭哧地编写那些协议解析的代码了!
在快速上手中,会编写第一版本的通讯录 1.0
。在通讯录 1.0
版本中,将实现:
PB
进⾏序列化,并将结果打印出来。PB
进⾏反序列,解析出联系⼈信息并打印出来。通过通讯录 1.0
,我们便能了解使⽤ ProtoBuf
初步要掌握的内容,以及体验到 ProtoBuf
的完整使用流程。
.proto
文件 下面先给出通讯录 1.0
中 contacts.proto
文件的内容:
// 非注释的首行:声明proto的语法版本
syntax = "proto3";
// 指定对应的包名,防止冲突
package contacts;
// 定义联系人消息(下面的等号其实不是赋值,而是指定标记,实现protobuf的高效率特点)
message PeopleInfo
{
string name = 1;
int32 age = 2;
}
下面我们来解释一下上述内容!
.proto
⽂件时,文件命名应该使用全小写字母命名,多个字⺟之间⽤ _
连接。 例如:lower_snake_case.proto
。.proto
⽂件代码时,应使⽤ 两个空格的缩进,而不是一个 TAB
的缩进!//
或者 /* ... */
。proto3
语法 Protocol Buffers
语言版本3,简称 proto3
,是 .proto
文件最新的语法版本。proto3
简化了 Protocol Buffers
语言,既易于使用,又可以在更广泛的编程语言中使用。
在 .proto
文件中,要 使用 syntax = "proto3";
来指定文件语法为 proto3
,并且 必须写在除去注释内容的第一行。 如果没有指定,编译器会使用 proto2
语法。内容如下:
// 非注释的首行:声明proto的语法版本
syntax = "proto3";
package
声明符 package
声明 的作用是为了指定生成的代码的命名空间。这在 防止消息类型之间的命名冲突 是非常重要的。考虑到 Protocol Buffers
可以用于定义通信协议,而在这些协议中可能会定义相同的消息类型名称,因此必须有一种机制来确保不同的消息类型之间不会发生命名冲突。
通过指定 package
,你可以为消息类型指定一个命名空间。这意味着不同的消息类型可以在不同的命名空间中定义相同的名称而不会发生冲突,因为它们在不同的包中。
例如,假设有两个文件 A.proto
和 B.proto
,它们都定义了一个名为 Message
的消息类型。如果这两个文件没有使用 package
声明来指定命名空间,那么生成的代码可能会因为命名冲突而出现问题。但是,如果它们分别使用了不同的 package
声明,比如 package A
和 package B
,那么生成的代码会将它们放置在不同的命名空间中,从而避免了冲突。
所以我们在通讯录 1.0
版本中可以指定以下命名空间:
// 指定对应的包名,防止冲突
package contacts;
消息(message
): 表示要定义的结构化对象,我们可以给这个结构化对象中定义其对应的属性内容。
这⾥再提⼀下为什么要定义消息?在⽹络传输中,我们需要为传输双⽅定制协议。定制协议说⽩了就是定义结构体或者结构化数据,⽐如
tcp
、udp
报⽂就是结构化的。 再⽐如将数据持久化存储到数据库时,会将⼀系列元数据统⼀⽤对象组织起来,再进⾏存储。 所以ProtoBuf
就是以message
的⽅式来⽀持我们定制协议字段,后期帮助我们形成类和⽅法来使⽤。
在通讯录 1.0
中我们就需要为“联系人”定义⼀个 message
。
message 消息类型名 {
消息类型字段
}
消息类型命名规范:使用驼峰命名法,首字母大写。如下所示:
// 定义联系人消息
message PeopleInfo {
}
此外,一个 .proto
文件中是可以有多个 message
类的,并且 每个 message
类中的字段唯一编号是可以相同,因为这是互不影响的!
在 message
中我们可以定义其属性字段,字段定义格式为:字段类型 字段名 = 字段唯一编号;
_
连接。 该表格展示了定义于消息体中的标量数据类型,以及编译 .proto
文件之后自动生成的类中与之对应的字段类型。在这里展示了与 C++
语言对应的类型:(下面提到的变长编码,是指经过 protobuf
编码后,原本 4
字节或 8
字节的数可能会被变为其他字节数)
.proto 类型 | 强调内容 | C++ 类型 |
---|---|---|
double | double | |
float | float | |
int32 | 使用变长编码。负数的编码效率低,若字段可能为负值,应使用 sint32 代替 | int32 |
int64 | 使用变长编码。负数的编码效率低,若字段可能为负值,应使用 sint64 代替 | int64 |
uint32 | 使用变长编码 | uint32 |
uint64 | 使用变长编码 | uint64 |
sint32 | 使用变长编码。符号整型。负值的编码效率高于常规的 int32 类型 | int32 |
sint64 | 使用变长编码。符号整型。负值的编码效率高于常规的 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 |
更新 contacts.proto
文件,新增姓名、年龄字段:
// 定义联系人消息(下面的等号其实不是赋值,而是指定标记,实现protobuf的高效率特点)
message PeopleInfo
{
string name = 1;
int32 age = 2;
}
在这里还要特别讲解⼀下字段唯一编号的范围:1 ~ 2^29-1
,其中 19000 ~ 19999
是不可用的。(19000 ~ 19999
不可用是因为:在 Protobuf
协议的实现中,对这些数进行了预留。如果非要在 .proto
文件中使用这些预留标识号,例如将 name
字段的编号设置为 19000
,编译时就会报警)
还值得一提的是,范围为 1 ~ 15
的字段编号需要一个字节进行编码, 16 ~ 2047
内的数字需要两个字节进行编码。编码后的字节不仅只包含了编号,还包含了字段类型。所以 1 ~ 15
要用来标记出现非常频繁的字段,要为将来有可能添加的、频繁出现的字段预留一些出来。
contacts.proto
⽂件,⽣成 C++
⽂件 编译命令的格式如下所示:
protoc [--proto_path=IMPORT_PATH] --cpp_out=OUT_DIR path/to/file.proto
Protocol Buffer
提供的命令⾏编译⼯具。.proto
⽂件所在⽬录,可多次指定。可简写成 -I IMPORT_PATH
。如不指定该参数,则在当前⽬录进⾏搜索。当某个 .proto
⽂件 import
其他 .proto
⽂件时,或需要编译的 .proto
⽂件不在当前⽬录下,这时就要⽤ -I
来指定搜索⽬录。C++
⽂件。path/to/file.proto
要编译的 .proto
⽂件。 编译 contacts.proto
文件命令如下:
protoc --cpp_out=./contacts.proto
.proto
文件后发生什么 编译 contacts.proto
文件后,会生成所选择语言的代码,我们选择的是 C++
,所以编译后生成了两个文件:contacts.pb.h
文件以及 contacts.pb.cc
文件。
它们包含了以下内容 :
.proto
⽂件⽣成 .h
和 .cc
⽂件,分别⽤来存放类的声明与类的实现。message
,都会⽣成⼀个对应的消息类。set
、get
函数等。 下面是 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);
};
上述的例子中:(contacts.pb.cc
中的代码就是对类声明方法的一些实现,在这里就不展开了)
getter
的名称与⼩写字段完全相同,setter
⽅法以 set_
开头。clear_
⽅法,可以将字段重新设置回 empty
状态。 到这里有同学可能就有疑惑了,那之前提到的序列化和反序列化方法在哪里呢?其实是 在消息类的父类 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
可以参⻅完整列表。创建一个测试文件 main.cc
,方法中我们实现:
PB
进⾏序列化,并将结果打印出来。PB
进⾏反序列,解析出联系⼈信息并打印出来。#include <iostream>
#include <string>
#include "contacts.pb.h"
int main()
{
std::string people_str;
{
// 对⼀个联系⼈的信息使⽤ PB 进行序列化,并将结果打印出来。
contacts::PeopleInfo people;
people.set_age(20);
people.set_name("lirendada杨桐");
if(!people.SerializeToString(&people_str))
{
std::cerr << "序列化联系人失败" << std::endl;
return -1;
}
std::cout << "序列化联系人成功,结果为:" << people_str << std::endl;
}
std::cout << "----------------------------------------" << std::endl;
{
// 对序列化后的内容使⽤ PB 进⾏反序列,解析出联系⼈信息并打印出来。
contacts::PeopleInfo people;
if(!people.ParseFromString(people_str))
{
std::cerr << "反序列化联系人失败" << std::endl;
return -1;
}
std::cout << "反序列化联系人成功,结果为:" << std::endl
<< "姓名:" << people.name() << std::endl
<< "年龄:" << people.age() << std::endl;
}
return 0;
}
代码书写完成后,编译 main.cc
和 contacts.pb.cc
,生成可执行程序 main
:
g++ -o main main.cc contacts.pb.cc -std=c++11 -lprotobuf
注意事项:
-lprotobuf
:必加,不然会有链接错误。-std=c++11
:必加,需要用到 c++11
的语法。 最后看看运行结果:
// 运行结果:
序列化联系人成功,结果为:
lirendada杨桐
----------------------------------------
反序列化联系人成功,结果为:
姓名:lirendada杨桐
年龄:20
由于 ProtoBuf
是把联系人对象序列化成了二进制序列,这里用 string
来作为接收二进制序列的容器。所以在终端打印的时候会有换行等一些乱码显示。
所以相对于 xml
和 JSON
来说,因为 ProtoBuf
被编码成二进制,破解成本增大,ProtoBuf
编码是相对安全的。