你有没有经历过这样一通电话:先拨号,听到对方说“喂”,你回一句“听到听到”,通话正式开始;聊完要说再见,你说“我挂了”,对方说“好”,你挂了电话却还握着听筒多看了两秒,才彻底放回底座——这整个过程,其实就是 TCP(传输控制协议)状态机的日常。 一次 TCP 连接从诞生到销毁,会像这台电话一样经历 11 个“心情状态”:从拨号前的待机(LISTEN)、拨出后的等待(SYN_SENT),到通话中的畅通(ESTABLISHED),再到告别时的拉锯(FIN_WAIT_1、CLOSE_WAIT……),最后还有那个“握着听筒多看两秒”的收尾状态(TIME_WAIT)。这些状态不是随意的,它们一环扣一环,构成了网络世界里最经典的一张“状态迁移图”。 看懂这张图,你就能回答这些灵魂拷问:为什么 TCP 要握手三次而不是两次?为什么断开要四步?服务器上那个令人头疼的 CLOSE_WAIT 到底是卡在了哪一步?《TCP状态机的11种状态》就是网络排查的地基。

岗位关联

对任何一个写网络程序、排查线上连接异常的 AI 从业者来说,状态机是你定位“连接卡住、端口耗尽、响应变慢”的第一张地图。学会它,你就有了一双透视网络的眼睛。

目录
  1. 可视化演示
  2. 状态机总览
  3. 建立连接三握手
  4. 数据传输保序
  5. 正常挥手四步曲
  6. 边界异常状态
  7. 定时器配合
  8. 故障排查实战
  9. AI 应用场景
  10. Python 演示
  11. 常见误区
  12. 面试题

可视化演示

通过三次握手、四次挥手和11状态全景图三个模式,完整展示TCP连接生命周期的状态迁移机制。

本节配有交互动效(进入视口后自动加载,约 3KB)。

TCP三次握手状态迁移 客户端:CLOSED→SYN_SENT→ESTABLISHED 服务端:LISTEN→SYN_RCVD→ESTABLISHED Client: SYN → Server: SYN+ACK → Client: ACK 三次握手确保双方都具备发送和接收能力,SYN同步序列号

状态机总览

想象你是一台老式电话机。你的生活只有十一个“人生阶段”:在底座上待机(CLOSED)、有人拨号时响铃(LISTEN)、拨出电话等对方接听(SYN_SENT)、听到对方说“喂”的那一瞬间(SYN_RCVD)、通话中(ESTABLISHED)、以及挂断前的各种拉扯(FIN_WAIT_1、FIN_WAIT_2、CLOSE_WAIT、LAST_ACK)、挂断后的发呆(TIME_WAIT)、还有两个极端情况(CLOSING 和回到 CLOSED)。这四个阶段加起来,正好十一种。

这套“人生阶段”在计算机网络里就叫做状态机。TCP 协议把一次连接的生命周期拆成了 11 种状态,每种状态都有一个名字,名字本身就是它的说明书:待机叫 CLOSED,监听叫 LISTEN,建立中叫 SYN_SENT/SYN_RCVD,传输中叫 ESTABLISHED,关闭中叫 FIN_WAIT_1 等。状态之间不是乱跳的,只有特定“事件”发生才会迁移——就像电话只有听到铃声才会响铃,只有你拿起听筒才会进入通话。

为什么我们要关心这 11 种状态?因为网络程序出错时,netstat 显示的正是状态名。看到一堆 CLOSE_WAIT,你知道是有连接“该挂没挂”;看到大量 SYN_RCVD,你怀疑有人只拨号不接听(SYN 洪泛攻击)。状态机的价值,就是让你一眼看出连接卡在电话旅程的哪一步。

你看,这 11 种状态就像电话旅程的 11 个节点,每个节点都对应一个“对话动作”。理解了这张大图,接下来的每一节就是逐个节点地“试机”。

