或许您和我一样,在第一次看完这些先容后还是不明白 Protobuf 究竟是什么,那么我想一个大略的例子该当比较有助于理解它。

2、一个大略的例子

2.1安装 Google Protocol Buffer

在网站 https://github.com/52im/protobuf 上可以下载 Protobuf 的源代码
然后解压编译安装便可以利用它了。
安装步骤如下所示:

protobufphpwindowsProtobuf通讯协定详解代码演示具体道理介绍等 Python

tar -xzf protobuf-2.1.0.tar.gzcd protobuf-2.1.0./configure --prefix=$INSTALL_DIRmakemake checkmake install

2.2关于大略例子的描述

我打算利用 Protobuf 和 C++ 开拓一个十分大略的例子程序。
该程序由两部分组成。
第一部分被称为 Writer,第二部分叫做 Reader。
Writer 卖力将一些构造化的数据写入一个磁盘文件,Reader 则卖力从该磁盘文件中读取构造化数据并打印到屏幕上。
准备用于演示的构造化数据是 HelloWorld,它包含两个基本数据:

ID,为一个整数类型的数据Str,这是一个字符串

2.3书写 .proto 文件

首先我们须要编写一个 proto 文件,定义我们程序中须要处理的构造化数据,在 protobuf 的术语中,构造化数据被称为 Message。
proto 文件非常类似 java 或者 C 措辞的数据定义。
代码清单 1 显示了例子运用中的 proto 文件内容。
清单 1. proto 文件:

