LWIP之TCP源码详解(一)

目录

TCP原理

LWIP 源码分析

TCP状态定义

TCP状态转移

TCP首部数据结构

TCP控制块

报文段数据结构

TCP控制块链表

 lwip tcp数据结构

​编辑

TCP单帧入站相关数据

 TCP函数汇总

新建控制块:tcp_new()

绑定本地服务:tcp_bind()

监听:tcp_listen()

接受连接:tcp_accept()

连接远端:tcp_connect()

应用层通知TCP内核成功接收数据:tcp_recved()

关闭连接:tcp_close()


TCP原理

Wireshare抓包分析TCP协议-CSDN博客

LWIP 源码分析

TCP状态定义

路径:lwip-2.1.2\src\include\lwip\tcpbase.h

一个枚举体,共11种状态

也在tcp.c中定义了该字符数组:

在这里简单讲解几个状态:
1. ESTABLISHED 状态:这个状态是处于稳定连接状态,建立连接的 TCP 协议两端的主机都是处于这个状态,它们相互知道彼此的窗口大小、序列号、最大报文段等信息。


2. FIN_WAIT_1 与 FIN_WAIT_2 状态:处于这个状态一般都是单向请求终止连接,
然后主机等待对方的回应,而如果对方产生应答,则主机状态转移为FIN_WAIT_2,此时{主机->对方}方向上的 TCP 连接就断开,但是{对方->主机}方向上的连接还是存在的。此处有一个注意的地方:如果主机处于 FIN_WAIT_2状态,说明主机已经发出了 FIN 报文段,并且对方也已对它进行确认,除非主机是在实行半关闭状态,否则将等待对方主机的应用层处理关闭连接,因为对方已
经意识到它已收到 FIN 报文段,它需要主机发一个 FIN 来关闭{对方->主机}方向上的连接。只有当另一端的进程完成这个关闭,主机这端才会从 FIN_WAIT_2 状态进入 TIME_WAIT 状态。否则这意味着主机这端可能永远保持这个FIN_WAIT_2 状态,另一端的主机也将处于 CLOSE_WAIT 状态,并一直保持这个状态直到应用层决定进行关闭。


3. TIME_WAIT 状态:TIME_WAIT 状态也称为 2MSL 等待状态。每个具体 TCP 连接的实现必须选择一个 TCP 报文段最大生存时间 MSL(Maximum Segment Lifetime),如 IP 数据报中的 TTL 字段,表示报文在网络中生存的时间,它是任何报文段被丢弃前在网络内的最长时间,这个时间是有限的,为什么需要等待呢?我们知道 IP 数据报是不可靠的,而 TCP 报文段是封装在 IP 数据报中,TCP 协议必须保证发出的 ACK 报文段是正确被对方接收, 因此处于该状态的主机必须在
这个状态停留最长时间为 2 倍的 MSL,以防最后这个 ACK 丢失,因为 TCP 协议必须保证数据能准确送达目的地

TCP状态转移

  • 虚线:表示服务器的状态转移。

  • 实线:表示客户端的状态转移。

  • 图中所有“关闭”、“打开”都是应用程序主动处理。

  • 图中所有的“超时”都是内核超时处理。

进入连接,就是通过三次握手来确认客户端和服务器连接:

三次握手,就是首先由客户端发送SYN给服务器,这是第一次握手;然后服务器就会回复SYN+ACK信号给客户端,客户端进入SYN-SENT模式,而服务器切换到LISTEN模式,这是第二次握手;第三次握手,就是客户端发送ACK信号给服务器。至此,完成三次握手,客户端和服务器均进入ESTAB-LISHED状态,可以完成数据的互发。

四次挥手,客户端和服务器都可以发起,以此来完成断联。客户端在ESTAB-LISHED状态发送FIN给服务器,客户端进入FIN-WAIT-1状态,这就是第一次挥手;然后服务器在ESTAB-LISHED状态接受FIN,回应一个ACK信号给客户端,服务器进入CLOSE-WAIT状态,这就是第二次挥手;然后服务器发送一个FIN信号给客户端,客户端进入FIN-WAIT-2状态,服务器进入LAST-ACK状态,这就是第三次挥手;最给服务后客户端发送ACK信号器,自身进入TIME-WAIT状态(2s),然后进入CLOSED状态,服务器也进入CLOSED状态,这就是第四次挥手。

TCP首部数据结构

路径:lwip-2.1.2\src\include\lwip\prot\tcp.h

TCP首部的数据结构及字段操作都在这个文件中。

TCP首部数据结构:struct tcp_hdr

对应TCP报文段示例图:

TCP控制块

TCP控制块(TCP PCB)这个是每个TCP连接的中央,非常重要,保存了TCP相关的重要数据,所以先了解下TCP控制块的各个字段。

tcp_pcb结构体的功能大概如图

/** the TCP protocol control block */
struct tcp_pcb {
/** common PCB members */
  IP_PCB;   //ip层的成员(ip地址等)
/** protocol specific PCB members */
  TCP_PCB_COMMON(struct tcp_pcb);     /* 协议特定的 PCB 成员 */

  /* ports are in host byte order */
  u16_t remote_port;    //tcp层的远端端口

