TCP 连接管理:三次握手与四次挥手

三次握手和四次挥手,几乎每个学过网络的人都能把步骤复述出来,但真正能讲清楚的人不多。问题往往出在学习顺序上:很多人是先记住"为什么要三次"这个结论,却没先搞懂握手过程中,双方的状态和序列号到底是怎么一步步变化的。结论悬在空中,自然站不稳。

所以这一篇我们换个顺序:先把机制本身讲透——每一步发了什么报文、报文里装了哪些字段、双方的连接状态如何迁移——把这套运转过程看清楚之后,再回过头解释每一步是为了解决什么问题。

要先记住一件事:TCP 是一个有状态的协议。每一条连接的两端,内核里都各自维护着一个状态机。握手和挥手的全过程,本质上就是这两个状态机在报文的驱动下一步步迁移。盯住状态的变化,整件事就清楚了。

一、先认识几个关键字段和状态

要看懂机制,得先认识参与其中的"零件"。

报文头里我们这次重点关注三个标志位和两个编号:

  • SYN:同步标志,置 1 表示这是一个用于建立连接、同步序列号的报文;
  • ACK:确认标志,置 1 表示报文里的"确认号"字段有效;
  • FIN:结束标志,置 1 表示发送方已经没有数据要发了;
  • seq(序列号):本报文所携带数据的第一个字节的编号;
  • ack(确认号):期望收到对方的下一个字节的编号,等于"已收到的最后一个字节编号 + 1"。

连接两端各自会经历一系列状态,建立阶段涉及:CLOSEDLISTENSYN_SENTSYN_RCVDESTABLISHED;关闭阶段涉及:FIN_WAIT_1FIN_WAIT_2CLOSE_WAITLAST_ACKTIME_WAITCLOSED

下面所有内容,就是看这些状态在报文驱动下怎么走。先不用记,跟着流程走一遍自然就懂了。

二、三次握手的机制:状态与序列号怎么变

我们假设客户端主动连接服务端。服务端已经在 LISTEN 状态等待,客户端处于 CLOSED。先用一张时序图建立整体印象,再逐步拆解:

sequenceDiagram participant C as 客户端 participant S as 服务端 Note over C: CLOSED Note over S: LISTEN C->>S: SYN=1, seq=x Note over C: SYN_SENT S->>C: SYN=1, ACK=1, seq=y, ack=x+1 Note over S: SYN_RCVD C->>S: ACK=1, seq=x+1, ack=y+1 Note over C: ESTABLISHED Note over S: ESTABLISHED

第一步——客户端发送 SYN:

客户端选一个初始序列号(ISN),设 seq = x,把 SYN 标志置 1,发出报文。

  • 报文内容:SYN=1, seq=x
  • 客户端状态:CLOSEDSYN_SENT

注意 SYN 报文本身不携带数据,但它会消耗一个序列号,也就是说它逻辑上占了编号 x 这个位置。

第二步——服务端回 SYN + ACK:

服务端收到后,也选自己的初始序列号 seq = y,同时确认客户端的 SYN。

  • 报文内容:SYN=1, ACK=1, seq=y, ack=x+1
  • ack = x+1 表示:“你的 x 我收到了,下一个我期望收到 x+1”
  • 服务端状态:LISTENSYN_RCVD

这一个报文同时干了两件事:用 SYN 同步自己的序列号 y,用 ACK 确认对方的序列号 x。

第三步——客户端回 ACK:

客户端收到后,确认服务端的序列号。

  • 报文内容:ACK=1, seq=x+1, ack=y+1
  • ack = y+1 表示:“你的 y 我也收到了”
  • 客户端状态:SYN_SENTESTABLISHED
  • 服务端收到这个 ACK 后:SYN_RCVDESTABLISHED

到这里,双方都进入 ESTABLISHED,连接建立完成,后续数据传输的序列号就分别从 x+1 和 y+1 接着往下走。

把机制串起来看,三次握手其实是两组"SYN + 对应 ACK":客户端的 SYN(x) 被服务端的 ACK(x+1) 确认,服务端的 SYN(y) 被客户端的 ACK(y+1) 确认。只不过中间那一次,服务端把"确认你的"和"同步我的"合并进了一个报文,所以看起来是三次而不是四次。

