加载中…
个人资料
老衲五木
老衲五木
  • 博客等级:
  • 博客积分:0
  • 博客访问:358,013
  • 关注人气:330
  • 获赠金笔:0支
  • 赠出金笔:0支
  • 荣誉徽章:
正文 字体大小:

TCP快速恢复重传和Nagle算法《LwIP协议栈源码详解——TCP/IP协议的实现》

(2012-08-28 20:56:42)
标签:

lwip

tcp

重传

nagle

it

分类: 嵌入式网络那些事

前面介绍过,在收到一个失序的报文段时,该报文段会被挂接到ooseg队列上,同时向发送端返回一个ACK(期待的下一个字节),很明显,这个ACK一定是个重复的ACK,且这个重复的ACK被发送出去的时候不会有任何延迟。接收端利用该重复的ACK,目的在于让对方知道收到一个失序的报文段,并告诉对方自己希望收到的序号。

但是在发送方看来,它不可能知道一个重复的ACK是由一个丢失的报文段引起的,还是由于仅仅出现了几个报文段的重新排序引起。因此我们需要等待少量重复的ACK到来。假如这只是一些报文段的重新排序,则在重新排序的报文段被处理并产生一个新的ACK之前,只可能产生1 ~ 2个重复的ACK。如果一连串收到3个或3个以上的重复ACK,就非常可能是一个报文段丢失了。于是我们就重传丢失的数据报文段,而无需等待超时定时器溢出。这就是快速重传算法。在上节讲超时重传时说到,当超时发生后,ssthresh会被设置为有效发送窗口的一半,而cwnd被设置为一个报文段大小,即执行的是慢启动算法。而在这里,当执行完快速重传后,接下来执行的不是慢启动算法而是拥塞避免算法,这就是所谓的快速恢复算法了。

在快速重传后没有执行慢启动的原因在于,由于收到重复的 ACK不仅仅告诉我们一个分组丢失了。而且由于接收方只有在收到另一个报文段,并将该报文段挂接到ooseg队列后,才会产生重复的ACK,这就说明,在收发两端之间仍然有流动的数据,而我们不想执行慢启动来突然减少数据流。

卷一中描述的该算法步骤如下:

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;

}

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

…….

}

上面这段代码有两个地方需要说明一下:一是调用函数tcp_rexmit重传丢失的报文段,这个函数和上一节讲到的函数tcp_rexmit_rto相类似,都是重传数据包。这个函数功能是将unacked队列上的第一个数据段放到unsent队列首部,并调用函数tcp_output输出数据包。第二个需要注意的地方是:在收到三个以上的重复ACK后,代码只是将cwnd的值增加一个报文段大小,而没像向上面2)中所述的那样发送一个数据包。

快速重传与恢复就这么多了,下面是Nagle算法部分。

基于窗口的流量控制方案,会导致一种被称为“糊涂窗口综合症SWS (Silly Window Syndrome)”的状况。当TCP接收方通告了一个小窗口并且 TCP发送方立即发送数据填充该窗口时,SWS 就会发生,当一个小的报文段被确认,窗口再一次以较小单元被打开而发送方将再一次发送一个小的报文段填充这个窗口。这样就会造成 TCP 数据流包含一些非常小的报文段情况的发生,而不是满长度的报文段。糊涂窗口综合症是一种能够导致网络性能严重下降的 TCP 现象,因为小单元的数据段中IP头部和TCP头部这些字段占了大部分空间,而真正的TCP数据却很少。

该现象可能由TCP连接两端中的任何一端引起。这是由于接收方可以通告一个小的窗口(而不是一直等到有大的窗口时才通告),而发送方也可以发送少量的数据(而不是等待其他的数据以便发送一个大的报文段)。

为了避免SWS的发生,在发送方和接收方必须设法消除这种情况。接收方不必通告小窗口更新,并且发送方在只有小窗口提供时不必发送小的报文段。可以在任何一端采取措施避免出现糊涂窗口综合症的现象。

接收方解决SWS的方法是接收方不通告小窗口。通常的算法是接收方不通告一个比当前窗口大的窗口(可以为0),除非窗口可以增加一个报文段大小(也就是将要接收的MSS)或者可以增加接收方缓存空间的一半,不论实际有多少。

发送方避免出现糊涂窗口综合症的措施是只有以下条件之一满足时才发送数据:

(a)可以发送一个满长度的报文段;

(b)可以发送至少是接收方通告窗口大小一半的报文段;

(c)可以发送任何数据并且不希望接收 ACK(也就是说,我们没有还未被确认的数据)或者该连接上不能使用Nagle算法。

条件(b)主要对付那些总是通告小窗口(也许比 1个报文段还小)的主机,它要求发送方始终监视另一方通告的最大窗口大小,这是一种发送方猜测对方接收缓存大小的企图。虽然在连接建立时接收缓存的大小可能会减小,但在实际中这种情况很少见。