  tcpflags_t flags;
#define TF_ACK_DELAY   0x01U   /* Delayed ACK. */    /* 延迟回答 ack会在fastmr定时器中被发送 Delayed ACK.*/
#define TF_ACK_NOW     0x02U   /* Immediate ACK. */   /* 立即发送 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) */  /* fin报文fastmr定时器中被发送.  关闭挂起*/
#define TF_RXCLOSED    0x10U   /* rx closed by tcp_shutdown */   /* rx 由 tcp_shutdown 关闭 */
#define TF_FIN         0x20U   /* Connection was closed locally (FIN segment enqueued). */  /* 本地主动请求关闭. 连接在本地关闭 */
#define TF_NODELAY     0x40U   /* Disable Nagle algorithm */  /* 禁用 Nagle 算法 */
#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
#define TF_BACKLOGPEND 0x0200U /* If this is set, a connection pcb has increased the backlog on its listener */
#endif
#if LWIP_TCP_TIMESTAMPS
#define TF_TIMESTAMP   0x0400U   /* Timestamp option enabled */
#endif

/* RTO 计时器 */
#define TF_RTO         0x0800U /* RTO timer has fired, in-flight data moved to unsent and being retransmitted */  /* RTO计时器已触发,unacked队列数据已迁回unsent队列,并正在重传 */
#if LWIP_TCP_SACK_OUT
#define TF_SACK        0x1000U /* Selective ACKs enabled */
#endif

  /* the rest of the fields are in host byte order
     as we have to do some math with them */

  /* Timers */
  /* 空闲poll周期回调相关:polltmr会周期性增加,当其值超过pollinterval时,poll函数会被调用。 */
  u8_t polltmr, pollinterval;
  /* 控制块被最后一次处理的时间 */
  u8_t last_timer;
  /* 保存这控制块的TCP节拍起始值。用于当前PCB的时基初始值参考 */
  /* 活动计时器,收到合法报文时自动更新。 */
  u32_t tmr;

  /* receiver variables */
  /* 接收窗口相关的字段 */
  u32_t rcv_nxt;   /* next seqno expected */    /* 期待收到的下一个seq号。一般发送报文段时,ACK值就是该值 */
  tcpwnd_size_t rcv_wnd;   /* receiver window available */ /* 接收窗口实时大小:从远端收到数据,该值减小;应用层读走数据,该值增加。 */
  tcpwnd_size_t rcv_ann_wnd; /* receiver window to announce */  /* 窗口通告值大小:即是告诉发送方,我们这边的接口窗口的大小 */
  u32_t rcv_ann_right_edge; /* announced right edge of window */  /* 窗口通告值右边界 */

#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 */

  /* Retransmission timer. */
  s16_t rtime;    /* 超时重传计时器值,当该值大于RTO值时,重传报文 */

  u16_t mss;   /* maximum segment size */    /* 远端的MSS, 最大报文段大小 */

  /* RTT (round trip time) estimation variables */
  /* RTT(往返时间)估计变量 */
  u32_t rttest; /* RTT estimate in 500ms ticks */     /* RTT测量,发送时的时间戳。精度500ms , 以为 500 毫秒递增, rttest=tcpticks开启rtt,rttest=0关闭*/
  u32_t rtseq;  /* sequence number being timed */     /* 开始计算RTT时对应的seq号 用于测试 RTT 的报文段序号 */
  /* RTT估计出的平均值和时间差。
      注意:sa为算法中8倍的均值;sv为4倍的方差。再去分析LWIP实现RTO的算法。 */
  s16_t sa, sv; /* @see "Congestion Avoidance and Control" by Van Jacobson and Karels */   /* RTT 估计得到的平均值与时间差 */

  s16_t rto;    /* retransmission time-out (in ticks of TCP_SLOW_INTERVAL) */   /* 重传超时时间。节拍宏:TCP_SLOW_INTERVAL。初始超时时间宏:LWIP_TCP_RTO_TIME  rtime>rto时触发超时重传 rto是动态计算的*/
  u8_t nrtx;    /* number of retransmissions */     /* 重传次数. 重传次数<7*/

  /* fast retransmit/recovery */
  /* 快重传和快恢复相关*/
  u8_t dupacks;   //快速重连,回答重复次数. 收到最大重复ACK的次数:一般收1-2次认为是重排序引起的。收到3次后,可以确认为失序,需要立即重传。然后执行拥塞避免算法中的快恢复。
  u32_t lastack; /* Highest acknowledged seqno. */  /* 接收到的最大有序ACK号 */

  /* congestion avoidance/control variables */
  /* 拥塞避免/控制变量 */
  tcpwnd_size_t cwnd;   //拥塞窗口大小
  tcpwnd_size_t ssthresh;     //拥塞算法启动条件 /* 拥塞避免算法启动阈值。也叫慢启动上门限值。 */

  /* first byte following last rto byte */
  /* rto重传的那些报文段的下一个seq号。用于解除rto状态。 */
  u32_t rto_end;

