使用 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
回调函数。
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_RST
、ERR_CLSD
和 ERR_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.连接关闭
这是为了处理异常情况。
当接收到远端的 FIN 标志,表示远端要关闭 TCP 连接,此时协议栈会通知应用程序。通常情况下,应用程序应该调用 tcp_close
函数关闭连接。但是,若应用才程序没有执行这个步骤,则 TCP 状态处于 LADT_ACK 并且接收到远端的 ACK 标志后,调用错误类型为 ERR_CLSD
的错误回调函数,简化后的代码为:
void tcp_input(struct pbuf *p, struct netif *inp)
{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_flags & TF_CLOSED) { /*处于 LAST_ACK 状态时收到 ACK 标识*//* The connection has been closed and we will deallocate the PCB. */if (!(pcb->flags & TF_RXCLOSED)) {/* Connection closed although the application has only shut down thetx side: call the PCB's err callback and indicate the closure toensure the application doesn't continue using the PCB. */TCP_EVENT_ERR(pcb->errf, pcb->callback_arg, ERR_CLSD); // <--- 这里}tcp_pcb_remove(&tcp_active_pcbs, pcb);memp_free(MEMP_TCP_PCB, pcb);}}
}
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 控制块!
具体就是:
- 先调用
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 标志。
这里的第 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 控制块处于
ESTABLISHED
或CLOSE_WAIT
状态,超时达到pcb->keep_idle + TCP_KEEP_DUR(pcb)
秒(默认 2 小时 10 分 48 秒)
tcp_slowtmr
函数简化后的代码为:
/*** 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);} }
}
读后有收获,资助博主养娃 - 千金难买知识,但可以买好多奶粉 (〃‘▽’〃)