状态总数=1(待机)+2(握手)+1(通话)+4(告别)+3(收尾)=11\displaystyle \text{状态总数} = 1(\text{待机}) + 2(\text{握手}) + 1(\text{通话}) + 4(\text{告别}) + 3(\text{收尾}) = 11
11 种状态不是随机凑数,而是把电话旅程的每个“动作间隙”都记了一笔账。
  1. 待机 1 种(CLOSED)电话放在底座上,没有连接,所有 TCP 连接都从这里开始、回到这里结束
    CLOSED→open()SYN_SENT\displaystyle CLOSED \xrightarrow{\text{open()}} SYN\_SENT
  2. 握手 2 种(SYN_SENT、SYN_RCVD)拨号方发出 SYN 后进入“等接听”,接听方收到 SYN 后进入“回应中”
    SYN_SENT→收到SYN+ACKESTABLISHED\displaystyle SYN\_SENT \xrightarrow{\text{收到SYN+ACK}} ESTABLISHED
  3. 通话 1 种(ESTABLISHED)双方确认互通,进入长达整个数据传输阶段的状态
    ESTABLISHED→close()FIN_WAIT_1\displaystyle ESTABLISHED \xrightarrow{\text{close()}} FIN\_WAIT\_1
  4. 告别 4 种关闭连接的四个回合,每回合都有一方“说再见”、一方“应声”
    FIN_WAIT_1→FIN_WAIT_2→TIME_WAIT\displaystyle FIN\_WAIT\_1 \rightarrow FIN\_WAIT\_2 \rightarrow TIME\_WAIT
  5. 收尾 3 种(TIME_WAIT、CLOSING、CLOSED)最后一次确认后的等待、双方同时关闭的罕见情况、以及最终归零
    TIME_WAIT→2MSL超时CLOSED\displaystyle TIME\_WAIT \xrightarrow{\text{2MSL超时}} CLOSED

建立连接三握手

电话接通前最紧张的一幕:你拨出号码,听筒里传来“嘟——嘟——”的回铃音,然后对方拿起电话说“喂”,你赶紧回一句“喂,听到了”。TCP 建立连接也上演同样的三次对话,史称“三次握手”。

第一步,客户端发送一个 SYN(同步)报文,好比你拨出号码,进入 SYN_SENT(拨号等待)状态。第二步,服务端收到 SYN 后,回一个 SYN+ACK(同步+确认),好比对方听到铃声拿起听筒说“喂”,进入 SYN_RCVD(响铃接听)状态。第三步,客户端再回一个 ACK 确认,好比你说“喂喂,听到了”,双方进入 ESTABLISHED(通话中)。

为什么必须是三次?想想这个场景:你只喊了一声“喂”(第一次握手),对方就以为通话开始了——那如果这声“喂”在半路被噪音吞掉了呢?双方就会“各自以为在通话”,其实谁也没听到谁。三次握手保证了一个关键结论:双方都能确认“我的消息你能收到,你的消息我也能收到”。前两次握手证明服务端能收能发,第三次握手证明客户端也能收能发,缺一次都不完整。

有人问:那两次行不行?假如只有两次,客户端发的 SYN 在网络中迷路重传了一个“幽灵包”,服务端收到后建立连接、分配资源,但客户端根本没想通话——白白浪费了资源,还可能被恶意利用。三次握手让服务端在分配资源前,必须等客户端“确认收到我的回应”,把幽灵包挡在门外。

电话这头的“喂喂听到了”就是那最后 0.5 个 RTT——对方不需要再确认你的确认,因为他说“喂”的时候已经默认你会回话。三次握手之后,双方终于进入畅快的通话阶段。

Tconnect≈1.5×RTT\displaystyle T_{\text{connect}} \approx 1.5 \times RTT
三次握手只需 1.5 个往返时间:SYN 发出到收到 SYN+ACK 是一个完整往返,最后的 ACK 只需半个往返(不用等回应)。
  1. RTT(往返时延)一个数据包从 A 到 B 再回到 A 的时间,相当于打电话时“我说一句到你听到”的间隔
    RTT=t收到回复−t发出请求\displaystyle RTT = t_{\text{收到回复}} - t_{\text{发出请求}}
  2. 1.5 倍的计算SYN 和 SYN+ACK 占一个完整 RTT,最后那个 ACK 只有去程、无需回程,所以再算 0.5 个 RTT
    Tconnect=RTT+0.5×RTT=1.5×RTT\displaystyle T_{\text{connect}} = RTT + 0.5 \times RTT = 1.5 \times RTT
  3. 为什么 ACK 不算完整 RTTTCP 是单方向确认的,最后一个 ACK 不需要对方回应,连接就已经建立

数据传输保序

通话中最怕什么?怕你说“今晚八点老地方见”,对方听成“明晚八点老地方见”。网络传输比这更糟:数据包可能走不同的路,先发的反而后到。TCP 怎么解决这个问题?靠的是 ESTABLISHED 状态下的三件法宝:序列号、确认号、滑动窗口。

序列号像是给每个字节贴上了“编号”:第 1 个字节是序列号 1,第 2 个是序列号 2……接收方拿到包后先看编号,按编号排队,乱序的包先存着,缺的序号就标记“还没到”。这就像你把信撕成 10 片寄出去,每片都写上页码,对方收到后按页码拼起来,哪怕 7、8、9 片比 3、4、5 片先到,也能拼出完整内容。

确认号是接收方的“回执”:收到第 N 号字节后,回一个 ACK 说“我收到 N 了,请从 N+1 继续发”。发送方如果发现某个确认迟迟不来,就知道那个包丢了,触发重传。这就是“确认重传”机制——像快递签收一样,没签收就再发一次。

