跟着动画来学习 TCP 三次握手和四次挥手
Category(分类): NET Protocol Status: 未知
TCP 三次握手和四次挥手是面试中最常见的考点之一。很多读者都知道三次和四次,但是如果问得深入一点,往往无法作出准确回答。
本篇尝试使用动画和生活中的例子来讲解这个知识点,帮助读者更简单地理解 TCP 连接建立、数据传输和连接关闭的过程。
本文中的“招手”“说话”等内容是帮助理解的类比。实际 TCP 通信使用的是 TCP 报文段、序列号、确认号和状态机。
TCP 三次握手
TCP 三次握手就好比两个人在街上隔着 50 米看见了对方,但是因为雾霾等原因不能 100% 确认,所以要通过招手的方式相互确认对方是否认识自己。

张三首先向李四招手(SYN),表示自己想建立连接,并告诉李四自己准备使用的初始序列号。
李四看到张三向自己招手后,需要同时完成两件事:一是确认已经收到张三的招手,二是向张三发出自己的招手。因此,李四会在一个 TCP 报文段中同时设置 SYN 和 ACK 标志位,即发送 SYN+ACK,而不是先发送一个 ACK、再发送一个 SYN。
张三看到李四的 SYN+ACK 后,再发送一个 ACK,表示自己已经收到了李四的招手。至此,双方都确认了对方能够正常收发数据,连接建立完成,进入 ESTABLISHED 状态。

如果把李四的动作拆成两个逻辑动作,那么它相当于先确认张三的招手,再向张三招手;TCP 把这两个逻辑动作合并到了同一个 SYN+ACK 报文段中,所以实际只需要三个报文段:
客户端 服务端
SYN-SENT -------- SYN ------------------> LISTEN → SYN-RECEIVED
ESTABLISHED <------ SYN+ACK -------------- SYN-RECEIVED
ESTABLISHED -------- ACK ----------------> ESTABLISHED
三次握手的具体过程
设客户端的初始序列号为 x,服务端的初始序列号为 y:
| 步骤 | 发送方向 | TCP 报文段 | 主要含义 |
|---|---|---|---|
| 1 | 客户端 → 服务端 | SYN,序列号为 x | 客户端请求建立连接,并同步自己的初始序列号 |
| 2 | 服务端 → 客户端 | SYN+ACK,序列号为 y,确认号为 x+1 | 服务端确认收到了客户端的 SYN,并同步自己的初始序列号 |
| 3 | 客户端 → 服务端 | ACK,确认号为 y+1 | 客户端确认收到了服务端的 SYN+ACK |
SYN 和 FIN 都会占用一个序列号,因此服务端确认客户端的 SYN 时使用 x+1,客户端确认服务端的 SYN 时使用 y+1。
三次握手并不只是为了“打三次招呼”,它还用于:
- 同步双方的初始序列号;
- 让双方确认两个方向都具备收发能力;
- 降低历史重复 SYN 报文导致错误建立连接的可能性。
握手过程中的 TCP 状态
握手尚未完成时,常见的两个状态是 SYN-SENT 和 SYN-RECEIVED。更准确地说,它们是“握手未完成状态”,而不是简单地把两个状态都称为“半打开状态”。
SYN-SENT:主动打开方已经发送 SYN,正在等待对方的响应。SYN-RECEIVED:已经收到对方的 SYN,并发送了 SYN+ACK,正在等待对方最后的 ACK。
在通常的客户端—服务器模型中,客户端执行主动打开,服务器执行被动打开。但 TCP 也支持同时打开的情况,因此不能绝对地认为客户端一定是主动打开方、服务器一定是被动打开方。
另外,SYN 是 TCP 报文段中的一个控制标志位;日常也常说“SYN 报文”,更准确的说法是 “SYN 报文段”。
TCP 数据传输
TCP 数据传输就是两个人隔空对话,差了一点距离,所以需要对方对已经收到的内容进行确认。

