ProtoBuf
- Protocol Buffer ( protoBuf 或 PB )是 google 的一种数据交换的格式,它独立于语言,独立于平台。
- google 提供了多种语言的实现:java、c#、c++、go 和 python,每一种实现都包含了相应语言的编译器以及库文件。
- 由于它是一种二进制的格式,比使用 XML 进行数据交换快许多,可以把它用于分布式应用之间的数据通信或者异构环境下的数据交换,作为一种效率和兼容性都很优秀的二进制数据传输格式,可以用于诸如网络传输、配置文件、数据存储等诸多领域。
- 相比XML,它更小、更快、也更简单。可以定义自己的数据结构,然后使用代码生成器生成的代码来读写这个数据结构,甚至可以在无需重新部署程序的情况下更新数据结构,只需使用 Protobuf 对数据结构进行一次描述,即可利用各种不同语言或从各种不同数据流中对你的结构化数据轻松读写。
- “向后”兼容性好。不必破坏已部署的、依靠“老”数据格式的程序就可以对数据结构进行升级,这样程序就可以不必担心因为消息结构的改变而造成的大规模的代码重构或者迁移的问题,因为添加新的消息中的 field 并不会引起已经发布的程序的任何改变。
- Protobuf 语义更清晰。无需类似 XML 解析器的东西,Protobuf 编译器就可将 .proto 文件编译生成对应的数据访问类以对 Protobuf 数据进行序列化、反序列化操作。
- 使用 Protobuf 无需学习复杂的文档对象模型。Protobuf 的编程模式比较友好,简单易学,同时它拥有良好的文档和示例。
- 相比XML,它功能简单,无法用来表示复杂的概念。
- XML 已经成为多种行业标准的编写工具,Protobuf 只是 Google 公司内部使用的工具,在通用性上还差很多。
- 由于文本并不适合用来描述数据结构,所以 Protobuf 也不适合用来对基于文本的标记文档(如 HTML)建模。
- 由于 XML 具有某种程度上的自解释性,它可以被人直接读取编辑,在这一点上 Protobuf 不行,它以二进制的方式存储,除非你有 .proto 定义,否则你没法直接读出 Protobuf 的任何内容。
.proto 文件是 ProtoBuf 一个重要的文件,它定义了需要序列化数据的结构,用法步骤:
- 在.proto文件中定义消息格式
- 用 ProtoBuf 编译器编译 .proto 文件
- 用 C++ 对应的 ProtoBuf API 来写或者读消息
下载地址:https://github.com/protocolbuffers/protobuf/releases
下载的安装包:protoc-3.9.0-win32.zip、protoc-3.9.0-win64.zip
操作系统:WIN10 64位
下载的源码包:protobuf-3.9.0.zip
安装使用
1、解压protoc-3.9.0-win32.zip到任意目录
2、将解压目录 \protoc-3.9.0-win32\bin 配置到环境变量path下
3、在命令行下即可使用 protoc 命令编译 .proto 文件生成 C++ 对应的 .h 和 .cc 文件
在 cmd 命令行中进行:
> protoc --version#查看protoc的版本
#可以先 cd 到指定目录,然后设置为相对路径
> protoc -I=$SRC_DIR --cpp_out=$DST_DIR $SRC_DIR/xxx.proto #编译 .proto 文件
#-I:主要用于指定待编译的 .proto 消息定义文件所在的目录,即可能出现的包含文件的路径,该选项可以被同时指定多个,此处指定的路径不能为空,如果是当前目录,直接使用.,如果是子目录,直接使用子目录相对径
编译流程
操作系统:WIN10 64位
编译工具:VS 2015
在 Win 上编译需要用到 CMake :https://cmake.org/download/
Win 上编译使用流程:
1、解压源码到任意路径下 \protobuf-3.9.0下
2、到官网(或国内下载站)下载 CMake,并安装
3、CMake 生成 VS 2015 工程
- 打开CMake
- 设置源码路径下的cmake目录 /protobuf-3.9.0/cmake
- 设置任意构建目录 /probobuf_build
- 点 configure 按钮
- 选择对应的VS,这里选的是VS 2015,选择编译为WIN32,编译器默认
- 点击 Finish 按钮,开始自动编译(只要不报error表示顺利,否则根据日志查找原因)
- 点击 Generate 按钮生成VS项目
- 用VS打开生成的工程,根据需要选择编译libprotobuf、libprotobuf-lite、libprotoc和protoc项目
- 编译完成
5、 .proto文件编译生成对应的.h和.cc文件
6、引入自己的工程使用
语法规则
1、编码规范
- 描述文件以.proto做为文件后缀。
- 结构定义包括:message、service、enum,这些以外的语句以分号结尾,rpc方法定义结尾的分号可有可无。
- message 命名采用驼峰命名方式,字段命名采用小写字母加下划线分隔方式。
- enums 类型名采用驼峰命名方式,字段命名采用大写字母加下划线分隔方式。
- service与rpc方法名统一采用驼峰式命名。
提供以下两种注释方式:
// 单行注释
/* 多行注释 */
3、默认值
解析消息时,如果编码消息不包含特定的单数元素,则解析对象中的相应字段将设置为该字段的默认值,这些默认值是特定于类型的。
- 对于字符串类型,默认值为空字符串
- 对于字节类型,默认值为空字节
- 对于布尔类型,默认值为false
- 对于数字类型,默认值为零
- 对于枚举,默认值是第一个定义的枚举值,该值必须为0
- 对于消息字段,未设置该字段,它的确切值取决于语言
- 重复字段的默认值为空(通常是相应语言的空列表)
- 在.proto文件定义消息,message是.proto文件最小的逻辑单元,由一系列name-value键值对构成。
package hw;
message test
{
required int32 id = 1;
required string str = 2;
optional int32 opt = 3;
//可选字段
repeated string phone_num = 4;
}
- message消息包含一个或多个编号唯一的字段,每个字段由【字段限制+字段类型+字段名+编号】组成,字段限制分为【optional(可选的)、required(必须的)、repeated(重复的)】
- required关键字 表示是一个必须字段,必须相对于发送方,在发送消息之前必须设置该字段的值,对于接收方,必须能够识别该字段的意思。发送之前没有设置required字段或者无法识别required字段都会引发编解码异常,导致消息被丢弃。
- optional关键字 字面意思是可选的意思,具体protobuf里面怎么处理这个字段呢,就是protobuf处理的时候另外加了一个bool的变量,用来标记这个optional字段是否有值,发送方在发送的时候,如果这个字段有值,那么就给bool变量标记为true,否则就标记为false,接收方在收到这个字段的同时,也会收到发送方同时发送的bool变量,拿着bool变量就知道这个字段是否有值了,可选对于发送方,在发送消息时,可以有选择性的设置或者不设置该字段的值。对于接收方,如果能够识别可选字段就进行相应的处理,如果无法识别,则忽略该字段,消息中的其它字段正常处理。optional字段的特性,很多接口在升级版本中都把后来添加的字段都统一的设置为optional字段,这样老的版本无需升级程序也可以正常的与新的软件进行通信,只不过新的字段无法识别而已,因为并不是每个节点都需要新的功能,因此可以做到按需升级和平滑过渡。
- repeated关键字 表示该字段可以包含0~N个元素。每一次可以包含多个值,可以看作是在传递一个数组的值。也是optional字段一样,另外加了一个count计数变量,用于标明这个字段有多少个,这样发送方发送的时候,同时发送了count计数变量和这个字段的起始地址,接收方在接受到数据之后,按照count来解析对应的数据即可。
- 定义好消息后,使用ProtoBuf编译器生成C++对应的.h和.cc文件(hw.test.pb.h,hw.test.pb.cc),源文件提供了message消息的序列化和反序列化等方法,在生成的头文件中定义了一个 C++ 类,该类提供了一系列操作定义结构的方法。
protoc -I=$SRC_DIR --cpp_out=$DST_DIR $SRC_DIR/hw.test.proto
5、嵌套以及导入message
import hw.test;
//用 Import 关键字引入在其他 .proto 文件中定义的消息
package addresslist;
message Person
{
//1到15范围内的字段编号需要一个字节进行编码,包括字段编号和字段类型
//16到2047范围内的字段编号占用两个字节
//应该为非常频繁出现的消息元素保留数字1到15
//要为将来可能添加的常用元素留出一些空间
required string name = 1;
required int32 id = 2;
optional string email = 3;
enum PhoneType //定义枚举的时候,我们要保证第一个枚举值必须是0,枚举值不能重复,
//除非使用 option allow_alias = true 选项来开启别名
//枚举值的范围是32-bit integer,但因为枚举值使用变长编码,
//所以不推荐使用负数作为枚举值,因为这会带来效率问题
{
MOBILE = 0;
HOME = 1;
WORK = 2;
}
message PhoneNumber //定义嵌套消息
{
required string number = 1;
optional PhoneType type = 2 [default = HOME];
}
repeated PhoneNumber phone = 4;
}
6、map映射
如果要在数据定义中创建关联映射,Protocol Buffers提供了一种方便的语法:
map< key_type, value_type> map_field = N ;
其中key_type可以是任何整数或字符串类型。
- 枚举不是有效的key_type,value_type可以是除map映射类型外的任何类型。
- map的字段可以是repeated。
- 序列化后的顺序和map迭代器的顺序是不确定的,所以你不要期望以固定顺序处理map。
- 当为.proto文件产生生成文本格式的时候,map会按照key 的顺序排序,数值化的key会按照数值排序。
- 从序列化中解析或者融合时,如果有重复的key则后一个key不会被使用,当从文本格式中解析map时,如果存在重复的key,则解析可能会失败。
- 如果为映射字段提供键但没有值,则字段序列化时的行为取决于语言。
- 在Python中,使用类型的默认值。
如果你的消息中有很多可选字段, 并且同时至多一个字段会被设置, 你可以加强这个行为,使用oneof特性节省内存。为了在.proto定义oneof字段, 你需要在名字前面加上oneof关键字
message testMessage
{
oneof test_oneof
{
string name = 4;
SubMessage sub_message = 9;
}
}
然后你可以增加oneof字段到 oneof 定义中. 你可以增加任意类型的字段。
注意Oneof特性:
- oneof字段只有最后被设置的字段才有效,即后面的set操作会覆盖前面的set操作
- oneof不可以是repeated的
- 反射API可以作用于oneof字段
- 如果使用C++要防止内存泄露,即后面的set操作会覆盖之前的set操作,导致前面设置的字段对象发生析构,要注意字段对象的指针操作
- 如果使用C++的Swap()方法交换两条oneof消息,两条消息都不会保存之前的字段
- 向后兼容。添加或删除oneof字段的时候要注意,如果检测到oneof字段的返回值是None/NOT_SET,这意味着oneof没有被设置或者设置了一个不同版本的oneof的字段,但是没有办法能够区分这两种情况,因为没有办法确认一个未知的字段是否是一个oneof的成员
- 编号复用问题。1)删除或添加字段到oneof:在消息序列化或解析后会丢失一些信息,一些字段将被清空;2)删除一个字段然后重新添加:在消息序列化或解析后会清除当前设置的oneof字段;3)分割或合并字段:同普通的删除字段操作
Any类型是一种不需要在.proto文件中定义就可以直接使用的消息类型,使用前import google/protobuf/any.proto文件即可。
import"google/protobuf/any.proto";
message ErrorStatus{
string message =1;
repeated google.protobuf.Any details =2;
}
C++使用PackFrom()和UnpackTo()方法来打包和解包Any类型消息。
// Storing an arbitrary message type in Any.
NetworkErrorDetails details =...;
ErrorStatus status;
status.add_details()->PackFrom(details);
// Reading an arbitrary message from Any.
ErrorStatus status =...;
for(constAny& detail : status.details())
{
if(detail.Is())
{
NetworkErrorDetails network_error;
detail.UnpackTo(&network_error);
...
processing network_error
...
}
}
9、定义服务
如果想在RPC系统中使用消息类型,就需要在.proto文件中定义RPC服务接口,然后使用编译器生成对应语言的存根。
service SearchService {
rpc Search (SearchRequest) returns (SearchResponse);
}
10、更新一个数据类型
在实际的开发中会存在这样一种应用场景,既消息格式因为某些需求的变化而不得不进行必要的升级,但是有些使用原有消息格式的应用程序暂时又不能被立刻升级,这便要求我们在升级消息格式时要遵守一定的规则,从而可以保证基于新老消息格式的新老程序同时运行。规则如下:
- 不要修改已经存在字段的标签号。
- 任何新添加的字段必须是optional和repeated限定符,否则无法保证新老程序在互相传递消息时的消息兼容性。
- 在原有的消息中,不能移除已经存在的required字段,optional和repeated类型的字段可以被移除,但是他们之前使用的标签号必须被保留,不能被新的字段重用。
- int32、uint32、int64、uint64和bool等类型之间是兼容的,sint32和sint64是兼容的,string和bytes是兼容的,fixed32和sfixed32,以及fixed64和sfixed64之间是兼容的,这意味着如果想修改原有字段的类型时,为了保证兼容性,只能将其修改为与其原有类型兼容的类型,否则就将打破新老消息格式的兼容性。
- optional和repeated限定符也是相互兼容的。
1、定义.proto文件
syntax = "proto2";
//不定义默认是这个(有警告),也可以定义成"proto3"option java_package = "com.demo.myproto";
//使用精简版的protobuf库,需要链接libprotobuf-lite.lib
option optimize_for = LITE_RUNTIME;
//生成的可执行程序速度快,体积小,以牺牲Protocol Buffer提供的反射功能为代价package myproto;
message HelloWorld {
required int32 id = 1;
required string str = 2;
optional int32 opt=3;
}
保存成myproto.proto文件,放到指定目录
2、生成 C++ 文件
打开 cmd 命令行,cd 到指定目录下
> protoc -I ./ --cpp_out=./ myproto.proto #在本目录下转
3、将生成的 C++ 文件加入工程中
4、附加protobuf头文件目录(protobuf源文件 src 下的 google 文件夹引入到工程,生成的 C++ 文件需要)、附加依赖库文件目录、附加依赖库文件名 libprotobuf-lite.lib、设置运行库一致
5、编写测试程序
#include .h>
#include .h>#include "myproto.pb.h"
#include
#include /*
message HelloWorld {
required int32 id = 1;
required string str = 2;
optional int32 opt=3;
}
*/int main()
{
printf("My Hello World Application !\r\n");
//设置参数
myproto::HelloWorld in_msg;
in_msg.set_id(1);
in_msg.set_str("zab");
in_msg.set_opt(0);
//对消息进行编码
int nSize = in_msg.ByteSize() + 8;
char *pcData = https://www.it610.com/article/new char[nSize];
memset(pcData, 0, nSize);
int nEncodedLen = 0;
google::protobuf::io::ArrayOutputStream array_stream(pcData, nSize);
google::protobuf::io::CodedOutputStream output_stream(&array_stream);
//操作编码时的varints
//Varint 是一种紧凑的表示数字的方法
//它用一个或多个字节来表示一个数字,值越小的数字使用越少的字节数
//这能减少用来表示数字的字节数
//Varint 中的每个 byte 的最高位 bit 有特殊的含义,如果该位为 1,表示后续的 byte 也是该数字的一部分,如果该位为 0,则结束
//其他的 7 个 bit 都用来表示数字
//因此小于 128 的数字都可以用一个 byte 表示
//大于 128 的数字,会用两个字节
output_stream.WriteVarint32(in_msg.ByteSize());
//写为Varint
if (in_msg.SerializeToCodedStream(&output_stream)) {
nEncodedLen = output_stream.ByteCount();
//对消息进行解码
myproto::HelloWorld out_msg;
//构造一个InputStream,返回data所指向的字节数组
//若指定了block_size,则每次调用Next()返回不超过指定大小的数据块;否则,返回整个字节数组
google::protobuf::io::CodedInputStream input_stream ((google::protobuf::uint8*)pcData, nEncodedLen);
//操作解码时的varints
google::protobuf::uint32 ui_size = 0;
if (input_stream.ReadVarint32(&ui_size)) {//还原Varint
google::protobuf::io::CodedInputStream::Limit limit = input_stream.PushLimit(ui_size);
//保护在长度范围内解析
if (out_msg.MergeFromCodedStream(&input_stream)) {//分割填充
// 当对报文进行解析后,以进一步判断报文是否被以正确方式读取完毕
if (input_stream.ConsumedEntireMessage()) {
input_stream.PopLimit(limit);
//释放保护
printf("HelloWorld id=%d,str=%s,opt=%d.\r\n", out_msg.id(), out_msg.str().c_str(), out_msg.opt());
} else {
printf("consume msg fail!\r\n");
}
} else {
printf("merge msg stream fail!\r\n");
}
} else {
printf("read msg stream fail!\r\n");
}
} else {
printf("encode msg fail!\r\n");
}
if (NULL != pcData) {
delete[] pcData;
pcData = https://www.it610.com/article/NULL;
}
nEncodedLen = 0;
printf("My Hello World Application End!\r\n");
system("pause");
return 0;
}
ProtoBuf的三种优化级别
optimize_for是文件级别的选项,Protocol Buffer 定义三种优化级别SPEED、CODE_SIZE、LITE_RUNTIME,缺省情况下是SPEED。
- SPEED: 表示生成的代码运行效率高,但是由此生成的代码编译后会占用更多的空间
- CODE_SIZE: 和SPEED恰恰相反,代码运行效率较低,但是由此生成的代码编译后会占用更少的空间,通常用于资源有限的平台,如Mobile
- LITE_RUNTIME: 生成的代码执行效率高,同时生成代码编译后的所占用的空间也是非常少,以牺牲Protocol Buffer提供的反射功能为代价的,在C++中链接Protocol Buffer库时仅需链接libprotobuf-lite,而非libprotobuf,在Java中仅需包含protobuf-java-2.4.1-lite.jar,而非protobuf-java-2.4.1.jar
- SPEED和LITE_RUNTIME相比,在于调试级别上,例如 msg.SerializeToString(&str) 在SPEED模式下会利用反射机制打印出详细字段和字段值,但是LITE_RUNTIME则仅仅打印字段值组成的字符串,因此可以在程序调试阶段使用 SPEED模式,而上线以后使用提升性能使用 LITE_RUNTIME 模式优化
动态编译
一般情况下,使用 Protobuf 的人们都会先写好 .proto 文件,再用 Protobuf 编译器生成目标语言所需要的源代码文件,可是在某且情况下,人们无法预先知道 .proto 文件,他们需要动态处理一些未知的 .proto 文件,比如一个通用的消息转发中间件,它不可能预知需要处理怎样的消息,这需要动态编译 .proto 文件,并使用其中的 Message。
无须编译.proto生成.pb.cc和.pb.h,需要使用protobuf中提供的两个头文件
- protobuf中头文件的位置/usr/local/include/google/protobuf
- google/protobuf/compiler/importer.h ,google/protobuf/dynamic_message.h
#include
#include >
#include
#include class MyErrorCollector: public google::protobuf::compiler::MultiFileErrorCollector
{
virtual void AddError(const std::string & filename, int line, int column, const std::string & message){
// define import error collector
printf("%s, %d, %d, %s\n", filename.c_str(), line, column, message.c_str());
}
};
int main()
{
google::protobuf::compiler::DiskSourceTree sourceTree;
// source tree
sourceTree.MapPath("", "/home/szw/code/protobuf/tmp2");
// initialize source tree
MyErrorCollector errorCollector;
// dynamic import error collector
google::protobuf::compiler::Importer importer(&sourceTree, &errorCollector);
// importerimporter.Import("test.proto");
// find a message descriptor from message descriptor pool
const google::protobuf::Descriptor * descriptor = importer.pool()->FindMessageTypeByName("lm.helloworld");
if (!descriptor){
perror("Message is not found!");
exit(-1);
}// create a dynamic message factory
google::protobuf::DynamicMessageFactory * factory = new google::protobuf::DynamicMessageFactory(importer.pool());
// create a const message ptr by factory method and message descriptor
constgoogle::protobuf::Message * tmp = factory->GetPrototype(descriptor);
// create a non-const message object by const message ptr
// define import error collector
google::protobuf::Message * msg = tmp->New();
return 0;
}
其中,头文件
- 首先初始化一个DiskSourceTree对象, 指明目标.proto文件的根目录
- 创建一个MyErrorCollector对象, 用于收集动态编译过程中产生的编译bug, 该对象需要根据proto提供的纯虚基类派生, 并需要重写其中的纯虚函数, 使之不再是一个抽象类
- 初始化一个Importer对象, 用于动态编译, 将DiskSourceTree和MyErrorCollector的地址传入
- 将目标.proto动态编译, 并加入编译器池中
- 可以通过FindMessageTypeName()和消息名, 来获取到目标消息的Message_Descriptor
- 创建动态工厂, 并利用工厂方法GetPrototype和目标消息的Message_Descriptor获取一个指针const message *
- 利用消息实例指针的New()方法获取到non-const message ptr
类型反射即是根据字段的名称获取到字段类型的一种机制,protobuf自身便配备了类型反射机制,需要头文件
protobuf对于每个元素都有一个相应的descriptor,这个descriptor包含该元素的所有元信息。
文章图片
- FileDescriptor: 对一个proto文件的描述,它包含文件名、包名、选项(如package, java_package, java_outer_classname等)、文件中定义的所有message、文件中定义的所有enum、文件中定义的所有service、文件中所有定义的extension、文件中定义的所有依赖文件(import)等。在FileDescriptor中还存在一个DescriptorPool实例,它保存了所有的dependencies(依赖文件的FileDescriptor)、name到GenericDescriptor的映射、字段到FieldDescriptor的映射、枚举项到EnumValueDescriptor的映射,从而可以从该DescriptorPool中查找相关的信息,因而可以通过名字从FileDescriptor中查找Message、Enum、Service、Extensions等。可以通过 --descriptor_set_out 指定生成某个proto文件相对应的FileDescriptorSet文件。
- Descriptor: 对一个message定义的描述,它包含该message定义的名字、所有字段、内嵌message、内嵌enum、关联的FileDescriptor等。可以使用字段名或字段号查找FieldDescriptor。
- FieldDescriptor:对一个字段或扩展字段定义的描述,它包含字段名、字段号、字段类型、字段定义(required/optional/repeated/packed)、默认值、是否是扩展字段以及和它关联的Descriptor/FileDescriptor等。
- EnumDescriptor:对一个enum定义的描述,它包含enum名、全名、和它关联的FileDescriptor。可以使用枚举项或枚举值查找EnumValueDescriptor
- EnumValueDescriptor:对一个枚举项定义的描述,它包含枚举名、枚举值、关联的EnumDescriptor/FileDescriptor等。
- ServiceDescriptor:对一个service定义的描述,它包含service名、全名、关联的FileDescriptor等。
- MethodDescriptor:对一个在service中的method的描述,它包含method名、全名、参数类型、返回类型、关联的FileDescriptor/ServiceDescriptor等。
能不能通过程序生成protobuf文件呢?答案是可以的。FileDescriptorProto允许你动态的定义你的proto文件:
//syntax = "proto3";
//message mymsg
//{
//uint32 len = 1;
//uint32 type = 2;
//}
FileDescriptorProto file_proto;
file_proto.set_name("my.proto");
file_proto.set_syntax("proto3");
DescriptorProto *message_proto = file_proto.add_message_type();
message_proto->set_name("mymsg");
FieldDescriptorProto *field_proto = NULL;
field_proto = message_proto->add_field();
field_proto->set_name("len");
field_proto->set_type(FieldDescriptorProto::TYPE_UINT32);
field_proto->set_number(1);
field_proto->set_label(FieldDescriptorProto::LABEL_OPTIONAL);
field_proto = message_proto->add_field();
field_proto->set_name("type");
field_proto->set_type(FieldDescriptorProto::TYPE_UINT32);
field_proto->set_number(2);
DescriptorPool pool;
const FileDescriptor *file_descriptor = pool.BuildFile(file_proto);
cout << file_descriptor->DebugString();
Protobuf 序列化原理
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 的方式。
文章图片
消息经过序列化后会成为一个二进制数据流,该流中的数据为一系列的 Key-Value 对。如下图所示:
文章图片
采用这种 Key-Pair 结构无需使用分隔符来分割不同的 Field。对于可选的 Field,如果消息中不存在该 field,那么在最终的 Message Buffer 中就没有该 field,这些特性都有助于节约消息本身的大小。
时间效率
通过protobuf序列化/反序列化的过程可以得出:protobuf是通过算法生成二进制流,序列化与反序列化不需要解析相应的节点属性和多余的描述信息,所以序列化和反序列化时间效率较高。
空间效率
xml、json是用字段名称来确定类实例中字段之间的独立性,所以序列化后的数据多了很多描述信息,增加了序列化后的字节序列的容量。Protobuf的序列化/反序列化过程可以得出:protobuf是由字段索引(fieldIndex)与数据类型(type)计算(fieldIndex<<3|type)得出的key维护字段之间的映射且只占一个字节,所以相比json与xml文件,protobuf的序列化字节没有过多的key与描述符信息,所以占用空间要小很多。
【Win10 下 ProtoBuf 安装编译以及在 C++ 中的用法】参考:
https://www.ibm.com/developerworks/cn/linux/l-cn-gpb/