LWIP TCP 连接断开状态机

张开发
2026/4/10 21:21:18 15 分钟阅读

分享文章

LWIP TCP 连接断开状态机
一引言本文章基于 lwip-2.2.1 版本(2025年)的源码进行讲解主要讲原理和代码的结合。阅读本文章要有 TCP/IP 协议基础起码要知道 TCP 的三次握手和四次挥手。之前已经写过一篇 TCP 连接的状态机没看过的可以看LWIP TCP 连接状态机在这篇文章主要写断开的状态机。LWIP 使用了一个枚举定义了 TCP 连接状态enum tcp_state { CLOSED 0, //关闭或初始状态此时连接未建立 LISTEN 1, //监听状态独属于服务器监听客户端连接时 SYN_SENT 2, //发送SYN后的状态 SYN_RCVD 3, //作为服务器收到SYN返回确认后的状态 ESTABLISHED 4, //连接状态握手完成后的状态 FIN_WAIT_1 5, //主动关闭方发送FIN包后的状态 FIN_WAIT_2 6, //收到对方第一个FIN的ACK后等待对方发送FIN的状态 CLOSE_WAIT 7, //被动关闭方收到第一个FIN包等待本地应用关闭连接 CLOSING 8, //同时关闭情况下发送FIN后等待对方FIN的确认 LAST_ACK 9, //被动关闭方发送最后一个ACK后的状态 TIME_WAIT 10 //最终状态等待确保对方收到最后的ACK };LWIP 状态机的核心处理函数是tcp_process()此函数主要是根据当前状态对收到的所有数据包1进行处理但不是所有的状态都在这里进行切换与设置。本文会讲解代码但是不会一股脑将函数内所有的代码放入贴出讲解只讲相关的部分否则太长还会影响学习。如果不想看源码讲解可以跳过。或者部分源码看不懂直接跳过也行。二四次挥手TCP 连接的断开是通过 直接发送 RST 或 四次挥手完成的客户端进行四次挥手流程如下第一次挥手客户端发送 FIN 给服务器表示不再发送数据第二次挥手服务器响应 ACK 给客户端确认收到关闭请求第三次挥手服务器发送 FIN 给客户端表示数据发送完毕第四次挥手客户端响应 ACK 给服务器确认收到关闭请求正常的四次挥手报文如下如果服务器收到 FIN 后没有数据需要发送了此时第二第三步挥手可以合并发送所以这个挥手报文只有三段。三挥手状态变迁TCP 协议不在乎是由客户端还是由服务器主动挥手只区分主动关闭和被动关闭。主动断开连接的状态流程ESTABLISHED → FIN_WAIT_1 → FIN_WAIT_2 → TIME_WAIT → (等待 2MSL) → CLOSED被动断开连接的状态流程ESTABLISHED → CLOSE_WAIT → LAST_ACK → CLOSED四close() 和 shutdown()4.1 close() 函数close() 函数关闭文件描述符释放相关资源。此函数不能选择关闭读或写的方向它是一个全关闭操作调用 close() 函数关闭套接字时该套接字描述符不再有效不可再进行 recv() 和 send() 操作。说白了就是调用 close() 函数后socket 资源会被全部释放不可以再使用了。close() 会受SO_LINGER选项的影响主要体现在是否阻塞以及如何处理套接字发送缓冲区中未发送的数据。SO_LINGER选项有两个配置一个是开关(onoff)另一个是时间(linger秒单位)默认 onoff 等于零不开启SO_LINGER选项。onoff 1linger 0调用close()不会发送FIN而是发送RST重置连接onoff 1linger x此时 close() 的行为取决于套接字是否为阻塞模式。如果套接字是阻塞的close() 会阻塞直到所有数据发送完毕并收到对端确认或超时 x 秒后返回错误如果套接字是非阻塞的在 Linux 等系统上 close() 会立即返回 EWOULDBLOCK 错误(套接字未关闭阻塞)4.2 shutdown() 函数选择性关闭 Socket 的读、写或双向通道但不释放套接字描述符。关闭读或写其中一项时可以说是半关闭通过参数控制关闭方向SHUT_RD关闭接收端不再读取数据但发送端仍可用。SHUT_WR关闭发送端发送缓冲区中的数据会被发送后触发 FIN 包但接收端仍可用。SHUT_RDWR同时关闭读写等同于依次调用SHUT_RD和SHUT_WD使用shutdown(sock, SHUT_WD)会发送 TCP 的FIN包本地不再发送数据对端可继续发送数据直到其关闭。当对端没有数据要发送后会发送FIN(第三次挥手)本地也会回应以进行完整的四次挥手断开连接。但是要注意shutdown()函数即使能够进行完整的四次挥手也不会释放套接字描述符所以在调用shutdown()函数后还是需要再调用close()函数去释放资源。五主动关闭状态机代码讲解5.1 ESTABLISHED → FIN_WAIT_1我们要知道的是一个 tcp 连接不只有在调用close()或shutdowm()函数后才会主动发送FIN包 断开连接。举两个例子1. 给新连接分配 pcb 控制块失败拒绝连接时发送。 2. 设置了关闭但无法发送(发送缓冲区满等)的情况下会在快速定时器任务tcp_fasttmr中重试发送。调用函数主动断开 tcp 连接实际发送 FIN包 是在tcp_close_shutdown_fin()函数函数内部再使用tcp_send_fin()进行发送。下面讲这两个函数static err_t tcp_close_shutdown_fin(struct tcp_pcb *pcb) { err_t err; /* 根据当前 pcb 控制块状态处理 * SYN_RCVD 还未连接成功主动放弃建立连接 * CLOSE_WAIT 发送四次挥手的第三步 * ESTABLISHED 已连接主动正常断开 */ switch (pcb-state) { case SYN_RCVD: err tcp_send_fin(pcb); //发送 FIN if (err ERR_OK) { tcp_backlog_accepted(pcb); //减少监听pcb计数 MIB2_STATS_INC(mib2.tcpattemptfails); //增加连接失败计数 pcb-state FIN_WAIT_1; //更新状态为FIN_WAIT_1 } break; case ESTABLISHED: err tcp_send_fin(pcb); //发送 FIN if (err ERR_OK) { MIB2_STATS_INC(mib2.tcpestabresets); //更新关闭数 pcb-state FIN_WAIT_1; //更新状态为FIN_WAIT_1 } break; case CLOSE_WAIT: err tcp_send_fin(pcb); //发送 FIN if (err ERR_OK) { MIB2_STATS_INC(mib2.tcpestabresets); //更新关闭数 pcb-state LAST_ACK; //更新状态为LAST_ACK } break; default: return ERR_OK; } if (err ERR_OK) { /* 尝试输出数据包 */ tcp_output(pcb); } else if (err ERR_MEM) { /* 前面因为内存不足无法发送设置 TF_CLOSEPEND 标志位表示关闭操作未完成 * 这里设置 TF_CLOSEPEND 标志是为了能在 tcp_fasttmr 定时器中重试 * 返回 OK 表示操作已排队此时 pcb 不应再被使用避免应用程序继续使用此连接 */ tcp_set_flags(pcb, TF_CLOSEPEND); return ERR_OK; } return err; }err_t tcp_send_fin(struct tcp_pcb *pcb) { /* 如果还有未发送的数据段 * 通过 for循环 查找最后一个未发送的段 * 这是为了优化如果最后一个段没有数据负载将 FIN标志 附加到该段 */ if (pcb-unsent ! NULL) { struct tcp_seg *last_unsent; for (last_unsent pcb-unsent; last_unsent-next ! NULL; last_unsent last_unsent-next); /* 如果该段不包含 SYN/FIN/RST代表这是纯数据段可以添加 FIN 标志 */ if ((TCPH_FLAGS(last_unsent-tcphdr) (TCP_SYN | TCP_FIN | TCP_RST)) 0) { TCPH_SET_FLAG(last_unsent-tcphdr, TCP_FIN); tcp_set_flags(pcb, TF_FIN); return ERR_OK; } } /* 无数据无长度无标志无选项数据设置 FIN 标志 */ return tcp_enqueue_flags(pcb, TCP_FIN); }5.2 FIN_WAIT_1 → FIN_WAIT_2/CLOSING/TIME_WAIT在 第三节 已经讲过四次握手实际上不一定是四次。当被动方没有数据需要发送时会将第二第三次握手合并发送此时状态不只有一种转变。状态机核心处理函数static err_t tcp_process(struct tcp_pcb *pcb) { ... /* 根据当前 tcp 状态处理 */ switch (pcb-state) { case FIN_WAIT_1 /* 已经发送 FIN 包本地没有数据需要再发送但是对端可能还有数据要发送 * 调用 tcp_receive 处理可能携带的数据 */ tcp_receive(pcb); if (recv_flags TF_GOT_FIN) //收到数据段携带 FIN 标志 { if ((flags TCP_ACK) (ackno pcb-snd_nxt) pcb-unsent NULL) { /* ACK 等于本地期望的下一序列号本地没有未发送的数据也没有未应答的数据 * 此时代表着 同时收到 FINACK跳过 FIN_WAIT_2 状态 */ tcp_ack_now(pcb); //响应 ACK第四次挥手 tcp_pcb_purge(pcb); //清理 pcb 缓存数据并释放缓存内存 TCP_RMV_ACTIVE(pcb); //从活跃链表中移除 pcb-state TIME_WAIT; //更新状态为 TIME_WAIT TCP_REG(tcp_tw_pcbs, pcb); //将 pcb 注册到 FIN_WAIT 链表 } else { /* 收到 FIN但 ACK 不匹配 * 即对端没有响应本地的 FIN而是给本地发送了 FIN * 这代表着双方同时关闭 * 响应 ACK切换到 CLOSING 状态后面会专门讲一下这个 */ tcp_ack_now(pcb); pcb-state CLOSING; } } else if ((flags TCP_ACK) (ackno pcb-snd_nxt) pcb-unsent NULL) { /* ACK 等于本地期望的下一序列号本地没有未发送的数据也没有未应答的数据 * 这个是第二次握手包本地只切换状态不做响应 */ pcb-state FIN_WAIT_2; } break; /* case FIN_WAIT_1 */ ... }/* switch (pcb-state) */ return ERR_OK; }5.3 FIN_WAIT_2 → TIME_WAIT能处于FIN_WAIT_2代表着对端可能还有数据没有发送完成在此状态下还需要处理对端发送的数据同时检查一下是否携带FIN包以进入TIME_WAIT状态。进入TIME_WAIT状态后会将 pcb 添加到tcp_tw_pcbs链表中。static err_t tcp_process(struct tcp_pcb *pcb) { ... /* 根据当前 tcp 状态处理 */ switch (pcb-state) { case FIN_WAIT_2 /* 对端没有发送 FIN 包代表可能还有数据要发送 * 调用 tcp_receive 处理可能携带的数据 */ tcp_receive(pcb); if (recv_flags TF_GOT_FIN) { /* 数据段携带 FIN 表示 */ tcp_ack_now(pcb); //响应 ACK第四次挥手 tcp_pcb_purge(pcb); //清理 pcb 缓存数据并释放缓存内存 TCP_RMV_ACTIVE(pcb); //从活跃链表中移除 pcb-state TIME_WAIT; //更新状态为 TIME_WAIT TCP_REG(tcp_tw_pcbs, pcb); //将 pcb 注册到 FIN_WAIT 链表 } break; /* case FIN_WAIT_2*/ ... }/* switch (pcb-state) */ return ERR_OK; }5.4 TIME_WAIT → CLOSED前面已经知道进入TIME_WAIT会将控制块添加到tcp_tw_pcbs链表中。想想看为什么要专门添加到这个链表中?为什么要专门在这个链表维护TIME_WAIT状态的连接?首先我们要知道一个点第四次挥手的 ACK 包不一定就能真正发送到对端。有可能会因为网络问题丢失了。ACK数据丢失四次挥手未完全对端会重发 FIN 包。为了防止这种情况在发送第四次挥手后还需要等待一段时间来处理可能出现的重传FIN。此状态需要进行专门的处理所以使用tcp_tw_pcbs链表专门去存储这个连接的 pcb控制块。方便通过定时器tcp_slowtmr定期(每 500ms 执行一次)检查清理超时的 pcb控制块。这种分离管理的方式能够很大的提高效率这种编程思想蛮有意思的。void tcp_slowtmr(void) { struct tcp_pcb *pcb, *prev; tcpwnd_size_t eff_wnd; u8_t pcb_remove; /* flag if a PCB should be removed */ u8_t pcb_reset; /* flag if a RST should be sent when removing */ /* 每 500ms 递增 */ tcp_ticks; tcp_timer_ctr; /* 省略 tcp_active_pcbs 链表的处理 */ ... /* tcp_tw_pcbs 链表的处理 */ prev NULL; //前一个节点 pcb tcp_tw_pcbs; while (pcb ! NULL) { pcb_remove 0; /* 检查是否等待了足够的时间 2*MSL * tcp_ticks每 500ms 增加一次 * pcb-tmr 该 pcb 进入 TIME_WAIT 状态时的时间戳 * TCP_SLOW_INTERVAL 值为 500时间单位转换后比较是否超时 */ if ((u32_t)(tcp_ticks - pcb-tmr) 2 * TCP_MSL / TCP_SLOW_INTERVAL) { pcb_remove; } if (pcb_remove) { /* 如果超时可以移除 */ struct tcp_pcb *pcb2; tcp_pcb_purge(pcb); /* 从 tcp_tw_pcbs 链表中移除 PCB */ if (prev ! NULL) { prev-next pcb-next; } else { tcp_tw_pcbs pcb-next; } pcb2 pcb; pcb pcb-next; tcp_free(pcb2); //释放控制块内存 } else { /* 当前节点未超时移动到下一个节点 */ prev pcb; pcb pcb-next; } } /* while (pcb ! NULL) */ }阅读完代码可以发现实际并不是真的有哪行代码将 pcb 的状态而是将 pcb控制块 内存释放了所以也可以理解未回复 CLOSED 状态了。六CLOSING 状态非常值得注意的一个状态CLOSING。这个特地拉出来讲因为普通的四次挥手不会进入此状态只有在双方同时关闭时才会出现。假设我们是客户端在我们发送 FIN 包后收到服务器发送的 纯FIN 包而不是对我们的 FIN 包的 ACK这代表两端在同时断开连接。此时两端都切换成 CLOSING 状态同时 ACK 对端的FIN 包具体流程如下假设 A 和 B 同时挥手 A 发送 FIN进入 FIN_WAIT_1。 B 发送 FIN进入 FIN_WAIT_1。 A 收到 B 的 FIN发送 ACK 确认 B 的 FIN并进入 CLOSING。 B 收到 A 的 FIN发送 ACK 确认 A 的 FIN并进入 CLOSING。 A 收到 B 的 ACK进入 TIME_WAIT。 B 收到 A 的 ACK进入 TIME_WAIT。 双方 都在 TIME_WAIT 状态下等待 2MSL确保对方收到了最后的 ACK。 最终 TIME_WAIT 超时双方释放连接 CLOSED这部分的代码没什么好讲解的跟其他代码重复很大下面贴出来看一下static err_t tcp_process(struct tcp_pcb *pcb) { ... /* 根据当前 tcp 状态处理 */ switch (pcb-state) { case CLOSING: tcp_receive(pcb); if ((flags TCP_ACK) ackno pcb-snd_nxt pcb-unsent NULL) { tcp_pcb_purge(pcb); TCP_RMV_ACTIVE(pcb); pcb-state TIME_WAIT; TCP_REG(tcp_tw_pcbs, pcb); } break; ... }/* switch (pcb-state) */ return ERR_OK; }七被动关闭状态机代码讲解7.1 ESTABLISHED → CLOSE_WAIT在状态机核心处理函数中ESTABLISHED和CLOSE_WAIT状态的处理是一样的。当 pcb 处于这两个状态时都会使用tcp_receive()函数。tcp_receive()函数主要负责处理接收到的数据段在收到 FIN 会设置 FIN接收标志。if (TCPH_FLAGS(inseg.tcphdr) TCP_FIN) { recv_flags | TF_GOT_FIN; }在ESTABLISHED和CLOSE_WAIT状态下调用tcp_receive()处理接收的数据处理完成后判断recv_flags是否设置了 TF_GOT_FIN 接收标志用于切换状态为CLOSE_WAIT回应ACK表示开始被动关闭。static err_t tcp_process(struct tcp_pcb *pcb) { ... switch (pcb-state) { case CLOSE_WAIT: case ESTABLISHED: tcp_receive(pcb); if (recv_flags TF_GOT_FIN) { tcp_ack_now(pcb); pcb-state CLOSE_WAIT; } break; ... } }7.2 关闭事件通知原本不想讲关闭事件通知的但是就上面那点状态切换的代码也没意思所以多讲一点。在socket编程中被动关闭也是需要调用close()或shutdowm()函数的。所以我们可以推理出回复第二次握手包后LWIP会通知应用程序(关闭事件)否则上层无法调用关闭函数(close)完成后续的挥手流程。一个 TCP 数据从网卡收到数据后先选择是 以太网 或者是 ppp 接口然后选择是 IPV4 还是 IPV6然后进入 tcp 处理函数才到状态机处理函数。前面已经知道在tcp_process()函数中会设置recv_flags的 FIN接收标志位在退出 状态机核心处理函数后(回到 tcp_input 函数)就会使用TCP_EVENT_CLOSED上报关闭事件。void tcp_input(struct pbuf *p, struct netif *inp) { ... if (pcb ! NULL) { ... err tcp_process(pcb); if (err ! ERR_ABRT) { if (recv_flags TF_RESET) { ... } /* if (recv_flags TF_RESET) */ else{ ... if (recv_data ! NULL) { /* 如果存在有效的数据也就是在 ESTABLISHED 状态下收到数据 */ /* 1. 检查连接状态然后已经关闭接收释放数据包并终止 pcb */ ... /* 2. 正常情况下会上报读事件 */ TCP_EVENT_RECV(pcb, recv_data, ERR_OK, err); } if (recv_flags TF_GOT_FIN) { /* recv_flags 设置了 TF_GOT_FIN 标志位即收到 FIN 包 */ if (pcb-refused_data ! NULL) { /* 如果收到 PCB 有被拒绝的数据将 FIN 附加到拒绝数据的标志位中 * 这样是为了延迟处理 FIN先让应用程序处理已有的数据 */ pcb-refused_data-flags | PBUF_FLAG_TCP_FIN; } else { /* 1. lwip 手动递增接收窗口维护正确的窗口状态 */ if (pcb-rcv_wnd ! TCP_WND_MAX(pcb)) { pcb-rcv_wnd; } /* 2. 上报关闭事件通知应用程序连接已关闭 * 实际上关闭事件就是 读事件但数据为空 */ TCP_EVENT_CLOSED(pcb, err); if (err ERR_ABRT) { goto aborted; } } } /* if (recv_flags TF_GOT_FIN) */ } /* if (recv_flags TF_RESET) -- else */ } /* if (err ! ERR_ABRT) */ else{ ... } /* if (err ! ERR_ABRT) -- else */ } /* if (pcb ! NULL) */ else{ ... } /* if (pcb ! NULL) -- else */ }在 socket 编程中select 是无法直接监听关闭事件的而是监听到一个读事件但读出数据为空在代码中体现如下。7.3 CLOSE_WAIT → LAST_ACK阅读7.2 关闭事件通知已经知道被动关闭也是需要调用close()或shutdowm()函数。从CLOSE_WAIT切换到LAST_ACK状态 与5.1章节从ESTABLISHED切换到FIN_WAIT_1状态都在函数tcp_close_shutdown_fin()进行。//详细函数可阅读回 5.1 章节。 static err_t tcp_close_shutdown_fin(struct tcp_pcb *pcb) { err_t err; switch (pcb-state) { case SYN_RCVD: ... case ESTABLISHED: ... case CLOSE_WAIT: err tcp_send_fin(pcb); //发送 FIN if (err ERR_OK) { MIB2_STATS_INC(mib2.tcpestabresets); //更新关闭数 pcb-state LAST_ACK; //更新状态为LAST_ACK } break; default: return ERR_OK; } ... }7.4 LAST_ACK → CLOSED在 pcb 状态机核心处理函数中收到第四次挥手时不会马上将状态切换为CLOSED而是设置 recv_flags | TF_CLOSEDstatic err_t tcp_process(struct tcp_pcb *pcb) { ... switch (pcb-state) { ... case LAST_ACK: /* 1. 调用 tcp_receive 的原因 * 即使处于 LAST_ACK状态PCB中可能仍存在已接收但未被应用程序处理的数据 * 在正式关闭连接前需要确保所有已接收的数据都被正确处理 */ tcp_receive(pcb); /* 2. 收到正确的 第四次挥手包处理 * 状态不会直接切换成 CLOSED而是设置了 TF_CLOSED 标志位 * 使用标志位是为了确保资源正确释放 */ if ((flags TCP_ACK) ackno pcb-snd_nxt pcb-unsent NULL) { recv_flags | TF_CLOSED; } break; ... } /* switch (pcb-state) */ }与7.2章节一样都是在执行完 pcb状态机核心处理函数回到tcp_input()函数后进行后续的处理真正执行资源释放的函数是tcp_input_delayed_close()在此函数内检查TF_CLOSED标志来进行释放资源。static int tcp_input_delayed_close(struct tcp_pcb *pcb) { /* 连接关闭从激活链表中移除释放 pcb 控制块 */ if (recv_flags TF_CLOSED) { if (!(pcb-flags TF_RXCLOSED)) { /* 调用 close() 或 shutdowm() 会设置 TF_RXCLOSED * 如果没有设置就上报 ERR_CLSD 连接关闭错误 */ TCP_EVENT_ERR(pcb-state, pcb-errf, pcb-callback_arg, ERR_CLSD); } tcp_pcb_remove(tcp_active_pcbs, pcb); //从活跃链表中移除 tcp_free(pcb); //释放资源 return 1; } return 0; }从活跃链表中移除就会将状态切换为CLOSED不过后续就释放资源了可以理解为释放前的最后状态标记走个状态流程。

更多文章