Protobuf的研究理解

引导性问题抛出:

在java语言的API中有关于网络编程的socket封装套接字,有过java网络编程的都会了解,java语言有自己的网络数据传输方法,即内置的Serializable序列化接口,实现类的序列化,然后使用API中io下的对象流即可进行数据传输。而该方法的逻辑本质上就是一种简单的网路协议,所谓协议就是一种规则规范,让通讯双方能够知道对方传输来的信息应该如何解读。

而与之相似的还有XML和JSON,但是无论是哪一种数据存储或者数据传输的方法,都会有一些短板,要么受到语言环境的限制,要么编码体积不理想,传输效率低,或者兼容扩展性较差等。于是,protobuf诞生了。

 

  • 初识Protobuf

Protobuf,全称为Google Protocol Buffer,是一种轻便高效的结构化数据存储格式,可用于通讯协议和数据存储等主要方面。

Protobuf平台无关,语言无关,可扩展,并且提供了较为友好的动态库,使用简单。

核心也是采用了序列化和反序列化的思想。而其序列化逻辑采用了较为理想的编码和压缩方法,使得流数据非常简洁紧凑,存储字节量较为可观,解析速度较快!同时,protobuf在设计的时候考虑到了兼容问题,实现了较好的前后兼容性。

一个基本流程解释:

①首先,编写一个proto文件(.proto后缀),使用proto文件的语法构造协议规则数据

②其次,使用protoc(proto文件的编译器)生成对应语言的源码文件,以java为例,就是使用protoc将proto文件生成一个对应的.java文件

③最后,我们使用这个.java文件即可(当然也有相应的API可供使用)

 

  • Protobuf下proto文件编写的主要语法规则

Protobuf所谓的数据是以内部的消息(Message)定义实现的。

而消息中又有若干个字段,字段决定了真实的数据定义,而字段又有其格式。

在研究消息定义之前,我们先来看一下proto文件中的文件控制语法。

指定语法

有一个说明proto版本的语法:syntax=”protoX”,X代表版本

这个指定语法必须是文件的非空非注释的第一行内容。

文件选项

在消息定义前标注一系列的option,用来设置文件选项。

①java_package:这个选项表明proto文件生成java类所在的包

②java_outer_classname:生成的java类名

例如:option java_package = "com.test";

导入定义:

也就是当前proto文件下去使用已有的其他.proto文件中的定义。

例如:import  “other.proto”;

Package:

可以为.proto文件新增一个可选的package声明符,用来防止命名冲突。

该声明符会根据使用语言的不同影响生成的代码,在java中会变成一个包,除非在.proto文件中提供了具体的java_package文件选项。

(I)消息基本的字段定义

字段格式如下(以proto2的语法规则为例):

限定修饰符

数据类型

字段名称

=

字段标识编号

字段选项

例如:optional int32 id = 1 [default = 1];

限定修饰符

在proto2中,限定修饰符有三种且必须指定,包含如下:

①required:标识必需设置字段,发送方(或者说序列化前)必须设置该值。而对于接收方,则必须能够识别该字段。

②optional:标识是一个可选字段,发送方在发送消息的时候,可以有选择性的设置或不设置该值。对于接收方,如果能够识别,就进行相应的处理,如果无法识别,就直接忽略该字段,消息中的其他字段正常处理。

③repeated:标识该字段可以包含0或多个元素,其本质特性也是optional的,但是一次可以包含多个值(可以类比理解为一个动态的数组)。这里需要注意一下:基本数值类型的repeated的字段并没有被尽可能的高效编码。在编写过程中,应该使用附加的特殊选项[packed = true]来保证更高效的编码。

数据类型

Protobuf定义了一套基本数据类型,几乎都可以映射到java等语言的数据类型中。

而protobuf支持合成类型,包括枚举类型或其他嵌套消息类型。

数据类型

简单描述

编码方法

Java映射

备注

int32/64

有符号32/64位整数

非固定编码长度N

int/long

对于负数的编码效率较低

sint32/64

有符号32/64位整数

非固定编码长度N

int/long

对于负数的编码效率较高

uint32/64

无符号32/64位整数

非固定编码长度N

int/long①

 

fixed32/64

无符号

固定4/8字节编码

int/long①

数值总是比2^28/56大的话,会比uint32/64高效

sfixed32/64

有符号

固定4/8字节编码

