首先,我们需要了解一种最基本的编码方式varints(原文档的单词,没有找到特别准确的翻译,所以就就保留英文),这是一种用1个或多个字节对Integer进行编码的方法
当一个数字采用这种方式编码后,除了最后一个字节,每一个字节的最高位都是1,而最后一个字节的最高位则是0,从而在解码的时候可以通过判断最高位的值来确定是否已经解码到了最后一个字节。
每一个字节除了最高位的其他7个bit则用来存放数字本身的编码
例如300,编码后得到2个字节
1010 1100 0000 0010
其中第一个字节最高位为1,表示后面还有字节需要一并进行解码。第二个字节最高位为0,则表示已经到达最后一个字节了
解码时
1.去掉2个字节的最高位
010 1100 000 0010
2.反转2个字节的顺序
000 0010 010 1100
3.连接2个字节,构成了300的二进制形式
100101100
在demo中,ProtobufTest.varintTest()方法还展示了91809的编码
10100001 11001101 00000101
去掉最高位
-》0100001 1001101 0000101
反转字节
-》0000101 1001101 0100001
合并
-》10110011010100001
然后,我们回顾之前定义的模型,其中包含1个int类型的字段
message Person {
required string name = 1;
required int32 id = 2;
...
}
在应用中,如果给一个person的对象的id赋值150,再进行编码后,会得到3个字节
16 -106 1
对应
00010000 10010110 00000001
其中后2个字节就是用varints编码后的150,各位可以根据前面所述进行验证
而第一个字节,就是用来表示该字段的类型和序号的
00010000
由于该字段的编码方式也是varints,所以最高位依然是表示是否到达最后一个字节
剩下的7位中
0010000
其中最低3位表示该字段的类型,类型表如下,000就表示Type 0 即用varint编码的数字
去掉最高位,和最低的3位,剩下4位
0010
表示2,对应于我们模型定义中的2
required int32 id = 2;
在demo中,ProtobufTest.varintTest()方法展示了上述编码
当仅有1个字节来表示“序号”与“类型”时,只有4位表示序号,因此所能表示的最大序号为1111,即15
如果该字段的序号大于15,则需要再增加一个字节。
例如如果我们给Person对象的序号为23的large字段同样赋值150,得到的结果是
-72 1 -106 1
10111000 00000001 10010110 00000001
同样,后2个字节表示150本身
前2个字节表示序号+类型
第一个字节的最后3位表示类型
000对应int32
剩下的10111 00000001
通过varints解码,得到23
0111 00000001
->0111 0000001
->0000001 0111
在demo中,ProtobufTest.protobufBaseEncodeTest()方法展示了上述编码
上面我们了解了protobuf的类型+序号,以及int类型数据的编码方式,接下去介绍其他基本类型的编码方式
string
编码形式为:字符串长度+字符串的Utf-8编码
例如我们给Person的largeStr赋值abc
-38 1 3 97 98 99
11011010 00000001 00000011 01100001 01100010 01100011
前2个字节表示序号27的字符串类型字段
第三个字节表示,字符串长度为3
后3个字节表示abc的utf-8编码
在demo中,ProtobufTest.protobufBaseEncodeTest()方法展示了上述编码
负数、uint和sint
对于有符号的整数,编码后的字段大小为固定的10个字节,即它被当做一个很大的无符号整数进行处理了
例如给Person的id赋值-3,得到
16 -3 -1 -1 -1 -1 -1 -1 -1 -1 1
00010000 11111101 11111111 11111111 11111111 11111111 11111111 11111111 11111111 11111111 00000001
第一个字节表示序号2的int类型,后面10个字节经过varints解码后,即可得到-3
在demo中,ProtobufTest.negativeIntTest()方法展示了上述编码
值得注意的是
如果指定的类型为uint(java中其实还是映射到int类型),此时当赋值为负数时,则固定的字节数为5个
如果指定的类型为sint(java中同样还是映射到int类型),此时,编码会采用一种被称为ZigZag的编码方式,它将每一个数字都映射到一个正数,从而不再采用固定长度的字节
例如,-1被映射为1,1被映射为2,-2被映射到3,2被映射到4,以此类推
对于int32类型,映射规则为
(n << 1) ^ (n >> 31)
对于int64类型,映射规则为
(n << 1) ^ (n >> 63)
其实通俗来讲就是,如果n>0,则映射为2n,如果n<0,则映射为-2n-1
在demo中,ProtobufTest.negativeUIntTest()和ProtobufTest.negativeSIntTest()方法展示了上述编码
double和float
对于double(fixed64)类型和float(fixed32)类型,采用的都是little-endian的编码方式,protobuf分别用类型1和类型5来标记,从而可以判断出接下去是需要64位(8个字节)还是32位(4个字节)参与解码
这里需要提一点,double对应到java的double,但是fixed64对应到java的还是int,只不过在编码时会固定占用8个字节
float和fixed32同理
这个具体的编码方式可以自行了解
在demo中,ProtobufTest.doubleAndFloatTest()方法展示了上述编码
子对象
标记的类型为2,下一个字节表示子对象的字节数,之后的字节就是子对象的编码,子对象的编码方式和主对象一致
例如demo中的embeddedTest
90 2 8 1
01011010 00000010 00001000 00000001
90表示序号为11,类型为2
2表示接下去的2个字节是数据
8和1则表示子对象
8表示子对象中序号为1,类型为0
1表示具体的数据1
在demo中,ProtobufTest.embeddedTest()方法展示了上述编码
repeated对象
如果是普通字符串或者子对象,那就是一个不断重复的类型2
例如demo中的repeatedTest
34 1 49 34 1 50 34 1 51
00100010 00000001 00110001 00100010 00000001 00110010 00100010 00000001 00110011
34表示类型序号为4,类型为2,则后面一个字节就表示数据长度为1,49就是具体的数据
第二组和第三组同理
如果是int类型,则会被打包到一起
98 3 1 2 3
01100010 00000011 00000001 00000010 00000011
98表示序号为12,类型为2,
3表示后面3个字节都是数据
1,2,3表示list中的数据
在demo中,ProtobufTest.repeatedStringTest()和ProtobufTest.repeatedIntTest()方法展示了上述编码
这时候如果思考一下就会发现一个问题,既然string和int的repeated类型都是2,那么该如何区分是该解析成数字还是解析成字符串呢?我目前的看法是,如果仅从二进制编码来看,是无法区分的,而protobuf在解码的时候不会出错,是通过什么方法保证的呢?
因为通过protobuf编译器生成模型的时候,相应的编码解码代码就已经在模型文件中了,因此是不需要通过字节数据进行判断的
再思考一个问题,为何int类型时可以打包的,但是string类型不行?
因为int类型采用varint编码方式,是可以自行寻找到int编码的开始和结束字节,而string类型则不行