滑动窗口解决的是“发多快”的问题:发送方不需要发一个等一个,而是可以一口气把“窗口”内的所有包全发出去,窗口大小由接收方的缓冲区决定。好比打电话时你不需要每说一个字就停下来等对方“嗯”一声,而是可以连续说一段话,对方一次性回应“都听到了”。窗口越大,单位时间能“说”的字越多,这就是吞吐量的来源。

ESTABLISHED 是整趟电话旅程中最长、最快乐的一段。前面所有的握手,都是为了这一刻的信息畅通;后面所有的挥手,也都是为了给这段通话画一个体面的句号。

Throughput≈WindowRTT\displaystyle Throughput \approx \frac{\text{Window}}{\text{RTT}}
吞吐量 ≈ 窗口大小 ÷ 往返时延:窗口像你的“说话量”,RTT 像“说一句到对方回应”的间隔。
  1. Window(滑动窗口)发送方无需等待确认就能连续发送的字节数,窗口越大,一次能说的话越多
    Window=min⁡(接收方通告窗口,拥塞窗口)\displaystyle Window = \min(\text{接收方通告窗口}, \text{拥塞窗口})
  2. RTT(往返时延)每一批数据从发出到收到确认的时间,相当于“你说完到对方回应”的周期
    Throughput=WindowRTT\displaystyle Throughput = \frac{\text{Window}}{RTT}
  3. 关系理解窗口固定时,RTT 越大吞吐越低;同样 RTT 下,窗口越大吞吐越高——这就是为什么高延迟链路上要用大窗口

正常挥手四步曲

通话结束时的尴尬时刻:你想挂电话,但对方还在滔滔不绝。TCP 的关闭也讲究礼节——谁先挂,谁就要走完四步。

第一步:主动关闭方调用 close(),发出一个 FIN 报文,进入 FIN_WAIT_1,意思是“我要挂了”。

第二步:被动关闭方收到 FIN,回一个 ACK,进入 CLOSE_WAIT,意思是“知道了,但我的事还没说完”。主动方收到这个 ACK 后进入 FIN_WAIT_2,安静地等对方把话说完。

第三步:被动方终于处理完手头数据,发出自己的 FIN,进入 LAST_ACK,意思是“好了,我也说完了,该挂了”。

第四步:主动方收到 FIN 后回最后一个 ACK,进入 TIME_WAIT;被动方收到 ACK 后直接 CLOSED,挂断电话。

注意,这四步是两个角色各管一半:主动方经历了 FIN_WAIT_1 → FIN_WAIT_2 → TIME_WAIT,被动方经历了 CLOSE_WAIT → LAST_ACK → CLOSED。如果你在服务器上看到大量 CLOSE_WAIT,说明被动方“该挂没挂”——多半是应用层忘记调用 close(),或者没把连接释放干净。

还有一个细节:为什么被动方的 ACK 和 FIN 要分成两步?因为被动方收到“我要挂了”时,可能还有数据没发完,所以先回 ACK 稳住对方,等数据发完再发 FIN。如果被动方恰好没有数据要发,它可以把 ACK 和 FIN 合并成一个报文,四步就压缩成了三步。

电话挂断那一下,“咔嗒”声背后其实藏着两轮确认。如果哪一轮确认丢了,电话另一端的人就会困惑:他到底听到我说的再见了吗?TCP 用 TIME_WAIT 这个“握听筒发呆”的动作来收尾,保证双方都死得明白。

Tclose=2×RTT\displaystyle T_{\text{close}} = 2 \times RTT
挥手四步正好花了两个往返:第一个 RTT 是"FIN→ACK“(你说挂,我回应),第二个 RTT 是”FIN→ACK"(我说完,你确认)。
  1. 第一个 RTT主动方发 FIN → 被动方回 ACK,等于你说“我挂了”,对方说“好”
    FIN→RTT1ACK\displaystyle \text{FIN} \xrightarrow{\text{RTT}_1} \text{ACK}
  2. 第二个 RTT被动方发 FIN → 主动方回 ACK,等于对方说“我挂了”,你说“好”
    FIN→RTT2ACK\displaystyle \text{FIN} \xrightarrow{\text{RTT}_2} \text{ACK}
  3. RTT 数为什么固定是 2恰好四步报文、两个来回,不多不少——每次“告别”都要一个完整往返

边界异常状态

正常挥手之外,TCP 还有三个“边界状态”,它们是电话旅程中的极端情况:TIME_WAIT(挂断后发呆)、CLOSING(双方同时说再见)、CLOSED(电话归位)。

