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

Posted 研究是为了理解

tags:

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

使用 lwIP 协议栈进行 TCP 裸机编程,其本质就是编写协议栈指定的各种回调函数。将你的应用逻辑封装成函数,注册到协议栈,在适当的时候,由协议栈自动调用,所以称为回调

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

向协议栈注册回调函数有专门的接口,如下所示:

tcp_err(pcb, errf);							//注册 TCP 接到 RST 标志或发生错误回调函数 errf
tcp_connect(pcb, ipaddr, port, connected);	//注册 TCP 建立连接成功回调函数 connecter
tcp_accept(pcb, accept);					//注册 TCP 处于 LISTEN 状态时,监听到有新的连接接入
tcp_recv(pcb, recv);						//注册 TCP 接收到数据回调函数 recv
tcp_sent(pcb, sent);						//注册 TCP 发送数据成功回调函数 sent
tcp_poll(pcb, poll, interval);				//注册 TCP 周期性执行回调函数 poll

errf 回调函数

在 TCP 控制块中,函数指针 errf 指向用户实现的 TCP 错误处理函数,当 TCP 连接发送错误时,由协议栈调用此函数。
函数指针 errf 的类型为 tcp_err_fn ,该类型定义在 tcp.h 中:

/** Function prototype for tcp error callback functions. Called when the pcb
 * receives a RST or is unexpectedly closed for any other reason.
 *
 * @note The corresponding pcb is already freed when this callback is called!
 *
 * @param arg Additional argument to pass to the callback function (@see tcp_arg())
 * @param err Error code to indicate why the pcb has been closed
 *            ERR_ABRT: aborted through tcp_abort or by a TCP timer
 *            ERR_RST: the connection was reset by the remote host
 */
typedef void  (*tcp_err_fn)(void *arg, err_t err);

从注释得知,错误处理函数在接收到 RST 标志,或者连接意外关闭时,由协议栈调用。
注意,当这个函数调用的时候,TCP 控制块已经释放掉了。

协议栈通过宏 TCP_EVENT_ERR(last_state,errf,arg,err) 调用 errf 指向的错误处理函数,宏 TCP_EVENT_ERR 定义在 tcp_priv.h 中:

#define TCP_EVENT_ERR(last_state,errf,arg,err)                 \\
  do                                                          \\
    LWIP_UNUSED_ARG(last_state);                               \\
    if((errf) != NULL)                                         \\
      (errf)((arg),(err));                                     \\
   while (0)

可以看到这个宏的第 4 个参数就是传递给错误处理函数的错误码
以关键字 TCP_EVENT_ERR 搜索源码,可以搜索到 4 处使用:

TCP_EVENT_ERR(pcb->state, pcb->errf, pcb->callback_arg, ERR_RST);
TCP_EVENT_ERR(pcb->state, pcb->errf, pcb->callback_arg, ERR_CLSD);
TCP_EVENT_ERR(last_state, errf, errf_arg, ERR_ABRT);
TCP_EVENT_ERR(last_state, err_fn, err_arg, ERR_ABRT);

用到了 3 个错误码:ERR_RSTERR_CLSDERR_ABRT ,分别表示连接复位、连接关闭和连接异常。

1.连接复位

当远端连接发送 RST 标志,并且报文序号正确是,调用错误类型为 ERR_RST 的错误处理回调函数,这一过程在 tcp_input 函数中执行。

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

  // 经过一系列检测,没有错误

  flags = TCPH_FLAGS(tcphdr);	// 这里获取数据包的 [标志] 字段

  /* 在本地找到有效的控制块 pcb */
  if (pcb != NULL) 

    tcp_input_pcb = pcb;
    err = tcp_process(pcb);		// [标志]中有 RST, 且报文序号正确:recv_flags |= TF_RESET
    /* A return value of ERR_ABRT means that tcp_abort() was called
       and that the pcb has been freed. If so, we don't do anything. */
    if (err != ERR_ABRT) 
      if (recv_flags & TF_RESET) 
        /* TF_RESET means that the connection was reset by the other
           end. We then call the error callback to inform the
           application that the connection is dead before we
           deallocate the PCB. */
        TCP_EVENT_ERR(pcb->state, pcb->errf, pcb->callback_arg, ERR_RST);
        tcp_pcb_remove(&tcp_active_pcbs, pcb);
        tcp_free(pcb);
       
    
   
  return;

