一、总述
TCP,Transmission Control Protocol,是一个面向连接、基于流式传输的可靠传输协议,考虑到的内容很多,比如数据包的丢失、损坏、分片和乱序等,TCP协议通过多种不同的机制来实现可靠传输。今天,重点分析重传机制、滑动窗口,以及拥塞控制。
二、重传机制
在三握四挥的过程中,服务器和客户端之间就通过带有不同标志位的TCP报文来通知或判断对端或本地是否成功建立、断开连接。
接收主机在接收到数据之后往往都会返回一个应答消息,网络错综复杂,面对随时可能发生的数据丢失问题,TCP使用重传机制解决。常见的重传机制有以下四种:
- 超时重传
- 快速重传
- SACK
- D_SACK
(一) 超时重传
超时重传,顾名思义,在发送数据时,会设定一个定时器,当超过指定的时间过后,没有收到对端的ACK确认应答报文,就会重写发送该数据。而这又可以分为两种情况:
- 数据包丢失
- ACK确认应答丢失
在了解如何设置超时时间之前,先来看看什么是RTT(Round-Trip Time)往返时延。
RTT,往返时延,数据从一端到另一端。其中,往返这个词是表明了什么范围是所需的时间。
知道了RTT,在来看点相关的:RTO,Retransmission Timeout,直译“超时重传时间”。这个时间的设置毫无疑问关系到我们重传机制的效率高低,看以下两种情况:
- RTO>>RTT,重发慢,没有效率;
- RTO<<RTT,包可能还没到就开始重发,重发出去的包数量多了,网络无疑会拥塞,超时的包越来越多,恶性循环。
那么结论显而易见——RTO的值,应该略大于RTT。
很容易想到的是,报文往返的RTT值会是经常变化的,所以RTO也应该是一个动态变化的值。(在Linux中,通常会采样RTT的值然后加权算平均,不详细谈了)
而在超时时,TCP的策略是超时间隔加倍。
(二) 快速重传
Fast Retransmit,快速重传,不以时间为驱动,而以数据驱动重传。
在上图中,Seq2一直没有成功被接收方收到,当发送端收到三个Ack=2的确认,就会在定时器过期之前,重传丢失的Seq2。
不过,发送方并不知道Ack=2是谁传回来的,那么是重传Seq2还是把之前的所有包都重传呢?根据TCP实现的不同,上述两种情况都是可能的。
(三) SACK
SACK是指Selective Acknowledgment,选择性确认,这种方式通过在TCP头部"选项"字段添加一个SACK,把缓存的地图发送给发送方,这样发送方就知道哪些数据需要重传了。
如果要支持SACK,双方都要支持,在Linux下,通过net.ipv4.tcp_sack这个参数打开(Linux2.4后默认打开)。
(四) Duplicate SACK
D_SACK,主要使用SACK来通知发送方有哪些数据被重复接收了,下面通过两个例子来说明,这个Duplicate到底有什么妙用。
- ACK丢包
发送端通过ACK和SACK就可以明确,是发出去的包丢了还是接收方返回的ACK确认报文丢了。
- 网络延时
在判定网络延迟时,Duplicate的含义才更加明显地体现了出来,即复制的、完全一样的。
如上图中提及,在经历了网络延迟和三次相同ACK触发快速重传后,网络延迟的包终于送达,此时返回ACK=3000,SACK=1000~1500(注意之前的SACK范围总是大于ACK),就知道了这个SACK是D_SACK,是重复的包。
三、滑动窗口(流量控制)
(一) 滑动窗口
滑动窗口,Sliding Window,是一种流量控制机制,同时也是一种保持通信效率的技术。已知的是,每当有一个数据包发出,发送端总盼望得到一个ACK确认;那么要是在得到ACK之前不做任何动作,效率的高低明显可见。
为此,TCP引入了窗口的概念,通过指定窗口大小(数据最大值),来进行无需等待确认应答的通信。
在实际实现时,是由操作系统开辟一个缓存空间。在发送方得到确认应答前,已发送的数据都会保存在缓冲区,如果按期收到确认应答,此时数据就可以从缓冲区清除。
如此一来,有了累计确认(或累计应答)模式:
- 那么引申出一个问题——窗口的大小由哪一方决定?
TCP报头中有一个16位的字段:窗口尺寸Window,这个字段是由接收方通知发送方自己还有多少缓冲区可以用来接收数据。以免接收方无法正常接收到数据。
- 发送方,滑动窗口分为4个部分
它的工作方式很容易想到:ACK确认一部分,可用窗口就扩大一部分;当发送窗口满了,在接收ACK之前就不再发送数据。
- 接收方,滑动窗口分为3个部分
值得注意的是,两个窗口的大小是约等于的关系,而不是一模一样。因为滑动窗口不是一成不变的。如果接收方的读取速度有了很大提升,会通过TCP报文通知发送方新的窗口大小。
(二) 窗口关闭问题
TCP中,通过接收方指定窗口尺寸来进行流量控制。在通信中,当接收方窗口被填满,会向发送方说明窗口尺寸位0;等处理好数据后,才会又通告一个窗口非0的ACK报文。不过,要是这个非0报文丢失,就会陷入死锁的状态(双方同时等待)。
- 窗口探测报文
为了解决这个问题,TCP为每个连接设置了一个持续计时器,只要收到0窗口通告,就启动计时器。当计时器超时,就会发送窗口探测报文,这个报文的用意显而易见。(窗口探测的次数一般是3次)。
(三) 糊涂窗口问题
糊涂窗口是指接收方在处理数据时的速度过慢,导致窗口的尺寸不断变小的现象。实际上就是两个动作让这个现象出现:
- 接收方通告小窗口
- 发送方发送小数据
想要避免这种现象,解决上述两个问题就好了。
- 在接收方,当窗口小于min(MSS, 缓存空间/2),就会告知对方0窗口,到后面合适的时机再通知非0窗口。
- 在发送方,使用Nagle算法进行延时处理,要等到发送窗口大小>=MSS,或者接收到ACK确认报文,才会停止囤积数据。这个算法是默认打开的,在使用telnet和ssh等交互性比较强的程序时,通过TCP_NODELAY来关闭。
四、拥塞控制
(一) 什么是拥塞
上文的流量控制是避免发送方的数据填满接收方的缓存,而拥塞控制,则是为了避免在整个网络环境处于拥堵时,还继续发送大量数据包的手段(可能导致数据包时延、丢失等,重传也会加重拥塞)。
那么很明显,拥塞控制是在发送端实现的。为了调节发送数据的量,定义了“拥塞窗口”的概念。
(二) 什么是拥塞窗口
拥塞窗口,是一个由发送方维护的状态变量,根据网络的拥塞程度动态变化。前面的发送窗口和接收窗口在有了拥塞窗口的加入以后,是这样的关系:
- 发送=min(拥塞,接收)
拥塞窗口的动态变化也很简答:有拥塞,就变小;没拥塞,就变大。
(三) 如何判断拥塞
只要发送方没在规定的时间内接收到ACK确认报文(发生超时重传),就会认为网络出现了拥塞。
(四) 拥塞控制算法——慢启动
TCP在刚建立完连接后,会经历慢启动,逐步提高发送数据包的数量。规则是:
- 发送方每收一个ACK,拥塞窗口cwnd的大小就增加1。
所以,这个增长是指数性的。
那什么时候是个头呢?——当达到慢启动门限(slow start threshold)后,就会使用拥塞避免算法。
(五) 拥塞避免算法
慢启动门限ssthresh一般的大小为65535字节,在进入拥塞避免算法后,窗口增长的规则是:
- 每当收到一个ACK,cwnd增加1/cwnd。
如此一来,线性增长。
在此后一直增长,网络就会进入拥塞的状态,出现丢包和丢包重传,触发了重传,也就进入了拥塞发生算法。
(六) 拥塞发生
在上文提到过,重传的机制也有两种:1. 超时重传,2. 快速重传。接下来进行分述:
- 超时重传的拥塞发生算法
ssthresh设置为cwnd/2,cwnd重置为1,然后重新开始慢启动。不过这种方式会突然减少数据流,可能网络卡顿(就像是急刹车)。
- 快速重传的拥塞发生算法
在快速重传时,接收方发送三次前一个包的ACK通知发送端重传(大部分没丢,只丢了小部分)。
cwnd=cwnd/2,ssthresh=cwnd,然后进入快速恢复算法。
(七) 快速恢复
快速重传一般和快速恢复算法同时使用,这种情况会判断网络情况并不是特别严峻,反映也不会像RTO那样强烈。
快速恢复算法:
- cwnd=ssthresh+3
- 重传丢失的数据包
- 如果再收到重复的ACK,cwnd+1
- 如果收到新ACK(说明D_SACK时的数据全部收到,恢复过程结束),cwnd=ssthresh,恢复到之前二点拥塞避免状态。
本文作为笔记,图片来源:
30 张图解: 面试必问的 TCP 重传、滑动窗口、流量控制、拥塞控制_面试回答 tcp流量控制-CSDN博客https://blog.csdn.net/qq_34827674/article/details/105606205