【lwip】13-TCP協議分析之源碼篇鏈接:[https://www.cnblogs.com/lizhuming/p/17438682.html](https://www.cnblogs.com/lizhuming/p/17438682.html)

来源:https://www.cnblogs.com/lizhuming/archive/2023/05/28/17438682.html
-Advertisement-
Play Games

## 前言 TCP源碼篇,當前只分析TCP層的源碼實現,按功能分塊分析,介面為RAW介面。 NETCONN介面和SOCKET介面會獨立一篇文章進行分析。 本文基於讀者已學習了TCP協議原理篇的基礎上進行源碼分析,不再在此篇文章中過多解析TCP相關概念。 ‍ 建議讀者對著LWIP庫源碼進行閱讀。對於初 ...


前言

TCP源碼篇,當前只分析TCP層的源碼實現,按功能分塊分析,介面為RAW介面。

NETCONN介面和SOCKET介面會獨立一篇文章進行分析。

本文基於讀者已學習了TCP協議原理篇的基礎上進行源碼分析,不再在此篇文章中過多解析TCP相關概念。

建議讀者對著LWIP庫源碼進行閱讀。對於初學者,可有點難度的,但是對於喜歡讀源碼的同學來說,會充實TCP原理。

‍上一年就寫好了,一直沒時間整理出來,現在不整理了,直接放出來。
鏈接:https://www.cnblogs.com/lizhuming/p/17438682.html

TCP首部數據結構

參考文件:./src/include/lwip/prot/tcp.h

TCP首部的數據結構及欄位操作都在這個文件中。

如:TCP首部數據結構struct tcp_hdr

#define PACK_STRUCT_FIELD(x) x
struct tcp_hdr {
  PACK_STRUCT_FIELD(u16_t src);
  PACK_STRUCT_FIELD(u16_t dest);
  PACK_STRUCT_FIELD(u32_t seqno);
  PACK_STRUCT_FIELD(u32_t ackno);
  PACK_STRUCT_FIELD(u16_t _hdrlen_rsvd_flags);
  PACK_STRUCT_FIELD(u16_t wnd);
  PACK_STRUCT_FIELD(u16_t chksum);
  PACK_STRUCT_FIELD(u16_t urgp);
} PACK_STRUCT_STRUCT;

TCP控制塊

TCP控制塊(TCP PCB)這個是每個TCP連接的中央,非常重要,保存了TCP相關的重要數據,所以先瞭解下TCP控制塊的各個欄位。

對於初學者,可以先略過一眼TCP控制塊的各個欄位,在分析具體操作源碼時,遇到不懂的變數可以回TCP控制塊查找。

/** the TCP protocol control block */
struct tcp_pcb {
/** common PCB members */
  IP_PCB;
/** protocol specific PCB members */
  TCP_PCB_COMMON(struct tcp_pcb);

  /* ports are in host byte order */
  u16_t remote_port;

  tcpflags_t flags;
#define TF_ACK_DELAY   0x01U   /* Delayed ACK. */
#define TF_ACK_NOW     0x02U   /* Immediate ACK. */
#define TF_INFR        0x04U   /* In fast recovery. */
#define TF_CLOSEPEND   0x08U   /* If this is set, tcp_close failed to enqueue the FIN (retried in tcp_tmr) */
#define TF_RXCLOSED    0x10U   /* rx closed by tcp_shutdown */
#define TF_FIN         0x20U   /* Connection was closed locally (FIN segment enqueued). */
#define TF_NODELAY     0x40U   /* Disable Nagle algorithm */
#define TF_NAGLEMEMERR 0x80U   /* nagle enabled, memerr, try to output to prevent delayed ACK to happen */
#if LWIP_WND_SCALE
#define TF_WND_SCALE   0x0100U /* Window Scale option enabled */
#endif
#if TCP_LISTEN_BACKLOG
/* 接入的TCP客戶端握手成功,等待被accept() */
#define TF_BACKLOGPEND 0x0200U
#endif
#if LWIP_TCP_TIMESTAMPS
#define TF_TIMESTAMP   0x0400U   /* Timestamp option enabled */
#endif
#define TF_RTO         0x0800U /* RTO計時器已觸發,unacked隊列數據已遷回unsent隊列,並正在重傳 */
#if LWIP_TCP_SACK_OUT
#define TF_SACK        0x1000U /* Selective ACKs enabled */
#endif

  /* Timers */
  /* 空閑poll周期回調相關:polltmr會周期性增加,當其值超過pollinterval時,poll函數會被調用。 */
  u8_t polltmr, pollinterval;
  /* 控制塊被最後一次處理的時間 */
  u8_t last_timer;
  /* 保存這控制塊的TCP節拍起始值。用於當前PCB的時基初始值參考 */
  /* 活動計時器,收到合法報文時自動更新。 */
  u32_t tmr;

  /* receiver variables */
  u32_t rcv_nxt; /* 期待收到的下一個seq號。一般發送報文段時,ACK值就是該值 */
  tcpwnd_size_t rcv_wnd; /* 接收視窗實時大小:從遠端收到數據,該值減小;應用層讀走數據,該值增加。 */
  tcpwnd_size_t rcv_ann_wnd; /* 視窗通告值大小:即是告訴發送方,我們這邊的介面視窗的大小 */
  u32_t rcv_ann_right_edge; /* 視窗通告值右邊界 */

#if LWIP_TCP_SACK_OUT
  /* SACK ranges to include in ACK packets (entry is invalid if left==right) */
  struct tcp_sack_range rcv_sacks[LWIP_TCP_MAX_SACK_NUM]; /* SACK左右邊界,TCP協議最多支持4對 */
#define LWIP_TCP_SACK_VALID(pcb, idx) ((pcb)->rcv_sacks[idx].left != (pcb)->rcv_sacks[idx].right)
#endif /* LWIP_TCP_SACK_OUT */

  s16_t rtime; /* 超時重傳計時器值,當該值大於RTO值時,重傳報文 */

  u16_t mss;   /* 遠端的MSS */

  /* RTT (round trip time) 估算 */
  u32_t rttest; /* RTT測量,發送時的時間戳。精度500ms */
  u32_t rtseq;  /* 開始計算RTT時對應的seq號 */
  /* RTT估計出的平均值和時間差。
      註意:sa為演算法中8倍的均值;sv為4倍的方差。再去分析LWIP實現RTO的演算法。 */
  s16_t sa, sv; /* @see "Congestion Avoidance and Control" by Van Jacobson and Karels */

  s16_t rto;    /* 重傳超時時間。節拍巨集:TCP_SLOW_INTERVAL。初始超時時間巨集:LWIP_TCP_RTO_TIME *//* retransmission time-out (in ticks of TCP_SLOW_INTERVAL) */
  u8_t nrtx;    /* 重發次數 */

  /* 快重傳和快恢復相關:參考卷一中的快速重傳和快速恢復章節:21.7 */
  u8_t dupacks; /* 收到最大重覆ACK的次數:一般收1-2次認為是重排序引起的。收到3次後,可以確認為失序,需要立即重傳。然後執行擁塞避免演算法中的快恢復。 */
  u32_t lastack; /* 接收到的最大有序ACK號 */

  /* congestion avoidance/control variables */
  tcpwnd_size_t cwnd; /* 擁塞視窗大小 */
  tcpwnd_size_t ssthresh; /* 擁塞避免演算法啟動閾值。也叫慢啟動上門限值。 */

  /* rto重傳的那些報文段的下一個seq號。用於解除rto狀態。 */
  u32_t rto_end;

  u32_t snd_nxt;  /* 下一個需要發送的seq號。一般也是收到最新最大的ACK號。 */ /* next new seqno to be sent */
  u32_t snd_wl1, snd_wl2; /* 上次發送視窗更新時,收到的seq號和ack號。在tcp_receive()用於更新發送視窗。 */ /* Sequence and acknowledgement numbers of last window update. */
  u32_t snd_lbb;  /* 下一個被緩衝的應用程式數據的seq號 */  /* Sequence number of next byte to be buffered. */
  tcpwnd_size_t snd_wnd; /* 發送視窗的大小:實時的。發出數據,該值減少;收到ACK,該值增加。 */  /* sender window */
  tcpwnd_size_t snd_wnd_max; /* 發送視窗最大值:就是遠端的視窗通告值大小。 */ /* the maximum sender window announced by the remote host */

  tcpwnd_size_t snd_buf; /* 發送緩衝區剩餘空間 */ /* Available buffer space for sending (in bytes). */
#define TCP_SNDQUEUELEN_OVERFLOW (0xffffU-3)
  u16_t snd_queuelen; /* 發送緩衝區中現有的pbuf個數 */  /* Number of pbufs currently in the send buffer. */

#if TCP_OVERSIZE
  /* 在未發送的TCP數據中,最後一個pbuf剩餘的未使用的空間size */
  u16_t unsent_oversize;
#endif /* TCP_OVERSIZE */

  tcpwnd_size_t bytes_acked; /* 累計ACK新數據的量。擁塞避免時,用於判斷cwnd是否需要+1MSS。 */

  /* 幾條TCP報文段緩存隊列指針 */
  struct tcp_seg *unsent;  /* 未發送的報文段隊列 */
  struct tcp_seg *unacked; /* 已發送,但是未收到ACK的報文段隊列 */
#if TCP_QUEUE_OOSEQ
  struct tcp_seg *ooseq; /* 接收到的亂序報文段隊列 */
#endif /* TCP_QUEUE_OOSEQ */

  struct pbuf *refused_data; /* 接收到,但未被應用層取走的報文段隊列 */

#if LWIP_CALLBACK_API || TCP_LISTEN_BACKLOG
  /* 當前連接屬於哪個伺服器 */
  struct tcp_pcb_listen* listener;
#endif /* LWIP_CALLBACK_API || TCP_LISTEN_BACKLOG */

#if LWIP_CALLBACK_API
  /* 幾個回調函數。由用戶註冊。 */
  /* 數據發送成功後被回調 */
  tcp_sent_fn sent;
  /* 收到有序數據後被回調 */
  tcp_recv_fn recv;
  /* 建立連接後被回調 */
  tcp_connected_fn connected;
  /* 該函數被內核周期性回調。參考polltmr */
  tcp_poll_fn poll;
  /* 發生錯誤時被回調 */
  tcp_err_fn errf;
#endif /* LWIP_CALLBACK_API */

#if LWIP_TCP_TIMESTAMPS /* TSOPT選項:用於時間戳和防止序列號迴繞。 */
  u32_t ts_lastacksent; /* 期待收到下一個回顯時間戳對應的seq號 */
  u32_t ts_recent; /* 收到對端的時間戳 */
#endif /* LWIP_TCP_TIMESTAMPS */

  /* keepalive計時器的上限值 */
  u32_t keep_idle;
#if LWIP_TCP_KEEPALIVE
  /* keepalive探測間隔 */
  u32_t keep_intvl;
  /* keepalive探測的上限次數 */
  u32_t keep_cnt;
#endif /* LWIP_TCP_KEEPALIVE */

  /* 堅持定時器:用於解決遠端接收視窗為0時,定時詢問使用 */
  u8_t persist_cnt; /* 堅持定時器節拍計數值 */
  u8_t persist_backoff; /* 堅持定時器探查報文時間間隔列表索引 */
  u8_t persist_probe; /* 堅持定時器視窗0時發出的探查報文次數 */

  /* KEEPALIVE counter */
  /* 保活定時器 */
  /* 保活計數值 */
  u8_t keep_cnt_sent;

#if LWIP_WND_SCALE /* WSOPT選項欄位。用於TCP視窗擴展。 */
  u8_t snd_scale; /* 發送視窗偏移bit */
  u8_t rcv_scale; /* 接收視窗偏移bit */
#endif
};

報文段數據結構

TCP是基於位元組流的傳輸層通信協議。

每次收發都是報文段形式,所以需要相關數據結構來管理收發的報文段。

在TCP控制塊中有三個緩衝隊列,都已報文段形式保存:

  • struct tcp_seg *unsent:未發送隊列。即是等待發送的報文段隊列。
  • struct tcp_seg *unacked:空中報文隊列。即是已經發送,但是還沒收到ACK的報文段隊列。
  • struct tcp_seg *ooseq:亂序報文隊列。即是收到的報文是視窗內,但是不是當前期待收到的下一個SEQ的報文段。先用改隊列存起來,等收到前面空缺的報文後就可以直接接上這些報文段了。

tcp_seg​數據結構中維護TCP首部指針struct tcp_hdr *tcphdr;​是很有必要的,因為tcp_seg​在處理過程中,會頻繁移動pbuf->payload​指針,所以需要一個專門的TCP首部指針來維護。

struct tcp_seg

/* This structure represents a TCP segment on the unsent, unacked and ooseq queues */
struct tcp_seg {
  struct tcp_seg *next; /* 鏈表節點 */
  struct pbuf *p; /* TCP報文:TCP首部 + TCP數據 */
  u16_t len; /* 報文段的純TCP數據長度(不統計SYN和FIN) */
#if TCP_OVERSIZE_DBGCHECK
  u16_t oversize_left; /* 當前報文段中最後一個pbuf的可用剩餘空間 */
#endif /* TCP_OVERSIZE_DBGCHECK */
#if TCP_CHECKSUM_ON_COPY
  u16_t chksum;
  u8_t  chksum_swapped;
#endif /* TCP_CHECKSUM_ON_COPY */
  u8_t  flags;
#define TF_SEG_OPTS_MSS         (u8_t)0x01U /* Include MSS option (only used in SYN segments) */
#define TF_SEG_OPTS_TS          (u8_t)0x02U /* Include timestamp option. */
#define TF_SEG_DATA_CHECKSUMMED (u8_t)0x04U /* ALL data (not the header) is
                                               checksummed into 'chksum' */
#define TF_SEG_OPTS_WND_SCALE   (u8_t)0x08U /* Include WND SCALE option (only used in SYN segments) */
#define TF_SEG_OPTS_SACK_PERM   (u8_t)0x10U /* Include SACK Permitted option (only used in SYN segments) */
  struct tcp_hdr *tcphdr;  /* the TCP header */
};

重要全局數據

TCP控制塊鏈表

參考文件:./src/core/tcp.c

TCP控制塊鏈表是記錄每個TCP連接,根據TCP狀態而寄存到不同的鏈表中:

/* 處於已綁定狀態的TCP PCB */
struct tcp_pcb *tcp_bound_pcbs;
/* 處於監聽狀態的TCP PCB */
union tcp_listen_pcbs_t tcp_listen_pcbs;
/* 處於穩定狀態的TCP PCB */
struct tcp_pcb *tcp_active_pcbs;
/* 處於TIME_WAIT狀態的TCP PCB */
struct tcp_pcb *tcp_tw_pcbs;

把這些鏈表都統一管理起來:

/** An array with all (non-temporary) PCB lists, mainly used for smaller code size */
struct tcp_pcb **const tcp_pcb_lists[] = {&tcp_listen_pcbs.pcbs, &tcp_bound_pcbs,
         &tcp_active_pcbs, &tcp_tw_pcbs
};

監聽態的TCP PCB數據結構:

因為處於監聽態的TCP PCB沒有實際的TCP連接,TCP PCB數據結構中的大量數據都用不到,所以處於監聽態的TCP PCB,使用另一種數據結構來管理,降低記憶體使用。

監聽鏈表相關的數據結構:

/* 註意:是聯合體 */
union tcp_listen_pcbs_t {
  struct tcp_pcb_listen *listen_pcbs;
  struct tcp_pcb *pcbs;
};

/* 因為監聽態的連接沒有大量複雜邏輯的數據交互需求,所以監聽態的TCP PCB比較簡單 */
struct tcp_pcb_listen {
/** Common members of all PCB types */
  IP_PCB;
/** Protocol specific PCB members */
  TCP_PCB_COMMON(struct tcp_pcb_listen);

#if LWIP_CALLBACK_API
  /* 偵聽到有連接接入時被調用的函數 */
  tcp_accept_fn accept;
#endif /* LWIP_CALLBACK_API */

#if TCP_LISTEN_BACKLOG
  u8_t backlog; /* 等待accept()連接的上限值 */
  u8_t accepts_pending; /* 握手成功,準備準備好了,但是還沒有accept()的連接的數量 */
#endif /* TCP_LISTEN_BACKLOG */
};

TCP單幀入站相關數據

LWIP是一個內核單線程的TCPIP協議棧,所以收到TCP包後,LWIP內核就會單獨處理該TCP包,不會出現同一個LWIP內核併發處理多個TCP包。

所以可以為TCP包創建一些全局值,減少函數間的參數傳遞。

/* 這些全局變數有tcp_input()收到TCP報文段後設置的,表示當前接收到,正在處理的TCP報文段信息 */
static struct tcp_seg inseg; /* TCP報文段數據結構 */
static struct tcp_hdr *tcphdr; /* TCP首部 */
static u16_t tcphdr_optlen; /* 選項欄位長度 */
static u16_t tcphdr_opt1len; /* 選項欄位在第一個pbuf中的長度 */
static u8_t *tcphdr_opt2; /* 在下一個pbuf中的選項欄位指針 */
static u16_t tcp_optidx; /* 選項欄位索引 */
static u32_t seqno, ackno; /* TCP的seq號和ack號 */
static tcpwnd_size_t recv_acked; /* 本次接收到的報文段中能確認pcb->unacked報文的長度(遇到SYN|FIN會--,所以最終是TCP數據長度) */
static u16_t tcplen; /* 報文段的數據區長度。註意:SYN或FIN也占用seq號,該值+1 */
static u8_t flags; /* TCP首部各個標誌欄位 */

static u8_t recv_flags; /* 記錄tcp_process()對報文段的處理結果 */
static struct pbuf *recv_data; /* 單次提交到應用層的數據緩衝區。本次input_receive()處理後,把需要遞交到應用層的數據,緩存到這裡。 */

/* 當前進行輸入處理的TCP PCB。時刻唯一 */
struct tcp_pcb *tcp_input_pcb;

TCP RAW介面分析

先分析北向介面,這些介面可供用戶使用。

相關文件:

  • lwip/src/core/tcp.c
  • lwip/src/include/lwip/tcp.h

LWIP介面層級:RAW --> NETCONN --> SOCKET。

RAW介面使用

建立連接

用於建立連接的函數類似於連續API和BSD套接字API的函數。

使用tcp_new()函數創建一個新的TCP連接標識符(即協議控制塊PCB)。

然後可以將這個PCB設置為監聽新的傳入連接,或者顯式地連接到另一個主機。

參考使用:

tcp_new(); /* 新建一個TCP */
tcp_bind(); /* 綁定本地服務 */
tcp_listen(); /* or */ tcp_listen_with_backlog(); /* 監聽(用於服務端) */
tcp_accept(); /* 接受連接(用於服務端) */
tcp_connect(); /* 建立一個連接(用於客戶端) */

發送數據

通過調用tcp_write()​對數據進行排隊,並通過調用tcp_output()​觸發發送TCP數據。

當數據成功傳輸到遠程主機時,將通過tcp_sent()​指定回調函數回調通知到應用程式。

tcp_write(); /* 該函數用於把數據插入TCP發送緩衝區 */
tcp_output(); /* 該函數用於觸發TCP緩衝區發送數據 */
tcp_sent(); /* 註冊發送回調函數 */

接收數據

TCP數據接收是基於回調函數實現的。

當新數據到達時調用應用程式之前tcp_recv()​註冊的回調函數。

當應用程式獲得數據後,它必須調用tcp_recved()​函數來指示TCP可以通告增加接收視窗。

tcp_recv(); /* 註冊接收回調函數 */
tcp_recved(); /* 應用層成功接收到數據通知回TCP的函數 */

應用輪詢(守護)

邏輯功能:就是註冊一個poll()函數到TCP內核,這個函數會被TCP內核周期調用。

當連接空閑時(即,既沒有傳輸數據也沒有接收數據),lwip將通過調用指定的回調函數來反覆輪詢應用程式。

這既可以用作看門狗定時器來終止空閑時間過長的連接,也可以用作一種等待記憶體可用的方法。

例如,如果由於記憶體不可用而導致tcp_write()​發送數據失敗,則應用程式可能在連接空閑一段時間後使用輪詢功能再次調用tcp_write()​。

tcp_poll(); /* 註冊周期回調函數,被TCP內核周期調用 */

關閉連接

關閉和中止連接。

tcp_close()​是通過四次揮手(FIN​)正常關閉連接。

tcp_abort()​是通過RST​強制終止連接。

tcp_err()​是註冊異常回調函數。當TCP異常時,會通過該函數註冊的回調函數通知應用層。

  • 註意:當調用這個回調時,相應的pcb已經被釋放了!
tcp_close(); /* 正常關閉連接,釋放PCB資源 */
tcp_abort(); /* RST方式終止連接 */
tcp_err(); /* 註冊異常回調函數 */

新建控制塊:tcp_new()

tcp_new()​介面調用tcp_alloc()​介面。

一個TCP連接需要TCP PCB(TCP 控制塊)來管理本連接的相關數據。

在本函數中,能瞭解到LWIP申請TCP PCB的記憶體管理邏輯,也能找到TCP性能的預設值(這個對TCP網路分析的同學挺有用的)。

struct tcp_pcb *tcp_alloc(u8_t prio):申請&初始化TCP PCB。

  • u8_t prio:新建的TCP PCB優先順序。
  • 如果MEMP_TCP_PCB記憶體池還有空間,則直接從該記憶體池申請。
  • 如果MEMP_TCP_PCB記憶體池空間不足,則按照以下順序進行強制占用:最老的:TIME-WAIT > LAST_ACK > CLOSING > 優先順序更低的已激活的連接。
/**
 * 申請tcp pcb記憶體。
 * 如果記憶體不足,按以下順序釋放pcb:最老的:TIME-WAIT > LAST_ACK > CLOSING > 優先順序更低的已激活的連接。
 * tcp pcb記憶體資源申請成功後,初始化部分欄位。
 *
 */
struct tcp_pcb *
tcp_alloc(u8_t prio)
{
  struct tcp_pcb *pcb;

  LWIP_ASSERT_CORE_LOCKED();

  pcb = (struct tcp_pcb *)memp_malloc(MEMP_TCP_PCB);
  if (pcb == NULL) {
    /* 先處理那些處於TF_CLOSEPEND狀態的pcb。主動觸發他們再次發起FIN。(之前發送FIN失敗的pcb,這些pcb都是我們想關閉的pcb了) */
    tcp_handle_closepend();

    LWIP_DEBUGF(TCP_DEBUG, ("tcp_alloc: killing off oldest TIME-WAIT connection\n"));
    /* 記憶體不足,幹掉最老的TIME_WAIT連接 */
    tcp_kill_timewait();
    pcb = (struct tcp_pcb *)memp_malloc(MEMP_TCP_PCB);
    if (pcb == NULL) {
      LWIP_DEBUGF(TCP_DEBUG, ("tcp_alloc: killing off oldest LAST-ACK connection\n"));
      /* 還是記憶體不足,就幹掉最老的LAST_ACK連接 */
      tcp_kill_state(LAST_ACK);
      pcb = (struct tcp_pcb *)memp_malloc(MEMP_TCP_PCB);
      if (pcb == NULL) {
        LWIP_DEBUGF(TCP_DEBUG, ("tcp_alloc: killing off oldest CLOSING connection\n"));
        /* 還是記憶體不足,幹掉最老的CLOSING連接 */
        tcp_kill_state(CLOSING);
        pcb = (struct tcp_pcb *)memp_malloc(MEMP_TCP_PCB);
        if (pcb == NULL) {
          LWIP_DEBUGF(TCP_DEBUG, ("tcp_alloc: killing oldest connection with prio lower than %d\n", prio));
          /* 還是記憶體不足,那就幹掉優先順序更低的最老的連接 */
          tcp_kill_prio(prio);
          pcb = (struct tcp_pcb *)memp_malloc(MEMP_TCP_PCB);
          if (pcb != NULL) {
            /* 還是記憶體不足,沒辦法了 */
            MEMP_STATS_DEC(err, MEMP_TCP_PCB);
          }
        }
        if (pcb != NULL) {
          /* adjust err stats: memp_malloc failed multiple times before */
          MEMP_STATS_DEC(err, MEMP_TCP_PCB);
        }
      }
      if (pcb != NULL) {
        /* adjust err stats: memp_malloc failed multiple times before */
        MEMP_STATS_DEC(err, MEMP_TCP_PCB);
      }
    }
    if (pcb != NULL) {
      /* adjust err stats: memp_malloc failed above */
      MEMP_STATS_DEC(err, MEMP_TCP_PCB);
    }
  }
  if (pcb != NULL) {
    /* 申請成功 */
    memset(pcb, 0, sizeof(struct tcp_pcb)); /* 清空所有欄位 */
    pcb->prio = prio; /* 設置控制塊優先順序 */
    pcb->snd_buf = TCP_SND_BUF; /* 設置發送緩衝區大小 */
    pcb->rcv_wnd = pcb->rcv_ann_wnd = TCPWND_MIN16(TCP_WND); /* 初始化接收視窗和視窗通告值 */
    pcb->ttl = TCP_TTL; /* TTL */
    pcb->mss = INITIAL_MSS; /* 初始化MSS,在SYN時,會在選項欄位發送到對端。 */
    pcb->rto = LWIP_TCP_RTO_TIME / TCP_SLOW_INTERVAL; /* 初始RTO時間為LWIP_TCP_RTO_TIME,預設3000ms */
    pcb->sv = LWIP_TCP_RTO_TIME / TCP_SLOW_INTERVAL; /* 初始RTT時間差為RTO的初始值 */
    pcb->rtime = -1; /* 初始為停止重傳計時值計時 */
    pcb->cwnd = 1; /* 初始擁塞視窗值 */
    pcb->tmr = tcp_ticks; /* 保存當前TCP節拍值為當前PCB的TCP節拍初始值 */
    pcb->last_timer = tcp_timer_ctr; /* 初始化PCB最後一次活動的時間 */

    /* RFC 5618建議設置ssthresh值儘可能高,比如設置為最大可能的視窗通告值大小(可以理解為最大可能的發送視窗大小 )。 */
    /* 這裡先設置為本地發送緩衝區大小,即是最大飛行數據量。後面進行視窗縮放和自動調優時自動調整。 */
    pcb->ssthresh = TCP_SND_BUF;

#if LWIP_CALLBACK_API
    /* 預設接收回調 */
    pcb->recv = tcp_recv_null;
#endif /* LWIP_CALLBACK_API */

    /* 保活計時器超時值:預設7200秒,即是兩小時。 */
    pcb->keep_idle  = TCP_KEEPIDLE_DEFAULT;

#if LWIP_TCP_KEEPALIVE
    /* 保活時間間隔:預設75秒 */
    pcb->keep_intvl = TCP_KEEPINTVL_DEFAULT;
    /* 保活探測數:預設9次。 */
    pcb->keep_cnt   = TCP_KEEPCNT_DEFAULT;
#endif /* LWIP_TCP_KEEPALIVE */
  }
  return pcb;
}

綁定本地服務:tcp_bind()

TCP PCB新建後,需要綁定本地的IP和埠號,這樣就能表示一個接入到應用層的連接了。

err_t tcp_bind(struct tcp_pcb *pcb, const ip_addr_t *ipaddr, u16_t port)

  • struct tcp_pcb *pcb:TCP PCB。

  • const ip_addr_t *ipaddr:需要綁定的本地IP地址。如果本地IP填了NULLIP_ANY_TYPE,則表示任意IP,綁定本地所有IP的意思。

  • u16_t port:需要綁定的綁定埠號。如果本地埠號填了0,則會調用tcp_new_port()申請一個隨機埠號。如果指定了埠號,需要檢查是否有復用。

  • SO_REUSE:如果設置了SO_REUSEADDR選項,且綁定的IP和PORT已經被使用且處於TIME_WAIT狀態,也可以被重覆使用。如果沒有設置,則不能釋放處於TIME_WAIT狀態的PCB。

  • IP&PORT復用檢查:遍歷所有pcb鏈表tcp_pcb_lists[],如果當前IP和埠號已經被使用了,且任意一個PCB沒有開啟埠復用選項SO_REUSEADDR,本地綁定都視為綁定失敗。

    • 需要註意的是:任意IP(全0)是萬能的。
  • 綁定成功後,把當前PCB遷移到tcp_bound_pcbs鏈表。

/**
 * @ingroup tcp_raw
 * Binds the connection to a local port number and IP address. 
 * If the IP address is not given (i.e., ipaddr == IP_ANY_TYPE), the connection is bound to all local IP addresses.
 * If another connection is bound to the same port, the function will return ERR_USE, otherwise ERR_OK is returned.
 * @see MEMP_NUM_TCP_PCB_LISTEN and MEMP_NUM_TCP_PCB
 *
 * @param pcb the tcp_pcb to bind (no check is done whether this pcb is already bound!)
 * @param ipaddr the local ip address to bind to (use IPx_ADDR_ANY to bind to any local address
 * @param port the local port to bind to
 * @return ERR_USE if the port is already in use
 *         ERR_VAL if bind failed because the PCB is not in a valid state
 *         ERR_OK if bound
 */
err_t
tcp_bind(struct tcp_pcb *pcb, const ip_addr_t *ipaddr, u16_t port)
{
  int i;
  int max_pcb_list = NUM_TCP_PCB_LISTS;
  struct tcp_pcb *cpcb;
#if LWIP_IPV6 && LWIP_IPV6_SCOPES
  ip_addr_t zoned_ipaddr;
#endif /* LWIP_IPV6 && LWIP_IPV6_SCOPES */

  LWIP_ASSERT_CORE_LOCKED();

#if LWIP_IPV4
  /* Don't propagate NULL pointer (IPv4 ANY) to subsequent functions */
  if (ipaddr == NULL) {
    ipaddr = IP4_ADDR_ANY;
  }
#else /* LWIP_IPV4 */
  LWIP_ERROR("tcp_bind: invalid ipaddr", ipaddr != NULL, return ERR_ARG);
#endif /* LWIP_IPV4 */

  LWIP_ERROR("tcp_bind: invalid pcb", pcb != NULL, return ERR_ARG);

  LWIP_ERROR("tcp_bind: can only bind in state CLOSED", pcb->state == CLOSED, return ERR_VAL);

#if SO_REUSE /* 選項:SO_REUSEADDR */
  /* 如果設置了SO_REUSEADDR選項,且綁定的IP和PORT已經被使用且處於TIME_WAIT狀態,也可以被重覆使用。
      如果沒有設置,則不能釋放處於TIME_WAIT狀態的PCB。 */
  if (ip_get_option(pcb, SOF_REUSEADDR)) {
    /* 不用遍歷處於TIME_WAIT狀態的TCP PCB是否被覆用,因為SO_REUSEADDR選項運行其復用行為 */
    max_pcb_list = NUM_TCP_PCB_LISTS_NO_TIME_WAIT;
  }
#endif /* SO_REUSE */

#if LWIP_IPV6 && LWIP_IPV6_SCOPES
  /* If the given IP address should have a zone but doesn't, assign one now.
   * This is legacy support: scope-aware callers should always provide properly
   * zoned source addresses. Do the zone selection before the address-in-use
   * check below; as such we have to make a temporary copy of the address. */
  if (IP_IS_V6(ipaddr) && ip6_addr_lacks_zone(ip_2_ip6(ipaddr), IP6_UNICAST)) {
    ip_addr_copy(zoned_ipaddr, *ipaddr);
    ip6_addr_select_zone(ip_2_ip6(&zoned_ipaddr), ip_2_ip6(&zoned_ipaddr));
    ipaddr = &zoned_ipaddr;
  }
#endif /* LWIP_IPV6 && LWIP_IPV6_SCOPES */

  if (port == 0) {
    /* 自動生成埠號 */
    port = tcp_new_port();
    if (port == 0) {
      /* 埠號申請失敗,綁定失敗 */
      return ERR_BUF;
    }
  } else {
    /* 指定埠號。遍歷TCP PCB鏈表,IP和PORT是否被占用。 */
    /* Check if the address already is in use (on all lists) */
    for (i = 0; i < max_pcb_list; i++) {
      for (cpcb = *tcp_pcb_lists[i]; cpcb != NULL; cpcb = cpcb->next) {
        if (cpcb->local_port == port) {
#if SO_REUSE
          /* 如果兩個TCP PCB都設置了SO_REUSEADDR選項,則可以復用同一個IP和埠號 */
          if (!ip_get_option(pcb, SOF_REUSEADDR) ||
              !ip_get_option(cpcb, SOF_REUSEADDR))
#endif /* SO_REUSE */
          {
            /* @todo: check accept_any_ip_version */
            /* 註意:任意IP即是萬能IP */
            if ((IP_IS_V6(ipaddr) == IP_IS_V6_VAL(cpcb->local_ip)) &&
                (ip_addr_isany(&cpcb->local_ip) ||
                 ip_addr_isany(ipaddr) ||
                 ip_addr_eq(&cpcb->local_ip, ipaddr))) {
              /* 如果IP和PORT已經被占用了,則返回ERR_USE */
              return ERR_USE;
            }
          }
        }
      }
    }
  }

  if (!ip_addr_isany(ipaddr) /* 綁定的IP不是任意IP */
#if LWIP_IPV4 && LWIP_IPV6
      /* 綁定的IP類型和原有IP類型不一致,也要更新 */
      || (IP_GET_TYPE(ipaddr) != IP_GET_TYPE(&pcb->local_ip))
#endif /* LWIP_IPV4 && LWIP_IPV6 */
     ) {
    /* 綁定IP,更新TCP PCB本地IP欄位 */
    ip_addr_set(&pcb->local_ip, ipaddr);
  }
  /* 本地PORT */
  pcb->local_port = port;
  TCP_REG(&tcp_bound_pcbs, pcb);
  LWIP_DEBUGF(TCP_DEBUG, ("tcp_bind: bind to port %"U16_F"\n", port));
  return ERR_OK;
}

監聽:tcp_listen()

用於服務端。

tcp_listen()​調用tcp_listen_with_backlog()​調用tcp_listen_with_backlog_and_err()​。

#define          tcp_listen(pcb) tcp_listen_with_backlog(pcb, TCP_DEFAULT_LISTEN_BACKLOG)
struct tcp_pcb *
tcp_listen_with_backlog(struct tcp_pcb *pcb, u8_t backlog)
{
  LWIP_ASSERT_CORE_LOCKED();
  return tcp_listen_with_backlog_and_err(pcb, backlog, NULL);
}

struct tcp_pcb *tcp_listen_with_backlog_and_err(struct tcp_pcb *pcb, u8_t backlog, err_t *err)​:

  • struct tcp_pcb *pcb​​:PCB。

  • u8_t backlog​:等待accept()​連接的上限值。

  • err_t *err​:當返回NULL時,該回傳參數包含錯誤原因。

  • 當前函數就是設置PCB進入LISTEN狀態。如果已經是LISTEN狀態,則不需要處理。

  • SO_REUSE​:如果設置了SOF_REUSEADDR​則需要檢查是否有IP&PORT服務已經處於LISTEN狀態,如果有,則本次進入LISTEN失敗(因為不支持同時存在兩個及以上的正常服務)。

  • 重置PCB的數據結構為tcp_pcb_listen​,降低記憶體浪費。並初始化新的數據結構,當然包括lpcb->state = LISTEN;​。

    • 具體看本函數源碼。
  • 把當前PCB插入tcp_listen_pcbs.pcbs​鏈表中。

/**
 * @ingroup tcp_raw
 * 把當前PCB設為LISTEN狀態(不可逆),表示可以處理連接進來的TCP客戶端。
 * TCP PCB重新分配為監聽專用的PCB,降低記憶體占用。
 *
 * @param pcb the original tcp_pcb
 * @param backlog the incoming connections queue limit
 * @param err when NULL is returned, this contains the error reason
 * @return tcp_pcb used for listening, consumes less memory.
 *
 * @note The original tcp_pcb is freed. This function therefore has to be
 *       called like this:
 *             tpcb = tcp_listen_with_backlog_and_err(tpcb, backlog, &err);
 */
struct tcp_pcb *
tcp_listen_with_backlog_and_err(struct tcp_pcb *pcb, u8_t backlog, err_t *err)
{
  struct tcp_pcb_listen *lpcb = NULL;
  err_t res;

  LWIP_UNUSED_ARG(backlog);

  LWIP_ASSERT_CORE_LOCKED();

  LWIP_ERROR("tcp_listen_with_backlog_and_err: invalid pcb", pcb != NULL, res = ERR_ARG; goto done);
  LWIP_ERROR("tcp_listen_with_backlog_and_err: pcb already connected", pcb->state == CLOSED, res = ERR_CLSD; goto done);

  if (pcb->state == LISTEN) {
    /* 已經是監聽狀態了,不需要重覆處理 */
    lpcb = (struct tcp_pcb_listen *)pcb;
    res = ERR_ALREADY;
    goto done;
  }
#if SO_REUSE
  if (ip_get_option(pcb, SOF_REUSEADDR)) {
    /* Since SOF_REUSEADDR allows reusing a local address before the pcb's usage
       is declared (listen-/connection-pcb), we have to make sure now that
       this port is only used once for every local IP. */
    /* 不能有相同IP和PORT的TCP伺服器 */
    for (lpcb = tcp_listen_pcbs.listen_pcbs; lpcb != NULL; lpcb = lpcb->next) {
      if ((lpcb->local_port == pcb->local_port) &&
          ip_addr_eq(&lpcb->local_ip, &pcb->local_ip)) {
        /* this address/port is already used */
        lpcb = NULL;
        res = ERR_USE;
        goto done;
      }
    }
  }
#endif /* SO_REUSE */
  /* 由於當前伺服器原有的TCP PCB為tcp_pcb,對於TCP伺服器的監聽TCP來說,裡面的很多欄位都沒用到,
      所以LWIP使用tcp_pcb_listen作為監聽TCP的PCB,這樣占用記憶體更小。 */
  /* 申請TCP LISTEN PCB資源 */
  lpcb = (struct tcp_pcb_listen *)memp_malloc(MEMP_TCP_PCB_LISTEN);
  if (lpcb == NULL) {
    res = ERR_MEM;
    goto done;
  }
  /* 申請成功,填寫相關欄位 */
  lpcb->callback_arg = pcb->callback_arg;
  lpcb->local_port = pcb->local_port;
  lpcb->state = LISTEN; /* 標記為監聽狀態 */
  lpcb->prio = pcb->prio;
  lpcb->so_options = pcb->so_options;
  lpcb->netif_idx = pcb->netif_idx;
  lpcb->ttl = pcb->ttl;
  lpcb->tos = pcb->tos;
#if LWIP_VLAN_PCP
  lpcb->netif_hints.tci = pcb->netif_hints.tci;
#endif /* LWIP_VLAN_PCP */
#if LWIP_IPV4 && LWIP_IPV6
  IP_SET_TYPE_VAL(lpcb->remote_ip, pcb->local_ip.type);
#endif /* LWIP_IPV4 && LWIP_IPV6 */
  ip_addr_copy(lpcb->local_ip, pcb->local_ip);
  if (pcb->local_port != 0) {
    /* 先把原生監聽TCP PCB從tcp_bound_pcbs鏈表中移除 */
    TCP_RMV(&tcp_bound_pcbs, pcb);
  }
#if LWIP_TCP_PCB_NUM_EXT_ARGS
  /* copy over ext_args to listening pcb  */
  memcpy(&lpcb->ext_args, &pcb->ext_args, sizeof(pcb->ext_args));
#endif
  /* 釋放原生監聽TCP PCB */
  tcp_free(pcb);
#if LWIP_CALLBACK_API
  /* 配置預設accept() */
  lpcb->accept = tcp_accept_null;
#endif /* LWIP_CALLBACK_API */
#if TCP_LISTEN_BACKLOG
  /* 目前沒有阻塞需要接入當前伺服器的客戶端連接 */
  lpcb->accepts_pending = 0;
  tcp_backlog_set(lpcb, backlog);
#endif /* TCP_LISTEN_BACKLOG */
  /* 修改點:https://github.com/yarrick/lwip/commit/6fb248c9e0a540112d0b4616b89f0130e4d57270 */
  /*        http://savannah.nongnu.org/task/?func=detailitem&item_id=10088#options */
  /* 把新的簡版監聽TCP PCB插回對應狀態鏈表中 */
  TCP_REG(&tcp_listen_pcbs.pcbs, (struct tcp_pcb *)lpcb);
  res = ERR_OK;
done:
  if (err != NULL) {
    *err = res;
  }
  return (struct tcp_pcb *)lpcb;
}

接受連接:tcp_accept()

用於服務端。

註冊一個accept()​回調函數到TCP內核中,當TCP內核監聽到TCP客戶端並握手成功後會調用該回調函數通知應用層。

void tcp_accept(struct tcp_pcb *pcb, tcp_accept_fn accept)​:

  • struct tcp_pcb *pcb​:PCB。
  • tcp_accept_fn accept​:需要註冊的回調函數。
/**
 * @ingroup tcp_raw
 * 用於指定當偵聽連接已連接到另一個主機時應調用的函數。
 * @see MEMP_NUM_TCP_PCB_LISTEN and MEMP_NUM_TCP_PCB
 *
 * @param pcb tcp_pcb to set the accept callback
 * @param accept callback function to call for this pcb when LISTENing
 *        connection has been connected to another host
 *
 * 註冊accept()函數,TCP伺服器接受一條客戶端連接時被調用。
 */
void
tcp_accept(struct tcp_pcb *pcb, tcp_accept_fn accept)
{
  LWIP_ASSERT_CORE_LOCKED();
  if ((pcb != NULL) && (pcb->state == LISTEN)) {
    struct tcp_pcb_listen *lpcb = (struct tcp_pcb_listen *)pcb;
    lpcb->accept = accept;
  }
}

連接遠端:tcp_connect()

用於客戶端。

會觸發三次握手的介面。

對於服務端來說,綁定成功後還需要對該IP&PORT進行監聽,監聽到了就進行ACCETP處理即可,表示已經連接完成。

而對於客戶端來說,綁定成功後,就可以調用當前函數連接服務端了。

err_t tcp_connect(struct tcp_pcb *pcb, const ip_addr_t *ipaddr, u16_t port, tcp_connected_fn connected)

  • struct tcp_pcb *pcb:PCB。

  • const ip_addr_t *ipaddr:需要連接的遠端IP地址。

  • u16_t port:需要連接的遠端埠號。

  • tcp_connected_fn connected:連接情況回調函數。

  • 本函數用於連接到遠端TCP主機。

  • 當前函數是非阻塞的,如果不能連接(如記憶體不足,參數錯誤等等),會立即返回。如果SYN報文能正常入隊,則會立即返回ERR_OK

    • 當連接成功後,註冊進去的connected()回調函數會被調用。
    • 當連接失敗會調用之前註冊的err()回調函數返回結果。(如對端主機拒絕連接、沒收到對端響應等握手失敗的可能)
  • 如果當前PCB的埠號為0,在當前連接函數中,也會隨機分配一個空閑埠號。

  • SO_REUSE:如果設置了SOF_REUSEADDR選項值,則需要判斷五元組唯一才能連接:本地IP、本地PORT、遠端IP、遠端PORT和TCP PCB狀態。

    • 說明:復用IP和埠號,是不能復用連接的,所以復用的IP和埠號中,只能由一個能建立正常連接。
  • 初始化報文相關欄位,如ISS(起始SEQ)、接收視窗、發送視窗、擁塞視窗、註冊connected()回調。

  • SYN報文插入發送隊列。

  • 調用tcp_output()觸發處理髮送隊列的報文段。

/**
 * @ingroup tcp_raw
 * Connects to another host. 
 * The function given as the "connected" argument will be called when the connection has been established.
 * Sets up the pcb to connect to the remote host and sends the initial SYN segment which opens the connection.
 *
 * The tcp_connect() function returns immediately; it does not wait for the connection to be properly setup. 
 * Instead, it will call the function specified as the fourth argument (the "connected" argument) when the connection is established.
 * If the connection could not be properly established, either because the other host refused the connection or because the other host didn't answer, the "err" callback function of this pcb (registered with tcp_err, see below) will be called.
 *
 * The tcp_connect() function can return ERR_MEM if no memory is available for enqueueing the SYN segment.
 * If the SYN indeed was enqueued successfully, the tcp_connect() function returns ERR_OK.
 *
 * @param pcb the tcp_pcb used to establish the connection
 * @param ipaddr the remote ip address to connect to
 * @param port the remote tcp port to connect to
 * @param connected callback function to call when connected (on error,
                    the err callback will be called)
 * @return ERR_VAL if invalid arguments are given
 *         ERR_OK if connect request has been sent
 *         other err_t values if connect request couldn't be sent
 */
err_t
tcp_connect(struct tcp_pcb *pcb, const ip_addr_t *ipaddr, u16_t port,
            tcp_connected_fn connected)
{
  struct netif *netif = NULL;
  err_t ret;
  u32_t iss;
  u16_t old_local_port;

  LWIP_ASSERT_CORE_LOCKED();

  LWIP_ERROR("tcp_connect: invalid pcb", pcb != NULL, return ERR_ARG);
  LWIP_ERROR("tcp_connect: invalid ipaddr", ipaddr != NULL, return ERR_ARG);

  LWIP_ERROR("tcp_connect: can only connect from state CLOSED", pcb->state == CLOSED, return ERR_ISCONN);

  LWIP_DEBUGF(TCP_DEBUG, ("tcp_connect to port %"U16_F"\n", port));
  ip_addr_set(&pcb->remote_ip, ipaddr);
  pcb->remote_port = port;

  if (pcb->netif_idx != NETIF_NO_INDEX) {
    netif = netif_get_by_index(pcb->netif_idx);
  } else {
    /* check if we have a route to the remote host */
    netif = ip_route(&pcb->local_ip, &pcb->remote_ip);
  }
  if (netif == NULL) {
    /* Don't even try to send a SYN packet if we have no route since that will fail. */
    return ERR_RTE;
  }

  /* check if local IP has been assigned to pcb, if not, get one */
  if (ip_addr_isany(&pcb->local_ip)) {
    const ip_addr_t *local_ip = ip_netif_get_local_ip(netif, ipaddr);
    if (local_ip == NULL) {
      return ERR_RTE;
    }
    ip_addr_copy(pcb->local_ip, *local_ip);
  }

#if LWIP_IPV6 && LWIP_IPV6_SCOPES
  /* If the given IP address should have a zone but doesn't, assign one now.
   * Given that we already have the target netif, this is easy and cheap. */
  if (IP_IS_V6(&pcb->remote_ip) &&
      ip6_addr_lacks_zone(ip_2_ip6(&pcb->remote_ip), IP6_UNICAST)) {
    ip6_addr_assign_zone(ip_2_ip6(&pcb->remote_ip), IP6_UNICAST, netif);
  }
#endif /* LWIP_IPV6 && LWIP_IPV6_SCOPES */

  old_local_port = pcb->local_port;
  if (pcb->local_port == 0) {
    pcb->local_port = tcp_new_port();
    if (pcb->local_port == 0) {
      return ERR_BUF;
    }
  } else {
#if SO_REUSE
    if (ip_get_option(pcb, SOF_REUSEADDR)) {
      /* 如果設置了SOF_REUSEADDR選項值,
          則需要判斷五元組唯一才能連接:本地IP、本地PORT、遠端IP、遠端PORT和TCP PCB狀態 */
      struct tcp_pcb *cpcb;
      int i;
      /* TCP PCB狀態鏈表只遍歷穩定態和TIME_WAIT狀態的,不遍歷綁定態和監聽態的,因為設置了SOF_REUSEADDR,是允許客戶端復用伺服器的。 */
      for (i = 2; i < NUM_TCP_PCB_LISTS; i++) {
        for (cpcb = *tcp_pcb_lists[i]; cpcb != NULL; cpcb = cpcb->next) {
          if ((cpcb->local_port == pcb->local_port) &&
              (cpcb->remote_port == port) &&
              ip_addr_eq(&cpcb->local_ip, &pcb->local_ip) &&
              ip_addr_eq(&cpcb->remote_ip, ipaddr)) {
            /* linux returns EISCONN here, but ERR_USE should be OK for us */
            return ERR_USE;
          }
        }
      }
    }
#endif /* SO_REUSE */
  }

  iss = tcp_next_iss(pcb); /* 獲取第一個要發送的seq號值 */
  pcb->rcv_nxt = 0;
  pcb->snd_nxt = iss;
  pcb->lastack = iss - 1;
  pcb->snd_wl2 = iss - 1;
  pcb->snd_lbb = iss - 1;
  /* 初始化接收視窗、視窗通告值、視窗通告值右邊沿值 */
  pcb->rcv_wnd = pcb->rcv_ann_wnd = TCPWND_MIN16(TCP_WND);
  pcb->rcv_ann_right_edge = pcb->rcv_nxt;
  /* 初始化發送視窗 */
  pcb->snd_wnd = TCP_WND;
  /* 初始化MSS,LWIP限制在536 */
  pcb->mss = INITIAL_MSS;
#if TCP_CALCULATE_EFF_SEND_MSS
  /* 根據netif和遠端IP來設置MSS */
  pcb->mss = tcp_eff_send_mss_netif(pcb->mss, netif, &pcb->remote_ip);
#endif /* TCP_CALCULATE_EFF_SEND_MSS */
  /* 擁塞視窗初始值 */
  pcb->cwnd = 1;
#if LWIP_CALLBACK_API
  /* 回調函數connected() */
  pcb->connected = connected;
#else /* LWIP_CALLBACK_API */
  LWIP_UNUSED_ARG(connected);
#endif /* LWIP_CALLBACK_API */

  /* 構造一個連接請求報文到TCP PCB中:SYN + MSS option */
  ret = tcp_enqueue_flags(pcb, TCP_SYN);
  if (ret == ERR_OK) {
    /* 更新為SYN_SENT狀態 */
    pcb->state = SYN_SENT;
    if (old_local_port != 0) {
      /* 舊TCP PCB埠不為0,則將TCP PCB先從tcp_bound_pcbs狀態鏈表移除 */
      TCP_RMV(&tcp_bound_pcbs, pcb);
    }
    /* 再把當前TCP PCB插入到穩定態tcp_active_pcbs鏈表 */
    TCP_REG_ACTIVE(pcb);
    MIB2_STATS_INC(mib2.tcpactiveopens);

    /* 將TCP PCB上的報文發送出去 */
    tcp_output(pcb);
  }
  return ret;
}

應用層通知TCP內核成功接收數據:tcp_recved()

tcp_recved()​函數是被應用層調用,用於通知TCP內核:應用層已經從接收到的數據size,你可以釋放這部分數據的記憶體了。

void tcp_recved(struct tcp_pcb *pcb, u16_t len)​:

  • struct tcp_pcb *pcb​:pcb。

  • u16_t len​:成功接收的長度。

  • 視窗滑動:

    • 當前接收視窗恢復。
    • 在糊塗視窗演算法下,通告接收視窗。
/**
 * @ingroup tcp_raw
 * @param pcb the tcp_pcb for which data is read
 * @param len the amount of bytes that have been read by the application
 *
 * 應用程式從PCB緩衝區中提取走數據後,應該調用當前函數來更新當前PCB的接收視窗。
 *
 */
void
tcp_recved(struct tcp_pcb *pcb, u16_t len)
{
  u32_t wnd_inflation;
  tcpwnd_size_t rcv_wnd;

  LWIP_ASSERT_CORE_LOCKED();

  LWIP_ERROR("tcp_recved: invalid pcb", pcb != NULL, return);

  /* pcb->state LISTEN not allowed here */
  LWIP_ASSERT("don't call tcp_recved for listen-pcbs",
              pcb->state != LISTEN);

  /* 接收視窗擴大len */
  rcv_wnd = (tcpwnd_size_t)(pcb->rcv_wnd + len);
  /* 更新接收視窗值 */
  if ((rcv_wnd > TCP_WND_MAX(pcb)) || (rcv_wnd < pcb->rcv_wnd)) {
    /* window got too big or tcpwnd_size_t overflow */
    LWIP_DEBUGF(TCP_DEBUG, ("tcp_recved: window got too big or tcpwnd_size_t overflow\n"));
    pcb->rcv_wnd = TCP_WND_MAX(pcb);
  } else  {
    pcb->rcv_wnd = rcv_wnd;
  }

  /* 更新滑動視窗。支持糊塗視窗避免演算法。 */
  wnd_inflation = tcp_update_rcv_ann_wnd(pcb);

  /* 如果接收視窗右邊界滑動了 (1/4接收緩衝) || (4個MSS) 都可以立即發送視窗通告值到對端; */
  /* 如果接收視窗右邊界滑動達不到閾值,就等正常發送數據時才附帶視窗通告值。 */
  if (wnd_inflation >= TCP_WND_UPDATE_THRESHOLD) {
    tcp_ack_now(pcb);
    tcp_output(pcb);
  }

  LWIP_DEBUGF(TCP_DEBUG, ("tcp_recved: received %"U16_F" bytes, wnd %"TCPWNDSIZE_F" (%"TCPWNDSIZE_F").\n",
                          len, pcb->rcv_wnd, (u16_t)(TCP_WND_MAX(pcb) - pcb->rcv_wnd)));
}

關閉連接:tcp_close()

LISTEN狀態、未連接的PCB直接被釋放,不能再被引用。

如果PCB建立了連接(包括收到了SYN或處於closing狀態),就關閉連接,並按照狀態機轉換進入對應的狀態。其PCB會在tcp_slowtmr()​慢時鐘中被釋放。

註意,當前函數也是一個協議不安全函數,存在必要時會發送RST來關閉連接導致數據丟失:(ESTABLISHED || CLOSE_WAIT) && (應用層還沒讀取完接收緩衝區的數據)​。

返回:

  • ERR_OK​:關閉成功。

  • another err_t​:關閉失敗或PCB沒有被釋放。

    • ERR_MEM​,在關閉連接時,可能需要發送FIN​報文,這就需要申請報文段資源,如果申請失敗,就表示FIN​發送不了,返回ERR_MEM​通知回來。

所以,既然tcp_close()​這個介面會因為記憶體不足而導致關閉失敗,返回ERR_MEM​,那麼我們就需要檢查返回值操作,如遇到內部記憶體不足導致關閉失敗就需要繼續調用tcp_close()​,而不是忽略返回值導致更多的記憶體泄漏。

err_t
tcp_close(struct tcp_pcb *pcb)
{
  LWIP_ASSERT_CORE_LOCKED();

  LWIP_ERROR("tcp_close: invalid pcb", pcb != NULL, return ERR_ARG);
  LWIP_DEBUGF(TCP_DEBUG, ("tcp_close: closing in "));

  tcp_debug_print_state(pcb->state);

  if (pcb->state != LISTEN) {
    /* Set a flag not to receive any more data... */
    tcp_set_flags(pcb, TF_RXCLOSED);
  }
  /* ... and close */
  return tcp_close_shutdown(pcb, 1);
}

報文段相關函數

學完RAW介面函數後,需要學習TCP內部實現了。

TCP數據流是以報文段形式來承接的,所以本章前段也描述了報文段的相關數據結構,現在繼續描述報文段的相關函數。

新建一個報文段:tcp_create_segment()

其主要內容是創建一個新的報文段segment並初始化。

/**
 * Create a TCP segment with prefilled header.
 *
 * Called by @ref tcp_write, @ref tcp_enqueue_flags and @ref tcp_split_unsent_seg
 *
 * @param pcb Protocol control block for the TCP connection.
 * @param p pbuf that is used to hold the TCP header.
 * @param hdrflags TCP flags for header.
 * @param seqno TCP sequence number of this packet
 * @param optflags options to include in TCP header
 * @return a new tcp_seg pointing to p, or NULL.
 * The TCP header is filled in except ackno and wnd.
 * p is freed on failure.
 */
static struct tcp_seg *
tcp_create_segment(const struct tcp_pcb *pcb, struct pbuf *p, u8_t hdrflags, u32_t seqno, u8_t optflags);

TCP發送數據

TCP數據收發都是一個複雜的協議實現。

在RAW介面層來看:

  1. 先調用tcp_sent()​​註冊發送回調函數。
  2. 調用tcp_write()​​把需要發送的數據組裝成報文段,插入TCP發送緩衝區中。
  3. 主動調用或內部定時調用tcp_output()​​函數來檢查發送緩衝區中的報文段併發送到IP層處理。

調用關係:tcp_output()​-->tcp_output_segment()​-->ip_output_if()​給到IP層。

組裝報文段到PCB:tcp_write()

tcp_write()​函數用於發送數據,但不是立即發送,而是把數據組裝成報文段插入TCP發送緩衝區:pcb->unsent​隊列中。

相關參數:

  • struct tcp_pcb *pcb​:pcb。

  • void *arg​:需要發送的數據的指針。

  • u16_t len​:需要發送的數據的長度。

  • u8_t apiflags​:

    • TCP_WRITE_FLAG_COPY​:數據會被覆制到新的記憶體中。
    • TCP_WRITE_FLAG_MORE​:表示後續還有數據,這個標誌位會導致TCP首部PSH​標誌位不會被標記。

如果需要發送的數據長度超過發送緩衝區空閑size 或 發送隊列的segment個數超過上限 都會返回ERR_MEM​表示因記憶體問題而發送失敗。

其函數目標並很簡單:只是把數據合法封裝成報文段格式,插入到發送緩衝隊列中。

其函數的實現複雜度不在於協議的實現,而是兼顧記憶體的做法,所以對LWIP具體實現沒有深入研究的興趣的話,不需要分析當前函數具體實現。

確定MSS

先為本次發送組包選擇一個MSS值:

/* 選擇本次發送的MSS,MIN(pcb->mss, 發送視窗/2) */
mss_local = LWIP_MIN(pcb->mss, TCPWND_MIN16(pcb->snd_wnd_max / 2));
mss_local = mss_local ? mss_local : pcb->mss;

檢查本次是否寫

  • 會檢查TCP狀態、發送緩衝區size和發送緩衝區中報文段個數。
  /* 檢查本次是否可寫 */
  err = tcp_write_checks(pcb, len);
  if (err != ERR_OK) {
    return err;
  }

選項欄位

確認選項欄位長度:目前只支持TS:時間戳選項欄位。

#if LWIP_TCP_TIMESTAMPS /* 時間戳選項欄位 */
  if ((pcb->flags & TF_TIMESTAMP)) {
    /* Make sure the timestamp option is only included in data segments if we
       agreed about it with the remote host. */
    optflags = TF_SEG_OPTS_TS;
    optlen = LWIP_TCP_OPT_LENGTH_SEGMENT(TF_SEG_OPTS_TS, pcb);
    /* ensure that segments can hold at least one data byte... */
    mss_local = LWIP_MAX(mss_local, LWIP_TCP_OPT_LEN_TS + 1);
  } else
#endif /* LWIP_TCP_TIMESTAMPS */
  {
    optlen = LWIP_TCP_OPT_LENGTH_SEGMENT(0, pcb); /* 統計選項欄位長度 */
  }

組件報文段

TCP報文段組建分為三個步驟,且難度逐漸增加,如果某個步驟就能完成數據寫入,後面步驟就不需要繼續了:

  1. 把數據先copy到最後一個segment​的oversize​中。
  2. 新建一個pbuf,接入最後一個未發送segment​中。
  3. 創建一個新的segments​。

註意:在處理的途中隨時可能會遇到記憶體耗盡問題。所以,我們應該return ERR_MEM​和不要改動PCB任何值。我們在處理時,先使用局部變數,在當前函數處理完畢時,再把改變PCB的所有值一併推送到PCB。一些pcb欄位在本地副本中維護:

  • queuelen = pcb->snd_queuelen​;
  • oversize = pcb->unsent_oversize​。
  /* 先找到pcb->unsent隊列尾 */
  if (pcb->unsent != NULL) {
    u16_t space;  /* last_unsent這個segment剩餘可以插入的空間 */
    u16_t unsent_optlen;

    /* @todo: 可以在PCB中追加last_unsent變數來快速找到unsent隊列尾部 */
    for (last_unsent = pcb->unsent; last_unsent->next != NULL;
         last_unsent = last_unsent->next);

    /* 報文段MSS限制:最後一個unsent報文段的可用空間 */
    unsent_optlen = LWIP_TCP_OPT_LENGTH_SEGMENT(last_unsent->flags, pcb); /* unsent報文段選項欄位長度 */
    LWIP_ASSERT("mss_local is too small", mss_local >= last_unsent->len + unsent_optlen);
    space = mss_local - (last_unsent->len + unsent_optlen); /* segment剩餘空間 */
    /*
     * Phase 1: 先填滿最後一個segment的oversize。
     *
     * 複製的位元組數記錄在oversize_used變數中。
     * 實際的複製是在函數的底部完成的。
     */
#if TCP_OVERSIZE  /* 支持oversize */
#if TCP_OVERSIZE_DBGCHECK
    /* check that pcb->unsent_oversize matches last_unsent->oversize_left */
    LWIP_ASSERT("unsent_oversize mismatch (pcb vs. last_unsent)",
                pcb->unsent_oversize == last_unsent->oversize_left);
#endif /* TCP_OVERSIZE_DBGCHECK */
    oversize = pcb->unsent_oversize;  /* oversize */
    if (oversize > 0) {
      LWIP_ASSERT("inconsistent oversize vs. space", oversize <= space);
      seg = last_unsent;
      oversize_used = LWIP_MIN(space, LWIP_MIN(oversize, len)); /* 選出需要copy的size */
      pos += oversize_used; /* 游標更新 */
      oversize -= oversize_used;  /* 更新oversize */
      space -= oversize_used; /* 更新segment可用空間 */
    }
    /* now we are either finished or oversize is zero */
    LWIP_ASSERT("inconsistent oversize vs. len", (oversize == 0) || (pos == len));
#endif /* TCP_OVERSIZE */

#if !LWIP_NETIF_TX_SINGLE_PBUF
    /*
     * Phase 2: 新建一個pbuf,接入last_unsent這個segment中。
     *
     * 如果數據不支持copy,即是pbuf為PBUF_ROM類型,last_unsent最後一個pbuf的數據區也是不支持copy的,
     * 而且本次需要寫入發送緩衝區的數據地址*arg是僅跟著last_unsent最後一個pbuf數據區的最後一個數據地址
     * (即是地址連續)(p->payload + p->len == (const u8_t *)arg),那就直接擴展last_unsent最後一個pbuf的有效size即可,
     * 這樣就能省一個pbuf ROM。
     *
     */
    if ((pos < len) && (space > 0) && (last_unsent->len > 0)) {
      u16_t seglen = LWIP_MIN(space, len - pos);
      seg = last_unsent;

      /* 創建或引用一個pbuf。 */
      if (apiflags & TCP_WRITE_FLAG_COPY) {
        /* 數據支持copy */
        if ((concat_p = tcp_pbuf_prealloc(PBUF_RAW, seglen, space, &oversize, pcb, apiflags, 1)) == NULL) {
          LWIP_DEBUGF(TCP_OUTPUT_DEBUG | LWIP_DBG_LEVEL_SERIOUS,
                      ("tcp_write : could not allocate memory for pbuf copy size %"U16_F"\n",
                       seglen));
          goto memerr;
        }
#if TCP_OVERSIZE_DBGCHECK
        oversize_add = oversize;
#endif /* TCP_OVERSIZE_DBGCHECK */
        TCP_DATA_COPY2(concat_p->payload, (const u8_t *)arg + pos, seglen, &concat_chksum, &concat_chksum_swapped);
#if TCP_CHECKSUM_ON_COPY
        concat_chksummed += seglen;
#endif /* TCP_CHECKSUM_ON_COPY */
        queuelen += pbuf_clen(concat_p);
      } else {
        /* 數據不支持copy */
        /* 如果last unsent pbuf是PBUF_ROM類型,就try to extend它 */
        struct pbuf *p;
        for (p = last_unsent->p; p->next != NULL; p = p->next); /* 找到last_unsent的最後一個pbuf */
        if (((p->type_internal & (PBUF_TYPE_FLAG_STRUCT_DATA_CONTIGUOUS | PBUF_TYPE_FLAG_DATA_VOLATILE)) == 0) && /* 數據不可改(PBUF_ROM) */
            (const u8_t *)p->payload + p->len == (const u8_t *)arg) { /* 地址連續,可擴展 */
          LWIP_ASSERT("tcp_write: ROM pbufs cannot be oversized", pos == 0);
          extendlen = seglen; /* 擴展這個ROM pbuf */
        } else {  /* 不是PBUF_ROM或地址不連續,就需要新建一個pbuf */
          if ((concat_p = pbuf_alloc(PBUF_RAW, seglen, PBUF_ROM)) == NULL) {  /* 新建一個PBUF_ROM */
            LWIP_DEBUGF(TCP_OUTPUT_DEBUG | LWIP_DBG_LEVEL_SERIOUS,
                        ("tcp_write: could not allocate memory for zero-copy pbuf\n"));
            goto memerr;
          }
          /* reference the non-volatile payload data */
          ((struct pbuf_rom *)concat_p)->payload = (const u8_t *)arg + pos; /* 綁定payload */
          queuelen += pbuf_clen(concat_p);  /* 累計pbuf個數 */
        }
#if TCP_CHECKSUM_ON_COPY
        /* calculate the checksum of nocopy-data */
        tcp_seg_add_chksum(~inet_chksum((const u8_t *)arg + pos, seglen), seglen,
                           &concat_chksum, &concat_chksum_swapped);
        concat_chksummed += seglen;
#endif /* TCP_CHECKSUM_ON_COPY */
      }

      pos += seglen;  /* 更新游標 */
    }
#endif /* !LWIP_NETIF_TX_SINGLE_PBUF */
  } else {
#if TCP_OVERSIZE
    LWIP_ASSERT("unsent_oversize mismatch (pcb->unsent is NULL)",
                pcb->unsent_oversize == 0);
#endif /* TCP_OVERSIZE */
  }

  /*
   * Phase 3: 創建一個新的segment。
   * 如果last_unsent這個segment未能全部裝完本次需要發送的數據,就只能新建segment了。
   *
   * The new segments are chained together in the local 'queue' variable,
   * ready to be appended to pcb->unsent.
   */
  while (pos < len) { /* 還有數據未寫入TCP發送緩衝區,新建segment來發送這些數據 */
    struct pbuf *p;
    u16_t left = len - pos;
    u16_t max_len = mss_local - optlen;
    u16_t seglen = LWIP_MIN(left, max_len);
#if TCP_CHECKSUM_ON_COPY
    u16_t chksum = 0;
    u8_t chksum_swapped = 0;
#endif /* TCP_CHECKSUM_ON_COPY */

    if (apiflags & TCP_WRITE_FLAG_COPY) { /* 數據支持copy */
      /* 把剩餘數據copy到新的pbuf中 */
      if ((p = tcp_pbuf_prealloc(PBUF_TRANSPORT, seglen + optlen, mss_local, &oversize, pcb, apiflags, queue == NULL)) == NULL) {
        LWIP_DEBUGF(TCP_OUTPUT_DEBUG | LWIP_DBG_LEVEL_SERIOUS, ("tcp_write : could not allocate memory for pbuf copy size %"U16_F"\n", seglen));
        goto memerr;
      }
      LWIP_ASSERT("tcp_write: check that first pbuf can hold the complete seglen",
                  (p->len >= seglen));
      TCP_DATA_COPY2((char *)p->payload + optlen, (const u8_t *)arg + pos, seglen, &chksum, &chksum_swapped);
    } else {  /* 數據不支持copy */
      /* 申請PBUF_ROM類型的pbuf來holding數據。(這些pbuf的payload是有應用層維護的,內部無權釋放,所以只能申請PBUF_ROM類型) */
      struct pbuf *p2;
#if TCP_OVERSIZE
      LWIP_ASSERT("oversize == 0", oversize == 0);
#endif /* TCP_OVERSIZE */
      if ((p2 = pbuf_alloc(PBUF_TRANSPORT, seglen, PBUF_ROM)) == NULL) {
        LWIP_DEBUGF(TCP_OUTPUT_DEBUG | LWIP_DBG_LEVEL_SERIOUS, ("tcp_write: could not allocate memory for zero-copy pbuf\n"));
        goto memerr;
      }
#if TCP_CHECKSUM_ON_COPY
      /* calculate the checksum of nocopy-data */
      chksum = ~inet_chksum((const u8_t *)arg + pos, seglen);
      if (seglen & 1) {
        chksum_swapped = 1;
        chksum = SWAP_BYTES_IN_WORD(chksum);
      }
#endif /* TCP_CHECKSUM_ON_COPY */
      /* reference the non-volatile payload data */
      ((struct pbuf_rom *)p2)->payload = (const u8_t *)arg + pos; /* pbuf綁定數據 */

      /* 然後再申請TCP首部PBUF */
      if ((p = pbuf_alloc(PBUF_TRANSPORT, optlen, PBUF_RAM)) == NULL) {
        /* If allocation fails, we have to deallocate the data pbuf as well. */
        pbuf_free(p2);
        LWIP_DEBUGF(TCP_OUTPUT_DEBUG | LWIP_DBG_LEVEL_SERIOUS, ("tcp_write: could not allocate memory for header pbuf\n"));
        goto memerr;
      }
      /* 拼接TCP首部+TCP數據區 */
      pbuf_cat(p/*header*/, p2/*data*/);
    }

    queuelen += pbuf_clen(p); /* 累計pbuf數量 */

    /* 如果pbuf數量溢出,就暫停本次發送,返回記憶體不足 */
    if (queuelen > LWIP_MIN(TCP_SND_QUEUELEN, TCP_SNDQUEUELEN_OVERFLOW)) {
      LWIP_DEBUGF(TCP_OUTPUT_DEBUG | LWIP_DBG_LEVEL_SERIOUS, ("tcp_write: queue too long %"U16_F" (%d)\n",
                  queuelen, (int)TCP_SND_QUEUELEN));
      pbuf_free(p);
      goto memerr;
    }

    /* 新建segment */
    if ((seg = tcp_create_segment(pcb, p, 0, pcb->snd_lbb + pos, optflags)) == NULL) {
      goto memerr;
    }
#if TCP_OVERSIZE_DBGCHECK
    seg->oversize_left = oversize;  /* 更新oversize */
#endif /* TCP_OVERSIZE_DBGCHECK */
#if TCP_CHECKSUM_ON_COPY
    seg->chksum = chksum;
    seg->chksum_swapped = chksum_swapped;
    seg->flags |= TF_SEG_DATA_CHECKSUMMED;
#endif /* TCP_CHECKSUM_ON_COPY */

    /* 維護局部segment鏈 */
    if (queue == NULL) {
      queue = seg;
    } else {
      /* Attach the segment to the end of the queued segments */
      LWIP_ASSERT("prev_seg != NULL", prev_seg != NULL);
      prev_seg->next = seg;
    }
    /* remember last segment of to-be-queued data for next iteration */
    prev_seg = seg;

    LWIP_DEBUGF(TCP_OUTPUT_DEBUG | LWIP_DBG_TRACE, ("tcp_write: queueing %"U32_F":%"U32_F"\n",
                lwip_ntohl(seg->tcphdr->seqno),
                lwip_ntohl(seg->tcphdr->seqno) + TCP_TCPLEN(seg)));

    pos += seglen;  /* 游標更新 */
  }

報文段插入發送緩衝區隊列

到此,TCP確認了能寫入,報文段也組裝好了,可以把相關數據推送到PCB:

  1. 如果數據已經添加到last_unsent的剩餘空間,我們更新pbuf鏈的長度欄位,讓這些數據生效。
  2. 把需要插入發送緩衝區中最後一個報文段的數據進行插入操作。
  3. 接入新的segment,如果前面兩個步驟能裝滿本次發送的數據,就不需要當前segment,即是queue為空,接入也是無效的。

  /*
   * Phase 1: 如果數據已經添加到last_unsent的剩餘空間,我們更新pbuf鏈的長度欄位,讓這些數據生效。
   */
#if TCP_OVERSIZE
  if (oversize_used > 0) {
    struct pbuf *p;
    /* Bump tot_len of whole chain, len of tail */
    /* 更新last_unsent這個segment的pbuf欄位,讓追加的數據生效 */
    for (p = last_unsent->p; p; p = p->next) {
      p->tot_len += oversize_used;
      if (p->next == NULL) {
        TCP_DATA_COPY((char *)p->payload + p->len, arg, oversize_used, last_unsent);
        p->len += oversize_use

您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • # HTML實用合集1 ## 1.框架 使用英文!,按tab鍵出現框架。 ![uTools_1685239763398](https://img2023.cnblogs.com/blog/3178390/202305/3178390-20230528120408799-1395303523.png) ...
  • WMS產品實時數據倉庫介紹 目標以ERP、跨境電商ERP 、 WMS(倉儲管理系統)為核心,搭建融合商業智能BI、新零售、訂貨系統等為一體的產品矩陣,幫助電商企業快速佈局 跨境 和 新零售,提升訂單處理效率,實現數字化業務管理,為企業降本增效。痛點業務+數據雙中台 Hologres是阿裡巴巴自主研發 ...
  • 設計一個支持百萬用戶的系統是具有挑戰性的,這是一段需要不斷改進和不斷提升的旅程。在本章中,我們將構建一個支持單個用戶的系統,並逐漸擴展以服務於數百萬用戶。閱讀本章後,您將掌握一些技巧,幫助您解決系統設計面試問題。 > AI不會取代你,使用AI的人會。歡迎關註我的公眾號:更AI。以程式員的視角來看AI ...
  • ### 命令模式(Command Pattern) #### 一、定義 命令模式(Command Pattern)是一種數據驅動的設計模式,它屬於行為型模式。請求以命令的形式包裹在對象中,並傳給調用對象。調用對象尋找可以處理該命令的合適的對象,並把該命令傳給相應的對象,該對象執行命令。 將一個請求封 ...
  • ## 網路分層結構 電腦網路體系大致分為三種,OSI七層模型、TCP/IP四層模型和五層模型。一般面試的時候考察比較多的是五層模型。最全面的Java面試網站:[最全面的Java面試網站](https://topjavaer.cn) ![](http://img.topjavaer.cn/img/t ...
  • Java的Object類是所有類的根類,它提供了一些通用的方法。下麵是一些常用的Object類方法: 1. equals(Object obj):判斷當前對象是否與給定對象相等。預設情況下,equals方法比較的是對象的引用,但可以通過在具體類中重寫equals方法來改變其比較行為。 2. hash ...
  • ini 配置文件格式如下 要求:ini 文件必須是GBK編碼,如果是UTF-8編碼,python讀取配置文件會報錯。 # 這裡是註釋內容 # [FY12361] #婦幼保健介面服務埠 serverIP=192.168.1.11 serverPort=8400 [SM] #國產SM加密服務埠 se ...
  • # Rust Web 全棧開發之 Actix 嘗鮮並構建REST API ## 一、Actix 嘗鮮 ### 需要使用的crate - actix-web v4.3.1 - actix-rt v2.8.0 ```bash ~ via 🅒 base ➜ cd rust ~/rust via 🅒 b ...
一周排行
    -Advertisement-
    Play Games
  • C#TMS系統代碼-基礎頁面BaseCity學習 本人純新手,剛進公司跟領導報道,我說我是java全棧,他問我會不會C#,我說大學學過,他說這個TMS系統就給你來管了。外包已經把代碼給我了,這幾天先把增刪改查的代碼背一下,說不定後面就要趕鴨子上架了 Service頁面 //using => impo ...
  • 委托與事件 委托 委托的定義 委托是C#中的一種類型,用於存儲對方法的引用。它允許將方法作為參數傳遞給其他方法,實現回調、事件處理和動態調用等功能。通俗來講,就是委托包含方法的記憶體地址,方法匹配與委托相同的簽名,因此通過使用正確的參數類型來調用方法。 委托的特性 引用方法:委托允許存儲對方法的引用, ...
  • 前言 這幾天閑來沒事看看ABP vNext的文檔和源碼,關於關於依賴註入(屬性註入)這塊兒產生了興趣。 我們都知道。Volo.ABP 依賴註入容器使用了第三方組件Autofac實現的。有三種註入方式,構造函數註入和方法註入和屬性註入。 ABP的屬性註入原則參考如下: 這時候我就開始疑惑了,因為我知道 ...
  • C#TMS系統代碼-業務頁面ShippingNotice學習 學一個業務頁面,ok,領導開完會就被裁掉了,很突然啊,他收拾東西的時候我還以為他要旅游提前請假了,還在尋思為什麼回家連自己買的幾箱飲料都要叫跑腿帶走,怕被偷嗎?還好我在他開會之前拿了兩瓶芬達 感覺感覺前面的BaseCity差不太多,這邊的 ...
  • 概述:在C#中,通過`Expression`類、`AndAlso`和`OrElse`方法可組合兩個`Expression<Func<T, bool>>`,實現多條件動態查詢。通過創建表達式樹,可輕鬆構建複雜的查詢條件。 在C#中,可以使用AndAlso和OrElse方法組合兩個Expression< ...
  • 閑來無聊在我的Biwen.QuickApi中實現一下極簡的事件匯流排,其實代碼還是蠻簡單的,對於初學者可能有些幫助 就貼出來,有什麼不足的地方也歡迎板磚交流~ 首先定義一個事件約定的空介面 public interface IEvent{} 然後定義事件訂閱者介面 public interface I ...
  • 1. 案例 成某三甲醫預約系統, 該項目在2024年初進行上線測試,在正常運行了兩天後,業務系統報錯:The connection pool has been exhausted, either raise MaxPoolSize (currently 800) or Timeout (curren ...
  • 背景 我們有些工具在 Web 版中已經有了很好的實踐,而在 WPF 中重新開發也是一種費時費力的操作,那麼直接集成則是最省事省力的方法了。 思路解釋 為什麼要使用 WPF?莫問為什麼,老 C# 開發的堅持,另外因為 Windows 上已經裝了 Webview2/edge 整體打包比 electron ...
  • EDP是一套集組織架構,許可權框架【功能許可權,操作許可權,數據訪問許可權,WebApi許可權】,自動化日誌,動態Interface,WebApi管理等基礎功能於一體的,基於.net的企業應用開發框架。通過友好的編碼方式實現數據行、列許可權的管控。 ...
  • .Net8.0 Blazor Hybird 桌面端 (WPF/Winform) 實測可以完整運行在 win7sp1/win10/win11. 如果用其他工具打包,還可以運行在mac/linux下, 傳送門BlazorHybrid 發佈為無依賴包方式 安裝 WebView2Runtime 1.57 M ...