一、前言 一般来说,我们总是希望数据传输能尽可能快一点。但如果发送方把数据发送得过快的话,接收方就可能来不及接收,这就会造成数据的丢失。而TCP的流量控制机制就是为了解决这个端到端的数据传输速率问题 。
所谓流量控制就是根据接收方的实际接收能力,来控制发送方的数据发送速率。从而让发送方的发送速率不要太快,要让接收方来得及接收 。
流量控制解决的是一个端到端的问题,是接收端控制发送端发送数据的速率,以便使接收端来得及接收 。
TCP协议使用滑动窗口机制来实现对发送方的流量控制。
二、滑动窗口机制 2.1 利用滑动窗口可以提高传输速度 我们知道,TCP是以报文段为单位发送数据的 ,如果每发一个段就进行一次确认应答处理的话,这样的传输方式有一个缺点。那就是,包的往返时间越长通信性能就越低。为了解决这个问题,TCP引入了窗口 这个概念。即使在往返时间较长的情况下,它也能控制TCP网络通信性能的下降。
具体是怎么做的呢?
为了便于说明滑动窗口机制的工作原理,我们假定数据传输只在一个方向上进行,A(发送方) —> B(接收方),即 A 发送数据,B 接收数据并给出确认。
发送方发送数据时不再是一次只发送一个TCP报文段,而是一次连续发送多个报文段。也就是说,发送方在发送了一个段之后不必一直等待这个段的确认应答,而是继续发送下一个报文段。每当收到一个报文段的确认应答后,窗口就向前滑动一个报文段的长度,因为已发送并收到确认应答的报文段不需要再保留在窗口中了,但已发送还未收到确认应答的段还必须保留在窗口中,以便在超时重传时使用。需要注意的是,滑动窗口是以字节为单位向前滑动的 。
示例 :假设一个TCP报文段包含有1000字节的数据(注意:不包含TCP首部长度),发送端一次发送4个报文段。如下图所示:
说明 :滑动窗口的大小是4个报文段的数据部分长度,即4000字节。这里的窗口大小就是指无需等待确认应答而可以继续发送数据的最大值。
当主机A收到第1个报文段的确认应答后(收到主机B发来的确认号1001),即序号 1 ~ 1000 的数据已被确认,此时滑动窗口就可以向前移动1000字节的长度,此时窗口的左边界序号是1001,右边界序号是5001。右边界 - 左边界 = 窗口大小。
当主机A收到第2个报文段的确认应答后(收到主机B发来的确认号2001),即序号 1001 ~ 2000 的数据已被确认,此时滑动窗口继续向前移动1000字节的长度,此时窗口的左边界序号是2001,右边界序号是6001。
分析 :窗口以外的部分中,窗口左边部分表示已发送且已收到确认应答 。这些数据显然不需要再保留了。窗口右边部分表示不允许发送的数据 。因为接收方都没有为这部分数据保留临时存放的缓存空间。
可以发现,发送窗口的位置由窗口左边界和右边界共同确定。发送窗口左边界每次都是滑动到收到确认应答号的位置上,而发送窗口右边界则是不允许发送数据的第1个字节序号 。
这种可以顺序地将多个报文段同时发送以提高TCP通信性能的机制,被称为滑动窗口机制 。
2.2 发送窗口与接收窗口 上面所讲的内容是从发送方的角度来解释滑动窗口机制的。发送方的窗口称为发送窗口,而接收方也有一个窗口,称为接收窗口。
发送窗口 ,表示的是发送方一次可以连续发送出去的数据量,以字节为单位。凡是已经发送过的数据,在未收到确认之前都必须暂时保留在发送窗口中,以便在超时重传时使用。发送窗口内的数据都是尚未收到确认应答的。
接收窗口 ,表示的是接收方一次能够连续接收的数据量,以字节为单位。接收窗口内的数据都是允许被接收的。
需要注意的是,发送窗口值不是由发送方自己决定的,而是根据接收方给出的窗口值,发送方再构造出自己的发送窗口值。
在TCP报文段的首部结构中,有一个“窗口”字段,其作用是告诉对方本端的接收窗口的大小,以便控制发送方的发送窗口大小。
二者的关系:发送窗口值 ≤ 接收窗口值
此外,发送方的发送窗口大小还要受到当前网络拥塞程度的制约。我们暂时不考虑网络拥塞的影响,后续讲述TCP拥塞控制时再讨论。
为了便于下面的解释说明,我们假定一个TCP报文段的数据部分长度为1字节,数据传输方向为:A(发送方) —> B(接收方)
假定发送方A的发送窗口大小为20字节,接收方B的接收窗口也为20字节。
现在假定A发送了序号为 31~41 的数据。这时,发送窗口位置并未改变,但发送窗口内靠后面的有11字节(黑色小方框表示)表示已发送但未收到确认。而发送窗口内靠前面的9个字节(42~50)是允许发送但尚未发送的。如下图所示:
从上图可以看出,要描述一个发送窗口的状态需要三个指针:P1,P2 和 P3。指针都指向字节的序号。这三个指针指向的几个部分的意义如下:
P3 - P1 = A的发送窗口 P2 - P1 = 已发送但尚未收到确认的字节数 P3 - P2 = 允许发送但目前尚未发送的字节数(又称为可用窗口 或有效窗口 ) 同时,可以看到,发送方的包可以分为四种状态:
已发送并收到确认的包 已发送但尚未收到确认的包 允许发送但尚未发送的包 不允许被发送的包 <说明 > 这里的包指的是TCP报文段,因为TCP是以报文段为单位发送数据的。
再来看看接收方B的接收窗口。B 的接收窗口大小为20字节。在接收窗口以外的部分,到序号30为止的数据都是已经发送过确认,并且已经交付上层应用了。因此在 B 的接收窗口中可以不用再保留这些数据。接收窗口内的数据(31~50)是允许接收的。在上图中,B 收到了序号为 32 和 33 的数据。这些数据没有按序到达,因为序号为 31 的数据没有收到(也许丢失了,也许滞留在网络中的某处)。请注意,接收方 B 只能对按序收到的数据中的最高序号给出确认应答,因此 B 发送的确认报文段中的确认号仍为 31(即期望下一次收到的序号),而不能是 32 或是 33,更不能是34。
现在假定 B 收到了序号为31的数据,并把序号为 31~33的数据交付主机,然后B从接收缓存中删除这些数据。接着把接收窗口向前移动3个序号的长度,同时给A发送确认,其中B的接收窗口值仍为20,但确认号为34。这表明 B 已经收到了序号 33 为止的数据。我们注意到,B 还收到了序号为37,38 和 40 的数据,但这些都没有按序到达,只能先暂存在接收窗口中。A收到B的确认后,就可以把发送窗口向前滑动3个序号的长度,但指针P2不动。可以看出,现在A的可用窗口增大了,可发送的序号范围是 42~53,即12个序号的长度,之前是9个序号的长度。
如下图所示:
A收到新的确认号,发送窗口向前移动 A 在继续发送 42 ~ 53 的数据后,指针P2向前移动和指针P3重合,意味着此时可用窗口大小为0。发送窗口内的序号都已用完,但还没有收到确认(如下图所示)。由于A的发送窗口已满,可用窗口已减小到零,因此必须停止发送数据。请注意,存在下面这种可能性:
发送窗口内所有的数据都已正确达到B,B 也早已发出了确认。但不幸的是,所有这些确认都滞留在网络中了。在没有收到B的确认时,A 不能猜测:“获取 B 收到了吧”。为了保证可靠传输,A 只能认为 B 还没有收到这些数据。于是,A 在经过一段时间后(由超时计时器控制)就重传发送窗口中的这部分数据(34 ~ 53),重新设置超时计时器,直到收到B的确认为止。如果 A 收到的确认号落在了发送窗口内,那么A就可以使发送窗口继续向前滑动,并发送新的数据。此时,指针P1指向的位置就是A收到的确认号的位置。
发送窗口内的序号都属于已发送但未被确认 滑动窗口总结
1、TCP首部中的窗口字段指出了现在运行对方发送的数据量。窗口值是经常动态变化者的。
2、TCP 使用滑动窗口机制。发送窗口里面的序号表示允许发送的序号。发送窗口左边的部分表示已发送且已收到了确认应答,而发送窗口右边的部分表示不允许发送。发送窗口右边界的变化有两种情况:一是不动(没有收到新的确认应答);二是前移(收到了新的确认应答)。发送窗口通常是不断向前移动的。
2.3 发送缓存与接收缓存,以及窗口与缓存的关系 缓存也就是缓冲区。
发送缓存 :发送方的缓冲区用于存储已经准备就绪的数据和已经发送但尚未收到确认的数据。
接收缓存 :接收方缓冲区用于存储已经被接收但是还没有被用户进程消费的数据和未按序到达的数据(这部分数据不能被用户进程消费)。
发送方的应用进程把字节流数据写入 TCP 的发送缓存,而接收方的应用进程则从 TCP 的接收缓存中读取字节流数据。
下图是缓存和窗口的关系:
图中画出了发送方维持的发送缓存 和发送窗口 ,以及接收方维持的接收缓存 和接收窗口 。
TCP 的缓存和窗口的关系 1、发送方的情况
由上图 (a) 可以看到,发送缓存用来暂时存放:
(1)发送应用程序传送给发送方TCP准备发送的数据。
(2)TCP已发送但尚未收到确认的数据(图中黑色部分)。
发送窗口通常只是发送缓存的一部分 。已被确认的数据应当从发送缓存中删除,因此发送缓冲和发送窗口的左边界是重合的。发送应用程序最后写入发送缓存的字节序号减去最后被确认的字节序号,就是还保留在发送缓冲中的被写入的字节数。发送应用程序必须控制写入发送缓冲的速率,不能太快,否则发送缓存就没有存放数据的空间了。
2、接收方的情况
由上图 (b) 可以看到,接收缓存用来暂时存放:
(1)按序达到的、但尚未被接收应用程序读取的数据。
(2)未按序到达的数据。
接收窗口通常也是接收缓存的一部分 。如果收到的报文段被检测出有差错,则要丢弃。如果接收应用程序来不及读取收到的数据,接收缓存最终会被填满,使接收窗口减小到零。反之,如果接收应用程序能够及时从接收缓存中读取收到的数据,接收窗口就可以增大,但最大不能超过接收缓存的大小。图 (b) 中还指出了下一个期望收到的字节序号。这个字节序号也就是接收方回复给发送方的确认报文段的首部中的确认号。
根据以上的讨论,我们还需要强调以下三点:
第一,虽然发送方的发送窗口是根据接收方的接收窗口设置的,但在同一时刻,发送方的发送窗口并不总是和 接收方的接收窗口一样大 。这是因为通过网络传送窗口值需要经历一定的时间滞后(这个时间是不确定的)。另外,发送方还可能根据当前网络的拥塞情况适当减小自己的发送窗口数值。本文前面已经对此说明了发送窗口和接收窗口值之间的关系。
第二,对于不按序到达的数据应如何处理,TCP标准并无明确规定 。如果接收方把不按序到达的数据一律丢弃,那么接收窗口的管理将会比较简单,但这样做对网络资源的利用不利(因为发送方会重复传送较多的数据)。因此 TCP 通常对不按序到达的数据是先临时存放在接收窗口中,等到字节流中所缺少的字节收到后,再按序交付上层的应用进程 。
第三,TCP 要求接收方必须有累计确认 的功能,这样可以减少传输开销,同时也能提高传输效率 。接收方可以在合适的时候发送确认,也可以在自己有数据要发送时把确认信息顺便捎带 上。但请注意两点:一是接收方不应过分推迟发送确认,否则就会导致发送方不必要的重传,这反而浪费了网络的资源。TCP标准规定,确认推迟的时间不应超过 0.5 秒 。若收到一连串具有最大长度的报文段(即报文段长度为MSS),则必须每隔一个报文段就发送一个确认 [RFC 1122]。二是捎带确认实际上并不经常发生,因为大多数应用程序很少同时在两个方向上发送数据。
最后再强调一下,TCP 的通信是全双工通信 。通信中的每一方都在发送和接收报文段。因此,每一方都有自己的发送窗口和结束窗口。在谈到这些窗口时,一定要弄清楚是哪一方的窗口。
三、TCP的流量控制 上面的部分我们讲了滑动窗口机制,知道了滑动窗口机制可以提高TCP通信的传输速率 ,同时利用滑动窗口机制还可以很方便地实现对TCP的流量控制 。
我们需要弄清楚,所谓TCP的流量控制,它控制的对象是谁?解决的是什么问题?TCP控制的对象是发送方,解决的问题是控制发送方的数据发送速率问题 。接下来我们就要弄清楚,TCP是如何利用滑动窗口机制来解决这个流量控制问题的?
下面我们通过一个例子来说明如何利用滑动窗口机制进行流量控制。
假设,数据传输方向是 A —> B。在三次握手建立连接时,B 告诉了 A:“我的接收窗口是 rwnd=400”(这里rwnd 表示 receiver window)。因此,发送方的发送窗口不能超过接收方给出的接收窗口的数值 。请注意,TCP 的窗口大小的单位是字节,而不是TCP报文段 。再假设每一个报文段为 100 字节长度,而数据报文段的序号的初始值设为 1(即下图中序号 seq=1)。请注意,下图中箭头上面的大写ACK表示首部中的确认位ACK,小写ack表示确认字段的数值 。如下图所示:
从上图中我们可以看到,接收方的主机 B 进行了三次流量控制。第一次把接收窗口减小到 rwnd = 300,第二次又减小到 rwnd = 100,最后减小到 rwnd = 0,即不允许发送发再发送数据了。这种使发送方暂停发送的状态将一直持续到接收方主机 B 重新发出一个新的窗口值为止。我们还应注意到,接收方 B 向 发送方 A 发送的三个报文段都设置了 ACK=1,这是因为只有在 ACK=1 时确认号字段才有意义 。
现在我们考虑一种情况。在上图 5-22 中,B 向 A 发送了零窗口的报文段后不久,B 的接收缓存又有了一些存储空间。于是 B 向 A 发送了 rwnd=400 的报文段。然而这个报文段在传送过程中丢失了。A 一直等待收到 B 发送的非零窗口的通知,而 B 也一直在等待 A 发送的数据。如果没有其他措施,这种互相等待的死锁僵局 将一直延续下去。
为了解决这个问题,TCP 为每一个连接设有一个持续计时器 (persistence timer)。只要TCP 连接的一方接收到对方的零窗口通知,就启动持续计时器。若持续计时器设置的超时时间到期,就发送一个零窗口探测报文段 (仅携带一个字节的数据 ),而对方就可在确认这个探测报文段时给出现在新的窗口值。如果窗口值仍为零,那么收到这个报文段的一方就重新设置持续计时器。如果窗口值不为零,那么死锁的僵局就可以打破了。
<说明 > TCP规定,即使设置为零窗口,也必须接收以下几种报文段:零窗口探测报文段、确认报文段(ACK=1)和携带紧急数据的报文段(URG=1)。
四、TCP的传输效率 前面已经讲过,应用进程把数据传送到 TCP 的发送缓存后,剩下的发送任务就由 TCP 来控制了。可以用不同的机制来控制 TCP 报文段的发送时机。例如,第一种机制是 TCP 维持一个变量,它等于 最大报文段长度MSS 。只要缓存中存放的数据达到MSS字节时,就组装成一个 TCP 报文段发送出去。第二种机制是由发送方的应用进程指明要求发送报文段,即 TCP 支持的 推送 (push)操作。第三种机制是发送方的一个计时器期限到了,这是就把当前已有的缓存数据装入报文段(但长度不能超过MSS)发送出去。
但是,如何控制 TCP 发送报文段的时机仍然是一个较为复杂的问题。
例如,一个交互式的用户使用 Telnet连接(传输层为TCP协议)。假设用户只发一个字符,加上20字节的TCP首部后,得到21字节长的 TCP 报文段。再加上 20 字节的 IP 首部,形成 41 字节长的 IP 数据报。在接收方 TCP 收到数据后立即发出确认,构成的 IP 数据报是40字节长(假定没有发送数据)。若用户要求远地主机回送这一字符,则又要发回 41 字节长的 IP 数据报和 40 字节长的确认IP数据报。这样,用户仅发1个字符的数据,线路上就需传送总长度为 162 字节共 4 个报文段。当线路带宽并不富裕时,这种传送方法的效率的确不高。因此应适当推迟发回确认报文,并尽量使用捎带确认 的方法。
在TCP的实现中广泛使用 Nagle(纳格)算法 。算法如下:
1、若发送应用进程把要发送的数据逐个字节地送到 TCP 的发送缓存,则发送方就把第一个数据字节发送出去,把后面到达的数据字节都先缓存起来。
2、当发送方接收到对第一个数据字符的确认后,再把发送缓存中的所有数据组装成一个报文段再发送出去,同时继续对随后到达的数据进行缓存。只有在收到对前一个报文段的确认后,才继续发送下一个报文段。
3、当数据到达较快而网络速率较慢时,用这样的方法可明显地减少所用的网络带宽。
4、Nagle算法还规定:当到达的数据已达到发送窗口大小的一半或已经达到报文段的最大长度时,就立即发送一个报文段 。
另一个问题叫糊涂窗口综合征 (silly window syndrome)[RFC 813],有时也会使 TCP 的性能变坏。设想一种情况:TCP 接收方的缓存已满,而交互式的应用进程一次只从接收缓存中读取 1 个字节(这样就使接收缓存空间仅腾出 1 个字节),然后向发送方发送确认,并把窗口设置为 1 个字节(但发送的数据报为 40 字节长)。接着,发送方又发来 1 个字节的数据(请注意,发送方的 IP 数据报是 41 字节长)。接收方发回确认,仍然将窗口设置为 1 个字节。这样进行下去,使网络的传输效率很低。
要解决这个问题,可以让接收方等待一段时间 ,使得或者接收缓存已有足够空间容纳一个最长的报文段MSS,或者等到接收方缓存已有一半空闲的空间 。只要出现这两种情况之一,接收方就发出确认报文,并向发送方通知当前的窗口大小。此外,发送方也不要发送太小的报文段,而是把数据积累成足够大的报文段,或达到接收方缓存的空间的一半大小。
上述两种方法可以配合使用。使得在发送方不发送很小的报文段的同时,接收方也不要在缓存刚刚有了一点小的空间就急忙把这个很小的接收窗口大小信息通知给发送方。
参考 《计算机网络(第7版-谢希仁)》第五章
《图解TCP_IP(第5版)》第六章
畅谈linux下TCP(下)