TCP/IP传输层协议实现 - TCP的坚持定时器(lwip)

Posted arm7star

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了TCP/IP传输层协议实现 - TCP的坚持定时器(lwip)相关的知识,希望对你有一定的参考价值。

(参考《TCP-IP详解卷 1:协议》“第22章 TCP的坚持定时器”)

1、糊涂窗口综合症

《TCP-IP详解卷 1:协议》"22.3 糊涂窗口综合症"

基于窗口的流量控制方案,如TCP所使用的,会导致一种被称为“糊涂窗口综合症SWS(Silly Window Syndrome)”的状况。如果发生这种情况,则少量的数据将通过连接进行交换,而不是满长度的报文段 [Clark 1982]。
该现象可发生在两端中的任何一端:接收方可以通告一个小的窗口(而不是一直等到有大的窗口时才通告) ,而发送方也可以发送少量的数据(而不是等待其他的数据以便发送一个大的报文段)。
 

2、避免出现糊涂窗口综合症

2.1、接收方不通告小窗口

接收窗口恢复时,调用tcp_update_rcv_ann_wnd更新通告窗口。

如果新的通告窗口比旧的通告窗口大于等于一个报文,那么用新的接收窗口大小作为通告窗口大小。

  if (TCP_SEQ_GEQ(new_right_edge, pcb->rcv_ann_right_edge + pcb->mss)) { // rcv_ann_right_edge旧的发送窗口的右边沿,new_right_edge - pcb->rcv_ann_right_edge >= pcb->mss表示新的窗口增加大于等于一个mss的报文
    /* we can advertise more window */
    pcb->rcv_ann_wnd = pcb->rcv_wnd;
    return new_right_edge - pcb->rcv_ann_right_edge;
  } else {

如果新的通告窗口增加小于一个mss报文(new_right_edge - pcb->rcv_ann_right_edge < pcb->mss),并且接收窗口已满(下一个待接收的序号大于旧的通告窗口(恢复前的接收窗口)的右边沿,旧的通告窗口通常指向恢复前的接收窗口的右边沿,也就是接收窗口恢复前,可用接收窗口为0),那么,接收窗口恢复后,可用接收窗口大小为(new_right_edge - pcb->rcv_ann_right_edge) + 0,即new_right_edge - pcb->rcv_ann_right_edge,之前已经判断恢复的窗口大小小于一个mss报文,那么接收窗口恢复后,新的接收窗口仍小于一个mss报文,通告窗口设置为0。

    if (TCP_SEQ_GT(pcb->rcv_nxt, pcb->rcv_ann_right_edge)) {
      /* Can happen due to other end sending out of advertised window,
       * but within actual available (but not yet advertised) window */
      pcb->rcv_ann_wnd = 0;
    } else {

如果新的通告窗口增加小于一个mss报文,并且接收窗口恢复前,接收窗口没有占满,那么使用恢复前的接收窗口的可用窗口大小作为新的通告窗口大小。(接收通告窗口右边沿不变,此时的通告窗口大小可能小于一个mss报文,因此需要在发送方采取措施避免出现糊涂窗口综合症)

    } else {
      /* keep the right edge of window constant */
      pcb->rcv_ann_wnd = pcb->rcv_ann_right_edge - pcb->rcv_nxt;
    }

(在数据被应用层接收前,接收窗口的右边沿保持不变,可用接收窗口大小不保证大于等于一个mss报文,因此也需要在发送方采取措施避免出现糊涂窗口综合症)

2.2、发送方避免糊涂窗口综合症

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

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

        ( b ) 可以发送至少是接收方通告窗口大小一半的报文段;(避免发送一个太小的报文,lwip没有实现这个条件,lwip都是按照一个满长度的报文段来发送)

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

lwip在调用tcp_output发送报文时,报文在发送窗口内,调用tcp_do_output_nagle检查报文是否可以发送(非TF_FIN|TF_NAGLEMEMERR,tcp_do_output_nagle返回1时才可以发送)。

#define tcp_do_output_nagle(tpcb) ((((tpcb)->unacked == NULL) || \\
                            ((tpcb)->flags & TF_NODELAY) || \\
                            (((tpcb)->unsent != NULL) && (((tpcb)->unsent->next != NULL) || \\
                              ((tpcb)->unsent->len >= (tpcb)->mss))) \\
                            ) ? 1 : 0)

“(tpcb)->unacked == NULL”: 满足条件(c)中的“我们没有还未被确认的数据”;

"(tpcb)->flags & TF_NODELAY": 禁用了Nagle算法,"#define TF_NODELAY     ((u8_t)0x40U)   /* Disable Nagle algorithm */",lwip中TF_NODELAY标志禁用Nagle算法,满足条件(c)中的“该连接上不能使用Nagle算法”;