tcp_process 函数中关于 RST 标志的判断代码:

static err_t
tcp_process(struct tcp_pcb *pcb)

  /* Process incoming RST segments. */
  if (flags & TCP_RST) 		// flags 保存数据包的 [标志] 字段,在 tcp_input 函数中取得		
    /* First, determine if the reset is acceptable. */
    if (pcb->state == SYN_SENT) 
      /* "In the SYN-SENT state (a RST received in response to an initial SYN),
          the RST is acceptable if the ACK field acknowledges the SYN." */
      if (ackno == pcb->snd_nxt) 
        acceptable = 1;
      
     else 
      /* "In all states except SYN-SENT, all reset (RST) segments are validated
          by checking their SEQ-fields." */
      if (seqno == pcb->rcv_nxt) 
        acceptable = 1;
       else  if (TCP_SEQ_BETWEEN(seqno, pcb->rcv_nxt,
                                  pcb->rcv_nxt + pcb->rcv_wnd)) 
        /* If the sequence number is inside the window, we send a challenge ACK
           and wait for a re-send with matching sequence number.
           This follows RFC 5961 section 3.2 and addresses CVE-2004-0230
           (RST spoofing attack), which is present in RFC 793 RST handling. */
        tcp_ack_now(pcb);
      
    

    if (acceptable) 
      LWIP_DEBUGF(TCP_INPUT_DEBUG, ("tcp_process: Connection RESET\\n"));
      recv_flags |= TF_RESET;
      tcp_clear_flags(pcb, TF_ACK_DELAY);
      return ERR_RST;
    
  

2.连接关闭

这部分代码没有理解清楚,暂时保留

3.连接异常

3.1 由 tcp_abandon 函数调用

tcp_abandon 函数会调用错误类型为 ERR_ABRT 的错误回调函数,简化后的代码为:

void
tcp_abandon(struct tcp_pcb *pcb, int reset)

  if (pcb->state == TIME_WAIT) 
    tcp_pcb_remove(&tcp_tw_pcbs, pcb);
    tcp_free(pcb);
   else 
	// 从链表中移除 TCP_PCB
	// 按需释放[未应答]、[未发送]、[失序]报文内存
	// 按需发送 RST 标志
	// 释放 TCP_PCB :tcp_free(pcb)
    TCP_EVENT_ERR(last_state, errf, errf_arg, ERR_ABRT);	// <-- 这里
  

tcp_abandon 函数又是谁在调用呢?

3.1.1 tcp_listen_input 函数中

tcp_listen_input 函数中,检测接收到 SYN 标志报文,则创建新的 TCP_PCB,然后发送 SYN|ACK 标志报文。在这一过程中,若发送 SYN|ACK 标志报文失败,则调用 tcp_abandon 函数放弃这个连接,在 tcp_abandon 函数内部会调用错误类型为 ERR_ABRT 的错误处理回调函数。简化后的代码为:

static void
tcp_listen_input(struct tcp_pcb_listen *pcb)

  /* In the LISTEN state, we check for incoming SYN segments,
     creates a new PCB, and responds with a SYN|ACK. */
  if (flags & TCP_SYN) 
    npcb = tcp_alloc(pcb->prio);
    
	/* 这里 TCP PCB 申请成功,初始化新的 PCB*/
	// ...
    npcb->state = SYN_RCVD;
    // ...

    /* Send a SYN|ACK together with the MSS option. */
    rc = tcp_enqueue_flags(npcb, TCP_SYN | TCP_ACK);
    if (rc != ERR_OK) 
      tcp_abandon(npcb, 0);		// <-- 这里
      return;
    
    tcp_output(npcb);
  
  return;

