基本原理
可能性两个依据:
1、 在PPP或者SLIP这些串行线路协议中,不同于以太网中无法确定下一跳的位置,它们的通讯两端是固定的。
2、 对于同一个TCP连接,在TCP/IP头中,对属于同一个连接的包的协议头是很类似的。例如它们的seq号,window size,ack号都是差几个数字,另外它们的IP号,端口号都很一般是相同的。所以可以在通讯双方进行协商,我们只传输TCP/IP头中发生变化的部分,而且之传输Delta值,即差值,这样可以极大减少传输的数据。
必要性:
很多TCP连接中的数据包都是一些交互数据,数据量非常小,而TCP/IP头占据了很大的比例。如果能够减少TCP/IP头的大小,则非常有效的减少网络负载。
基本原理:
如果PPP或者SLIP的两端能够对每个tcp连接的TCP/IP头维护一个缓存,那么我们可以每次只传输头中发生了何种的变化。因为变化是比较小的,所以传输量会大幅度减少。在发送端根据当前头和该连接前一个包的头计算出变化量并传输,在接收端根据变化量和该连接前一个包的头计算出该包的真实头。在传输中会有一个连接号来标识不同的连接。
压缩细节
可以参考上面这张图,它描述了TCP/IP头。在上图灰色的部分是同一个连接中不会发生变化的,包括:IP部分(协议版本,头长度,服务类型,DF标志,MF标志,碎片偏移(因为对于碎片IP我们不进行压缩,因为根本无法判断上层是TCP协议),TTL,上层协议,源IP地址,目标IP地址),TCP部分(源端口,目标端口,首部长度,保留的6位,ack,rst,syn,fin标志)。
对于可能发生变化的字段,则并非一定发生变化。所以需要维护一个位图,每位标识某个字段是否发生了变化。对于发生了变化的字段,也并非传输新字段,而是可能传输变化的delta值。因为对于IP packet id, TCP seq等字段其变化值很可能是很小的数,通过这种方式可以进一步减少传输的数据。具体而言:
IP头中的Packet ID一般是每次加1,所以分配了8位来表示delta值,它可以容纳<256的正整数。于是我们认为该字段默认是每次加1的。如果不是加1,则需要在压缩头中记录其delta值。TCP头中的SEQ字段一般是上一个SEQ字段加上上次传输的IP包的大小,因为IP包最大是65536,所以该字段使用16位来表示delta;对于ack字段也是同样道理,也是使用了16位。以上三者只能是正数,不能表示负数。
TCP头中的window size也是使用16位表示delta值,可正可负。因为window size不经常改变,所以也传输delta值。紧急指针也需要16位表示delta值,当TCP头中URG被设置时会在压缩头中进行紧急指针的传输,而且传输真实值而非delta值。(但如果TCP头中的URG标志没设置而URG指针发生变化时,则传输非压缩的TCP/IP包。这么处理估计是因为这种情况比较少发生)。
另外TCP Check sum是一定要传输的。还可能传输一个connection number。这个标识了一个tcp连接。因为很可能连续两个包的连接是相同的,如果相同就不需要传输了,所以说是可能传输。
对于上面的connnection number,ungent pointer, delta window size,delta ack,delta sequence,delta IP ID都是可能传输的,所以需要传输一个位图标识这些字段有没有传输。这总共是6位。
因为上述需要传输的值,如果是8位,则直接传输8位数;如果是16位,则需要在16位之前多传输8位的0。通过0表示下面是一个16位数。换句话说,8位数据就传输8位;16位数据却传输24位,前面8位传输0.因为某些情况下我们并不知道数据应该按照16位还是8位来解析。
对于TCP头中PSH标识,如果设置了,在在位图中也要设置。
另外为了进一步减少传输量,算法特别考虑了两个特殊情况:
1、 seq和ack改变同样的值,而且是上个包的负载长度。此时window size没有改变,URG也没有改变。这种情况一般表示一个回应包(echoed terminal traffic)。此时需要设置SWU三个位。此时不需要设置seq,ack字段,因为可以通过上个包得到,所以减少了传输的数据。
2、 seq改变的值是上个包的负载长度,而且ack ,window size,urg不变化。这种情况一般表示单向通讯,此时设置SAWU四个位。此时也不需要设置seq位,因为可以通过上个包获得这个长度。
这两种特殊情况相对比较常见,所以需要特殊处理。这样就有些哈夫曼电报码的意味了。当然,有时候确实会出现S,W,U三个位都被设置的包,对于这些包就要进行非压缩传输,否则就混淆了。
下面的图表示了压缩后的TCP/IP头:
压缩过程
压缩过程实在IP处理函数中,需要在向链路层转发数据之前调用压缩过程函数。
第一步:判断是否压缩
只对TCP协议进行压缩,而UDP等协议不进行压缩,对IP碎片也不进行压缩(因为根本没有TCP头)。另外如果TCP标志中的syn,fin,rst被设置(连接建立或者断开或者复位)或者ack没有设置(正常连接中ack是一定被设置的),也不进行压缩。这些情况下,发送的数据包的类型是TYPE_IP。这个类型会在下层的PPP(SLIP应该也会表现)中表现出来。因为之后的解压缩中会用到它。
第二步:判断数据包类型是UNCOMPRESSED_TCP或者是COMPRESSED_TCP。
UNCOMPRESSED_TCP表示该包按照一般包发送,惟一的区别是IP头部的协议类型字段修改位连接号,它本来是6表示上层协议是TCP。COMPRESSED_TCP则传输压缩过的TCP数据包。对于需要压缩的数据包序列,其第一个必须被缓存起来以供生成后面的数据包,所以第一个要设置为UNCOMPRESSED_TCP。
两种情况下会被设置为UNCOMPRESSED_TCP:
1、如果该包对应的四元组(源目的IP,源目的端口)并没有被缓存,则类型是UNCOMPRESSED_TCP。
2、如果该包对应的四元组已经存在,但发现该该包和缓存的包的不该改变的字段发生了变化,则也需要标识类型为UNCOMPRESSED_TCP。另外如果tcp和ip的option选项发生变化,也需要标志类型位UNCOMPRESSED_TCP。
另外如果类型为UNCOMPRESSED_TCP,需要将他缓存下来。
第三步:进行压缩,并且在发现无法压缩时停止压缩
1、 对于URG
a) 因为如果当该标志不设置并且URG POINTER发生变化的时候不进行压缩,所以此时需要设置类型为UNCOMPRESSED_TCP;
b) 如果该标志没有设置并且URG POINTER没有发生变化,不设置U。
c) 如果URG被设置,提取urgent pointer到压缩头中,并且设置U位。
2、 如果ack值的变化大于65535或者小于0,则不进行压缩。因为我们分配的ack delta只有16个位;否则提取ack值并求与缓存的ack值的差值,如果非0,写入到压缩头中,并设置A位。
3、 seq值与ack值的处理相同。将差值写入压缩头,并设置S位。
4、 提取window size,如果非0,写入差值到压缩头,并设置W位。
5、 处理上一节中的两个特殊情况
a) 如果U,S,W位已经被设置,则该包作为UNCOMPRESSED_TCP发送。这是为了避免混淆。
b) 处理第二种特殊情况。如果S被设置并且seq的 变化值和最后一个数据包的长度相同,设置标志位图为SAWU。
c) 处理第一种特殊情况,如果S和A设置并且它们的delta值都等于最后一个数据包的哦长度,设置标志位图为SWU。
d) 如果Seq和Ack都没有发生变化,则查看是否该数据包没有数据或者。。。
第四步,进行其它压缩,这个过程中不会停止压缩
1、 计算packet ID的delta,如果不是1,则设置I位并且记录packet ID的delta
2、 如果Push标志在TCP头中被设置,则设置P位
3、 查看connection number是否发生变化,如果变化,则记录并设置C位
4、 记录checksum在压缩头中
5、 把最新的TCP/IP头拷贝到本地缓存,用于下一次的计算
6、 将压缩后的头替换之前的tcp/ip头
解压过程
相对于压缩过程,解压过程简单的多。它处理三种类型的数据包:
1、 TYPE_IP:不进行处理,直接向上层协议(ip协议)传输
2、 UMCOMPRESSED_TCP:找到IP头中替换掉上层协议的connection number。如果其合法,则将该包的TCP/IP头写入本地缓存(之前必须要使用TCP协议号替换connection number在IP头的上层协议字段中),并且设置最后一个收到的连接号是该连接号(因为当相邻两个包的连接号相同的时候,连接号字段可以省略掉)。之后向上层协议传输。
3、 COMPRESSED_TCP:这是需要解压缩TCP/IP头为原始头。
第三种情况最复杂,但跟压缩过程比还是简单了不少。
第一步:检查Connection number。如果C位被设置,则直接获得连接号,并验证其对应的缓存中存在;如果不存在,则连接号是上次的连接号。
第二步:根据连接号获取该连接对应的缓存中保存的TCP/IP头。将缓存的TCP/IP头作为原本,用压缩头对之进行修改。缓存的TCP/IP头是该连接上一个包对应的头,修改后的是当前包的包头。
1、 用压缩头的checksum替代缓存头的checksum。
2、 如果压缩头的P设置,则缓存头的P也进行设置。
3、 如果S,A,W,U都设置了,这是前面所提的第一种特殊情况,需要首先计算出缓存头对应包(即该连接号的上面一个包)的负载长度,然后用前一个包的seq号加上该负载长度得到该包的seq;
4、 如果s,w,u都设置,a没有设置,则是前面所提的第二种特殊情况,此时也需奥首先计算上个包的负载长度,然后用上个包的seq加负载长度得到当前包的seq,用上个包的ack加负载长度得到当前包的seq。
5、 如果U位被设置,则当前包头的TCP URG标志被设置,并且压缩包头的URG POINTER写到TCP头中的URG POINTER
6、 如果W位被设置,window delta加上之前包的window size为新的window size
7、 如果A位被设置,ack delta加上之前包的ack得到新的ack。
8、 如果S位被设置,seq delta加上之前包的seq位新的seq。
9、 如果I位设置,则packet ID delta加上之前包的packet ID为新的packet ID;否则加1为新的packet ID。
上面这9部必须依次做,因为不同位对应的值的顺序就是上面的顺序。
第三步:需要计算新的IP包的总长度,这只需要把本来的负载长度加上新的TCP/IP头长度即可。
参考资料
RFC1414
LINUX2.4.20源代码的drivers/net/slhc.c和include/net/shlc_vj.h。主要是shlc_compress和shlc_uncompress函数。