前面我们已经通过示例看到如果接收端的应用层一直没有读取数据,那么window size就会慢慢变小最终可能变为0,此时我们假设一种场景,如果应用层读取少量数据(比如十几bytes),接收端TCP有了少量的新的接收缓存后如果立即进行window update把新的window size通告发送端的话,发送端如果立即发送数据,那么接收端缓存可能又会立即耗尽,window size又变为0,接着应用层重复读取少量数据,这个过程重复的话,那么发送端就会频繁的发送大量的小包,这种场景我们就称呼为Silly Window Syndrome(SWS),中文一般翻译为糊涂窗口综合征或者愚蠢窗口综合征。之前我们介绍Nagle算法的时候已经介绍过,大量的小包会降低网络利用率甚至造成网络拥塞,最终降低网络性能。
从上面的关于SWS的介绍中我们可以看到两个关键点,一个是接收端通告了比较小的window size,另外一个是发送端收到比较小的window size的时候,立即响应发出了对应的小包。因此如果要避免SWS,按照RFC1122协议可以从这两个方面着手:
对于接收端要避免以小的增量来推进接收窗的右边沿(RCV.NXT+RCV.WND),即使接收的数据包都是小包。只有当接收窗口的增量大于min( Fr * RCV.BUFF, Eff.snd.MSS )的时候才通告新的窗口。其中Fr是一个分数因子,协议建议值为1/2,Eff.snd.MSS为对端的发送MSS。
对于发送端来说在不超出对端接收窗口的前提下至少满足下列三个条件中的一个才能发送数据:(1)、一个full-sized的数据包(即大小满足Eff.snd.MSS)可以被发送;(2)、数据包的大小超过对端曾经通告过的window size的一半;(3)、当TCP发送端禁用了Nagle算法或者所有发出的数据都已经被对端ACK确认的话,那么TCP可以发送小数据包;
RFC1122要求TCP的接收端和发送端都是需要实现SWS避免算法的。另外Nagle算法和SWS避免算法是互补的,都是在不同场景下避免发送小数据包。
二、linux实现介绍
在linux实现上对于接收端大体处理如下
1、应用层读取了新的数据时候,如果空闲的接收缓存大于等于总接收缓存的1/16,并且大于等于MSS的估计值的时候,会根据这个空闲缓存计算出一个新的window size,如果这个新的window size大于等于两倍的当前接收窗口才会立即触发window update
2、回复对端window probe或者ACK确认包时候,如果空闲的接收缓存小于总接收缓存的1/16/或者小于MSS的估计值的时候,在不shrink窗口的前提下则回复window size为0的报文。
上面所说的MSS估计值就是rcv_mss和总接收缓存两者之间的最小值,rcv_mss的具体初始化方式和更新方式前面延迟ACK内容中已经介绍过了。
对于发送端实际上我们前面内容已经看到,发送端维护的发送MSS最大也只能为接收端曾经通告过的最大的window size的一半。因此发送端的条件(1)、(2)可以合并,条件3中的Nagle算法我们之前已经介绍过了相关内容。
三、wireshark示例
测试之前我们先进行如下设置
sudo ip route change local127.0.0.1 dev lo src 127.0.0.2 mtu 1530
这个命令的含义是,当client发起主动连接到127.0.0.1的时候,优选源地址为127.0.0.2,MTU大小设置为1530。设置MTU的大小的目的是限制client端的发送MSS为1490bytes(1500减掉20bytes的ip头在减掉20bytes的tcp头),这样方便观察对比,,同时也说明了server端维护rcv_mss的时候为什么没有直接使用SYN报文中的MSS选项而采取了保守估计的方式。另外我们在程序中通过通过SO_RCVBUF选项设置server端用于接收有效TCP数据的缓存为3500bytes,即window size大小为3500。SO_RCVBUF这个选项设置为3500的时候,实际内核内部的接收缓存为7000bytes,其中有1/2用于接收TCP载荷数据,另外的缓存用于存储相关的数据结构等。这个比例可以通过/proc/sys/net/ipv4/tcp_adv_win_scale设置。
此处示例不再细致解释延迟ACK、quick ACK、window probe的指数回退和对window probe的ACK特殊处理,详细请参考前面文章。
1、SWS综合示例
client和server建立连接后,client立即连续写入三次数据,每次write写入2048bytes,server端则先休眠15s,然后每隔2s读取256bytes的数据
No1-No3:client与server通过三次握手建立连接,可以看到client主动发起连接到127.0.0.1的server端的时候,使用的ip地址为127.0.0.2。另外可以看到server端SYN-ACK报文中的MSS为65495,但是我们通过路由设置了到127.0.0.1地址的MTU为1530(换算为MSS为1490),因此client端会取两者的较小值为MSS,即client端的发送MSS为1490,在扣除TSopt选项所占的12bytes的空间后,client端最大能发送的数据报文的大小为1478bytes,client端内核实际维护的MSS值为扣除tsopt后的1478。