  /* sender variables */
  u32_t snd_nxt;   /* next new seqno to be sent */  /* 下一个需要发送的seq号。一般也是收到最新最大的ACK号。 */
  u32_t snd_wl1, snd_wl2; /* Sequence and acknowledgement numbers of last window update. */
                              /* 上次发送窗口更新时,收到的seq号和ack号。在tcp_receive()用于更新发送窗口。 */
                             //snd_wl1 = last seqno
                            //snd_wl2 = last ackno 
  u32_t snd_lbb;       /* Sequence number of next byte to be buffered. */  /* 下一个被缓冲的应用程序数据的seq号 */
  tcpwnd_size_t snd_wnd;   /* sender window */  /* 发送窗口的大小(由接收方大小决定):实时的。发出数据,该值减少;收到ACK,该值增加。 */ 
  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; /* Number of pbufs currently in the send buffer. */  /* 发送缓冲区中现有的pbuf个数 */

#if TCP_OVERSIZE
  /* Extra bytes available at the end of the last pbuf in unsent. */
  /* 在未发送的TCP数据中,最后一个pbuf剩余的未使用的空间size */
  u16_t unsent_oversize;
#endif /* TCP_OVERSIZE */

  tcpwnd_size_t bytes_acked;  /* 累计ACK新数据的量。拥塞避免时,用于判断cwnd是否需要+1MSS。 */

  /* These are ordered by sequence number: */
  /* 几条TCP报文段缓存队列指针 */
  struct tcp_seg *unsent;   /* Unsent (queued) segments. */     /* 未发送的报文段队列 */
  struct tcp_seg *unacked;  /* Sent but unacknowledged segments. */   /* 已发送,但是未收到ACK的报文段队列 */
#if TCP_QUEUE_OOSEQ
  struct tcp_seg *ooseq;    /* Received out of sequence segments. */    /* 接收到的乱序报文段队列 (序号小的在前)*/
#endif /* TCP_QUEUE_OOSEQ */

  /* 接收到,但未被应用层取走的报文段队列 */
  struct pbuf *refused_data; /* Data previously received but not yet taken by upper layer */

#if LWIP_CALLBACK_API || TCP_LISTEN_BACKLOG
  /* 当前连接属于哪个服务器 */
  struct tcp_pcb_listen* listener;
#endif /* LWIP_CALLBACK_API || TCP_LISTEN_BACKLOG */

  /* 几个回调函数。由用户注册。 */
#if LWIP_CALLBACK_API
  /* Function to be called when more send buffer space is available. */
  /* 数据发送成功后被回调  当报文被对方确认时执行,用于释放资源*/
  tcp_sent_fn sent;
  /* Function to be called when (in-sequence) data has arrived. */
  /* 收到有序数据后被回调  当接收到数据时执行*/
  tcp_recv_fn recv;
  /* Function to be called when a connection has been set up. */
  /* 建立连接后被回调 */
  tcp_connected_fn connected;
  /* Function which is called periodically. */
  /* 该函数被内核周期性回调。参考polltmr 周期回调函数:用于处理一些超时等*/
  tcp_poll_fn poll;
  /* Function to be called whenever a fatal error occurs. */
  /* 发生错误时被回调 当接收到RST报文或其他异常断开时执行*/
  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 */

  /* idle time before KEEPALIVE is sent */
   /* keepalive计时器的上限值 保活机制,超过此值,发送探查检查连接*/
  u32_t keep_idle;
#if LWIP_TCP_KEEPALIVE
 /* keepalive探测间隔 */
  u32_t keep_intvl;
  /* keepalive探测的上限次数 */
  u32_t keep_cnt;
#endif /* LWIP_TCP_KEEPALIVE */

  /* 坚持定时器:用于解决远端接收窗口为0时,定时询问使用 */
  /* Persist timer counter */
  u8_t persist_cnt;   /* 坚持定时器节拍计数值 当计数值超过某个值时,则发出零窗口探查数据包*/
  /* Persist timer back-off */
  u8_t persist_backoff;   /* 坚持定时器探查报文时间间隔列表索引 发出探查包的次数以及是否开启坚持*/
  /* Number of persist probes */
  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首部的,就是pcb控制块,协议特定的TCP_PCB_COMMON控制块,远程端口号以及标志位flags(用于判断处于什么状态,完成三次握手和四次挥手);
之后还会用到发送和接收成功的两个回调函数,以及连接成功的回调函数;轮询查阅是否有信息的函数,以及发生致命错误时的函数。

