• lwip无法连接指定个数TCP连接问题


    如题,一次测试中发现,lwip的TCP连接控制块明明设置为26,设备作为服务器,电脑作为客户端去连接,但当连接19个之后发现再连接TCP时,机器自动发送RST数据包,通过追踪发现是因为资源分配不足,tcp_alloc失败,后面看到一篇文章,是因为lwip的内存堆使用太小,本人使用的是rt-thread,apm32f407,最后发现是因为程序使用了110KB,总内存是128,所以给rtt只有18KB,内存堆过小。后面将一片空间分配给CCM后解决。

    #pragma  location = 0x10000000
    struct selfstruct_t      s_data[40*1024];
    
    • 1
    • 2

    RST被调用路径为:

    tcp_input->tcp_listen_input->tcp_alloc->tcp_kill_timewait->tcp_abort->tcp_abandon->tcp_rst
    
    • 1

    后面有测试发现只能连接21个,连接22个时自动发送了分手报文。经过排查发现sockets连接定义为52,导致sockets不足,修改宏定义:

    /* MEMP_NUM_NETCONN: the number of struct netconns. */
    #define MEMP_NUM_NETCONN            64 //52
    
    • 1
    • 2

    分手报文发送被调用的路径为:

    lwip_accept->alloc_socket 失败 执行 netconn_delete->
    err_t
    netconn_delete(struct netconn *conn)
    {
      struct api_msg msg;
      msg.function = do_delconn;
      msg.msg.conn = conn;
      tcpip_apimsg(&msg);
      netconn_free(conn);
      return ERR_OK;
    }
    之后再tcpip_thread中->do_delconn->do_close_internal->tcp_close->tcp_close_shutdown->tcp_send_fin->tcp_enqueue_flags.
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    题外话:
    RST 标志是通过 tcp_rst 函数发送的。这个函数声明为:

    /**
     * Send a TCP RESET packet (empty segment with RST flag set) either to
     * abort a connection or to show that there is no matching local connection
     * for a received segment.
     *
     * Called by tcp_abort() (to abort a local connection), tcp_input() (if no
     * matching local pcb was found), tcp_listen_input() (if incoming segment
     * has ACK flag set) and tcp_process() (received segment in the wrong state)
     *
     * Since a RST segment is in most cases not sent for an active connection,
     * tcp_rst() has a number of arguments that are taken from a tcp_pcb for
     * most other segment output functions.
     *
     * @param pcb TCP pcb (may be NULL if no pcb is available)
     * @param seqno the sequence number to use for the outgoing segment
     * @param ackno the acknowledge number to use for the outgoing segment
     * @param local_ip the local IP address to send the segment from
     * @param remote_ip the remote IP address to send the segment to
     * @param local_port the local TCP port to send the segment from
     * @param remote_port the remote TCP port to send the segment to
     */
    void
    tcp_rst(const struct tcp_pcb *pcb, 
    					u32_t seqno, 
    					u32_t ackno,
            			const ip_addr_t *local_ip, 
            			const ip_addr_t *remote_ip,
            			u16_t local_port, 
            			u16_t remote_port)
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30

    从注释得知,tcp_rst 函数发送 TCP RESET 数据包(带有 RST 标志的空帧),用于中止连接或者向对方表明你指定的数据接收者查无此人(接收到的数据帧没有匹配的本地控制块 pcb)。
    tcp_rst 函数主要由以下函数调用:

    tcp_abort 函数:中止一个本地连接
    tcp_input 函数:没有找到匹配的本地控制块 pcb
    tcp_listen_input 函数:接收到的帧设置了 ACK 标志
    tcp_process 函数:接收到包含错误状态的帧
    tcp_input 函数中
    1.本地连接接收端已经关闭,但仍收到数据,调用 tcp_abort 函数,发送 RST 标志,通知远程主机并非所有数据都被处理。简化后的代码如下所示:

    void
    tcp_input(struct pbuf *p, struct netif *inp)
    {
      // 经过一系列检测,没有错误
      
      /* 在本地找到有效的控制块 pcb */
      if (pcb != NULL) {
        err = tcp_process(pcb);
    	/* 报文中包含有效数据 */
    	if (recv_data != NULL) {
    	  if (pcb->flags & TF_RXCLOSED) {
    		/* received data although already closed -> abort (send RST) to
    		   notify the remote host that not all data has been processed */
    		pbuf_free(recv_data);
    		tcp_abort(pcb);
    		goto aborted;
    	  }
    	  
    	  /* Notify application that data has been received. */
    	  TCP_EVENT_RECV(pcb, recv_data, ERR_OK, err);	  
    	}
      }
      return;  
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    这个代码也能看出,如果报文中包含有效数据,指向数据的 pbuf 缓存由应用程序释放,其它情况都是由内核释放。

    2.根据接收到的数据包内容(IP、端口号)查找本地控制块 pcb ,发现匹配的 pcb 处于 TIME_WAIT 状态,则调用 tcp_timewait_input 函数,在这个函数中若满足:报文段包含握手 SYN 标志且报文段编号合法,则调用 tcp_rst 函数发送 RST 标志。

    void
    tcp_input(struct pbuf *p, struct netif *inp)
    {
      // 经过一系列检测,没有错误
      // 在 tcp_active_pcbs 链表中没有找到匹配的控制块 pcb
    	
      if (pcb == NULL) {
    	/*在 tcp_tw_pcbs 链表中查找匹配的控制块 pcb*/
        for (pcb = tcp_tw_pcbs; pcb != NULL; pcb = pcb->next) {
          if (pcb->remote_port == tcphdr->src &&
              pcb->local_port == tcphdr->dest &&
              ip_addr_cmp(&pcb->remote_ip, ip_current_src_addr()) &&
              ip_addr_cmp(&pcb->local_ip, ip_current_dest_addr())) {
            /* 找到匹配的控制块 pcb */
            LWIP_DEBUGF(TCP_INPUT_DEBUG, ("tcp_input: packed for TIME_WAITing connection.\n"));
            tcp_timewait_input(pcb);
            pbuf_free(p);
            return;
          }
        }
      }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    3.根据接收到的数据包内容(IP、端口号)查找本地控制块 pcb ,若发现“查无此人”,则向数据发送者发送 RST 标志,以复位连接。

    void
    tcp_input(struct pbuf *p, struct netif *inp)
    {
      // 经过一系列检测,没有错误
      // 在 tcp_active_pcbs 链表中【没有】找到匹配的控制块 pcb
      // 在 tcp_tw_pcbs 链表中【没有】找到匹配的控制块 pcb
      // 在 tcp_listen_pcbs.listen_pcbs 链表中【没有】找到匹配的控制块 pcb
    	
      /* If no matching PCB was found, send a TCP RST (reset) to the
           sender. */
      LWIP_DEBUGF(TCP_RST_DEBUG, ("tcp_input: no PCB match found, resetting.\n"));
      if (!(TCPH_FLAGS(tcphdr) & TCP_RST)) {
        tcp_rst(NULL, ackno, seqno + tcplen, ip_current_dest_addr(),
                  ip_current_src_addr(), tcphdr->dest, tcphdr->src);
      }
      pbuf_free(p);
      return;
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    这在客户端程序中很有用。

    假如你的设备使用 lwIP 协议栈(裸机),作客户端,使用出错重连机制连接上位机。如果上位机程序关闭,就会出现以下过程:

    设备发送 SYN 建立连接 -> 上位机操作系统找不到匹配的控制块 pcb (因为上位机程序关闭了)-> 上位机发送 RST 标志复位连接 -> 设备触发出错重连 -> 设备发送 SYN 建立连接 ->
    • 1

    出现死循环连接!!
    这一过程可能会非常快,当设备很多时,会在局域网中形成 SYN 风暴。

    这个知识还可以用于判断连接失败原因。
    比如你已经开启了上位机程序,但发现连不上,抓包发现每次连接,上位机都回复 RST 标志,最可能的原因是你连接的端口号错了。

    tcp_process 函数中
    1.处于 SYN_RCVD(LISTEN 状态的服务器,接收到 SYN 握手标志后,进入 SYN_RCVD )状态的连接。

    收到正确的 ACK 标志后,回调 accept 函数,表示有新的连接建立。若 accept 回调函数返回值不是 ERR_OK ,则调用 tcp_abort()函数,发送 RST 标志。
    如果收到的 ACK 序号不合法,则调用 tcp_rst 函数,发送 RST 标志。
    简化后的代码为:

    /**
     * Implements the TCP state machine. Called by tcp_input. In some
     * states tcp_receive() is called to receive data. The tcp_seg
     * argument will be freed by the caller (tcp_input()) unless the
     * recv_data pointer in the pcb is set.
     *
     * @param pcb the tcp_pcb for which a segment arrived
     *
     * @note the segment which arrived is saved in global variables, therefore only the pcb
     *       involved is passed as a parameter to this function
     */
    static err_t
    tcp_process(struct tcp_pcb *pcb)
    {
      /* Do different things depending on the TCP state. */
      switch (pcb->state) {
        case SYN_RCVD:
          if (flags & TCP_ACK) {
            /* expected ACK number? */
            if (TCP_SEQ_BETWEEN(ackno, pcb->lastack + 1, pcb->snd_nxt)) {
              pcb->state = ESTABLISHED;
                tcp_backlog_accepted(pcb);
              /* Call the accept function. */
              TCP_EVENT_ACCEPT(pcb->listener, pcb, pcb->callback_arg, ERR_OK, err);
              if (err != ERR_OK) {
                /* If the accept function returns with an error, we abort
                 * the connection. */
                /* Already aborted? */
                if (err != ERR_ABRT) {
                  tcp_abort(pcb);											// <--这里
                }
                return ERR_ABRT;
              }
            } else {
              /* incorrect ACK number, send RST */
              tcp_rst(pcb, ackno, seqno + tcplen, ip_current_dest_addr(),	// <--这里
                      ip_current_src_addr(), tcphdr->dest, tcphdr->src);
            }
          } 
          break;
      }
      return ERR_OK;
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44

    2.处于 SYN_SEND( CLOSED 状态的客户端,发送 SYN 握手标志后,进入 SYN_SEND )状态的连接,期待收到 SYN + ACK 标志,如果收到的报文中只有 ACK 标志,则调用 tcp_rst 函数,发送 RST 标志。
    简化后的代码为:

    static err_t
    tcp_process(struct tcp_pcb *pcb)
    {
      /* Do different things depending on the TCP state. */
      switch (pcb->state) {
        case SYN_SENT:
          /* received SYN ACK with expected sequence number? */
          if ((flags & TCP_ACK) && (flags & TCP_SYN)
              && (ackno == pcb->lastack + 1)) {
            //处理正确的报文
          }
          /* received ACK? possibly a half-open connection */
          else if (flags & TCP_ACK) {
            /* send a RST to bring the other side in a non-synchronized state. */
            tcp_rst(pcb, ackno, seqno + tcplen, ip_current_dest_addr(),		// <-- 这里
                    ip_current_src_addr(), tcphdr->dest, tcphdr->src);
            /* Resend SYN immediately (don't wait for rto timeout) to establish
              connection faster, but do not send more SYNs than we otherwise would
              have, or we might get caught in a loop on loopback interfaces. */
            if (pcb->nrtx < TCP_SYNMAXRTX) {
              pcb->rtime = 0;
              tcp_rexmit_rto(pcb);
            }
          }
          break;
      }
      return ERR_OK;
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    tcp_rst 函数下面的代码也显示了另外一个小知识,在这种情况下,协议栈会立即重发一个 SYN 握手标志。这样做可以更快的建立连接。

    tcp_close 函数中
    发现还有数据没被应用层处理,或者接收窗口值不正确,则调用调用 tcp_rst 函数,发送 RST 标志。
    简化后的代码为:

    if ((pcb->state == ESTABLISHED) || (pcb->state == CLOSE_WAIT)) {
      if ((pcb->refused_data != NULL) || (pcb->rcv_wnd != TCP_WND_MAX(pcb))) {
        /* don't call tcp_abort here: we must not deallocate the pcb since
           that might not be expected when calling tcp_close */
        tcp_rst(pcb, pcb->snd_nxt, pcb->rcv_nxt, &pcb->local_ip, &pcb->remote_ip,	// <-- 这里
                pcb->local_port, pcb->remote_port);
    
        tcp_pcb_purge(pcb);
        TCP_RMV_ACTIVE(pcb);
        /* Deallocate the pcb since we already sent a RST for it */
        if (tcp_input_pcb == pcb) {
          /* prevent using a deallocated pcb: free it from tcp_input later */
          tcp_trigger_input_pcb_close();
        } else {
          tcp_free(pcb);
        }
        return ERR_OK;
      }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    tcp_slowtmr 函数中
    如果使能保活定时器,并且保活超时(默认超时时间:2 小时 + 9*75 秒),则调用 tcp_rst 函数,发送 RST 标志。
    简化后的代码为:

    /**
     * Called every 500 ms and implements the retransmission timer and the timer that
     * removes PCBs that have been in TIME-WAIT for enough time. It also increments
     * various timers such as the inactivity timer in each PCB.
     *
     * Automatically called from tcp_tmr().
     */
    void
    tcp_slowtmr(void)
    {
        /* Check if KEEPALIVE should be sent */
        if (ip_get_option(pcb, SOF_KEEPALIVE) &&
            ((pcb->state == ESTABLISHED) ||
             (pcb->state == CLOSE_WAIT))) {
          if ((u32_t)(tcp_ticks - pcb->tmr) >
              (pcb->keep_idle + TCP_KEEPCNT_DEFAULT * TCP_KEEPINTVL_DEFAULT) / TCP_SLOW_INTERVAL) {
            LWIP_DEBUGF(TCP_DEBUG, ("tcp_slowtmr: KEEPALIVE timeout. Aborting connection to "));
    
            ++pcb_remove;
            ++pcb_reset;
          } else if ((u32_t)(tcp_ticks - pcb->tmr) >
                     (pcb->keep_idle + pcb->keep_cnt_sent * TCP_KEEPINTVL_DEFAULT )
                     / TCP_SLOW_INTERVAL) {
            err = tcp_keepalive(pcb);
            if (err == ERR_OK) {
              pcb->keep_cnt_sent++;
            }
          }
        }
        /* If the PCB should be removed, do it. */
        if (pcb_remove) {
          if (pcb_reset) {
            tcp_rst(pcb, pcb->snd_nxt, pcb->rcv_nxt, &pcb->local_ip, &pcb->remote_ip,
                    pcb->local_port, pcb->remote_port);
          }
        }
      }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39

    与保活相关的时间定义在 tcp_priv.h 中:

    #ifndef  TCP_KEEPIDLE_DEFAULT
    #define  TCP_KEEPIDLE_DEFAULT     7200000UL /* Default KEEPALIVE timer in milliseconds */
    #endif
    
    #ifndef  TCP_KEEPINTVL_DEFAULT
    #define  TCP_KEEPINTVL_DEFAULT    75000UL   /* Default Time between KEEPALIVE probes in milliseconds */
    #endif
    
    #ifndef  TCP_KEEPCNT_DEFAULT
    #define  TCP_KEEPCNT_DEFAULT      9U        /* Default Counter for KEEPALIVE probes */
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    tcp_listen_input 函数中
    1.处于监听状态的连接只接收 SYN 标志,如果收到了 ACK 标志,则调用 tcp_rst 函数,发送 RST 标志。简化后的代码为:

    static void
    tcp_listen_input(struct tcp_pcb_listen *pcb)
    {
      if (flags & TCP_ACK) {
        /* For incoming segments with the ACK flag set, respond with a RST. */
        LWIP_DEBUGF(TCP_RST_DEBUG, ("tcp_listen_input: ACK in LISTEN, sending reset\n"));
        tcp_rst((const struct tcp_pcb *)pcb, ackno, seqno + tcplen, ip_current_dest_addr(),
                ip_current_src_addr(), tcphdr->dest, tcphdr->src);
      }
      return;
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    2.在 tcp_listen_input 函数中,接收到 SYN 标志后,就会调用 tcp_alloc 函数申请 TCP_PCB 控制块。
    tcp_alloc 函数设计原则是尽一切可能返回一个有效的 TCP_PCB 控制块,因此,当 TCP_PCB 不足时,函数可能 “杀死”(kill)正在使用的连接,以释放 TCP_PCB 控制块!
    具体就是:

    先调用 tcp_kill_timewait 函数,试图找到 TIME_WAIT 状态下生存时间最长的连接,如果找到符合条件的控制块 pcb ,则调用 tcp_abort(pcb) 函数 “杀” 掉这个连接,这会发送 RST 标志,以便通知远端释放连接;
    如果第 1 步失败了,则调用 tcp_kill_state 函数,试图找到 LAST_ACK 和 CLOSING 状态下生存时间最长的连接,如果找到符合条件的控制块 pcb ,则调用 tcp_abandon(pcb, 0) 函数 “杀” 掉这个连接,注意这个函数并不会发送 RST 标志,处于这两种状态的连接都是等到对方发送的 ACK 就会结束连接,不会有数据丢失;
    如果第 2 步也失败了,则调用 tcp_kill_prio(prio) 函数,试图找到小于指定优先级(prio)的最低优先级且生存时间最长的有效(active)连接!如果找到符合条件的控制块 pcb ,则调用 tcp_abort(pcb) 函数 “杀” 掉这个连接,这会发送 RST 标志。
    TCP 连接具有优先级。
    优先级由一个 u8_t 整数指定,数值越小,优先级越低。
    优先级可以在 TCP_PRIO_MIN 和 TCP_PRIO_MAX 之间选择,默认是 TCP_PRIO_NORMAL :

    #define TCP_PRIO_MIN    1
    #define TCP_PRIO_NORMAL 64
    #define TCP_PRIO_MAX    127
    
    • 1
    • 2
    • 3

    这个信息也很重要,它告诉我们低优先级的 TCP 连接可以被高优先级连接给抢占掉!!
    如果一个 TCP 连接很重要,那么你应该手动提高它的优先级。方法是在 accept 回调函数中,使用 tcp_setprio(pcb, new_prio) 函数更改 TCP 连接的优先级:

    static err_t xxxx_protocol_accept(void *arg, struct tcp_pcb *pcb, err_t err)
    {
        if(pcb == NULL)
            return ERR_OK;
        
    	tcp_setprio (pcb, TCP_PRIO_MAX);						//<--这里
    	tcp_recv(pcb, xxxx_protocol_recv);
        tcp_err(pcb, xxxx_protocol_err);
    	
    	pcb->so_options |= SOF_KEEPALIVE;                       //增加保活机制
        
        tcp_link_accept(pcb->remote_ip.addr, pcb->remote_port); //这是我的私有函数, 用于跟踪链接上线
        
    	return(ERR_OK);
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    另外一个需要注意的地方,上面我们说“调用 tcp_kill_prio(prio) 函数,试图找到小于指定优先级(prio)的最低优先级且生存时间最长的有效(active)连接”,这是针对 lwIP 2.0.0 及以上版本说的,lwIP 1.4.1 和这个不同,应表述为:
    对于lwIP 1.4.1 版本,调用 tcp_kill_prio(prio) 函数,试图找到小于等于指定优先级(prio)的最低优先级且生存时间最长的有效(active)连接。
    一个是“小于”,一个是“小于等于”。

  • 相关阅读:
    postgresql-管理数据表
    css3 文本超出容器后显示...以及超出几行后显示...
    Office文件在线预览大全-Word文档在线预览的实现方法-OFD文档在线预览-WPS文件在线预览
    365天搞定八股文——Day 002 内核态和用户态的区别
    order by、limit注入
    前端代码规范常见错误 一
    带头双向循环链表的实现(C语言)
    Spring AOP 底层实现原理
    37、CSS进阶——堆叠上下文及z-index
    Nginx Note03——异步非阻塞机制
  • 原文地址:https://blog.csdn.net/qq_40831436/article/details/133861611