TCP 实现可靠传输是基于序列号和确认应答号,当一方发送序列号为[1, 1999]
的报文后,另一方收到后会回复2000
的确认应答,如下图所示:
但网络情况是多变的,有很多原因可能导致报文无法顺利达到另一方,如:报文丢失、网络阻塞等,TCP 有四种常见的重传机制保证报文一定可以达到另一方
在一次完整的「请求-响应」中,可能会出现两种丢失的情况:数据包丢失;确认应答丢失。如下图所示:
RTT (Round-Trip Time) 是往返时延,表示一次完整的「请求-响应」时间,即发送方发送数据包到发送方收到接收方的确认应答所需的时间
RTO (Retransmission Timeout) 是超时重传时间,表示当发送方发送数据包后,如果在 RTO 时间内没有收到接收方的确认应答,那么发送方就重传数据包
一般来说,RTO 应略大于 RTT,但 RTT 并非保持不变,随着网络变化,RTT 或快或慢,所以 RTO 的设置比较麻烦
如果 RTO 设置过大,会导致重发慢,报文丢失了半天才开始重发,影响效率
如果 RTO 设置过小,会导致可能报文没有丢失就重发,增加网络阻塞
可以看出超时重传的时间设置难以把握,主要是 RTO 过大会重传不及时,过小会误重传!!
TCP 通信的双方都有缓存,所有并不需要每次都「一请求」「一应答」的模式通信,发送方可以一次发送多个数据包,接收方可以使用累计确认,如下图所示:
超时重传是以时间判断是否需要重传,而快速重传是以收到 ACK 确认应答的数量判断,当连续收到三次冗余数据包对应的确认应答就会触发重传,如下图所示:
注意:收到三次冗余的确认应答表示需要重传,但会发现上图接收方发送了四次 ACK 2,第一次 ACK 2 是正常的确认应答,而后三次才算冗余
快速重传的问题也很明显,无法确定要重传哪些数据包,如上图所示,到底是只用重传 Seq 2 就够了,还是需要重传 Seq 2 以及后面的数据包呢??
针对快速重传的问题提出了选择重传,也就是 SACK (Selective Acknowledgment) 方法,在确认应答中会附带接受方缺失数据包的序列号,如下图所示:
Duplicate SACK 又称 D-SACK,主要使用 SACK 来告诉发送方哪些数据被重复接收,还可以知道重复发送数据的原因:ACK 丢失、网络延迟、包复制
D-SACK 和 SACK 的区别:D-SACK 中第一个连续的 block 是记录重复包的序列空间。如果第一个 block 的范围被 ACK 范围覆盖,表示为 D-SACK;如果第一个 block 的范围被第二个 block 的范围覆盖,也表示为 D-SACK;其余情况都表示 SACK
例子一:ACK 丢失
可以看到第一个 block 的范围[100, 200)
,它被 ACK 范围覆盖,表示收到了重复数据。接收方发送的确认应答号为 300,但 D-SACK 第一个范围为[100, 200)
,表示 300 之前的数据都已经被接收,数据包[100, 200)
早已被接收,所以发送方可以知道是 ACK 丢失导致的重发
例子二:网络延迟
例子三:包复制
强调:可以看出,D-SACK 相比于 SACK 的好处在于不仅可以选择重传,而且可以判断出选择重传的原因,如:ACK 丢失、网络延迟、包复制
前文说过,采用 TCP 协议双方并没有使用「一请求」「一应答」的通信模式,如果 RTT 过长会导致这样通信的效率极低。TCP 引入窗口的概念,窗口的大小指无需等待确认应答,而可以发送的最大数据长度
窗口的底层实现是操作系统开辟了一块缓存空间,发送方在等待确认应答返回之前,必须在缓冲区中保留已发送的数据,如果按期收到确认应答,就将数据从缓冲区中移除
一般来说,接收方会告诉发送方自己还有多少缓冲区可以用来接收数据,发送方会根据接收方的缓冲区大小动态调整发送数据的数量 (单位:字节),否则可能导致接收方处理不过来
所以,发送方窗口大小是根据接收方窗口大小决定滴,且发送方窗口大小不能超过接收方窗口大小
对于发送方,根据数据的处理情况可分为四个部分,如下图所示:
通过三个指针可以表示滑动窗口的关键信息:
SND.WND (SEND.WINDOW):滑动窗口的总大小
SND.UNA (SEND.UNACK):已发送但未收到 ACK 的数据第一个字节的序列号,等价于窗口的第一个字节
SND.NXT (SEND.NEXT):未发送但在接收方处理范围内的数据第一个字节的序列号
通过这三个指针还可以计算出可用窗口大小,也就是未发送但在接收方处理范围内的数据大小,计算公式:SND.WND - (SND.NXT - SND.UNA)
对于接收方,根据数据的处理情况可分为三个部分,如下图所示:
通过两个指针可以表示滑动窗口的关键信息:
RCV.WND (RECEIVE.WINDOW):滑动窗口的总大小
RCV.NXT (RECEIVE.NEXT):未收到但可以接收的数据第一个字节的序列号
发送方不能无脑的向接收方发送数据,需要根据接收方处理数据的能力来决定发送数据的数量,这就是通过滑动窗口来控制
在 TCP 报文段 首部有一个窗口大小字段,它记录了报文段发送者可以接收数据的缓冲区大小,也就是接收方滑动窗口的大小,在每一次通信中都会交换该信息,这表示滑动窗口的大小是动态变化的
前文说过,滑动窗口就是操作系统开辟的一块缓存空间,如果同时收缩窗口和减少缓存可能会出现丢包的情况,因为缓存已经减少,但窗口的收缩还没有同步到发送方,发送方可能发送超过窗口大小的数据。一般都是先收缩窗口,等过一段时间再减少缓存,这样可以保证收缩窗口的操作同步到发送方
窗口关闭是指接收方窗口为 0,发送方无法再给接收方发送数据,除非接收方窗口变为非 0。当接收方窗口从 0 变为非 0,接收方会发送一个 ACK 通知给发送方,通知中包含当前的窗口大小
由于 ACK 不会重传,所以如果通知窗口非 0 的 ACK 丢失,接收方不会重传 ACK,那么发送方永远都不知道窗口变为非 0,就会一直等待非 0 窗口的通知,如下图所示:
为了解决这个问题,TCP 为每个连接设有一个持续计时器,只要 TCP 连接的任一一方收到对方的 0 窗口通知,就启动持续计时器,如果持续计时器超时,就会发送窗口探测 (Window probe) 报文,对方接收该报文后,会返回当前窗口的大小,如下图所示:
注意:一般窗口探测报文会发送三次,每次大约间隔 30 - 60 秒,如果发送三次后接收方窗口依旧为 0,有的 TCP 实现就会发送 RST 报文中断连接
如果接收方处理窗口内数据过慢,会导致发送方的发送窗口越来越小,直至窗口关闭。当接收方只处理了窗口内几个字节的数据后,发送方又可以发送数据,但也只能发送几个字节的数据,这样就有点得不偿失
因为 TCP + IP 头部就有 40 字节,如果数据只占几个字节,无疑白白增加了网络开销。如:一辆公交只有两个乘客就从起点送到终点,白白浪费资源,常见的做法是多等一会,等人多了再发车
上面描述的场景就是糊涂窗口综合症,由于窗口只腾出了几个字节的大小,但发送方也会义无返顾的发送几个字节的数据。针对这个问题,有两种解决方法:
接收方不通知小窗口给发送方,而是等窗口容量变多一点后再告诉发送方自己的窗口大小
发送方不发送小数据给接收方,而是等可以发送的数据变多一点后再一次性发送给接收方
流量控制只考虑接收方能处理数据的大小,发送方根据该大小调整发送数据的数量;而拥塞控制主要考虑通信时的网络情况,根据网络情况调整数据发送的数量
如果完全不考虑网络情况,很容易出现网络拥堵,增加数据包延时,丢失的风险,而且这种情况会无限叠加,也就是不加处理的话网络只会越来越拥堵,丢包数量会越来越多
当网络发生拥塞时,TCP 会自我牺牲,减少发送数据的数量。为了控制发送方发送数据的数量,定义了一个拥塞窗口 (cwnd)
在没有加入拥塞控制之前,发送方窗口大小只和接收方窗口大小有关,但加入拥塞控制后发送方窗口大小和接收方窗口大小、拥塞窗口都有关,即:swnd = min(cwnd, rwnd)
。其中:swnd 表示发送方窗口大小;rwnd 表示接收方窗口大小;cwnd 表示拥塞窗口大小
注意:swnd 和 rwnd 是以字节为单位;而 cwnd 是以报文段为单位,即:MSS。cwnd = 1 表示可以发送一个 MSS 大小的数据包
拥塞窗口的变化规则也很简单:当网络状态良好时,cwnd 增大;当网络出现拥塞时,cwnd 减少
判断网络出现拥塞的标准:只要发送方在规定时间内没有收到 ACK 应答报文,也就是发生了重传,就认为网络出现了拥塞
拥塞控制主要有四个算法:慢启动;拥塞避免;拥塞发生:快速恢复
在 TCP 连接刚刚建立完成后,有一个慢启动的过程,也就是慢慢增加发送数据包的数量。慢启动算法的规则:发送方每收到一个 ACK,拥塞窗口 cwnd 就增加 1
假定发送窗口 swnd 和拥塞窗口 cwnd 相等,慢启动过程的变化如下图所示:
慢启动过程中发送数据包的数量呈指数增加,也就是
有一个叫慢启动门限 ssthresh (slow start threshold) 状态的变量
当 cwnd < ssthresh 时,使用慢启动算法
当 cwnd >= ssthresh 时,使用拥塞避免算法
当 cwnd >= ssthresh 时,将使用拥塞避免算法,该算法的规则:发送方每收到一个 ACK,拥塞窗口 cwnd 就增加 1/cwnd
假设 ssthresh = 8,那么发送方收到 8 个 ACK 时,拥塞窗口就增加 1,如下图所示:
拥塞避免过程中发送数据包的数量呈线程增加,当网络出现拥塞时,也就是出现了重传现象时,进入下一阶段:拥塞发生
当网络出现拥塞时,也就是出现了重传时,将使用拥塞发生算法。重传机制有两种:超时重传和快速重传,不同的重传机制有不同的拥塞发生算法
超时重传的拥塞发生
这种情况下,ssthresh 和 cwnd 的值会发生如下变化:
ssthresh 变为 cwnd / 2
cwnd 重置为初始值 (本例子中假设初始值为 1,但 Linux 中 cwnd 初始值为 10)
下面给出变化图:
快速重传的拥塞发生
这种情况下,ssthresh 和 cwnd 的值会发生如下变化:
cwnd = cwnd / 2
ssthresh = cwnd
进入快速恢复算法
快速重传和快速恢复一般同时使用,快速恢复算法认为可以收到三个冗余的 ACK 确认应答,说明网络拥塞情况没有那么糟糕,拥塞窗口不需要从初始值开始,直接减半即可
在进入快速恢复之前,cwnd 和 ssthresh 已经被更新:cwnd = cwnd / 2;ssthresh = cwnd
快速恢复算法流程如下:
拥塞窗口 cwnd = ssthresh + 3,3 表示确认有 3 个数据包被收到了
重传丢失的数据包
如果再收到重复的 ACK,那么 cwnd 增加 1
如果收到新数据的 ACK,把 cwnd 设置为第一步中的 ssthresh 值,再次进入拥塞避免阶段