类比的例子
上司给下属交代任务。上司交代任务之后,下属能不能做到,能做到什么程度,什么时候能够交付,需要有应答和回复。
网络协议上,客户端每发送一个包,服务端就应该有个回复,如果服务端超过一定时间没有回复,客户端会重新发包,直到有回复。
日常生活中当然不会交代给下属一件事情,然后就看着他做,等他做完了再给他不止另外一件事情。而应该让他按照你的安排把先把时请记录下来,办完一件回复一件。在他办事的过程当中,你还可以交代新的事情,这样双方就并行了。
更好的办法是:你和下属都准备一个本子,你每交代一个事情,双方的本子都记录一下。当下属做完回复你之后,你在记录里讲对应条目划掉。同时每件事情都有一个最大期限,如果超过期限下属仍然没有回复,就主动重新交代。
既然多件事情可以一起处理,不方标号以防止顺序错乱。
TCP的策略
TCP协议为了保证包的顺序性,每个包都有一个序列号。建立连接的时候会协商起始的序列号,然后按照序列号一个一个发送。为了保证包不丢失,对于发送的包要进行应答,不是每个ID都要应答,而是应答某个ID之前,表示都收到了,叫累计确认或者累计应答。
TCP需要发送端和接收端分别用缓存保存记录。发送端的换村里是按照包的序列号顺序排列,包括:
- 发送且已经确认--->交代下属并且已经完成,需要划掉的任务
- 发送未确认---->交代下属尚未完成完成待下属回复之后划掉
- 待发送--->尚未交代给下属但是会马上交代
- 未发送且暂时不会发送--->尚未交代给下属且暂时也不会交代
3和4的区别在于:把握分寸。过多的工作会导致做不完,进一步加大工作量,会造成恶性循环。
接收端会给发送端抱一个窗口大小,叫Advertised Window,用以标识接收端的处理能力。窗口的大小应该等于上述的3和4的综合,超过这个量,接收端就处理不完了。
发送端维护数据结构:

对于接受端,缓存里面的记录要简单一些:
- 接收并且确认过---->上司安排下属做完的
- 还没接受但是马上要接收的----->我自己能接受的最大工作量
- 尚未接受也没法接收--->超过工作量,实在搞不定

顺序问题和丢包问题
根据上面的图片,发送端看来:1、2、3已经发送并确认;4、5、6、7、8、9发送了尚未确认,10、11、12没发送,13、14、15接收方没有空间,不准备发送。
在接收端来看,1、2、3、4、5完成了ACK,6、7等待接收,8、9已经接收尚未确认。
所以状态为:
- 1、2、3没问题,双方一致。
- 4、5接收说ACK,但是发送端没有收到。可能丢包,也可能在路上。
- 6、7、8、9已发送,8、9已经到了,但是6、7没到,出现乱序,缓存着没办法确认。
确认和重发机制
一种是超时重试。每个发出但是没有ACK的包都设一个定时器,超时就重试。时间不宜果断,必须大于往返时间,否则引起不必要重传;不宜过长,否则访问变慢。估计往返时间,需要TCP通过采样之后加权平均,并且这个值随着网络状况不断变化。除了往返时间,还需要采样RTT的波动范围,计算估计的超时时间。亦即自适应重传算法。
假设5、6、7超时,重发。接收方发现5已经接受过了,于是丢弃5;6收到了发送ACK,要求下一个是7,7不幸又丢了。当7再次超时,需要重传的时候,TCP的策略是超时时间加倍。每当遇到一次超时重传的时候,都会讲下一次超时重传的时间间隔设置为先前值的两倍。两超时,说明网络环境差,不宜频繁反复发送。
问题是可能引发超时周期过长。则有:
快速重传机制。当接收方收到序列号大于期望的报文段时,检测到数据流中间的空格,发送三个冗余的ACK,客户端收到后在定时器过期之前重发。假设收到了6、8、9,7没来,那么肯定丢包了,于是发送三个6的ACK,要求下一个是7.客户端收到,不等超时,马上重发7.
还有方式称为Selective Acknowledgement(SACK).在TCP头加上SACK,把缓存的信息发送给对方。例如发送ACK6、SACK8、SACK9,那么发送端就知道7丢包了,需要 重传。
流量控制
发送端会根据接收端ACK包的窗口大小来调整自己的发送行为。一种极端的情况,假设开始接收端允许发送的窗口长度为9,而接收端一直不去读取缓存中的数据,那么每次会给发送端A一个ACK包,都要把窗口长度减一,因为收到了尚未处理的数据全保存在缓存当中。知道窗口长度为0,这是A就会停止发送。
这样的话,发送方会会定时发送窗口探测数据,看是否有机会调整窗口大小。当接收方处理比较慢的时候要防止低窗口综合征,不要刚空出一点缓存据告诉对方,然后马上又填满了。一般策略,不更新窗口,直到达到一定大小或者缓冲器一半为空,才更新窗口。
拥塞控制
滑动窗口(rwnd)-----> 解决发送方把接收方缓存占满;拥塞窗口(cwnd)-----> 解决数据包把整个网络沾满的问题
拥塞窗口和滑动窗口共同控制发送速度。依据:LastByteSent - LastByteAcked = min{cwnd,rwnd}.TCP的拥塞控制就是不堵塞不丢包的情况下尽量发挥带宽,通道容量 = 带宽 X 往返延迟。如图:

假设往返时间为8s,去4s,返回4s,每秒发送一个包。经过8s,8个包都发送出去了,前4个包已经到达接收端,但是ACK尚未返回,还不能算发送成功。后四个包在路上,尚未被接收。这个时候,管道正好撑满,在发送端已发送未确认的8个包正好等于带宽,即每秒发送1个包,乘以来回时间8s。
如果增大发送速度会怎么样呢?假设,原来发送一个包,从一端到另一端,要经过4个设备,每个设备处理时间为1s。如果增大发送速度,单位时间内到达中间设备的包数量会增加,设备处理能力有限,会强制丢弃一部分数据包。解决办法是给设备增加缓存,使得处理不过来的包排在处理队列里面,这样包就不会丢失了。但是缺点是增加了时延,缓存的包4s肯定不能到达接收端了,试验到达一定程度,会超时重传。
于是TCP拥塞主要避免两种现象:包丢失和超时重传。类比往漏斗里灌水,开始慢慢的加水,发现总能加进去,就可以越来越快,叫做慢启动。
一条TCP连接开始,cwnd设置为一个报文段,一次只能发送一个;收到确认,cwnd加1,于是一次只能发送两个,类推这个过程,明显看出cwnd为指数型增长。增长的极限是ssthresh=65535个字节,达到这个值需要减慢速度,没收到一个确认,cwnd增加1/cwnd.成为线性增长。但是只要在增长,就有溢出来的时候,这个时候就需要降低流水速度,是溢出来的慢慢渗透下去。
拥塞的表现形式之一是丢包,丢包需要重传,把sshresh设定为cwnd/2,设置cwnd为1,重新开始慢启动,立马回到最初状态。
根据快速重传算法,当发现丢包的时候,会发送3次前一个包的ACK,于是发送端就好快速的重传,不必等待超时。TCP认为这种情况不严重,只丢了一小部分,把cwnd减半,sshresh=cwnd,当三个包返回的时候,cend = sshresh + 3,仍然维持在较高的水平。

但是,丢包不代表通道满了,也可能管子本身漏水,比如公网带宽不满也会丢包;TCP的拥塞控制要等到中间设备全都沾满才会丢包,从而降低速度,但是为时已晚。TCP只要占满管道就可以了,不应该继续发包直到占满缓存。所以诞生了TCP BBR拥塞算法。企图找到一个平衡点,将管道填满而不填满缓存。

BBR TCP拥塞算法:
- 慢启动开始时,以前期的延时时间为延迟时间最小值Tmin。然后监控延迟时间是否到达Tmin的n倍,超过阈值则认为已经占满带宽,且使用了一定缓存,进入排空阶段。
- 指数降低发送速率,知道延迟不在降低。
- 协议进入稳定状态。交替探测带宽和延迟,且大多数时间都处于带宽探测阶段。
网友评论