lwIP 2.1.x 主要更新详情

Posted 研究是为了理解

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了lwIP 2.1.x 主要更新详情相关的知识,希望对你有一定的参考价值。

主要BUG修复和性能改善

  • 新增对 IPv6 协议的支持
  • 重写 ppp 模块(与 apache pppd 保持同步)
  • 重写 SNMP 模块(包括 MIB 解析器)
  • 修复 DHCP 模块的一个 BUG,该 BUG 可能导致损失 DHCP 租约时间
  • 增强 RX 处理路径的健壮性
  • 支持 TCP 窗口缩放
  • 支持 FreeRTOS-MPU
  • 增强 DNS 健壮性
  • RX 数据包支持使用 PBUF_REF
  • 增加配置宏 LWIP_NETCONN_FULLDUPLEX ,允许 netconn/sockets 用于从每个单独的线程读取/写入
  • 移动和重新排序统计信息(主要用于 memp/mib2

网络接口标志

修改网络接口 netif 的up标志为管理标志。up标志不再具有以前的“IP4 地址有效”含义,现在,如果一个网络接口 netif 没有设置up标志,则用户不能在这个网络接口上收发数据!即便是使用 DHCP,在启动 DHCP 客户端之前,也要将 netif 设置为up状态。

这与lwIP 1.4.1版本不同。可以从两个版本的函数注释上看出来:

lwIP 1.4.1版本lwIP 2.1.2版本
设置网络接口netif为up状态,可用于处理流量。
注意:在为down状态的网络接口上启用DHCP,DHCP完成配置后会将接口设置为up状态。
设置网络接口netif为up状态,可用于处理流量。

通过调用函数netif_set_up(netif),将指定的netif设置为up状态。代码如下:

void netif_set_up(struct netif *netif)

  if (!(netif->flags & NETIF_FLAG_UP)) 
    netif->flags |= NETIF_FLAG_UP;

#if LWIP_SNMP
    snmp_get_sysuptime(&netif->ts);
#endif /* LWIP_SNMP */

    NETIF_STATUS_CALLBACK(netif);

    if (netif->flags & NETIF_FLAG_LINK_UP) 
      netif_issue_reports(netif, NETIF_REPORT_TYPE_IPV4|NETIF_REPORT_TYPE_IPV6);
    
  

可以看到将网络接口设置为up状态是通过为标志字段增加NETIF_FLAG_UP标志实现的。

netif_set_up(netif)做什么?

lwIP协议栈使用一个名为netif网络接口结构来描述物理网卡。负责控制和维护物理网卡状态的函数有数个,其中就有netif_set_upnetif_down_up

netif_set_up设置网络接口为up,这是一个软件标志,用于控制网络接口使能

具体怎么控制?

在IP层,函数ip_route(ip_addr_t *dest)根据指定的目的IP地址找到正确的网络接口 netif。这个函数会判断网络接口是否为up状态,如果网络接口没有up,则返回NULL,即没有找到合适的网络接口:

// 节选自 lwip1.4.1
/* iterate through netifs */
for (netif = netif_list; netif != NULL; netif = netif->next) 
 /* network mask matches? */
 if (netif_is_up(netif)) 									//<--这里
   if (ip_addr_netcmp(dest, &(netif->ip_addr), &(netif->netmask))) 
     /* return netif on which to forward IP packet */
     return netif;
   
 


if ((netif_default == NULL) || (!netif_is_up(netif_default))) //<--这里
 LWIP_DEBUGF(IP_DEBUG | LWIP_DBG_LEVEL_SERIOUS, ("ip_route: No route to %"U16_F".%"U16_F".%"U16_F".%"U16_F"\\n",
   ip4_addr1_16(dest), ip4_addr2_16(dest), ip4_addr3_16(dest), ip4_addr4_16(dest)));
 IP_STATS_INC(ip.rterr);
 snmp_inc_ipoutnoroutes();
 return NULL;


// 注意: lwip 2.1.x 略有不同, 不但检查了netif 是否处于 up 状态, 还检查了物理层连接是否处于 up 状态:
// 节选自lwip 2.1.3
    /* is the netif up, does it have a link and a valid address? */
    if (netif_is_up(netif) && netif_is_link_up(netif) && !ip4_addr_isany_val(*netif_ip4_addr(netif))) 
      /* network mask matches? */
      if (ip4_addr_netcmp(dest, netif_ip4_addr(netif), netif_ip4_netmask(netif))) 
        /* return netif on which to forward IP packet */
        return netif;
      
      /* gateway matches on a non broadcast interface? (i.e. peer in a point to point interface) */
      if (((netif->flags & NETIF_FLAG_BROADCAST) == 0) && ip4_addr_cmp(dest, netif_ip4_gw(netif))) 
        /* return netif on which to forward IP packet */
        return netif;
      
    
  

  if ((netif_default == NULL) || !netif_is_up(netif_default) || !netif_is_link_up(netif_default) ||
      ip4_addr_isany_val(*netif_ip4_addr(netif_default)) || ip4_addr_isloopback(dest)) 
    /* No matching netif found and default netif is not usable.
       If this is not good enough for you, use LWIP_HOOK_IP4_ROUTE() */
    LWIP_DEBUGF(IP_DEBUG | LWIP_DBG_LEVEL_SERIOUS, ("ip4_route: No route to %"U16_F".%"U16_F".%"U16_F".%"U16_F"\\n",
                ip4_addr1_16(dest), ip4_addr2_16(dest), ip4_addr3_16(dest), ip4_addr4_16(dest)));
    IP_STATS_INC(ip.rterr);
    MIB2_STATS_INC(mib2.ipoutnoroutes);
    return NULL;
  

我们再看看应用层的发送函数:

  1. UDP的发送API函数udp_sendudp_sendto都会调用ip_route
  2. TCP的发送API函数tcp_output也会调用ip_route

所以换句话说,如果一个netif没有设置成up状态,应用层发送的数据将被忽略

还是在IP层,函数ip_input用于处理IP数据包。函数会根据收的的IP数据包的目的IP找到正确的网络接口netif。在找的过程中,会判断网络接口是否是up状态,如果网络接口没有up,则表示找不到网络接口。

如果网络接口没有找到,并且数据包不是来自DHCP服务器则数据被丢掉。

/* 寻找网络接口 */
if ((netif_is_up(netif)) && (!ip_addr_isany(&(netif->ip_addr))))  //<--这里
    /* unicast to this interface address? */
    if (ip_addr_cmp(&current_iphdr_dest, &(netif->ip_addr)) ||
        /* or broadcast on this interface network address? */
        ip_addr_isbroadcast(&current_iphdr_dest, netif)) 
        LWIP_DEBUGF(IP_DEBUG, ("ip_input: packet accepted on interface %c%c\\n",
                               netif->name[0], netif->name[1]));
        /* break out of for loop */
        break;
    

在应用层,无论是TCP还是UDP接收API函数,都是从ip_input获取的数据,所以如果一个netif没有设置成up状态,应用层将接收不到数据。

协议栈通过一个软件标志,就可以把应用层发送和接收拿捏的死死的,这样可以方便的处理数量流量。但这只是针对应用层而言的,因为 lwip1.4.1 协议栈给DHCP开了后门。

DHCP发送数据时,直接调用udp_sendto_if,绕过了ip_route函数,即使netif没有设置up也可以发送数据。

接收方面,接收处理函数专门对DHCP开了绿灯,即便找不到合适的网络接口,也能将数据递交给HDCP处理函数,而且师出有名,有RFC 1542背书。

这就产生了不一致:如果使用静态IP,用户必须调用函数netif_set_up,将网络接口设置为up状态,但是如果使用DHCP动态获取IP,在分配到IP后,由DHCP客户端将网络接口设置为up状态。

DHCP客户端的这种操作,会让很多人误用up标志。网络接口设置为up状态的时候,就意味着设备已经分配到IP地址,所以很多人会检查网络接口是否为up状态来判断IP地址是否有效。这让这个标志产生了二义性:本来是一个方便处理数据流量的软件标志,又担当了**“IP地址有效”**的含义。

所以早在2012年8月,lwIP的开发人员Simon Goldschmidt就提出了这个问题:

网络接口netif为down状态时,DHCP 和 AutoIP 仍可以在这个网络接口上正确的发送和接收数据。

我认为这样不够清晰,应该只有一种方式,即在发送或接收时,netif 必须始终处于up状态。DHCP 和AutoIP 的初始化阶段应该只是将 netif 的 IP 地址设置为 ‘0.0.0.0’,并且路由器应该忽略使用 ‘0.0.0.0’ IP 的 netif。

然后在2015年3月份,Simon Goldschmidt修复了netif up/down处理不清晰的问题,也就是本节一开提到的更新内容:

修改网络接口 netif 的up标志为管理标志。up标志不再具有以前的“IP4 地址有效”含义,现在,如果一个网络接口 netif 没有设置up标志,则用户不能在这个网络接口上收发数据!即便是使用 DHCP,在启动 DHCP 客户端之前,也要将 netif 设置为up状态。

lwip2.1.x 是如何堵住DHCP的呢?

  1. 在 dhcp.c 中,去除所有netif_set_upnetif_set_down函数。即 DHCP 不能更新 netif 标志。

  2. 在 dhcp.c 的dhcp_start函数,增加代码:

    LWIP_ERROR("netif is not up, old style port?", netif_is_up(netif), return ERR_ARG;);
    

    如果 netif 不是up状态,则函数返回参数错误(ERR_ARG)代码。即如果 netif 不是up状态,DHCP不能启动。

小插曲:

2015年3月Simon Goldschmidt修复了netif up/down处理不清晰的问题后,函数netif_set_up的注释并没有修改,注释仍显示在为down状态的网络接口上启用DHCP,DHCP完成配置后会将接口设置为up状态。直到2015年的10月份,才由Erik Ekman修改了这个函数的注释,变成了2.1.2版本现在的样子。

IPv6

增加了 IPv6 功能(双栈或 IPv4/IPv6 二选一)

1.4.1 版本也有 IPv6,但那是实验性质的(见…\\lwip-1.4.1\\src\\core\\ipv6目录下的README文件),并不能在实际项目中使用的。

ipv6目录下的README文件内容:

IPv6 support in lwIP is very experimental.
lwIP 对 IPv6 的支持是非常实验性质的.

在 2011 年 5 月,开发者Ivan Delamer编写了可以并行运行 IPv4 和 IPv6 的双栈版本,这也是 lwIP 的第一个可以工作的 IPv6 版本。现在我们在…\\lwip-2.1.2\\src\\core\\ipv6 目录下看到的文件,也是在这个时候成型的。但这个版本仍很初级,并需要大量测试。

所以 2012 年 12 月发布的 lwIP 1.4.1 正式版中并不包括Ivan Delamer编写的IPv6模块。

时间来到了 2016 年 8 月 3 日。这天,开发者goldsimon删除了 ipv6 目录下的README文件,并在提交日志中写下了 IPv6 再也不是实验性质的了 😃:

IPv6 is NOT experimental any more 😃

此时距离第一个可以工作的 IPv6 版本,已经过去了 5 年多,有关 IPv6 的修改提交多达 350+ 次。

改善 IPv4/v6 的地址处理

2011 年 5 月,lwIP1.4.1 版本,IP 地址使用类型ip_addr_t表示。

#define IP_PCB \\
  /* ip addresses in network byte order */ \\
  ip_addr_t local_ip; \\
  ip_addr_t remote_ip; \\
   /* Socket options */  \\
  u8_t so_options;      \\
   /* Type Of Service */ \\
  u8_t tos;              \\
  /* Time To Live */     \\
  u8_t ttl               \\
  /* link layer address resolution hint */ \\
  IP_PCB_ADDRHINT

2011 年 5 月 18,lwIP 终于有了第一个可以并行运行 IPv4 和 IPv6 的双栈版本。随之而来的是,ip.h 文件中宏IP_PCB的定义也发生了变化,变量local_ipremote_ip需要兼容ipv6,所以IP地址使用了联合体表示:

#define IP_PCB \\
  IP_PCB_ISIPV6 \\
  /* ip addresses in network byte order */ \\
  union  \\					/*<--这里*/
    ip_addr_t ip4; \\
    IP_PCB_IP6 \\
   local_ip; \\
  union  \\					/*<--这里*/
    ip_addr_t ip4; \\
    IP_PCB_IP6 \\
   remote_ip; \\
   /* Socket options */  \\
  u8_t so_options;      \\
   /* Type Of Service */ \\
  u8_t tos;              \\
  /* Time To Live */     \\
  u8_t ttl               \\
  /* link layer address resolution hint */ \\
  IP_PCB_ADDRHINT

为了使代码更具可读性,也为了尽可能的合并 IPv4 和 IPv6 代码,2011 年 5 月 26 日,宏IP_PCB有了新的变化:

#define IP_PCB \\
  IP_PCB_ISIPV6_MEMBER \\
  /* ip addresses in network byte order */ \\
  ipX_addr_t local_ip; \\		/*<--这里*/
  ipX_addr_t remote_ip; \\		/*<--这里*/
   /* Socket options */  \\
  u8_t so_options;      \\
   /* Type Of Service */ \\
  u8_t tos;              \\
  /* Time To Live */     \\
  u8_t ttl               \\
  /* link layer address resolution hint */ \\
  IP_PCB_ADDRHINT

这次变化很小,只是定义了一个新的IP地址类型ipX_addr_t,用来代替上个版本的联合体:

#if LWIP_IPV6
/* A union struct for both IP version's addresses. */
typedef union 
  ip_addr_t ip4;
  ip6_addr_t ip6;
 ipX_addr_t;

#else /* LWIP_IPV6 */
typedef ip_addr_t ipX_addr_t;

#endif /* LWIP_IPV6 */

不知道你们是如何看待新的类型ipX_addr_t,我第一次看到它,虽然我还不怎么明白这个类型的含义,就觉得它不应该出现在源码中。

新的类型ipX_addr_t与 lwIP 代码风格很不搭,也容易让人疑惑,大写的X到底代表什么意思。

两年后,2013 年 6 月。Simon Goldschmidt表示对目前的 IPv6,有一件事情仍然不满意,那就是ipX_addr_t的实现。有几点原因:

  1. 不够不清晰。虽然类型ipX_addr_t同时涵盖 IPv4 和 IPv6 地址,但是一旦独立于 PCB(ip_pcb)使用,仍无法分辨类型ipX_addr_t定义的变量是 IPv4 还是 IPv6 地址。

  2. 不易使用。引入ipX_addr_t(或者是说联合体)是为了节省空间,但是Simon Goldschmidt深思之后,认为这不值得。能用 IPv6 的应用,不会计较以字节为单位的内存得失。应该把关注点放到如何更容易使用 IPv6 模块上。

  3. 编译出错。使能 IPv6 之后,下面的 smtp 示例代码无法编译:

    char* ipa = ip_ntoa(&pcb->local_ip);
    

最终,在 2015 年 3 月,为了代码整洁清晰,也为了应用程序的兼容性,Simon Goldschmidt决定删除所有ipX_addr_t

  • ip_addr_t应为通用地址类型,对 API 可见。将ipX_addr_t重命名为ip_addr_t
  • IPv4地址类型从ip_addr_t重命名为ip4_addr_t
  • IPv6地址类型保持不变,仍为ip6_addr_t
  • ip4_addr_tip6_addr_t仅用于协议栈内部,或者调用与版本相关的函数时使用。

这是一项大工程,涉及的文件达到了 74 个,其中新类型ip_addr_t定义为:

#if LWIP_IPV4 && LWIP_IPV6
/**
 * @ingroup ipaddr
 * A union struct for both IP version's addresses.
 * ATTENTION: watch out for its size when adding IPv6 address scope!
 */
typedef struct ip_addr 
  union 
    ip6_addr_t ip6;
    ip4_addr_t ip4;
   u_addr;
  /** @ref lwip_ip_addr_type */
  u8_t type;
 ip_addr_t;

#else /* LWIP_IPV4 && LWIP_IPV6 */

#if LWIP_IPV4
typedef ip4_addr_t ip_addr_t;

#else /* LWIP_IPV4 */
typedef ip6_addr_t ip_addr_t;
#endif /* LWIP_IPV4 */
#endif /* LWIP_IPV4 && LWIP_IPV6 */

lwIP 1.4.1 定义 IP 地址:

struct ip_addr app_ip;

lwIP 2.x.x 定义 IP 地址:

ip_addr_t app_ip;

TCP初始序列号

TCP 报文段首部有一个序列号字段,它是一个32位的计数器,从 0 到 4294967295,它的值为当前报文段中第一个数据的字节序号。TCP 在建立连接的时候需要初始序列号(Initial Sequence Number:ISN),lwIP 为每个新的 TCP 连接生成一个 TCP 初始序列号。tcp.c 中的函数tcp_next_iss用于这个目的。

在1.4.1版本中,这个函数长这样:

/**
 * Calculates a new initial sequence number for new connections.
 *
 * @return u32_t pseudo random sequence number
 */
u32_t
tcp_next_iss(void)

  static u32_t iss = 6510;
  
  iss += tcp_ticks;       /* XXX */
  return iss;

这个算法很简单,算法产生的值是可以预测的。以至于 lwIP 的 TCP 连接可能成为 TCP 欺骗攻击的目标。

对 ISN 的准确预测是 IP 欺骗、数据注入和会话劫持能成功的先决条件。举一个TCP Reset攻击例子:

如果能准确预测ISN,就可以假冒客户端向服务器发送RST包,要求复位连接。由于RST包并不需要向应用层提交,服务器端的 TCP 协议栈只要接到该包就立即终止此次 TCP 连接,这将让真正的客户端连不上服务器,导致了拒绝服务攻击。

根据研究结果表明,Windows 2000、Windows XP SP1 的 ISN 都是可以预测的,从 Windows XP SP2 开始,ISN才能难以预测。

RFC 6528 中标准化了 ISN 生成算法。但是,该算法需要高分辨率计时器和加密散列函数。这远远超出了 lwIP 本身的范围。

所以 lwIP 增加了LWIP_HOOK_TCP_ISN,这是一个钩子函数,可以让开发者根据自己的硬件平台实现自己的 ISN。该钩子函数提供了很大的灵活性,既可以用简单的随机数生成 ISN,也可以实现完整的 RFC 6528 规定的算法。

新的tcp_next_iss函数为:

/**
 * Calculates a new initial sequence number for new connections.
 *
 * @return u32_t pseudo random sequence number
 */
u32_t
tcp_next_iss(struct tcp_pcb *pcb)

#ifdef LWIP_HOOK_TCP_ISN
  LWIP_ASSERT("tcp_next_iss: invalid pcb", pcb != NULL);
  return LWIP_HOOK_TCP_ISN(&pcb->local_ip, pcb->local_port, &pcb->remote_ip, pcb->remote_port);
#else /* LWIP_HOOK_TCP_ISN */
  static u32_t iss = 6510;

  LWIP_ASSERT("tcp_next_iss: invalid pcb", pcb != NULL);
  LWIP_UNUSED_ARG(pcb);

  iss += tcp_ticks;       /* XXX */
  return iss;

核心应用文件移动

将一些核心应用从contrib仓库移动到lwIP仓库的src/apps文件夹。

对比版本lwIP 1.4.1lwIP 2.1.2src文件夹内容,可以发现lwIP 2.1.2版本多了一个apps文件夹。

最开始,也就是 2015 年 10 月 8 日,apps文件夹下只有一个应用sntp客户端,这是一个简单网络时间协议,可以实现与授时服务器对时的功能。

lwIP 1.4.1版本时,sntp客户端还位于contrib-1.4.1\\apps文件夹下。

新版本的 sntp 客户端程序有了少许升级:新的版本通过配置可以使用到 2104 年,而 lwIP 1.4.1 版本只能使用到 2036 年 2 月 7 日。

一天之后,也就是 10 月 9 日,Simon Goldschmidt 为 APP 头文件指定了规则:必须有一个 xxx.h 和一个可选的 xxx_opts.h 。比如 sntp 应用就有一个 sntp.h 和一个 sntp_opts.h 头文件。

还是在这一天,apps 文件夹下添加了 lwiperf 应用。这是一个简单的性能测试客户端/服务器,配合上位机软件 iPerf2 可用于测试运行 lwIP 设备的最大传输速度和带宽。目前仅能测试 TCP

还是在这一天,apps 文件夹下添加了 netbiosns 应用。这是NetBIOS 名称服务器的示例实现。

2015年11月13日,apps 文件夹下添加了 SNMP 应用。在这之前, SNMP 都是内核的一部分(路径 lwip-1.4.1\\src\\core\\snmp),现在 Dirk Ziegelmeier 将它彻底地从内核剥离出来,成为一个应用。

2015 年 11 月 16 日,apps 文件夹下添加了 http 应用。之前位于contrib-1.4.1\\apps文件夹下。

2016 年 8 月 14 日,apps 文件夹下添加了 mNDS 应用。

2016 年 10 月 3 日,apps 文件夹下添加了 tftp 服务器应用。

2016 年 12 月 20 日, apps 文件夹下添加了 MQTT 客户端应用。

2017 年 3 月 29 日, apps 文件夹下添加了 smtp 客户端应用。之前位于contrib-1.4.1\\apps文件夹下。

tcp_alloc

新建 tcp_pcb 控制块时,若内存不足,tcp_alloc 函数会尝试释放正在使用的控制块内存:

  1. 释放处于 TIME_WAIT 状态且存在时间最久的控制块内存;若本步骤没有释放任何内存,则:
  2. 释放优先级更低且存在时间最久的有效(active)控制块内存。

lwIP-1.4.1 和 lwIP-2.1.2 在处理步骤1时是相同的,处理步骤2则不同。

在步骤2中,lwIP-1.4.1 会释放优先级小于等于自己的控制块内存;lwIP-2.1.2 则是释放优先级小于自己的控制块内存。

accept 回调函数

在 TCP 服务器模式下,处于监听状态的 TCP_PCB ,如果收到客户端发送的 SYN 同步标志,表示一个客户端在请求建立连接了。
lwIP 会为这个新连接申请一个 TCP_PCB ,这一过程在 tcp_listen_input 函数中完成的。然而 TCP_PCB 的个数是有限的,如果申请失败,对于失败的处理, lwIP 2.1.x 版本与 lwIP 1.4.1不同。
先看 lwIP 1.4.1 的代码(经简化):

static err_t
tcp_listen_input(struct tcp_pcb_listen *pcb)

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

可以看到, lwIP 1.4.1 版本 tcp_listen_input 函数具有返回值,如果申请 TCP_PCB 失败,则返回 ERR_MEM 错误码。
再看 lwIP 2.1.x 的代码(经简化):

static void
tcp_listen_input(struct tcp_pcb_listen *pcb)

  	// 通过一系列检查 没有错误  	
    npcb = tcp_alloc(pcb->prio);	// 申请新的 TCP_PCB 
    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;

区别很明显,首先 lwIP 2.1.x 版本 tcp_listen_input 函数不具有返回值(返回类型为 void ),其次,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);

这个功能最早是由 Simon Goldschmidt 在 2016-03-23 提交的,提交记录为:

	tcp: call accept-callback with ERR_MEM when allocating a pcb fails on
    passive open to inform the application about this error
	ATTENTION: applications have to handle NULL pcb in accept callback!

tcp:在被动打开分配 pcb 失败时,使用 ERR_MEM 参数调用 accept 回调函数,以通知应用程序有关此错误
注意:应用程序必须在 accept 回调中处理 pcb 句柄为 NULL 的情况!

这就告诉我们一个重要的信息:lwIP 2.1.x 版本的 accept 回调函数编写方式与 lwIP 1.4.1 版本不同。lwIP 2.1.x 版本的 accept 回调函数 必须 在 accept 回调中处理 pcb 句柄为 NULL 的情况!!举个例子。
lwIP 1.4.1 版本的 accept 回调函数可以这么写:

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

    char * p_link_info = "已连接到Telnet!\\r\\n";

    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 回调函数需要这么写:

/* 客户端连接时, 回调此函数 */
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;

这里对 pcb 句柄是否为 NULL 做了处理,如果检测到 NULL,accpet 回调函数需要提前退出!。






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

以上是关于lwIP 2.1.x 主要更新详情的主要内容,如果未能解决你的问题,请参考以下文章

lwIP2.1.x 更新详情

LWIP协议栈:ARP协议

lwIP中的UDP客户端,该怎么解决

Lwip IP包分片重组

《嵌入式 - Lwip开发指南》第1章 LWIP概述

《嵌入式 - Lwip开发指南》第1章 LWIP概述