使用 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 calledand 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 otherend. We then call the error callback to inform theapplication that the connection is dead before wedeallocate 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 validatedby 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 ACKand 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 stateCLOSING/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 列表中移除 PCBprev->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;}// 申请成功,初始化新申请的pcbnpcb->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
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 版本 tcp_listen_input 函数不具有返回值(返回类型为 void ),其次,lwIP 2.1.x 版本处理内存错误是通过调用 accept 回调函数来实现的。宏展开代码(简化后)如下所示,注意第二个参数为 NULL ,错误码为 ERR_MEM

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 onpassive open to inform the application about this errorATTENTION: 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 回调函数需要提前退出!。

2 由 tcp_process 函数调用

处于 SYN_RCVD 状态的 TCP 控制块,如果接收的正确的 ACK 标志,则调用错误码为 ERR_OKaccept 回调函数,向用户报告接受了新的连接。简化后的代码为:

static err_t
tcp_process(struct tcp_pcb *pcb)
{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;/* 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. */if (err != ERR_ABRT) {tcp_abort(pcb);}return ERR_ABRT;}tcp_receive(pcb);} }break;}return ERR_OK;
}

recv 回调函数

在 TCP 控制块中,函数指针 recv 指向用户实现的函数,当接收到有效数据时,由协议栈调用此函数,通知用户处理接收到的数据。
函数指针 recv 的类型为 tcp_recv_fn ,该类型定义在 tcp.h 中:

/** Function prototype for tcp receive callback functions. Called when data has* been received.** @param arg Additional argument to pass to the callback function (@see tcp_arg())* @param tpcb The connection pcb which received data* @param p The received data (or NULL when the connection has been closed!)* @param err An error code if there has been an error receiving*            Only return ERR_ABRT if you have called tcp_abort from within the*            callback function!*/
typedef err_t (*tcp_recv_fn)(void *arg, struct tcp_pcb *tpcb,struct pbuf *p, err_t err);

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

#define TCP_EVENT_RECV(pcb,p,err,ret)                          \do {                                                         \if((pcb)->recv != NULL) {                                  \(ret) = (pcb)->recv((pcb)->callback_arg,(pcb),(p),(err));\} else {                                                   \(ret) = tcp_recv_null(NULL, (pcb), (p), (err));          \}                                                          \} while (0)

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

TCP_EVENT_RECV(pcb, recv_data, ERR_OK, err);
TCP_EVENT_RECV(pcb, refused_data, ERR_OK, err);

1 由 tcp_input 函数调用

指针 recv_data 是一个 struct pbuf 类型的指针,定义在 tcp_in.c 文件中,是一个静态变量:

static struct pbuf *recv_data;

经过 tcp_process 函数处理后,如果接收到有效数据,则指针 recv_data 指向数据 pbuf ,此时协议栈通过宏 TCP_EVENT_RECV 调用用户编写的数据处理函数。

简化后的代码为:

void
tcp_input(struct pbuf *p, struct netif *inp)
{// 经过一系列检测,没有错误/* 在本地找到有效的控制块 pcb */if (pcb != NULL) {err = tcp_process(pcb);/* A return value of ERR_ABRT means that tcp_abort() was calledand that the pcb has been freed. If so, we don't do anything. */if (err != ERR_ABRT) {if (recv_data != NULL) {/* Notify application that data has been received. */TCP_EVENT_RECV(pcb, recv_data, ERR_OK, err);if (err == ERR_ABRT) {goto aborted;}/* If the upper layer can't receive this data, store it */if (err != ERR_OK) {pcb->refused_data = recv_data;LWIP_DEBUGF(TCP_INPUT_DEBUG, ("tcp_input: keep incoming packet, because pcb is \"full\"\n"));}}/* Try to send something out. */tcp_output(pcb);        // <--- 注意这里调用了发送函数,所以 recv 回调函数就没必要再调用这个函数}}
}

从以上代码中可以看出:

  1. 回调函数有返回值,若发现异常,用户层可以主动调用 tcp_abort 函数终止连接,然后返回 ERR_ABRT 错误码,协议栈会完成后续的操作:
/* Notify application that data has been received. */
TCP_EVENT_RECV(pcb, recv_data, ERR_OK, err);
if (err == ERR_ABRT) {goto aborted;
}
  1. 如果正确的处理了数据,回调函数必须返回 ERR_OK 错误码,否则协议栈会认为用户没有接收这包数据,就会对它进行缓存:
/* If the upper layer can't receive this data, store it */
if (err != ERR_OK) {pcb->refused_data = recv_data;LWIP_DEBUGF(TCP_INPUT_DEBUG, ("tcp_input: keep incoming packet, because pcb is \"full\"\n"));
}

所以上层如果来不及处理数据,可以让协议栈暂存。这里暂存数据使用了指针 pcb->refused_data ,需要注意一下,因为接下来会再次看到它。

  1. 注意这里会调用 TCP 发送函数:
/* Try to send something out. */
tcp_output(pcb);

recv 回调函数中,处理完接收到的数据后,通常我们还会调用 tcp_write 函数回送数据。函数原型为:

/*** @ingroup tcp_raw* Write data for sending (but does not send it immediately).** It waits in the expectation of more data being sent soon (as* it can send them more efficiently by combining them together).* To prompt the system to send data now, call tcp_output() after* calling tcp_write().* * This function enqueues the data pointed to by the argument dataptr. The length of* the data is passed as the len parameter. The apiflags can be one or more of:* - TCP_WRITE_FLAG_COPY: indicates whether the new memory should be allocated*   for the data to be copied into. If this flag is not given, no new memory*   should be allocated and the data should only be referenced by pointer. This*   also means that the memory behind dataptr must not change until the data is*   ACKed by the remote host* - TCP_WRITE_FLAG_MORE: indicates that more data follows. If this is omitted,*   the PSH flag is set in the last segment created by this call to tcp_write.*   If this flag is given, the PSH flag is not set.** The tcp_write() function will fail and return ERR_MEM if the length* of the data exceeds the current send buffer size or if the length of* the queue of outgoing segment is larger than the upper limit defined* in lwipopts.h. The number of bytes available in the output queue can* be retrieved with the tcp_sndbuf() function.** The proper way to use this function is to call the function with at* most tcp_sndbuf() bytes of data. If the function returns ERR_MEM,* the application should wait until some of the currently enqueued* data has been successfully received by the other host and try again.** @param pcb Protocol control block for the TCP connection to enqueue data for.* @param arg Pointer to the data to be enqueued for sending.* @param len Data length in bytes* @param apiflags combination of following flags :* - TCP_WRITE_FLAG_COPY (0x01) data will be copied into memory belonging to the stack* - TCP_WRITE_FLAG_MORE (0x02) for TCP connection, PSH flag will not be set on last segment sent,* @return ERR_OK if enqueued, another err_t on error*/
err_t
tcp_write(struct tcp_pcb *pcb, const void *arg, u16_t len, u8_t apiflags)

通过注释可以得知,这个函数会尽可能把发送的数据组合在一起,然后一次性发送出去,因为这样更有效率。换句话说,调用这个函数并不会立即发送数据,如果希望立即发送数据,需要在调用 tcp_write 函数之后调用 tcp_output 函数。

而现在我们又知道了,在 tcp_input 函数中,调用 recv 回调函数后,协议栈会执行一次 tcp_output 函数,这就是我们在 recv 回调函数中调用 tcp_write 函数能够立即将数据发送出去的原因!

2 由 tcp_process_refused_data 函数调用

在上一节提到 “上层如果来不及处理数据,可以让协议栈暂存。这里暂存数据使用了指针 pcb->refused_data ”,而 tcp_process_refused_data 函数就是把暂存的数据重新提交给应用层处理。提交的方法是调用 recv 回调函数,简化后的代码为:

err_t
tcp_process_refused_data(struct tcp_pcb *pcb)
{/* set pcb->refused_data to NULL in case the callback frees it and thencloses the pcb */struct pbuf *refused_data = pcb->refused_data;pcb->refused_data = NULL;/* Notify again application with data previously received. */TCP_EVENT_RECV(pcb, refused_data, ERR_OK, err);if (err == ERR_ABRT) {return ERR_ABRT;} else if(err != ERR_OK){/* data is still refused, pbuf is still valid (go on for ACK-only packets) */pcb->refused_data = refused_data;return ERR_INPROGRESS;}return ERR_OK;
}

协议栈会在两处调用 tcp_process_refused_data 函数。
2.1 在 tcp_input 函数中调用

void
tcp_input(struct pbuf *p, struct netif *inp)
{// 经过一系列检测,没有错误/* 在本地找到有效的控制块 pcb */if (pcb != NULL) {/* If there is data which was previously "refused" by upper layer */if (pcb->refused_data != NULL) {if ((tcp_process_refused_data(pcb) == ERR_ABRT) ||      // <--- 这里((pcb->refused_data != NULL) && (tcplen > 0))) {/* pcb has been aborted or refused data is still refused and the new segment contains data */if (pcb->rcv_ann_wnd == 0) {/* this is a zero-window probe, we respond to it with current RCV.NXTand drop the data segment */tcp_send_empty_ack(pcb);}goto aborted;}}err = tcp_process(pcb);/* A return value of ERR_ABRT means that tcp_abort() was calledand that the pcb has been freed. If so, we don't do anything. */if (err != ERR_ABRT) {if (recv_data != NULL) {/* Notify application that data has been received. */TCP_EVENT_RECV(pcb, recv_data, ERR_OK, err);if (err == ERR_ABRT) {goto aborted;}/* If the upper layer can't receive this data, store it */if (err != ERR_OK) {pcb->refused_data = recv_data;}}/* Try to send something out. */tcp_output(pcb);}}
}

通过以上代码可以知道:

  1. 在处理接收数据之前,先检查一下是否有上次暂存的数据,如果有则调用 tcp_process_refused_data 函数,将暂存数据上报给应用层处理。
  2. 无论上层有多少数据没有处理,协议栈只暂存最后一次接收且上层没有处理的数据:
/* If the upper layer can't receive this data, store it */
if (err != ERR_OK) {pcb->refused_data = recv_data;
}

2.2 在 tcp_fasttmr 函数中调用
协议栈每隔 TCP_TMR_INTERVAL (默认 250)毫秒调用一次 tcp_fasttmr 函数,在这个函数中会检查 TCP_PCB 是否有尚未给上层应用处理的暂存数据,如果有则调用 tcp_process_refused_data 函数,将暂存数据上报给应用层处理。简化后的代码为:

void
tcp_fasttmr(void)
{++tcp_timer_ctr;tcp_fasttmr_start:pcb = tcp_active_pcbs;while (pcb != NULL) {if (pcb->last_timer != tcp_timer_ctr) {next = pcb->next;/* If there is data which was previously "refused" by upper layer */if (pcb->refused_data != NULL) {tcp_active_pcbs_changed = 0;tcp_process_refused_data(pcb);     // <--- 这里if (tcp_active_pcbs_changed) {/* application callback has changed the pcb list: restart the loop */goto tcp_fasttmr_start;}}pcb = next;} else {pcb = pcb->next;}}
}

recv 函数的复用行为

前面看到了错误回调函数、连接成功回调函数、接收到数据回调函数,后面还会看到发送成功回调函数等。那么我们合理推测,应该也有连接关闭回调函数。在连接关闭时,协议栈确实回调了一个函数,但这个函数也是 recv 回调函数!协议栈并没有提供单独的连接关闭回调函数,而是复用recv 回调函数。协议栈使用宏 TCP_EVENT_CLOSED 封装了这一过程,代码为:

#define TCP_EVENT_CLOSED(pcb,ret)                                \do {                                                           \if(((pcb)->recv != NULL)) {                                  \(ret) = (pcb)->recv((pcb)->callback_arg,(pcb),NULL,ERR_OK);\} else {                                                     \(ret) = ERR_OK;                                            \}                                                            \} while (0)

注意调用 recv 函数时,第 3 个参数为 NULL ,这很重要。我们又知道,recv 的原型为:

typedef err_t (*tcp_recv_fn)(void *arg, struct tcp_pcb *tpcb, struct pbuf *p, err_t err);

所以第三个参数是 struct pbuf 型指针。
也就是说,我们必须recv 回调函数中处理 pbuf 指针为 NULL 的特殊情况,这表示远端主动关闭了连接,这时我们应主动调用 tcp_close 函数,关闭本地连接。一个典型的 recv 回调函数框架为:

static err_t
app_recv(void *arg, struct tcp_pcb *pcb, struct pbuf *p, err_t err)
{if (p == NULL) {// 连接关闭前的处理,可选tcp_close(pcb);} else {if (err != ERR_OK) {// 目前还没有使用 ERR_OK 之外的回调参数,这里兼容以后的协议栈pbuf_free(p);return err;}// 更新窗口值,必须调用tcp_recved(pcb,p->tot_len);// 在这里处理接收到的数据// 释放 pbuf,必须    pbuf_free(p);}return ERR_OK;
}

协议栈在 tci_input 函数中调用宏 TCP_EVENT_CLOSED ,简化后的代码为:

void
tcp_input(struct pbuf *p, struct netif *inp)
{// 经过一系列检测,没有错误/* 在本地找到有效的控制块 pcb */if (pcb != NULL) {err = tcp_process(pcb);if (err != ERR_ABRT) {if (recv_flags & TF_RESET) {// 收到 RST 标志,回调 errf 函数TCP_EVENT_ERR(pcb->state, pcb->errf, pcb->callback_arg, ERR_RST);tcp_pcb_remove(&tcp_active_pcbs, pcb);tcp_free(pcb);} else {if (recv_acked > 0) {// 收到数据 ACK 应答,回调 sent 函数TCP_EVENT_SENT(pcb, (u16_t)acked16, err);if (err == ERR_ABRT) {goto aborted;}recv_acked = 0;}if (recv_data != NULL) {// 收到有效数据, 回调 recv 函数TCP_EVENT_RECV(pcb, recv_data, ERR_OK, err);if (err == ERR_ABRT) {goto aborted;}}if (recv_flags & TF_GOT_FIN) {// 收到 FIN 标志,回调 recv 函数,远端关闭连接TCP_EVENT_CLOSED(pcb, err);        // <--- 这里if (err == ERR_ABRT) {goto aborted;}}/* Try to send something out. */tcp_output(pcb);}}}
}

sent 回调函数

在 TCP 控制块中,函数指针 sent 指向用户实现的函数,当成功发送数据后,由协议栈调用此函数,通知用户数据已成功发送。
函数指针 sent 的类型为 tcp_sent_fn ,该类型定义在 tcp.h 中:

/** Function prototype for tcp sent callback functions. Called when sent data has* been acknowledged by the remote side. Use it to free corresponding resources.* This also means that the pcb has now space available to send new data.** @param arg Additional argument to pass to the callback function (@see tcp_arg())* @param tpcb The connection pcb for which data has been acknowledged* @param len The amount of bytes acknowledged* @return ERR_OK: try to send some data by calling tcp_output*            Only return ERR_ABRT if you have called tcp_abort from within the*            callback function!*/
typedef err_t (*tcp_sent_fn)(void *arg, struct tcp_pcb *tpcb,u16_t len);

通过注释可以知道当数据成功发送后(收到远端主机 ACK 应答),调用 sent 回调函数,用于释放某些资源(如果用到的话)。这也意味着 PCB 现在有可以发送新的数据的空间了。
协议栈通过宏 TCP_EVENT_SENT(pcb,space,ret) 调用 pcb->sent 指向的函数。宏 TCP_EVENT_SENT 定义在 tcp_priv.h 中:

#define TCP_EVENT_SENT(pcb,space,ret)                          \do {                                                         \if((pcb)->sent != NULL)                                    \(ret) = (pcb)->sent((pcb)->callback_arg,(pcb),(space));  \else (ret) = ERR_OK;                                       \} while (0)

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

TCP_EVENT_SENT(pcb, (u16_t)acked16, err);

这是在 tcp_input 函数中,如果收到数据 ACK 应答,则回调 sent 函数,应答的数据字节数作为参数传到到回调函数。

void
tcp_input(struct pbuf *p, struct netif *inp)
{// 经过一系列检测,没有错误/* 在本地找到有效的控制块 pcb */if (pcb != NULL) {err = tcp_process(pcb);if (err != ERR_ABRT) {if (recv_flags & TF_RESET) {// 收到 RST 标志,回调 errf 函数TCP_EVENT_ERR(pcb->state, pcb->errf, pcb->callback_arg, ERR_RST);tcp_pcb_remove(&tcp_active_pcbs, pcb);tcp_free(pcb);} else {if (recv_acked > 0) {// 收到数据 ACK 应答,回调 sent 函数TCP_EVENT_SENT(pcb, (u16_t)acked16, err);    // <--- 这里if (err == ERR_ABRT) {goto aborted;}recv_acked = 0;}if (recv_data != NULL) {// 收到有效数据, 回调 recv 函数TCP_EVENT_RECV(pcb, recv_data, ERR_OK, err);if (err == ERR_ABRT) {goto aborted;}}if (recv_flags & TF_GOT_FIN) {// 收到 FIN 标志,回调 recv 函数,远端关闭连接TCP_EVENT_CLOSED(pcb, err);       if (err == ERR_ABRT) {goto aborted;}}/* Try to send something out. */tcp_output(pcb);}}}
}

poll 回调函数

在 TCP 控制块中,函数指针 poll 指向用户实现的函数,协议栈周期性的调用此函数,“周期“由用户在注册回调函数时指定,最小为 TCP_SLOW_INTERVAL 毫秒(默认 500),用户层可以使用这个回调函数做一些周期性处理。
函数指针 poll 的类型为 tcp_poll_fn ,该类型定义在 tcp.h 中:

/** Function prototype for tcp poll callback functions. Called periodically as* specified by @see tcp_poll.** @param arg Additional argument to pass to the callback function (@see tcp_arg())* @param tpcb tcp pcb* @return ERR_OK: try to send some data by calling tcp_output*            Only return ERR_ABRT if you have called tcp_abort from within the*            callback function!*/
typedef err_t (*tcp_poll_fn)(void *arg, struct tcp_pcb *tpcb);

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

#define TCP_EVENT_POLL(pcb,ret)                                \do {                                                         \if((pcb)->poll != NULL)                                    \(ret) = (pcb)->poll((pcb)->callback_arg,(pcb));          \else (ret) = ERR_OK;                                       \} while (0)

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

TCP_EVENT_POLL(prev, err);

这是在 tcp_slowtmr 函数中,当达到设定的时间时,调用 poll 回调函数。简化后的代码为:

void
tcp_slowtmr(void)
{++prev->polltmr;if (prev->polltmr >= prev->pollinterval) {prev->polltmr = 0;TCP_EVENT_POLL(prev, err);  // <-- 这里/* if err == ERR_ABRT, 'prev' is already deallocated */if (err == ERR_OK) {tcp_output(prev);}}}
}

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

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

  1. 【Android 高性能音频】AAudio 音频流 数据回调细节 ( 数据回调函数优先级 | 数据回调函数 | 采样率 | 采样数 | 缓冲区调整 | 线程不安全 )

    文章目录 I . 数据回调函数优先级 II . 数据回调函数 相关内容 III . 采样率 处理细节 IV . 数据回调函数 每次 采样个数 numFrames V . 数据回调函数 缓冲区 ( AA ...

  2. C语言中的回调函数(Callback Function)

    C语言中的回调函数(Callback Function) 1 定义和使用场合 回调函数是指 使用者自己定义一个函数,实现这个函数的程序内容,然后把这个函数(入口地址)作为参数传入别人(或系统)的函数中 ...

  3. C语言回调函数callback

    一.定义和使用场合 回调函数是指 使用者自己定义一个函数,实现这个函数的程序内容,然后把这个函数(入口地址)作为参数传入别人(或系统)的函数中,由别人(或系统)的函数在运行时来调用的函数.函数是你实现 ...

  4. linux中rcf命名管道,RCF的简单使用教程以及什么是回调函数

    RCF的使用教程 RCF(Remote Call Framework)是一个使用C++编写的RPC框架,在底层RCF支持多种传输实现方式(transport implementations). 包括T ...

  5. C语言回调函数的定义和写法

    C语言中的回调函数(Callback Function) 1 定义和使用场合 回调函数是指 使用者自己定义一个函数,实现这个函数的程序内容,然后把这个函数(入口地址)作为参数传入别人(或系统)的函数中 ...

  6. delphi 回调函数

    "回调机制"是window 在执行某个API函数的过程中,调用指定的一个函数(回调函数). 比如:你有一个任务,但是有一部分你不会做,或者说不愿做,所以我来帮你做这部分,你做你其它 ...

  7. 【校招面试 之 C/C++】第15题 C 回调函数

    转自:https://segmentfault.com/a/1190000008293902 做略微改动 什么是回调函数 我们先来看看百度百科是如何定义回调函数的: 回调函数就是一个通过函数指针调用的 ...

  8. TLS回调函数(1)

    简述 代码逆向分析领域中,TLS(Thread Local Storage,线程局部存储)回调函数(Callback Function)常用反调试.TLS回调函数的调用运行要先于EP代码的执行,该特征 ...

  9. 回调函数的意义以及python实现

    回调函数(callback),百度百科的解释是这样的: 回调函数就是一个通过函数指针调用的函数.如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回 ...

最新文章

  1. Python 知识点笔记一常用数据类型
  2. ansi编码_了解字符编码,不再恐惧文件乱码
  3. stm32 薄膜键盘原理_市面上的笔记本键盘优缺点解析,看完秒懂
  4. java swing 字体设置_java如何改变Swing应用程序的默认字体/字号
  5. Rails源码笔记-ActiveSupport-core_ext-array
  6. YOLO系列专题——YOLOv3实践篇
  7. 英特尔核显驱动hd630_英特尔发新处理器,换新 Logo,还把 AMD 吊打了一轮
  8. matlab实验数据拟合,利用Matlab对实验数据拟合曲线与函数方法
  9. date日期格式化 java,Java日期格式化常用方法
  10. [2019IEEE Transactions on Cybernetics ] Asymptotic Soft Filter Pruning for Deep Convolutional Neural
  11. GMP法规附录《计算机化系统》那些事儿
  12. 如何查看计算机连接的打印机驱动,打印机已经连接电脑了。360驱动大师怎么检查不出来打印机驱动在那里?...
  13. 激活windows错误代码 0x80072F8F
  14. HTTPS 加密、证书、签名与握手
  15. java构造块与静态块
  16. win10鼠标右键问题,导致桌面刷新重启,资源管理器explorer重启,文件夹闪退,应用管理员模式无法运行等等
  17. 使用在线链接阿里图标库(iconfont)(vue)
  18. 互联网架构正逐渐成为企业IT发展刚需
  19. 利用Windows 自带的任务计划功能设置闹钟
  20. vue+mintui+picker弹框选择器

热门文章

  1. 第十一届蓝桥杯国赛题目
  2. CVPR 2021 Oral | Transformer再发力!华南理工和微信提出UP-DETR:无监督预训练检测器...
  3. 【2004-3】【平分核桃】
  4. 字节跳动Android开发大牛:90% 成功率的 BATZ Offer 收割机是怎样练成的!
  5. URI跳转方式地图导航的代码实践
  6. [RK3288][Android5.1] 调试笔记 --- 根据VIDPID读取不同的rilLib以实现4G模块的适配
  7. DBeaver改成英语
  8. C语言之逆序输出一个四位数
  9. GRECP/LPL RECOVERY
  10. Java程序员都是青春饭吗?