你有没有经历过这样一通电话:先拨号,听到对方说“喂”,你回一句“听到听到”,通话正式开始;聊完要说再见,你说“我挂了”,对方说“好”,你挂了电话却还握着听筒多看了两秒,才彻底放回底座——这整个过程,其实就是 TCP(传输控制协议)状态机的日常。
一次 TCP 连接从诞生到销毁,会像这台电话一样经历 11 个“心情状态”:从拨号前的待机(LISTEN)、拨出后的等待(SYN_SENT),到通话中的畅通(ESTABLISHED),再到告别时的拉锯(FIN_WAIT_1、CLOSE_WAIT……),最后还有那个“握着听筒多看两秒”的收尾状态(TIME_WAIT)。这些状态不是随意的,它们一环扣一环,构成了网络世界里最经典的一张“状态迁移图”。
看懂这张图,你就能回答这些灵魂拷问:为什么 TCP 要握手三次而不是两次?为什么断开要四步?服务器上那个令人头疼的 CLOSE_WAIT 到底是卡在了哪一步?《TCP状态机的11种状态》就是网络排查的地基。
对任何一个写网络程序、排查线上连接异常的 AI 从业者来说,状态机是你定位“连接卡住、端口耗尽、响应变慢”的第一张地图。学会它,你就有了一双透视网络的眼睛。
可视化演示
通过三次握手、四次挥手和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 种(
CLOSED)电话放在底座上,没有连接,所有 TCP 连接都从这里开始、回到这里结束 - 握手 2 种(
SYN_SENT、SYN_RCVD)拨号方发出 SYN 后进入“等接听”,接听方收到 SYN 后进入“回应中” - 通话 1 种(
ESTABLISHED)双方确认互通,进入长达整个数据传输阶段的状态 - 告别 4 种关闭连接的四个回合,每回合都有一方“说再见”、一方“应声”
- 收尾 3 种(
TIME_WAIT、CLOSING、CLOSED)最后一次确认后的等待、双方同时关闭的罕见情况、以及最终归零
建立连接三握手
电话接通前最紧张的一幕:你拨出号码,听筒里传来“嘟——嘟——”的回铃音,然后对方拿起电话说“喂”,你赶紧回一句“喂,听到了”。TCP 建立连接也上演同样的三次对话,史称“三次握手”。
第一步,客户端发送一个 SYN(同步)报文,好比你拨出号码,进入 SYN_SENT(拨号等待)状态。第二步,服务端收到 SYN 后,回一个 SYN+ACK(同步+确认),好比对方听到铃声拿起听筒说“喂”,进入 SYN_RCVD(响铃接听)状态。第三步,客户端再回一个 ACK 确认,好比你说“喂喂,听到了”,双方进入 ESTABLISHED(通话中)。
为什么必须是三次?想想这个场景:你只喊了一声“喂”(第一次握手),对方就以为通话开始了——那如果这声“喂”在半路被噪音吞掉了呢?双方就会“各自以为在通话”,其实谁也没听到谁。三次握手保证了一个关键结论:双方都能确认“我的消息你能收到,你的消息我也能收到”。前两次握手证明服务端能收能发,第三次握手证明客户端也能收能发,缺一次都不完整。
有人问:那两次行不行?假如只有两次,客户端发的 SYN 在网络中迷路重传了一个“幽灵包”,服务端收到后建立连接、分配资源,但客户端根本没想通话——白白浪费了资源,还可能被恶意利用。三次握手让服务端在分配资源前,必须等客户端“确认收到我的回应”,把幽灵包挡在门外。
电话这头的“喂喂听到了”就是那最后 0.5 个 RTT——对方不需要再确认你的确认,因为他说“喂”的时候已经默认你会回话。三次握手之后,双方终于进入畅快的通话阶段。
- RTT(往返时延)一个数据包从 A 到 B 再回到 A 的时间,相当于打电话时“我说一句到你听到”的间隔
- 1.5 倍的计算SYN 和 SYN+ACK 占一个完整 RTT,最后那个 ACK 只有去程、无需回程,所以再算 0.5 个 RTT
- 为什么 ACK 不算完整 RTTTCP 是单方向确认的,最后一个 ACK 不需要对方回应,连接就已经建立
数据传输保序
通话中最怕什么?怕你说“今晚八点老地方见”,对方听成“明晚八点老地方见”。网络传输比这更糟:数据包可能走不同的路,先发的反而后到。TCP 怎么解决这个问题?靠的是 ESTABLISHED 状态下的三件法宝:序列号、确认号、滑动窗口。
序列号像是给每个字节贴上了“编号”:第 1 个字节是序列号 1,第 2 个是序列号 2……接收方拿到包后先看编号,按编号排队,乱序的包先存着,缺的序号就标记“还没到”。这就像你把信撕成 10 片寄出去,每片都写上页码,对方收到后按页码拼起来,哪怕 7、8、9 片比 3、4、5 片先到,也能拼出完整内容。
确认号是接收方的“回执”:收到第 N 号字节后,回一个 ACK 说“我收到 N 了,请从 N+1 继续发”。发送方如果发现某个确认迟迟不来,就知道那个包丢了,触发重传。这就是“确认重传”机制——像快递签收一样,没签收就再发一次。
滑动窗口解决的是“发多快”的问题:发送方不需要发一个等一个,而是可以一口气把“窗口”内的所有包全发出去,窗口大小由接收方的缓冲区决定。好比打电话时你不需要每说一个字就停下来等对方“嗯”一声,而是可以连续说一段话,对方一次性回应“都听到了”。窗口越大,单位时间能“说”的字越多,这就是吞吐量的来源。
ESTABLISHED 是整趟电话旅程中最长、最快乐的一段。前面所有的握手,都是为了这一刻的信息畅通;后面所有的挥手,也都是为了给这段通话画一个体面的句号。
- Window(滑动窗口)发送方无需等待确认就能连续发送的字节数,窗口越大,一次能说的话越多
- RTT(往返时延)每一批数据从发出到收到确认的时间,相当于“你说完到对方回应”的周期
- 关系理解窗口固定时,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 这个“握听筒发呆”的动作来收尾,保证双方都死得明白。
- 第一个 RTT主动方发 FIN → 被动方回 ACK,等于你说“我挂了”,对方说“好”
- 第二个 RTT被动方发 FIN → 主动方回 ACK,等于对方说“我挂了”,你说“好”
- 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,你就能解释生产环境里“端口被占满”的奇案了。
- MSL(报文最大生存时间)一个数据包在网络中最多存活多久,超过就丢弃——相当于电话信号在电线里最多回荡多久
- 乘 2 的原因一个 MSL 留给最后一个 ACK 送达到对方,另一个 MSL 留给可能的重传 FIN 往返——一个来回正好 2 个 MSL
- 等待期间的作用确保被动方收到最后的 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 的“耐心刻度表”:什么时候该等,等多久,等几次,全部量化为毫秒级的判断。没有它们,状态机就像没有闹钟的人,要么早起傻等,要么睡过头误事。
- SRTT(平滑 RTT)对历史 RTT 做指数加权平均,越新的样本权重越大,避免均值被旧数据带偏
- RTTVAR(RTT 抖动)衡量 RTT 波动程度的指标,标准差越大代表网络越不稳定
- 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 时可能踩坑。
排查网络故障就像修电话线路:先看电话机处于哪个状态,再顺着状态反推是拨号没通、对方没接、还是线断了。有了这张状态机地图,你不再是“重启大法救一切”的门外汉,而是能精准定位到“这台电话卡在了'对方说好但没挂'这一步”的专家。
- N(CLOSE_WAIT)当前处于
CLOSE_WAIT状态的连接数,netstat或ss一查便知 - T(阈值)根据服务正常水平设定的经验值,比如正常情况下应为 0,持续大于 100 就要告警
- 判断逻辑超过阈值触发告警,然后结合抓包确认是哪个方向、哪个进程的问题
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 演示
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 对象分别模拟客户端和服务端,完整走一遍三次握手和四次挥手的状态迁移,直观看到每个状态由哪个事件触发。class TCPConnection封装一个 TCP 端点,用state属性记录当前状态,history记录全部迁移轨迹def transition(self, new_state, event)状态迁移的公共入口,每次迁移打印“旧状态——事件——>新状态”three_way_handshake(client, server)三次握手:SYN、SYN+ACK、ACK 三个事件驱动双方从CLOSED走到ESTABLISHEDfour_way_handshake(client, server)四次挥手:FIN、ACK、FIN、ACK 四个事件驱动双方从ESTABLISHED走到CLOSEDclient.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 报文后,进入以下哪个状态?
- 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 后,应该返回什么报文?
- A.仅
ACK - B.仅
FIN - C.
SYN+ACK← 正确答案 - D.
RST
答案:C。服务端收到 SYN 后同时确认对方的 SYN 并同步自己的序列号,所以返回 SYN+ACK 合并报文——好比对方说“喂”,你回“喂,听到了”。仅 ACK 无法完成序列号同步,FIN 是关闭连接用的,RST 是异常复位。
Q3. TIME_WAIT 状态出现在哪一方?它的持续时间是多少?
- A.被动关闭方,持续 1 个 MSL
- B.被动关闭方,持续 2 个 MSL
- C.主动关闭方,持续 1 个 MSL
- D.主动关闭方,持续 2 个 MSL← 正确答案
答案:D。TIME_WAIT 只属于主动关闭方——先挂电话的人需要握听筒发呆。持续时间是 2 个 MSL,一个 MSL 留给最后一个 ACK 送达,另一个 MSL 给残留报文消散时间,防止旧连接的报文串到新连接。
Q4. 以下哪个状态是“半关闭”状态下被动关闭方会停留的状态?
- A.
FIN_WAIT_2 - B.
CLOSE_WAIT← 正确答案 - C.
TIME_WAIT - D.
CLOSING
答案:B。被动方收到 FIN 并回 ACK 后进入 CLOSE_WAIT,此时它还可以继续向主动方发数据(对方只关了发送方向),这就是“半关闭”。FIN_WAIT_2 是主动方对应的状态;TIME_WAIT 是挥手结束后的等待;CLOSING 是双方同时关闭时出现。
Q5. 三次握手为什么必须是三次,而不是两次?
- A.三次才能协商窗口大小
- B.两次无法防止过期的 SYN 报文建立无效连接← 正确答案
- C.三次才能传输数据
- D.三次是为了加快速度
答案:B。两次握手无法解决“幽灵包”问题——客户端第一次发的 SYN 在网络中滞留,重传后服务端才收到旧 SYN 并建立连接,白白浪费资源。三次握手中服务端必须等客户端的 ACK 才建立连接,等于确认“你的回应我能收到”,把过期分组挡在门外。
Q6. 服务器上出现大量 CLOSE_WAIT 连接,最可能的原因是什么?
- 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. 在标准的四次挥手中,主动关闭方会依次经历以下哪些状态?
- 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. 下列哪些情况可能导致状态迁移与标准流程不同(出现异常或“少一步”)?
- 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(重传超时时间)是根据什么计算出来的?
- A.固定的 3 秒
- B.平滑 RTT 加上 4 倍 RTT 抖动← 正确答案
- C.拥塞窗口的大小
- D.报文的最大段长度
答案:B。RTO = SRTT + 4 × RTTVAR,其中 SRTT 是平滑后的平均往返时间,RTTVAR 是往返时间的抖动程度。这样设计让重传定时器“看人下菜碟”:网络稳就等短一点,网络抖就等长一点。固定超时和窗口大小都无法反映实时网络状况。
Q10. CLOSING 状态出现的完整触发条件是什么?
- 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 无关。