TCP的核心算法在lwip中的实现

时间:2022-07-20 04:04:19

      lwip是瑞士计算机科学院的一个开源的TCP/IP协议栈实现.

  LwIP是Light Weight (轻型)IP协议,有无操作系统的支持都可以运行。LwIP实现的重点是在保持TCP协议主要功能的基础上减少对RAM 的占用,一般它只需要几百字节的RAM和40K左右的ROM就可以运行,这使LwIP协议栈适合在低端的嵌入式系统中使用。

      本文主要讨论TCP的核心协议(滑动窗口、拥塞控制、慢启动、快速重传、快速恢复、Nagle算法、捎带ACK等)在lwip中的实现。

      lwip中负责TCP会话管理的核心数据结构是tcp_pcb

1、  滑动窗口

1.1发送窗口的使用:

        网络数据到达时:

更新pcb->snd_wnd

        网络数据发送时:

//发送数据大小不能超过发送窗口和拥塞窗口的最小值。

wnd =MINpcb->snd_wndpcb->cwnd);

        iflen(待发送数据包)> wnd{

暂不发送;

}

       1.2接收窗口的使用:

              网络数据到达时:

                     pcb->rcv_wnd -= len

              应用层读取数据时:

                     pcb->rcv_wnd += len

              rcv_wnd被调整时:

                 u32_t new_right_edge = pcb->rcv_nxt + pcb->rcv_wnd;

                 if(new_right_edge>=pcb->rcv_ann_right_edge + MIN((TCP_WND/2), pcb->mss))

                 {//数据被应用层程序取走,调大接收窗口大小,接收窗口右边沿向右移动。

                         pcb->rcv_ann_wnd = pcb->rcv_wnd;

                 } else {//设置接收窗口大小为0

                         if (pcb->rcv_nxt >= pcb->rcv_ann_right_edge) {

                               pcb->rcv_ann_wnd = 0;

                  } else {//调小接收窗口大小,接收窗口右边沿不动。

                            pcb->rcv_ann_wnd = pcb->rcv_ann_right_edge - pcb->rcv_nxt;

                  }

      

      

2、  RTT估计开启和关闭

2.1 RTT估计的开启

RTT估计的时机:说在内核没有进行rtt 估计时,且不处于超时重传状态(避免重传多义性)

LWIP中的实现:

wnd =MINpcb->snd_wndpcb->cwnd);

if((seg->tcphdr->seqno) - pcb->lastack + seg->len <= wnd)){//要发送的数据位于发送窗口

和拥塞窗口之中

        ……

        ifpcb->rttest == 0{

               开启RTT估计。

}

}

2.2 RTT估计的关闭:

LWIP的实现:

       pcb->rttest 置为0 即关闭rtt 估计,共有三处会关闭rtt 估计:

tcp_receive,完成了rtt 估计的运算,正常关闭,等待下次重新计算rto

tcp_rexmit,快速重传,关闭rtt 估计。

tcp_rexmit_rto,超时重传,关闭rtt 估计。

 

2.3RTT估计算法:

       

RTT计算方法:

        Err = M-A      //ARTT平均值,M是实际测量值,Err是误差

        AA + gErr   //用误差更新平均值

        DD + h( | Err |-D)      //D是均值偏差,用误差更新均值偏差

        RTO = A + 4D       //计算得到RTO(重传超时时间)

        g=0.125 ; h=0.25

 

LWIP的实现:

1、  发送数据时:

if((seg->tcphdr->seqno)- pcb->lastack + seg->len <= wnd)){

              ……

if (pcb->rttest == 0) {//需要重新进行rtt估计

pcb->rttest = tcp_ticks//记录当前时间戳(以500ms为单位)

pcb->rtseq = ntohl(seg->tcphdr->seqno)//记录当前数据分片的序列号      

              }

       }

2、  接收数据时:

if (flags &TCP_ACK) {//数据包含ACK数据确认

       if (pcb->rttest &&TCP_SEQ_LT(pcb->rtseq, ackno)) {//正在进行RTO估算,

       并且用于估算的数据的ACK回来了

          m= (s16_t)(tcp_ticks - pcb->rttest);

          //均值偏差估算算法。

m = m - (pcb->sa >> 3);

          pcb->sa+= m;

          if(m < 0) {

               m= -m;

          }

          m= m - (pcb->sv >> 2);

          pcb->sv+= m;

          pcb->rto= (pcb->sa >> 3) + pcb->sv;

          pcb->rttest= 0;//rto估算完成,准备下一次重新估算。

    }

}

3、超时重传时:

pcb->rto =((pcb->sa >> 3) + pcb->sv) << tcp_backoff[pcb->nrtx];//TCP指数退避重新计算重传时间

 

3、  超时重传、慢启动和拥塞避免

拥塞窗口(congestion windowcwnd),是指发送方在接收到对方的ACK确认前向允许网

络发送的数据量,数据发送后,拥塞窗口缩小;接收到对方的ACK后,拥塞窗口相 应增加,拥塞窗口越大,可发送的数据量越大。拥塞窗口初始值的RFC2581中被规定为不超过发送方MSS的两倍,而且不能超过两个TCP包,在 RFC3390中更新了初始窗口大小的设置方法。

慢启动阈值(slow start threshold, ssthresh),用来判断是否要使用慢启动或拥塞避免算法来

控制流量的一个参数,也是随通信过程不断变化的。

cwnd < ssthresh时,拥塞窗口值已经比较小了,表示未经确认的数据量增大,需要启动慢启动算法;当cwnd > ssthresh时,可发送数据量大,需要启动拥塞避免算法。

拥塞窗口cwnd是根据发送的数据量自动减小的,但扩大就需要根据对方的接收情况进行扩大,慢启动和拥塞避免算法都是描述如何扩大该值的。

在启动慢启动算法时,TCP发送方接收到对方的ACK后拥塞窗口最多每次增加一个发送方MSS字节的数值,当拥塞窗口超过sshresh后或观察到拥塞才停止算法。

启动拥塞避免算法时,拥塞窗口在一个连接往返时间RTT内增加一个最大TCP包长度的量,一般实现时用以下公式计算:

cwnd += max(SMSS*SMSS/cwnd,1) 

 

LWIP实现:

1、  数据到达时:

ifTCP_SEQ_BETWEEN(ackno,pcb->lastack+1, pcb->snd_nxt){//收到了一个正常的ACK

if(pcb->unacked == NULL){//未确认队列为空

pcb->rtime = -1;//关闭重传定时器

              }else {

pcb->rtime = 0;//重启重传定时器

pcb->nrtx = 0;//重传次数清零

}

 

if (pcb->state >= ESTABLISHED) {//会话建立的状态以后的状态,

if (pcb->cwnd < pcb->ssthresh) {//慢启动算法

if ((u16_t)(pcb->cwnd + pcb->mss) > pcb->cwnd) {

pcb->cwnd += pcb->mss;

}

} else {//拥塞避免算法

                  u16_t new_cwnd = (pcb->cwnd +pcb->mss * pcb->mss / pcb->cwnd);

if (new_cwnd > pcb->cwnd) {

pcb->cwnd = new_cwnd;

}

              }

        }

}

2、定时器的实现:LWIP中实现了两个定时器处理函数:tcp_fasttmr()和tcp_slowtmr()。tcp_fasttmr函数是每250ms调用一次;tcp_slowtmr函数每500ms调用一次。超时重传功能是在tcp_slowtmr中实现的。

 

ifpcb->persist_backoff <= 0{//坚持定时器还没有到时

if(pcb->rtime >= 0)//重传定时器开启了

            ++pcb->rtime;//重传定时器++

       }