int/long

 

bool

布尔类型

编码方式

详见下文中

关于字段类型所对应的编码方式

一表

boolean

 

float

32位浮点数

float

 

double

64位浮点数

double

 

string

字符串

String

必须是utf-8编码或者

7位ASCII编码的文本

bytes

字节数组

ByteString

包含任意顺序的字节数据

表格中的int/long①说明:Java中无符号的32/64位整数使用对应带符号的表示方法,最高位为符号

字段名称

字段名称也就是所谓的命名,相当于java中的变量名。

字段标识编号

其实,这个很容易理解,一个通信协议编写的核心逻辑,就是定义数据,保证通信双方能够识别对方发送过来的数据。而字段标识编号其实就相当于起到了这样一个作用。例如,发送方把一个定义required int32 id = 1的消息发送出去,当接收方收到数据后,读取出标识号为1,他就明白该字段是required的,类型为int32,命名为id。

下文在消息存储格式的key-value机制中会详细分析该字段标识号的逻辑。

需要注意的是:

①尽可能定义标识号在1~15内,此时的编码是最理想的(下文会分析到)。

②19000-19999是系统预留标识号,尽量不要自主使用定义。

③一个消息中各字段的标识号无需是连续的,只要确保其合法性。

④一个消息中各字段标识号唯一,不可重复。

字段选项

所谓的字段选项,就是增加一些控制设置,可有可无。

像上文中所涉及到的default和packed。语法格式以[]添加在字段最后。

(II)消息的合成字段定义

1.枚举类型

当需要定义一个消息类型的时候,我们有时候可能想为一个字段指定某个“预定义值序列”中的一个值。例如,定义一个人物的消息,其中的一个字段为人物的性别,而我们仅仅希望当前类型的取值,仅有“男”或者“女”的取值,而无关其他任何取值。而枚举类型很容易的实现了这样的一个结果。

2.消息嵌套

Protobuf的语法定义中是支持消息嵌套的,也就是一个消息的内部定义另外若干个消息或者使用另外的若干个消息。不过此处需要注意的就是,如果在父消息外部去使用一个嵌套消息,需要加上父消息的前缀。

用简单的例子看一下枚举和嵌套的使用:

Protobuf的研究理解_第1张图片Protobuf的研究理解_第2张图片

 

对上述的枚举类型进一步说明:

一个枚举类型的字段只能使用指定的常量集中的一个值作为其值(如果尝试指定其他的值,解析器就会把它当做一个位置的字段来对待)。

枚举常量的值必须是在32位整形值的范围内。下文会了解到,由于enum所采用的编码方式是varint可变编码方式,对负数不够高效,因此,尽可能不要对enum中的常量使用负数,以免造成性能上的影响。

(III)oneof

在阐述oneof之前,还是先思考一个问题:假如现在我们的消息定义中有很多可选字段,但是,现在想要若干可选字段中同时最多只能有一个被设置,应该怎么办呢?最直观的做法就是使用者自己本身有这个逻辑,知道某几个可选字段存在这种关系,但是会有很多弊端,例如,不好管理,浪费内存等。于是,proto提供了oneof语法。

Oneof字段限制了某些可选字段中同时最多一个字段可以被设置。Proto的做法很直接,直接让oneof语法下的可选字段共享内存,保证最多唯一性。

Protobuf的研究理解_第3张图片一个简单的oneof定义

Oneof的一些细节问题

①每次设置oneof中某一字段的值,就会覆盖之前的,所以oneof中设置的值永远是对oneof中字段的最后一次设置的值。

②oneof中的字段就是可选的,不能用限定修饰符修饰。

③oneof本身不支持repeated

④反射机制对oneof是有效的

(IV)更深层次的研究思考

1.optional的字段和默认值问题

以一个例子来测试:optional int32 id = 1 [default = 1];

我们来考虑这样一个问题,对于上述消息字段id的设置,假如此时发送方没有设置optional的值,然后其他字段正常,序列化后发送给接收方,会有什么情况呢?

通过测试,我们可以看到,在序列化的二进制流中是没有该字段的信息的,也就是相对于接收方和发送方之间的通信来说,id是缺省的。但是,我们调用接收方的getId()方法,结果得到了数值1。

所以说,对于optional字段来说,在解析消息的时候,如果二进制流中没有包含optional的元素值,那么对于解析方的对应字段就被设置为默认值。默认值可以在描述文件中指定,就像例子中的default。