张三喊了一句话(data),李四的 TCP 协议栈收到并校验数据后,会通过 ACK 告诉张三自己已经收到哪些字节。
这里的 ACK 只表示接收端的 TCP 协议栈已经接收并确认了相应序号的数据,并不表示对方的业务程序已经读取或处理了这些数据。数据是否真正被业务程序使用,还取决于接收端应用程序什么时候调用读取接口。
如果张三喊了一句话,过了一段时间还没有收到李四的确认,张三就会根据重传定时器判断报文可能丢失,并重新发送,这就是 TCP 重传。TCP 还可能根据重复 ACK 等情况进行快速重传。
也有可能李四已经收到了张三的话,但是李四发出的 ACK 在网络中丢失了,以至于张三没有收到确认。张三无法直接判断究竟是自己的数据丢失了,还是李四的 ACK 丢失了,因此可能会重新发送数据。
既然会重传,李四的 TCP 协议栈就可能收到同一段序列号对应的数据多次。TCP 会根据序列号识别重复数据,并在协议栈内部完成去重、排序和确认,用户层通常不会看到同一份网络重传数据两次。
不过,TCP 只负责网络传输层面的可靠性。如果应用程序自己重复发送了同一个业务请求,TCP 并不知道这两个请求在业务上是否相同,也不会替应用程序去重。应用层仍然需要设计消息边界、请求幂等性和业务重试策略。

张三可以向李四喊话,同样李四也可以向张三喊话,因为 TCP 连接是全双工的,双方都可以主动发起数据传输。双方发送的数据分别拥有自己的序列号和确认号。
无论是哪一方发送数据,接收方的 TCP 协议栈都会在适当的时候发送 ACK。ACK 可能单独发送,也可能和反向发送的数据一起携带;接收方还可以使用累计确认,一次 ACK 确认此前连续收到的多个字节。
延迟 ACK 和累计确认
张三可能是个高射炮,一连说了很多句话。这时候李四不必每听到一句话就立即回复,可以使用延迟 ACK 和累计确认来减少 ACK 报文数量。
但这并不是说李四一定要等到八句话全部说完才确认。通常情况下,接收端应至少每收到两个完整的 TCP 报文段确认一次,并且 ACK 不能被无限延迟;RFC 9293 要求 ACK 延迟小于 0.5 秒。收到乱序报文、填补缺口的报文或可能触发快速重传的报文时,通常还应尽快确认。
TCP 窗口
张三也不能一次性说太多话,李四的接收缓冲区短时间可能无法容纳太多数据。TCP 使用窗口机制控制发送方能够同时发送、但尚未收到确认的数据量。
这里需要区分两个概念:
- 接收窗口(
rwnd):接收方根据自己的接收缓冲区空间通告给发送方,用于流量控制,表示接收方还能接收多少数据。 - 拥塞窗口(
cwnd):发送方根据网络拥塞情况动态调整,用于拥塞控制。
实际能够在网络中同时传输、尚未确认的数据量通常受 min(rwnd, cwnd) 限制。因此,TCP 窗口不是简单协商出来的固定“发送和接收速率”。窗口扩大选项可以在握手时协商,但接收窗口和拥塞窗口在传输过程中都可能变化。
TCP 的有序字节流
网络环境的数据交互同人类之间的对话还要复杂一些,它可能存在数据包乱序的现象。同一个 TCP 连接中的不同 IP 数据报可能因为路由、负载均衡或网络排队等原因出现不同的到达顺序。
TCP 会根据序列号在协议栈内部缓存乱序数据,并在缺失的数据到达后重新组装,向用户层提供有序的字节流。如果前面的数据丢失,后面的数据即使已经到达,也可能暂时不会交付给应用程序。
需要注意,TCP 保证的是字节流的顺序,不保留应用层的消息边界。发送方调用多次 send,接收方不一定会按照相同的次数和边界调用 read 就能得到对应的数据,因此应用层通常需要自己设计消息分帧协议。
TCP 四次挥手
TCP 断开连接的过程和建立连接的过程比较类似。正常关闭时,连接的两个方向需要分别关闭,因此通常会出现四个报文段:张三挥手(FIN)——李四确认(ACK)——李四挥手(FIN)——张三确认(ACK)。在已经建立的连接中,这些报文段通常还会携带 ACK 标志位。

