google protocol buffer全解析(三)

339 阅读25分钟

第二部分,Protobuf编码原理

首先,我们需要了解一种最基本的编码方式varints(原文档的单词,没有找到特别准确的翻译,所以就就保留英文),这是一种用1个或多个字节对Integer进行编码的方法

当一个Integer采用这种方式编码后,除了最后一个字节,每一个字节的最高位都是1,而最后一个字节的最高位则是0,从而在解码的时候可以通过判断最高位的值来确定是否已经解码到了最后一个字节。

每一个字节除了最高位的其他7个bit则用来存放数字本身的编码

例如300,编码后得到2个字节,红色表示最高位bit,蓝色表示数字本身编码

1010 1100  0000 0010

其中第一个字节最高位bit为1,表示后面还有字节需要一并进行解码。第二个字节最高位bit为0,则表示已经到达最后一个字节了

解码时

1.去掉2个字节的最高位

010 1100  000 0010

2.反转2个字节的顺序

000 0010  010 1100

3.连接2个字节,构成了300的二进制形式

100101100

接着我们来看一个实际的例子,编码一个Person对象,只给里面的id字段赋值

/**
 * varint数字编码
 */
@Test
void varintTest() {
    BasicUsage.Person person = BasicUsage.Person.newBuilder()
            .setId(91809)
            .build();
    Utility.printByte(person.toByteArray());
}

输出的编码结果如下

16    -95    -51    5    
00010000 10100001 11001101 00000101 

其中黄色部分即是91809的varints编码,我们来验证一下

红色表示最高位,蓝色表示数字本身编码,在读取该部分字节的时候是一个一个读取的

读取到第一个字节时,发现最高位是1,因此会继续读取第二个字节,第二个字节最高位也是1,因此继续读取第三个字节,而第三个字节最高位为0,从而结束读取,就处理这3个字节

10100001 11001101 00000101 

1.去掉3个字节的最高位

0100001 1001101 0000101 

2.反转3个字节的顺序

0000101 1001101 0100001

3.连接3个字节,构成了91809的二进制形式

10110011010100001

接着我们看person编码结果的第一个字节

16    -95    -51    5    
00010000 10100001 11001101 00000101 

这个字节表示的是数据的序号类型,编码方式也是varient,因此我将其分为3个部分

00010000

红色0为最高位bit,表示是否解析到了本次varient的最后一个字节

中间蓝色的4个bit 0010表示序号,十进制2,即id的序号

最后3个黄色底的0为该字段的类型,000表示int32类型

此时一个最简单的protobuf的编码就解析完成了

到这里我们先总结一下protobuf编码的性质,将特别抽象的的内容转换成一个我们可以直觉理解的东西

先看原始数据,如果用json表示出来就是如下形式

{
    "id": 91890
}

而protobuf编码后的数据格式如下

00010000 10100001 11001101 00000101

其中第一个字节表示序号和字段类型,即序号为2,类型为int的字段

后三个字节表示数据的值,值为91890

这时候就会有这样一个问题,那id这个字段名去哪儿了?

答案就是,id的字段名被protobuf舍弃了!

所以,protobuf最终的编码结果是抛弃了所有的字段名,仅仅保留了字段的序号、类型和数据的值。

因此在第一篇文章的开头,就提到protobuf并非是一种可以完全自解释的编码格式,意思就是如此。

也正因为如此,所以我也认为这个序号正是protobuf编码的灵魂所在

有了这个概念之后,我们就可以解释之前5个示例了

**示例1: **protobuf的解码不需要类型相同,也不需要字段名相同

因为protobuf编码后的结果根本就不包含类的信息,也不包含字段名的信息,因此解码的时候自然也就不依赖于类和字段名 **示例2: **protobuf的解码依赖于序号的正确性

因为编码后的结果的序号和类型是在同一个字节中,是一一对应的关系,如果编码的对应关系和解码的对应关系不同,则自然编码和解码的过程会出问题

**示例3: **protobuf中的序号大小会影响最终编码大小

我们前面看到序号和字段类型的字节结构如下,表示序号的部分是中间的4个bit,0010

00010000

而4个bit所能表示的最大数是1111,也就是15,因此当序号大于15的时候,一个字节就不够表达了,就需要额外一个字节,例如序号为17,类型为int的字段,它的序号字节就会如下

10001000 00000001