3.1.2 tcp_kill_state 函数中
《lwIP 细节之二:协议栈什么情况下发送 RST 标志》博文中,有提到 tcp_alloc 函数,tcp_alloc 函数设计原则是尽一切可能返回一个有效的 TCP_PCB 控制块,因此,当 TCP_PCB 不足时,函数可能 “杀死”(kill)正在使用的连接,以释放 TCP_PCB 控制块!
具体就是:

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

这里的第 2 步,调用 tcp_abandon(pcb, 0) 函数 “杀” 掉这个连接时,会调用 tcp_abandon 函数放弃这个连接,在 tcp_abandon 函数内部会调用错误类型为 ERR_ABRT 的错误处理回调函数。简化后的代码为:

static void
tcp_kill_state(enum tcp_state state)

  inactivity = 0;
  inactive = NULL;
  /* Go through the list of active pcbs and get the oldest pcb that is in state
     CLOSING/LAST_ACK. */
  for (pcb = tcp_active_pcbs; pcb != NULL; pcb = pcb->next) 
    if (pcb->state == state) 
      if ((u32_t)(tcp_ticks - pcb->tmr) >= inactivity) 
        inactivity = tcp_ticks - pcb->tmr;
        inactive = pcb;
      
    
  
  if (inactive != NULL) 
    LWIP_DEBUGF(TCP_DEBUG, ("tcp_kill_closing: killing oldest %s PCB %p (%"S32_F")\\n",
                            tcp_state_str[state], (void *)inactive, inactivity));
    /* Don't send a RST, since no data is lost. */
    tcp_abandon(inactive, 0);
  