 lwIP将TCP的控制块连接成为单向链表如下所示:

通过TCP_PCB_COMMON里面的next指针进行链接,最后一个就是NULL,连接之后就可以遍历完成寻找。 

报文段数据结构

TCP连接的每一端都有接收缓冲区与发送缓冲区(也可以称之为缓冲队列,下文均用缓冲队列),而 TCP控制块只是维护缓冲区队列的指针,通过指针简单对这些缓冲区进行管理,LwIP为了更好管理TCP报文段的缓冲队列数据, 特地定义了一个数据结构,命名为tcp_seg,使用它将所有的报文段连接起来,这些报文可能是无发送的、 可能是已发送但未确认的或者是已经接收到的无序报文,都是需要缓冲在TCP控制块内部的,以便识别是哪个连接, 而TCP控制块,又不可能单独为每个连接开辟那么大的空间,只能使用指针来管理

在TCP控制块中有三个缓冲队列,都已报文段形式保存:

struct tcp_seg *unsent:未发送队列。即是等待发送的报文段队列。
struct tcp_seg *unacked:空中报文队列。即是已经发送,但是还没收到ACK的报文段队列。
struct tcp_seg *ooseq:乱序报文队列。即是收到的报文是窗口内,但是不是当前期待收到的下一个SEQ的报文段。先用改队列存起来,等收到前面空缺的报文后就可以直接接上这些报文段了。

(这3个字段是出现在我们前面的tcp控制块里的成员)

tcp_seg​数据结构中维护TCP首部指针struct tcp_hdr *tcphdr;​是很有必要的,因为tcp_seg​在处理过程中,会频繁移动pbuf->payload​指针,所以需要一个专门的TCP首部指针来维护。 

路径: lwip-2.1.2\src\include\lwip\priv\tcp_priv.h


struct tcp_seg {
  /* 链表节点 */
  struct tcp_seg *next;    /* used when putting segments on a queue */ 
  /* TCP报文:TCP首部 + TCP数据 */
  struct pbuf *p;          /* buffer containing data + TCP header */ 
  /* 报文段的纯TCP数据长度(不统计SYN和FIN) */
  u16_t len;               /* the TCP length of this segment */
#if TCP_OVERSIZE_DBGCHECK
 /* 当前报文段中最后一个pbuf的可用剩余空间 */
  u16_t oversize_left;     /* Extra bytes available at the end of the last
                              pbuf in unsent (used for asserting vs.
                              tcp_pcb.unsent_oversize only) */
#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控制块中维护了3个是指针,分别是unsent、unacked、ooseq,unsent指向未发送的报文段缓冲队列, unacked指向已发送但未收到确认的报文段缓冲队列,ooseq指向已经收到                  的无序报文段缓冲队列,当然啦, 如果都没有这些报文段,那么这些指针都会指向NULL,其指向示意图具体见: 

tcp_active_pcbs 是 lwIP(轻量级 IP 协议栈)中的一个全局变量,它用来跟踪所有处于活动状态的 TCP 协议控制块(PCB)。这些 PCB 代表了当前正在进行的 TCP 连接,通过 tcp_active_pcbs,lwIP 可以管理和维护这些连接的状态和数据传输 

TCP控制块链表

 路径:lwip\lwip-2.1.2\src\core\tcp.c

tcp服务器端除了有tcp server程序,也有tcp client程序,对于tcp server,监听的tcp信息在tcp_bound_pcbs、tcp_listen_pcbs里面,对于tcp client,使用的ip、端口等信息存储在tcp_active_pcbs、tcp_tw_pcbs里面

/* The TCP PCB lists. */

/** List of all TCP PCBs bound but not yet (connected || listening) */
struct tcp_pcb *tcp_bound_pcbs;    /* 存储已经绑定但尚未处于连接或监听状态的 TCP 协议控制块(PCB)。这有助于跟踪那些已经完成地址和端口绑定,但还未开始实际连接或监听操作的 TCP 连接 */
/** List of all TCP PCBs in LISTEN state */
union tcp_listen_pcbs_t tcp_listen_pcbs;  /* 保存处于监听状态的 TCP PCB。监听状态意味着该连接正在等待客户端的连接请求 */
/** List of all TCP PCBs that are in a state in which
 * they accept or send data. */
struct tcp_pcb *tcp_active_pcbs;   /* 包含处于能够接收或发送数据的稳定状态的 TCP PCB。这通常表示已经建立连接并且正在进行数据传输的连接 */
/** List of all TCP PCBs in TIME-WAIT state */
struct tcp_pcb *tcp_tw_pcbs;   /* 存储处于 TIME-WAIT 状态的 TCP PCB。TIME-WAIT 状态是在连接关闭后为了确保数据包能够正确传输和处理而设置的一种短暂状态 */

/* 这是一个数组,包含了上述几个不同状态的 TCP PCB 列表的指针。通过这个数组,可以更方便地对不同状态的 TCP 连接进行统一的管理和操作 */
/** 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,使用另一种数据结构来管理,降低内存使用。

/* The TCP PCB lists. */
union tcp_listen_pcbs_t { /* List of all TCP PCBs in LISTEN state. */
  struct tcp_pcb_listen *listen_pcbs;
  struct tcp_pcb *pcbs;    // 用于存储处于监听状态的 TCP 连接控制块
};

/** the TCP protocol control block for listening pcbs */
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
  /* Function to call when a listener has been connected. */
   /* 侦听到有连接接入时被调用的函数 */
  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 */
};

