第一部分:SWS
何谓糊涂窗口综合症
当发送端应用进程产生数据很慢、或接收端应用进程处理接收缓冲区数据很慢,或二者兼而有之,就会使应用进程间传送的报文段很小,特别是有效载荷很小。 极端情况下,有效载荷可能只有 1 个字节;而传输开销有40 字节(20 字节的 IP 头 + 20 字节的 TCP 头) 这种现象就叫糊涂窗口综合症。
发送端引起的SWS
发送端的 TCP 可能产生糊涂窗口综合症,如果发送端为产生数据很慢的应用程序服务(典型的有 telnet 应用)。例如,一次产生一个字节。这个应用程序一次将一个字节的数据写入发送端的 TCP 的缓存。如果发送端的 TCP 没有设置特殊的指令,它就产生只包括一个字节数据的报文段。结果有很多 41 字节的 IP 数据报就在互连网中传来传去。
解决的方法是:防止发送端的 TCP 逐个字节地发送数据。必须强迫发送端的 TCP 收集数据,然后用一个更大的数据块来发送。发送端的 TCP 要等待多长时间呢?如果它等待过长,它就会使整个的过程产生较长的时延。如果它的等待时间不够长,它就可能发送较小的报文段,于是,Nagle 找到了一个很好的解决方法,发明了Nagle 算法。而他选择的等待时间是一个 RTT ,即下个 ACK 来到时。
接收端引起的SWS
接收端的 TCP 可能产生糊涂窗口综合症,如果它为消耗数据很慢的应用程序服务。例如,一次消耗一个字节。假定发送应用程序产生了 1000 字节的数据块,但接收应用程序每次只吸收 1 字节的数据。再假定接收端的 TCP 的输入缓存为 4000 字节。发送端先发送第一个 4000 字节的数据。接收端将它存储在其缓存中。现在缓存满了。它通知窗口大小为零,这表示发送端必须停止发送数据。接收应用程序从接收端的 TCP 的输入缓存中读取第一个字节的数据。在入缓存中现在有了 1 字节的空间。接收端的 TCP 宣布其窗口大小为 1 字节,这表示正渴望等待发送数据的发送端的 TCP 会把这个宣布当作一个好消息,并发送只包括一个字节数据的报文段。这样的过程一直继续下去。一个字节的数据被消耗掉,然后发送只包含一个字节数据的报文段。
对于这种糊涂窗口综合症,即应用程序消耗数据比到达的慢,有两种建议的解决方法:
- Clark 解决方法:解决方法是只要有数据到达就发送确认,但宣布的窗口大小为零,直到或者缓存空间已能放入具有最大长度的报文段,或者缓存空间的一半已经空了。
- 延迟确认:解决方法是延迟一段时间后再发送确认。这表示当一个报文段到达时并不立即发送确认。接收端在确认收到的报文段之前一直等待,直到输入缓存有足够的空间为止。延迟确认防止了发送端的 TCP 滑动其窗口。当发送端的 TCP 发送完其数据后,它就停下来了。这样就防止了这种症状。迟延确认还有另一个优点:它减少了通信量。接收端不需要确认每一个报文段。但它也有一个缺点,就是迟延确认有可能迫使发送端重传其未被确认的报文段。可以用协议来平衡这个优点和缺点,例如现在定义了确认的延迟不能超过 500 毫秒。
第二部分:Nagle算法
TCP/IP 协议中,无论发送多少数据,总是要在数据前面加上协议头,同时,对方接收到数据,也需要发送 ACK 表示确认。为了尽可能的利用网络带宽,TCP 总是希望尽可能的发送足够大的数据。(一个连接会设置MSS 参数,因此,TCP/IP 希望每次都能够以 MSS 尺寸的数据块来发送数据)。Nagle 算法就是为了尽可能发送大块数据,避免网络中充斥着许多小数据块。
Nagle 算法的基本定义是:任意时刻,最多只能有一个未被确认的小段。
所谓“小段”,指的是小于 MSS 尺寸的数据块;所谓“未被确认”,是指一个数据块发送出去后,没有收到对方发送的 ACK 确认该数据已收到。
Nagle 算法的规则(可参考 tcp_output.c 文件里 tcp_nagle_check 函数注释):
- 如果包长度达到 MSS ,则允许发送;
- 如果该包含有 FIN ,则允许发送;
- 设置了 TCP_NODELAY 选项,则允许发送;
- 未设置 TCP_CORK 选项时,若所有发出去的小数据包(包长度小于 MSS )均被确认,则允许发送;
- 上述条件都未满足,但发生了超时(一般为 200ms ),则立即发送。
Nagle 算法只允许一个未被 ACK 的包存在于网络,它并不管包的大小,因此它事实上就是一个扩展的停-等协议,只不过它是基于包停-等的,而不是基于字节停-等的。Nagle 算法完全由 TCP 协议的 ACK 机制决定,这会带来一些问题,比如如果对端 ACK 回复很快的话,Nagle 事实上不会拼接太多的数据包,虽然避免了网络拥塞,网络总体的利用率依然很低。另外,他是一个自适应的方法,读者可以自己按上述规则试验一下。
Nagle 算法是 silly window syndrome(SWS) 预防算法的一个半集。SWS 算法预防发送少量的数据,Nagle 算法是其在发送方的实现,而接收方要做的时不要通告缓冲空间的很小增长,不通知小窗口,除非缓冲区空间有显著的增长。这里显著的增长定义为完全大小的段(MSS)或增长到大于最大窗口的一半。
注意:BSD 的实现是允许在空闲链接上发送大的写操作剩下的最后的小段,也就是说,当超过 1 个 MSS 数据发送时,内核先依次发送完 n 个 MSS 的数据包,然后再发送尾部的小数据包,其间不再延时等待。(假设网络不阻塞且接收窗口足够大)
TCP_NODELAY 选项
默认情况下,发送数据采用 Negale 算法。这样虽然提高了网络吞吐量,但是实时性却降低了,在一些交互性很强的应用程序来说是不允许的,使用 TCP_NODELAY 选项可以禁止 Negale 算法。
此时,应用程序向内核递交的每个数据包都会立即发送出去。需要注意的是,虽然禁止了 Negale 算法,但网络的传输仍然受到 TCP 确认延迟机制的影响。
TCP_CORK 选项
所谓的 CORK 就是塞子的意思,形象地理解就是用 CORK 将连接塞住,使得数据先不发出去,等到拔去塞子后再发出去。设置该选项后,内核会尽力把小数据包拼接成一个大的数据包(一个 MTU)再发送出去,当然若一定时间后(一般为 200ms ,该值尚待确认),内核仍然没有组合成一个 MTU 时也必须发送现有的数据(不可能让数据一直等待吧)。
然而,TCP_CORK 的实现可能并不像你想象的那么完美,CORK 并不会将连接完全塞住。内核其实并不知道应用层到底什么时候会发送第二批数据用于和第一批数据拼接以达到 MTU 的大小,因此内核会给出一个时间限制,在该时间内没有拼接成一个大包(努力接近 MTU )的话,内核就会无条件发送。也就是说若应用层程序发送小包数据的间隔不够短时,TCP_CORK 就没有一点作用,反而失去了数据的实时性(每个小包数据都会延时一定时间再发送)。
Nagle 算法与 CORK 算法区别
Nagle 算法和 CORK 算法非常类似,但是它们的着眼点不一样,Nagle 算法主要避免网络因为太多的小包(协议头的比例非常之大)而拥塞,而 CORK 算法则是为了提高网络的利用率,使得总体上协议头占用的比例尽可能的小。如此看来这二者在避免发送小包上是一致的,在用户控制的层面上,Nagle 算法完全不受用户socket 的控制,你只能简单的设置 TCP_NODELAY 而禁用它,CORK 算法同样也是通过设置或者清除TCP_CORK 使能或者禁用之,然而Nagle算法关心的是网络拥塞问题,只要所有的 ACK 回来则发包,而 CORK 算法却可以关心内容,在前后数据包发送间隔很短的前提下(很重要,否则内核会帮你将分散的包发出),即使你是分散发送多个小数据包,你也可以通过使能 CORK 算法将这些内容拼接在一个包内,如果此时用 Nagle 算法的话,则可能做不到这一点。