TCP的滑动窗口机制,谈谈其设计演化过程(如何从无到有?从不可靠到可靠?)

1,798 阅读12分钟

理解TCP滑动窗口是如何工作的,对于理解TCP的其他知识是至关重要的。

相比于更为简单,同为传输层协议的UDP而言,TCP提供了对传输数据的质量保证。

在可靠性上,TCP确保传输的数据不丢失、不重复,也不会产生乱序。

同时,TCP还提供了流量控制,用于控制数据发送的速度,防止较快主机导致较慢主机的缓冲区溢出。

接下来,从不可靠的协议开始说起,看看TCP面向流的滑动窗口机制是如何演化而来的。

不可靠的协议

对于不可靠的协议(例如IP、UDP),当数据发出去之后,可能到达目的地,也可能丢失。

如果没有提供消息反馈的机制,那么发送方将无法获知数据是否已成功的送达目的地,因而也无法对丢失的数据进行重传。

没有消息反馈机制,是造成传输不可靠,没有流控的一个主要原因。

支持重传的确认机制

为了能够在一个不可靠协议的基础之上,进行可靠的数据传输,引入了消息反馈机制。

消息反馈

发送方发送一条信息,当接收方收到数据之后,给发送方返回一个接收成功的反馈信息。

简单的实现流程如下:

  1. 主机A给主机B发送一条消息;
  2. 主机B收到后,给主机A发送一条确认消息,告诉A消息已经被成功的接收了;
  3. 主机A在收到B的确认消息之后,就知道该消息已发送成功。

但是这个简单的机制,可能存在2个问题:

  1. 由于IP协议是不可靠的,实际上消息可能没有到达目的地。这样,主机A将会处于等待消息确认的状态,可能永远也收不到确认。
  2. 另一方面,也有可能主机B在收到了从A发送过来的消息之后,返回了确认消息,但这个消息却意外的丢失了。

以上2种情况,都有可能导致主机A一直处于等待确认消息的状态,但却永远也等不到。

超时重传

为了解决这个问题,当主机A发送一条消息之后,可以同时启动一个计时器。这个计时器的时间要足够长,让消息能够送达主机B,同时让确认消息也能够返回给主机A。

如果在确认消息到达主机A之前,计时器超时了。那么,主机A会认为发送消息的过程中遇到了问题(可能是丢失了,或者网络堵塞导致包延迟),于是就重传该消息。

由于这种对消息的确认机制,既包含了对消息已收到的确认应答,也包含了发生超时后对消息的重传,因此被称为支持重传的确认机制(PAR,Positive Acknowledgment with Retransmission)

基于PAR机制,能够给数据传输提供基本的可靠性保证

但PAR机制存在一个缺点,就是在任何时候,只能有一条消息是未被确认的。这会使得系统变得非常慢,因为后续消息的发送都得等待前一条消息的确认。

消息标识和发送限制

PAR机制比较适用于传输少量数据,或者交互不是很频繁的协议。由于PAR传输效率低下,因此也不适用于像TCP这样的协议。

为了解决传输效率低下的问题,引入了消息标识。

消息标识

当发送方发送消息的时候,为这个消息增加一个唯一标识。接收方在收到消息之后,返回的确认消息也带上该消息标识。

这样,发送方就可以针对不同的消息使用不同的标识。发送方可以同时发送多条消息,在收到确认消息之后,只要根据消息标识进行匹配,确认对应的消息即可。

以上,只是从发送方的角度,解决了发送方的发送速率问题(在任何时候,只能有一条消息是未被确认的)。

发送限制

我们再换个角度,原来是发送一个消息,接收方处理完,返回确认消息后,等待下一条消息。这个过程,对于接收方来说,并不会存在什么问题。

在引入消息标识之后,发送方可以同时发送多条消息了。但如果发送过快,接收方可能由于处理不过来,被压垮了。

因此,对于接收方来说,需要保证在自己能够正常接收处理数据的情况下,通过一种机制,来通知发送方尽量不要发送得太快,发送的数据尽量按照自己的处理能力来。

比较容易想到的,就是在接收方返回确认消息的时候,将接收方的发送限制信息带上,传递给发送方

发送方在收到确认消息中的发送限制字段时,就可以按照接收方的要求来控制自己的发送速率。

有了这个发送限制,接收方就可以根据自己的实际负载情况,进行动态反馈以调整发送速率,最优化发送性能。

增加了消息标识和发送限制之后,可以认为是PAR机制的一个增强版。

对于数据的传输,现在可以提供可靠性、效率和基本的数据流控了。

TCP面向流的滑动窗口确认

在增加了消息标识和发送限制之后,整个消息的发送和确认过程,已经较为完善了。

但从传输的"数据"的角度来看,前面我们讲的,其实都是面向消息的。也就是说,发送和确认都是以消息为单位。

而实际上,TCP是面向字节流的,本身并没有消息的概念。TCP传输的数据,我们通常称之为报文段。具体如何识别并组装为一条完整的消息,是应用层的工作。

正因为如此,TCP是以字节为单位来处理数据流的。如果要对TCP传输的数据做标识,那就需要一个字节给一个标识。

这样一来,TCP每次就只能发送一个字节的数据,然后每次也只能确认一个字节的数据,这显然是不合理的。既浪费带宽,又降低了发送效率。

TCP的设计者确实也没有这么做,而是将数据划分为一个个数据段,每次可以发送一段数据

对于每一段数据,使用一个序号来进行标识。每次确认数据的时候,也是确认一段数据而不是一个字节。

事实上,序号是报文段中数据字节的偏移量,表示数据字节的位置。

TCP缓冲区