条件 (c)使我们在有尚未被确认的数据(正在等待被确认)以及在不能使用Nagle算法的情况下,避免发送小的报文段。如果应用进程在进行小数据的写操作(例如比该报文段还小),条件(c)可以避免出现糊涂窗口综合症。

这三个条件也可以让我们回答这样一个问题:在有尚未被确认数据的情况下,如果Nagle算法阻止我们发送小的报文段,那么多小才算是小呢?从条件 (a)中可以看出所谓“小”就是指字节数小于报文段的大小MSS。

在LwIP中,SWS在发送端就被自然的避免了,因为 TCP 报文段在建立和排队时不知道通告的接收器窗口。在大数据量发送中,输出队列将包括最大尺寸的报文段。这意味着,如果TCP 接收方通告了一个小窗口,发送方将不会发送队列中的第一个报文段,因为它比通告的窗口要大。相反,它会一直等待直至窗口有足够大的空间容下它。当作为 TCP接收方时,LwIP 将不会通告小于连接允许的最大报文段尺寸的接收器窗口(这点未懂)。

来看看LWIP在数据段发送的时候是如何让来避免糊涂窗口的,下面的代码经过一定的删减,只保留了与算法相关的部分。

seg = pcb->unsent; // 取得第一个数据段

while (seg != NULL && ntohl(seg->tcphdr->seqno) - pcb->lastack + seg->len <= wnd) { //整个

// 数据段是否在有效发送窗口内

if((tcp_do_output_nagle(pcb) == 0) &&  // nagle算法阻止发送数据包

((pcb->flags & (TF_NAGLEMEMERR | TF_FIN)) == 0)){ // 有内存错误标志和

break;       //  FIN包标志时,nagle算法失效

}

……

pcb->unsent = seg->next;  // 记录下一个数据段

tcp_output_segment(seg, pcb); // 发送数据段

……

seg = pcb->unsent; // 取得下一个数据段

}

这短短的几句就实现了nagle算法?有太多需要解释!第一个是while的循环条件里面,它要求将要发送的数据段内所有数据序号都必须在有效发送窗口内。这样的话,如果对方通告了一个小窗口,且发送的数据段很大的话则数据段不会被发送出去,这可以看作是上述条件(a) 和条件(b)的变形。对于其他情况,则可以利用Nagle算法来判断是否输出数据包。同时如果flags的TF_NAGLEMEMERR和TF_FIN标志置位时,Nagle算法失效,即数据包不会被Nagle算法阻止。TF_NAGLEMEMERR可以理解为表示内存错误,它是在tcp_enqueue函数组装数据包时出现发送缓存空间不足时被置位的,当这种情况发生时,已经组装好的数据包需要被尽快发送出去,所以Nagle算法在该标志置位时失效;TF_FIN标志置位时说明上层应用发出了关闭连接命令,所以此时也应尽快将该连接上的数据段发送出去,Nagle算法在这种情况下也失效。Nagle算法是通过宏tcp_do_output_nagle来实现的,如下:

#define tcp_do_output_nagle(tpcb) ((((tpcb)->unacked == NULL) || \

((tpcb)->flags & TF_NODELAY) || \

(((tpcb)->unsent != NULL) && ((tpcb)->unsent->next != \

NULL))) ?  1 : 0 )

不要被如此多的括号吓着,我们只关心上式等于0,即Nagle算法阻止数据包发送时的情况。具体为unacked队列不为空,且Nagle算法已使能(TF_NODELAY未置位),且unsent发送队列上有少于两个的待发送数据段时,tcp_do_output_nagle取值为0,tcp_output跳出while循环,不进行任何数据段的发送。

上面这段也可以按照(c)的说法来描述,当没有还未被确认的数据(unacked队列为空),或者Nagle算法未使能,或者unsent队列上有两个或两个以上的数据段时,数据段可以被发送出去。这里比(c)中多了一个unsent队列上数据包个数的限制,此时我们可以把这个多出的限制看作是对条件(a) 和条件(b)的另一种解释。

很多情况下需要禁止Nagle算法,这是通过设置控制块flags字段中的TF_NODELAY标志来实现的。

前面说过,接收方解决SWS的方法是不通告小窗口。但是若使用LwIP作为接收端,它貌似还未实现此功能,即它可能向发送端通告小窗口信息。难道我还没看懂。。。

0

阅读 收藏 喜欢 打印举报/Report
  

新浪BLOG意见反馈留言板 欢迎批评指正

新浪简介 | About Sina | 广告服务 | 联系我们 | 招聘信息 | 网站律师 | SINA English | 产品答疑

新浪公司 版权所有