TCP快速重传与快速恢复算法
在收到一个失序的报文段时,该报文段会被挂接到ooseg队列上,同时向发送端返回一个ACK(期待的下一个字节),很明显,这个ACK一定是个重复的ACK,且这个重复的ACK被发送出去的时候不会有任何延迟。接收端利用该重复的ACK,目的在于让对方知道收到一个失序的报文段,并告诉对方自己希望收到的序号。 但是在发送方看来,它不可能知道一个重复的ACK是由一个丢失的报文段引起的,还是由于仅仅出现了几个报文段的重新排序引起。因此我们需要等待少量重复的ACK到来。假如这只是一些报文段的重新排序,则在重新排序的报文段被处理并产生一个新的ACK之前,只可能产生1 ~ 2个重复的ACK。如果一连串收到3个或3个以上的重复ACK,就非常可能是一个报文段丢失了。于是我们就重传丢失的数据报文段,而无需等待超时定时器溢出。这就是快速重传算法。
当超时发生后,ssthresh 会被设置为有效发送窗口的一半,而cwnd被设置为一个报文段大小,即执行的是慢启动算法。而在这里,当执行完快速重传后,接下来执行的不是慢启动算法而是拥塞避免算法,这就是快速恢复算法。在快速重传后没有执行慢启动的原因在于,由于收到重复的 ACK 不仅仅告诉我们一个分组丢失了。而且由于接收方只有在收到另一个报文段,并将该报文段挂接到ooseg队列后,才会产生重复的ACK,这就说明,在收发两端之间仍然有流动的数据,而我们不想执行慢启动来突然减少数据流。
《TCPIP协议详解.卷1》中描述的该算法步骤如下:
1) 当收到第3个重复的ACK时,将ssthresh设置为当前拥塞窗口cwnd的一半。重传丢失的报文段。设置cwnd为ssthresh加上3倍的报文段大小。
2) 每次收到另一个重复的 ACK 时,cwnd增加1个报文段大小并发送 1 个分组(如果新的cwnd允许发送)。
3) 当下一个确认新数据的ACK到达时,设置cwnd为ssthresh(在第1步中设置的值)。这个ACK应该是在进行重传后的一个往返时间内对步骤 1 中重传的确认。另外,这个ACK也应该是对丢失的分组和收到的第1个重复的ACK之间的所有中间报文段的确认。
LWIP 也是在函数tcp_receive 中实现快速恢复与重传的,如下所示,整个过程与上面算法所述基本相同。
if (pcb->lastack == ackno) { // 如果该ACK是个重复的ACK
pcb->acked = 0; // 则被该ACK确认的数据个数为0
if (pcb->snd_wl1 + pcb->snd_wnd == right_wnd_edge){ // 如果进行窗口更新
++pcb->dupacks; // 收到重复确认的次数加1
if (pcb->dupacks >= 3 && pcb->unacked != NULL) { //如1)所述,三个以上充复ACK
if (!(pcb->flags & TF_INFR)) { // 此时快速重传开启,即dupacks为3次
tcp_rexmit(pcb); // 调用函数重传丢失的报文段
if (pcb->cwnd > pcb->snd_wnd) // ssthresh设置为有效发送窗口的一半
pcb->ssthresh = pcb->snd_wnd / 2;
else
pcb->ssthresh = pcb->cwnd / 2;
if (pcb->ssthresh < 2*pcb->mss) { // 修正ssthresh值,最小为2个报文段
pcb->ssthresh = 2*pcb->mss;
}
E-mail:[email protected]
pcb->cwnd = pcb->ssthresh + 3 * pcb->mss;// cwnd为ssthresh+3*报文段大小
pcb->flags |= TF_INFR; // 设置快速重传标志
}
else // 快速重传已经开始,即dupacks大于3次
{
if ((u16_t)(pcb->cwnd + pcb->mss) > pcb->cwnd) // 快速重传已经开始,如2)
pcb->cwnd += pcb->mss; // 每收到一个重复ACK,cwnd增加1个报文段大
} //这与2)中描述的有区别,这里收到重复ACK后没有发送 1 个分组
} // if dupacks 大于3
} //if 如果进行窗口更新
} // if 如果是重复的ACK
else if (TCP_SEQ_BETWEEN(ackno, pcb->lastack+1, pcb->snd_max)){ //确认了新的数据
if (pcb->flags & TF_INFR) { // 正处于快速重传状态,
pcb->flags &= ~TF_INFR; // 清除快速重传标志
pcb->cwnd = pcb->ssthresh; // 如3)所示,设置cwnd的值
}
…….
pcb->dupacks = 0; // 清除重复确认标志
pcb->lastack = ackno; // 记录ackno
…….
}