三、看懂机制后,再问:为什么必须三次

现在双方的状态迁移和序列号交换已经清楚了,我们再来回答那个经典问题,就会非常自然。

可靠通信要求两个方向的收发能力都被确认,也就是上面说的两组 SYN-ACK 都要完成。我们数一下,到第几步才凑齐:

  • 第二步结束时:客户端确认了"我发的 x 服务端收到了",也知道"服务端能发能收";
  • 第三步结束时:服务端才确认"我发的 y 客户端收到了"。

如果砍掉第三次,服务端永远无法确认"自己的 SYN(y) 到底有没有送达客户端"——它的序列号同步是单向的、没被确认的。少了这一步,可靠性的前提就缺了一角。

还有一个机制层面的理由——防止历史连接。假设客户端一个很早的 SYN 在网络里滞留许久才到达服务端,客户端其实早已放弃。如果只要两次握手,服务端一收到旧 SYN 就进入 ESTABLISHED 并分配资源。而有了第三次,客户端面对这个自己并不认账的连接,会回一个 RST 而不是 ACK,服务端就不会误建。

所以"第三次握手"不是多余的礼节,它在机制上补齐了"服务端序列号是否被确认"这唯一缺口,同时给了客户端否认历史连接的机会。

四、四次挥手的机制:为什么状态多了一倍

关闭连接的状态迁移比建立更复杂,因为 TCP 是全双工的——一条连接是两个方向的字节流,每个方向要各自独立地关闭。我们还是逐报文看状态怎么走。假设客户端主动关闭。

第一步——客户端发 FIN:

  • 报文内容:FIN=1, seq=u(u 是客户端当前序列号)
  • 客户端状态:ESTABLISHEDFIN_WAIT_1
  • 含义:"我这个方向的数据发完了。"FIN 同样消耗一个序列号。

第二步——服务端回 ACK:

  • 报文内容:ACK=1, ack=u+1
  • 服务端状态:ESTABLISHEDCLOSE_WAIT
  • 客户端收到这个 ACK 后:FIN_WAIT_1FIN_WAIT_2
  • 含义:"知道你不发了。“但此时服务端可能还有数据没发完,所以它只确认、暂不发自己的 FIN。这时客户端→服务端方向已关,服务端→客户端方向仍开着——这就是所谓的"半关闭”。

第三步——服务端发 FIN:

等服务端把自己剩下的数据都发完,它才发 FIN。

  • 报文内容:FIN=1, ACK=1, seq=w, ack=u+1
  • 服务端状态:CLOSE_WAITLAST_ACK
  • 含义:“现在我这个方向也发完了。”

第四步——客户端回 ACK:

  • 报文内容:ACK=1, ack=w+1
  • 客户端状态:FIN_WAIT_2TIME_WAIT
  • 服务端收到后:LAST_ACKCLOSED

机制上最关键的就是第二步和第三步不能合并:服务端收到 FIN 时手里很可能还有数据要发,所以"确认对方关闭"和"自己也要关闭"是两个时刻、两个报文。这正是挥手比握手多一次的根源——握手时服务端能把 ACK 和 SYN 合并,挥手时却不行。

五、TIME_WAIT 的机制:它在等什么

上面看到,主动关闭方在发出最后一个 ACK 后,没有直接 CLOSED,而是进入 TIME_WAIT,停留约 2 倍报文最大生存时间(2MSL)才真正关闭。从机制上讲,这段等待守着两件具体的事:

  1. 兜底最后那个 ACK。 万一第四步的 ACK 在网络里丢了,服务端(处于 LAST_ACK)会重发它的 FIN。如果客户端发完 ACK 就立刻 CLOSED,就没人回应这个重发的 FIN,服务端会一直卡在 LAST_ACK。停留在 TIME_WAIT 就是为了能重发这个 ACK。
  2. 让旧报文自然消亡。 等满 2MSL,能确保这条连接在网络里所有迟到的残留报文都已过期消失,不会窜进之后用相同四元组建立的新连接里造成污染。

六、把整套状态机连起来看

