【lwip】13-TCP协议分析之源码篇链接:[https://www.cnblogs.com/lizhuming/p/17438682.html](https://www.cnblogs.com/lizhuming/p/17438682.html)
前言
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填了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.
* @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接口层来看:
- 先调用
tcp_sent()
注册发送回调函数。 - 调用
tcp_write()
把需要发送的数据组装成报文段,插入TCP发送缓冲区中。 - 主动调用或内部定时调用
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报文段组建分为三个步骤,且难度逐渐增加,如果某个步骤就能完成数据写入,后面步骤就不需要继续了:
- 把数据先copy到最后一个
segment
的oversize
中。 - 新建一个pbuf,接入最后一个未发送
segment
中。 - 创建一个新的
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:
- 如果数据已经添加到last_unsent的剩余空间,我们更新pbuf链的长度字段,让这些数据生效。
- 把需要插入发送缓冲区中最后一个报文段的数据进行插入操作。
- 接入新的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_used;
}
}
last_unsent->len += oversize_used; /* 更新segment有效数据长度 */
#if TCP_OVERSIZE_DBGCHECK
LWIP_ASSERT("last_unsent->oversize_left >= oversize_used",
last_unsent->oversize_left >= oversize_used);
last_unsent->oversize_left -= oversize_used;
#endif /* TCP_OVERSIZE_DBGCHECK */
}
pcb->unsent_oversize = oversize; /* 更新segment有效数据长度 */
#endif /* TCP_OVERSIZE */
/*
* Phase 2: concat_p连接到last_unsent->p,除非最后的pbuf是ROM pbuf,且地址连续,可以扩展以包括新数据。
*/
if (concat_p != NULL) {
LWIP_ASSERT("tcp_write: cannot concatenate when pcb->unsent is empty",
(last_unsent != NULL));
pbuf_cat(last_unsent->p, concat_p); /* 拼接pbuf */
last_unsent->len += concat_p->tot_len; /* 更新segment数据长度 */
} else if (extendlen > 0) { /* 如果是扩展原有pbuf */
struct pbuf *p;
LWIP_ASSERT("tcp_write: extension of reference requires reference",
last_unsent != NULL && last_unsent->p != NULL);
for (p = last_unsent->p; p->next != NULL; p = p->next) {
p->tot_len += extendlen;
}
p->tot_len += extendlen;
p->len += extendlen;
last_unsent->len += extendlen;
}
#if TCP_CHECKSUM_ON_COPY
if (concat_chksummed) {
LWIP_ASSERT("tcp_write: concat checksum needs concatenated data",
concat_p != NULL || extendlen > 0);
/*if concat checksumm swapped - swap it back */
if (concat_chksum_swapped) {
concat_chksum = SWAP_BYTES_IN_WORD(concat_chksum);
}
tcp_seg_add_chksum(concat_chksum, concat_chksummed, &last_unsent->chksum,
&last_unsent->chksum_swapped);
last_unsent->flags |= TF_SEG_DATA_CHECKSUMMED;
}
#endif /* TCP_CHECKSUM_ON_COPY */
/*
* Phase 3: 接入新的segment,如果前面两个步骤能装满本次发送的数据,就不需要当前segment,即是queue为空,接入也是无效的。
*/
if (last_unsent == NULL) {
pcb->unsent = queue;
} else {
last_unsent->next = queue;
}
更新PCB字段:
/* 更新PCB其它字段 */
/* 更新发送缓冲区 */
pcb->snd_lbb += len;
pcb->snd_buf -= len;
pcb->snd_queuelen = queuelen;
LWIP_DEBUGF(TCP_QLEN_DEBUG, ("tcp_write: %"S16_F" (after enqueued)\n",
pcb->snd_queuelen));
if (pcb->snd_queuelen != 0) {
LWIP_ASSERT("tcp_write: valid queue length",
pcb->unacked != NULL || pcb->unsent != NULL);
}
/* 如果没有设置TCP_WRITE_FLAG_MORE,即是本次报文是在应用层看来是一个完整的报文段,可以设置PSH标志位 */
if (seg != NULL && seg->tcphdr != NULL && ((apiflags & TCP_WRITE_FLAG_MORE) == 0)) {
TCPH_SET_FLAG(seg->tcphdr, TCP_PSH);
}
return ERR_OK; /* 写入TCP发送缓冲区成功 */
经过上述层层代码,就组装好需要发送的报文段并合法推送到TCP PCB中了。
发送报文报文段:tcp_output()
tcp_output()
函数是把发送缓冲区中的报文段数据发送到IP层。
err_t tcp_output(struct tcp_pcb *pcb)
:
-
struct tcp_pcb *pcb
:PCB。 -
返回:
ERR_OK
:执行正常。another err_t
:执行异常。
-
数据安全:如果PCB正在处理接收数据,这里不输出,直接返回。相当于给数据处理上锁。
- 因为在PCB处理完输入数据后,内核会触发调用当前
tcp_output()
函数,所以这里返回ERR_OK
即可。
- 因为在PCB处理完输入数据后,内核会触发调用当前
-
发送窗口:从发送窗口和拥塞窗口中取小的作为有效的发送窗口。
-
立即ACK:如果发送缓冲区中没有数据需要发送,且标记了
TF_ACK_NOW
,则先响应一个纯ACK回去先。然后return
。 -
如果发送缓冲区中有数据需要发送,先打印相关信息。继续:
-
匹配网卡:通过
tcp_route()
匹配出口网卡。匹配失败,也就发送失败。- 匹配逻辑:参考前面的IP章节。
-
确定TCP源IP:如果PCB还没绑定本地IP,则先绑定本地IP地址,因为组TCP包需要。
-
窗口溢出检查:如果当前报文段size超出当前发送窗口,则本地报文段暂不发送,且需要以下操作:
- 零窗口探查:如果对端通告过来的窗口不够大,又没有飞行数据,则需要开启坚持定时器,并按规则发送零窗口探查报文。
- 响应ACK:标记了
TF_ACK_NOW
,则响应一个纯ACK回去。
-
满足发送条件,关闭坚持定时器。
-
先从
pcb->unacked
找出空中数据的最后一个报文段,方便当前报文段发送出去后,直接接入空中数据队列中。 -
使用
while()
方式把窗口内能发送的数据循环发送出去。- 发送时经过nagle算法。如果nagle算法生效,则延迟发送,
break
退出。 - 调试:打印窗口信息。
- 调用
tcp_output_segment()
发送报文段。 - 发送成功后,且该报文段占用
SEQ
号,则把该报文段从pcb->unsent
迁入空中数据队列pcb->unacked
。并且清除该报文段的oversize
。如果不占用SEQ
号,直接释放接口。
- 发送时经过nagle算法。如果nagle算法生效,则延迟发送,
/**
* @ingroup tcp_raw
* Find out what we can send and send it
*
* @param pcb Protocol control block for the TCP connection to send data
* @return ERR_OK if data has been sent or nothing to send
* another err_t on error
*
* 能发啥就发啥,如发送ACK、发送缓冲区中的数据。
*
*/
err_t
tcp_output(struct tcp_pcb *pcb)
{
struct tcp_seg *seg, *useg;
u32_t wnd, snd_nxt;
err_t err;
struct netif *netif;
#if TCP_CWND_DEBUG
s16_t i = 0;
#endif /* TCP_CWND_DEBUG */
LWIP_ASSERT_CORE_LOCKED();
LWIP_ASSERT("tcp_output: invalid pcb", pcb != NULL);
/* pcb->state LISTEN not allowed here */
LWIP_ASSERT("don't call tcp_output for listen-pcbs",
pcb->state != LISTEN);
/* 如果PCB正在处理输入数据,这里不输出,直接返回。相当于给数据处理上锁。
在PCB处理完输入数据后,内核会触发调用当前tcp_output()函数,所以这里返回ERR_OK即可。 */
if (tcp_input_pcb == pcb) {
return ERR_OK;
}
/* 从发送窗口和拥塞窗口中取小的作为有效的发送窗口 */
wnd = LWIP_MIN(pcb->snd_wnd, pcb->cwnd);
/* 取出未发送报文队列 */
seg = pcb->unsent;
if (seg == NULL) {
LWIP_DEBUGF(TCP_OUTPUT_DEBUG, ("tcp_output: nothing to send (%p)\n",
(void *)pcb->unsent));
LWIP_DEBUGF(TCP_CWND_DEBUG, ("tcp_output: snd_wnd %"TCPWNDSIZE_F
", cwnd %"TCPWNDSIZE_F", wnd %"U32_F
", seg == NULL, ack %"U32_F"\n",
pcb->snd_wnd, pcb->cwnd, wnd, pcb->lastack));
/* 如果标记TF_ACK_NOW,即表示立即回ACK。如果没有数据,则构造一个纯粹的ACK发出去。 */
if (pcb->flags & TF_ACK_NOW) {
return tcp_send_empty_ack(pcb);
}
/* 如果不用发送,则退出 */
goto output_done;
} else {
/* 如果有数据需要发送,先打印相关信息 */
LWIP_DEBUGF(TCP_CWND_DEBUG,
("tcp_output: snd_wnd %"TCPWNDSIZE_F", cwnd %"TCPWNDSIZE_F", wnd %"U32_F
", effwnd %"U32_F", seq %"U32_F", ack %"U32_F"\n",
pcb->snd_wnd, pcb->cwnd, wnd,
lwip_ntohl(seg->tcphdr->seqno) - pcb->lastack + seg->len,
lwip_ntohl(seg->tcphdr->seqno), pcb->lastack));
}
/* 匹配出口网卡 */
netif = tcp_route(pcb, &pcb->local_ip, &pcb->remote_ip);
if (netif == NULL) {
/* 匹配不了网卡,就发不出去 */
return ERR_RTE;
}
/* 如果当前连接没有配置本地IP地址,就把匹配到的网卡的IP地址作为当前连接的本地IP地址。 */
if (ip_addr_isany(&pcb->local_ip)) {
const ip_addr_t *local_ip = ip_netif_get_local_ip(netif, &pcb->remote_ip);
if (local_ip == NULL) {
return ERR_RTE;
}
ip_addr_copy(pcb->local_ip, *local_ip);
}
/* 先处理当前报文段是否符合在窗口范围内 */
if (lwip_ntohl(seg->tcphdr->seqno) - pcb->lastack + seg->len > wnd) {
/* 本报文段超窗口范围了 */
/* 如果发送窗口比拥塞窗口小 && 没有飞行中的数据(就是后面可能不会自动触发发送数据) && 坚持定时器没有开启 */
/* 为了防止对端响应附带窗口信息的ACK丢失,这里需要开启坚持定时器,定期询问对方的接收窗口 */
if (wnd == pcb->snd_wnd && pcb->unacked == NULL && pcb->persist_backoff == 0) {
pcb->persist_cnt = 0;
pcb->persist_backoff = 1;
pcb->persist_probe = 0;
}
/* 如果标记了TF_ACK_NOW,则需要立即响应一个ACK */
if (pcb->flags & TF_ACK_NOW) {
return tcp_send_empty_ack(pcb);
}
goto output_done;
}
/* 窗口满足发送数据条件,关闭坚持定时器 */
pcb->persist_backoff = 0;
/* 找出已发送,但是还没收到ACK的最后一个报文段 */
useg = pcb->unacked;
if (useg != NULL) {
for (; useg->next != NULL; useg = useg->next);
}
/* 把在窗口内未发送的数据发出去 */
while (seg != NULL &&
lwip_ntohl(seg->tcphdr->seqno) - pcb->lastack + seg->len <= wnd) {
LWIP_ASSERT("RST not expected here!",
(TCPH_FLAGS(seg->tcphdr) & TCP_RST) == 0);
/* 如果nagle算法生效,则延迟发送。
* 打破nagle算法生效的条件(即是nagle生效,也要马上发送的条件)之一:
* - 如果之前调用tcp_write()时有内存错误未能成功发送,为了防止延迟ACK超时,需要立即发送。
* - 如果FIN已经在队列中了,则没必要再延迟发送了,立即把数据发出,加速闭环。
* 注意:SYN一直都是单独报文段的。所以要么不存在SYN,如存在未发送数据seg->next != NULL; 要么只存在SYN,即是还没有发送数据,如pcb->unacked == NULL;。
* 注意:RST是不会通过tcp_wirte()和tcp_output()发送的。
*/
if ((tcp_do_output_nagle(pcb) == 0) &&
((pcb->flags & (TF_NAGLEMEMERR | TF_FIN)) == 0)) {
/* nagle算法生效 && 上次发送内存正常 && 还没有FIN */
break;
}
#if TCP_CWND_DEBUG /* 窗口信息打印 */
LWIP_DEBUGF(TCP_CWND_DEBUG, ("tcp_output: snd_wnd %"TCPWNDSIZE_F", cwnd %"TCPWNDSIZE_F", wnd %"U32_F", effwnd %"U32_F", seq %"U32_F", ack %"U32_F", i %"S16_F"\n",
pcb->snd_wnd, pcb->cwnd, wnd,
lwip_ntohl(seg->tcphdr->seqno) + seg->len -
pcb->lastack,
lwip_ntohl(seg->tcphdr->seqno), pcb->lastack, i));
++i;
#endif /* TCP_CWND_DEBUG */
if (pcb->state != SYN_SENT) { /* 除了发送握手第一步的SYN外,其他报文段都需要携带ACK。 */
TCPH_SET_FLAG(seg->tcphdr, TCP_ACK);
}
/* 把报文段发送出去 */
err = tcp_output_segment(seg, pcb, netif);
if (err != ERR_OK) {
/* 发送失败,把PCB标记为TF_NAGLEMEMERR,下次遇到nagle算法时,不要延迟,需要立即 */
tcp_set_flags(pcb, TF_NAGLEMEMERR);
return err;
}
/* 到这里,发送成功 */
#if TCP_OVERSIZE_DBGCHECK
seg->oversize_left = 0; /* 清空报文段溢出空间字段。表示不允许后续报文并入当前报文段了。 */
#endif /* TCP_OVERSIZE_DBGCHECK */
/* 把已发送的报文段从未发送队列中移除 */
pcb->unsent = seg->next;
if (pcb->state != SYN_SENT) { /* 因为SYN_SENT的SYN包不含ACK,无需清空 */
/* 清空ACK标志位 */
tcp_clear_flags(pcb, TF_ACK_DELAY | TF_ACK_NOW);
}
/* 更新下一个需要发送的seq号 */
snd_nxt = lwip_ntohl(seg->tcphdr->seqno) + TCP_TCPLEN(seg);
if (TCP_SEQ_LT(pcb->snd_nxt, snd_nxt)) {
pcb->snd_nxt = snd_nxt;
}
/* 如果发送的报文段中包含TCP数据(占用seq号),则需要把该报文段插入pcb->unacked队列:按seq号升序插入 */
if (TCP_TCPLEN(seg) > 0) {
seg->next = NULL;
/* unacked list is empty? */
if (pcb->unacked == NULL) {
pcb->unacked = seg;
useg = seg;
/* unacked list is not empty? */
} else {
/* In the case of fast retransmit, the packet should not go to the tail
* of the unacked queue, but rather somewhere before it. We need to check for
* this case. -STJ Jul 27, 2004 */
if (TCP_SEQ_LT(lwip_ntohl(seg->tcphdr->seqno), lwip_ntohl(useg->tcphdr->seqno))) {
/* 如果是重传的报文段,入队时也需要按序入队 */
/* add segment to before tail of unacked list, keeping the list sorted */
struct tcp_seg **cur_seg = &(pcb->unacked);
while (*cur_seg &&
TCP_SEQ_LT(lwip_ntohl((*cur_seg)->tcphdr->seqno), lwip_ntohl(seg->tcphdr->seqno))) {
cur_seg = &((*cur_seg)->next );
}
seg->next = (*cur_seg);
(*cur_seg) = seg;
} else {
/* add segment to tail of unacked list */
useg->next = seg;
useg = useg->next;
}
}
} else {
/* 如果当前报文段不含TCP数据(不占用seq号),直接释放。注意:SYN和FIN会占用一个seq号 */
tcp_seg_free(seg);
}
seg = pcb->unsent;
}
#if TCP_OVERSIZE
if (pcb->unsent == NULL) {
/* last unsent has been removed, reset unsent_oversize */
pcb->unsent_oversize = 0;
}
#endif /* TCP_OVERSIZE */
output_done:
tcp_clear_flags(pcb, TF_NAGLEMEMERR);
return ERR_OK;
}
TCP接收数据
接收数据的处理更加复杂,这部分需要对着本笔记自己认真分析源码。
TCP报文输入数据流:ip_input()
-->tcp_input()
-->tcp_process()
-->tcp_receive()
。
tcp_input()
:主要是层级处理,从IP层获取TCP报文,进行简单的分析,然后传给tcp_process()
做TCP协议算法处理。等待TCP协议算法处理完毕后,把收到的数据通过上层注册到TCP PCB的接收回调函数递交到上层,然后释放相关资源。
tcp_process()
:根据TCP状态机处理接收到的TCP报文段。如果需要接纳当前TCP报文段,可通过tcp_receive()
函数实现。
tcp_receive()
:该函数就是处理接收TCP报文的协议算法核心。内含窗口更新、RTT&RTO计算、报文段插入接收缓冲区、处理ACK
的空中数据等等。
TCP入口处理:tcp_input()
tcp_input()
该函数是被IP层调用的,使用该函数的主要作用是接收TCP数据,组装TCP报文段,匹配PCB,传入TCP更深层处理。
void tcp_input(struct pbuf *p, struct netif *inp)
:
struct pbuf *p
:收到的pbuf。struct netif *inp
:接收到该pbuf的网卡。
TCP报文合法性检查
TCP报文长度检查:
/* pbuf长度检查 */
if (p->len < TCP_HLEN) {
/* drop short packets */
TCP_STATS_INC(tcp.lenerr);
goto dropped;
}
/* 检查TCP首部长度字段 */
hdrlen_bytes = TCPH_HDRLEN_BYTES(tcphdr);
if ((hdrlen_bytes < TCP_HLEN) || (hdrlen_bytes > p->tot_len)) {
TCP_STATS_INC(tcp.lenerr);
goto dropped;
}
不处理广播包、组播包的TCP报文:
/* 不处理广播包、组播包的TCP报文 */
if (ip_addr_isbroadcast(ip_current_dest_addr(), ip_current_netif()) ||
ip_addr_ismulticast(ip_current_dest_addr())) {
TCP_STATS_INC(tcp.proterr);
goto dropped;
}
校验和:
#if CHECKSUM_CHECK_TCP
IF__NETIF_CHECKSUM_ENABLED(inp, NETIF_CHECKSUM_CHECK_TCP) {
/* Verify TCP checksum. */
/* TCP校验和字段校验。注意:包括TCP伪首部 */
u16_t chksum = ip_chksum_pseudo(p, IP_PROTO_TCP, p->tot_len,
ip_current_src_addr(), ip_current_dest_addr());
if (chksum != 0) {
tcp_debug_print(tcphdr);
TCP_STATS_INC(tcp.chkerr);
goto dropped;
}
}
#endif /* CHECKSUM_CHECK_TCP */
提取TCP首部
从IP层获取的数据pbuf的payload是指向TCP首部的,来的TCP层,我们应该提取TCP首部,然后payload指向数据区,隐藏TCP首部。
获取TCP首部指针和长度,pbuf隐藏TCP首部:
- 这里需要注意的是,TCP首部可能分布在两个buf中,所以需要特殊处理。
- lwip目前只支持TCP首部(不包括选项字段)那部分必须都在同一个pbuf中。
/* 提取TCP首部和选项字段后,pbuf payload偏移到TCP数据区 */
/* 获取选项字段长度 */
tcphdr_optlen = (u16_t)(hdrlen_bytes - TCP_HLEN);
tcphdr_opt2 = NULL;
if (p->len >= hdrlen_bytes) {
/* 所有选项字段都在第一个pbuf中 */
tcphdr_opt1len = tcphdr_optlen;
pbuf_remove_header(p, hdrlen_bytes); /* cannot fail */
} else {
u16_t opt2len;
/* TCP首部是全都在第一个pbuf中的,而选项字段不一定 */
/* 先偏移到第一个选项字段 */
pbuf_remove_header(p, TCP_HLEN);
tcphdr_opt1len = p->len; /* 选项字段在当前pbuf的长度 */
opt2len = (u16_t)(tcphdr_optlen - tcphdr_opt1len); /* 选项字段在其他pbuf的长度 */
/* 当前pbuf不含TCP数据区,所以全部隐藏 */
pbuf_remove_header(p, tcphdr_opt1len);
/* 如果选项字段超出了第二个pbuf,lwip不支持 */
if (opt2len > p->next->len) {
/* drop short packets */
TCP_STATS_INC(tcp.lenerr);
goto dropped;
}
/* 获取TCP选项字段在第二个pbuf中的位置 */
tcphdr_opt2 = (u8_t *)p->next->payload;
/* 隐藏TCP首部,使pbuf payload指向TCP数据区 */
pbuf_remove_header(p->next, opt2len);
p->tot_len = (u16_t)(p->tot_len - opt2len);
}
pbuf隐藏TCP首部成功后,把TCP首部信息提取到全局参数中,这些参数作为TCP本次输入处理的重要数据:
/* 提取TCP首部刚刚字段,并转换为主机字节序 */
tcphdr->src = lwip_ntohs(tcphdr->src);
tcphdr->dest = lwip_ntohs(tcphdr->dest);
seqno = tcphdr->seqno = lwip_ntohl(tcphdr->seqno);
ackno = tcphdr->ackno = lwip_ntohl(tcphdr->ackno);
tcphdr->wnd = lwip_ntohs(tcphdr->wnd);
flags = TCPH_FLAGS(tcphdr);
tcplen = p->tot_len;
if (flags & (TCP_FIN | TCP_SYN)) {
/* SYN和FIN占用一个seq号,需要ACK,计入TCP报文数据长度 */
tcplen++;
if (tcplen < p->tot_len) {
/* TCP报文数据区长度溢出的话,不处理当前TCP报文 */
TCP_STATS_INC(tcp.lenerr);
goto dropped;
}
}
网卡匹配优先级&快速遍历算法导读
匹配TCP PCB顺序:稳定态PCB、TIME_WAIT态PCB、监听态PCB。
匹配成功后,表示PCB活跃,将活跃的移到对应状态链表最前面,方便下次遍历。
PCB匹配之稳定态tcp_active_pcbs
检查完从IP层传入的TCP报文是协议合法的数据包,且提取、准备好TCP首部各字段数据后,我们就要根据这些数据进行匹配PCB,把当前TCP报文给到对应的IP&PORT应用服务中。
先从tcp_active_pcbs
,因为该链表记录的是可收发数据的链接:
-
接收到当前TCP报文的网卡 与 PCB绑定的网卡进行匹配。
-
IP&PORT服务匹配:
- 报文源IP 和 PCB远端IP;
- 报文源端口 和 PCB远端端口;
- 报文远端IP 和 PCB源IP;
- 报文远端端口 和 PCB源端口。
/* 遍历稳定态的TCP BCP链表,让TCP报文匹配。支持多路复用。 */
for (pcb = tcp_active_pcbs; pcb != NULL; pcb = pcb->next) {
/* 网卡匹配:检查当前TCP PCB绑定的网卡和接收TCP报文的网卡是否一致 */
if ((pcb->netif_idx != NETIF_NO_INDEX) &&
(pcb->netif_idx != netif_get_index(ip_data.current_input_netif))) {
prev = pcb;
continue;
}
/* 服务匹配:TCP报文 和 TCP PCB
- 报文源IP 和 PCB远端IP
- 报文源端口 和 PCB远端端口
- 报文远端IP 和 PCB源IP
- 报文远端端口 和 PCB源端口 */
if (pcb->remote_port == tcphdr->src &&
pcb->local_port == tcphdr->dest &&
ip_addr_eq(&pcb->remote_ip, ip_current_src_addr()) &&
ip_addr_eq(&pcb->local_ip, ip_current_dest_addr())) {
/* 匹配成功后,把当前PCB拉到最前,紧接着有数据进来后能快速遍历 */
if (prev != NULL) {
prev->next = pcb->next;
pcb->next = tcp_active_pcbs;
tcp_active_pcbs = pcb;
} else {
TCP_STATS_INC(tcp.cachehit);
}
break;
}
/* 未能匹配成功,遍历下一个TCP PCB */
prev = pcb;
}
PCB匹配之TIME_WAIT态tcp_tw_pcbs
如果稳定态的TCP PCB匹配失败,则去遍历匹配TIME_WAIT状态的TCP PCB。
TIME_WAIT态的PCB也可能接收到TCP报文的,如:
- 挥手时,最后一次挥手
ACK
没有到达对端,所以对端重发了FIN
过来。 - 也可能是挥手前发送的TCP报文迟来了。
tcp_tw_pcbs
链表记录的是TIME_WAIT状态的TCP:
-
网卡匹配。
-
IP&PORT服务匹配。
- 报文源IP 和 PCB远端IP;
- 报文源端口 和 PCB远端端口;
- 报文远端IP 和 PCB源IP;
- 报文远端端口 和 PCB源端口。
/* 如果稳定态的TCP PCB匹配失败,则去遍历匹配TIME_WAIT状态的TCP PCB */
for (pcb = tcp_tw_pcbs; pcb != NULL; pcb = pcb->next) {
/* 网卡匹配 */
if ((pcb->netif_idx != NETIF_NO_INDEX) &&
(pcb->netif_idx != netif_get_index(ip_data.current_input_netif))) {
continue;
}
/* 服务匹配 */
if (pcb->remote_port == tcphdr->src &&
pcb->local_port == tcphdr->dest &&
ip_addr_eq(&pcb->remote_ip, ip_current_src_addr()) &&
ip_addr_eq(&pcb->local_ip, ip_current_dest_addr())) {
#ifdef LWIP_HOOK_TCP_INPACKET_PCB
/* 钩子函数:匹配成功后优先传入钩子函数处理 */
if (LWIP_HOOK_TCP_INPACKET_PCB(pcb, tcphdr, tcphdr_optlen, tcphdr_opt1len,
tcphdr_opt2, p) == ERR_OK)
#endif
{
/* 由该函数处理 */
tcp_timewait_input(pcb);
}
/* 释放当前TCP报文 */
pbuf_free(p);
return;
}
PCB匹配之监听态tcp_listen_pcbs
如果前面都没有匹配成功,那就需要检查是否是客户端的握手请求报文。
tcp_listen_pcbs.listen_pcbs
记录的是能接收连接的TCP服务端:
-
网卡匹配。
-
IP&PORT服务匹配。因为监听态的PCB是监听新接入的连接,这些连接在我们本地还不知道其IP&PORT,所以我们只需要匹配他们接入的目标服务即可:
- 报文远端IP 和 PCB源IP;
- 报文远端端口 和 PCB源端口。
/* 依然没有匹配成功,则匹配监听态的TCP PCB。检查是否是有TCP客户端接入 */
prev = NULL;
for (lpcb = tcp_listen_pcbs.listen_pcbs; lpcb != NULL; lpcb = lpcb->next) {
/* 网卡匹配 */
if ((lpcb->netif_idx != NETIF_NO_INDEX) &&
(lpcb->netif_idx != netif_get_index(ip_data.current_input_netif))) {
prev = (struct tcp_pcb *)lpcb;
continue;
}
/* 服务匹配:
- 报文远端IP 和PCB源IP (注意任意IP)
- 报文远端端口 和 PCB源端口 */
if (lpcb->local_port == tcphdr->dest) {
if (IP_IS_ANY_TYPE_VAL(lpcb->local_ip)) {
/* 匹配:PCB为任意版本IP,匹配成功 */
#if SO_REUSE /* 支持端口复用功能 */
lpcb_any = lpcb;
lpcb_prev = prev;
#else /* SO_REUSE */
break;
#endif /* SO_REUSE */
} else if (IP_ADDR_PCB_VERSION_MATCH_EXACT(lpcb, ip_current_dest_addr())) {
if (ip_addr_eq(&lpcb->local_ip, ip_current_dest_addr())) {
/* 精确匹配成功:IP一致 */
break;
} else if (ip_addr_isany(&lpcb->local_ip)) {
/* 匹配:IP版本匹配 + PCB还是任意IP */
#if SO_REUSE /* 支持端口复用功能 */
lpcb_any = lpcb;
lpcb_prev = prev;
#else /* SO_REUSE */
break;
#endif /* SO_REUSE */
}
}
}
/* 匹配失败,遍历下一个PCB */
prev = (struct tcp_pcb *)lpcb;
}
监听链表匹配成功后,就是要接入我们的TCP服务端,传入tcp_listen_input()
监听处理即可,处理完就退出:
tcp_listen_input()
函数处理我们在后面章节分析。
if (lpcb != NULL) {
/* 在监听态链表中匹配到PCB的话,将其移到最前面,以便下次遍历 */
if (prev != NULL) {
((struct tcp_pcb_listen *)prev)->next = lpcb->next;
/* our successor is the remainder of the listening list */
lpcb->next = tcp_listen_pcbs.listen_pcbs;
/* put this listening pcb at the head of the listening list */
tcp_listen_pcbs.listen_pcbs = lpcb;
} else {
TCP_STATS_INC(tcp.cachehit);
}
LWIP_DEBUGF(TCP_INPUT_DEBUG, ("tcp_input: packed for LISTENing connection.\n"));
#ifdef LWIP_HOOK_TCP_INPACKET_PCB
/* 钩子函数:匹配成功后优先传入钩子函数处理 */
if (LWIP_HOOK_TCP_INPACKET_PCB((struct tcp_pcb *)lpcb, tcphdr, tcphdr_optlen,
tcphdr_opt1len, tcphdr_opt2, p) == ERR_OK)
#endif
{
/* 传入监听listen处理 */
tcp_listen_input(lpcb);
}
/* 当前pbuf处理完毕,释放 */
pbuf_free(p);
return;
}
传入钩子处理
稳定态匹配成功后才会来到这。
在TCP处理当前收到的TCP报文前,先传入对应钩子函数,在回来TCP处理:
- 用户可以在这钩子函数里实现自己的一些特性,比如入站记录,TCP防火墙等等。
#ifdef LWIP_HOOK_TCP_INPACKET_PCB
/* 钩子函数:匹配成功后优先传入钩子函数处理 */
if ((pcb != NULL) && LWIP_HOOK_TCP_INPACKET_PCB(pcb, tcphdr, tcphdr_optlen,
tcphdr_opt1len, tcphdr_opt2, p) != ERR_OK) {
pbuf_free(p);
return;
}
#endif
处理TCP报文段
稳定态匹配成功后,钩子函数没有过滤掉这次收到的TCP报文段,我们就需要进入TCP协议算法处理该报文:
构建segment数据结构:
/* 构建tcp_seg数据结构 */
inseg.next = NULL;
inseg.len = p->tot_len;
inseg.p = p;
inseg.tcphdr = tcphdr;
/* 初始化相关全局变量 */
recv_data = NULL;
recv_flags = 0;
recv_acked = 0;
检查PSH
标志位:
/* 检查PSH标志位 */
if (flags & TCP_PSH) {
p->flags |= PBUF_FLAG_PUSH;
}
如果当前TCP接收缓冲区中还有数据没有被应用层读取,先尝试递交到上层先:
- 调用
tcp_process_refused_data()
实现。 - (如果检查发现这个连接已经被关闭了) 或 (上层没有把缓冲区数据全部读走 && 当前收到的报文包含数据) 就忽略当前接收到的TCP报文段。如果接收通告窗口为0,还需要响应一个纯
ACK
到对端。
/* 如果PCB的接收缓冲区pcb->refused_data还有数据,则先尝试递交到上层 */
if (pcb->refused_data != NULL) {
if ((tcp_process_refused_data(pcb) == ERR_ABRT) ||
((pcb->refused_data != NULL) && (tcplen > 0))) {
/* (PCB已经被终止处理) || (上层没有把缓冲区数据全部读走 && 当前收到的报文包含数据) 跳到aborted */
if (pcb->rcv_ann_wnd == 0) {
/* 发送一个纯ACK,通告我方接收窗口为0 */
tcp_send_empty_ack(pcb);
}
TCP_STATS_INC(tcp.drop);
MIB2_STATS_INC(mib2.tcpinerrs);
goto aborted;
}
}
传给tcp_process()
处理:
- 该函数会根据状态机处理,然后也会调用
tcp_receive()
函数。
/* 锁定当前PCB,处理接收到的TCP报文 */
tcp_input_pcb = pcb;
err = tcp_process(pcb);
根据处理结果进行收尾操作:
-
TF_RESET
:收到RST,就要释放当前TCP PCB。 -
如果本地收到的TCP报文段
ACK
了更多的空中数据,那么本地发送缓冲区就会释放一些被ACK了的数据空间,所以我们需要调用上层注册的可写回调函数pcb->sent()
,通知上层,当前TCP发送缓冲区可写。 -
调用上层注册的接收回调函数
pcb->recv()
,把接收到的数据递交到上层。- 如果应用层没有接收这些数据,但又不是终止连接的情况下,把这些数据缓存到
pcb->refused_data
。
- 如果应用层没有接收这些数据,但又不是终止连接的情况下,把这些数据缓存到
-
TF_CLOSED
:如果收到FIN,并且挥手完成,则释放当前TCP PCB。 -
处理完接收到的TCP报文段后,还需要调用
tcp_output()
触发下发送数据的业务。发送缓冲区中的报文段,如果有ACK
响应需求,也能立即响应。 -
注意:上述的数据size如果在开启了窗口扩大系数后
LWIP_WND_SCALE
或乱序报文TCP_QUEUE_OOSEQ
后,需要在16bit溢出内循环处理,即是每次处理不能超时16bit的size,如果总报文段超过了,就需要while()循环处理。
/* 如果err为ERR_ABRT,表示tcp_abort()被调用了,并且TCP PCB已经被free了,这样的话,应该do nothing。 */
if (err != ERR_ABRT) {
if (recv_flags & TF_RESET) {
/* TF_RESET意味着当前连接已经被对端RST了。
我们应该先回调一个ERR_RST到应用层,然后再释放PCB。 */
TCP_EVENT_ERR(pcb->state, pcb->errf, pcb->callback_arg, ERR_RST);
tcp_pcb_remove(&tcp_active_pcbs, pcb);
tcp_free(pcb);
} else {
err = ERR_OK;
/* 在这里,如果应用层注册了->sent()回调函数,当发送缓冲区有新的可用空间时,调用->sent()这个回调函数。 */
if (recv_acked > 0) {
/* 对端ACK了一些数据,那么本地发送缓冲区就会释放一些被ACK了的数据空间 */
u16_t acked16;
#if LWIP_WND_SCALE /* WSOPT窗口扩大 */
/* 如果开了窗口扩大功能,那发送窗口可能会超过u16_t,对端ACK的量也可能超过u16_t。
但是sent()回调函数中的acked参数是u16_t,所以需要分多次处理。 */
u32_t acked = recv_acked;
while (acked > 0) { /* 循环处理 */
acked16 = (u16_t)LWIP_MIN(acked, 0xffffu); /* 限溢出 */
acked -= acked16; /* 本次循环处理的量 */
#else
{
acked16 = recv_acked; /* 没有开启WSOPT窗口扩大功能,则直接赋值,因为两者都是u16_t */
#endif
TCP_EVENT_SENT(pcb, (u16_t)acked16, err); /* 调用->sent()回调 */
if (err == ERR_ABRT) { /* ERR_ABRT说明当前PCB异常,需要释放当前连接及其资源 */
goto aborted;
}
}
recv_acked = 0; /* 处理完毕,清空ACK量 */
}
/* 如果TF_CLOSED被标记,则释放当前PCB资源 */
if (tcp_input_delayed_close(pcb)) {
goto aborted;
}
#if TCP_QUEUE_OOSEQ && LWIP_WND_SCALE
/* [lzm][lab][ooseq][220927][1020] */
/* 开了OOSEQ && 开了窗口扩大系数 recv_data 拼接上ooseq数据后可能会超过64K(u16_t),recv_data->tot_len也可能已经溢出了,
但是不用担心,pbuf_split_64k()能避开这个溢出。 */
while (recv_data != NULL) { /* 循环提取,每次提取不超过64K */
struct pbuf *rest = NULL;
pbuf_split_64k(recv_data, &rest); /* 截断,限制pbuf->payload数据不超过64K */
#else /* TCP_QUEUE_OOSEQ && LWIP_WND_SCALE */
/* 如果没有OOSEQ,pbuf->tot_len是不会溢出的,因为底层创建时就限制在u16_t */
/* 如果没有窗口扩大系数,pbuf->tot_len也是不会溢出的,因为窗口限制就在u16_t */
if (recv_data != NULL) {
#endif /* TCP_QUEUE_OOSEQ && LWIP_WND_SCALE */
LWIP_ASSERT("pcb->refused_data == NULL", pcb->refused_data == NULL);
/* 检查RX */
if (pcb->flags & TF_RXCLOSED) {
/* 如果RX已经被应用层关闭了,还收到对端新发来的数据,则直接响应RST,关闭当前连接 */
/* 释放这些数据 */
pbuf_free(recv_data);
#if TCP_QUEUE_OOSEQ && LWIP_WND_SCALE
if (rest != NULL) {
pbuf_free(rest);
}
#endif /* TCP_QUEUE_OOSEQ && LWIP_WND_SCALE */
tcp_abort(pcb); /* RST,终止当前连接 */
goto aborted;
}
/* 调用->recv()回调通知应用层接收数据 */
TCP_EVENT_RECV(pcb, recv_data, ERR_OK, err);
if (err == ERR_ABRT) {
#if TCP_QUEUE_OOSEQ && LWIP_WND_SCALE
if (rest != NULL) { /* 终止连接,释放未递交到应用层的数据 */
pbuf_free(rest);
}
#endif /* TCP_QUEUE_OOSEQ && LWIP_WND_SCALE */
goto aborted;
}
/* 如果应用层没有接收这些数据,但又不是终止连接的情况下,把这些数据缓存到pcb->refused_data */
if (err != ERR_OK) {
#if TCP_QUEUE_OOSEQ && LWIP_WND_SCALE
if (rest != NULL) {
pbuf_cat(recv_data, rest);
}
#endif /* TCP_QUEUE_OOSEQ && LWIP_WND_SCALE */
pcb->refused_data = recv_data;
LWIP_DEBUGF(TCP_INPUT_DEBUG, ("tcp_input: keep incoming packet, because pcb is \"full\"\n"));
#if TCP_QUEUE_OOSEQ && LWIP_WND_SCALE
break; /* 退出遍历 */
} else {
/* 如果递交到应用层成功,则循环处理剩余数据 */
recv_data = rest;
#endif /* TCP_QUEUE_OOSEQ && LWIP_WND_SCALE */
}
} /* 递交到应用层处理完毕 */
/* 如果收到了FIN,数据都已经全部递交到应用层了,就调用->recv(),buffer参数为NULL,回调表示需要关闭当前连接(EOF)。
数据还没有全部递交到应用层,就在这些数据的pbuf上标记收到FIN。 */
if (recv_flags & TF_GOT_FIN) {
if (pcb->refused_data != NULL) {
/* 还有数据未递交到应用层,在这些数据标记FIN即可 */
pcb->refused_data->flags |= PBUF_FLAG_TCP_FIN;
} else {
/* 如果全部数据都递交到应用层了,就回调到应用层,需要close()当前连接 */
if (pcb->rcv_wnd != TCP_WND_MAX(pcb)) {
pcb->rcv_wnd++; /* FIN也占用一个SEQ,我们接收处理了这个FIN,接收窗口要+1。(应用层只管TCP数据) */
}
TCP_EVENT_CLOSED(pcb, err);
if (err == ERR_ABRT) {
goto aborted;
}
}
}
tcp_input_pcb = NULL; /* 当前收到的报文段处理完毕,释放全局变量资源 */
/* 如果TF_CLOSED被标记,则释放当前PCB资源 */
if (tcp_input_delayed_close(pcb)) {
goto aborted;
}
/* 触发下发送数据的业务。发送缓冲区中的报文段 */
tcp_output(pcb);
#if TCP_INPUT_DEBUG
#if TCP_DEBUG
/* 调试LOG */
tcp_debug_print_state(pcb->state);
#endif /* TCP_DEBUG */
#endif /* TCP_INPUT_DEBUG */
}
}
TCP报文段协议处理:tcp_process()
TCP报文输入数据流:ip_input()
-->tcp_input()
-->tcp_process()
-->tcp_receive()
。
来到tcp_process()
,就是按状态机处理。
大概的处理步骤就是:
- 先判断是否是RST报文,如果是合法的RST报文,就复位连接。如果不是合法RST,就忽略不处理。
- 如果是SYN握手包,判断是否是正常状态下的SYN包。因为只有SYN_SENT状态下才会收到,其它状态要么是重传要么是异常的。
- 重置保活超时计时器、坚持计时器及其它相关的定时字段。
- 解析&处理TCP首部选项字段。
- 根据各种TCP状态,合法处理接收到的报文。
各种TCP状态下的处理都可能不同,需要看细节的可以分析源码。
我这里贴出本人分析的源码,需要的自己看,后面会重点分析,我们接纳的TCP包的处理tcp_receive()
。
static err_t
tcp_process(struct tcp_pcb *pcb)
{
struct tcp_seg *rseg;
u8_t acceptable = 0;
err_t err;
err = ERR_OK;
LWIP_ASSERT("tcp_process: invalid pcb", pcb != NULL);
/* 收到RST报文,根据当前状态判断RST报文是否合法,若合法,则复位当前连接 */
if (flags & TCP_RST) {
/* 1. 先判断RST的合法性。如果RST是按TCP有序响应给我们的,视为合法。 */
if (pcb->state == SYN_SENT) {
/* 情况1:处于SYN_SENT状态下。报文的ACK号 == 下一个准备发送的SEQ号即可 */
/* 发出SYN后,收到对端的ACK,含RST,我们需要复位当前连接 */
if (ackno == pcb->snd_nxt) {
acceptable = 1;
}
} else {
/* 情况2:处于其它状态下。报文的SEQ号 == 期待接收到的下一个SEQ号即可
注意:如果报文的SEQ号不是我们期待的下一个SEQ号,但却在窗口内,只做ACK响应。(防止RST欺骗) */
if (seqno == pcb->rcv_nxt) {
/* RST确实是给我们的,复位连接 */
acceptable = 1;
} else if (TCP_SEQ_BETWEEN(seqno, pcb->rcv_nxt,
pcb->rcv_nxt + pcb->rcv_wnd)) {
/* 报文的SEQ不是我们下一个期待的,但在窗口内,我们响应一个ACK回去,让其重发RST,直至SEQ对应。 */
/* 参考RFC 5961 section 3.2节,解决了RFC 793 RST处理中存在的CVE-2004-0230 (RST欺骗攻击)问题。 */
tcp_ack_now(pcb);
}
}
/* 2. 根据RST的合法性再进行对应处理 */
if (acceptable) {
LWIP_DEBUGF(TCP_INPUT_DEBUG, ("tcp_process: Connection RESET\n"));
LWIP_ASSERT("tcp_input: pcb->state != CLOSED", pcb->state != CLOSED);
/* 报文处理结果设置复位标志,tcp_input()将会删除该PCB */
recv_flags |= TF_RESET;
/* 清除延迟ACK标志,加速闭环 */
tcp_clear_flags(pcb, TF_ACK_DELAY);
return ERR_RST;
} else {
/* 复位报文不合法,直接返回,不对报文做处理 */
LWIP_DEBUGF(TCP_INPUT_DEBUG, ("tcp_process: unacceptable reset seqno %"U32_F" rcv_nxt %"U32_F"\n",
seqno, pcb->rcv_nxt));
LWIP_DEBUGF(TCP_DEBUG, ("tcp_process: unacceptable reset seqno %"U32_F" rcv_nxt %"U32_F"\n",
seqno, pcb->rcv_nxt));
return ERR_OK;
}
}
/* 收到SYN报文。如果连接已经建立了(即是不在握手阶段)收到SYN报文,
则该报文可能是一个重传的握手包,我们只需要响应ACK即可。 */
if ((flags & TCP_SYN) && (pcb->state != SYN_SENT && pcb->state != SYN_RCVD)) {
tcp_ack_now(pcb);
return ERR_OK;
}
if ((pcb->flags & TF_RXCLOSED) == 0) {
pcb->tmr = tcp_ticks; /* 如果TCP的RX还没被关闭,则收到合法报文时,需要刷新这个活动计时器值 */
}
pcb->keep_cnt_sent = 0; /* 保活计数器清零 */
pcb->persist_probe = 0; /* 坚持计时器清零 */
tcp_parseopt(pcb); /* 处理报文中的选项字段 */
/* SYN_SENT和SYN_RCVD这两种情况下才对SYN报文做更多协议响应 */
if (flags & TCP_SYN) {
/* accept SYN only in 2 states: */
if ((pcb->state != SYN_SENT) && (pcb->state != SYN_RCVD)) {
return ERR_OK;
}
}
/* 根据PCB不同状态进行不同处理 */
switch (pcb->state) {
case SYN_SENT:
LWIP_DEBUGF(TCP_INPUT_DEBUG, ("SYN-SENT: ackno %"U32_F" pcb->snd_nxt %"U32_F" unacked %s %"U32_F"\n",
ackno, pcb->snd_nxt, pcb->unacked ? "" : " empty:",
pcb->unacked ? lwip_ntohl(pcb->unacked->tcphdr->seqno) : 0));
/* SYN_SENT状态下收到SYN|ACK,响应ACK后,本地建立连接成功。
更新PCB相关字段,更新PCB状态:SYN_SENT --> ESTABLISHED */
if ((flags & TCP_ACK) && (flags & TCP_SYN)
&& (ackno == pcb->lastack + 1)) {
/* 注意:SYN和FIN占用一个SEQ号 */
pcb->rcv_nxt = seqno + 1; /* 更新下一个期待的SEQ号 */
pcb->rcv_ann_right_edge = pcb->rcv_nxt; /* 初始化窗口通告值右边界 */
pcb->lastack = ackno; /* 收到被确认的最大ACK号 */
pcb->snd_wnd = tcphdr->wnd; /* 发送窗口更新为收到对端的窗口通告值 */
pcb->snd_wnd_max = pcb->snd_wnd; /* 最大发送窗口,初始更新为发送窗口 */
pcb->snd_wl1 = seqno - 1; /* 上次更新发送窗口时收到的SEQ号,强制为seqno-1(比对端初始的seq号还小)这样tcp_receive()会强制更新发送窗口 */ /* initialise to seqno - 1 to force window update */
pcb->state = ESTABLISHED; /* 更新PCB状态 */
#if TCP_CALCULATE_EFF_SEND_MSS
/* 结合网卡MTU和现有的MSS,选择出更合适的MSS */
pcb->mss = tcp_eff_send_mss(pcb->mss, &pcb->local_ip, &pcb->remote_ip);
#endif /* TCP_CALCULATE_EFF_SEND_MSS */
/* 计算初始拥塞窗口 */
pcb->cwnd = LWIP_TCP_CALC_INITIAL_CWND(pcb->mss);
LWIP_DEBUGF(TCP_CWND_DEBUG, ("tcp_process (SENT): cwnd %"TCPWNDSIZE_F
" ssthresh %"TCPWNDSIZE_F"\n",
pcb->cwnd, pcb->ssthresh));
LWIP_ASSERT("pcb->snd_queuelen > 0", (pcb->snd_queuelen > 0));
/* SYNi的报文被ACK了,发送缓冲现有占用的pbuf数也就-1;删除SYNi报文段资源。 */
--pcb->snd_queuelen;
LWIP_DEBUGF(TCP_QLEN_DEBUG, ("tcp_process: SYN-SENT --queuelen %"TCPWNDSIZE_F"\n", (tcpwnd_size_t)pcb->snd_queuelen));
rseg = pcb->unacked;
if (rseg == NULL) {
/* 如果pcb->unacked没有报文段,那可能是发生了重传,导致SYNi的报文迁回了pcb->unsent队列。
这样直接提取pcb->unsent队列头的报文段出来删除掉即可。 */
rseg = pcb->unsent;
LWIP_ASSERT("no segment to free", rseg != NULL);
pcb->unsent = rseg->next;
} else {
/* 把SYNi报文段迁出队列 */
pcb->unacked = rseg->next;
}
/* 释放SYNi报文段资源 */
tcp_seg_free(rseg);
/* 如果pcb->unacked没有报文段,重传计时器要停止;
如果有未被ACK的报文段,则重新开始计时,因为连接现在才建立。 */
if (pcb->unacked == NULL) {
pcb->rtime = -1;
} else {
pcb->rtime = 0;
pcb->nrtx = 0;
}
/* 调用用户配置的connect()回到函数,返回连接成功 */
TCP_EVENT_CONNECTED(pcb, ERR_OK, err);
if (err == ERR_ABRT) {
return ERR_ABRT;
}
/* 立即为这个SYNr报文段响应一个ACK */
tcp_ack_now(pcb);
}
/* 只收到ACK,对这个ACK响应RST,并立即重传SYN请求。因为LWIP不支持半打开。 */
else if (flags & TCP_ACK) {
/* 先发送RST,使对端处于非SYN状态 */
tcp_rst(pcb, ackno, seqno + tcplen, ip_current_dest_addr(),
ip_current_src_addr(), tcphdr->dest, tcphdr->src);
/* 然后立即重传SYN请求,不等待RTO */
if (pcb->nrtx < TCP_SYNMAXRTX) {
/* 重新计时 */
pcb->rtime = 0;
/* 立即重传 */
tcp_rexmit_rto(pcb);
}
}
break;
case SYN_RCVD:
if (flags & TCP_SYN) {
if (seqno == pcb->rcv_nxt - 1) {
/* 对端重传的SYN到达,可能是对端没有收到我们的SYN|ACK,所以重发SYN|ACK就好了 */
tcp_rexmit(pcb);
}
} else if (flags & TCP_ACK) {
/* expected ACK number? */
if (TCP_SEQ_BETWEEN(ackno, pcb->lastack + 1, pcb->snd_nxt)) {
/* 收到我们发出的SYN|ACK报文的确认ACK,PCB状态更新:SYN_RCVD --> ESTABLISHED */
pcb->state = ESTABLISHED;
LWIP_DEBUGF(TCP_DEBUG, ("TCP connection established %"U16_F" -> %"U16_F".\n", inseg.tcphdr->src, inseg.tcphdr->dest));
#if LWIP_CALLBACK_API || TCP_LISTEN_BACKLOG
if (pcb->listener == NULL) {
/* PCB是被连接的,但是没有服务器归属,所以可能是listen pcb被关闭了。
所以返回“非法值”即可。 */
err = ERR_VAL;
} else
#endif /* LWIP_CALLBACK_API || TCP_LISTEN_BACKLOG */
{
#if LWIP_CALLBACK_API
LWIP_ASSERT("pcb->listener->accept != NULL", pcb->listener->accept != NULL);
#endif
/* 本地服务器与远端客户端成功建立连接,然后可以进行accept() */
/* 清空下当前PCB的等待accept()标记,有接口层netconn准备好netconn资源后再标记上 */
tcp_backlog_accepted(pcb);
/* 调用accept()函数 */
TCP_EVENT_ACCEPT(pcb->listener, pcb, pcb->callback_arg, ERR_OK, err);
}
if (err != ERR_OK) {
/* 当前连接没有被上层正常accept(),则断开连接 */
if (err != ERR_ABRT) {
tcp_abort(pcb); /* 给对端发送RST来终止该连接和回收资源 */
}
return ERR_ABRT;
}
/* 当前连接成功被上层accept() */
/* 如果当前ACK携带数据,则也把这些数据给到上层 */
tcp_receive(pcb);
/* 因为SYN占用一个seq号,所以实际确认TCP报文数据区的长度要-1 */
if (recv_acked != 0) {
recv_acked--;
}
/* 建立了新的连接,更新下拥塞窗口 */
pcb->cwnd = LWIP_TCP_CALC_INITIAL_CWND(pcb->mss);
LWIP_DEBUGF(TCP_CWND_DEBUG, ("tcp_process (SYN_RCVD): cwnd %"TCPWNDSIZE_F
" ssthresh %"TCPWNDSIZE_F"\n",
pcb->cwnd, pcb->ssthresh));
if (recv_flags & TF_GOT_FIN) {
/* 如果处理结果是被远端FIN,则需要正常响应ACK,并更新PCB状态:SYN_RCVD --> CLOSE_WAIT */
tcp_ack_now(pcb);
pcb->state = CLOSE_WAIT;
}
} else {
/* 收到错误的ACK号,响应RST即可 */
tcp_rst(pcb, ackno, seqno + tcplen, ip_current_dest_addr(),
ip_current_src_addr(), tcphdr->dest, tcphdr->src);
}
}
break;
case CLOSE_WAIT:
/* 服务器处于半关闭状态,已经不可能再接收到来自客户端的报文了 */
/* 服务器在此状态下会一直等待上层应用执行关闭命令 tcp_close(),并将PCB状态设置为LAST_ACK */
/* FALLTHROUGH */
case ESTABLISHED: /* 连接双方都处于稳定状态 */
tcp_receive(pcb); /* 处理报文段中的TCP数据 */
if (recv_flags & TF_GOT_FIN) { /* passive close */
/* 收到了对端的FINr,
就需要响应对方的FIN挥手,并需要更新PCB状态:ESTABLISHED --> CLOSE_WAIT */
tcp_ack_now(pcb);
pcb->state = CLOSE_WAIT;
}
break;
case FIN_WAIT_1:
tcp_receive(pcb); /* 处理报文段中的TCP数据 */
if (recv_flags & TF_GOT_FIN) {
/* 收到了对端的FINr */
/* 如果收到了FINi的ACK,并且本地没有未发送的数据,则直接进入TIME_WAIT状态 */
if ((flags & TCP_ACK) && (ackno == pcb->snd_nxt) &&
pcb->unsent == NULL) {
LWIP_DEBUGF(TCP_DEBUG,
("TCP connection closed: FIN_WAIT_1 %"U16_F" -> %"U16_F".\n", inseg.tcphdr->src, inseg.tcphdr->dest));
tcp_ack_now(pcb); /* 为这个FINr响应ACK */
tcp_pcb_purge(pcb); /* 释放PCB缓冲资源 */
TCP_RMV_ACTIVE(pcb); /* 把该PCB踢出ACTIVE态链表 */
pcb->state = TIME_WAIT; /* PCB状态更新为TIME_WAIT状态 */
TCP_REG(&tcp_tw_pcbs, pcb); /* PCB迁入TIME_WAI态链表 */
} else {
/* 还没收到FINi对应的ACK || 发送缓冲区中还有数据未发送 */
/* 这种情况下只响应ACK,然后进入CLOSING状态。 */
tcp_ack_now(pcb);
pcb->state = CLOSING;
}
} else if ((flags & TCP_ACK) && (ackno == pcb->snd_nxt) &&
pcb->unsent == NULL) {
/* 如果还没收到FINr,但是收到FINi的ACK,pcb->unsent没有报文,则进入FIN_WAIT_2状态 */
pcb->state = FIN_WAIT_2;
}
/* 说明:上面添加pcb->unsent == NULL,这个逻辑条件是想把unsent的数据都发送出去再关闭连接。
读者不必担心收到FINi的ACK,但是因为这个pcb->unsent == NULL导致丢弃这个FINi的ACK报文,
因为既然pcb->unsent不为NULL,说明PCB还会发数据,对端还会响应ACK或者RST,
这样依然能对我们的FINi进行ACK或者直接RST连接。 */
break;
case FIN_WAIT_2:
tcp_receive(pcb); /* 处理报文段中的TCP数据 */
/* 只需要等待收到远端的FINr报文段,则响应ACK,清除PCB缓冲资源,PCB迁入TIME_WAIT态链表,PCB状态更新为TIME_WAIT */
if (recv_flags & TF_GOT_FIN) {
LWIP_DEBUGF(TCP_DEBUG, ("TCP connection closed: FIN_WAIT_2 %"U16_F" -> %"U16_F".\n", inseg.tcphdr->src, inseg.tcphdr->dest));
tcp_ack_now(pcb);
tcp_pcb_purge(pcb);
TCP_RMV_ACTIVE(pcb);
pcb->state = TIME_WAIT;
TCP_REG(&tcp_tw_pcbs, pcb);
}
break;
case CLOSING:
tcp_receive(pcb); /* 处理报文段中的TCP数据 */
/* 只需要等待 收到确认了FINi的ACK && 没有未发送的数据 就可以进入TIME_WAIT 状态了 */
if ((flags & TCP_ACK) && ackno == pcb->snd_nxt && pcb->unsent == NULL) {
LWIP_DEBUGF(TCP_DEBUG, ("TCP connection closed: CLOSING %"U16_F" -> %"U16_F".\n", inseg.tcphdr->src, inseg.tcphdr->dest));
tcp_pcb_purge(pcb);
TCP_RMV_ACTIVE(pcb);
pcb->state = TIME_WAIT;
TCP_REG(&tcp_tw_pcbs, pcb);
}
break;
case LAST_ACK:
tcp_receive(pcb); /* 处理报文段中的TCP数据 */
/* 只需要等待 收到确认了FINi的ACK && 没有未发送的数据 就可以直接进入CLOSED状态了,无需等待2MSL */
if ((flags & TCP_ACK) && ackno == pcb->snd_nxt && pcb->unsent == NULL) {
LWIP_DEBUGF(TCP_DEBUG, ("TCP connection closed: LAST_ACK %"U16_F" -> %"U16_F".\n", inseg.tcphdr->src, inseg.tcphdr->dest));
/* 注意:bug修正#21699:不要在这里设置pcb->state 为 CLOSED,否则会有片段泄漏的风险 */
recv_flags |= TF_CLOSED;
}
break;
default:
break;
}
return ERR_OK;
}
TCP接纳TCP包处理:tcp_receive()
被tcp_process()接收到合法报文段时调用。
需要把接收到的报文更新到本地接收缓存,并尝试转交给应用层。
主要内容:
-
窗口更新。
-
检查ACK:
- 检查是否是重复ACK。
- 检查本次接收到的报文段的ACK,确认了pcb->unacked中多少报文段。然后释放这些被确认了的报文段,并更新发送窗口。
-
RTT&RTO:
- 计算RTO。
-
检查SEQ:检查本次收到了多少TCP数据,并把这些数据按序放入缓冲区:
- 有序的放入
receive_data
; - 乱序的放入
pcb->ooseq
。 - 放入缓冲区的pbuf用
pbuf_ref
管理,pbuf的引用+1,防止被其它地方free
。 - 入队是最麻烦的处理:需要考虑窗口、缓存、处理重叠。
- 有序的放入
-
SACK:
- 根据
->ooseq
队列生成SACK选项相关值。
- 根据
收到新的ACK:更新发送窗口+释放被ACK的数据内存
更新窗口:
/* 1. 先检查报文是否包含ACK */
if (flags & TCP_ACK) {
/* 获取当前发送窗口右边界:发送窗口大小 + 发送窗口左边界(上次更新发送窗口时,收到的ACK号) */
right_wnd_edge = pcb->snd_wnd + pcb->snd_wl2;
/* 更新窗口 */
/* pcb->snd_wl1:上次更新发送窗口时的SEQ号 */
/* pcb->snd_wl2:上次更新发送窗口时的ACK号(也是对端期待接收到的下一个SEQ号) */
/* 1. 比上次更新窗口时,收到新的数据(这个触发条件好像和发送窗口没啥关系,不过也无所谓) */
/* 2. 或 没有新数据,但收到更多报文的确认 */
/* 3. 或 没有新数据,也没有更多报文的确认,但收到接收窗口通告值变大了 */
if (TCP_SEQ_LT(pcb->snd_wl1, seqno) ||
(pcb->snd_wl1 == seqno && TCP_SEQ_LT(pcb->snd_wl2, ackno)) ||
(pcb->snd_wl2 == ackno && (u32_t)SND_WND_SCALE(pcb, tcphdr->wnd) > pcb->snd_wnd)) {
/* 更新发送窗口为对端的接收窗口通告值 */
pcb->snd_wnd = SND_WND_SCALE(pcb, tcphdr->wnd);
/* keep track of the biggest window announced by the remote host to calculate the maximum segment size */
/* 跟踪对端宣告的接收窗口通告值,记录历史最大那个 */
if (pcb->snd_wnd_max < pcb->snd_wnd) {
pcb->snd_wnd_max = pcb->snd_wnd;
}
pcb->snd_wl1 = seqno; /* 辅助:记录本次更新发送窗口时的SEQ号 */
pcb->snd_wl2 = ackno; /* 辅助:记录本次更新发送窗口时的ACK号 */
LWIP_DEBUGF(TCP_WND_DEBUG, ("tcp_receive: window update %"TCPWNDSIZE_F"\n", pcb->snd_wnd));
#if TCP_WND_DEBUG /* 窗口LOG */
} else {
/* 如果本次没有更新发送窗口,且本次报文附带的窗口和原窗口不一致,则打印相关LOG */
/* 如:收到一个旧的重复报文段时 */
if (pcb->snd_wnd != (tcpwnd_size_t)SND_WND_SCALE(pcb, tcphdr->wnd)) {
LWIP_DEBUGF(TCP_WND_DEBUG,
("tcp_receive: no window update lastack %"U32_F" ackno %"
U32_F" wl1 %"U32_F" seqno %"U32_F" wl2 %"U32_F"\n",
pcb->lastack, ackno, pcb->snd_wl1, seqno, pcb->snd_wl2));
}
#endif /* TCP_WND_DEBUG */
}
检查ACK,这里就不贴代码了,直接贴方法:
检查重复ACK是很重要的,决定着快速重传的判断。
/* (From Stevens TCP/IP Illustrated Vol II, p970.)
* 通过以下条件可以判断是否是重复的ACK:
* 1) 没有ACK新数据;
* 2) 没有TCP数据,也没有SYN、FIN标志;
* 3) 前面更新窗口算法中,本地发送窗口没有更新;
* 4) 本地还有unacked数据,并且重传计时器在跑;
* 5) 当前收到的ACK,是本次连接历史最大的ACK。
*
* 如果上面5个条件都满足,则是一个重复的ACK:
* a) 重复 < 3次:do nothing
* b) 重复 == 3次: 快重传
* c) 重复 > 3次: 拥塞窗口CWND+1MSS(拥塞避免算法)
*
* 如果只满足条件1、2、3:重置重复ACK计数器。(并添加到统计中,但是LWIP没有做这个统计)
*
* 如果只满足条件1:重置重复ACK计数器。
*
*/
收到新数据:推送到接收缓冲区
在正常状态下收到含数据的报文段后,主要做三件事:
- 如果收到的TCP数据包含我们期待收到的下一个SEQ号的数据,说明有有序的新数据到达。
我们需要把这些有序的新数据推送到应用层。
然后调整下一个期待收到的SEQ号、通告窗口和滑动一下接收窗口。 - 如果收到的TCP数据跳过了我们期待收到的下一个SEQ号,我们就要把这些乱序的报文有序地放到
pcb->ooseq
队列中缓存起来。
让后立即响应一个ACK(和SACK),表示我们收到了乱序报文。
待我们收到中间空隙的数据,再把这些数据拼接回来递交到应用层。 - 最后,我们检查下乱序报文队列
->ooseq
中是否有报文有序了,即是->ooseq
队列第一个报文SEQ号 <= 下一个期待接收的SEQ号->rcv_nxt
。
如果有,则提取、拼接、递交到应用层。然后调整相关字段:下一个期待的SEQ、通告窗口、接收窗口。
接收到合法的新数据是先保存到recv_data
全局变量中,在当前函数返回后,由调用者层级处理函数tcp_input()
处理递交到应用层。
tcp_input()
函数会调用TCP_EVENT_RECV()
来尝试递交给应用层,如果应用层没有成功接收,就会发到接收缓冲区pcb->refused_data
中。
剩下的就是LWIP实现的具体代码了,不贴出,有兴趣的可以看源码。
测量RTT和RTO计算
后面分析。
TCP层数据流图
问题记录
SO_REUSE复用IP和端口号
SOF_REUSEADD
选项表示可以复用处于TIME_WAIT
状态的端口。
所以,在绑定IP&PORT时,可以复用处于TIME_WAIT
状态的端口(相同IP&PORT的所有PCB都必须开启SOF_REUSEADD
,本次绑定才能复用成功)。
在建立连接时,如果开启了SOF_REUSEADD
,需要检查五元组:本地IP、本地PORT、远端IP、远端PORT和TCP PCB状态。才能正常连接,因为正常连接必须唯一对应一个服务。