【lwip】15-NETCONN介面

2023-05-30 15:00:51

前言

終於到介面層了。

原文:李柱明部落格: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介面。供使用者使用。

NETCONN重要組成

核心回撥介面

在raw/callback API程式設計時,使用者程式設計的方法就是向核心註冊各種自定義的回撥函數,回撥函數是與核心實現互動的唯一方式

協定棧API NETCONN是基於raw/callback API實現的。

核心通過呼叫註冊到TCP/UDP核心的回撥函數,把接收到的資料或可傳送資料的事件傳送回netconn對應的郵箱中,上層檢查這些郵箱即可和核心協定棧互動。

netbuf:資料緩衝

netbuf是應用層描述待傳送資料和已接收資料的基本結構。當然,最基本的粒度資料結構還是pbuf。

應用層接收資料:

  • 當協定棧收到封包後,會將資料封裝在一個netbuf​中,並遞交給應用層。

應用層傳送資料:

  • TCP:使用者只需要提供待發資料的起始地址和長度,核心會根據實際情況封裝在合適大小的封包中。
  • UDP:需要使用者自行將資料封裝在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 */
};

netconn:介面資料結構

代表一個連線,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​個郵件來解除這些應用層的阻塞。

NETCONN支援的協定型別

/** @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被netconn_write()介面呼叫,就處於WRITE狀態。
    也可以理解為當前netconn被哪些netconn API佔用 */
enum netconn_state {
  NETCONN_NONE,     /* 空閒狀態 */
  NETCONN_WRITE,    /* 正在傳送資料 */
  NETCONN_LISTEN,   /* 偵聽狀態 */
  NETCONN_CONNECT,  /* 連線狀態 */
  NETCONN_CLOSE     /* 關閉狀態 */
};

NETCONN北向事件

上層收:按次數統計。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相關介面

前提實現可以自行看原始碼。

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介面時,需要編寫這些回撥函數,並註冊到核心中。

註冊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接收回撥

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傳送回撥

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 poll函數

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;
}

err_tcp:TCP 異常回撥函數

poll_tcp()​是TCP netconn註冊到tcp的tcp_pcb->errf()​異常回撥函數。

TCP PCB出現錯誤時,會呼叫當前函數回撥到介面層處理:

  • 向netconn資料結構中的回撥函數傳送ERROR​、RCVPLUS​、SENDPLUS​事件;
  • 向netconn資料結構中的所有郵箱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 accept回撥函數

accept_function()​是TCP netconn註冊到tcp的lpcb->accept()​accept回撥函數。

  • 使用tcp_accept()​API註冊。
  • lpcb->accept()​,用於TCP伺服器,監聽型別的pcb。
  • TCP層收到使用者端連線,分配PCB,並握手成功後會呼叫當前回撥。(tcp申請新的使用者端PCB失敗時也會回撥)

其他需要註冊到協定棧核心的回撥函數

recv_udp()​:

  • udp的接收回撥函數,該函數會被udp_recv()函數註冊到UDP PCB中。在UDP收到資料時被呼叫,用於回撥資料到上層。
  • 是UDP收到的封包pbuf,組裝成一個上層封包格式netbuf。
  • 然後把這個netbuf投遞到連線netconn->recvmbox接收郵箱中。
  • 應用層可以通過呼叫API函數netconn_recv()從該郵箱中獲取netbuf格式的封包,然後提取出pbuf,再提取出UDP資料即可。

NETCONN介面執行緒安全實現(重要)

NETCONN介面執行緒安全原理

通過lwip核心實現的學習,我們知道,lwip核心實現是需要執行緒安全的。

目前有兩種方式:

  1. 開啟LWIP_TCPIP_CORE_LOCKING​核心安全鎖功能,使用該鎖來實現lwip核心的執行緒安全。
  2. 如果沒有開啟核心安全鎖,則可以把需要執行的lwip核心API通過API訊息傳送到LWIP核心執行緒去執行。

netconn使用者介面形式:netconn_xxx()

netconn核心介面形式:lwip_netconn_xxx()

使用者呼叫netconn使用者介面時,使用者介面的目的就是把netconn的核心介面通過安全鎖或api訊息傳送到wlip核心執行緒執行。

NETCONN介面執行緒安全具體實現

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執行緒執行的步驟:構建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()​:

  • 建立一個新的特定型別的PCB。
  • 執行完畢,釋放號誌,解除呼叫者執行緒阻塞。
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);
}

NETCONN核心介面

參考./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使用者介面

netconn介面就不分析原始碼了,直接描述功能。

這些介面在./src/include/lwip/api.h​路徑中。

下面只列出部分API,這些API都有兄嘚API,可以檢視上述路徑。

這些介面都是把netconn核心介面封裝到api_msg​中,然後將其轉發到lwip核心執行緒(或上鎖)執行。

netconn_new():新建一個netconn介面控制塊

是一個宏,實際是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介面控制塊

netconn_delete()​函數關閉一個netconn「連線」並釋放它的資源。

UDP和RAW連線是完全關閉的,TCP pcb可能仍然在等待狀態後返回。

netconn_getaddr():獲取地址資訊

獲取netconn的local或remote的IP地址和埠號。

對於RAW型別的netconn,返回的不是埠號,而是協定。

netconn_bind():繫結本地IP&PORT

netconn繫結指定的local IP地址和埠號。

一個netconn連續兩次繫結同一個IP(注意任意IP)和埠號,第二次會響應繫結失敗。

netconn_connect():連線遠端

netconn連線到指定的remote IP和埠號。

netconn_disconnect():斷開連線

netconn斷開當前連線(僅對UDP netconn有效)

netconn_listen():監聽

設定一個TCP netconn進入監聽模式,設定backlog數量上限。

#define netconn_listen(conn) netconn_listen_with_backlog(conn, TCP_DEFAULT_LISTEN_BACKLOG)

TCP_DEFAULT_LISTEN_BACKLOG​預設0xff。

netconn_accept():接受連線

accept()一個新的TCP使用者端連線。

伺服器呼叫該函數可以從conn->acceptmbox​郵箱中獲取一個新的連線,如果郵箱為空,該函數會一直阻塞,直至有新的連線到來。

在呼叫次函數之前,先呼叫netconn_listen()​讓伺服器加入監聽狀態。

netconn_recv():接收資料

該函數是從conn->recvmbox​郵箱中等待資料訊息:這些訊息就是資料快取佇列:

  • 對於UDP連線:回撥函數recv_udp()​會先將接收到的UDP資料封裝在netbuf​結構中,然後將資料訊息投遞到郵箱中。
  • 對於TCP連線:投遞到該郵箱中的資料依然是pbuf​封裝的,在接收到封包後,函數netconn_recv()​才將這些pbuf​封裝成netbuf​。然後傳送一個API訊息lwip_netconn_do_recv()​到核心,告知TCP核心,本次已經從TCP快取中接收了多少資料,讓核心呼叫tcp_recved()​函數滑動接收視窗。

netconn_sent():UDP/RAW傳送資料

通過過UDP或RAW網路(已經連線)傳送資料。

大概內容就是從API訊息中獲取目的地址資訊和資料包pbuf,然後呼叫udp_send()​或udp_sendto()​將資料放出去。

netconn_write():TCP傳送資料

用於在穩定的TCP連線中傳送資料。

netconn_close():關閉連線