到这里,建立和关闭的机制都拆开了,我们用状态迁移把它串成一条完整的主线,这是这一篇最该记住的骨架:

  • 建立CLOSED → SYN_SENT → ESTABLISHED(客户端);LISTEN → SYN_RCVD → ESTABLISHED(服务端)。
  • 关闭ESTABLISHED → FIN_WAIT_1 → FIN_WAIT_2 → TIME_WAIT → CLOSED(主动方);ESTABLISHED → CLOSE_WAIT → LAST_ACK → CLOSED(被动方)。

每一次状态迁移,都是被一个具体报文(SYN / ACK / FIN)触发的。一旦你脑子里有这张状态机,再看 netstat/ss 输出里的那些状态,就知道每条连接卡在了哪一步、下一步在等什么报文。

七、抓包视角:让机制变得可见

理解状态机最快的办法,是用 tcpdump 或 Wireshark 抓一次真实连接。

抓一次完整的 HTTP 请求,你会清楚看到开头三个包 SYNSYN, ACKACK,结尾四个包 FIN, ACKACKFIN, ACKACK。更进一步,把抓包里的 Seq=Ack= 字段和上面讲的 x、x+1、y、y+1 对上号,你会非常直观地看到序列号是怎么递进和确认的——这比任何流程图都让人记得牢。

八、和工程实践 / 后端开发的联系

把状态机和真实排障对应起来,这些知识立刻就有用了:

  • CLOSE_WAIT 堆积:连接卡在 CLOSE_WAIT 说明收到了对方的 FIN,但本端应用迟迟没调用 close(),挥手停在第二步之后走不下去。这几乎总是应用代码或连接池没正确关连接的 bug,是非常强的排障信号。
  • TIME_WAIT 过多:大量连接停在 TIME_WAIT 是主动关闭方的正常产物,尤其高并发短连接下。优化方向是长连接 / 连接池 / Keep-Alive,减少连接的建立与关闭次数。
  • 连接建立的 RTT 开销:三次握手要花一个往返时间,这正是 HTTP Keep-Alive 和连接池存在的根本动机——复用已经处于 ESTABLISHED 的连接,省掉反复握手。

九、学习这一部分最容易踩的坑

1. 跳过状态机,直接背"三次/四次"

数字是机制运转的结果。不盯住状态迁移和序列号变化,结论永远记不牢。

2. 以为挥手一定是四次

如果被动方收到 FIN 时已经没有数据要发,第二步的 ACK 和第三步的 FIN 可以合并,挥手就变成三次。四次是常见情况,不是铁律。

3. 把 CLOSE_WAIT 和 TIME_WAIT 搞混

CLOSE_WAIT被动关闭方,通常是应用没 close 的 bug;TIME_WAIT主动关闭方,多数是正常现象。二者排障方向完全相反,混了就会找错人。

4. 忽略 SYN/FIN 会消耗序列号

SYN 和 FIN 虽然不带数据,但各占一个序列号,所以确认号是 x+1 而不是 x。这点想不通,序列号就永远对不上。

总结

这一篇我们坚持先讲机制、再讲设计,核心要带走的是这张状态机和它背后的因果:

  • TCP 是有状态协议,握手挥手的本质是两端状态机在 SYN / ACK / FIN 报文驱动下的迁移;
  • 三次握手 = 两组"SYN + ACK"的序列号同步与确认,中间一次把 ACK 和 SYN 合并,所以是三次;
  • 必须三次,是为了让服务端的序列号也被确认,并挡住历史连接;
  • 四次挥手源于全双工——两个方向独立关闭,被动方收到 FIN 时可能还有数据要发,故 ACK 与 FIN 不能合并;
  • TIME_WAIT 停留 2MSL,是为了兜底最后的 ACK 重传、并让旧报文自然消亡;
  • 工程上,CLOSE_WAIT 指向应用没关连接,TIME_WAIT 指向短连接过多,方向截然不同。

当你能在脑子里默画出那条 SYN_SENT → ESTABLISHED → FIN_WAIT → TIME_WAIT 的迁移链,并说清每一步是被哪个报文推动的,这部分就真正属于你了。

参考资源