3.1.3 tcp_abort 函数中
tcp_abort 函数终止一个连接,会向远端主机发送一个 RST 标志。这个函数只能在某个 TCP 回调函数中调用,并返回 ERR_ABRT 错误码(其它情况绝不要返回 ERR_ABRT 错误码,否则可能会有内存泄漏的风险或者访问已经释放的内存!
tcp_abort 函数代码简单,原始无简化代码为:

void
tcp_abort(struct tcp_pcb *pcb)

  tcp_abandon(pcb, 1);

3.2 由 tcp_slowtmr 函数调用

tcp_slowtmr 函数中完成重传和超时处理,当重传达到设定次数,或者超时达到设定时间,则调用错误类型为 ERR_ABRT 的错误处理回调函数。

重传和超时事件有:

  • PCB 控制块处于 SYN_SENT 状态,重传次数达到 TCP_SYNMAXRTX 次(默认 6 次)
  • PCB 控制块处于其它状态,重传次数达到 TCP_MAXRTX 次(默认 12 次)
  • 坚持定时器探查窗口达到 TCP_MAXRTX 次(默认 12 次)
  • PCB 控制块处于 FIN_WAIT_2 状态,超时达到 TCP_FIN_WAIT_TIMEOUT 秒(默认 20 秒)
  • PCB 控制块处于 SYN_RCVD 状态,超时达到 TCP_SYN_RCVD_TIMEOUT 秒(默认 20 秒)
  • PCB 控制块处于 LAST_ACK 状态,超时达到 2 * TCP_MSL 秒(默认 120 秒)
  • 使能保活、PCB 控制块处于 ESTABLISHEDCLOSE_WAIT 状态,超时达到 pcb->keep_idle + TCP_KEEP_DUR(pcb) 秒(默认 2 小时 10 分 48 秒)

tcp_abort 函数简化后的代码为:

/**
 * 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)

  while (pcb != NULL) 
	/* 这里表明处于 CLOSED、LISTEN 和 TIME_WAIT 状态的连接不会有重传 */
    LWIP_ASSERT("tcp_slowtmr: active pcb->state != CLOSED\\n", pcb->state != CLOSED);
    LWIP_ASSERT("tcp_slowtmr: active pcb->state != LISTEN\\n", pcb->state != LISTEN);
    LWIP_ASSERT("tcp_slowtmr: active pcb->state != TIME-WAIT\\n", pcb->state != TIME_WAIT);

    if (pcb->state == SYN_SENT && pcb->nrtx >= TCP_SYNMAXRTX) 
      ++pcb_remove;				// 处于SYN_SENT 状态,重传达到 6 次
      LWIP_DEBUGF(TCP_DEBUG, ("tcp_slowtmr: max SYN retries reached\\n"));
     else if (pcb->nrtx >= TCP_MAXRTX) 
      ++pcb_remove;				// 其它状态,重传达到 12 次
      LWIP_DEBUGF(TCP_DEBUG, ("tcp_slowtmr: max DATA retries reached\\n"));
     else 
      if (pcb->persist_backoff > 0) 
        if (pcb->persist_probe >= TCP_MAXRTX) 
          ++pcb_remove; 		// 探查次数达到 12 次 */
        
    

    if (pcb->state == FIN_WAIT_2) 
      if (pcb->flags & TF_RXCLOSED) 
        if ((u32_t)(tcp_ticks - pcb->tmr) >
            TCP_FIN_WAIT_TIMEOUT / TCP_SLOW_INTERVAL) 
          ++pcb_remove;			// 处于 FIN_WAIT_2 状态,超时达到 20 秒
        
      
    

    /* 注意只有 ESTABLISHED 和 CLOSE_WAIT 状态才会有 KEEPALIVE(保活) */
    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_KEEP_DUR(pcb)) / TCP_SLOW_INTERVAL) 
        ++pcb_remove;			// 使能保活,超时 2 小时 10 分钟 48 秒
        ++pcb_reset;
       
    

    if (pcb->state == SYN_RCVD) 
      if ((u32_t)(tcp_ticks - pcb->tmr) >
          TCP_SYN_RCVD_TIMEOUT / TCP_SLOW_INTERVAL) 
        ++pcb_remove;			// 处于 SYN_RCVD 状态,超时达到 20 秒
      
    

    if (pcb->state == LAST_ACK) 
      if ((u32_t)(tcp_ticks - pcb->tmr) > 2 * TCP_MSL / TCP_SLOW_INTERVAL) 
        ++pcb_remove;			// 处于 LAST_ACK 状态,超时达到 120 秒
      
    

    /* 需要移除 PCB 控制块 */
    if (pcb_remove) 
      tcp_pcb_purge(pcb);		// 释放 PCB 中的数据缓冲区(refused_data、unsent、unacked、ooseq)
      
      if (prev != NULL) 		// 从 tcp_active_pcbs 列表中移除 PCB
        prev->next = pcb->next;
       else 
        tcp_active_pcbs = pcb->next;
      

      if (pcb_reset) 			// 根据需要发送 RST 标志
        tcp_rst(pcb, pcb->snd_nxt, pcb->rcv_nxt, &pcb->local_ip, &pcb->remote_ip,
                pcb->local_port, pcb->remote_port);
      
      tcp_free(pcb2);			// 释放 PCB 控制块内存
	  
	  /* 调用错误回调函数 */
      TCP_EVENT_ERR(last_state, err_fn, err_arg, ERR_ABRT);
     
  

connected 回调函数

在 TCP 控制块中,函数指针 connected 指向用户实现的函数,当一个 PCB 连接到远端主机时,由协议栈调用此函数。
函数指针 connected 的类型为 tcp_connected_fn,该类型定义在 tcp.h 中:

/** Function prototype for tcp connected callback functions. Called when a pcb
 * is connected to the remote side after initiating a connection attempt by
 * calling tcp_connect().
 *
 * @param arg Additional argument to pass to the callback function (@see tcp_arg())
 * @param tpcb The connection pcb which is connected
 * @param err An unused error code, always ERR_OK currently ;-) @todo!
 *            Only return ERR_ABRT if you have called tcp_abort from within the
 *            callback function!
 *
 * @note When a connection attempt fails, the error callback is currently called!
 */
typedef err_t (*tcp_connected_fn)(void *arg, struct tcp_pcb *tpcb, err_t err);

