[在此处输入文章标题]
摘要:
网络传输中的字节流,分析进一步优化的空间
问题发现:
在对现有网络数据抓包分析过程中, 发现存在”00\ 00\ 00”空字节的重复序列, 抓包数据如下:
统计数据如下:
”00\ 00\ 00”对于信息编码而言,未表示有价值的数据. 但是否能剔除这种冗余的连续空字节, 需要通过分析序列化协议,研究出现的原因, 从而针对性的解决冗余的连续空字节.
序列化过程, 用数学模型表示:
Fx=Lx+ n=0x Tn+Vn
函数符号解释:
- L(x): 写入长度
- T(n): 写入该成员的Tag
- V(n): 写入该成员的值
写入长度的公式如下:
Lx=INT=4
说明:
- 将长度的值作为int类型, 以4个字节的方式写入
写入Tag的公式如下:
Tn=Shiftn+WT(n)=USHORT=2
说明:
- 具体运算为Shift(n)对索引左移3位, 腾出的3位用来存放类型
- 运算后的数据以ushort类型,以2字节方式写入
对于不同的成员类型, V(x)的公式分别如下:
容器类型:
Vx=Lx+ Sx+WT(x)+ n=1xVn
- L(x): 总占用长度, 以int存储4字节
- S(x): 元素的个数, 以int存储, 4字节
- WT(x): 元素的类型
- map有两种元素, 的k和v分别1字节, 共2字节
- list只有一种元素, 固定1字节表示
String/buffer类型:
Vs=Ls+ s
整形:
Vi=Varint(ZigZigi)
说明:
- 对于整形, 使用ZigZig首位变换后的Varint变长编码
浮点型:
Vf=f
对于”00\ 00\ 00”字节流的分析:
通过协议分析, 可以发现, 连续的3个\0字节, 出现在使用4个字节整形上. 固定的写int的地方为写长度, 也就是正整数, 以下分析以正整数uint进行.
操作系统中,最小的存储的单元为字节, 以char表示, 由8位二进制组成.
在linux系统中, uint在内存空间中以小端存储, 则int在4个连续字节上, 可以表示为
fuint(x)= n=03fcharn≪8*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: 明确的序列化规则, 由以上对varint编码分析可以保证协议正确.
分析条件2: 拿到待序列化的值: 对于长度, 按照是否在序列化时就已经知道值, 有以下划分:
序列化时已经知道值的整形:
- string/buffer类型的长度
- map/list容器类型的元素个数
- Tag标记的值
序列化时不知道整形值的类型:
- map/list类型的总长度
- 复合类型的总长度
序列化时不知道值的原因分析:
对于一个完整的消息的序列化过程, 从以上数学公式
Fx=Lx+ n=0x Tn+Vn
可推导出: 只有当每个成员V(n)写入的长度确定时, 才能确定F(x)的写入长度
=> 必须V(n)对于每种类型都可直接获取长度
=> 每种成员类型, 没有变量保持该成员的长度, 从而无法直接返回长度, 必须计算获得
=> 必须通过一次运行时运算
对于运行时计算每个成员的长度从而获取总长度, 可以参考谷歌的protobuf是如何解决这个问题:
- 用protoc生成的序列化代码, 生成类的运算长度代码, 且添加额外成员变量记录长度
- 序列化前, 递归展开计算每个复合类长度, 作为成员
- 序列化时直接从复合类的成员上,获取长度
框架中的序列化, 暂时不考虑动态的计算复合类型和容器类型的长度,
仅考虑可直接获取长度使用varint编码后的可压缩率.
对于成员序列化的数学模型, 考虑导致导致类字节流膨胀的条件:
- 类成员为复合类型或容器类型, 复合类型的成员或容器元素的类型, 同样为复合或容器类型
再做一次简化, 类的成员为容器类型, 容器元素继续为容器类型
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