半关闭状态
之所以通常需要四个报文段,是因为 TCP 存在半关闭状态,也就是两个方向可以分别关闭。
张三已经挥了手,可是人还没有完全走,只是不再向李四发送数据,但是仍然可以继续接收李四发送的数据。李四收到张三的 FIN 后,先用 ACK 确认收到;如果李四还有数据要发送,就可以继续发送。等李四也不再发送数据时,再向张三发送 FIN。张三收到后回复 ACK,连接才完成正常关闭。
因此,服务端收到客户端 FIN 后进入 CLOSE-WAIT,并不代表连接立即彻底关闭;它还需要等待应用程序完成剩余工作并主动关闭自己的发送方向。如果应用程序迟迟不关闭,连接可能长时间停留在 CLOSE-WAIT。
正常四次挥手过程
| 步骤 | 发送方向 | 报文段 | 主动关闭方状态 | 被动关闭方状态 |
|---|---|---|---|---|
| 1 | 张三 → 李四 | FIN | FIN-WAIT-1 | 收到后进入 CLOSE-WAIT |
| 2 | 李四 → 张三 | ACK | 收到后进入 FIN-WAIT-2 | CLOSE-WAIT |
| 3 | 李四 → 张三 | FIN | 收到后进入 TIME-WAIT | 发送后进入 LAST-ACK |
| 4 | 张三 → 李四 | ACK | TIME-WAIT | 收到后进入 CLOSED |

TIME-WAIT 状态
上面有一个非常特殊的状态 TIME-WAIT。在通常的主动关闭场景中,主动关闭的一方收到对方的 FIN、回复最后一个 ACK 后进入这个状态;同时关闭等特殊场景下,双方都可能进入 TIME-WAIT。
TCP 规范要求 TIME-WAIT 持续 2 × MSL(Maximum Segment Lifetime,最长报文段寿命)。RFC 9293 在规范中将 MSL 取为 2 分钟,因此传统教材经常写成 4 分钟。但 MSL 是一种工程取值,具体系统实现可能采用不同的 TIME-WAIT 时长,不能把 4 分钟理解成所有操作系统都固定使用的时间。
TIME-WAIT 主要有两个作用:
- 确保最后的 ACK 有机会被重新发送:如果对方没有收到最后的 ACK,对方会重传 FIN。处于
TIME-WAIT的一方收到重传的 FIN 后,会重新发送 ACK,并重新启动2 × MSL计时器。 - 避免旧连接的延迟报文干扰新连接:在旧连接的四元组(源 IP、源端口、目的 IP、目的端口)被重新使用前,给网络中的旧报文留出足够的消退时间,避免它们被误认为是新连接的数据。
因此,不能简单理解为“TIME-WAIT 会把这段时间内所有残留报文立即丢弃”。TCP 仍然会按照序列号和状态机检查到达的报文,不符合当前连接要求的报文会被丢弃或按协议处理;如果收到对端重传的 FIN,则需要回复 ACK。
TIME-WAIT 保留的是内核中的 TCP 连接控制状态和连接四元组,并不意味着本地端口在任何情况下都绝对不能复用。是否能够复用,还与新的连接四元组、操作系统策略以及 SO_REUSEADDR 等套接字选项有关。应用层的 socket 文件描述符通常已经关闭,但内核仍可能暂时保留该连接的状态。
MSL 的含义是 Maximum Segment Lifetime,即“最长报文段寿命”。至于为什么使用 2 × MSL,可以简单理解为:既要给旧报文消退留时间,也要给对端发现最后 ACK 丢失并重传 FIN 留出时间。
三次挥手和其他关闭情况
四次挥手并不总是严格出现四个报文段。如果李四收到张三的 FIN 后已经没有数据要发送,并且应用程序马上关闭连接,那么李四可以把确认和自己的 FIN 合并到一个 FIN+ACK 报文段中,这时通常就是三个报文段:
张三 → 李四:FIN
李四 → 张三:FIN+ACK
张三 → 李四:ACK
在这种情况下,张三可能从 FIN-WAIT-1 直接进入 TIME-WAIT,跳过 FIN-WAIT-2。但这需要对方的 FIN 同时确认张三此前发送的 FIN;如果 FIN 到达时张三自己的 FIN 还没有被确认,状态转换可能不同,例如进入 CLOSING。
如果使用 RST 强制重置连接,则属于异常关闭,不是正常的四次挥手过程。
总结
- 三次握手的实际报文段是:
SYN → SYN+ACK → ACK。 - TCP 是全双工的,连接的两个方向可以独立传输和关闭。
- ACK 确认的是 TCP 协议栈收到的数据,不等于应用程序已经处理数据。
- TCP 提供有序字节流,不保留应用层消息边界。
- 四次挥手通常是:
FIN → ACK → FIN → ACK,但 ACK 和 FIN 在某些情况下可以合并。 TIME-WAIT的规范时长是2 × MSL,4 分钟只是采用 2 分钟 MSL 时的传统计算结果。