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];

RST被调用路径为:

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

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

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

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

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.

题外话:
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)

从注释得知,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;  
}

这个代码也能看出,如果报文中包含有效数据,指向数据的 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;
      }
    }
  }
}

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;
}

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

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

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

出现死循环连接!!
这一过程可能会非常快,当设备很多时,会在局域网中形成 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;
}

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;
}

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;
  }
}

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);
      }
    }
  }
}

与保活相关的时间定义在 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 */

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;
}

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

这个信息也很重要,它告诉我们低优先级的 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);
}

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

你可能感兴趣的:(tcp/ip,网络,网络协议)