package lm;message helloworld{required int32 id = 1; // IDrequired string str = 2; // stroptional int32 opt = 3; //optional field}

一个比较好的习气是负责对待 proto 文件的文件名。
比如将命名规则定于如下:

packageName.MessageName.proto

在上例中,package 名字叫做 lm,定义了一个 helloworld,该有三个成员,类型为 int32 的 id,另一个为类型为 string 的成员 str。
opt 是一个可选的成员,即中可以不包含该成员。

2.4编译 .proto 文件

写好 proto 文件之后就可以用 Protobuf 编译器将该文件编译成目标措辞了。
本例中我们将利用 C++。
假设您的 proto 文件存放在 $SRC_DIR 下面,您也想把天生的文件放在同一个目录下,则可以利用如下命令:

protoc -I=$SRC_DIR --cpp_out=$DST_DIR $SRC_DIR/addressbook.proto

命令将天生两个文件:

lm.helloworld.pb.h , 定义了 C++ 类的头文件lm.helloworld.pb.cc , C++ 类的实现文件

在天生的头文件中,定义了一个 C++ 类 helloworld,后面的 Writer 和 Reader 将利用这个类来对进行操作。
诸如对的成员进行赋值,将序列化等等都有相应的方法。

2.5编写 writer 和 Reader

如前所述,Writer 将把一个构造化数据写入磁盘,以便其他人来读取。
如果我们不该用 Protobuf,实在也有许多的选择。
一个可能的方法是将数据转换为字符串,然后将字符串写入磁盘。
转换为字符串的方法可以利用 sprintf(),这非常大略。
数字 123 可以变成字符串”123”。
这样做彷佛没有什么欠妥,但是仔细考虑一下就会创造,这样的做法对写 Reader 的那个人的哀求比较高,Reader 的作者必须了 Writer 的细节。
比如”123”可以是单个数字 123,但也可以是三个数字 1,2 和 3,等等。
这么说来,我们还必须让 Writer 定义一种分隔符一样的字符,以便 Reader 可以精确读取。
但分隔符大概还会引起其他的什么问题。
末了我们创造一个大略的 Helloworld 也须要写许多处理格式的代码。
如果利用 Protobuf,那么这些细节就可以不须要运用程序来考虑了。
利用 Protobuf,Writer 的事情很大略,须要处理的构造化数据由 .proto 文件描述,经由上一节中的编译过程后,该数据化构造对应了一个 C++ 的类,并定义在 lm.helloworld.pb.h 中。
对付本例,类名为 lm::helloworld。
Writer 须要 include 该头文件,然后便可以利用这个类了。
现在,在 Writer 代码中,将要存入磁盘的构造化数据由一个 lm::helloworld 类的工具表示,它供应了一系列的 get/set 函数用来修正和读取构造化数据中的数据成员,或者叫 field。
当我们须要将该构造化数据保存到磁盘上时,类 lm::helloworld 已经供应相应的方法来把一个繁芜的数据变成一个字节序列,我们可以将这个字节序列写入磁盘。
对付想要读取这个数据的程序来说,也只须要利用类 lm::helloworld 的相应反序列化方法来将这个字节序列重新转换会构造化数据。
这同我们开始时那个“123”的想法类似,不过 Protobuf 想的远远比我们那个粗糙的字符串转换要全面,因此,我们不如放心将这类事情交给 Protobuf 吧。
程序清单 2 演示了 Writer 的紧张代码,您一定会以为很大略吧?清单 2. Writer 的紧张代码:

#include "lm.helloworld.pb.h"…int main(void){lm::helloworld msg1;msg1.set_id(101);msg1.set_str(“hello”);// Write the new address book back to disk.fstream output("./log", ios::out | ios::trunc | ios::binary);if (!msg1.SerializeToOstream(&output)) {cerr << "Failed to write msg." << endl;return -1;}return 0;}

Msg1 是一个 helloworld 类的工具,set_id() 用来设置 id 的值。
SerializeToOstream 将工具序列化后写入一个 fstream 流。
代码清单 3 列出了 reader 的紧张代码。
清单 3. Reader:

#include "lm.helloworld.pb.h"…void ListMsg(const lm::helloworld & msg) {cout << msg.id() << endl;cout << msg.str() << endl;}int main(int argc, char argv[]) {lm::helloworld msg1;{fstream input("./log", ios::in | ios::binary);if (!msg1.ParseFromIstream(&input)) {cerr << "Failed to parse address book." << endl;return -1;}}ListMsg(msg1);…}

同样,Reader 声明类 helloworld 的工具 msg1,然后利用 ParseFromIstream 从一个 fstream 流中读取信息并反序列化。
此后,ListMsg 中采取 get 方法读取消息的内部信息,并进行打印输出操作。

2.6运行结果

运行 Writer 和 Reader 的结果如下:

>writer>reader101Hello

Reader 读取文件 log 中的序列化信息并打印到屏幕上。
本文中所有的例子代码都可以在附件中下载。
您可以亲自体验一下。
这个例子本身并无意义,但只要您稍加修正就可以将它变成更加有用的程序。
比如将磁盘更换为网络 socket,那么就可以实现基于网络的数据交流任务。
而存储和交流正是 Protobuf 最有效的运用领域。

3、和其他类似技能的比较

3.1简述

看完这个大略的例子之后,希望您已经能理解 Protobuf 能做什么了,那么您可能会说,世上还有很多其他的类似技能啊,比如 XML,JSON,Thrift 等等。
和他们比较,Protobuf 有什么不同呢?大略说来 Protobuf 的紧张优点便是:大略,快。
这有测试为证,项目 thrift-protobuf-compare 比较了这些类似的技能,图 1 显示了该项目的一项测试结果,Total Time.图 1. 性能测试结果:

Total Time 指一个工具操作的全体韶光,包括创建工具,将工具序列化为内存中的字节序列,然后再反序列化的全体过程。
从测试结果可以看到 Protobuf 的成绩很好,感兴趣的读者可以自行到网站 https://github.com/eishay/jvm-serializers/wiki上理解更详细的测试结果。

3.2Protobuf 的优点

Protobuf 有如 XML,不过它更小、更快、也更大略。
你可以定义自己的数据构造,然后利用代码天生器天生的代码来读写这个数据构造。
你乃至可以在无需重新支配程序的情形下更新数据构造。
只需利用 Protobuf 对数据构造进行一次描述,即可利用各种不同措辞或从各种不同数据流中对你的构造化数据轻松读写。
它有一个非常棒的特性,即“向后”兼容性好,人们不必毁坏已支配的、依赖“老”数据格式的程序就可以对数据构造进行升级。
这样您的程序就可以不必担心由于构造的改变而造成的大规模的代码重构或者迁移的问题。
由于添加新的中的 field 并不会引起已经发布的程序的任何改变。
Protobuf 语义更清晰,无需类似 XML 解析器的东西(由于 Protobuf 编译器会将 .proto 文件编译天生对应的数据访问类以对 Protobuf 数据进行序列化、反序列化操作)。
利用 Protobuf 无需学习繁芜的文档工具模型,Protobuf 的编程模式比较友好,大略易学,同时它拥有良好的文档和示例,对付喜好大略事物的人们而言,Protobuf 比其他的技能更加有吸引力。

3.3Protobuf 的不敷

Protbuf 与 XML 比较也有不敷之处。
它功能大略,无法用来表示繁芜的观点。
XML 已经成为多种行业标准的编写工具,Protobuf 只是 Google 公司内部利用的工具,在通用性上还差很多。
由于文本并不适宜用来描述数据构造,以是 Protobuf 也不适宜用来对基于文本的标记文档(如 HTML)建模。
其余,由于 XML 具有某种程度上的自阐明性,它可以被人直接读取编辑,在这一点上 Protobuf 弗成,它以二进制的办法存储,除非你有 .proto 定义,否则你没法直接读出 Protobuf 的任何内容【 2 】。

4、Protobuf 的更多细节

4.1简述

人们一贯在强调,同 XML 比较, Protobuf 的紧张优点在于性能高。
它以高效的二进制办法存储,比 XML 小 3 到 10 倍,快 20 到 100 倍。
对付这些 “小 3 到 10 倍”,“快 20 到 100 倍”的说法,严明的程序员须要一个阐明。
因此在本文的末了,让我们轻微深入 Protobuf 的内部实现吧。
有两项技能担保了采取 Protobuf 的程序能得到相对付 XML 极大的性能提高:

第一点:我们可以稽核 Protobuf 序列化后的信息内容。
您可以看到 Protocol Buffer 信息的表示非常紧凑,这意味着的体积减少,自然须要更少的资源。
比如网络上传输的字节数更少,须要的 IO 更少等,从而提高性能。
第二点:我们须要理解 Protobuf 封解包的大致过程,从而理解为什么会比 XML 快很多。

4.2Google Protocol Buffer 的 Encoding

Protobuf 序列化后所天生的二进制非常紧凑,这得益于 Protobuf 采取的非常奥妙的 Encoding 方法。
稽核构造之前,让我首先要先容一个叫做 Varint 的术语。
Varint 是一种紧凑的表示数字的方法。
它用一个或多个字节来表示一个数字,值越小的数字利用越少的字节数。
这能减少用来表示数字的字节数。
比如对付 int32 类型的数字,一样平常须要 4 个 byte 来表示。
但是采取 Varint,对付很小的 int32 类型的数字,则可以用 1 个 byte 来表示。
当然凡事都有好的也有不好的一壁,采取 Varint 表示法,大的数字则须要 5 个 byte 来表示。
从统计的角度来说,一样平常不会所有的中的数字都是大数,因此大多数情形下,采取 Varint 后,可以用更少的字节数来表示数字信息。
下面就详细先容一下 Varint。
Varint 中的每个 byte 的最高位 bit 有分外的含义,如果该位为 1,表示后续的 byte 也是该数字的一部分,如果该位为 0,则结束。
其他的 7 个 bit 都用来表示数字。
因此小于 128 的数字都可以用一个 byte 表示。
大于 128 的数字,比如 300,会用两个字节来表示:1010 1100 0000 0010。
下图演示了 Google Protocol Buffer 如何解析两个 bytes。
把稳到终极打算前将两个 byte 的位置相互交流过一次,这是由于 Google Protocol Buffer 字节序采取 little-endian 的办法。
图 6. Varint 编码:

经由序列化后会成为一个二进制数据流,该流中的数据为一系列的 Key-Value 对。
如下图所示。
图 7. Message Buffer:

采取这种 Key-Pair 构造无需利用分隔符来分割不同的 Field。
对付可选的 Field,如果中不存在该 field,那么在终极的 Message Buffer 中就没有该 field,这些特性都有助于节约本身的大小。
以代码清单 1 中的为例。
假设我们天生如下的一个 Test1:

Test1.id = 10;Test1.str = “hello”;

则终极的 Message Buffer 中有两个 Key-Value 对,一个对应中的 id;另一个对应 str。
Key 用来标识详细的 field,在解包的时候,Protocol Buffer 根据 Key 就可以知道相应的 Value 该当对应于中的哪一个 field。
Key 的定义如下:

(field_number << 3) | wire_type

可以看到 Key 由两部分组成。
第一部分是 field_number,比如 lm.helloworld 中 field id 的 field_number 为 1。
第二部分为 wire_type。
表示 Value 的传输类型。
Wire Type 可能的类型如下表所示:

在我们的例子当中,field id 所采取的数据类型为 int32,因此对应的 wire type 为 0。
细心的读者或许会看到在 Type 0 所能表示的数据类型中有 int32 和 sint32 这两个非常类似的数据类型。
Google Protocol Buffer 差异它们的紧张意图也是为了减少 encoding 后的字节数。
在打算机内,一个负数一样平常会被表示为一个很大的整数,由于打算机定义负数的符号位为数字的最高位。
如果采取 Varint 表示一个负数,那么一定须要 5 个 byte。
为此 Google Protocol Buffer 定义了 sint32 这种类型,采取 zigzag 编码。
Zigzag 编码用无符号数来表示有符号数字,正数和负数交错,这便是 zigzag 这个词的含义了。
图 8. ZigZag 编码:

利用 zigzag 编码,绝对值小的数字,无论正负都可以采取较少的 byte 来表示,充分利用了 Varint 这种技能。
其他的数据类型,比如字符串等则采取类似数据库中的 varchar 的表示方法,即用一个 varint 表示长度,然后将别的部分紧跟在这个长度部分之后即可。
通过以上对 protobuf Encoding 方法的先容,想必您也已经创造 protobuf 的内容小,适于网络传输。
如果您对那些有关技能细节的描述缺少耐心和兴趣,那么下面这个大略而直不雅观的比较该当能给您更加深刻的印象。
对付代码清单 1 中的,用 Protobuf 序列化后的字节序列为:

08 65 12 06 48 65 6C 6C 6F 77

而如果用 XML,则类似这样:

31 30 31 3C 2F 69 64 3E 3C 6E 61 6D 65 3E 68 656C 6C 6F 3C 2F 6E 61 6D 65 3E 3C 2F 68 65 6C 6C6F 77 6F 72 6C 64 3E

一共 55 个字节,这些奇怪的数字须要轻微阐明一下,其含义用 ASCII 表示如下:

<helloworld><id>101</id><name>hello</name></helloworld>

4.3封解包的速率

首先我们来理解一下 XML 的封解包过程。
XML 须要从文件中读取出字符串,再转换为 XML 文档工具构造模型。
之后,再从 XML 文档工具构造模型中读取指定节点的字符串,末了再将这个字符串转换成指定类型的变量。
这个过程非常繁芜,个中将 XML 文件转换为文档工具构造模型的过程常日须要完成词法文法剖析等大量花费 CPU 的繁芜打算。
反不雅观 Protobuf,它只须要大略地将一个二进制序列,按照指定的格式读取到 C++ 对应的构造类型中就可以了。
从上一节的描述可以看到的 decoding 过程也可以通过几个位移操作组成的表达式打算即可完成。
速率非常快。
为相识释这并不是我拍脑袋随意想出来的说法,下面让我们大略剖析一下 Protobuf 解包的代码流程吧。
以代码清单 3 中的 Reader 为例,该程序首先调用 msg1 的 ParseFromIstream 方法,这个方法解析从文件读入的二进制数据流,并将解析出来的数据授予 helloworld 类的相应数据成员。
该过程可以用下图表示,图 9. 解包流程图:

全体解析过程须要 Protobuf 本身的框架代码和由 Protobuf 编译器天生的代码共同完成。
Protobuf 供应了基类 Message 以及 Message_lite 作为通用的 Framework,CodedInputStream 类,WireFormatLite 类等供应了对二进制数据的 decode 功能。
从 5.1 节的剖析来看,Protobuf 的解码可以通过几个大略的数学运算完成,无需繁芜的词法语法剖析,因此 ReadTag() 等方法都非常快。
在这个调用路径上的其他类和方法都非常大略,感兴趣的读者可以自行阅读。
相对付 XML 的解析过程,以上的流程图实在是非常大略吧?这也便是 Protobuf 效率高的第二个缘故原由了。

5、结束语

每每理解越多,人们就会越以为自己无知。
我惶恐地创造自己竟然写了一篇关于序列化的文章,文中一定有许多想当然而自以为是的东西,还希望各位能够去伪存真,更希望真的高手能不吝见教,给我来信。
感激。

顺便推举一个关于Protobuf协议详解的视频给大家点击Protobuf协议详细先容就可以不雅观看了!
其余还有一些关于c++ Linux后台做事器开拓的一些知识点分享:Linux,Nginx,MySQL,Redis,P2P,K8S,Docker,TCP/IP,协程,DPDK,webrtc,音视频等等视频。

喜好的朋友可往后台私信【1】获取学习视频

附上一份音视频学习课程大纲给大家