先说 TIME_WAIT。主动关闭方发出最后一个 ACK 后,没有立刻进入 CLOSED,而是进入 TIME_WAIT,等待 2 × MSL(MSL 是报文在网络中的最大生存时间,通常 30 秒到 2 分钟)。为什么?两个原因。第一,怕最后一个 ACK 丢了,被动方会重发 FIN,主动方需要留在原地重新回 ACK。第二,怕这条连接上的“残余报文”游荡到网络中,万一被新连接误收,会造成数据混乱。所以它要等 2 个 MSL,相当于“挂完电话还要握两秒听筒,确认对方真的听到了再见,也让线路上残留的信号消散干净”。

再说 CLOSING。这是最罕见的双人舞:双方同时调用 close(),互相发 FIN,互相等对方的 ACK——就像两个人几乎同一秒说“我先挂了!”,结果谁也没挂成,只好面面相觑。这个状态是 FIN_WAIT_1 的一种特殊去向:主动方发了 FIN 后,没等到 ACK,反而先收到了对方的 FIN。CLOSING 状态存在的时间极短,抓包都很难碰到。

最后是 CLOSED。它既是旅程的起点也是终点。严格说 TCP 有 12 种状态(CLOSED + 11 种),但标准上 CLOSED 只是“无连接”的抽象。还有一个容易混淆的概念叫半关闭:一端发 FIN 关闭发送方向后,另一端仍可以继续发数据——对应 FIN_WAIT_2 和 CLOSE_WAIT 这对组合。这就像一人挂了电话,但把听筒留给还在说话的人:“你说,我听着。”

这“握着听筒的两秒”是 TCP 设计者的一份执念:宁可晚两秒挂断,也不能让一次告别留下隐患。理解了 TIME_WAIT,你就能解释生产环境里“端口被占满”的奇案了。

TTIME_WAIT=2×MSL\displaystyle T_{\text{TIME\_WAIT}} = 2 \times MSL
时间等待 = 两倍报文最大生存时间,给最后一个确认和所有残留报文留足消散时间。
  1. MSL(报文最大生存时间)一个数据包在网络中最多存活多久,超过就丢弃——相当于电话信号在电线里最多回荡多久
    MSL≈30s∼2min\displaystyle MSL \approx 30\text{s} \sim 2\text{min}
  2. 乘 2 的原因一个 MSL 留给最后一个 ACK 送达到对方,另一个 MSL 留给可能的重传 FIN 往返——一个来回正好 2 个 MSL
    TTIME_WAIT=MSL(去)+MSL(回)=2×MSL\displaystyle T_{\text{TIME\_WAIT}} = MSL(\text{去}) + MSL(\text{回}) = 2 \times MSL
  3. 等待期间的作用确保被动方收到最后的 ACK 能顺利关闭,且旧连接的报文不会串到新连接里

定时器配合

打电话时,如果你说了一句话对方半天没反应,你会“喂?喂?”地追问。TCP 也是靠一组定时器来完成这种“追问”的,它们和状态机紧密绑定,决定了一个连接在某个状态停留多久、重试几次。

重传定时器:发送方发出数据后启动定时器,如果在 RTO(重传超时时间)内没收到 ACK,就把数据重发一次。这个 RTO 不是拍脑袋定的,而是根据历史 RTT 动态计算的:最近 RTT 稳定就设短一点,抖动大就设长一点,避免“误杀”慢包。公式里的 SRTT 是平滑后的平均 RTT,RTTVAR 是 RTT 的抖动程度,4 倍抖动是给极端情况留的余量。

保活定时器:TCP 连接建立后如果长时间没数据,谁也不知道对方是死是活。保活定时器每隔一段时间(默认 2 小时)发一个探测包,如果对端没回应就重新探测,多次无响应就判定连接断开,把状态从 ESTABLISHED 强行拉回 CLOSED。这就好比电话通了但没人说话,你每隔一会儿“喂”一声,喊不应就挂断。

2MSL 定时器:就是上一节说的 TIME_WAIT 计时器,从发出最后一个 ACK 开始倒计时,2 个 MSL 后把连接从 TIME_WAIT 释放回 CLOSED。还有一个超时重试的概念:建立连接时 SYN 发出去没回应,会重试多次(tcp_syn_retries,默认 6 次),每次超时时间翻倍,最终放弃。

定时器就是 TCP 的“耐心刻度表”:什么时候该等,等多久,等几次,全部量化为毫秒级的判断。没有它们,状态机就像没有闹钟的人,要么早起傻等,要么睡过头误事。

