protocalbuffer序列化原理

以前项目中使用过protocolbuffer进行序列化,当时就只是使用了protocalbuffer的工具生成了一个类的序列化工具。今天研究公司的序列化项目,发现应该是借用protocalbuffer的序列化思想来实现的,特地研究一下protocalbuffer的序列化实现原理。html

主要思想

将对象的字段的类型字段名序列化为tag表示,值序列化为value。框架

存储时按照t-l-v的顺序,先序列化tag类型标签,在记录length长度,在记录value值。 固定长度是不须要length。工具

protocalbuffer序列化方式

tag

记录字段的标识号(fieldnumber)和 数据类型(wiretype),即Tag = 字段数据类型(wiretype) + 标识号(fieldnumber) 占用 一个字节 的长度(若是标识号超过了16,则占用多一个字节的位置)性能

具体实现编码

// Tag 的具体表达式以下

Tag = (fieldnumber << 3) | wiretype 

// 参数说明:

// fieldnumber:对应于 .proto文件中消息字段的标识号,表示这是消息里的第几个字段 

// fieldnumber << 3:表示 fieldnumber = 将 Tag的二进制表示 右移三位 后的值 

// fieldnum左移3位不会致使数据丢失,由于表示范围仍是足够大地去表示消息里的字段数目

// wiretype:表示 字段 的数据类型

// wiretype = Tag的二进制表示 的最低三位值 

// wiretype的取值 enum WireType { WIRETYPEVARINT = 0, WIRETYPEFIXED64 = 1, WIRETYPELENGTHDELIMITED = 2, WIRETYPESTARTGROUP = 3, WIRETYPEENDGROUP = 4, WIRETYPEFIXED32 = 5 };

// 从上面能够看出,wire_type最多占用 3位 的内存空间(由于 3位 足以表示 0-5 的二进制)

先介绍一下最主要的两种编码方式,在对字段名和类型的标识中会用到这两种编码方式。spa

writetype=0时的序列化

主要用到了varint类型的编码和zigzag编码code

varint编码

一种变长的编码方式htm

varint编码

主要思想就是:每一个字节的第一位标识后面本字节是不是当前value的最后一个字节,剩下七位表示value的值,因此若是值很小的时候能够用一个字节就能表示。对象

不足之处

由于一般序列化的value值都比较小,因此一般用vartint编码均可以减小字节长度,可是若是值比较大就会占用到5个字节。负数由于首位的符号位是1,因此若是用varint标识负数都会使用5个字节。

zigzag编码

一种变长的编码方式blog

zigzag编码

主要思想就是:先使用ZigZig编码将负数映射成正数,而后再使用Varint编码。

实际数字的表示以下图所示:

zigzag实例

Wire Type = 1& 5时的序列化

编码后的数据具有固定大小 = 64位(8字节) / 32位(4字节),都是高位在后,低位在前,采用t-v方式存储。

Wire Type = 2时的序列化

对length使用varint方式编码,对于value值为string类型的使用utf-8方式编码。对于value值为message类型的再根据message的类型进行选择。

message为嵌套类型时,根据嵌套的类型应该用上述的类型序列化均可以解决。

message为repeat类型时不带packed=true,会致使Tag的冗余,即相同的Tag存储屡次。

使用建议

根据上面的序列化原理分析,我总结出如下Protocol Buffer的使用建议

  • 建议1:多用 optionalrepeated修饰符
    由于若optionalrepeated 字段没有被设置字段值,那么该字段在序列化时的数据中是彻底不存在的,即不须要进行编码

    相应的字段在解码时才会被设置为默认值

  • 建议2:字段标识号(Field_Number)尽可能只使用 1-15,且不要跳动使用
    由于Tag里的Field_Number是须要占字节空间的。若是Field_Number>16时,Field_Number的编码就会占用2个字节,那么Tag在编码时也就会占用更多的字节;若是将字段标识号定义为连续递增的数值,将得到更好的编码和解码性能

  • 建议3:若须要使用的字段值出现负数,请使用 sint32 / sint64,不要使用int32 / int64
    由于采用sint32 / sint64数据类型表示负数时,会先采用Zigzag编码再采用Varint编码,从而更加有效压缩数据

  • 建议4:对于repeated字段,尽可能增长packed=true修饰
    由于加了packed=true修饰repeated字段采用连续数据存储方式,即T - L - V - V -V方式

序列化 & 反序列化过程

序列化过程以下:
1. 判断每一个字段是否有设置值,有值才进行编码
2. 根据 字段标识号&数据类型 将 字段值 经过不一样的编码方式进行编码

反序列化过程以下:
1. 调用 消息类的 parseFrom(input) 解析从输入流读入的二进制字节数据流
2. 将解析出来的数据 按照指定的格式读取到 JavaC++Phyton 对应的结构类型中

因为:
a. 编解码方式简单(只须要简单的数学运算 = 位移等等)
b. 采用 Protocol Buffer 自身的框架代码 和 编译器 共同完成

因此Protocol Buffer的序列化和反序列化速度很是快。

发现一篇文章https://halfrost.com/protobuf_encode/

负数在计算机中的表示-128https://www.cnblogs.com/daxin/p/4093644.html

相关文章
相关标签/搜索