那么,既然可以指定default,那如果不指定default会怎样呢?Protobuf对其采用的解决方法为,如果没有显式设置默认值,则会使用系统与特定类型关联的隐式默认值。对于string来说,默认值是空字符串。对bool来说,默认值是false,对数值类型来说,默认值是0,对enum类型来说,默认值是枚举类型定义中的第一个值。

2.新旧消息的兼容性问题分析

Protobuf是支持“向后兼容”和“向前兼容”的。

(a)讨论兼容性之前,我们先研究一下protobuf使用过程中,如果逻辑需求产生变更,我们应该怎么办?

为了更好的解决这个问题,protobuf中进行了下述规则的约束(以proto2为例)。

①不要更改任何已有字段的数值标识。

②非required字段可以移除,但是注意他们的标识号不能再被使用(除非可以绝对保证所有使用该消息的地方全部更新,这相对于仅不使用标识号是非常困难的)。很容易理解,如果移除该字段后,新增字段又使用了这个标识号,这样在旧消息的代码逻辑下,还是把新字段当成了原来被移除的那个字段,产生异常。个人觉得最好的做法,是对想要移除的字段进行重命名,而不是删除,因为删除以后,很可能会无意识的用到之前的标识号。

③更新proto采用的是添加新的字段的方式,这时要求所添加的任何字段都必须是optional或者repeated(本质还是optional)的。这就意味着任何使用旧消息格式的代码序列化的信息流可以被新的代码所解析(optional是可选的,相当于发送方没有设置optional)。类似的,新消息格式的序列化信息也能够被老的代码解析,老的二进制程序在解析的时候只是简单的将新字段忽略。这里需要强调一下,这些新字段只是被忽略(因为旧代码无法识别),但是不会被丢弃。如果旧代码再把消息序列化传给新代码,这些新的字段是仍然可用的。

(b)其实上述阐述中已经较为可观的解释了protobuf新旧消息版本的兼容性问题。

这里以一个实际的例子进行简单理解:我们用新旧版本来区别更改一个消息后和更改前。此处以新版本相较于旧版本增加了一个optional的字段为说明。

所谓的“向后兼容”,就是说,当模块B升级了以后,使用新版本消息,但是他依然能够正确识别模块A发出的旧版本的消息。

对应的“向前兼容”,就是说,当模块A升级了以后,使用新版本消息,模块B依然能够正确识别模块A发出的新版本的消息。

(c)这种兼容性为protobuf带来的好处:一句话概括,当你更新或者升级的时候,尽可能的保证整个项目的各个模块间的不受影响。

(d)protobuf还对各种数据类型的兼容性作出了说明:

①int32/64,uint32/64,和bool是全部兼容的,这意味着可以将这些类型中的一个转换成另外一个,而不会破坏兼容性。如果解析出来的数字与对应的类型不相符,那么结果就像在java中一样进行强制类型转换(高精度转低精度会截取导致数据丢失)

②sint32和sint64是互相兼容的,但是他们与其他整数类型不兼容

③string和bytes是兼容的,只要bytes是有效的UTF-8编码

④fixed32和sfixed32是兼容的,fixed64和sfixed64是兼容的

 

三.编码方式和压缩算法的深入学习

问题思考:

我们以Java语言的角度考虑这样的一个问题,java中对于基本类型int的编码方式为4个字节,那么,现在我们存储一个基本int整数1;此时会发现,整个4字节的数据,其实是毫无必要的,因为对于1这个简单数值,我们只需要1位存储位,当然了,为了计算机底层硬件的处理效率,我们最多不过使用1个字节就可以了。

因此,对于java中整个int在空间上的编码方法来看,其实是不够优秀的。那么我们是否可以对此进行优化呢?答案是肯定的。

其实,问题的产生在于,固定字长编码所导致的无信息冗余前置0,换句话说,我们存储一个1,要在二进制1前方加上31bit的前置0,以确保其32位的编码方式,而这31个0其实是毫无意义的。在某些方面,这无可厚非,但是如果作用于网络传输等即时交互的环境下传输数据,毫无疑问,其所产生的弊端会被无限放大!因为,越多的数据就会造成越长的传输时间和越复杂的传输条件,并且所传信息还是毫无价值的!

  1. Varints技术——非固定编码长度

