建连接三个包,断连接四个包:不对称的原因是「半关闭」。 TIME_WAIT 要等 2MSL,为的是「最后一个 ACK 丢了还能重发」。
6.4.3 讲完了「丢包怎么补」,这一节讲「连接本身怎么开始和结束」。 6.2.4 提过 TIME_WAIT 会占端口,这一节讲它到底在等什么。
一、三次握手
三步,每一步都有明确目的:
| 步 | 方向 | 报文 | 状态 |
|---|---|---|---|
| 1 | 客户端到服务端 | SYN | SYN_SENT |
| 2 | 服务端到客户端 | SYN 加 ACK | SYN_RCVD |
| 3 | 客户端到服务端 | ACK | ESTABLISHED |
为什么三步而不是两步:两步的话,服务端无法确认「客户端收到了我的 SYN」。 如果那个 SYN 加 ACK 在半路丢了,服务端会以为连接已经建立, 于是开始等数据,而客户端根本不知道有这么个连接。 多一个 ACK,就把「双方都确认」这件事闭合了。 「三次」是最少的,不是「设计成三」:两次不够,四次多余。 所以「为什么不是四次」的答案是: 第三步和第四步可以合并成一个 ACK。
二、四次挥手
断开比建立多一步,因为 TCP 是「全双工」的: 两个方向各自独立关闭,所以要各发一个 FIN、各回一个 ACK。
| 步 | 方向 | 报文 | 状态 |
|---|---|---|---|
| 1 | 主动方到被动方 | FIN | FIN_WAIT_1 |
| 2 | 被动方到主动方 | ACK | FIN_WAIT_2 |
| 3 | 被动方到主动方 | FIN | LAST_ACK |
| 4 | 主动方到被动方 | ACK | TIME_WAIT |
第 2 步和第 3 步之间可以隔很久: 这段间隔就是「半关闭」状态——一方说「我没数据要发了」,但另一方可能还有一堆要发。 所以只能先回一个 ACK,等自己也发完了再发 FIN。 最典型的例子是 HTTP 的老式请求:客户端发完请求就半关闭,但服务端要传完整个响应才关闭。 所以「关闭」在应用看来是「我先说完,你继续」。
三、TIME_WAIT 在等什么
主动关闭的那一方,最后要等 2MSL 才能真的关掉(MSL 是报文最大生存时间,通常取 2 分钟或 30 秒, 各系统不同)。 等的原因有两个:一是「最后一个 ACK 可能丢」, 如果丢了,对方会重发 FIN,这时还能再回一个 ACK。 二是「让旧连接的残余包消失」,免得它们被「同一个四元组的新连接」误收。 代价是「端口被占着」:所以高频短连接的服务端容易把临时端口耗光, 这就是 5.6.4 讲的端口耗尽在 TCP 层的版本。 工程上的对策有三个:用长连接、让客户端主动关、或者调小 2MSL 的等待时间。
四、动手 Demo:三次握手,四次挥手
把两种流程的报文和状态列出来,再数一下本机有多少条 TIME_WAIT。 这个数直接反映「短连接有多频繁」。
import psutil
n = sum(1 for c in psutil.net_connections(kind='inet')
if c.status == 'TIME_WAIT')
print('TIME_WAIT', n)
实跑输出:
--------------------------------------------------------------------
三次握手,四次挥手
--------------------------------------------------------------------
**建连接要三个包,断连接要四个包**——**为什么不对称,是这一节最该记住的事。**
步 方向 报文 状态变化
------------------------------------------------------------------------
**1** **客户端到服务端** **SYN** **进入 SYN_SENT**
**2** **服务端到客户端** **SYN + ACK** **进入 SYN_RCVD**
**3** **客户端到服务端** **ACK** **双方 ESTABLISHED**
**第三行是「顺便带上」的**:**客户端本来就要回一个确认,干脆用它把连接确认掉**。
**所以三次是「最少」而不是「设计成三」**——
**两次不够,因为服务端无法确认「客户端收到了我的 SYN」。**
步 方向 报文 状态变化
------------------------------------------------------------
**1** **主动方到被动方** **FIN** **FIN_WAIT_1**
**2** **被动方到主动方** **ACK** **FIN_WAIT_2**
**3** **被动方到主动方** **FIN** **LAST_ACK**
**4** **主动方到被动方** **ACK** **TIME_WAIT**
**比握手多一步,是因为「半关闭」**:
**一方说「我没数据要发了」,不代表「我不想收」**——
**所以收到 FIN 只能先回 ACK,等自己也不发了再发 FIN。**
**这个中间状态就是「半关闭」,网页下载完先关发送方向,还能继续收。**
**本机 TIME_WAIT 有 34 条**:
**这是「主动关闭方」必须等的 2 倍报文寿命(2MSL)**,
**目的是「万一最后一个 ACK 丢了,还能重发」**。
**代价是「端口要占着一段时间」,所以高频短连接的服务端容易把临时端口耗光**——
**这就是 5.6.4 讲的端口耗尽在 TCP 层的版本。**
几十条 TIME_WAIT 是正常水平:每打开一个网页、每刷一次接口,都可能留下一条。 它们会自己消失,不需要处理。 什么时候才算异常:数量持续上涨、而且「新连接开始失败」,那才是端口池不够了。 只看数字大小会误判——几万条 TIME_WAIT 在繁忙服务器上是常态。 最后一个观察:「谁先关」决定了「谁背 TIME_WAIT」。 所以高并发服务端通常会让客户端先关, 或者用长连接把「关闭次数」降下来——这是「连接池」存在的一个很实际的理由。
30 秒实验
- 想一下:为什么建立要三次、断开设要四次?
- 想一下:TIME_WAIT 的两个作用分别是什么?
- 算一下:本机 TIME_WAIT 数量占全部连接的比例是多少?
常见误区
- 以为「握手四次更可靠」。 三次已经够,第四次可以和第三次合并。
- 以为「TIME_WAIT 是故障」。 是正常状态,只在一定条件下才是问题。
- 以为「关闭总是客户端发起」。 谁先关谁背 TIME_WAIT,服务端常常故意让客户端先关。
自测三问(点开对答案)
1. 三次握手的三个报文是什么? —— **SYN、SYN 加 ACK、ACK。** 2. 为什么断连接要四次? —— **全双工,两个方向各自关闭**,而且中间可能隔着很久的半关闭状态。 3. TIME_WAIT 为什么要等 2MSL? —— **让最后一个 ACK 丢了还能补发,同时让旧连接的残余包消散。**下一节讲「一个段到底发多大」。
- ☐ (本节完,下一节 6.4.5 TCP以段为单位发送数据)