设想一个场景,主机A和主机B建立了一个新的连接:

  1. 主机A有一个很大的数据字节流需要传输,但是主机B没有办法全部接收;
  2. 所以主机B限制主机A报文段能够传输的字节数大小,直到已发送的字节被确认;
  3. 主机B允许主机A发送更多的字节。

所以,每台主机都需要跟踪哪些字节已经发送了,哪些字节还没有发送,哪些已经确认了。这个流程,和前面以消息为单位的发送限制机制很类似。

为此,TCP引入了缓冲区的概念,将发送方的缓冲区,划分为四个部分。

发送方的缓冲区

如图所示,发送方的缓冲区,被划分为四个部分,具体说明如下。

第一部分:已发送,并且已确认的字节

(Bytes Sent And Acknowledged)

对应于图中的 Category #1 部分,从第1到第31个字节,共31字节。

第二部分:已发送,但尚未确认的字节

(Bytes Sent But Not Yet Acknowledged)

对应于图中的 Category #2 部分,从第32到第45个字节,共14字节。

第三部分:尚未发送,但可以立即发送的字节

(Bytes Not Yet Sent For Which Recipient Is Ready)

对应于图中的 Category #3 部分,从第46到第51个字节,共6字节。

这部分,代表发送方当前还可以发送的字节数,也是接收方当前还可以接收的字节数。

第四部分:尚未发送,并且还不能发送的字节

(Bytes Not Yet Sent For Which Recipient Is Not Ready)

对应于图中的 Category #4 部分,从第52到第95个字节,共44字节。

这部分字节,已经超出了接收方可以接收的最大窗口大小,因此暂不能发送。

发送窗口(Send Window)

黑色方框部分,表示发送窗口(Send Window),通常也简单的称为窗口。

发送窗口由两部分组成,对应于发送缓冲区的第二部分(Category #2,灰色方框内)和第三部分(Category #3,红色方框内)。

如图所示,发送窗口大小为20字节,第二、三部分分别为14字节和6字节。

窗口的左边缘为窗口的第1个字节(第32字节),右边缘为窗口的最后一个字节(第51字节)。

窗口的左边缘位置,决定了窗口的右边缘位置。左边缘是第一和第二部分的分界线,右边缘是第三和第四部分的分界线。

发送窗口的大小,决定了发送方可以同时发送的字节数,即未被接收方确认的字节数。

可用窗口(Usable Window)

可用窗口(Usable Window),对应于缓冲区的第三部分(Category #3),即发送方当前仍可以发送的字节数。

发送消息之后窗口的变化

如果在可用窗口内,发送了一些数据,将会改变发送窗口的位置,以及各部分的大小。

在上面的例子中,发送方的可用窗口为6字节。如果发送方将6个字节的数据发送出去,那么这6个字节将会变成未确认状态,合并到第二部分。

第二部分的范围变为,从第32到第51个字节,共20个字节。

而可用窗口,即窗口的第三部分,将变为0。这通常被称为零窗口(Zero Window)

发送方需要等到有新的确认包到达,才会有可用的窗口可以继续发送数据。

确认消息之后窗口的变化

接收方成功接收了数据之后,会给发送方返回一个确认包。在确认包中,包含了一个确认序号,用于通知发送方,在这个序号之前的数据都已经成功接收了。(确认序号的主要作用,是用来解决不丢包问题。通知接收端已经接收了哪些数据。)

假设,已发送未确认部分(第32到第45字节),是由4个TCP报文段进行传输的。每个报文段的范围分别为:

  • 第一段为32~34字节;
  • 第二段为35~36字节;
  • 第三段为37~41字节;
  • 第四段为42~45字节。

假设第一段、第二段和第四段,都已经成功的发送给接收方了。但是,第三段还没有送达。

那么,接收方将只能确认第一段和第二段,即第32到第36字节已经接收成功了。而对于第四段,还不能进行确认。

这是因为,TCP采用的是累积确认的机制,确认序号表示在这个序号之前(确认序号值减去1)的数据都已经被成功接收。

由于第三段接收方还没有收到,因此不能越过这部分,去确认第四段。否则,发送方会认为第三段也已经被成功的接收了。

(事实上,现在的TCP支持SACK机制,如果启用了,可以支持确认非连续的数据块。)

发送方在接收到确认包之后,窗口将会向右移动5个字节(第32到36共确认5个字节)。而第32到36字节,将会合并到第一部分,变为已发送已确认状态。

由于确认了5个字节,窗口右移之后,又新建了一个5个字节的可用窗口(第52到56字节)。

这个过程会在每次发送方接收到确认包时发生,然后窗口会进行相应的移动,以调整缓冲区各部分的大小。(滑动窗口很形象)

有了滑动窗口机制,就可以使用一个确认序号来确认一整段数据。这为TCP面向字节流的服务,提供了可靠性支持,并且不需要为每一个序号的确认而耗费时间。

丢失确认之后窗口的变化

发送方在发送了第52到56字节的数据之后,就停下来不再发送数据了。因为被卡在了第37到41字节上,这个范围的字节仍然没有返回确认包。

当然,和PAR机制类似的,TCP也支持超时检测和重传数据包。

因此,在计时器超时之后,发送方将重发已丢失的报文段,并希望这一次能够到达目的地。

非常不幸的是,TCP的累积确认机制,仍存在一个缺点,它并不会独立的确认每个报文段。

这意味着,TCP有可能会重复发送那些实际已经被接收方成功接收的报文段。

在我们的例子中,第四段(第42到45字节)已经被成功接收。但只要第三段没有被确认,当超时进行重发时,第四段也会被重发。

(完)


题图:pixabay.com

参考

www.tcpipguide.com/free/t_TCPS…

《TCP/IP 详解》卷1:协议

《趣谈网络协议》

个人公众号

更多文章,请关注公众号:二进制之路

二进制之路