RTO=SRTT+4×RTTVAR\displaystyle RTO = SRTT + 4 \times RTTVAR
重传超时 = 平均往返时间 + 4 倍往返抖动,像你根据对方平常回话速度和偶尔的迟疑,决定“等多久算他没听见”。
  1. SRTT(平滑 RTT)对历史 RTT 做指数加权平均,越新的样本权重越大,避免均值被旧数据带偏
    SRTT=(1−α)×SRTT+α×RTT,α≈0.125\displaystyle SRTT = (1-\alpha) \times SRTT + \alpha \times RTT, \quad \alpha \approx 0.125
  2. RTTVAR(RTT 抖动)衡量 RTT 波动程度的指标,标准差越大代表网络越不稳定
    RTTVAR=(1−β)×RTTVAR+β×∣RTT−SRTT∣,β≈0.25\displaystyle RTTVAR = (1-\beta) \times RTTVAR + \beta \times |RTT - SRTT|, \quad \beta \approx 0.25
  3. 4 倍抖动给异常情况留足缓冲,让重传定时器既不过于激进也不过于保守

故障排查实战

纸上谈兵结束,现在上战场。假设你是一台生产服务器的运维,半夜收到告警:“连接数飙升,服务响应变慢”。你 ssh 登上服务器,敲下 netstat -an | awk '{print $6}' | sort | uniq -c | sort -rn,看到一列状态统计。这时候,状态机知识就是你破案的钥匙。

第一类异常:CLOSE_WAIT 堆积。 大量连接卡在 CLOSE_WAIT,说明服务端收到了对方的 FIN,也回了 ACK,但应用层没有调用 close() 释放连接。这是典型的应用层泄漏——比如 Java 代码里忘了关 Socket,或者线程池里的连接没有归还。排查方法:lsof -i 看是哪个进程占据了连接,再用 jstack 看线程栈卡在哪个业务逻辑上,定位到代码后补上 close() 或 try-with-resources。

第二类异常:SYN_RCVD 堆积。 大量连接停在 SYN_RCVD,说明服务端发了 SYN+ACK 但一直等不到客户端的 ACK。两种可能:一是客户端请求太猛、服务端 accept 队列满了(正常拥塞);二是 SYN 洪泛攻击——攻击者发一堆不回的 SYN,把半连接队列塞满。验证方法:抓包看源 IP 是否分散、是否有重传的 SYN+ACK。对付洪泛可以用 tcp_syncookies 内核参数,握手不分配资源,验证通过才建连。

第三类异常:TIME_WAIT 过多。 高并发短连接的场景(比如 Nginx 代理大量小请求),主动关闭方会积压几万个 TIME_WAIT 连接,占用本地端口号,端口耗尽后新连接就建立不了。经典解法:调整 tcp_tw_reuse(复用处于 TIME_WAIT 的连接)或 tcp_fin_timeout(缩短 TIME_WAIT 时长)。但要注意 tcp_tw_reuse 依赖时间戳,跨 NAT 时可能踩坑。

排查网络故障就像修电话线路:先看电话机处于哪个状态,再顺着状态反推是拨号没通、对方没接、还是线断了。有了这张状态机地图,你不再是“重启大法救一切”的门外汉,而是能精准定位到“这台电话卡在了'对方说好但没挂'这一步”的专家。

