TCP的流量控制
下面简单介绍了TCP的流量控制。
流量控制是发送方和接收方合作完成的端到端的流量控制,用于防止发送方发送数据包的速率超过了接收方的接收速率,造成了接收方溢出。
流量控制早于拥塞控制出现,是实现拥塞控制的基础,因此在深入学习拥塞控制之前,有必要回顾流量控制。
1.1 Acknowledgement机制
TCP基于ACK实现可靠传输。sender每发送一个packet,都会等待receiver的响应,而receiver收到packet之后,会立即反馈一个ACK,ACK表明receiver成功收到了该packet[5]。
ACK机制得以稳定运行的前提之一是所有的数据都是按序编号的,因此packet和其ACK便有了对应的依据。实际上,TCP不但将所有数据依次编号,而且精确到字节。在sender发送的每个packet的头部信息中,都写明了其序号(sequence number),同时,receiver在每个ACK中,都写明了所期望的下一个packet的序号,sender再基于该序号发送接下来的packet。ACK机制使得TCP得以自我运转,而不依赖外部因素,因此在一些文献中,Acknowledgement机制又称为Self-Clocking机制。
基于Acknowledgement机制,产生了“数据包守恒准则”[6]。根据该准则,如果一个网络上稳定地传输着满额的数据包,就认为它是平衡的。一个平衡的网络上,只有在有数据包离开时,才会有新的数据包加入。因此,平衡的网络可以避免拥塞的发生。
从sender发送packet起,到收到相应的ACK止,这个时间间隔称为一个RTT(roung trip time),该时间是在TCP运行的过程中,通过实时统计和估算得到的,并且是动态改变的[1]。
1.2 Cumulative Acknowledgement
在非SACK[17]的TCP中,receiver采取累积确认的模式[2]。在发送ACK时,总是回复这样一个序号,该序号之前的所有packet都已被receiver接收,与它相邻的下一个packet没有被receiver接收。即累积回复从左到右最大的一块连续区域。累积确认在所到达的packet填补了receiver缓冲区的空洞时,会被体现出来。
1.3 Sliding Window机制
sender和receiver都维持各自的window,分别称为发送窗口和接收窗口。发送窗口(send window)规定了可发送的范围,sender只能按序地发送该窗口内的packet。接收窗口(receive window)则相当于receiver的可用缓冲区(不一定相等,但有直接关系),用于接收来自sender的packet。另外,发送窗口又分为前后两部分:
[SND.UNA, SND.NXT)之间的部分,此窗口内的packet已经被发送出去,但尚未收到ACK,RFC1122称这些packet为outstanding的,即还在网络中传输的。
[SND.NXT, SND.UNA+SND.WND]之间的部分,称为可用窗口(usable window),此窗口内的packet尚未被发送出去。
其中,SND.UNA表示尚未收到ACK的packet中最靠左的一个,也是发送窗口的起点。
SND.NXT表示即将发送的下一个packet的序号。
SND.WND表示发送窗口的大小。
可以看出,随着sender不断地接受ACK,SND.UNA的值会递增,发送窗口就会不断地向右滑动,这就是滑动窗口机制。
窗口的概念是Flow Control的基础。Flow Control协调双方的速率,实际上就是协调双方窗口大小。实现窗口协调的前提是,至少有一方能够获知对方窗口的大小。为此,在每个ACK中,都会写明receiver的接收窗口的大小(称为offered window),使得sender可以依此调节发送窗口的大小。sender将接收窗口大小减去尚未收到ACK的数据的大小,作为可用窗口的大小,从而控制发送速率[5]。
对于receiver来说,
LastByteRcvd – LastByteRead <= RcvBuffer
rwnd = RcvBuffer – [LastByteRcvd – LastByteRead]
对于sender来说,
LastByteSent – LastByteAcked <= rwnd
SND.UNA=LastByteAcked-1
SND.NXT=LastByteSent-1
SND.WND=rwnd
1.4 Silly Window Syndrome
所谓Silly Window Syndrome[5],可以用下面一个例子解释。
假设receiver最初告诉sender,接收窗口的初始值为有1000字节,那么发送窗口/可用窗口的初始值也为1000字节。再假设每个packet的大小被设置为200字节,那么第一次sender会发送5个packet。当receiver接收到第一个packet后,有两种处理方式:(1)可以将其立即提交给应用层,从而重新腾出缓冲区,此时ACK中的offered window仍然是1000字节。(2)也可以不处理,等到一定时刻,再提交给应用层,此时ACK中的offered window就是1000-200=800字节。对于第二种方式,sender在收到ACK后,计算可用窗口的大小,结果为800-800=0,即sender不能继续发送数据,直到receiver腾出更多缓冲区为止。
我们下面只考虑第一种方式,此时sender计算到的可用窗口的大小为1000-800=200,因此sender可以发送一个新的200字节的packet。实际上可以看出,sender发送数据的大小就是receiver接收数据的大小。接下来考虑一种异常,某个时刻,可用窗口的大小为200字节,但是应用层要发送的数据只有50字节并且设置了PUSH,此时sender只能将50字节的数据作为一个packet发送出去,一个RTT后,sender收到对应的ACK,重新计算可用窗口的大小为1000-950=50字节,因此sender仍然只能发送50字节的packet。可以预见,50字节的窗口会一直存在,网络中会持续出现零碎的packet,因此TCP性能会降低。
在RFC813中提出了解决方法,主要思想是当sender遇到小窗口时,推迟新packet的发送,以等待receiver腾出更大的接收窗口再发送。这种思想分别运用到sender和receiver之上,形成了两种算法。(1)在sender端,会计算可用窗口相对接收窗口的比例,当其小于一个阈值时,就推迟新packet的发送。(2)在receiver端,如果发现offered window很小,那么会进一步将ACK中的offered window的值减小,使得sender计算得到的可用窗口过小而无法发送新的packet。等腾出更大接收窗口时,再一次性通知sender,使其得以继续发送packet。