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 周期性執行回調函數 pollerrf 回調函數
在 TCP 控制塊中,函數指針 errf 指向用戶實現的 TCP 錯誤處理函數,當 TCP 連接發送錯誤時,由協議棧調用此函數。
函數指針 errf 的類型為 tcp_err_fn ,該類型定義在 tcp.h 中:
從注釋得知,錯誤處理函數在接收到 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 處使用:
用到了 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.連接關閉
這部分代碼沒有理解清楚,暫時保留
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 控制塊!
具體就是:
這里的第 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 函數代碼簡單,原始無簡化代碼為:
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_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 中:
協議棧通過宏 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 中:
協議棧通過宏 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_MEM 的 accept 回調函數,向用戶報告內存分配失敗。簡化后的代碼為:
這里需要注意,申請 TCP_PCB 失敗的處理方法,lwIP 2.1.x 版本與 lwIP 1.4.1 不同。
再看看 lwIP 1.4.1 的 tcp_listen_input 函數代碼(經簡化):
可以看到, 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 回調函數可以這么寫:
而 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_OK 的 accept 回調函數,向用戶報告接受了新的連接。簡化后的代碼為:
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 中:
協議棧通過宏 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 回調函數就沒必要再調用這個函數}} }從以上代碼中可以看出:
所以上層如果來不及處理數據,可以讓協議棧暫存。這里暫存數據使用了指針 pcb->refused_data ,需要注意一下,因為接下來會再次看到它。
在 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 函數中調用
通過以上代碼可以知道:
2.2 在 tcp_fasttmr 函數中調用
協議棧每隔 TCP_TMR_INTERVAL (默認 250)毫秒調用一次 tcp_fasttmr 函數,在這個函數中會檢查 TCP_PCB 是否有尚未給上層應用處理的暫存數據,如果有則調用 tcp_process_refused_data 函數,將暫存數據上報給應用層處理。簡化后的代碼為:
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 回調函數框架為:
協議棧在 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 中:
通過注釋可以知道當數據成功發送后(收到遠端主機 ACK 應答),調用 sent 回調函數,用于釋放某些資源(如果用到的話)。這也意味著 PCB 現在有可以發送新的數據的空間了。
協議棧通過宏 TCP_EVENT_SENT(pcb,space,ret) 調用 pcb->sent 指向的函數。宏 TCP_EVENT_SENT 定義在 tcp_priv.h 中:
以關鍵字 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 中:
協議棧通過宏 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 回调函数是何时调用的的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: mybatis plus 查询排序_My
- 下一篇: python定时更换mac 超美桌面背景