if (pcb->unacked != NULL && pcb->rtime >=pcb->rto){ //重传定时器到时

if (pcb->state != SYN_SENT) {//重新设定超时重传时间

                pcb->rto= ((pcb->sa >> 3) + pcb->sv) << tcp_backoff[pcb->nrtx];

        }

pcb->rtime = 0;//重传计时器清零

 

eff_wnd = LWIP_MIN(pcb->cwnd, pcb->snd_wnd);

               //重新计算慢启动门限

pcb->ssthresh = eff_wnd >> 1;

(pcb->ssthresh < (pcb->mss << 1)) {

            pcb->ssthresh = (pcb->mss<< 1);

}

pcb->cwnd = pcb->mss;

        //重传,把unasked队列整体移动到unsent队列前端;++pcb->nrtx

tcp_rexmit_rto(pcb);

}

}           

4、  快速重传和快速恢复

TCP接收方收到错序的TCP包时要发送复制的ACK包回应,提示发送方可能出现网络丢包;发送方收到连续3个重复的ACK包后启动快速重传算法,根据确认号快速重传那个可能丢失的包而不必等重传定时器超时后再重传,普通的重传是要等到重传定时器超时还没收到ACK才进行的。这个算法是TCP发送方应该(SHOULD)实现的,不是必须。TCP发送方进行了快速重传后进入快速恢复阶段,直到没再接收重复的ACK包。


快速重传和快速恢复具体过程为:
1.
当收到第3个重复的ACK包时,ssthreh值按ssthresh = max (FlightSize / 2,2*SMSS) 重新设置;

2. 重传丢失的包后,将拥塞窗口cwnd设置为sshresh+3*SMSS,人工扩大了拥塞窗口;

3. 对于每个接收到的重复的ACK包,cwnd相应增加MSS,扩大拥塞窗口;

4. 如果新的拥塞窗口cwnd值和接收方的通告窗口值允许的话,可以继续发新包;

5. 当收到下一个ACK确认了新数据时,将cwnd大小调整为sshresh,减少窗口;对接收方来说,接收到重发的TCP包后就要发此ACK确认当前接收的数据。

 

接收数据时:

if (TCP_SEQ_LEQ(ackno, pcb->lastack)) {//收到一个之前确认过的ACK

pcb->acked = 0;//最近一次ACK确认的数据量为0

if (tcplen == 0) {//数据包长度为0

if (pcb->snd_wl2 + pcb->snd_wnd == right_wnd_edge){//通告接收窗口不变

               if(pcb->rtime >= 0) {//超时重传计时器正在运行

                    if(pcb->lastack == ackno) {//确认序列号和上次的相同

                          found_dupack= 1;

                          if(pcb->dupacks + 1 > pcb->dupacks)

                               ++pcb->dupacks;

                          if(pcb->dupacks > 3) {//人工调整拥塞窗口

                               if ((u16_t)(pcb->cwnd + pcb->mss) >pcb->cwnd) {

                                     pcb->cwnd += pcb->mss;

                               }

                          }else if (pcb->dupacks == 3) {//快速重传

                               tcp_rexmit_fast(pcb);

                                           /*

                                        tcp_rexmit_fast做如下工作:

                                                  wnd= MINpcb->snd_wndpcb-> cwnd);

if (pcb->ssthresh < 2*pcb->mss) {

                                                     pcb->ssthresh= 2*pcb->mss;

                                               }

                                                  pcb->cwnd= pcb->ssthresh + 3 * pcb->mss;

                                               pcb->flags|= TF_INFR;//表示正在进行快速重传

                                           */

}

                             }

                      }

}

}

if (!found_dupack) {//dupack重传计数清零

              pcb->dupacks = 0;

         }

} else if(TCP_SEQ_BETWEEN(ackno, pcb->lastack+1, pcb->snd_nxt)){/*正常ACK*/

if (pcb->flags & TF_INFR) {//该会话正处于快速重传状态

pcb->flags &= ~TF_INFR;   //恢复状态为正常状态

pcb->cwnd = pcb->ssthresh;       //调整拥塞窗口

}

……

       }

5、  Nagle算法