协议栈通过宏 TCP_EVENT_CONNECTED(pcb,err,ret) 调用 pcb->connected 指向的函数,宏 TCP_EVENT_CONNECTED 定义在 tcp_priv.h 中:

#define TCP_EVENT_CONNECTED(pcb,err,ret)                         \\
  do                                                            \\
    if((pcb)->connected != NULL)                                 \\
      (ret) = (pcb)->connected((pcb)->callback_arg,(pcb),(err)); \\
    else (ret) = ERR_OK;                                         \\
   while (0)

以关键字 TCP_EVENT_CONNECTED 搜索源码,可以搜索到 1 处使用:

TCP_EVENT_CONNECTED(pcb, ERR_OK, err);

这句代码在 tcp_process 函数中,PCB 控制块处于 SYN_SENT 状态的连接,收到 SYN + ACK 标志且序号正确,则调用宏 TCP_EVENT_CONNECTED 回调 connected 指向的函数,报告应用层连接

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)) 
        // PCB 字段更新

        /* Call the user specified function to call when successfully connected. */
        TCP_EVENT_CONNECTED(pcb, ERR_OK, err);
        if (err == ERR_ABRT) 
          return ERR_ABRT;
        
        tcp_ack_now(pcb);
      
      break;
  
  return ERR_OK;

accept 回调函数

在 TCP 控制块中,函数指针 accept 指向用户实现的函数,当监听到有新的连接接入时,由协议栈调用此函数,通知用户接受了新的连接或者通知用户内存不足。
函数指针 accept 的类型为 tcp_accept_fn ,该类型定义在 tcp.h 中:

/** Function prototype for tcp accept callback functions. Called when a new
 * connection can be accepted on a listening pcb.
 *
 * @param arg Additional argument to pass to the callback function (@see tcp_arg())
 * @param newpcb The new connection pcb
 * @param err An error code if there has been an error accepting.
 *            Only return ERR_ABRT if you have called tcp_abort from within the
 *            callback function!
 */
typedef err_t (*tcp_accept_fn)(void *arg, struct tcp_pcb *newpcb, err_t err);

协议栈通过宏 TCP_EVENT_ACCEPT(lpcb,pcb,arg,err,ret) 调用 lpcb->accept 指向的函数。宏 TCP_EVENT_ACCEPT 定义在 tcp_priv.h 中:

#define TCP_EVENT_ACCEPT(lpcb,pcb,arg,err,ret)                 \\
  do                                                          \\
    if((lpcb)->accept != NULL)                                 \\
      (ret) = (lpcb)->accept((arg),(pcb),(err));               \\
    else (ret) = ERR_ARG;                                      \\
   while (0)

以关键字 TCP_EVENT_ACCEPT 搜索源码,可以搜索到 2 处使用:

TCP_EVENT_ACCEPT(pcb, NULL, pcb->callback_arg, ERR_MEM, err);
TCP_EVENT_ACCEPT(pcb->listener, pcb, pcb->callback_arg, ERR_OK, err);

1 由 tcp_listen_input 函数调用

处于 LISTEN 状态的 TCP 控制块 ,如果收到客户端发送的 SYN 同步标志,表示一个客户端在请求建立连接了。
lwIP 会为这个新连接申请一个 TCP_PCB ,这一过程在 tcp_listen_input 函数中完成的。然而 TCP_PCB 的个数是有限的,如果申请失败,则会调用错误码为 ERR_MEMaccept 回调函数,向用户报告内存分配失败。简化后的代码为:

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
    
    npcb->state = SYN_RCVD;
    // 发送 ACK|SYN 标志
  	return;

这里需要注意,申请 TCP_PCB 失败的处理方法,lwIP 2.1.x 版本与 lwIP 1.4.1 不同
再看看 lwIP 1.4.1 的 tcp_listen_input 函数代码(经简化):

static err_t
以上是关于lwIP 细节之三:TCP 回调函数是何时调用的的主要内容,如果未能解决你的问题,请参考以下文章

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

LWIP学习之一些细节

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

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

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

LWIP应用指南学习。