其中黄色底的000表示类型Int,去除后,剩下的bit通过标准的varient解码后,得到的结果就是17

因此,如果序号超过15,那么就会多需要一个字节来表示序号。回过头看示例3,model3编码结果正好比model1编码结果多3个字节,正是3个字段的序号导致的

**示例4: **protobuf的对象类型可以向String类型兼容

上面提到了int的类型在字节中的bit表示是000,那么接下去我么可以看下其他类型对应的bit表示

TypeMeaningUsed For
0Varintint32, int64, uint32, uint64, sint32, sint64, bool, enum
164-bitfixed64, sfixed64, double
2Length-delimitedstring, bytes, embedded messages, packed repeated fields
3Start groupgroups (deprecated)
4End groupgroups (deprecated)
532-bitfixed32, sfixed32, float

这里可以看到,0就是表示int32,表达方式是varient

而2则可以表示string、embedded messages等,而这里的embedded messages对应的就是子对象

既然类型的表示是相同的,那么在解码的时候自然就是可以从embeedded messages向string兼容

然而由于messages的结构是要比string复杂的,因此反向是无法兼容的

其实这个更广域和普世来说,总是复杂信息可以向简单信息转换,而反向一般是不可行的

示例5 protobuf可以和json完全兼容,且编码大小要比json小

兼容性是由java类库实现的,这个不在编码原理的范畴内,这里主要看下编码大小比json小的原因

例如示例中的json

{"email":"personJson@google.com","id":1,"name":"personJson"}

