protobuf编解码原理

varints 编码

基本思想/基本逻辑

通常来说, 普通的int数据类型,无论其值的大小,所占用的存储空间都是相等的, 这点可以引起人们的思考“是否 可以根据数值的大小来动态地占用存储空间,使得值比较小的数字占用较少的字节数, 值比较大的数字占用比较多的字节数” 这就是  变长 整型编码的 基本思想。

Protobuf 中使用的是 base128 Varints 编码

varints 编码 使用每个字节的 最高有效位 作为标志位,而剩余的 7位 以二进制补码的 形式 来存储数字值本身, 当最高位有效位 为 1 时,代表其后还跟有字节, 最高有效位为0 时,代表 是 该数字的最后一个字节。

 

相对不好的点:

无法对一个序列的数值进行随机查找,因为每个数字所占用的存储空间不是等长的, 因此若要获得序列中的第N个数字,无法像等长存储那样 在查找之前 直接计算

出 offset, 只能从头开始 顺序查找。

缺点:

如果数字是 负数, 则采用varints 编码 会恒定占用 10个字节。

。。。

protobuf 内部将 int32 类型的 负数 转换为 uint64 来处理。

转换后的 uint64 数值的高位 全为 1, 相等于一个8字节的很大的无符号数, 因此采用 base128varints 编码后将恒定占用 10个字节的空间。

可见 varints 编码 对于 表示负数 毫无优势, 甚至比 普通的固定32位 存储还要多占 4个字节。 

zigzag 编码 就是为了解决这个问题。

zigzag 编码

zigZag 编码的大致思想是 首选对负数做一次变换,将其映射为一个正数,变换以后可以使用 varints编码进行压缩。

将负数 映射为一个 正数的 算法

假设n 是 32位类型的 数字, zigzag 编码的计算方式 为:

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

(n >> 1) - (n &1)         逆向

zigzag 编码在protobuf 中并不单独使用, 而是配合varints 编码 共同来进行数据压缩。

。。。

 

zigzag 编码+ varints 编码 在protobuf 运用

field_num 与 wire_type 对编码的影响

对于int32类型的数据,protobuf 都会转为 uint64 而后使用varints 编码来处理,因此当字段可能为负数时, 我们应使用 sint32 或 sint64,这样protobuf 会按照zigzag编码将数据变化后再

采用 varints 编码进行压缩,从而缩短数据的二进制位数。

sint32->   zigzag编码 + varints 编码 合起来。

与 json xml 等相比:

                   protobuf 不是一种完全自描述的 协议格式, 即接收端在 没有 proto文件 定义的前提下 是无法解码 一个protobuf 消息体的, 与此相对的,json, xml 等协议格式 是完全自描述的。  其实对于客户端 和服务端通信双方来说, 约定好消息格式之后 完全没有必要在每一条消息中都携带字段名称。

protobuf 除了存储字段的值之外, 还存储了字段的编号,以及字段在通信线路上的 格式类型(wire-type),具体的存储方式为:

field_num << 3 | wire type

有了字段编号 和 wire type ,其后所跟的数据的长度便是 确定的,因此protobuf 是一种非常紧密的数据组织格式, 其不需要特别地加入额外的分割符 来分割一个消息字段, 这可以大大地提升通信的效率,规避冗余的数据传输。

 

例子:看一下 protobuf 实际序列化之后的完整二进制数据。

 

 

 syntax = "proto3"; 

 package pbTest; 

 message Request { 

 int32 age_test = 1; 

 }

假设 age  为  5

        由于 age 在 proto 文件中 定义的是 int32 类型, 因此 序列化之后的 wire type  为 0,  

字段编号为1 

即 1 << 3 | 0  ,   占一个字节,即 00001000

后面跟上 字段值 5 的 varints 编码,所以整个结构体序列化之后为:

wire_type 不同值时的二进制结构

1. 当wire_type 等于0 的时候整个二进制结构为:

Tag-Value.

value 的编码 采用 varints 编码 ,故不需要额外的位来表示 整个value 的长度。 因为 varint的 msb 位 标示下一个字节 是否是  有效的 起到了 指示长度的作用。

 

2. 当wire_type 等于 1,5 的时候  二进制结构为:

 Tag-Value

因为 都是取固定的 32位, 或者 64位,因此也不需要额外的位 来表示整个value的长度。

 

3. 当 wire_type 等于 2的时候 整个二进制结构为:

Tag-[length]-Value

   因为表示的是可变长度的值,需要有额外的位来指示长度。

field_num 范围 与二进制编码

1. 1到 15,仅使用 1bytes

用一个字节表达 就是  0 0000 000    

第一位 表示 是否有后续字节, 如果 0  表示 没有,即就一个字节,蓝色部分表示 field-number, 绿色部分则是 wire_type 部分,表示数据类型。

 

2. 16  到 2047, 以两个bytes为例:

10000000 0 0000 000    

红色部分 依然是 符号位,  每个byte 的第一位都用来表示下 一byte 是否和自己有关, 表示field_num 大小的有 11位 ,能表达的数字最大值就是 2的 11次方 减 1 , 即 2047

例子

int32   n1_int32  = 16;

红色是 field_number 的 指示位, 蓝色 为 field_number的值, 绿色为 wire_type

 

16  -> 16进制:   80  01   01     二进制: 10000 000  00000001 00000001    即  0000001 0000  —》 得出来16

64 ->  16进制:  80   04  01     二进制: 10000 000  00000100 00000001   即   0000100 0000 —》 得出来 64

3000 -> 16进制   c0  bb  01 01   二进制   11000 000  10111011    00000001   00000001   即   0000001 0111011 1000 —》 得出来 3000

 

对于int32类型的数据,protobuf 都会转为 uint64 而后使用varints 编码来处理,因此当字段可能为负数时, 我们应使用 sint32 或 sint64,这样protobuf 会按照zigzag编码将数据变化后再

采用 varints 编码进行压缩,从而缩短数据的二进制位数。

 

protobuf 协议消息升级  带来的兼容性等问题

遇到查阅即可。

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值