 lwip tcp数据结构

tcp相关数据结构如下,tcp_pcb_listen为服务器的监听tcp链表,各监听的tcp通过next指针连接成一个链表,tcp_pcb为tcp控制块(tcp_pcb_listen监听仅需要端口、ip地址等信息,tcp_pcb为通信过程的tcp控制块,需要包含tcp通信协议的数据等,tcp_pcb包含了tcp_pcb_listen所有的信息,因为监听仅需要少部分信息,为了节省内存,因此从tcp_pcb提取了部分必要监听数据组成tcp_pcb_listen),tcp_seg为tcp报文段链表(tcp报文段有大小限制mss,大的数据被拆分成一个个小的报文段连接在tcp_seg链表里面),tcp_hdr为tcp首部(1.1节中TCP首部的实现),pbuf存储数据(数据连接在pbuf链表里面,tot_len为pbuf链表的数据长度,len为该pbuf的数据长度)

TCP单帧入站相关数据

LWIP是一个内核单线程的TCPIP协议栈,所以收到TCP包后,LWIP内核就会单独处理该TCP包,不会出现同一个LWIP内核并发处理多个TCP包。

所以可以为TCP包创建一些全局值,减少函数间的参数传递。

路径:lwip-2.1.2\src\core\tcp_in.c

/* These variables are global to all functions involved in the input
   processing of TCP segments. They are set by the tcp_input()
   function. */
   /* 这些全局变量有tcp_input()收到TCP报文段后设置的,表示当前接收到,正在处理的TCP报文段信息 */
static struct tcp_seg inseg;  /* TCP报文段数据结构 可能包括报文段的数据、长度等 */
static struct tcp_hdr *tcphdr; /* 指向接收到的 TCP 头部的指针,通过它可以访问和解析 TCP 头部的字段 */
static u16_t tcphdr_optlen;  /* 表示 TCP 头部选项字段的长度 */
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首部各个标志字段 如 SYN、ACK、FIN 等*/

static u8_t recv_flags;  /* 记录tcp_process()对报文段的处理结果 */
static struct pbuf *recv_data;  /* 单次提交到应用层的数据缓冲区。本次input_receive()处理后,把需要递交到应用层的数据,缓存到这里。 */

struct tcp_pcb *tcp_input_pcb;  /* 当前进行输入处理的TCP PCB。时刻唯一 */

 TCP函数汇总

新建控制块:tcp_new()

就是调用了tcp_alloc函数;这个函数里面定义了tcp_pcb的结构体pcb,然后内存池的方式memp_malloc申请内存,然后设置控制块参数,完成后返回pcb;

在本函数中,能了解到LWIP申请TCP PCB的内存管理逻辑,也能找到TCP性能的默认值(这个对TCP网络分析的同学挺有用的)。

/**
 * Allocate a new tcp_pcb structure.
 *
 * @param prio priority for the new pcb
 * @return a new tcp_pcb that initially is in state CLOSED
 */
/**
 * 申请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);  // 尝试从内存池中分配一个 TCP PCB
  if (pcb == NULL) {
    /* Try to send FIN for all pcbs stuck in TF_CLOSEPEND first */
    /* 先处理那些处于TF_CLOSEPEND状态的pcb。主动触发他们再次发起FIN。(之前发送FIN失败的pcb,这些pcb都是我们想关闭的pcb了) */
    tcp_handle_closepend();