Nagle的文档定义了一种他称之为小封包问题的解决方法。当某个应用程序每次只产生 一字节的数据,就会导致网络由于这样的小封包而过载(该情况通 常被称为发送端SB窗口并发症),从而产生该问题。一个源自键盘的单一字符-1字节的数据-可能导致一个41字节的封包被传送,该封包包含了1字节的 有用数据和40字节的头部数据。

       Nagle算法他的主要职责是数据的累积,实际上有两个门槛:一个就是缓 冲区中的字节数达到了一定量,另一个就是等待了一定的时间(一般的Nagle算法都是等待200ms);这两个门槛的任何一个达到都必须发送数据了。Nagle算法兼顾了实时性和发送效率。

       Nagle算法相关的有两个TCP选线:TCP_NODELAYTCP_CORK

       TCP_NODELAYTCP_CORK基本上控制了包的“去Nagle化”。

       TCP_NODELAY禁用Nagle算法:有数据时立即发送。

       TCP_CORK实际上也禁用了Nagle算法,,这种数据传输方式有益于大量数据的通信性能,典型的应用就是文件服务器。TCP_CORK就相当于一个“塞子”,塞子塞上之后所有网络数据都被塞住,塞子拔掉之后,所有数据一起发送出来。

 

       示例代码如下:

       intfd,on = 1;

      

       setsockopt(fd, SOL_TCP, TCP_CORK, &on, sizeof (on)); /* cork */

       write(fd, …);

       fprintf(fd, …);

       sendfile(fd, …);

       write(fd, …);

       sendfile(fd, …);

      

       on= 0;

       setsockopt(fd, SOL_TCP, TCP_CORK, &on, sizeof (on)); /* 拔去塞子 */

 

       LWIP的实现,LWIPNagle算法的实现是比较“简陋”的,并且只支持TCP_NODELAY选项,并不支持TCP_CORK选项。

       #definetcp_do_output_nagle(tpcb) ((((tpcb)->unacked == NULL) || /

              ((tpcb)->flags& (TF_NODELAY | TF_INFR)) || /

              (((tpcb)->unsent!= NULL) && (((tpcb)->unsent->next != NULL) || /

              ((tpcb)->unsent->len>= (tpcb)->mss))) /

       ) ? 1 : 0)

       tcp_do_output_nagle0时执行Nagle算法,其条件是:unacked队列不能为空、用户没有设置NoDelay选项、会话不能处于快速重传状态、unsent队列为空或者只有一个消息、unsent队列的第一个对象的数据长度要小于mss

 

6、  立即/延迟/捎带ACK

       6.1立即ACK

       1、状态为SYN_SENT时,发送立即ACK

       2、接收到FIN数据包后,发送立即ACK

       3、收到的整个数据包都在pcb->rcv_nxt,立即发送一个dupack

       4、收到tcp长度为0的数据包,并且序列号落在接收窗口中,发送立即ACK

       LWIP实现:

              #definetcp_ack_now(pcb) /

             do {/

                  (pcb)->flags |= TF_ACK_NOW;/    //设置为立即发送标记

             } while (0)

       6.2延迟ACK:稍等看是否有数据可以捎带

       LWIP实现:

       1、有数据到达时:

       #definetcp_ack(pcb) /

             do {/

                  if((pcb)->flags &TF_ACK_DELAY) {/

                (pcb)->flags &=~TF_ACK_DELAY;/

                (pcb)->flags |=TF_ACK_NOW;/

           }/

           else { /

                (pcb)->flags |=TF_ACK_DELAY;/

           } /

      } while (0)

2、  在定时器处理函数tcp_fasttmr()(每250ms调用一次)中

       if (pcb && (pcb->flags &TF_ACK_DELAY)) {//发现TF_ACK_DELAY已经置位了

          tcp_ack_now(pcb);//发送延迟ACK

          tcp_output(pcb);

          pcb->flags&= ~(TF_ACK_DELAY | TF_ACK_NOW);

}

       6.3捎带ACK

       tcp_ack被调用后,tcp_fasttmr被触发前,tcp_output函数被调用,那么:

       if(pcb->state != SYN_SENT) {

           TCPH_SET_FLAG(seg->tcphdr, TCP_ACK);//发送捎带ACK

         pcb->flags &=~(TF_ACK_DELAY | TF_ACK_NOW);//ACK标记位清零。

       }