gRPC快速入门
gRPC快速入门(一)——Protobuf简介
https://blog.51cto.com/u_9291927/2331980
gRPC快速入门(一)——Protobuf简介
一、Protobuf简介
1、Protobuf简介
Protobuf即Protocol Buffers,是Google公司开发的一种跨语言和平台的序列化数据结构的方式,是一个灵活的、高效的用于序列化数据的协议。
与XML和JSON格式相比,protobuf更小、更快、更便捷。protobuf是跨语言的,并且自带一个编译器(protoc),只需要用protoc进行编译,就可以编译成Java、Python、C++、C#、Go等多种语言代码,然后可以直接使用,不需要再写其它代码,自带有解析的代码。
只需要将要被序列化的结构化数据定义一次(在.proto文件定义),便可以使用特别生成的源代码(使用protobuf提供的生成工具)轻松的使用不同的数据流完成对结构数据的读写操作。甚至可以更新.proto文件中对数据结构的定义而不会破坏依赖旧格式编译出来的程序。
GitHub地址: https://github.com/protocolbuffers/protobuf
不同语言源码版本下载地址:
https://github.com/protocolbuffers/protobuf/releases/latest
2、Protobuf的优缺点
Protobuf的优点如下:
A、性能号,效率高
序列化后字节占用空间比XML少3-10倍,序列化的时间效率比XML快20-100倍。
B、有代码生成机制
将对结构化数据的操作封装成一个类,便于使用。
C、支持向后和向前兼容
当客户端和服务器同时使用一块协议的时候, 当客户端在协议中增加一个字节,并不会影响客户端的使用
D、支持多种编程语言
Protobuf目前已经支持Java,C++,Python、Go、Ruby等多种语言。
Protobuf的缺点如下:
A、二进制格式导致可读性差
B、缺乏自描述
二、Protobuf编译器安装
1、C++版本Protobuf编译器安装
下载C++版本的Protobuf源码protobuf-cpp-3.6.1.tar.gz
解压Protobuf源码:tar -zxvf protobuf-cpp-3.6.1.tar.gz
进入protobuf-3.6.1源码目录:cd protobuf-3.6.1
配置变量:./configure --prefix=/usr/local/protobuf
编译:make
检查、测试:make check
安装:sudo make install
设置环境变量:
检查版本号:protoc --version
2、Protobuf编译器使用
Protobuf提供了protoc编译器,用于通过定义好的.proto文件来生成Java,Python,C++,Ruby,Objective-C,C#,Go等语言代码。protoc --proto_path=IMPORT_PATH --cpp_out=DST_DIR --java_out=DST_DIR --python_out=DST_DIR --go_out=DST_DIR --ruby_out=DST_DIR --javanano_out=DST_DIR --objc_out=DST_DIR --csharp_out=DST_DIR path/to/file.proto
(1)导入目录设置
IMPORT_PATH声明了一个.proto文件所在的解析import具体目录。如果忽略该值,则使用当前目录。如果有多个目录则可以多次调用–proto_path,会顺序的被访问并执行导入。-I=IMPORT_PATH是–proto_path的简化形式。
(2)生成代码指定
(3)导入proto消息文件指定
必须指定一个或多个.proto文件作为输入,多个.proto文件可以只指定一次。虽然文件路径是相对于当前目录的,每个文件必须位于其IMPORT_PATH下,以便每个文件可以确定其规范的名称。
(4)生成编程语言相关代码
当用Protobuf编译器来运行.proto文件时,编译器将生成所选择语言的代码,相应语言的代码可以操作在.proto文件中定义的消息类型,包括获取、设置字段值,将消息序列化到一个输出流中以及从一个输入流中解析消息。
对C++语言,编译器会为每个*.proto文件生成一个*.h文件和一个.cc文件,.proto文件中的每一个消息有一个对应的类。
对Java语言,编译器为每一个消息类型生成了一个.java文件以及一个特殊的Builder类(用来创建消息类接口的)。
对Go语言,编译器会为每个消息类型生成了一个.pb.go文件。
对Ruby语言,编译器会为每个消息类型生成了一个.rb文件。
三、Protobuf3语法
1、消息定义
Protobuf中,消息即结构化数据。
Person消息格式有3个字段,在消息中承载的数据分别对应于每一个字段,其中每个字段都有一个名字和一种类型。
在一个消息文件.proto中可以定义多个消息类型,在定义多个相关的消息的时候较为有用。
.proto文件中非注释非空的第一行必须使用Proto版本声明,版本声明如下:
syntax = “proto3”;
如果不使用proto3版本声明,Protobuf编译器默认使用proto2版本。
Proto消息文件的命名如下:
packageName.MessageName.proto
packageName为package声明的包名
MessageName为消息名称
2、添加注释
添加注释可以使用C/C++/java风格的双斜杠(//)语法格式。
3、Package
.proto文件中可以新增一个可选的package声明符,用来防止不同的消息类型有命名冲突。包的声明符会根据使用语言的不同影响生成的代码:
A、对于C语言,产生的类会被包装在C的命名空间中。
B、对于Java语言,包声明符会变为java的一个包,除非在.proto文件中提供了一个明确有java_package。
C、对于Go语言,包可以被用做Go包名称,除非显式的提供一个option go_package在.proto文件中。
Protobuf语法中类型名称的解析与C++是一致的:首先从最内部开始查找,依次向外进行,每个包会被看作是其父类包的内部类。当然对于Company.Person以“.”分隔的是从最外围开始的。
Protobuf编译器会解析.proto文件中定义的所有类型名。 对于不同语言的代码生成器会知道如何来指向每个具体的类型,即使它们使用了不同的规则。
4、字段类型
字段类型包括标量类型和合成类型。
标量类型包括:
合成类型包括枚举(enumerations)或其它消息类型。
5、标识符
在消息定义中,每个字段都有唯一的一个数字标识符。标识符用来在消息的二进制格式中识别各个字段,一旦使用就不能够再改变。
最小的标识符可以从1开始,最大到2^29 - 1(536,870,911),不可以使用其中[19000-19999]( Protobuf协议实现中进行了预留,从FieldDescriptor::kFirstReservedNumber 到 FieldDescriptor::kLastReservedNumber)的标识号。如果非要在.proto文件中使用预留标识符,编译时就会报警。
[1,15]内的标识号在编码的时候会占用一个字节。[16,2047]之内的标识号则占用2个字节。所以应该为频繁出现的消息元素保留[1,15]内的标识号。
6、字段规则
消息的字段修饰符必须是如下之一:
A、singular:一个格式良好的message应该有0个或者1个该字段(但不能超过1个)。
B、repeated:在一个格式良好的消息中,该字段可以重复任意多次(包括0次),重复值的顺序会被保留。
在proto3中,repeated的标量字段默认情况下使用packed。
7、保留标识符
如果通过删除或者注释所有字段,以后的用户在更新消息类型的时候可能重用标识符。如果使用旧版本代码加载相同的.proto文件会导致严重的问题,包括数据损坏、隐私错误等等。为了确保不会发生向前兼容可以为字段tag(reserved name可能会JSON序列化的问题)指定reserved标识符,Protobuf编译器会警告未来尝试使用相应字段标识符的用户。
不要在同一行reserved声明中同时声明字段名字和标识符。
8、默认值
当一个消息被解析的时候,如果编码消息里不包含一个特定的singular元素,被解析的对象所对应的字段被设置为一个默认值,不同类型默认值如下:
对于string,默认是一个空string
对于bytes,默认是一个空的bytes
对于bool,默认是false
对于数值类型,默认是0
对于枚举,默认是第一个定义的枚举值,必须为0
对于消息类型(message),字段没有被设置,确切的消息是根据语言确定的,通常情况下是对应语言中空列表。
对于标量消息字段,一旦消息被解析,就无法判断字段是被设置为默认值还是根本没有被设置,应该在定义消息类型时注意。
9、枚举
当定义一个消息类型时,需要为消息中的某个字段指定预定义值序列中的一个值,此时可以使用枚举定义预定以序列。如为Person消息添加一个PhoneType类型的字段,PhoneType类型的值可能是MOBILE,HOME,WORK。
每个枚举类型必须将其第一个类型映射为0。
可以通过allow_alias选项为true,将不同的枚举常量指定为相同的值,否则编译器会在别名的地方产生一个错误信息。
枚举常量必须在32位整型值的范围内。因为enum值是使用可变编码方式的,对负数不够高效,因此不推荐在enum中使用负数。
可以在一个消息定义的内部或外部定义枚举,枚举可以在.proto文件中的任何消息定义里重用。可以在一个消息中声明一个枚举类型,而在另一个不同的消息中使用枚举(采用MessageType.EnumType的语法格式)。
当对一个使用了枚举的.proto文件运行Protobuf编译器的时候,生成的代码中将有一个对应的enum(Java或C++),被用来在运行时生成的类中创建一系列的整型值符号常量(symbolic constants)。
在反序列化的过程中,无法识别的枚举值会被保存在消息中。对支持开放枚举类型超出指定范围外的语言(例如C++和Go),未识别的值会被表示成所支持的整型;对封闭枚举类型的语言中(Java),使用枚举中的一个类型来表示未识别的值,并且可以使用所支持整型来访问;在其它情况下,如果解析的消息被序列号,未识别的值将保持原样。
10、引用其它消息类型
可以将其它消息类型用作字段类型。对于同一个消息文件内部定义的消息,可以在其它消息内部直接引用消息类型;对于在其它消息文件定义的消息类型,可以通过导入其他消息文件中的定义来使用相应的消息类型。如使用google.protobuf.Timestamp消息类型需要导入相应消息文件:import "google/protobuf/timestamp.proto";
如果要在父消息类型的外部重用消息类型,需要以Parent.Type的形式使用。
11、Any类型
Any类型消息允许在没有指定.proto定义的情况下使用消息作为一个嵌套类型。一个Any类型包括一个可以被序列化bytes类型的任意消息以及一个URL作为一个全局标识符和解析消息类型。
为了使用Any类型,需要导入import google/protobuf/any.proto。
对于给定的消息类型的默认类型URL是type.googleapis.com/packagename.messagename
。
不同语言的实现会支持动态库以线程安全的方式去帮助封装或者解封装Any值。例如在java中,Any类型会有特殊的pack()和unpack()访问器,在C++中会有PackFrom()和UnpackTo()方法。
12、Oneof
Oneof定义用来代表在实现的时候,该组属性中有且只能有一个被定义,不能出现多个。
上述定义中只能出现name或者sub_message的出现,不能同时出现,同时Oneof不能出现repeated域。重复传递值到Oneof多个域仅仅最后的会生效,其它的将被忽略掉。
13、Map
如果要创建一个关联映射,Protobuf提供了一种快捷的语法:
其中key_type可以是任意Integer或者string类型(除了floating和bytes的任意标量类型都可以),value_type可以是任意类型,但不能是map类型。
例如,创建一个Project的映射,每个Projecct使用一个string作为key:
Map的字段可以是repeated。
序列化后的顺序和map迭代器的顺序是不确定的,所以不要期望以固定顺序处理Map
当为.proto文件产生生成文本格式的时候,map会按照key 的顺序排序,数值化的key会按照数值排序。
从序列化中解析或者融合时,如果有重复的key则后一个key不会被使用,当从文本格式中解析map时,如果存在重复的key。
向后兼容性问题
map语法序列化后等同于如下内容,因此即使是不支持map语法的Protobuf实现也可以处理数据:
14、定义服务
如果想要将消息类型用在RPC(远程方法调用)系统中,可以在.proto文件中定义一个RPC服务接口,Protobuf编译器将会根据所选择的不同语言生成服务接口代码及stub。如要定义一个RPC服务并具有一个方法Search,Search方法能够接收SearchRequest并返回一个SearchResponse,可以在.proto文件中进行如下定义:
最直观的使用Protobuf的RPC系统是gRPC,由谷歌开发的语言和平台中的开源的PRC系统,gRPC在使用Protobuf时非常有效,如果使用特殊的Protobuf插件可以直接从.proto文件中产生相关的RPC代码。
如果不想使用gRPC,可以使用Protobuf用于自己的RPC实现。
15、JSON映射
Proto3支持JSON的编码规范,便于在不同系统之间共享数据。
如果JSON编码的数据丢失或者其本身是null,数据会在解析成Protobuf的时候被表示成默认值。如果一个字段在Protobuf中表示为默认值,会在转化成JSON编码的时候忽略掉以节省空间。
16、更新消息类型
如果一个已有的消息格式已无法满足新的需求,需要在要息中添加一个额外的字段,但同时旧版本写的代码仍然可用。可以使用更新消息解决,更新消息而不破坏已有代码是非常简单的。更新消息时规则如下:
A、不要更改任何已有字段的标识符。
B、如果增加新的字段,使用旧格式的字段仍然可以被新产生的代码所解析。应该记住元素的默认值,新代码就可以以适当的方式和旧代码产生的数据交互。通过新代码产生的消息也可以被旧代码解析,但新增加的字段会被忽视掉。未被识别的字段会在反序列化的过程中丢弃掉,如果消息再被传递给新的代码,新的字段依然是不可用的。
C、非required的字段可以移除。只要标识符在新的消息类型中不再使用(推荐重命名字段,例如在字段前添加“OBSOLETE_”
前缀)。
D、int32, uint32, int64, uint64,和bool是全部兼容的,可以相互转换,而不会破坏向前、 向后的兼容性。
E、sint32和sint64是互相兼容的,但与其它整数类型不兼容。
F、string和bytes是兼容的——只要bytes是有效的UTF-8编码。
G、嵌套消息与bytes是兼容的——只要bytes包含该消息的一个编码过的版本。
H、fixed32与sfixed32是兼容的,fixed64与sfixed64是兼容的。
I、枚举类型与int32,uint32,int64和uint64相兼容(注意如果值不相兼容则会被截断),然而在客户端反序列化后可能会有不同的处理方式,例如,未识别的proto3枚举类型会被保留在消息中,但表示方式会依照语言而定。int类型的字段总会保留他们的
J、可以添加新的optional或repeated的字段, 但必须使用新的标识符(消息中从未使用过的标识符,不能使用已经被删除过的标识符)。
17、选项
在定义.proto文件时能够标注一系列的options。options并不改变整个文件声明的含义,但却能够影响特定环境下处理方式。完整的可用选项可以在google/protobuf/descriptor.proto找到。
一些选项是文件级别的,意味着它可以作用于最外范围,不包含在任何消息内部、enum或服务定义中。一些选项是消息级别的,意味着它可以用在消息定义的内部。当然有些选项可以作用在域、enum类型、enum值、服务类型及服务方法中。到目前为止,并没有一种有效的选项能作用于所有的类型
optimize_for(文件选项): 可以被设置为LITE_RUNTIME,SPEED,CODE_SIZE。这些值将通过如下的方式影响C及Java代码的生成:
SPEED (default): Protobuf编译器将通过在消息类型上执行序列化、语法分析及其它通用的操作,生成的代码最优。
CODE_SIZE:Protobuf编译器将会产生最少量的类,通过共享或基于反射的代码来实现序列化、语法分析及各种其它操作。采用CODE_SIZE方式产生的代码将比SPEED要少得多,但操作要相对慢些。CODE_SIZE方式生成代码中实现的类及其对外的API与SPEED模式都是一样的,常用在一些包含大量的.proto文件而且并不盲目追求速度的应用中。
LITE_RUNTIME:Protobuf编译器依赖于运行时核心类库来生成代码(即采用libprotobuf-lite替代libprotobuf)。libprotobuf-lite核心类库由于忽略了一些描述符及反射,要比全类库小得多。这种模式经常在移动手机平台应用多一些。编译器采用LITE_RUNTIME模式产生的方法实现与SPEED模式不相上下,产生的类通过实现MessageLite接口,但仅仅是Messager接口的一个子集。option optimize_for = CODE_SIZE;
cc_enable_arenas(文件选项):对于C产生的代码启用arena allocation。
objc_class_prefix(文件选项):设置Objective-C类的前缀,添加到所有Objective-C从此.proto文件产生的类和枚举类型。没有默认值,所使用的前缀应该是×××荐的3-5个大写字符,注意2个字节的前缀是苹果所保留的。
deprecated(字段选项):如果设置为true则表示该字段已经被废弃,并且不应该在新的代码中使用。在大多数语言中没有实际的意义。int32 old_field = 6 [deprecated=true];
java_package (file option):指定生成java类所在的包,如果在.proto文件中没有明确的声明java_package,会使用默认包名。不需要生成java代码时不起作用。
java_outer_classname (file option):指定生成Java类的名称,如果在.proto文件中没有明确声明java_outer_classname,生成的class名称将会根据.proto文件的名称采用驼峰式的命名方式进行生成。如(foo_bar.proto生成的java类名为FooBar.java),不需要生成java代码时不起任何作用
objc_class_prefix (file option): 指定Objective-C类前缀,会前置在所有类和枚举类型名之前。没有默认值,应该使用3-5个大写字母。注意所有2个字母的前缀是Apple保留的。
四、proto文件编码规范
Proto文件编码规范如下:
A、描述文件以.proto做为文件后缀。
B、除结构定义外的语句以分号结尾,结构定义包括:message、service、enum;rpc方法定义结尾的分号可有可无。
C、Message命名采用驼峰命名方式,字段命名采用小写字母加下划线分隔方式。
D、Enums类型名采用驼峰命名方式,字段命名采用大写字母加下划线分隔方式。
E、Service与rpc方法名统一采用驼峰式命名。
gRPC快速入门(二)——Protobuf序列化原理解析
https://blog.51cto.com/u_9291927/2332264
gRPC快速入门(二)——Protobuf序列化原理解析
一、Protobuf序列化原理简介
1、序列化
序列化是将数据结构或对象转换成二进制字节流的过程。
Protobuf对于不同的字段类型采用不同的编码方式和数据存储方式对消息字段进行序列化,以确保得到高效紧凑的数据压缩。
Protobuf序列化过程如下:
(1)判断每个字段是否有设置值,有值才进行编码。
(2)根据字段标识号与数据类型将字段值通过不同的编码方式进行编码。
(3)将编码后的数据块按照字段类型采用不同的数据存储方式封装成二进制数据流。
2、反序列化
反序列化是将在序列化过程中所生成的二进制字节流转换成数据结构或者对象的过程。
Protobuf反序列化过程如下:
(1)调用消息类的parseFrom(input)解析从输入流读入的二进制字节数据流。
(2)将解析出来的数据按照指定的格式读取到Java、C++、Phyton对应的结构类型中。
二、Protobuf编码方式
1、Varint编码
Varint编码是一种变长的编码方式,编码原理是用字节表示数字,值越小的数字,使用越少的字节数表示。因此,可以通过减少表示数字的字节数进行数据压缩。
对int32类型的数字,一般需要4个字节表示。如果采用Varint编码,对于很小的int32类型数字,则可以用1个字节来表示;虽然大的数字会需要5个字节来表示,但大多数情况下,消息都不会有很大的数字,所以采用Varint编码方式总是可以用更少的字节数来表示数字。
Varint编码后每个字节的最高位都有特殊含义:
A、如果是1,表示后续的字节也是数字的一部分。
B、如果是0,表示本字节是最后一个字节,且剩余7位都用来表示数字。
当使用Varint解码时时,只要读取到最高位为0的字节时,表示本字节是一个值经Varint编码后得到的字节流的最后一个字节。
在计算机内,负数一般会被表示为很大的整数 ,因为计算机定义负数的符号位为数字的最高位,如果采用Varint编码方式表示一个负数,那么一定需要5个byte(因为负数的最高位是1,会被当做很大的整数处理)
Protobuf定义了sint32 / sint64类型表示负数,通过先采用Zigzag编码(将有符号数转换成无符号数),再采用Varint编码,从而用于减少编码后的字节数。
对于一个int32类型的值300的Varint编码如下:
300的二进制编码为:100101100(256+32+8+4)
从字节流末尾取出7bit并在最高位增加1构成一个字节:[1]010 1100
从字节流末尾取出7bit并在最高位增加1构成一个字节,如果是最后一个字节增加0:[0]0000010
两字节为:[0]0000010 [1]010 1100
转换为小端模式:10101100 00000010
编码结果:1010 1100 0000 0010
2、Zigzag编码
Zigazg编码是一种变长的编码方式,其编码原理是使用无符号数来表示有符号数字,使得绝对值小的数字都可以采用较少字节来表示,特别对表示负数的数据能更好地进行数据压缩。
Zigzag编码对Varint编码在表示负数时不足的补充,从而更好的帮助Protobuf进行数据的压缩。因此,如果提前预知字段值是可能取负数的时候,需要采用sint32/sint64数据类型。
Protobuf通过Varint和Zigzag编码后,大大减少了字段值占用字节数。
-2的Zigzag过程如下:
三、Protobuf数据存储方式
1、T-L-V数据存储方式
T-L-V(Tag - Length - Value),即标识符-长度-字段值的存储方式,其原理是以标识符-长度-字段值表示单个数据,最终将所有数据拼接成一个字节流,从而实现数据存储的功能。
其中Length可选存储,如储存Varint编码数据就不需要存储Length,此时为T-V存储方式。
T-L-V 存储方式的优点:
A、不需要分隔符就能分隔开字段,减少了分隔符的使用。
B、各字段存储得非常紧凑,存储空间利用率非常高。
C、如果某个字段没有被设置字段值,那么该字段在序列化时的数据中是完全不存在的,即不需要编码,相应字段在解码时才会被设置为默认值。
2、T-V数据存储方式
消息字段的标识号、数据类型、字段值经过Protobuf采用Varint和Zigzag编码后,以T-V(Tag-Value)方式进行数据存储。
对于Varint与Zigzag编码方式编码的数据,省略了T-L-V中的字节长度Length。
Tag是消息字段标识符和数据类型经Varint与Zigzag编码后的值,因此Tag存储了字段的标识符(field_number)和数据类型(wire_type),即Tag = 字段数据类型(wire_type) + 标识号(field_number)。
Tag占用一个字节的长度(如果标识符大于15,则占用多一个字节的位置),字段数据类型(wire_type)占用3个bit,字段标识符(field_number)占用4个bit,最高位用于Varint编码保留。
解码时,Protobuf根据Tag将Value对应于消息中的字段。
对于Person消息的name字段的Tag编码如下:
根据Tag解码得到filed_number、wire_type:
四、Protobuf序列化原理解析
1、Protobuf序列化简介
Protobuf对于数据存储的三大原则:
(1)Protocol Buffer将消息中的每个字段进行编码后,利用T - L - V 存储方式进行数据的存储,最终得到一个二进制字节流。
(2)ProtoBuf对于不同数据类型采用不同的序列化方式(数据编码方式与数据存储方式)
Protobuf对于不同的字段类型采用不同的编码和数据存储方式对消息字段进行序列化,以确保得到高效紧凑的数据压缩。不同类型的数据采用的编码方式和存储方式如下:
对于Varint编码数据的存储,不需要存储字节长度Length,使用T-V存储方式进行存储;对于采用其它编码方式(如LENGTH_DELIMITED)的数据,使用T-L-V存储方式进行存储。
(3)ProtoBuf对于数据字段值的独特编码方式与T-L-V数据存储方式,使得 ProtoBuf序列化后数据量体积极小。
2、WireType=0的序列化
WireType=0的类型包括int32,int64,uint32,unint64,bool,enum以及sint32和sint64。
编码方式采用Varint编码(如果为负数,采用Zigzag辅助编码),数据存储方式使用T-V方式存储二进制字节流。
3、WireType=1的序列化
WireType=1的类型包括fixed64,sfixed64,double。
编码方式采用64bit编码(编码后数据大小为64bit,高位在后,低位在前),数据存储方式使用T-V方式存储二进制字节流。
4、WireType=2的序列化
WireType=2的类型包括string,bytes,嵌套消息,packed repeated字段。
对于编码方式,标识符Tag采用Varint编码,字节长度Length采用Varint编码,string类型字段值采用UTF-8编码,嵌套消息类型的字段值根据嵌套消息内部的字段数据类型进行选择,
数据存储方式使用T-L-V方式存储二进制字节流。
5、WireType=5的序列化
WireType=5的类型包括fixed32,sfixed32,float。
编码方式采用32bit编码(编码后数据大小为32bit,高位在后,低位在前),数据存储方式使用T-V方式存储二进制字节流。
五、Protobuf序列化示例
1、String类型
String类型字段的值使用UTF-8编码。消息数据流如下:
2、嵌套消息类型
嵌套消息类型采用T-L-V的存储方式,外部消息的V即为嵌套消息的字段
,在T-L-V的V中嵌套了一系列的T-L-V。
编码方式:字段值(即V)根据字段的数据类型采用不同编码方式。
3、通过packed修饰的 repeat 字段
如果序列化时对多个 T - V对存储(不带packed=true),则会导致Tag的冗余,即相同的Tag存储多次。
为了解决Tag数据冗余,采用带packed=true的repeated字段存储方式,即将相同的Tag只存储一次、添加repeated字段下所有字段值的长度Length、连续存储repeated字段值,组成一个大的Tag - Length - Value -Value -Value对,即T - L - V - V - V对。
通过采用带packed=true 的 repeated字段存储方式,从而更好地压缩序列化后的数据长度。
六、Protobuf使用建议
基于Protobuf序列化原理分析,为了有效降低序列化后数据量的大小,可以采用以下措施:
(1)多用 optional或 repeated修饰符
若optional 或 repeated 字段没有被设置字段值,那么该字段在序列化时的数据中是完全不存在的,即不需要进行编码,但相应的字段在解码时会被设置为默认值。
(2)字段标识号(Field_Number)尽量只使用1-15,且不要跳动使用
Tag是需要占字节空间的。如果Field_Number>16时,Field_Number的编码就会占用2个字节,那么Tag在编码时就会占用更多的字节;如果将字段标识号定义为连续递增的数值,将获得更好的编码和解码性能。
(3)若需要使用的字段值出现负数,请使用sint32/sint64,不要使用int32/int64。
采用sint32/sint64数据类型表示负数时,会先采用Zigzag编码再采用Varint编码,从而更加有效压缩数据。
(4)对于repeated字段,尽量增加packed=true修饰
增加packed=true修饰,repeated字段会采用连续数据存储方式,即T - L - V - V -V方式。
参考文献:
Carson_Ho:Protocol Buffer序列化原理大揭秘
gRPC快速入门(三)——Protobuf应用示例
https://blog.51cto.com/u_9291927/2332269
gRPC快速入门(三)——Protobuf应用示例
一、Protobuf使用流程
在工程开发中使用Protobuf流程如下:
(1)定义proto描述文件,以proto作为后缀名。
(2)使用Protobuf编译器protoc来生成编程语言代码文件,对消息格式以特定的语言方式描述。
(3)使用Protobuf库提供的API来编写应用程序 。
二、Protobuf C++示例
1、环境变量设置
2、定义proto文件
addressbook.proto文件如下:
3、生成C++代码
protoc --cpp_out=. addressbook.proto
生成C++代码addressbook.pb.h和addressbook.pb.cc。
addressbook.pb.h文件:
4、序列化接口
bool SerializeToString(string* output) const
序列化消息,将消息对象以string方式输出。bool ParseFromString(const string& data)
反序列化消息,解析给定的string为消息对象 bool SerializeToOstream(ostream* output) const
序列化消息,将消息对象写入给定的c++ ostream中bool ParseFromIstream(istream* input)
反序列化消息,从给定的c++ istream中解析出消息对象
5、使用示例
main.cpp文件:
6、运行结果测试
编译程序:g++ addressbook.pb.cc main.cpp -o readwrite
pkg-config --cflags --libs protobuf -std=c++11
运行程序:./readwrite book
结果如下:
三、Protobuf Go示例
1、定义proto文件
book/book.proto文件如下:
2、生成Go代码
protoc --go_out=. book.proto
生成book/book.pb.go文件,内容如下:
3、使用示例
main.go文件如下: