lwIP 细节之一:TCP 最大链接数

Posted 研究是为了理解

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了lwIP 细节之一:TCP 最大链接数相关的知识,希望对你有一定的参考价值。

lwIP 协议栈为每个 TCP 连接分配不同的 TCP_PCB 控制块。然而 lwIP 能分配的 TCP_PCB 控制块最大数目是编程人员指定的。这是因为嵌入式硬件 RAM 都很少,而每个 TCP_PCB 都要占用 RAM,具体数目根据配置的不同而不同,我这边的典型配置每个 TCP_PCB 占用 164 字节 RAM 。
MEMP_NUM_TCP_PCB 指定了 lwIP 能分配的tcp_pcb 最大数量,即同时有效的 TCP 连接个数
MEMP_NUM_TCP_PCBopt.h 文件中定义,默认 TCP 连接个数为 5。

注:除非特别说明,以下内容针对 lwIP 2.0.0 及以上版本。

/**
 * MEMP_NUM_TCP_PCB: the number of simultaneously active TCP connections.
 * (requires the LWIP_TCP option)
 */
#if !defined MEMP_NUM_TCP_PCB || defined __DOXYGEN__
#define MEMP_NUM_TCP_PCB                5
#endif

如果需要更改这个宏,要在 lwipopts.h 文件中重新定义该宏:

#define MEMP_NUM_TCP_PCB                6       

直接在 opt.h 文件中修改宏 MEMP_NUM_TCP_PCB 可以吗?
不可以!
opt.h 文件是 lwIP 协议栈内核的一部分,其中的宏可以理解为 lwIP 的默认值,如果用户需要更改这些默认值,则需要在 lwipopts.h 文件中重新定义。
lwipopts.h 文件是用户提供的一个头文件,这样做的好处是将来 lwIP 协议栈更新后,可以直接替换内核文件而不影响应用程序的功能。(替换内核文件会覆盖 opt.h 文件,其中的修改也会被还原)
同理,不要图快捷而修改 lwIP 内核的其它文件,这都会给将来的版本升级带来不确定性风险。特别是修改的人已经不在这个项目组,而接手的人又不能了解所有情况时。

对于某个应用程序,如果当前的 TCP 链接数已经达到宏 MEMP_NUM_TCP_PCB 指定的数目,此时再有一个客户端申请连接,lwIP 如何处理?
在 TCP 服务器模式下,负责处理连接接入的是监听(listen)模块。
当客户端发送 SYN 同步标志,表示要建立一个新的连接时,接收到的数据交由 tcp_input 函数处理。由于当前处于监听状态,tcp_input 函数会调用 tcp_listen_input 函数处理这个数据包。
tcp_input 是个功能复杂的函数,我们这里仅观察处于监听状态下,tcp_input 函数的处理流程:

/**
 * The initial input processing of TCP. It verifies the TCP header, demultiplexes
 * the segment between the PCBs and passes it on to tcp_process(), which implements
 * the TCP finite state machine. This function is called by the IP layer (in
 * ip_input()).
 *
 * @param p received TCP segment to process (p->payload pointing to the TCP header)
 * @param inp network interface on which this segment was received
 */
void
tcp_input(struct pbuf *p, struct netif *inp)

	// 通过一系列检测,报文是合法的
	// 通过一系列操作, 在 tcp_listen_pcbs 中查找到了控制块
	tcp_listen_input(lpcb);		// <-- 这里 
    pbuf_free(p);
    return;

tcp_listen_input 函数的主要功能之一是申请一个 TCP_PCB 控制块内存并初始化:

/**
 * Called by tcp_input() when a segment arrives for a listening
 * connection (from tcp_input()).
 *
 * @param pcb the tcp_pcb_listen 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 void
tcp_listen_input(struct tcp_pcb_listen *pcb)

  	// 通过一系列检查 没有错误  	
    npcb = tcp_alloc(pcb->prio);	// <-- 这里 
    if (npcb == NULL) 				// 内存错误处理
      LWIP_DEBUGF(TCP_DEBUG, ("tcp_listen_input: could not allocate PCB\\n"));
      TCP_EVENT_ACCEPT(pcb, NULL, pcb->callback_arg, ERR_MEM, err);
      return;
    
    // 申请成功,初始化新申请的pcb
    // 发送 ACK|SYN 标志
  	return;

但是 TCP_PCB 控制块的数目是有限的,这里调用 tcp_alloc(pcb->prio) 函数申请 TCP_PCB 会因内存不足而失败。lwIP 会处理内存错误,通过宏 TCP_EVENT_ACCEPT(pcb, NULL, pcb->callback_arg, ERR_MEM, err) 调用 tcp_accept API 注册的回调函数。宏展开代码(简化后)如下所示,注意第二个参数为 NULL

if(pcb->accept != NULL)
	pcb->accept(pcb->callback_arg, NULL, ERR_MEM);

如果我们关心 TCP 连接个数是否不足,编程人员就可以在 tcp_accept API 注册的回调函数中检查 err 是否为 ERR_MEM 来检查内存不足错误:

/* 客户端连接时, 回调此函数 */
static err_t telnet_accept(void *arg, struct tcp_pcb *pcb, err_t err)

    char * p_link_info = "已连接到Telnet!\\r\\n";
    
    if(pcb == NULL)
    
    	if(err == ERR_MEM)
    		// 处理 TCP 连接个数不足
        return ERR_OK;
    
    
    tcp_recv(pcb,telnet_recv);
    tcp_err(pcb,NULL);
    pcb->so_options |= SOF_KEEPALIVE;  //增加保活机制
    
    tcp_write(pcb, p_link_info, strlen(p_link_info), TCP_WRITE_FLAG_COPY);
    return ERR_OK;

这里尤其要注意的是,lwIP 2.1.x 版本的 accept 回调函数 必须accept 回调中处理 pcb 句柄为 NULL 的情况!
因此,当连接个数已经用完的前提下,再有客户端连接服务器,服务器不会有任何响应,但会将这个信息以特殊参数的形式提交给编程人员,编程人员在 accept 回调函数内做相应的处理。当然,这个是可选的,也可以不做任何处理。


还没结束!
以上都是针对 lwIP 2.0.0 ~ lwIP 2.1.3 版本,目前仍大量使用的 lwIP 1.4.1 与之不同,我们先看一下 1.4.1 版本的 tcp_listen_input 函数,照例做了简化

static err_t
tcp_listen_input(struct tcp_pcb_listen *pcb)

  	// 通过一系列检查 没有错误  	
    npcb = tcp_alloc(pcb->prio);
    /* If a new PCB could not be created (probably due to lack of memory),
       we don't do anything, but rely on the sender will retransmit the
       SYN at a time when we have more memory available. */
    if (npcb == NULL) 
      LWIP_DEBUGF(TCP_DEBUG, ("tcp_listen_input: could not allocate PCB\\n"));
      return ERR_MEM;
    
    // 申请成功,初始化新申请的pcb
    // 发送 ACK|SYN 标志
  	return ERR_OK;

最大的不同是 1.4.1 版本的 tcp_listen_input 函数有返回值,而 2.0.0 以上版本该函数返回类型为 void
返回值是给调用者使用的,我们再看一下在 1.4.1 版本中, 调用者tcp_input 函数如何使用这个返回值:

void
tcp_input(struct pbuf *p, struct netif *inp)

	// 通过一系列检测,报文是合法的
	// 通过一系列操作, 在 tcp_listen_pcbs 中查找到了控制块
	tcp_listen_input(lpcb);		// <-- 这里 
    pbuf_free(p);
    return;

嗯~ 压根没有用到返回值!!
所以对于 lwIP 1.4.1 版本,当连接个数已经用完的前提下,再有客户端连接服务器,服务器不会有任何响应,编程人员也根本没有途径得知这一信息。






读后有收获,资助博主养娃 - 千金难买知识,但可以买好多奶粉 (〃‘▽’〃)

以上是关于lwIP 细节之一:TCP 最大链接数的主要内容,如果未能解决你的问题,请参考以下文章

lwIP 细节之一:TCP 最大链接数

lwIP 细节之三:TCP 回调函数是何时调用的

LWIP学习之一些细节

lwIP 细节之三:TCP 回调函数是何时调用的(编辑中)

lwIP 细节之三:TCP 回调函数是何时调用的(编辑中)

lwIP 细节之二:协议栈什么情况下发送 RST 标志