最近面试中被问protobuf的加解码原理,非常的尴尬,因为我没了解过,这里稍作总结
proto文件
每一个proto文件其实对应着我们正常的一个模型(也就是model),只是proto文件是用来描述这样的一个模型的文件,并非实际上的工程model。
举个例子,文件Person.proto:
message Person{
required int32 id = 1;
optional string query = 2;
}
来看看这个文件,proto使用message表示一个消息格式,类似于一般编程语言中的class,struct。
- Person 描述模型的名称
- id 和 query描述字段名称
- int32 和s tring用来描述字段类型
- 1 和 2用来解码时标志对应的字段,
- required和optional表示字段的规则
proto使用流程
这里就不介绍使用方式了,简单说一下proto文件的使用流程,为下面讲解编解码原理做一下铺垫
- 编写proto文件
- 使用工具将proto文件转成对应语言的model文件(Person.proto->Person.swift)
- 安装google提供的三方库protobuf
- 调用转换出来的Person.swift文件中的encode和decode方法完成编解码等各类工作
protobuf编解码
接下来讲一下重点的内容
protobuf中整型数据的编码
protobuf中采用Varints编码对整型数据进行编码,经过Varints编码后的数据,它的每一个字节的高位是一个标记位。如果标记位是1,则代表下一个字节仍然是当前整型数据的组成;如果标记位是0,则代表当前数据结束了。
举个例子:
->00000001 表示数字1,因为首位是标记为表示只需要当前字节就能表示数据了,后七位就是这个值
->10101100 00000010 表示数字300,为什么,首先看两个字节的最高位,1和0,1表示该下一个字节还是属于这个数字的一部分,0表示这个数据到此子节表示结束,因此真实的数据是0101100 0000010 由于采用的是小端序,所以实际计算位0000010 0101100为 256+32+8+4 = 300
还有负数编码,因为负数采用补码,比如-1,表示成11111111,会导致每次数值都需要字节拉满才能表示的了(用Variant编码),所以对于负数protobuf采用ZigZag编码公式编码以后再使用Varint编码。
protobuf整体的编码
protobuf的消息是经过编码序列化的一系列key-value对,一个类型的数据对应一个key-value。value就是原始数据经过编码后的数据,而key由field number和wire type组成。其中field number是指.proto文件中定义的数据变量的field值(也就是前面proto格式中的1和2)。wire type用于区分字段值的类型(用于区分string,int32)
即:
wire_type | type |
---|---|
0 | int32、int64、uint32、uint64、sint32、sint64、bool、enum |
1 | fixed、sfixed64、double |
2 | string、bytes、embedded、messages、packed repeated fields |
举个例子:
key = (field_number << 3) | wire_type
比如00001000,后三位表示wiretype = 0,前五位使用Varint编码,因此field number为1,也就是id这个字段
如果我们给id设置一个值150,那么对应的编码就是
08 96 01
-> 00001000 10010110 00000001
-> wiretype = 0,field number为1即字段为id以及 0000001 0010110 = 128+16+4+2 =150
protobuf中字符串的编码
字符串本身使用UTF8编码,整个字符串字段使用key-len-value形式存储,
protobuf中嵌套消息的编码
类似于字符串,采用key-len-value来存储,len表示该字段对应的消息所需要的消息长度
归总
相对来说protobuf跟json的编码完全不同,一个编码到字符,一个编码到字节,因此速度和数据量上有一个质的飞跃