日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

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

發布時間:2024/1/8 编程问答 24 豆豆
生活随笔 收集整理的這篇文章主要介紹了 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_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 控制塊!
具體就是:

  • 先調用 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_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; // 使能?;?#xff0c;超時 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_MEM 的 accept 回調函數,向用戶報告內存分配失敗。簡化后的代碼為:

    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; //增加?;顧C制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; //增加?;顧C制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 中:

    /** 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 回調函數就沒必要再調用這個函數}} }

    從以上代碼中可以看出:

  • 回調函數有返回值,若發現異常,用戶層可以主動調用 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; }
  • 如果正確的處理了數據,回調函數必須返回 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 ,需要注意一下,因為接下來會再次看到它。

  • 注意這里會調用 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);}} }

    通過以上代碼可以知道:

  • 在處理接收數據之前,先檢查一下是否有上次暫存的數據,如果有則調用 tcp_process_refused_data 函數,將暫存數據上報給應用層處理。
  • 無論上層有多少數據沒有處理,協議棧只暫存最后一次接收且上層沒有處理的數據:
  • /* 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 回调函数是何时调用的的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。