json的编码后,为了保证格式的正确和自解释的功能,其中还包含了很多格式字符,包括{ " , }等,还包括了email、id、name字段名本身

而protobuf编码后,则仅仅保留了序号、类型,以及字段的值,没有任何其他额外的符号,因此就比json节省了很多字节数

最后总结下这一大部分的内容,通过5个示例展示了protobuf在使用上的一些特性,并通过基本的编码原理解释了特性的本质原因

特性有以下5点

1.protobuf的解码不需要类型相同,也不需要字段名相同

2.protobuf的解码依赖于序号的正确性

3.protobuf中的序号大小会影响最终编码大小

4.protobuf的对象类型可以向String类型兼容

5.protobuf可以和json完全兼容,且编码大小要比json小

我们通过一些示例了解了protobuf的使用特性,以及和这些特性相关的基础编码原理。

编码原理只开了个头,所以接着将继续展示protobuf剩余的编码原理

在之前,我们只是定义了一些非常简单的模型,其中只包含了string、int和一个Name对象,所以我们首先先定义一个更复杂的模型

.proto文件如下

syntax = "proto3";
​
option java_package = "cn.tera.protobuf.model";
option java_outer_classname = "ProtobufStudent";
​
message Student{
  int32 age = 1;
  int64 hairCount = 2;
  bool isMale = 3;
  string name = 4;
  double height = 5;
  float weight = 6;
  Parent father = 7;
  Parent mother = 8;
  repeated string friends = 9;
  repeated Hobby hobbies = 10;
  Color hairColor = 11;
  bytes scores = 12;
  uint32 uage = 13;
  sint32 sage = 14;
}
​
message Parent {
  string name = 1;
  int32 age = 2;
}
​
message Hobby {
  string name = 1;
  int32 cost = 2;
}
​
enum Color {
  BLACK = 0;
  RED = 1;
  YELLOW = 2;
}

相比之前定义的模型,这里新增了int64,bool,double,float,repeated,enum,uint,sint类型

repeated类型对应的是java中的list

protobuf将这些具体的类型分为了几个大类,如下面这个表格所示

TypeMeaningUsed For
0Varintint32, int64, uint32, uint64, sint32, sint64, bool, enum
164-bitfixed64, sfixed64, double
2Length-delimitedstring, bytes, embedded messages, packed repeated fields
532-bitfixed32, sfixed32, float

接着我们就通过实例来看下每种数据结构的编码方式

1.Varint

这种类型的数据,在序号字节中的类型部分表示为000,即表格中的Type字段0

首先我们看最简单的4种类型,protobuf类型为int32、int64、bool、enum,模型中对应这种类型的字段是age、hairCount、isMale、hairColor,因此我们分别给这4个字段赋值

age测试代码

/**
 * protobuf基础编码,varint类型
 */
@Test
void protobufBaseEncodeTest() {
    ProtobufStudent.Student student = ProtobufStudent.Student.newBuilder()
            .setAge(15)
    Utility.printByte(student.toByteArray());
}

输出结果

8    15    
00001000 00001111

这里复习一下之前中关于protobuf的编码基础

第一个字节表示字段的序号和类型

黄色底000,表示该数据类型是varint

蓝色0001,表示序号为1

红色0,表示序号解析到了最后一个字节

第二个字节表示数字的值15

通过varint解码后,即是15

hairCount测试代码

@Test
void protobufBaseEncodeTest() {
    ProtobufStudent.Student student = ProtobufStudent.Student.newBuilder()
            .setHairCount(239281373231123L)
            .build();
    Utility.printByte(student.toByteArray());
}

输出结果

16    -109    -16    -126    -54    -128    -76    54    
00010000 10010011 11110000 10000010 11001010 10000000 10110100 00110110 

第一个字节表示字段的序号和类型

黄色底000,表示该数据类型是varint

蓝色0010,表示序号为2

红色0,表示序号解析到了最后一个字节

后面7个字节,通过varint解码后,即是239281373231123L

isMale测试代码

@Test
void protobufBaseEncodeTest() {
    ProtobufStudent.Student student = ProtobufStudent.Student.newBuilder()
            .setIsMale(true)
            .build();
    Utility.printByte(student.toByteArray());
}

输出结果

24    1    
00011000 00000001 

序号字节结构和之前一样

这里因为赋值的是true,所以值是1,如果赋值是false的话,那么该字段就不会被编码了(因为bool类型默认就是false)

hairColor测试代码

@Test
void protobufBaseEncodeTest() {
    ProtobufStudent.Student student = ProtobufStudent.Student.newBuilder()
            .setHairColor(ProtobufStudent.Color.RED)
            .build();
    Utility.printByte(student.toByteArray());
}

输出结果

88    1    
01011000 00000001 

序号字节结构和之前一样,这里因为赋值的是Color.RED,我们查看枚举值表即为1,如果赋值的是Color.BLACK,则该字段将不会被编码(因为int类型默认值就是0)

上面4个例子是可以通过正数就可以表达的类型,接着我们看对于有符号的正数,protobuf是如何表达的

protobuf类型为int32、uint32、sint32,对应模型中的age、uage、sage (这里注意,虽然在.proto文件中我们分了3个类型进行定义,但最终映射到java的类型都是int)

负数age测试代码

/**
 * protobuf基础编码,有符号的整数
 */
@Test
void negativeIntTest() {
    ProtobufStudent.Student student = ProtobufStudent.Student.newBuilder()
            .setAge(-7)
            .build();
    Utility.printByte(student.toByteArray());
}

输出结果

8    -1    -1    -1    -1    -1    -1    -1    -1    -1    1    
00001000 11111001 11111111 11111111 11111111 11111111 11111111 11111111 11111111 11111111 00000001 

可以看到数据体占用了10个字节,通过varint解码后就可以得到-7

因为一般负数的二进制结果都是采用正数补码的形式存储,所以protobuf使用了一个长度固定为10个字节的空间对负数进行编码,即使是-7也需要10个字节进行存储,其实是十分不合理的,因此我们看下uint和sint的表现

uage测试代码

/**
 * protobuf基础编码,有符号的整数
 */
@Test
void negativeIntTest() {
    ProtobufStudent.Student student = ProtobufStudent.Student.newBuilder()
            .setUage(-7)
            .build();
    Utility.printByte(student.toByteArray());
}

输出结果

104    -1    -1    -1    -1    15    
01101000 11111001 11111111 11111111 11111111 00001111 

如果定义为uint32的话,那么固定的数据存储空间则会缩减为5个字节

sage测试代码

/**
 * protobuf基础编码,有符号的整数
 */
@Test
void negativeIntTest() {
    ProtobufStudent.Student student = ProtobufStudent.Student.newBuilder()
            .setSage(-7)
            .build();
    Utility.printByte(student.toByteArray());
}

输出结果

112    13    
01110000 00001101 

粗看一下还不错,至少是用一个字节就表示了,但是仔细观察就会发现,我们传入的数字明明是-7,但是编码结果却是13

原因在于如果我们定义的是sint32,那么protobuf会采用一种叫做ZigZag的编码方式,即一种数据的映射,表格如下

Signed OriginalEncoded As
00
-11
12
-23
24
......

即设需要编码的整数为n:如果n>=0,则映射为2n;如果n<0,则映射为-2n-1

用代码来表示的话:

对于int32类型,映射规则为

(n << 1) ^ (n >> 31)

对于int64类型,映射规则为

(n << 1) ^ (n >> 63)

映射之后,再将映射的值通过varint的方式编码成字节

回过头看-7,对应的映射正是13,因此编码结果中也就是13

当然采取这种ZigZag进行映射后,对于负数编码所需的空间会减少,但对于正数的编码结果则会多出1个bit(看一下映射规则,如果n>=0,则映射为2n)

因此综上测试结果,如果我们能够预知在使用的过程中会遇到负数,那么从编码结果字节数的角度来说,采用sint定义.proto的字段将会是一个更优的选择

这里总结一下Varint的编码方式,首先由一个序号字节标识字段的序号和类型,数据体无论是int、long、bool、enum,因为其最终总能用一个数字表示,因此他们都能统一地通过varint进行编码,所以这种数据类型的分类就叫Varint

对于正数来说,直接通过varint编码即可

而对于负数来说,int会采用固定的10个字节对补码进行varint编码,uint会采用固定的5个字节对补码进行varint编码,而sint则是采用ZigZag的映射将负数映射成一个正数后再进行varint编码

顺带一提,在进行varint解码的时候,我们会发现它是需要将字节顺序反转之后才能解析出我们需要的数字(这里详解见protobuf的使用特性及编码原理),而这种反转存放的形式正是小端存储,即little-endian,这部分有兴趣的同学可以自己再去了解一下

2.64-bit和32-bit

这里我将64bit和32bit放到一起,因为他们之间的编码区别仅在于最终字节数量的不同,而编码原理是一样的

这两种类型的数据,在序号字节中的类型部分表示为001和101,即表格中的Type字段0和5

模型中对应这种类型的字段是height、weight因此我们分别给这2个字段赋值

height测试代码

/**
 * protobuf基础编码,double和float类型
 */
@Test
void protobufBaseEncodeTestDoubleAndFloat() {
    ProtobufStudent.Student student = ProtobufStudent.Student.newBuilder()
            .setHeight(99.6)
            .build();
    Utility.printByte(student.toByteArray());
}

输出结果

41    102    102    102    102    102    -26    88    64    
00101001 01100110 01100110 01100110 01100110 01100110 11100110 01011000 01000000 

第一个字节表示字段的序号和类型

黄色底001,即十进制的1,表示该数据类型64-bit

蓝色0101,表示序号为5

红色0,表示序号解析到了最后一个字节

后续的8个字节正是99.6的二进制表达形式,protobuf采用的标准是IEEE754。不过因为该标准的内容比较复杂,可以单独成文,所以就不放在这里展开了,本文还是专注于protobuf本身。

这里提供两个网址

维基百科:zh.wikipedia.org/wiki/IEEE_7…

线上转换:www.binaryconvert.com/result_doub…

我们进入线上转换站点,输入99.6,得到结果

img

和varint类型数据的存储方式一样,这里采用的也是小端存储(little-endian),因此将图中的字节反转之后,即可得到我们前面代码输出的内容 weight测试代码

/**
 * protobuf基础编码,double和float类型
 */
@Test
void protobufBaseEncodeTestDoubleAndFloat() {
    ProtobufStudent.Student student = ProtobufStudent.Student.newBuilder()
            .setWeight(99.6F)
            .build();
    Utility.printByte(student.toByteArray());
}

输出结果

53    51    51    -57    66    
00110101 00110011 00110011 11000111 01000010 

序号字节结构和double一样,只不过表示类型的3个bit是101,即十进制的5,表示bit-32类型

后面4个字节,也是float类型的IEEE754标准编码,并且采用小端存储的方式,我们可以再去转换验证一下,如下图

img

这里总结一下bit64和bit32的编码方式:和varint一样,通过一个序号字节来标识序号和类型,而数据体是long和double类型,采用的都是IEEE754标准的编码方式,并且字节是小端存储

3.Length-delimited

这种类型的数据,在序号字节中的类型部分表示为010,即表格中的Type字段2

从名字上来看,就是“长度限定”的意思,也就是说这种类型的数据都是需要指定长度的

包括string, 字节数组, 子对象,list,对应我们模型中的name、scores、father、friends

name测试代码

/**
 * protobuf基础编码,LengthDelimited类型
 */
@Test
void protobufBaseEncodeTestLengthDelimited() {
    ProtobufStudent.Student student = ProtobufStudent.Student.newBuilder()
            .setName("tera")
            .build();
    Utility.printByte(student.toByteArray());
}

输出结果

34    4    116    101    114    97    
00100010 00000100 01110100 01100101 01110010 01100001 

第一个字节表示字段的序号和类型

黄色底010,即十进制的2,表示该数据类型Length-delimited

蓝色0100,表示序号为4

红色0,表示序号解析到了最后一个字节

之前提到这种类型的名称叫“长度限定”,因此和varint以及64bit类型的数据相比,这里会额外多出一个字节,代表后续数据的字节长度,用粉色底表示

这里我们看到长度为00000100,即十进制的4,也就是说后面4个字节是实际的数据

而"tera"4个英文字母对应的utf-8字节正是01110100 01100101 01110010 01100001

scores测试代码

/**
 * protobuf基础编码,LengthDelimited类型
 */
@Test
void protobufBaseEncodeTestLengthDelimited() {
    ProtobufStudent.Student student = ProtobufStudent.Student.newBuilder()
            .setScores(ByteString.copyFrom(new byte[200]))
            .build();
    Utility.printByte(student.toByteArray());
}

输出结果

98    -56    1    0    0   ...   0     
01100010 11001000 00000001 00000000 00000000 ... 00000000

因为数组的长度我设定为200,所以就用...表示中间省略的输出

序号字节结构和之前一样

第二和第三个字节为长度字节,编码方式为varint,解码后得到11001000,即十进制的200,表示后面200个字节都是数据

没有给byte数组赋值,所以数据的值默认都是0

friends测试代码

/**
 * protobuf基础编码,LengthDelimited类型
 */
@Test
void protobufBaseEncodeTestLengthDelimited() {
    ProtobufStudent.Student student = ProtobufStudent.Student.newBuilder()
            .addFriends("a")
            .addFriends("b")
            .build();
    Utility.printByte(student.toByteArray());
}

输出结果

74    1    97    74    1    98    
01001010 00000001 01100001 01001010 00000001 01100010 

因为这是一个list,因此其中包含了多个数据段

第一和第四个字节分别表示2个数据的序号和类型,特别注意的是,因为这2个数据是属于同一个list的,所以序号都是1001,即十进制的9

第二和第五个字节表示数据的长度,因为我这里只是添加了a和b,所以数据长度都是1

第三和第六个字节表示数据的值,也就是a和b对应的utf-8编码

father测试代码

/**
 * protobuf基础编码,lengthDelimited类型
 */
@Test
void protobufBaseEncodeTestLengthDelimited() {
    ProtobufStudent.Student student = ProtobufStudent.Student.newBuilder()
            .setFather(ProtobufStudent.Parent.newBuilder()
                    .setName("MrTera"))
            .build();
    Utility.printByte(student.toByteArray());
}

输出结果

58    8    10    6    77    114    84    101    114    97    
00111010 00001000 00001010 00000110 01001101 01110010 01010100 01100101 01110010 01100001  

第一个字节为序号和类型,表示序号7,Length-Delimited类型的数据

第二个字节为十进制的8,即表示后面8个字节就是数据体

这里特别注意,因为father字段本身就是一个Parent对象,所以这个数据体本身就是一个完整的protobuf的数据结构

此时我们就可以将后8个字节看成一个独立的protobuf结构

第三个字节为father中的字段序号和类型,表示序号1,Length-Delimited类型的数据,注意,这里的序号1代表的是father字段的Parent类中序号为1

第四个字节为father中的数据长度,表示后面6个字节为数据体

最后6个字节即为字符串"MrTera"的utf-8编码

这里总结一下Length-Delimited类型的数据,同样通过一个序号字节来标识序号和类型,额外有字节标识数据体的长度。

对于string和byte数组的数据体,以utf-8的形式进行编码

而对于子对象来说,数据体就是一个独立完整的protobuf结构

对于list来说,则根据其内容的不同,分别采用直接utf-8或者完整protobuf结构编码

这里额外展示一个示例,用list存储对象,我将bit相应的含义都用颜色标识好,具体的含义留给读者自行分析,从而可以更好地理解protobuf的编码原理

/**
 * protobuf基础编码,lengthDelimited类型
 */
@Test
void protobufBaseEncodeTestLengthDelimited() {
    ProtobufStudent.Student student = ProtobufStudent.Student.newBuilder()
            .addHobbies(ProtobufStudent.Hobby.newBuilder().setName("a"))
            .addHobbies(ProtobufStudent.Hobby.newBuilder().setName("b"))
            .build();
    Utility.printByte(student.toByteArray());
}

输出结果

82    3    10    1    97    82    3    10    1    98    
01010010 00000011 00001010 00000001 01100001 01010010 00000011 00001010 00000001 01100010 

最后让我们把前面所讲到的编码原理结合到一起,看一下一个相对完整的编码结果

测试代码

/**
 * 一个相对完整的模型
 */
@Test
void entireModelTest() {
    ProtobufStudent.Student student = ProtobufStudent.Student.newBuilder()
            .setAge(12)
            .setName("tera")
            .setIsMale(true)
            .setFather(ProtobufStudent.Parent.newBuilder()
                    .setName("MrTera"))
            .addFriends("peter")
            .build();
    Utility.printByte(student.toByteArray());
}

输出结果

8    12    24    1    34    4    116    101    114    97    58    8    10    6    77    114    84    101    114    97    74    5    112    101    116    101    114    
00001000 00001100 00011000 00000001 00100010 00000100 01110100 01100101 01110010 01100001 00111010 00001000 00001010 
00000110 01001101 01110010 01010100 01100101 01110010 01100001 01001010 00000101 01110000 01100101 01110100 01100101 01110010 

整个结果就是之前每一组单独示例的结果拼合到一起,这里我大致标识下每个字节的意义,序号字节就不细分了,而是全部采用黄色背景,其他颜色和之前的都一致。有兴趣的同学可以再仔细分析一下

此时我们考虑这样一个问题,对于所属大类相同,但是实际类型不同的数据,在解码的时候该如何区分?

例如,我们编码如下一组数据

/**
 * 数据类型的分辨
 */
@Test
void differDatatype() {
    ProtobufStudent.Student student = ProtobufStudent.Student.newBuilder()
            .setAge(14)
            .setSage(-7)
            .build();
    Utility.printByte(student.toByteArray());
}

输出结果

8    14    112    14    
00001000 00001110 01110000 00001110 

可以看到age和sage的在编码后的值都是00001110,而且数据类型都是000(varint),唯一有区别的仅仅是序号

那么当我们仅仅拿到这4个字节的时候,是完全无法将编码后的数据还原成原始数据的!!

任何大类型相同,而实际类型不同的数据都有可能发生这种情况,例如字符串和字节数组

/**
 * 数据类型的分辨
 */
@Test
void differDatatype() {
    ProtobufStudent.Student student = ProtobufStudent.Student.newBuilder()
            .setName("aaa")
            .setScores(ByteString.copyFrom(new byte[]{97, 97, 97}))
            .build();
    Utility.printByte(student.toByteArray());
}

输出结果

34    3    97    97    97    98    3    97    97    97    
00100010 00000011 01100001 01100001 01100001 01100010 00000011 01100001 01100001 01100001

可以看到数据类型、长度、值都是相同的

这里再次强调,protobuf是一个不可以自解释的编码方式,除了字段的名字被抛弃之外,数据解析的歧义性也是其不可自解释的原因之一

所以protobuf的正确解析必须依赖于我们在编译.proto文件时,自动生成的那个巨大的java文件

也正是因为这个protobuf的不可自解释,在我们传递protobuf编码的时候,数据发送方和数据接收方都必须有相同的.proto文件,其中的字段序号必须一一对应,一旦有偏差,那么最终的解析一定是有问题的,再次说明序号是protobuf编码的灵魂所在

最后总结一下protobuf的编码原理

1.最重要的序号字节,每一个数据段都必须有该字节;标识字段的序号和类型;类型共有3种大类型;

2.varint,可以转化为int类型的数据,包括各种int32、int64等整型相关类型以及bool、enum,编码方式为varint,字节顺序为小端存储

3.double和float,浮点类型数据,编码方式为IEEE754标准,字节顺序为小端存储

4.Length-Delimited类型,需要指定数据体长度的类型,包括string、list、byte数组、子对象等,除了序号字节和数据内容字节,还需要额外的字节标识数据的长度

5.对于大类型相同,而小类型不同的数据,在解码的时候是通过编译时生成的java代码对其进行区分,仅依靠字节数据本身是无法区分的