Varint的引入

我们试着来解决上述所阐述的问题:

抛开上述java对于int的4字节固定编码长度,我们回到问题的本质,既然,很多时候会造成无端的前置0,我们摒弃这一做法,也就是说,我们就用最小能保证信息完整的字节数来保存整个数值。回到上述的例子,我们还是要存储1,此时我们就使用1个字节,此时的字节使用率将大大提高,并且字节冗余大大减少,尽管仍旧有7个前置0,但那只是为了保证数据在8bit/byte下的计算机处理!

上述思考看似解决了问题,其实有一个bug,那就是,如果每个数字仅仅是使用满足存储的字节数,那必然存在的问题就是其在不同情况下对于不同数值的编码长度不同,这就直接导致了致命的问题:数据编码后无法解码!

于是,问题被我们转化成了,在保证数据二进制存储最小化的可能下,如何对数值的长度进行解析!例如00000001  00000001,我们怎么知道这是两个1,还是一个257。

此时,varint编码技术对此作出了较为可观的处理方法!

Varint技术的原理:

Varint较为简单的把每个字节进行逻辑隔开,方法为单字节8bit,其中最高位字节保留一位标记位,而后七位存储真实数据。

标识位仅有两种情况:1标识后续字节仍是当前所解析数据的一部分,0标识当前字节为正在解析数据的最后一个字节。

我们来看一个简单的例子进行分析:10000001  00000001,这两个字节在解析的时候就能够知道所表示的是一个数据,因为第一个字节最高位为1,表示数据没有读完,下一个字节仍旧是当前所处理数据的一部分,第二个字节的最高位又标识为0,表示后续没有当前所处理数据的有效字节。而00000001  00000001,这两个字节在解析的时候就能够知道所表示的两个数据,因为字节最高位标识为0。

深入了解varint编码的详细流程:

先来看一下正数的处理方法:

我们直接研究一个encoding的例子:varint编码下对整数300(十进制)进行二进制处理 300的形式拆分:256+32+8+4

300的二进制表示:100101100(常规二进制,共9bits数据存储)

①varint的真实数据存储每字节只有七位,故我们把二进制拆分

此时的处理结果为:0000010(7bits)  0101100(7bits)

②varint对于数据的编码所采用的是little-endian方式(小端字节序,即低位字节在前)

此时的处理结果为:0101100(低字节在前)  0000010(高字节)

③最后补全所有单个字节的标识位,即除最后字节为0,其余全部为1

此时的处理结果为:10101100(8bits)  00000010(8bits)

由此我们得到了300的varint编码下的二进制表示为:10101100  00000010

而对于负数要复杂的多:

分析protobuf中varint对负数的处理,根据源码我们发现,varint对负数的操作最终都是以long类型处理的,即使该负数本身是int,也会进行转换。而对于这个负数的处理是当做一个很大的无符号整数处理的,占用10个字节。

深入分析varint编码处理所产生的问题:

基于上述逻辑阐述,其实已经看到了varint所带来的存储性能提升!

但是深入研究不难发现其处理算法中所产生的问题:

①实现了一定程度上的数据字节存储长度的压缩,但是额外使用了1bit/byte的标识量

很容易理解:原本4字节32位数据存储的空间被缩小为4*7=28位。

换句话说,当数据值接近当前类型的最大存储量时,该方法性能低于固定字节编码!

而这个临界量为当前存储的字节数*7.

所以,在varint编码技术下要时钟考虑到这个临界量的问题!

②对于负数的处理很不理想

如果使用varint表示一个负数,值无论是-1还是-2147483648,其编码后长度将始 终为10个字节,就如同对待一个很大的无符号整型一样。

  1. ZigZag技术

ZigZag技术的引入:其实正如上述对于varint编码的问题分析,zigzag在protobuf中的作用就是解决了varint编码下对于负数操作的问题解决。

ZigZag编码的原理:分析研究原理的本质,其实就是为了找到该技术是为了解决什么样的问题。ZigZag很巧妙的解决了负数操作时高位符号位所带来的问题,本质思路是把符号数统一映射到无符号数的一种编码方案如下表所示:

原始值

映射值

0

0

-1

1

1

2

-2

3

2

4

-3

5

表现在数学上的映射方法即为:

对于正数,映射后的值恰好是原始值的2倍。