    /* Try killing oldest connection in TIME-WAIT. */
    /* 内存不足,干掉最老的TIME_WAIT连接 */
    LWIP_DEBUGF(TCP_DEBUG, ("tcp_alloc: killing off oldest TIME-WAIT connection\n"));
    tcp_kill_timewait();
    /* Try to allocate a tcp_pcb again. */
    pcb = (struct tcp_pcb *)memp_malloc(MEMP_TCP_PCB);
    if (pcb == NULL) {
      /* Try killing oldest connection in LAST-ACK (these wouldn't go to TIME-WAIT). */
      /* 还是内存不足,就干掉最老的LAST_ACK连接 */
      LWIP_DEBUGF(TCP_DEBUG, ("tcp_alloc: killing off oldest LAST-ACK connection\n"));
      tcp_kill_state(LAST_ACK);
      /* Try to allocate a tcp_pcb again. */
      pcb = (struct tcp_pcb *)memp_malloc(MEMP_TCP_PCB);
      if (pcb == NULL) {
        /* Try killing oldest connection in CLOSING. */
        /* 还是内存不足,干掉最老的CLOSING连接 */
        LWIP_DEBUGF(TCP_DEBUG, ("tcp_alloc: killing off oldest CLOSING connection\n"));
        tcp_kill_state(CLOSING);
        /* Try to allocate a tcp_pcb again. */
        pcb = (struct tcp_pcb *)memp_malloc(MEMP_TCP_PCB);
        if (pcb == NULL) {
          /* Try killing oldest active connection with lower priority than the new one. */
          /* 还是内存不足,那就干掉优先级更低的最老的连接 */
          LWIP_DEBUGF(TCP_DEBUG, ("tcp_alloc: killing oldest connection with prio lower than %d\n", prio));
          tcp_kill_prio(prio);
          /* Try to allocate a tcp_pcb again. */
          pcb = (struct tcp_pcb *)memp_malloc(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 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) {
    /* 申请成功 */
    /* zero out the whole pcb, so there is no need to initialize members to zero */
    memset(pcb, 0, sizeof(struct tcp_pcb));  /* 清空所有字段 */
    pcb->prio = prio;  /* 设置控制块优先级 */
    pcb->snd_buf = TCP_SND_BUF; /* 设置发送缓冲区大小 */
    /* Start with a window that does not need scaling. When window scaling is
       enabled and used, the window is enlarged when both sides agree on scaling. */
    pcb->rcv_wnd = pcb->rcv_ann_wnd = TCPWND_MIN16(TCP_WND);  /* 初始化接收窗口和窗口通告值 */
    pcb->ttl = TCP_TTL;  /*  设置生存时间 */
    /* As initial send MSS, we use TCP_MSS but limit it to 536.
       The send MSS is updated when an MSS option is received. */
    pcb->mss = INITIAL_MSS;  /* 初始化MSS,在SYN时,会在选项字段发送到对端。 */
    pcb->rto = 3000 / TCP_SLOW_INTERVAL;  /* 初始RTO时间为LWIP_TCP_RTO_TIME,默认3000ms */
    pcb->sv = 3000 / 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 5681 recommends setting ssthresh abritrarily high and gives an example
    of using the largest advertised receive window.  We've seen complications with
    receiving TCPs that use window scaling and/or window auto-tuning where the
    initial advertised window is very small and then grows rapidly once the
    connection is established. To avoid these complications, we set ssthresh to the
    largest effective cwnd (amount of in-flight data) that the sender can have. */
    /* RFC 5618建议设置ssthresh值尽可能高,比如设置为最大可能的窗口通告值大小(可以理解为最大可能的发送窗口大小 )。 */
    /* 这里先设置为本地发送缓冲区大小,即是最大飞行数据量。后面进行窗口缩放和自动调优时自动调整。 */
    pcb->ssthresh = TCP_SND_BUF;  // 设置慢启动阈值

#if LWIP_CALLBACK_API
    /* 默认接收回调 */
    pcb->recv = tcp_recv_null;
#endif /* LWIP_CALLBACK_API */

    /* Init KEEPALIVE timer */
    /* 保活计时器超时值:默认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;
}

其中ttl的含义为:

TTL 是一个字段,用于指定数据包在网络中可以经过的最大跳数(路由器的数量)。每经过一个路由器,TTL 的值就会减 1。当 TTL 减为 0 时,路由器会丢弃该数据包,以防止数据包在网络中无限循环。

举例来说,假设一个数据包的 TTL 初始值被设置为 64。当它从源主机出发,经过第一个路由器时,TTL 会减 1 变为 63。然后经过第二个路由器时,变为 62,以此类推。如果在 TTL 变为 0 之前还没有到达目的地,那么这个数据包就会被丢弃。

TTL 的主要作用包括:

  1. 防止数据包在网络中无限循环,造成网络拥塞。
  2. 有助于控制网络的范围和路由选择。

绑定本地服务:tcp_bind()

 TCP PCB新建后,需要绑定本地的IP和端口号,这样就能表示一个接入到应用层的连接了。

这个函数用于将给定的 tcp_pcb 结构体绑定到指定的本地 IP 地址和端口。如果指定的端口已被使用且不满足重用条件,或者 PCB 状态不正确,函数会返回相应的错误码。 

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填了NULL或IP_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.
 *
 * @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;  // 最大的 PCB 列表数量
  struct tcp_pcb *cpcb;  // 用于遍历其他 PCB 的指针
#if LWIP_IPV6 && LWIP_IPV6_SCOPES
  ip_addr_t zoned_ipaddr;   // 用于处理 IPv6 地址的区域
#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) {   // 如果 IPv4 地址为空,设置为 ANY 地址
    ipaddr = IP4_ADDR_ANY;
  }
#else /* LWIP_IPV4 */
  LWIP_ERROR("tcp_bind: invalid ipaddr", ipaddr != NULL, return ERR_ARG);  // 错误处理:IPv4 时地址不能为 NULL
#endif /* LWIP_IPV4 */

  LWIP_ERROR("tcp_bind: invalid pcb", pcb != NULL, return ERR_ARG);  // 错误处理:PCB 指针不能为 NULL

  LWIP_ERROR("tcp_bind: can only bind in state CLOSED", pcb->state == CLOSED, return ERR_VAL);  // 错误处理:只能在 CLOSED 状态绑定

#if SO_REUSE
  /* Unless the REUSEADDR flag is set,
     we have to check the pcbs in TIME-WAIT state, also.
     We do not dump TIME_WAIT pcb's; they can still be matched by incoming
     packets using both local and remote IP addresses and ports to distinguish.
   */
  /* 如果设置了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;  // 调整最大 PCB 列表数量
  }
#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++) {  // 遍历所有 PCB 列表
      for (cpcb = *tcp_pcb_lists[i]; cpcb != NULL; cpcb = cpcb->next) {  // 遍历列表中的每个 PCB
        if (cpcb->local_port == port) {   // 如果找到相同端口的 PCB
#if SO_REUSE
          /* Omit checking for the same port if both pcbs have REUSEADDR set.
             For SO_REUSEADDR, the duplicate-check for a 5-tuple is done in
             tcp_connect. */
             /* 如果两个TCP PCB都设置了SO_REUSEADDR选项,则可以复用同一个IP和端口号 */
          if (!ip_get_option(pcb, SOF_REUSEADDR) ||   // 如果当前 PCB 未设置重用地址标志
              !ip_get_option(cpcb, SOF_REUSEADDR))  // 或者找到的 PCB 未设置重用地址标志
#endif /* SO_REUSE */
          {
            /* @todo: check accept_any_ip_version */
            /* 注意:任意IP即是万能IP */
            if ((IP_IS_V6(ipaddr) == IP_IS_V6_VAL(cpcb->local_ip)) &&  // 检查 IP 版本是否相同
                (ip_addr_isany(&cpcb->local_ip) ||   // 或者本地 IP 为任意地址  
                 ip_addr_isany(ipaddr) ||   // 或者给定的 IP 为任意地址
                 ip_addr_cmp(&cpcb->local_ip, ipaddr))) {   // 或者本地 IP 与给定的 IP 相同
                  /* 如果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;  // 设置 PCB 中的本地端口
  TCP_REG(&tcp_bound_pcbs, pcb);  // 将 PCB 注册到绑定的 PCB 列表中
  LWIP_DEBUGF(TCP_DEBUG, ("tcp_bind: bind to port %"U16_F"\n", port));
  return ERR_OK;
}

SO_REUSEADDR 是一个套接字选项,主要作用是允许在特定情况下重用本地地址和端口。
以下是一些常见的作用和示例场景:
作用:
允许同一台主机上的多个进程或套接字绑定到相同的本地端口,前提是它们绑定的本地 IP 地址不同。
当一个套接字处于 TIME_WAIT 状态(通常在关闭连接后),允许新的套接字立即重用该端口。
在服务器重启的场景中,如果服务器意外关闭,新启动的服务器进程可以快速重用之前使用的端口,而无需等待 TIME_WAIT 状态结束。
示例场景:
假设我们有一个服务器程序,使用端口 8080 进行通信。由于某些原因,服务器意外关闭。如果没有设置 SO_REUSEADDR 选项,当服务器重新启动时,可能会因为之前关闭的连接仍处于 TIME_WAIT 状态而无法立即绑定到端口 8080,导致启动失败或延迟。但如果设置了 SO_REUSEADDR 选项,新的服务器进程就可以忽略之前的 TIME_WAIT 状态,直接绑定到端口 8080 并开始服务,从而减少服务中断的时间。
再比如,在一个多进程的服务器环境中,多个进程可能需要同时监听同一个端口,但绑定不同的本地 IP 地址。通过设置 SO_REUSEADDR 选项,这些进程可以顺利完成绑定,实现并发处理。
总之,SO_REUSEADDR 选项提供了在特定条件下更灵活地使用本地地址和端口的能力,有助于提高网络应用程序的可用性和性能。

tcp端口的创建(tcp_new_port)

tcp绑定时如果端口为0,则由tcp协议动态创建一个端口,lwip调用tcp_new_port函数创建一个端口。tcp_new_port有一个静态局部变量port,用于记录查找到的可用端口,tcp_new_port第一次从TCP_LOCAL_PORT_RANGE_START开始查找,下次从port开始查找(在下一次调用tcp_new_port创建新的端口前,如果port前的端口没有被释放或者有部分被释放,那么port前有很多端口不被占用,在port后查找到可用端口到可能性及效率远高于在port前查找,因此每次都从port开始往后查找可用端口)。

tcp_new_port检查tcp_active_pcbs、tcp_tw_pcbs、tcp_listen_pcbs是否占用新的端口,都没有占用则该端口可用。(没有检查tcp_bound_pcbs的端口,tcp_bind函数检查tcp_new_port的端口是否被tcp_bound_pcbs占用;另外有些tcp server绑定的是0.0.0.0,所以lwip端口占用检查并没有检查ip,多个网卡时,不同的ip理论上可以绑定同样的端口)

绑定过程需要查找ip及端口地址是否被占用,在下图中从上到下查找各个链表。

static u16_t
tcp_new_port(void)
{
  struct tcp_pcb *pcb;
#ifndef TCP_LOCAL_PORT_RANGE_START
#define TCP_LOCAL_PORT_RANGE_START 4096 // 开始端口(动态端口仅从TCP_LOCAL_PORT_RANGE_START~TCP_LOCAL_PORT_RANGE_END之间分配)
#define TCP_LOCAL_PORT_RANGE_END   0x7fff
#endif
  static u16_t port = TCP_LOCAL_PORT_RANGE_START; // 开始查找的端口
  
 again:
  if (++port > TCP_LOCAL_PORT_RANGE_END) { // 已经到了最大端口,重新从TCP_LOCAL_PORT_RANGE_START开始查找
    port = TCP_LOCAL_PORT_RANGE_START;
  }
  
  for(pcb = tcp_active_pcbs; pcb != NULL; pcb = pcb->next) { // 检查该端口是否在已连接的tcp_active_pcbs中被占用
    if (pcb->local_port == port) { // 端口被占用
      goto again; // 检查下一个端口
    }
  }
  for(pcb = tcp_tw_pcbs; pcb != NULL; pcb = pcb->next) { // 检查该端口是否在正在关闭的连接tcp_tw_pcbs中被占用
    if (pcb->local_port == port) { // 端口被占用
      goto again; // 检查下一个端口
    }
  }
  for(pcb = (struct tcp_pcb *)tcp_listen_pcbs.pcbs; pcb != NULL; pcb = pcb->next) { // 检查该端口是否在监听的tcp_listen_pcbs中被占用
    if (pcb->local_port == port) {
      goto again;
    }
  }
  return port;
}

监听: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处理即可,表示已经连接完成。

而对于客户端来说,绑定成功后,就可以调用当前函数连接服务端了。

函数作用
该函数用于尝试建立一个 TCP 连接。它设置了连接相关的参数,如本地和远程的 IP 地址、端口,初始化了一些窗口和序列号等参数,并发送 SYN 数据包来启动连接建立过程。

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 calback 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
 */
/**
 * 尝试建立一个 TCP 连接
 *
 * @param pcb 要进行连接操作的 TCP 协议控制块
 * @param ipaddr 目标的 IP 地址
 * @param port 目标端口
 * @param connected 连接成功时的回调函数
 * @return 错误码,ERR_OK 表示操作成功,其他值表示不同类型的错误
 */
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);  // 检查 IP 地址是否有效,无效则返回错误
 
  LWIP_ERROR("tcp_connect: can only connect from state CLOSED", pcb->state == CLOSED, return ERR_ISCONN);   // 检查 PCB 状态,非 CLOSED 状态则返回错误

  LWIP_DEBUGF(TCP_DEBUG, ("tcp_connect to port %"U16_F"\n", port));
  ip_addr_set(&pcb->remote_ip, ipaddr);  // 设置远程 IP 地址  
  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 */
  /* 检查本地 IP 是否已分配给 PCB,如果没有,获取一个 */
  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. */
  /* 如果给定的远程 IP 地址应该有区域但没有,现在分配一个。
   * 鉴于我们已经有目标网络接口,这很容易且成本低。 */
  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) {   // 如果本地端口为 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)) {  // 如果设置了重用地址选项
      /* Since SOF_REUSEADDR allows reusing a local address, we have to make sure
         now that the 5-tuple is unique. */
      /* 如果设置了SOF_REUSEADDR选项值,
          则需要判断五元组唯一才能连接:本地IP、本地PORT、远端IP、远端PORT和TCP PCB状态 */   
      struct tcp_pcb *cpcb;
      int i;
      /* TCP PCB状态链表只遍历稳定态和TIME_WAIT状态的,不遍历绑定态和监听态的,因为设置了SOF_REUSEADDR,是允许客户端复用服务器的。 */
      /* Don't check listen- and bound-PCBs, check active- and TIME-WAIT PCBs. */
      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_cmp(&cpcb->local_ip, &pcb->local_ip) &&   // 检查本地 IP 是否相同
              ip_addr_cmp(&cpcb->remote_ip, ipaddr)) {   // 检查远程 IP 是否相同
            /* 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;  // 接收的下一个序列号设置为 0
  pcb->snd_nxt = iss;  // 发送的下一个序列号设置为获取的 iss
  pcb->lastack = iss - 1;  // 最后确认的序列号
  pcb->snd_wl2 = iss - 1;
  pcb->snd_lbb = iss - 1;
  /* Start with a window that does not need scaling. When window scaling is
     enabled and used, the window is enlarged when both sides agree on scaling. */
  /* 初始化接收窗口、窗口通告值、窗口通告值右边沿值 */   
  pcb->rcv_wnd = pcb->rcv_ann_wnd = TCPWND_MIN16(TCP_WND);  // 设置接收窗口
  pcb->rcv_ann_right_edge = pcb->rcv_nxt;   // 接收窗口的右边界
  /* 初始化发送窗口 */
  pcb->snd_wnd = TCP_WND;
  /* As initial send MSS, we use TCP_MSS but limit it to 536.
     The send MSS is updated when an MSS option is received. */
   /* 作为初始发送的最大段大小,我们使用 TCP_MSS 但限制为 536。
     当接收到 MSS 选项时会更新发送的 MSS。 */  
   /* 初始化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 */

  /* Send a SYN together with the MSS option. */
  /* 构造一个连接请求报文到TCP PCB中:SYN + MSS option */
  ret = tcp_enqueue_flags(pcb, TCP_SYN);  // 发送 SYN 标志
  if (ret == ERR_OK) {
    /* SYN segment was enqueued, changed the pcbs state now */
    /* 更新为SYN_SENT状态 */
    /* SYN 段已入队,现在更改 PCB 的状态 */
    pcb->state = SYN_SENT;  // PCB 状态设置为 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,你可以释放这部分数据的内存了。

/**
 * @ingroup tcp_raw
 * This function should be called by the application when it has
 * processed the data. The purpose is to advertise a larger window
 * when the data has been processed.
 *
 * @param pcb the tcp_pcb for which data is read
 * @param len the amount of bytes that have been read by the application
 */
/*tcp_recved()​函数是被应用层调用,用于通知TCP内核:应用层已经从接收到的数据size,你可以释放这部分数据的内存了。
  处理 TCP 接收数据时的窗口更新
  应用程序从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 */
    // 窗口过大或 tcpwnd_size_t 类型溢出
    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);

  /* If the change in the right edge of window is significant (default
   * watermark is TCP_WND/4), then send an explicit update now.
   * Otherwise wait for a packet to be sent in the normal course of
   * events (or more window to be available later) */
  /* 如果接收窗口右边界滑动了 (1/4接收缓冲) || (4个MSS) 都可以立即发送窗口通告值到对端; */
  /* 如果接收窗口右边界滑动达不到阈值,就等正常发送数据时才附带窗口通告值。 */
  if (wnd_inflation >= TCP_WND_UPDATE_THRESHOLD) {    // 如果窗口增加量达到阈值
    tcp_ack_now(pcb);  // 立即发送 ACK
    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);

  // 如果不是 LISTEN 状态
  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);
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值