LWIP的TCP协议
1. 输入主路径:ethernet_input → ip4_input → tcp_input → tcp_process → tcp_receive
-
入口与校验
- ip4_input() 识别协议为 TCP 后,交给 tcp_input(struct pbuf* p, struct netif*).
- tcp_input 做三件事:
- 首部合法性与校验和检查(TCP_HLEN、伪首部校验)。
- 基于四元组定位 PCB(active -> timewait -> listen 顺序)。
- 将 p->payload 指向 TCP 数据区,并计算 tcplen/flags/seqno/ackno 等临时全局。
-
状态机分发(对应你的“流程方框:tcp_input→tcp_process→tcp_receive()”)
- 找到活动 PCB 后,inseg 结构指向当前段,随后调用 tcp_process(pcb) 执行“TCP FSM”:
- SYN_SENT:处理对端的 SYN/ACK,建立连接,初始化 cwnd/mss/窗口后进入 ESTABLISHED。
- SYN_RCVD:等待对端 ACK,转入 ESTABLISHED(或错误时 RST)。
- ESTABLISHED/CLOSE_WAIT/FIN_WAIT_x/…:统一走 tcp_receive(pcb) 处理 ACK、数据、窗口更新、重排等。
- 未找到 PCB:tcp_input 发送 RST(tcp_rst_netif)。
- 找到活动 PCB 后,inseg 结构指向当前段,随后调用 tcp_process(pcb) 执行“TCP FSM”:
-
数据接收与乱序重排(和你图中“recv_data/应用回调/邮箱投递”等一致)
- tcp_receive(pcb) 核心逻辑:
- ACK 处理:移动 unacked/unsent 队列(tcp_free_acked_segments),更新 cwnd/ssthresh(慢启动/拥塞避免)、rto/rtt 等。
- 窗口与重复 ACK:
- 重复 ACK 计数≥3 触发快速重传 tcp_rexmit_fast();同时调整拥塞窗口。
- 就绪数据与乱序:
- 就绪(seq==rcv_nxt):rcv_nxt 前进、rcv_wnd 递减,必要时拼接 ooseq 变为有序,再设置 recv_data。
- 乱序:插入 pcb->ooseq(tcp_oos_insert_segment),必要时裁剪重叠;可选生成 SACK。
- 向上递交:TCP_EVENT_RECV(pcb, recv_data) 交给上层,若应用暂时接不走,存 pcb->refused_data。
- ACK:针对收到的数据/窗口移动调用 tcp_ack()/tcp_send_empty_ack()。
- tcp_receive(pcb) 核心逻辑:
-
零窗口探测(Zero-Window Probe)
- 对端通告窗口为 0 且仍有未发送数据:由慢计时器进入“persist 计时”,tcp_zero_window_probe() 每次发 1 字节数据或者 FIN 探测。
2. 输出主路径:应用 → tcp_write → (分段/入队) → tcp_output → tcp_output_segment → ip_output_if
-
tcp_write(pcb, buf, len, apiflags)
- 三阶段分段与入队:
- OVERSIZE 尾部直接追加(减少 pbuf 分配/拷贝)。
- 复用/拼接到最后一个 unsent 段(concat_p/extendlen)。
- 创建新段 tcp_create_segment(构建 TCP 头但不填 ack/wnd/chksum),串到 pcb->unsent。
- PSH 设置:若未带 TCP_WRITE_FLAG_MORE,则给最后一个段置 PSH。
- 典型“粘包”来源就在 1)+2) 阶段:多次小写入极易被合并为更少的段(见第 3 节)。
- 三阶段分段与入队:
-
tcp_output(pcb)
- 依据有效发送窗 wnd = min(snd_wnd, cwnd) 和 Nagle 算法决定是否发送。
- 逐段发送 unsent 链表中满足窗的段:
- 设置 ACK 标志(非 SYN_SENT)。
- 通过 tcp_output_segment(seg, pcb, netif) 下发到 IP。
- 发送后移到 unacked(仅数据/FIN/SYN计序的段)。
-
tcp_output_segment
- 回填首部字段:ackno=rcv_nxt,wnd=本端通告窗口(考虑窗口缩放),可选写时间戳/窗口扩大/SACK_PERM。
- 计算 TCP 校验和(支持“拷贝时校验”优化 TCP_CHECKSUM_ON_COPY)。
- 调 ip_output_if(p, src, dst, ttl, tos, IP_PROTO_TCP, netif)。
-
拥塞控制与重传
- RTO 触发:tcp_slowtmr() 中 rtime≥rto → tcp_rexmit_rto_prepare/commit,rto 指数回退、cwnd=1*MSS。
- 快速重传:见 tcp_receive 的 dupack≥3 分支调用 tcp_rexmit_fast(),并调整 ssthresh/cwnd。
-
unsent:已构建但尚未通过 ip 发送的 tcp_seg 链(发前/被退回重发时也会放回这里)。
-
unacked:已发送、等待对端确认的 tcp_seg 链(RTO、快速重传操作针对该链)。
-
ooseq:接收端保存的乱序段链,等待前序到达并合并后上交给应用。
-
关键序列/窗口:snd_nxt(下一个要发送的序号)、lastack(最后被确认的序号)、snd_lbb(拥塞控制基准)、rcv_nxt(下一个期望接收序号)、snd_wnd/cwnd/ssthresh(发送窗/拥塞窗/阈值)。
状态机与队列行为对应 -
LISTEN
- 不发送数据;收到 SYN 时会为新连接分配 PCB(SYN_RCVD)并把 SYN|ACK 放入 unsent/unacked(视是否已发送)。
-
SYN_SENT
- 主动发送 SYN(SYN 段在 unsent -> 发送后移入 unacked,等待 SYN|ACK 的 ACK)。超时重传作用于 unacked。
-
SYN_RCVD
- 被动接受 SYN 后发 SYN|ACK(SYN|ACK 在 unacked),收到对端 ACK 后进入 ESTABLISHED 并释放与 ACK 相关的 unacked 段。
-
ESTABLISHED
- 发送:tcp_write 将数据构造为 tcp_seg 串入 unsent;tcp_output 按 wnd/cwnd/Nagle 将满足条件的段从 unsent 发送并移入 unacked。
- 接收:按 seq 与 rcv_nxt 判断,按序的数据直接交付(rcv_nxt 前移);乱序插入 ooseq,后续拼接后交付。
- 重传:dup-ACK 快速重传或 RTO(基于 unacked)把需要重发的段移回 unsent 或直接发送。
-
FIN_WAIT1 / LAST_ACK
- 发送 FIN 时 FIN 段作为一个 tcp_seg 放入 unsent -> 发送后在 unacked 等待对端的 ACK。收到 ACK 后根据状态转为 FIN_WAIT2 或 CLOSED。
-
FIN_WAIT2 / CLOSING
- 如果先收到对端 FIN,而自身 FIN 仍未被 ACK,状态变为 CLOSING(两端都已发 FIN,等待对方或 ACK)。unacked 仍可能包含本端的 FIN 段。
-
TIME_WAIT
- 双方完成四次挥手且本端最后发 ACK 后进入 TIME_WAIT;通常 unacked 已被确认(无待确认段),PCB 被放到 timewait 列表等待 2MSL 后释放。
-
CLOSE_WAIT
- 被动收到 FIN,交给应用(p==NULL 表示 EOF),应用要调用 tcp_close/tcp_shutdown 触发 FIN 发送(形成 LAST_ACK)。
-
异常(RST/abort)
- 调用 tcp_abort 会立即发 RST 并释放所有队列(unacked/unsent/ooseq 被丢弃并释放 pbuf)。
-
“tcp_seg + pbuf 结构图”:正是 unsent/unacked/ooseq 三条链的节点形态。
-
“滑动窗口图(snd_wnd/lastack/snd_nxt)”:变量名与图一一对应(lastack、snd_nxt、snd_lbb、snd_wnd)。
3. “粘包/拆包”在 lwIP 中如何产生与规避
-
产生位置
- 发送端:tcp_write 的“OVERSIZE 追加/拼接”与 Nagle 算法;网卡/内核的分段合并(TSO/GSO)也会使多个小写合并。
- 接收端:TCP 是字节流,tcp_receive 可能一次回调给你多个应用“消息”的字节;也可能拆成多次回调。
-
与 IP 分片无关
- IP 分片发生在 IP 层,目的端重组后才进入 TCP,TCP 仍是连续字节流,不提供消息边界。
-
正确做法
- 应用协议自带帧:固定长度 / 定界符 / 长度前缀(最常见)。
- 降低合并概率(并不能恢复“消息边界”):TCP_NODELAY 关闭 Nagle;配合“批量写”或 MSG_MORE/TCP_CORK 类策略。
-
在 lwIP 的开关
- 关闭 Nagle:tcp_nagle_disable(pcb) 或 socket 侧 TCP_NODELAY。
- 大包/MSS:合理设置 TCP_MSS 与路径 MTU,减少 IP 分片,提高效率。
4. 与抓包字段对齐
-
TCP 头字段(你图中“20B首部+标志+窗口”)
- 源/目的端口:tcphdr->src/dest
- 序号/确认号:tcphdr->seqno/ackno
- 首部长度与标志:TCPH_HDRLEN_FLAGS_SET / TCPH_FLAGS()
- 窗口:tcphdr->wnd(收到时经窗口缩放变更为 snd_wnd/rcv_wnd)
- 校验和:tcphdr->chksum(ip_chksum_pseudo 计算)
-
Wireshark“Len=1452/Seq/Ack/Flags/Win”
- Len≈MSS,Seq/NextSeq 对应 lwip_ntohl(seg->tcphdr->seqno) 与 TCP_TCPLEN(seg)。
- Window size value 与 Window size scaling factor 对应 TF_WND_SCALE/snd_scale/rcv_scale。
5. 连接建立/关闭:“三次握手/四次挥手图”
-
主动打开(tcp_connect)
- 填 remote/local, 分配本地端口、初始 ISS、窗口、MSS。
- 通过 tcp_enqueue_flags(TCP_SYN) 发送 SYN;进入 SYN_SENT。
-
被动打开(tcp_listen_input)
- LISTEN 收到 SYN:分配新 pcb(SYN_RCVD),记录对端 wnd/mss,入队 SYN|ACK,tcp_output 发送。
- LISTEN 收到 SYN:分配新 pcb(SYN_RCVD),记录对端 wnd/mss,入队 SYN|ACK,tcp_output 发送。
-
三次握手达成
- SYN_SENT 收到 SYN|ACK 且 ack 正确 → ESTABLISHED,回 ACK。
- SYN_RCVD 收到 ACK(ack 范围正确)→ ESTABLISHED,触发 accept 回调。
-
关闭(四次挥手与 TIME_WAIT)
- tcp_close()/tcp_shutdown() 发送 FIN(tcp_send_fin/tcp_enqueue_flags),状态递进:ESTABLISHED→FIN_WAIT_1→…→TIME_WAIT。
- 被动关闭收到 FIN:置 TF_GOT_FIN,回 ACK,交给应用 EOF(recv 回调 p==NULL)。
- TIME_WAIT 定时在 tcp_slowtmr 中清理(2MSL)。
实际上用wireshark抓包发现有时候挥手也是三次,四次是为了避免数据没有完全发完罢了。
- 异常/放弃
- tcp_abort()/tcp_abandon(reset=1):立刻发 RST 并释放 PCB。
- 超时与重试次数:SYNMAXRTX / MAXRTX 受限后由慢计时器清理并可选复位。
6. 窗口、拥塞与定时器
-
发送窗口与拥塞窗口
- 发送有效窗 wnd=min(snd_wnd, cwnd)。
- 慢启动/拥塞避免:
- 初始 cwnd=LWIP_TCP_CALC_INITIAL_CWND(mss)。
- cwnd<ssthresh:每 ACK 按已确认字节线性增长(每 RTT 近似翻倍)。
- cwnd≥ssthresh:拥塞避免,按 acked 累计到 1cwnd 时再加 1MSS。
-
快速重传/恢复
- dupacks≥3:tcp_rexmit_fast(); ssthresh=max(min(cwnd,snd_wnd)/2, 2MSS),cwnd=ssthresh+3MSS,TF_INFR 进入恢复。
-
RTO 重传
- rtime≥rto:将 unacked 迁回 unsent,cwnd=1*MSS,rto 指数回退,tcp_output 再发。
-
关键计时器
- tcp_fasttmr() 250ms:发送延迟 ACK(TF_ACK_DELAY);重试挂起 FIN(TF_CLOSEPEND);处理 refused_data。
- tcp_slowtmr() 500ms:
- RTO/Persist/Keepalive/超时清理(SYN_RCVD/LAST_ACK/TIME_WAIT 等)。
- 零窗口探测(persist):tcp_zero_window_probe。
- ooseq 超时丢弃,防内存占用。
7. 选项与扩展:MSS/WS/TS/SACK/KEEPALIVE
- 解析(接收):tcp_parseopt
- MSS(LWIP_TCP_OPT_MSS)、窗口扩大(WS)、时间戳(TS)、SACK_PERM。
- 发送(控制段/数据段):tcp_output_segment / tcp_output_fill_options
- 按 flags 决定是否携带 TS/WS/SACK,SACK 的实际块由接收端的 ooseq 情况决定。
- 保活:tcp_keepalive() 在 tcp_slowtmr 驱动,超出 keep_idle+keep_cnt*keep_intvl 仍无响应则复位连接。
8. 典型问题
- 小包粘包:查看发送侧是否启用 Nagle(TF_NODELAY),观察 tcp_write 阶段 1/2 是否合并,抓包验证 PSH/段大小。
- 吞吐上不去:确认 MSS/MTU、路径 MTU、窗口扩大(TF_WND_SCALE)、rcv_wnd/snd_wnd/cwnd 的约束。
- 丢包/重传多:看 tcp_slowtmr 中 RTO 是否频繁,dupacks 与快速重传是否发生;定位链路问题或过小的缓冲。
- 收到数据但应用没取:pcb->refused_data 非空;检查上层回调是否及时 tcp_recved()。
- 零窗口死锁:persist 机制是否工作(persist_backoff/pcb->persist_probe),确认对端确实通告 0 窗口。
- 关闭卡住:状态若停在 FIN_WAIT_2/LAST_ACK,结合 tmr 差值(tcp_ticks-pcb->tmr)看是否超时清理。
9. 参数与调优(lwipopts 相关)
- 性能
- TCP_SND_BUF / TCP_SND_QUEUELEN:发送缓冲/队列深度。
- TCP_WND / 窗口扩大(LWIP_WND_SCALE、TCP_RCV_SCALE)。
- TCP_MSS 与路径 MTU(tcp_eff_send_mss_netif)。
- TCP_OVERSIZE/TCP_CHECKSUM_ON_COPY:降低 copy/计算开销。
- 可靠性/特性
- LWIP_TCP_SACK_OUT / LWIP_TCP_TIMESTAMPS / KEEPALIVE。
- LWIP_NETIF_TX_SINGLE_PBUF:传输出单 pbuf,提升 DMA 友好性。
- 行为
- Nagle(默认开):需要低时延小包时关闭 TCP_NODELAY。
- 端口复用:SO_REUSEADDR(被动监听/主动连接的 5 元组判重逻辑不同)。
10. 小结
- 输入链路:tcp_input → tcp_process → tcp_receive 决定 ACK/重排/交付;配合 fast/slow 计时器实现“延迟 ACK、快速重传、RTO、persist、keepalive”。
- 输出链路:tcp_write 分段入队 + tcp_output 受窗口/Nagle 约束下发到 IP;unsent/unacked 队列维护在途数据。
- 粘包/拆包是 TCP 字节流的自然行为,需在应用层做帧化;与 IP 分片无关。
全链路调用顺序如下。
输入链路:NIC_ISR (中断/DMA) → ethernetif_input (low_level_input) → netif->input (tcpip_input) → tcpip_thread (message) → ethernet_input → ip4_input → tcp_input → tcp_process → tcp_receive
输出链路:APP(socket/netconn/raw)→ tcp_write() → 构造 tcp_seg 并追加到 pcb->unsent → tcp_output()(检查 wnd/cwnd/Nagle)→ 填写 TCP/IP 首部(tcp_output_segment)→ ip_output_if()/ip4_output_if() → netif->output(如 ethernetif_output / low_level_output)→ 启动 NIC DMA 发送 → NIC 硬件发帧 → NIC TX 完成中断(NIC_ISR)→ ethernetif 的 TX 完成回调/任务处理(可选通知 tcpip_thread)。