对于负数,映射后的值是原始值绝对值的2倍减1。

当Zigzag对数值进行映射后,再统一进行varint技术编码。

ZigZag技术的具体实现方法:

①正数的处理:很简单,直接左移一位(位运算相较于算数运算要快),相当于乘2

  例:00000001 直接变为 00000010

②负数的处理:按照原理中的分析,应该是绝对值*2后再减1。

Zigzag中的逻辑为,把最高位符号位放到最后面,前面剩余所有位取反。

  例:(-11) 11110101 -> 11101011 -> 00010101(21)

在protobuf中像sint32,编码方式就是先zigzag映射后再进行varint编码!

  1. Length-delimited编码技术

Length-delimited的引入:考虑之前我们在研究varint的时候曾经涉及到这样一个问题,我们在varint中是使用自己单字节的最高位来实现对整个数据的长度的确认,并且也因此产生了一个弊端,就是每多一个字节就会浪费一个最高位的标识bit。那假如现在,我们需要存储一个57位的数据,假如使用varint编码,需要57/7=9byte,而这9byte所产生的9个最高位的标识量竟然已经超出了一个字节,这显然是不太理想的。

Length-delimited的原理:正如上述阐述,我们想要避免在数据字节数较大的情况下去浪费更多的标识位,又要知道当前数据的存储字节数。采用的方法是,先计算出当前存储数据的字节数,然后单独使用varint字节去存储这个数据的字节数。这样,在解析的时候,自然而然的就很明确的知道了数据整个需要解析的字节总数。

Length-delimited的具体实现:我们以存储一个string为例,具体数据为8个a,a的ASCII编码整形数值为97。于是,该string的编码结果应该是:

00001000(8) 后面跟着8个 01100001(97)

需要注意的是,最前面添加的保存字节数的数据所采用的是varint编码。

Length-delimited所带来的思考:

其实,上述已经提到过了,这种编码方式,很明显的至少额外需要一个字节来存储一个字节数量的数据,所以说,数据存储字节数较小时,是不太理想的。所以,protobuf中也很自然的将这种方法应用在了string,bytes等数据存储所需字节数量较大的类型上。

 

四.Protobuf中整个消息的存储格式

上述内容我们详细了解了针对不同类型所采用的不同的编码方式,我们也知道在protobuf中的核心基础就是message,而每个message中又有很多的字段,那一个消息中的若干个字段是怎么组合成一个消息,并且能够在反序列化的时候成功解析呢?

Key-Value机制:

Protobuf把消息中的每一个字段都作为一个连续二进制流中的一个field,而每个field又是由一个Key-Value对组成的。

Key

Value

field

Field

Field

Field

......

我们来深入分析一下这个Key-Value的原理:

①Key值的表示:Key 的序列化公式:tag <<< 3 | Type

以一个具体的例子来说明一下上述序列化逻辑的各项内容:

Proto文件中定义为:int32 id = 1;那么此时的key值为00001 000

其中,tag代表的是proto文件中所赋予的标识号,也就是例子中的1,Type代表的是当前类型在处理时候的二进制编码值(详见下表),int32所对应的是0(varint)。

字段类型

二进制编码类型

二进制编码值

int32/64,uint32/64,sint32/64,bool,enum

Varint(可变长度int)

0

fixed64,sfixed64,double

64bit固定长度

1

string,bytes,inner messages,packed repeated fields

Length-delimited

2

groups(不推荐使用 deprecated)

Start group

3

groups(不推荐使用 deprecated)

End group

4

fixed32,sfixed32,float

32bit固定长度

5

我们再来计算这个公式:tag无符号左移三位,然后逻辑或运算type

直观来说,就是一个字节最后三位存储type,而剩余位存储tag。注意整个Key的字节编码形式也是varint,故而真正存储tag的只有4位(以Key只有一个字节为例),此时我们也能够得到一个启示,我们在proto文件编写赋予字段编号的时候,尽可能的去使用[1,15]的范围,因为这样一个字节就可以完成了。

②Value值的表示:其实就是字段类型所对应的编码方式形成的二进制数据。

假设在上述例子中,我们把id的值设置为11,又因为int32类型所采用的是varint编码

所以很容易得到value的值为:0 0001011.

③反序列化下的key-value机制:

Protobuf的核心机制就是序列化和反序列化的逻辑,上述我们基本已经深入的了解了message的整个序列化的细节,那么当二进制流传输到接收方的时候,是怎么进行解析的呢?其实,就是这个key-value的机制所起到的作用。

当收到一个或者读取到二进制流后,首先去解析field中的key,从key中读取到type,也就读取到了后续value的编码方式,对应解析出value值,然后根据key中的tag确认所解析的value值是哪一个字段的,相应设置赋值即可。从这里也能看出我们编写proto文件时对tag的设置的一些细节,例如不能重复,尽可能取值1~15.

此处以一个例子分析一下上述内容:

 Protobuf的研究理解_第4张图片Protobuf的研究理解_第5张图片

 

上面所示代码:以proto2为例:即syntax=proto2;

左图为proto文件中消息的定义,分别使用了三种修饰符及不同的字段类型用于测试分析

右图为程序中对各个字段的赋值操作,其中repeated下student暂且添加了两个元素

逻辑分析:

首先,第一个字段组成的field域中key值应该为:0 0001 000,这也是当前消息二进制流下的第一个字节,从信息中能够得出编码方式为0(varint),唯一编号为1 。后面紧跟的字节应该为 10010110 00000001,反推varint编码,得到数值150.

经过①后,根据varint编码已经知道第一个字段数据已经读取完毕(上述已读最后一个字节的最高位标识为0),然后此时读取下一个字节,很明显,应该是下一个字段的Field域中的key值,0 0010 010,得到编码方式是2,此时也明白采用的是Length-delimited算法,那也就明白接下来要读的就是该编码算法下后续真正保存该字段数据的字节数,二进制为00000110,可以看到后续该数据下有三个字节,读取三个字节结束为:01101100,01101001,01110101这三个字节解析ASCII码恰好为108,105,117,对应为liu。

后续的解析仍旧是这个逻辑,只不过读repeated字段时候,每一个单独的repeated都有一个单独的field。比如例子中,repeated添加了两个内容,这两个内容应该是连续的两个独立的sint32的field。所以后续也能看到有两个字节一模一样,很明显是每个field单独计算了两个一样的key。00011000 10101011 00000010 00011000 10000000 00000001。这里需要注意的是proto2下默认repeated的编码方式不是packed,下文在proto2和proto3的版本差异性中会进行对比。

 

  • Proto3和Proto2之间的版本差异

1.指定语法标记

proto3版本的protoc编译器已经可以支持proto2语法和proto3语法,如果proto文件中没有添加版本说明syntax的话,proto3版本的编译器会报错,提示默认proto2支持,请添加语法标记syntax = ”proto2”;

2.对限定修饰符的更改

①proto3的语法规则中去除了required和optional这两个限定修饰符。

②用singular取代了optional,且singular为proto3中默认缺省的。

③保留了required修饰符,且无需指定,默认packed编码。

3.Packed问题

在proto3中repeated修饰符下的字段默认采用packed编码

在proto2中repeated需要明确[packed = true]来设置packed编码

4.移除了default选项

在proto2中,可以使用default选项为某一字段指定默认值

在proto3中,字段的默认值只能根据字段类型由系统隐式指定

深入思考:proto3的这种改变造成了一种什么样的结果?

①首先,proto3中字段被设置为默认值的时候,该字段不需要被序列化,这样可以提高效率。理由很简单,在这种情况下,即使序列化,在被接收方反序列化得到结果后,该结果和接收方直接设置默认值是一致的。由此也产生了下面的问题。

②由上述①中的情况会产生一个新的问题,当设置值等同于系统默认值不被序列化的时候,其实是无法区分某字段是根本没赋值,还是赋值了一个等同于默认值的值。

5.枚举类型的系统默认值一定为0

这是一种语法强制约定,在proto3的语法规则下,要求枚举类型的默认值为0.

Proto2中,枚举类型的默认值是定义的第一个常量的值,但是没有限制为0.

Proto3中,强制限定了定义枚举类型的第一个常量的值必须是0.

6.保留标识符--Reserved

在上述讨论消息更新的时候,我们研究过这样一种情况。我们可以在更新消息的时候移除非required修饰的字段,但是即使移除了这些字段,该字段使用过的标识编号也不能再被使用(为了保证兼容性)。但是,这些字段已经被移除了,更多时候,我们很可能无意识的使用到这些标识编号,或者其他人使用到,而产生问题。

