序列化优化-分析与设计

[在此处输入文章标题]

 

 

 

 

 

摘要:

网络传输中的字节流,分析进一步优化的空间

 

 

问题发现:

在对现有网络数据抓包分析过程中, 发现存在”00\ 00\ 00”空字节的重复序列, 抓包数据如下:

 

统计数据如下:

 

 

 

”00\ 00\ 00”对于信息编码而言,未表示有价值的数据. 但是否能剔除这种冗余的连续空字节, 需要通过分析序列化协议,研究出现的原因, 从而针对性的解决冗余的连续空字节.

 

 

 

序列化过程, 用数学模型表示:

Fx=Lx+ n=0x  Tn+Vn   

函数符号解释:

  1. L(x): 写入长度
  2. T(n): 写入该成员的Tag
  3. V(n): 写入该成员的值

 

写入长度的公式如下:

Lx=INT=4 

说明:

  1. 将长度的值作为int类型, 以4个字节的方式写入

 

 

写入Tag的公式如下:

Tn=Shiftn+WT(n)=USHORT=2 

说明:

  1. 具体运算为Shift(n)对索引左移3位, 腾出的3位用来存放类型
  2. 运算后的数据以ushort类型,以2字节方式写入

 

 

对于不同的成员类型, V(x)的公式分别如下:

 

容器类型:

Vx=Lx+ Sx+WT(x)+ n=1xVn  

  1. L(x): 总占用长度, 以int存储4字节
  2. S(x): 元素的个数, 以int存储, 4字节
  3. WT(x): 元素的类型
    1. map有两种元素, 的k和v分别1字节, 共2字节
    2. list只有一种元素, 固定1字节表示

 

 

String/buffer类型:

Vs=Ls+ s 

 

整形:

Vi=Varint(ZigZigi)   

说明:

  1. 对于整形, 使用ZigZig首位变换后的Varint变长编码

 

浮点型:

Vf=f 

 

 

对于”00\ 00\ 00”字节流的分析:

通过协议分析, 可以发现, 连续的3个\0字节, 出现在使用4个字节整形上. 固定的写int的地方为写长度, 也就是正整数, 以下分析以正整数uint进行.

 

操作系统中,最小的存储的单元为字节, 以char表示, 由8位二进制组成.

在linux系统中, uint在内存空间中以小端存储, 则int在4个连续字节上, 可以表示为

fuint(x)= n=03fcharn8*3-n 

 

1个字节可表示的数字, 最大为 f(bit=8)= (2bit)-1= (28)-1=511 

以此类推, 连续的字节数,逐个表示的范围为:

fx=fx*8bit=(28*x)-1=(512x)-1 

 

 

通过分析发现, 当x<=511时, 4个连续的字节, 仅使用1个字节, 其他三个连续字节为”00\ 00\ 00”

 

由此可见, 对于用整形存储的长度, 如果用使用现有的varint重新编码, 可以减少空字节的浪费.

 

由于长度不可能为负值, 即为正整数, 则无须通过ZigZig变换, 直接使用varint编码即可.

Varint编码的规则, 对于正整数, 将每个字节的首位用来表示编码连续性, 首位为0则表示编码结束.

则一个varint字节最大可以表示 f(bit=7)= (2bit)-1= (27)-1=127 

尤其varint编码特性, 当使用4个字节时, 能表示的数字为:

f(bit=7*4)= (2bit)-1= (228)-1=268435457 

超过该数字, 则需要5个字节才能存储.

 

 

使用varint替换直接用int表示长度可行性分析:

 

对于消息协议处理, 反序列化时, 顺序读取字节流, 保持与写入时同样的规则即可读取.

而序列化时, 能够正确序列化依赖于两个条件:

  1. 明确的序列化规则
  2. 拿到待序列化的值

 

 

现在针对长度字段, 分析是否可行.

 

分析条件1: 明确的序列化规则, 由以上对varint编码分析可以保证协议正确.

 