"(((tpcb)->unsent != NULL) && (((tpcb)->unsent->next != NULL) || ((tpcb)->unsent->len >= (tpcb)->mss)))": 如果未发送队列不为空且未发送队列有多个报文("((tpcb)->unsent->next != NULL)"表示未发送队列有多个报文,通常两个小报文会合并成一个大的mss报文,未发送队列的第一个报文正常情况下是一个mss大小的报文,即一个满长度的报文段),或者未发送队列的第一个报文的长度大于等于一个报文段("((tpcb)->unsent->len >= (tpcb)->mss)",lwip数据缓存到未发送队列时基本不会超过mss,因此可以认为是一个满长度的报文段),那么满足条件(a)中的“可以发送一个满长度的报文段”。

3、坚持定时器

3.1、启动坚持定时器

(参考《TCP-IP详解卷 2:实现》"25.9 tcp_setpersist函数")

坚持定时器是在发送窗口已满并且还有待发送的数据时启动。

tcp_output会把发送窗口/拥塞窗口内能发送的数据都发送出去,如果发送出去后,还有未发送的报文(seg不为空),那么发送方认为接收方的接收窗口为0(接收方接收到这些数据后,接收窗口减少为0),启动坚持定时器。

  if (seg != NULL && pcb->persist_backoff == 0 && // 发送窗口外有待发送的报文且还没有启动坚持定时器
      ntohl(seg->tcphdr->seqno) - pcb->lastack + seg->len > pcb->snd_wnd) {
    /* prepare for persist timer */
    pcb->persist_cnt = 0; // 复位坚持定时器计数(persist_cnt时间后发送探测报文)
    pcb->persist_backoff = 1; // 探测报文发送次数persist_backoff设置为1(persist_backoff有多个作用,persist_backoff大于0表示启用了坚持定时器,persist_backoff另外还记录坚持定时器的超时次数,坚持定时器超时时间采用退避算法,坚持定时器每次超时时间间隔不一样)
  }

3.2、坚持定时器超时

