終於到介面層了。
原文:李柱明部落格:https://www.cnblogs.com/lizhuming/p/17442931.html
前面我們已經學完了,都知道raw介面了,其實也可以直接用,就是麻煩點。
這裡NETCONN就是封裝了raw介面,讓使用者使用更加簡單。
socket介面是封裝NETconn介面的,讓使用者使用更加標準,方便應用程式移植。
NETCONN的介面框架:
解耦:編寫回撥函數xxx_tcp()、xxx_udp(),註冊到協定棧裡面。協定棧通過回撥函數告知介面層,當前PCB的狀態資訊。介面層根據當前PCB的狀態資訊做相應處理即可。
檔案:
api_msg.c
:構建api msg,被netconn呼叫,傳送到核心鎖或tcpip核心執行緒執行指定的回撥函數。api_lib.c
:netconn API。sockets.c
:socket通訊端介面層,封裝netconn介面。供使用者使用。
在raw/callback API程式設計時,使用者程式設計的方法就是向核心註冊各種自定義的回撥函數,回撥函數是與核心實現互動的唯一方式。
協定棧API NETCONN是基於raw/callback API實現的。
核心通過呼叫註冊到TCP/UDP核心的回撥函數,把接收到的資料或可傳送資料的事件傳送回netconn對應的郵箱中,上層檢查這些郵箱即可和核心協定棧互動。
netbuf是應用層描述待傳送資料和已接收資料的基本結構。當然,最基本的粒度資料結構還是pbuf。
應用層接收資料:
netbuf
中,並遞交給應用層。應用層傳送資料:
netbuf
結構中。
netbuf資料結構:
/** "Network buffer" - contains data and addressing info */
struct netbuf {
struct pbuf *p, *ptr; /* 包緩衝區。p:pbuf鏈。ptr:pbuf鏈中當前pbuf遊標。 */
ip_addr_t addr; /* 傳送方IP */
u16_t port; /* 傳送方埠 */
#if LWIP_NETBUF_RECVINFO || LWIP_CHECKSUM_ON_COPY
u8_t flags; /* 標誌位 */
u16_t toport_chksum; /* 目的埠號。用於checksum */
#if LWIP_NETBUF_RECVINFO
ip_addr_t toaddr; /* 目的地址 */
#endif /* LWIP_NETBUF_RECVINFO */
#endif /* LWIP_NETBUF_RECVINFO || LWIP_CHECKSUM_ON_COPY */
};
代表一個連線,TCP或UDP等。
相關檔案:api.h
分析完UDP和TCP協定實現後,會分析他們的原生介面udp_xxx()和tcp_xxx()都是互相獨立的。
而連線結構netconn就是為了統一這些介面。
netconn控制塊:
/** A netconn descriptor */
struct netconn {
/** type of the netconn (TCP, UDP or RAW) */
enum netconn_type type; /* 連線型別 */
enum netconn_state state; /* netconn當前狀態。即是當前netconn被哪些netconn API佔用 */
union {
struct ip_pcb *ip; /* IP控制塊 */
struct tcp_pcb *tcp; /* TCP控制塊 */
struct udp_pcb *udp; /* UDP控制塊 */
struct raw_pcb *raw; /* TCP控制塊 */
} pcb; /* 核心中與連線相關的控制塊指標 */
/* 此netconn的最新未報告的非同步錯誤 */
err_t pending_err;
#if !LWIP_NETCONN_SEM_PER_THREAD /* 只能每個netconn資料結構佔用一個號誌 */
/* 號誌。是對一個API完成兩部分執行緒的同步。如使用者呼叫API,API呼叫核心API,並等待核心API完成後通過該號誌通知當前API。 */
sys_sem_t op_completed;
#endif
/* 接收資料的郵箱。資料緩衝佇列。 */
sys_mbox_t recvmbox;
#if LWIP_TCP
/* 用於TCP伺服器端。連線請求的緩衝佇列。 */
sys_mbox_t acceptmbox;
#endif /* LWIP_TCP */
#if LWIP_NETCONN_FULLDUPLEX /* 全功率 */
/* mbox的讀阻塞執行緒數。當執行緒在waiting時closing,需要解除所有執行緒的阻塞。 */
int mbox_threads_waiting;
#endif
union {
int socket; /* socket */
void *ptr; /* 指標 */
} callback_arg; /* 回撥引數 */
#if LWIP_SO_SNDTIMEO /* 傳送超時 */
/* 等待傳送資料超時值,單位ms。 */
s32_t send_timeout;
#endif /* LWIP_SO_RCVTIMEO */
#if LWIP_SO_RCVTIMEO /* 接收超時 */
/* 等待接收新資料的超時時間,單位ms。 */
u32_t recv_timeout;
#endif /* LWIP_SO_RCVTIMEO */
#if LWIP_SO_RCVBUF /* 接收緩衝區 */
/* 應用層的接收緩衝區size。限制recvmbox上所有資料的size。 */
int recv_bufsize;
/* recvmbox 當前接收到的資料size,用於FIONREAD。 */
int recv_avail;
#endif /* LWIP_SO_RCVBUF */
#if LWIP_SO_LINGER /* SO_LINGER選項 */
/* < 0: 關閉該功能。
= 0: 立即關閉。傳送緩衝區殘留有資料時,RST給對端。
> 0: 超時值。單位:秒。超時前儘量把傳送緩衝區中的資料傳送出去。 */
s16_t linger;
#endif /* LWIP_SO_LINGER */
/* 包含更多的netconn-internal狀態。參考NETCONN_FLAG_x宏 */
u8_t flags;
#if LWIP_TCP
/* 當呼叫netconn_write()函數傳送的資料不適合傳送到緩衝區時,
資料會暫時儲存在current_msg中,等待資料合適的時候進行傳送。 */
struct api_msg *current_msg;
#endif /* LWIP_TCP */
/* netconn相關的回撥函數。socket API時使用。 */
netconn_callback callback;
};
部分變數描述:
type
:連線型別:TCP、UDP、RAW。state
:當前連線狀態。pcb
:協定棧核心連線控制塊:TCP控制塊、UDP控制塊等等。err
:記錄當前連線上函數呼叫的執行結果。op_completed
:是上下兩部分API實現同步的重要欄位,netconn_xxx()
函數在投遞完訊息後,便會阻塞在連線的這個號誌上,當核心的do_xxx()
執行完成後便釋放號誌。recvmbox
:該連線的資料接收郵箱,也是緩衝佇列。核心接收到屬於該連線的封包(封裝在netbuf中)投遞到該郵箱。應用程式呼叫資料接收函數,就是從該佇列中讀取資料。recv_avail
:記錄當前接收郵箱中已經緩衝好的資料總長度。acceptmbox
:該連線作為TCP伺服器時使用到,核心會把所有新建立好的連線結構netconn
投遞到該郵箱,伺服器程式呼叫netconn_accept()
函數便會得到一個新的連線結構。mbox_threads_waiting
:表示讀阻塞在當前連線的應用程式數量,在關閉連線時,需要往recvmbox
郵箱傳送mbox_threads_waiting
個郵件來解除這些應用層的阻塞。
/** @ingroup netconn_common
* 協定族和netconn型別。
*
*/
enum netconn_type {
NETCONN_INVALID = 0, /* 無效型別 */
/** TCP IPv4 */
NETCONN_TCP = 0x10,
#if LWIP_IPV6
/** TCP IPv6 */
NETCONN_TCP_IPV6 = NETCONN_TCP | NETCONN_TYPE_IPV6 /* 0x18 */,
#endif /* LWIP_IPV6 */
/** UDP IPv4 */
NETCONN_UDP = 0x20,
/** UDP IPv4 lite */
NETCONN_UDPLITE = 0x21,
/** UDP IPv4 no checksum */
NETCONN_UDPNOCHKSUM = 0x22,
#if LWIP_IPV6
/** UDP IPv6 (dual-stack by default, unless you call @ref netconn_set_ipv6only) */
NETCONN_UDP_IPV6 = NETCONN_UDP | NETCONN_TYPE_IPV6 /* 0x28 */,
/** UDP IPv6 lite (dual-stack by default, unless you call @ref netconn_set_ipv6only) */
NETCONN_UDPLITE_IPV6 = NETCONN_UDPLITE | NETCONN_TYPE_IPV6 /* 0x29 */,
/** UDP IPv6 no checksum (dual-stack by default, unless you call @ref netconn_set_ipv6only) */
NETCONN_UDPNOCHKSUM_IPV6 = NETCONN_UDPNOCHKSUM | NETCONN_TYPE_IPV6 /* 0x2a */,
#endif /* LWIP_IPV6 */
/** Raw connection IPv4 */
/* RAW ipv4 連線 */
NETCONN_RAW = 0x40
#if LWIP_IPV6
/** Raw connection IPv6 (dual-stack by default, unless you call @ref netconn_set_ipv6only) */
, NETCONN_RAW_IPV6 = NETCONN_RAW | NETCONN_TYPE_IPV6 /* 0x48 */
#endif /* LWIP_IPV6 */
};
/* 當前netconn介面資料結構所處的狀態。
如當前netconn被netconn_write()介面呼叫,就處於WRITE狀態。
也可以理解為當前netconn被哪些netconn API佔用 */
enum netconn_state {
NETCONN_NONE, /* 空閒狀態 */
NETCONN_WRITE, /* 正在傳送資料 */
NETCONN_LISTEN, /* 偵聽狀態 */
NETCONN_CONNECT, /* 連線狀態 */
NETCONN_CLOSE /* 關閉狀態 */
};
上層收:按次數統計。NETCONN中有多少個可被上層接收。
上層發:按是否有無統計。NETCONN中是否可寫。
/*
* netconn_x()API 通知更上層(如socket)的事件。
*
* 事件說明:
* 在netconn實現中,有三種方法來阻塞使用者端:
* - accept mbox:netconn_accept()函數中的sys_arch_mbox_fetch()
* - receive mbox:netconn_recv_data()函數中的sys_arch_mbox_fetch()
* - send queue if full:lwip_netconn_do_write()函數中的sys_arch_sem_wait()
*
* 這些事件都是給這些mboxes/semaphores標記狀態的事件。
* 對於非阻塞式的連線,我們可以通過這些事件,提前知道呼叫netconn API是否會阻塞。
*
* NETCONN_EVT_RCVPLUS: 加。mboxes/semaphores 物件,可安全呼叫相關netconn API不會被阻塞的次數+1。
* 如在sockets中是按次計數:如accept mbox連續收到三個NETCONN_EVT_RCVPLUS事件,
* 則可以連續三次呼叫netconn_accept()不會被阻塞。receive mbox也一樣。
*
* NETCONN_EVT_RCVMINUS: 減。mboxes/semaphores 物件,可安全呼叫相關netconn API不會被阻塞的次數-1。
* 一般在呼叫對應函數成功後,統計一次。
*
* 而對於TX,沒有次數統計,只是一個標誌。
*
* NETCONN_EVT_SENDPLUS: 表示呼叫netconn_send()傳送資料不會阻塞。
* 一般發生在傳送緩衝區中的資料被ACK了,緩衝區空閒空間增加時會回撥該事件到上層。
*
* NETCONN_EVT_SENDMINUS: 表示呼叫netconn_send()會阻塞。
* 一般發生在協定棧內部PCB不可傳送資料時會通過該事件通知上層,此時呼叫netconn_send()會阻塞,如傳送緩衝區不足,記憶體不足等等。
* 觸發該事件後,內部PCB會在pcb->poll()函數會檢查PCB是否可傳送資料,如果可發,就會觸發NETCONN_EVT_SENDPLUS事件通知上層。
*
*/
enum netconn_evt {
NETCONN_EVT_RCVPLUS, /* 收到資料。可安全呼叫API不會被阻塞次數+1 */
NETCONN_EVT_RCVMINUS, /* 可安全呼叫API不會被阻塞次數-1 */
NETCONN_EVT_SENDPLUS, /* PCB可傳送資料事件 */
NETCONN_EVT_SENDMINUS,/* PCB不可傳送事件 */
NETCONN_EVT_ERROR /* 錯誤事件 */
};
前提實現可以自行看原始碼。
netbuf
操作介面:
/* Network buffer functions: */
struct netbuf * netbuf_new (void);
void netbuf_delete (struct netbuf *buf);
void * netbuf_alloc (struct netbuf *buf, u16_t size);
void netbuf_free (struct netbuf *buf);
err_t netbuf_ref (struct netbuf *buf,
const void *dataptr, u16_t size);
void netbuf_chain (struct netbuf *head, struct netbuf *tail);
err_t netbuf_data (struct netbuf *buf,
void **dataptr, u16_t *len);
s8_t netbuf_next (struct netbuf *buf);
void netbuf_first (struct netbuf *buf);
在學完TCP、UDP核心實現後,就知道我們需要往這些核心裡註冊回撥函數,用於核心和上層互動。如tcp的tcp_recv()
就是往核心註冊接收回撥函數。
所以在實現NETCONN介面時,需要編寫這些回撥函數,並註冊到核心中。
TCP:setup_tcp()
:
/**
* 註冊netconn tcp基礎介面相關的回撥到TCP層
*
*/
static void
setup_tcp(struct netconn *conn)
{
struct tcp_pcb *pcb;
pcb = conn->pcb.tcp;
tcp_arg(pcb, conn); // PCB繫結NETCONN介面控制塊
tcp_recv(pcb, recv_tcp); // 註冊接收回撥
tcp_sent(pcb, sent_tcp); // 註冊傳送回撥
tcp_poll(pcb, poll_tcp, NETCONN_TCP_POLL_INTERVAL); // 註冊poll
tcp_err(pcb, err_tcp); // 註冊異常回撥
}
recv_tcp()
是TCP netconn註冊到tcp的tcp_pcb->recv()
接收回撥函數。
TCP核心收到資料後會通過當前回撥函數傳送封包到conn->recvmbox
,如果投遞失敗,則不能刪除這些pbuf
,因為tcp_fasttmr()
會在後面再次通知我們上層接收。
這裡傳送失敗但是不能刪除這些pbuf
的原因:我們TCP已經ACK了這些資料,對端不會再發這些資料了的,所以我們不能完全刪除,只能晚點上交給應用層。
和recv_udp()
略有區別,這裡不封裝netbuf
,在呼叫上層呼叫netconn_recv()
函數中再把pbuf
裝成netbuf
。
這樣做的目的是因為TCP封包的封裝、處理設計其它很多額外的操作,而當前函數卻是一個回撥函數,不適合多做業務及長時間佔有。
static err_t
recv_tcp(void *arg, struct tcp_pcb *pcb, struct pbuf *p, err_t err)
{
struct netconn *conn;
u16_t len;
void *msg;
LWIP_UNUSED_ARG(pcb);
LWIP_ASSERT("recv_tcp must have a pcb argument", pcb != NULL);
LWIP_ASSERT("recv_tcp must have an argument", arg != NULL);
LWIP_ASSERT("err != ERR_OK unhandled", err == ERR_OK);
LWIP_UNUSED_ARG(err); /* for LWIP_NOASSERT */
conn = (struct netconn *)arg;
if (conn == NULL) {
return ERR_VAL;
}
LWIP_ASSERT("recv_tcp: recv for wrong pcb!", conn->pcb.tcp == pcb);
if (!NETCONN_MBOX_VALID(conn, &conn->recvmbox)) {
/* recvmbox已經被刪除了。如shutdown RX */
if (p != NULL) {
tcp_recved(pcb, p->tot_len); /* 把這些資料從TCP接收緩衝區中全部讀走,並更新接收視窗 */
pbuf_free(p); /* 然後釋放這些pbuf */
}
return ERR_OK; /* 算是接收成功 */
}
/* 與UDP或RAW pcb不同,不要使用recv_avail檢查可用空間,因為這可能會破壞連線。
(這些資料都是已經被我們ACK了的) */
if (p != NULL) { /* 有資料 */
msg = p;
len = p->tot_len;
} else { /* 沒資料也觸發當前回撥,說明TCP協定棧底層是想表示連線已斷開 */
msg = LWIP_CONST_CAST(void *, &netconn_closed);
len = 0;
}
if (sys_mbox_trypost(&conn->recvmbox, msg) != ERR_OK) {
/* 不要釋放p:它稍後會從tcp_fasttmr再次給我們! */
return ERR_MEM;
} else {
#if LWIP_SO_RCVBUF
SYS_ARCH_INC(conn->recv_avail, len);
#endif /* LWIP_SO_RCVBUF */
/* 通知上層,有資料可讀 */
API_EVENT(conn, NETCONN_EVT_RCVPLUS, len);
}
return ERR_OK;
}
sent_tcp()
是TCP netconn註冊到tcp的tcp_pcb->sent()
傳送回撥函數。
當TCP收到更多ACK,傳送緩衝區可用空間增大了,就會呼叫當前回撥函數。
主要是喚醒阻塞等待連線關閉或資料傳送的應用程式執行緒。
檢查和通知介面層(netconn、socket),有更多緩衝空間了,如果有資料,可以發過來。
static err_t
sent_tcp(void *arg, struct tcp_pcb *pcb, u16_t len)
{
struct netconn *conn = (struct netconn *)arg;
LWIP_UNUSED_ARG(pcb);
LWIP_ASSERT("conn != NULL", (conn != NULL));
if (conn) { /* 介面連線還存在 */
if (conn->state == NETCONN_WRITE) { /* 介面層需要傳送資料 */
lwip_netconn_do_writemore(conn WRITE_DELAYED); /* 把資料寫入TCP傳送緩衝區 */
} else if (conn->state == NETCONN_CLOSE) { /* 介面層已經關閉了當前連線 */
lwip_netconn_do_close_internal(conn WRITE_DELAYED); /* TCP內部資源也關閉 */
}
/* 檢查水位線:TCP傳送緩衝區 可用空間size在水位線上 && 當前pbuf數量在水位線下
即可通知上層,可往TCP傳送緩衝區寫入資料。 */
if ((conn->pcb.tcp != NULL) && (tcp_sndbuf(conn->pcb.tcp) > TCP_SNDLOWAT) &&
(tcp_sndqueuelen(conn->pcb.tcp) < TCP_SNDQUEUELOWAT)) {
netconn_clear_flags(conn, NETCONN_FLAG_CHECK_WRITESPACE); /* 清除 檢查緩衝區可寫 標誌 */
API_EVENT(conn, NETCONN_EVT_SENDPLUS, len); /* 通知介面層,當前TCP傳送緩衝區可寫 */
}
}
return ERR_OK;
}
poll_tcp()
是TCP netconn註冊到tcp的tcp_pcb->poll()
周期函數。
tcp_pcb->poll()
被TCP慢時鐘tcp_slowtmr()
時鐘呼叫。
NETCONN_TCP_POLL_INTERVAL==2
,表示每秒會輪詢一次該函數。
主要是喚醒阻塞等待連線關閉或資料傳送的應用程式執行緒。
解除應用程式執行緒阻塞的方式:傳送號誌conn->sem。
如果關閉失敗,netconn_close()等待conn->sem。
static err_t
poll_tcp(void *arg, struct tcp_pcb *pcb)
{
struct netconn *conn = (struct netconn *)arg;
LWIP_UNUSED_ARG(pcb);
LWIP_ASSERT("conn != NULL", (conn != NULL));
if (conn->state == NETCONN_WRITE) { /* 如果netconn處於正在傳送資料狀態,那tcp層就繼續從netconn取資料發出去 */
lwip_netconn_do_writemore(conn WRITE_DELAYED);
} else if (conn->state == NETCONN_CLOSE) { /* netconn已經close當前連線了,內部tcp層也要close */
#if !LWIP_SO_SNDTIMEO && !LWIP_SO_LINGER /* 沒開socket傳送超時 && 沒開close()後殘留資料超時 */
if (conn->current_msg && conn->current_msg->msg.sd.polls_left) {
conn->current_msg->msg.sd.polls_left--;
}
#endif /* !LWIP_SO_SNDTIMEO && !LWIP_SO_LINGER */
lwip_netconn_do_close_internal(conn WRITE_DELAYED);
}
/* 之前是否有非阻塞的寫操作失敗?有就檢查寫緩衝區是否有可用空間 */
if (conn->flags & NETCONN_FLAG_CHECK_WRITESPACE) { /* 之前存在非阻塞寫入失敗 */
/* 檢查傳送緩衝區:緩衝區可用size是否足夠 和 pbuf數量是否超限 */
if ((conn->pcb.tcp != NULL) && (tcp_sndbuf(conn->pcb.tcp) > TCP_SNDLOWAT) &&
(tcp_sndqueuelen(conn->pcb.tcp) < TCP_SNDQUEUELOWAT)) {
netconn_clear_flags(conn, NETCONN_FLAG_CHECK_WRITESPACE); /* tcp層有更多的傳送緩衝區空間可用,則清除該標記 */
API_EVENT(conn, NETCONN_EVT_SENDPLUS, 0); /* 觸發一個可寫事件到netconn的上層(如socket層) */
}
}
return ERR_OK;
}
poll_tcp()
是TCP netconn註冊到tcp的tcp_pcb->errf()
異常回撥函數。
TCP PCB出現錯誤時,會呼叫當前函數回撥到介面層處理:
ERROR
、RCVPLUS
、SENDPLUS
事件;recv_mboxes
、accept_mboxes
傳送異常事件;這種做法的目的就是喚醒因各種情況而阻塞的應用程式,告知當前連線發生錯誤,需要處理。
static void
err_tcp(void *arg, err_t err)
{
struct netconn *conn;
enum netconn_state old_state;
void *mbox_msg;
SYS_ARCH_DECL_PROTECT(lev);
conn = (struct netconn *)arg;
LWIP_ASSERT("conn != NULL", (conn != NULL));
SYS_ARCH_PROTECT(lev); /* 系統保護:進入臨界 */
/* 發生錯誤,PCB就會被釋放,所以可在介面層解除繫結 */
conn->pcb.tcp = NULL;
/* 儲存錯誤碼 */
conn->pending_err = err;
/* 防止應用程式執行緒在'recvmbox'/'acceptmbox'上阻塞 */
conn->flags |= NETCONN_FLAG_MBOXCLOSED;
/* 在喚醒其它執行緒前,重置當前狀態 */
old_state = conn->state;
conn->state = NETCONN_NONE;
SYS_ARCH_UNPROTECT(lev); /* 退出臨界 */
/* 通知socket層,當前連線異常。 */
API_EVENT(conn, NETCONN_EVT_ERROR, 0);
/* 給socket層一個可讀、可寫事件,可讓應用層不會阻塞於讀、寫。 */
API_EVENT(conn, NETCONN_EVT_RCVPLUS, 0);
API_EVENT(conn, NETCONN_EVT_SENDPLUS, 0);
mbox_msg = lwip_netconn_err_to_msg(err); /* err翻譯成msg */
/* 通過error message到recvmbox來喚醒阻塞於recv的應用層執行緒 */
if (NETCONN_MBOX_VALID(conn, &conn->recvmbox)) {
/* use trypost to prevent deadlock */
/* 使用trypost,可以防止死鎖 */
sys_mbox_trypost(&conn->recvmbox, mbox_msg);
}
/* 通過error message到acceptmbox來喚醒阻塞於accept的應用層執行緒 */
if (NETCONN_MBOX_VALID(conn, &conn->acceptmbox)) {
/* 使用trypost,可以防止死鎖 */
sys_mbox_trypost(&conn->acceptmbox, mbox_msg);
}
if ((old_state == NETCONN_WRITE) || (old_state == NETCONN_CLOSE) ||
(old_state == NETCONN_CONNECT)) { /* 處於非監聽的所有有效態 */
/* PCB已經被幹掉了,所以沒必要呼叫lwip_netconn_do_writemore()、lwip_netconn_do_close_internal()這些函數了 */
int was_nonblocking_connect = IN_NONBLOCKING_CONNECT(conn); /* 獲取當前netconn是否處於非阻塞連線 */
SET_NONBLOCKING_CONNECT(conn, 0); /* 清除netconn中該標記 */
if (!was_nonblocking_connect) { /* 不處於非阻塞連線狀態 */
sys_sem_t *op_completed_sem;
/* set error return code */
LWIP_ASSERT("conn->current_msg != NULL", conn->current_msg != NULL);
if (old_state == NETCONN_CLOSE) {
/* netconn處於close狀態,則返回OK,表示close成功 */
conn->current_msg->err = ERR_OK;
} else {
/* 如果處於寫或連線狀態,則返回對應ERR,表示當前連線異常。 */
conn->current_msg->err = err;
}
/* 獲取當前netconn的同步號誌 */
op_completed_sem = LWIP_API_MSG_SEM(conn->current_msg);
LWIP_ASSERT("invalid op_completed_sem", sys_sem_valid(op_completed_sem));
conn->current_msg = NULL; /* 解綁netconn中的當前的同步號誌 */
/* 喚醒阻塞與寫或連線的應用程式執行緒 */
sys_sem_signal(op_completed_sem);
} else { /* 應用程式執行緒是非阻塞連線 */
/* @todo: 測試非阻塞連線的錯誤情況 */
}
} else {
/* netconn處於監聽態或空閒態 */
LWIP_ASSERT("conn->current_msg == NULL", conn->current_msg == NULL);
}
}
accept_function()
是TCP netconn註冊到tcp的lpcb->accept()
accept回撥函數。
tcp_accept()
API註冊。lpcb->accept()
,用於TCP伺服器,監聽型別的pcb。
recv_udp()
:
通過lwip核心實現的學習,我們知道,lwip核心實現是需要執行緒安全的。
目前有兩種方式:
LWIP_TCPIP_CORE_LOCKING
核心安全鎖功能,使用該鎖來實現lwip核心的執行緒安全。
netconn使用者介面形式:netconn_xxx()
netconn核心介面形式:lwip_netconn_xxx()
使用者呼叫netconn使用者介面時,使用者介面的目的就是把netconn的核心介面通過安全鎖或api訊息傳送到wlip核心執行緒執行。
netconn使用者介面使用netconn_apimsg()
-->tcpip_send_msg_wait_sem()
來共同實現。
netconn_apimsg()
:
tcpip_callback_fn fn
:需要執行緒安全的netconn核心API。struct api_msg *apimsg
:API的指標形參(既然形參是指標,說明是雙向引數)static err_t
netconn_apimsg(tcpip_callback_fn fn, struct api_msg *apimsg)
{
err_t err;
#ifdef LWIP_DEBUG
/* 捕獲不設定錯誤的函數 */
apimsg->err = ERR_VAL;
#endif /* LWIP_DEBUG */
#if LWIP_NETCONN_SEM_PER_THREAD
apimsg->op_completed_sem = LWIP_NETCONN_THREAD_SEM_GET(); /* 獲取同步號誌 */
#endif /* LWIP_NETCONN_SEM_PER_THREAD */
/* 把fn()搞到tcpip核心鎖內執行 */
err = tcpip_send_msg_wait_sem(fn, apimsg, LWIP_API_MSG_SEM(apimsg));
if (err == ERR_OK) {
return apimsg->err;
}
return err;
}
tcpip_send_msg_wait_sem()
:
tcpip_callback_fn fn
:需要執行緒安全的netconn核心API。void *apimsg
:API的指標形參。sys_sem_t *sem
:同步號誌。用於阻塞。tcpip_msg
,傳送到tcpip_mbox
,由TCPIP核心執行緒監測、執行。fn
釋放該同步號誌來解除呼叫者執行緒阻塞。LWIP_TCPIP_CORE_LOCKING
核心安全鎖,因為這是執行時開銷最小的方法。err_t
tcpip_send_msg_wait_sem(tcpip_callback_fn fn, void *apimsg, sys_sem_t *sem)
{
#if LWIP_TCPIP_CORE_LOCKING /* 開啟了核心鎖,直接在當前執行緒呼叫即可 */
LWIP_UNUSED_ARG(sem);
LOCK_TCPIP_CORE(); /* 核心鎖上鎖 */
fn(apimsg); /* 執行回撥 */
UNLOCK_TCPIP_CORE(); /* 釋放核心鎖 */
return ERR_OK;
#else /* LWIP_TCPIP_CORE_LOCKING */ /* 沒有開啟核心鎖,需要把回撥函數外包到TCPIP核心執行緒 */
TCPIP_MSG_VAR_DECLARE(msg); /* 定義一個tcpip_msg */
LWIP_ASSERT("semaphore not initialized", sys_sem_valid(sem));
LWIP_ASSERT("Invalid mbox", sys_mbox_valid_val(tcpip_mbox));
TCPIP_MSG_VAR_ALLOC(msg); /* 開了MPU,這個就為NULL了 */
TCPIP_MSG_VAR_REF(msg).type = TCPIP_MSG_API; /* 無回傳的API訊息型別 */
TCPIP_MSG_VAR_REF(msg).msg.api_msg.function = fn; /* API */
TCPIP_MSG_VAR_REF(msg).msg.api_msg.msg = apimsg; /* apimsg */
sys_mbox_post(&tcpip_mbox, &TCPIP_MSG_VAR_REF(msg)); /* 往tcpip_mbox傳送一個tcpip_msg */
sys_arch_sem_wait(sem, 0); /* 等待同步號誌被回撥函數fn()釋放 */
TCPIP_MSG_VAR_FREE(msg); /* 釋放tcpip_msg */
return ERR_OK;
#endif /* LWIP_TCPIP_CORE_LOCKING */
}
這個同步號誌,就是用於阻塞的,具體是:netconn介面控制塊中的op_completed
號誌。
#define LWIP_API_MSG_SEM(msg) (&(msg)->conn->op_completed)
通過一個API例子來範例化執行緒安全的使用。
使用者呼叫netconn使用者介面netconn_new()
,其實就是netconn_new_with_proto_and_callback()
:
#define netconn_new(t) netconn_new_with_proto_and_callback(t, 0, NULL)
而netconn_new_with_proto_and_callback()
原始碼實現如下:
api_msg
資源,把需要核心執行的netconn核心介面和該介面需要的資料打包到api_msg
。然後將該msg傳送到lwip核心(或上鎖)執行。如果是傳送到lwip核心,則當前執行緒會等待同步號誌conn->op_completed
,如果核心執行了netconn核心介面,這個介面會釋放該號誌,表示核心已經執行了對應API。struct netconn *
netconn_new_with_proto_and_callback(enum netconn_type t, u8_t proto, netconn_callback callback)
{
struct netconn *conn;
API_MSG_VAR_DECLARE(msg); // 定義一個api_msg資料結構
API_MSG_VAR_ALLOC_RETURN_NULL(msg); // 申請api_msg資料結構資源,指定錯誤時返回NULL
conn = netconn_alloc(t, callback); // 申請netconn控制塊資源
if (conn != NULL) {
err_t err;
API_MSG_VAR_REF(msg).msg.n.proto = proto; // 把使用者連線協定記錄到api_msg中
API_MSG_VAR_REF(msg).conn = conn; // 把netconn控制塊記錄到api_msg中
// 把這個api_msg資源和lwip_netconn_do_newconn()函數封裝好,線上程安全下跑(上鎖或傳送到lwip核心)
err = netconn_apimsg(lwip_netconn_do_newconn, &API_MSG_VAR_REF(msg));
if (err != ERR_OK) {
LWIP_ASSERT("freeing conn without freeing pcb", conn->pcb.tcp == NULL);
LWIP_ASSERT("conn has no recvmbox", sys_mbox_valid(&conn->recvmbox));
#if LWIP_TCP
LWIP_ASSERT("conn->acceptmbox shouldn't exist", !sys_mbox_valid(&conn->acceptmbox));
#endif /* LWIP_TCP */
#if !LWIP_NETCONN_SEM_PER_THREAD
LWIP_ASSERT("conn has no op_completed", sys_sem_valid(&conn->op_completed));
sys_sem_free(&conn->op_completed);
#endif /* !LWIP_NETCONN_SEM_PER_THREAD */
sys_mbox_free(&conn->recvmbox);
memp_free(MEMP_NETCONN, conn);
API_MSG_VAR_FREE(msg);
return NULL;
}
}
API_MSG_VAR_FREE(msg); // 釋放api_msg資源
return conn;
}
lwip_netconn_do_newconn()
:
void
lwip_netconn_do_newconn(void *m)
{
struct api_msg *msg = (struct api_msg *)m;
msg->err = ERR_OK;
if (msg->conn->pcb.tcp == NULL) {
pcb_new(msg); // 建立一個新的特定型別的PCB。
}
/* 釋放號誌 */
TCPIP_APIMSG_ACK(msg);
}
參考./src/include/lwip/priv/api_msg.h
netconn核心介面是在LWIP執行緒安全的下執行的,要麼上lwip核心資源鎖,要麼傳送到lwip核心執行緒去執行,這些操作俊友netconn使用者介面去實現。
netconn核心介面主要封裝各個協定棧的RAW介面實現,如果執行緒安全是傳送到lwip核心實現,則需要在業務執行完畢後呼叫TCPIP_APIMSG_ACK(msg);
來解除呼叫者執行緒的阻塞。
部分介面列表:
void lwip_netconn_do_newconn (void *m);
void lwip_netconn_do_delconn (void *m);
void lwip_netconn_do_bind (void *m);
void lwip_netconn_do_bind_if (void *m);
void lwip_netconn_do_connect (void *m);
void lwip_netconn_do_disconnect (void *m);
void lwip_netconn_do_listen (void *m);
void lwip_netconn_do_send (void *m);
void lwip_netconn_do_recv (void *m);
#if TCP_LISTEN_BACKLOG
void lwip_netconn_do_accepted (void *m);
#endif /* TCP_LISTEN_BACKLOG */
void lwip_netconn_do_write (void *m);
void lwip_netconn_do_getaddr (void *m);
void lwip_netconn_do_close (void *m);
void lwip_netconn_do_shutdown (void *m);
#if LWIP_IGMP || (LWIP_IPV6 && LWIP_IPV6_MLD)
void lwip_netconn_do_join_leave_group(void *m);
void lwip_netconn_do_join_leave_group_netif(void *m);
#endif /* LWIP_IGMP || (LWIP_IPV6 && LWIP_IPV6_MLD) */
#if LWIP_DNS
void lwip_netconn_do_gethostbyname(void *arg);
#endif /* LWIP_DNS */
netconn介面就不分析原始碼了,直接描述功能。
這些介面在./src/include/lwip/api.h
路徑中。
下面只列出部分API,這些API都有兄嘚API,可以檢視上述路徑。
這些介面都是把netconn核心介面封裝到api_msg
中,然後將其轉發到lwip核心執行緒(或上鎖)執行。
是一個宏,實際是netconn_new_with_proto_and_callback()
:
#define netconn_new(t) netconn_new_with_proto_and_callback(t, 0, NULL)
struct netconn *
netconn_new_with_proto_and_callback(enum netconn_type t, u8_t proto, netconn_callback callback)
{}
建立一個新的netconn,指定協定,指定回撥函數。
協定型別檢視netconn_type
。
回撥函數檢視netconn_callback
:
/* 通知netconn事件的回撥原型 */
typedef void (* netconn_callback)(struct netconn *, enum netconn_evt, u16_t len);
netconn_delete()
函數關閉一個netconn「連線」並釋放它的資源。
UDP和RAW連線是完全關閉的,TCP pcb可能仍然在等待狀態後返回。
獲取netconn的local或remote的IP地址和埠號。
對於RAW型別的netconn,返回的不是埠號,而是協定。
netconn繫結指定的local IP地址和埠號。
一個netconn連續兩次繫結同一個IP(注意任意IP)和埠號,第二次會響應繫結失敗。
netconn連線到指定的remote IP和埠號。
netconn斷開當前連線(僅對UDP netconn有效)
設定一個TCP netconn進入監聽模式,設定backlog數量上限。
#define netconn_listen(conn) netconn_listen_with_backlog(conn, TCP_DEFAULT_LISTEN_BACKLOG)
TCP_DEFAULT_LISTEN_BACKLOG
預設0xff。
accept()一個新的TCP使用者端連線。
伺服器呼叫該函數可以從conn->acceptmbox
郵箱中獲取一個新的連線,如果郵箱為空,該函數會一直阻塞,直至有新的連線到來。
在呼叫次函數之前,先呼叫netconn_listen()
讓伺服器加入監聽狀態。
該函數是從conn->recvmbox
郵箱中等待資料訊息:這些訊息就是資料快取佇列:
recv_udp()
會先將接收到的UDP資料封裝在netbuf
結構中,然後將資料訊息投遞到郵箱中。pbuf
封裝的,在接收到封包後,函數netconn_recv()
才將這些pbuf
封裝成netbuf
。然後傳送一個API訊息lwip_netconn_do_recv()
到核心,告知TCP核心,本次已經從TCP快取中接收了多少資料,讓核心呼叫tcp_recved()
函數滑動接收視窗。
通過過UDP或RAW網路(已經連線)傳送資料。
大概內容就是從API訊息中獲取目的地址資訊和資料包pbuf,然後呼叫udp_send()
或udp_sendto()
將資料放出去。
用於在穩定的TCP連線中傳送資料。