Proto3增添了保留标识符Reserved,其实就是确保避免发生上述问题,这种方法就是为移除的字段所使用的标识编号指定reserved标识符。Protocol buffer的编译器会警告未来尝试使用这些标识符的用户。

在proto3下用一个实际的例子进行测试:

Protobuf的研究理解_第6张图片

 

从上述例子中能够看到,编译器会提示,name字段使用到了保留标识符2是不合法的。

7.新增支持Map语法

参考Protocol Buffer第三个版本的使用手册:当需要创建一个关系映射的时候,proto3提供了快捷语法:map mpField = N;

其中keyType可以是任意Integer或者string类型,valueType可以是任意类型

兼容性问题

Map语法序列化后等同于下述内容,因此不支持map的实现也是可以处理的。

Protobuf的研究理解_第7张图片

 

上述语法中的keyType和valueType是实际映射关系中的key-value类型。

8.支持JSON序列化

9.增加了更多种的语言支持

最后附上自己学习理解时候的proto文件和java代码。

有关protobuf下载和安装,以及编译转化为java文件的流程自行查找办法!

.proto文件

//test.proto
//作者:***
//功能:该文件综合各种proto常见语法定义,旨在配合java语言练习使用

	syntax = "proto3";	//选择最新的版本proto3
	
	//设置文件选项
	option java_package = "com.test";
	option java_outer_classname = "MyTestProto";
	
	//定义消息,尽可能的覆盖proto使用语法
	message Info2
	{
		int32 id = 1;				//可选标量类型id
		repeated int32 id2 = 2;		//重复标量类型id2,proto3下默认采用packed编码
		
		enum Sex	//定义枚举类型Sex
		{
			MAN = 0;	//以0为第一个默认值
			WOMAN = 1;
		}
		Sex sex = 3;	//使用枚举类型
		
		oneof PayWay	//使用oneof
		{
			int32 money = 4;
			int32 card = 5;
		}
		
		message BigName	//消息的内部嵌套
		{	
			string name = 1;
		}
		BigName bigName = 6;	//使用内部嵌套消息
		
		reserved 8,9;	//reserved保留标识编号8和9
	}

Java语言下的简单测试代码:

package com.test;

import java.io.FileInputStream;
import java.io.FileOutputStream;

/*
* 作者:***
* 功能:练习使用protobuf在java环境下的API以及基本使用方法
*/
public class MyTestProtoCode
{
    public static void main(String [] args)
    {
        //MyTestProto是proto文件解析后的java类
        //而Info2则是proto文件中所定义的messages所对应的java类

        //java语言中是以构造器的方法创建指定的消息类的
        MyTestProto.Info2.Builder builder = MyTestProto.Info2.newBuilder();
        builder.setId(1999);
        builder.addId2(10);
        builder.addId2(11);
        builder.setSexValue(1);

        builder.setMoney(150);
        builder.setCard(150);

        //消息的内部嵌套
        MyTestProto.Info2.BigName.Builder bigNameBuilder = MyTestProto.Info2.BigName.newBuilder();
        bigNameBuilder.setName("liu");
        builder.setBigName(bigNameBuilder.build());

        //创建一个消息类
        MyTestProto.Info2 info2 = builder.build();
        //以文件流为例,测试proto消息的序列化和反序列化
        FileOutputStream fileOutputStream = null;
        try
        {
            //protobuf序列化写进文件
            fileOutputStream = new FileOutputStream("protoGet.bin");
            info2.writeTo(fileOutputStream);
            fileOutputStream.close();

            //得到其序列化的二进制内容
            byte[] result = info2.toByteArray();
            int byteLength = result.length;
            for(int i = 0; i < byteLength; i++)
            {
                System.out.print(Integer.toHexString(result[i]) + "  ");
            }
            System.out.println();

            //从文件流中读取
            MyTestProto.Info2 infoCopy = info2.parseFrom(new FileInputStream("protoGet.bin"));
            //打印出反序列化的信息进行比对
            //这里的println打印结果表明info对toString进行了输出内容的逻辑重写
            System.out.println(infoCopy);
            //System.out.println(infoCopy.getMoney());
            //System.out.println(infoCopy.getCard());
        } catch (Exception e)
        {
            e.printStackTrace();
        }

    }
}

 

你可能感兴趣的:(Protobuf,protocol,buffer)