tcp_slowtmr每次对坚持定时器加1,如果坚持定时器超时,那么persist_backoff加1(persist_backoff直到加到sizeof(tcp_persist_backoff)后,就一直保持不变),调用tcp_zero_window_probe探测接收方的接收窗口。(坚持状态与重传超时之间一个不同的特点就是TCP从不放弃发送窗口探查。(persist_backoff没有次数限制(加到最大后,保持不变))

      if (pcb->persist_backoff > 0) { // 已启用坚持定时器
        /* If snd_wnd is zero, use persist timer to send 1 byte probes
         * instead of using the standard retransmission mechanism. */
        pcb->persist_cnt++; // 坚持定时器加1
        if (pcb->persist_cnt >= tcp_persist_backoff[pcb->persist_backoff-1]) { // 坚持定时器超时
          pcb->persist_cnt = 0; // 坚持定时器重新开始计数
          if (pcb->persist_backoff < sizeof(tcp_persist_backoff)) {
            pcb->persist_backoff++; // persist_backoff加1,persist_backoff永远不会大于sizeof(tcp_persist_backoff),因此TCP从不放弃发送窗口探查
          }
          tcp_zero_window_probe(pcb); // 发送窗口探测报文
        }
      } else { // else分支(超时重传),坚持定时器启动后,不走else分支;坚持定时器启动后,超时重传定时器并没有在计数(坚持定时器超时,调用tcp_zero_window_probe发送探测报文时,tcp_zero_window_probe会发送unacked队列里面的第一个字节,坚持定时器实际上在重传报文,因此不需要用到超时重传定时器;超时重传定时器与坚持定时器互斥)

tcp_zero_window_probe发送窗口探测报文。(《TCP-IP详解》解释的是0窗口探测,在lwip中是发送窗口已满时启用坚持定时器,发送窗口已满表示已经发送接收方接收窗口大小的数据,一定程度上可以理解为接下来接收方的可用接收窗口会变为0,接收方0通告窗口的ACK报文可能在网络中丢失(发送方可能接收不到0通告窗口的报文),因此,发送窗口满的时候,发送方开始启用坚持定时器)

tcp_zero_window_probe拷贝待确认报文的第一个字节组成一个新的重复的数据报文。

void
tcp_zero_window_probe(struct tcp_pcb *pcb)
{
  struct pbuf *p;
  struct tcp_hdr *tcphdr;
  struct tcp_seg *seg;

  LWIP_DEBUGF(TCP_DEBUG, 
              ("tcp_zero_window_probe: sending ZERO WINDOW probe to %"
               U16_F".%"U16_F".%"U16_F".%"U16_F"\\n",
               ip4_addr1(&pcb->remote_ip), ip4_addr2(&pcb->remote_ip),
               ip4_addr3(&pcb->remote_ip), ip4_addr4(&pcb->remote_ip)));

  LWIP_DEBUGF(TCP_DEBUG, 
              ("tcp_zero_window_probe: tcp_ticks %"U32_F
               "   pcb->tmr %"U32_F" pcb->keep_cnt_sent %"U16_F"\\n", 
               tcp_ticks, pcb->tmr, pcb->keep_cnt_sent));

  seg = pcb->unacked; // 获取unacked的第一个报文

  if(seg == NULL)
    seg = pcb->unsent; // 获取unsent的第一个报文

  if(seg == NULL) // 没有待确认的报文,也没有待发送的报文,直接返回,不需要探测
    return;

  p = pbuf_alloc(PBUF_IP, TCP_HLEN + 1, PBUF_RAM); // 申请一个TCP_HLEN + 1的内存(存储tcp首部以及1个数据)
   
  if(p == NULL) {
    LWIP_DEBUGF(TCP_DEBUG, ("tcp_zero_window_probe: no memory for pbuf\\n"));
    return;
  }
  LWIP_ASSERT("check that first pbuf can hold struct tcp_hdr",
              (p->len >= sizeof(struct tcp_hdr)));

  tcphdr = tcp_output_set_header(pcb, p, 0, seg->tcphdr->seqno); // 探测报文的序号设置为unacked或者unsent第一个报文的seqno

  /* Copy in one byte from the head of the unacked queue */
  *((char *)p->payload + sizeof(struct tcp_hdr)) = *(char *)seg->dataptr; // 拷贝一个字节到探测报文里面

#if CHECKSUM_GEN_TCP
  tcphdr->chksum = inet_chksum_pseudo(p, &pcb->local_ip, &pcb->remote_ip,
                                      IP_PROTO_TCP, p->tot_len);
#endif
  TCP_STATS_INC(tcp.xmit);

  /* Send output to IP */
#if LWIP_NETIF_HWADDRHINT
  ip_output_hinted(p, &pcb->local_ip, &pcb->remote_ip, pcb->ttl, 0, IP_PROTO_TCP,
    &(pcb->addr_hint));
#else /* LWIP_NETIF_HWADDRHINT*/
  ip_output(p, &pcb->local_ip, &pcb->remote_ip, pcb->ttl, 0, IP_PROTO_TCP); // 调用ip_output直接发送报文(探测报文不会重发,只有坚持定时器每次超时后会发送一次)
#endif /* LWIP_NETIF_HWADDRHINT*/

  pbuf_free(p);

  LWIP_DEBUGF(TCP_DEBUG, ("tcp_zero_window_probe: seqno %"U32_F
                          " ackno %"U32_F".\\n",
                          pcb->snd_nxt - 1, pcb->rcv_nxt));
}

3.3、关闭坚持定时器

persist_backoff 等于0时表示关闭坚持定时器。

tcp_receive更新发送窗口时,如果新的发送窗口snd_wnd大于0并且已经启动坚持定时器,那么关闭坚持定时器,否则不关闭坚持定时器。

    /* Update window. */
    if (TCP_SEQ_LT(pcb->snd_wl1, seqno) ||
       (pcb->snd_wl1 == seqno && TCP_SEQ_LT(pcb->snd_wl2, ackno)) ||
       (pcb->snd_wl2 == ackno && tcphdr->wnd > pcb->snd_wnd)) {
      pcb->snd_wnd = tcphdr->wnd;
      pcb->snd_wl1 = seqno;
      pcb->snd_wl2 = ackno;
      if (pcb->snd_wnd > 0 && pcb->persist_backoff > 0) {
          pcb->persist_backoff = 0; // 关闭坚持定时器
      }
      LWIP_DEBUGF(TCP_WND_DEBUG, ("tcp_receive: window update %"U16_F"\\n", pcb->snd_wnd));
#if TCP_WND_DEBUG
    } else {

 

 

 

以上是关于TCP/IP传输层协议实现 - TCP的坚持定时器(lwip)的主要内容,如果未能解决你的问题,请参考以下文章

网络基础tcp/ip协议五

TCP/IP协议之四TCP协议(上)—理论+实践给你讲清楚

TCP/IP传输层协议实现 - TCP报文接收/发送(lwip)

TCP/IP 协议——十四章:TCP超时与重传

TCP/IP传输层协议实现 - TCP连接的建立与终止(lwip)

TCP/IP传输层协议实现 - TCP的超时与重传(lwip)