分析条件2: 拿到待序列化的值: 对于长度, 按照是否在序列化时就已经知道值, 有以下划分:

 

序列化时已经知道值的整形:

  1. string/buffer类型的长度
  2. map/list容器类型的元素个数
  3. Tag标记的值

 

序列化时不知道整形值的类型:

  1. map/list类型的总长度
  2. 复合类型的总长度

 

序列化时不知道值的原因分析:

对于一个完整的消息的序列化过程, 从以上数学公式

Fx=Lx+ n=0x  Tn+Vn   

可推导出: 只有当每个成员V(n)写入的长度确定时, 才能确定F(x)的写入长度

=> 必须V(n)对于每种类型都可直接获取长度

=> 每种成员类型, 没有变量保持该成员的长度, 从而无法直接返回长度, 必须计算获得

=> 必须通过一次运行时运算

 

对于运行时计算每个成员的长度从而获取总长度, 可以参考谷歌的protobuf是如何解决这个问题:

  1. 用protoc生成的序列化代码, 生成类的运算长度代码, 且添加额外成员变量记录长度
  2. 序列化前, 递归展开计算每个复合类长度, 作为成员
  3. 序列化时直接从复合类的成员上,获取长度

 

 

框架中的序列化, 暂时不考虑动态的计算复合类型和容器类型的长度,

仅考虑可直接获取长度使用varint编码后的可压缩率.

 

 

对于成员序列化的数学模型, 考虑导致导致类字节流膨胀的条件:

  1. 类成员为复合类型或容器类型, 复合类型的成员或容器元素的类型, 同样为复合或容器类型

 

再做一次简化, 类的成员为容器类型, 容器元素继续为容器类型

 

Fx=Lx+ n=0x  Tn+Vn =Lx+ n=0x  Tn+{Ln+Sn+yn}=Lx+ n=0x  Tn+Ln+Sn+ j=0n  Lj+Sj+V(j)     

 

设定类X, 只有一个成员K, 类型为list, 容器元素个数为a, 元素类型为list. 元素设定为相同的Y

元素Y特性: 类型为list, 容器元素个数依然为a, 元素类型为list, 元素设定为相同的Z

设定一共嵌套b层, 则可以推导:

常量数据, 合并为U(x)

 

Fx=Lx+Tx+Fk=Ux+Fk=U+ La+ n=0a  Vn =U+L(a)+ n=0aLa+ n=0a  Vn     

 

对公式再做一次抽象, 省略常量, 每个容器需要写入长度为L, 则

fx=L+afx-1=L+aL+afx-2=L+aL+a2Lfx-2=La0+a1+…+an-1+anf0=La0+a1+…+an-1+anL=La0+a1+…+an=L( (an+1-1)/(a-1) ) 

 

 

设置压缩前, L=4, 压缩后, a<511, 则L=1

压缩前, frx=4an+1-1a-1=4U 

压缩后fox=an+1-1a-1=U 

 

 

代码测试:

设计数据结构A, 内含一个成员list, 容器元素为list, 容器个数为100, 容器嵌套层次为3, 查看字节流压缩率, 与压缩时cpu消耗对比

 

数据记录:

 

 

 

总字节数

3个连续空字节个数(粗略)

压缩前

172020

7601

压缩后

131414

6888

 

数据分析:

压缩比率为(172020-131414)/ 172020 = 23.6%
压缩比率比与理论分析, 存在差距, 差距分析

具体代码中, 仅能将list中的容器个数size做压缩, size的个数为:

100*100 = 10000, 可压缩的字节数为:

4 * 10000 – 1 * 10000 = 30000

具体压缩的字节数 172020-131414 = 40606

多压缩了 40606-30000 = 10606

 

多压缩的字节数, 为压缩Tag, 此前Tag需要2字节, 用变长varint存储, 由于index<<3 || wt < 127, 仅用一个字节

三层嵌套, 则含有100*100个容器的容器, 一共有 10000 ~= 10606

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

悟世者

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值