告警={1,CLOSE_WAIT 连接数≥T阈值0,其他情况\displaystyle \text{告警} = \begin{cases} 1, & \text{CLOSE\_WAIT 连接数} \geq T_{\text{阈值}} \\ 0, & \text{其他情况} \end{cases}
大部分异常状态都能总结成一条“阈值告警”规则:超过阈值就告警,然后顺着状态名回推是哪一个环节掉了链子。
  1. N(CLOSE_WAIT)当前处于 CLOSE_WAIT 状态的连接数,netstat 或 ss 一查便知
    NCLOSE_WAIT=∑1[state=CLOSE_WAIT]\displaystyle N_{\text{CLOSE\_WAIT}} = \sum \mathbb{1}[\text{state} = \text{CLOSE\_WAIT}]
  2. T(阈值)根据服务正常水平设定的经验值,比如正常情况下应为 0,持续大于 100 就要告警
    T=正常基线×3\displaystyle T = \text{正常基线} \times 3
  3. 判断逻辑超过阈值触发告警,然后结合抓包确认是哪个方向、哪个进程的问题

AI 应用场景

服务端 TIME_WAIT 端口耗尽优化

阶段:模型训练 / 推理服务的通信底座调优

解决的问题:AI 服务的训练集群里,参数服务器与 worker 之间频繁建立短连接(PyTorch DDP、gRPC 通信)。每次通信结束主动关闭方都会产生一个 TIME_WAIT 连接,高并发下本地端口很快耗尽,表现为“连接超时、Address already in use”且难以复现。

具体案例:某分布式训练任务在 200 个 worker 同步梯度时,每个 worker 每秒建 500 个 gRPC 连接,几分钟内 TIME_WAIT 积压到 3 万,端口范围(默认 28232 个)被占满,训练直接卡死。用 netstat -s 确认 TIME_WAIT 溢出后,开启 net.ipv4.tcp_tw_reuse=1 并缩短 tcp_fin_timeout=15,端口复用后训练恢复稳定。

微服务 CLOSE_WAIT 泄漏排查

阶段:模型推理服务的健康检查与故障恢复

解决的问题:HTTP 客户端调推理服务时主动断开连接,但服务端应用代码忘记关闭底层 socket,导致连接卡在 CLOSE_WAIT,文件描述符被耗尽,服务失去响应。传统排查靠“重启大法”,治标不治本。

具体案例:某在线推荐系统每夜高峰后连接数只增不减,ss -s 显示 CLOSE_WAIT 从 0 涨到 2000。用 ss -tnp 定位到占用进程,jstack 发现线程阻塞在数据库连接池的归还逻辑上,连接没有路由到 close()。修复代码后在 CI 里加了“状态数检查”流水线,CLOSE_WAIT 超过阈值即失败告警。

网络状态监控看板

阶段:在线学习 / 实时推理系统的可观测性建设

解决的问题:4 层负载均衡器(LVS/DPVS)上流量突增时,哪些状态在剧变?人工登录服务器一条条看状态太慢,需要把 TCP 状态分布变成实时指标,联动告警。

具体案例:用 Prometheus 的 node_exporter 定时采集 /proc/net/tcp,按状态(ESTABLISHED、SYN_RCVD、CLOSE_WAIT、TIME_WAIT)聚合出 11 个状态的时间序列曲线。某次线上故障中,SYN_RCVD 曲线陡增,看板自动触发“疑似 SYN 洪泛”告警,值班人员 3 分钟内确认是外部扫描器攻击,用 iptables 限流后曲线回落。

Python 演示

python48 行
import time class TCPConnection:    """一个迷你 TCP 状态机:用状态迁移模拟三次握手与四次挥手"""    def __init__(self, name):        self.name = name        self.state = 'CLOSED'        self.history = []     def transition(self, new_state, event):        old = self.state        self.state = new_state        line = '{}: {} --{}--> {}'.format(self.name, old, event, new_state)        self.history.append(line)        print(line) def three_way_handshake(client, server):    client.transition('SYN_SENT', 'send SYN')    server.transition('SYN_RCVD', 'recv SYN, send SYN+ACK')    client.transition('ESTABLISHED', 'recv SYN+ACK, send ACK')    server.transition('ESTABLISHED', 'recv ACK') def four_way_handshake(client, server):    client.transition('FIN_WAIT_1', 'send FIN')    server.transition('CLOSE_WAIT', 'recv FIN, send ACK')    client.transition('FIN_WAIT_2', 'recv ACK')    server.transition('LAST_ACK', 'send FIN')    client.transition('TIME_WAIT', 'recv FIN, send ACK')    server.transition('CLOSED', 'recv ACK')    client.transition('CLOSED', '2MSL timeout') def simulate():    client = TCPConnection('client')    server = TCPConnection('server')    print('==== 三次握手:建立连接 ====')    three_way_handshake(client, server)    print('\n==== 数据通信进行中 ====')    print('client: ESTABLISHED  send 1024 bytes')    print('server: ESTABLISHED  recv 1024 bytes')    print('\n==== 四次挥手:关闭连接 ====')    four_way_handshake(client, server)    print('\n==== 最终状态 ====')    print('client final state:', client.state)    print('server final state:', server.state) if __name__ == '__main__':    simulate()    time.sleep(0)  # 占位,保证文件可独立运行
目的:用两个 TCPConnection 对象分别模拟客户端和服务端,完整走一遍三次握手和四次挥手的状态迁移,直观看到每个状态由哪个事件触发。
  1. class TCPConnection封装一个 TCP 端点,用 state 属性记录当前状态,history 记录全部迁移轨迹
  2. def transition(self, new_state, event)状态迁移的公共入口,每次迁移打印“旧状态——事件——>新状态”
  3. three_way_handshake(client, server)三次握手:SYN、SYN+ACK、ACK 三个事件驱动双方从 CLOSED 走到 ESTABLISHED
  4. four_way_handshake(client, server)四次挥手:FIN、ACK、FIN、ACK 四个事件驱动双方从 ESTABLISHED 走到 CLOSED
  5. client.transition('TIME_WAIT', 'recv FIN, send ACK')主动关闭方收到 FIN 后进入 TIME_WAIT,等待 2MSL 后归零

常见误区

误区“断开连接后,主动和被动双方都会进入 `TIME_WAIT`,所以服务器上大量 `TIME_WAIT` 很正常,不用管。”

正解TIME_WAIT 只出现在主动关闭方,被动关闭方收到最后一个 ACK 后直接进入 CLOSED。电话的比喻里,TIME_WAIT 是“先挂电话的人握听筒发呆”,后挂的人不会发呆。明白这一点非常关键:如果你在服务端看到大量 TIME_WAIT,说明你的服务是主动断开的那一方(比如反向代理),需要优化连接复用;如果是 CLOSE_WAIT 堆积,那才是“对方挂了你不挂”的应用层问题。

为何常见:很多人只记住了“挥手四步”的完整链路图,没区分哪些状态属于哪一方,被动方在图上也有 CLOSE_WAIT 和 LAST_ACK,就误以为 TIME_WAIT 双方都有。

误区“TCP 关闭连接必须是四次挥手,每次都是独立的报文,所以总是需要 4 个包。”

正解四次挥手是“最标准、最完整”的流程,但在实际中可以压缩成 3 次甚至 2 次。如果被动关闭方在收到 FIN 时已经没有数据要发,它可以把自己的 ACK 和 FIN 合并成一个报文,这样总报文数变成 3 个。更极端地,如果双方同时调用 close(),就会出现 CLOSING 状态,流程也略不同于标准的四次挥手。所以严格说,挥手“最多四步”而不是“一定四步”。

为何常见:教科书和面试题都强调“四次挥手”,学员把理想模型当成了真实世界的唯一答案,忽略了 TCP 的“捎带”和“合并”优化。

误区“客户端发出 SYN 后进入 `SYN_SENT`,服务端收到 SYN 后进入 `SYN_RCVD`,所以 `SYN_RCVD` 永远只属于服务端。”

正解在标准的主动连接场景中,SYN_RCVD 确实是服务端的状态。但如果两端同时发起连接(同时 open()),双方都会发出 SYN,也都会收到对方的 SYN——此时两端的角色都是“主动打开”,都会进入 SYN_RCVD。这种“同时打开”的场景在现实中很少见(比如某些 P2P 打洞场景),但概念上完全成立。这也是为什么状态机里有 CLOSING 这种对称状态。

为何常见:常见的三次握手示意图总是画成“客户端主动、服务端被动”,学习者把角色和状态绑死,没理解状态描述的是“连接本身”而不是“某个角色”。

误区“CLOSED 意味着连接销毁,不需要单独记忆,数状态时数 10 种就够了。”

正解CLOSED 是 TCP 状态机里真实存在的状态,是每个连接的起点和终点。完整的 TCP 状态机有 12 个状态(含 CLOSED),大家在 ping 命令、netstat 里看不到它,只是因为“不存在”的连接通常不会被显示。但注意一个反直觉的细节:LISTEN 和 CLOSED 都是从 CLOSED 出发的分支——被动方 socket() + listen() 后从 CLOSED 进入 LISTEN,而主动方 connect() 后从 CLOSED 进入 SYN_SENT。CLOSED 不是“空洞”,它是状态的基准原点。

为何常见:CLOSED 没有出现在握手和挥手的链路图上,初学者数链条上的节点时自然把它漏掉,导致“11 种状态”和“链条上的 10 个节点”对不上。

面试题

Q1. 客户端调用 connect() 发出 SYN 报文后,进入以下哪个状态?

单选题 · easy

  • A.SYN_SENT← 正确答案
  • B.SYN_RCVD
  • C.ESTABLISHED
  • D.LISTEN

答案:A。客户端是主动打开方,发出 SYN 后就像拨出电话等待接听,进入 SYN_SENT。SYN_RCVD 是服务端收到 SYN 后的状态;ESTABLISHED 要等收到 SYN+ACK 并回 ACK 后才进入;LISTEN 是服务端监听时进入的状态。

Q2. TCP 三次握手中,服务端收到客户端的 SYN 后,应该返回什么报文?

单选题 · easy

  • A.仅 ACK
  • B.仅 FIN
  • C.SYN+ACK← 正确答案
  • D.RST

答案:C。服务端收到 SYN 后同时确认对方的 SYN 并同步自己的序列号,所以返回 SYN+ACK 合并报文——好比对方说“喂”,你回“喂,听到了”。仅 ACK 无法完成序列号同步,FIN 是关闭连接用的,RST 是异常复位。

Q3. TIME_WAIT 状态出现在哪一方?它的持续时间是多少?

单选题 · mid

  • A.被动关闭方,持续 1 个 MSL
  • B.被动关闭方,持续 2 个 MSL
  • C.主动关闭方,持续 1 个 MSL
  • D.主动关闭方,持续 2 个 MSL← 正确答案

答案:D。TIME_WAIT 只属于主动关闭方——先挂电话的人需要握听筒发呆。持续时间是 2 个 MSL,一个 MSL 留给最后一个 ACK 送达,另一个 MSL 给残留报文消散时间,防止旧连接的报文串到新连接。

Q4. 以下哪个状态是“半关闭”状态下被动关闭方会停留的状态?

单选题 · mid

  • A.FIN_WAIT_2
  • B.CLOSE_WAIT← 正确答案
  • C.TIME_WAIT
  • D.CLOSING

答案:B。被动方收到 FIN 并回 ACK 后进入 CLOSE_WAIT,此时它还可以继续向主动方发数据(对方只关了发送方向),这就是“半关闭”。FIN_WAIT_2 是主动方对应的状态;TIME_WAIT 是挥手结束后的等待;CLOSING 是双方同时关闭时出现。

Q5. 三次握手为什么必须是三次,而不是两次?

单选题 · mid

  • A.三次才能协商窗口大小
  • B.两次无法防止过期的 SYN 报文建立无效连接← 正确答案
  • C.三次才能传输数据
  • D.三次是为了加快速度

答案:B。两次握手无法解决“幽灵包”问题——客户端第一次发的 SYN 在网络中滞留,重传后服务端才收到旧 SYN 并建立连接,白白浪费资源。三次握手中服务端必须等客户端的 ACK 才建立连接,等于确认“你的回应我能收到”,把过期分组挡在门外。

Q6. 服务器上出现大量 CLOSE_WAIT 连接,最可能的原因是什么?

单选题 · hard

  • A.SYN 洪泛攻击
  • B.本地端口被 TIME_WAIT 占满
  • C.应用进程收到 FIN 后没有关闭 socket← 正确答案
  • D.TCP 丢包严重触发重传

答案:C。CLOSE_WAIT 是对方发了 FIN、你回了 ACK 但还没发 FIN 的状态。大量堆积说明应用层“知道该挂了但始终没挂”——通常是代码忘了调用 close() 或连接没归还连接池。SYN 洪泛会堆积 SYN_RCVD,端口占满会报 Address already in use,丢包重传不会体现在这个状态上。

Q7. 在标准的四次挥手中,主动关闭方会依次经历以下哪些状态?

多选题 · mid

  • A.FIN_WAIT_1← 正确答案
  • B.FIN_WAIT_2← 正确答案
  • C.CLOSE_WAIT
  • D.TIME_WAIT← 正确答案

答案:ABD。主动关闭方发 FIN 进入 FIN_WAIT_1,收到 ACK 进入 FIN_WAIT_2,再收到对方的 FIN 并回 ACK 后进入 TIME_WAIT。CLOSE_WAIT 是被动关闭方的状态——主动关闭方扮演的是“先挂电话”的角色。

Q8. 下列哪些情况可能导致状态迁移与标准流程不同(出现异常或“少一步”)?

多选题 · hard

  • A.被动方在收到 FIN 时无数据可发,把 ACK 和 FIN 合并发送← 正确答案
  • B.双方同时调用 close(),出现 CLOSING 状态← 正确答案
  • C.收到 RST 报文,连接直接复位到 CLOSED← 正确答案
  • D.收到重复的 SYN 报文,重新进入 SYN_SENT

答案:ABC。A 是“捎带”优化,四步变三步;B 是同时打开,双方都进 CLOSING;C 是异常复位,RST 跳过挥手直接关闭。D 错误——收到重复 SYN 时服务端应回 SYN+ACK(进入 SYN_RCVD),不会把已发 SYN 的客户端拉回 SYN_SENT。

Q9. RTO(重传超时时间)是根据什么计算出来的?

单选题 · mid

  • A.固定的 3 秒
  • B.平滑 RTT 加上 4 倍 RTT 抖动← 正确答案
  • C.拥塞窗口的大小
  • D.报文的最大段长度

答案:B。RTO = SRTT + 4 × RTTVAR,其中 SRTT 是平滑后的平均往返时间,RTTVAR 是往返时间的抖动程度。这样设计让重传定时器“看人下菜碟”:网络稳就等短一点,网络抖就等长一点。固定超时和窗口大小都无法反映实时网络状况。

Q10. CLOSING 状态出现的完整触发条件是什么?

单选题 · extreme

  • A.主动关闭方收到对方的 RST 报文
  • B.服务端收到 SYN 后进入半连接队列
  • C.连接两端同时发起主动关闭,互发 FIN 后未收到 ACK 先收到 FIN← 正确答案
  • D.被动关闭方发送 FIN 后等待 ACK 超时

答案:C。CLOSING 是“双方同时说再见”的对称状态:两端同时调用 close(),各自发 FIN 后进入 FIN_WAIT_1,之后没等到 ACK 却收到对方的 FIN,于是进入 CLOSING(谁也没让着谁)。RST 复位不会经过 CLOSING,服务端收 SYN 进 SYN_RCVD,被动方发 FIN 进 LAST_ACK,都与 CLOSING 无关。