把书读薄(TCP/IP详解 卷一 第二十一章)
为什么需要重传?
TCP本身需要提供可靠的服务,方式之一就是确认接收方真的收到了数据,如果过了一段时间,即超时了,还没有收到确认的报文,认为报文可能被丢失,就重新传送报文,确保数据都能被收到
超时发生重传不一定重传同样的报文段,可以重新分组发送一个较大的报文段,只要它不超过接收方声明的MSS
为什么要动态的计算超时时间?
网络流量和路由器在包的传输过程中可能改变,因此RTT(Round Trip Time)也会变化,如果超时时间保持不变,假如RTT变的大了,可能出现ACK还在再发送的路上,却直接重发了包,造成不必要的浪费
如何动态计算超时重传时间?
TCP经典算法RTT是:R <- αR + (1-α)M
,重传时间为 RTO=Rβ
其中M表示测量时间意思是发送一个某序列号的字节和接收到包含该序列号的确认之间的往返时间
α是一个推荐值为0.9的平滑因子: 对公式稍微做变形为
R <- (1-g)R + gM
并重新组合R<-R+g(M-R)
,假设R是对下一个测量结果的预估,那么 M-R 表示预估的结果和真实结果的偏差,整个公式所表明的就是预估的结果加上部分的预估偏差即为新的预估结果。偏差可能包括两部分,1是噪音,具有一定的随机性,用Er表示,2是预估的错误,选用的R初始值有问题,用Ee表示,则R<-R+g*Er+g*Ee
,对于Ee来讲,它的目标是把预估的结果往正确的方向去靠拢,而Er由于存在一定的随机性,经过多个样本之后,可能最终影响的结果都互相抵消,那么对于预估来讲,希望Ee的因子要大些,Er的因子要小些,使得R的最终取值能够朝着正确的平均值去收敛,而对于Er来讲无论g取何值都会使得结果往好的方向走,因而取g为0.1-0.2能做一个好的值,也就是说α取值为0.9,0.8即可R是估算的RTT的平均值
RTO表示重传超时时间(Retransmission Timeout)意思是如果超过这个时间还没有收到ack就重新发送
β 是RTT的变异系数,当传输时间可以忽略不计的时候,最大时延和平均时延的变化最大,可以看做所有的时延都是因为处理所造成的,这个时候最大值是平均值的两倍,推荐β取值为2。【假设往返时间最大值是R,如果传输时延忽略不计,那么这两次变化的平均传输时延就是0.5R,也就说最大值是平均值的两倍】
β取值,详见 https://tools.ietf.org/html/rfc813 第五章 和 jacobson算法 http://www.cs.binghamton.edu/~nael/cs428-528/deeper/jacobson-congestion.pdf
这种衡量方式没有考虑到,RTT变化范围很大的时候,经典的RTO的变化跟不上,从而引起不必要的重传,此时网络已经处于饱和状态,再重传更会增加网络负载
jacobson算法中提到 β取值为2,此时的负载最多为30%,远不能处理真实的情况
另一个没有没有解决的问题是,假定一个分组被发送,当超时发生时,分组以更长的RTO进行重传,然后收到一个确认,那么收到的这个ACK是针对第一个分组还是第二个分组呢?这种场景的解决方式是Karn算法,主要思想是超时和重传发生时,在重传数据的确认最后到达之前,不能更新RTT估算值
tcp协议当前实现估算超时时间的方法是什么?
使用jacobson算法,RTO依赖于被平滑的RTT和被平滑的均值偏差,而不是均值的常数倍
实现代码https://elixir.bootlin.com/linux/v2.6.32/ident/tcp_rtt_estimator
如何避免分组被丢弃?
使用拥塞避免算法,它假定分组丢失就是因为网络发生了拥塞。发送方使用两个变量来做拥塞控制,一个是拥塞窗口cwnd,一个是慢启动阈值ssthresh,当cwnd小于等于ssthresh时使用慢启动,否则使用拥塞避免算法 。原则如下:
1. 发送方发送的字节小于等于cwnd和接收方通告窗口大小的最小值
2. 发生超时,即在超时定时器溢出时还没有收到ACK,ssthresh被设置为当前窗口大小的一半,cwnd被设置为1个报文段
3. 接收到新ACK时,如果cwnd<=ssthresh,就执行慢启动,cwnd值加1,否则执行拥塞控制,cwnd增加1/cwnd
cwnd值加1会造成窗口按照指数方式增长,比如刚开始是1,那么当它收到ack之后,下次发送两个包,然后会收到两个ack,cwnd立马增长为4,依此类推
cwnd值增加1/cwnd是一种加性增长,每接收到cwnd个包才加1
收到重复ack之后怎么处理?
收到一个重复ack之后,其实无法确认是报文丢失还是报文段重新排序引起的,因此会等待少量重复ack到来,一般会等待3个或者以上。如果连续收到3个或以上的重复ack,则判定可能报文丢失了,选择立马重传,而不需要等待超时定时器溢出,这种方式称为快速重传算法。接下来执行快速恢复算法,两者合并整个过程如下:
1. 收到3个重复的ack之后,将ssthresh的值设置为当前拥塞窗口的一半。重传丢失的报文段,设置cwnd为ssthresh加上3倍的报文段大小【重发了3个,所以增加3个报文段】
2. 每收到另一个重复的ack,cwnd就增加1个报文段大小,如果新的cwnd允许发送,则发送1个分组
3. 确认一个新的ACK到达时,设置cwnd为第一步中设置的ssthresh大小。
这个新的ACK应该是确认第一步中丢失的报文那一刻起发送的报文到第一步中重发的报文期间所有报文,包括第一步中重发的报文。
收到重复ack之后不执行慢启动(即设置cwnd为1),是因为收到重复的ack仅仅表示网络中有数据丢失了。对于接收方而言,只有收到另一个报文段才会产生重复的ack,而该报文已经已经离开网络并进入接收方的缓存,说明,收发端之间数据仍然在流动,不需要执行慢启动来突然减少数据流。 详见 https://tools.ietf.org/html/rfc2001 第4章
整个过程图例如下
状态转移为:
如何对上述过程中涉及的指标进行初始化?
在较新的TCP实现中,有一个路由表来维持指标,包括:被平滑的RTT、被平滑的均值偏差以及慢启动门限。一个TCP连接关闭时,如果已经发送了16个窗口的数据(这就足够多了),且目的节点的路由表不是默认的表向,就会存储起来。建立连接是(部分主动还是被动),只要路由表中有对应的值,就用它初始化
TCP是如何处理给定连接返回的ICMP差错的?
TCP常见的ICMP差错包括源站抑制、主机不可达和网络不可达
1. 主机不可达和网络不可达实际上都被忽略,因为它们被认为是一种短暂现象(可能场景包括路由器替换需要花费几分钟才恢复)。此时的TCP连接没有关闭,反而会发送引起差错的数据
2. 源站抑制引起cwnd被设置为1个报文段大小,从而发起慢启动,但是慢启动的ssthresh不会变化
源站抑制指路由或者主机接收数据的速度比处理的速度快