深入理解 TCP 协议(三):连接管理机制 —— 三次握手和四次挥手详解

作者:Mortalbreeze日期:2026/8/29

目录

前言

一、为什么 TCP 需要建立连接

1.1 TCP 是面向连接的协议

1.2 连接到底是什么

1.3 建立连接时必须要解决的问题

1.3.1 确认双方都具备通信条件

1.3.2 同步双方的初始序号

1.3.3 协商 TCP 通信所需要的参数

1.4 小结

二、TCP 三次握手

2.1 第一次握手

2.2 第二次握手

2.3 第三次握手

2.4 为什么需要三次握手

2.5 三次握手过程中的 TCP 状态变化

2.6 listen()、connect()、accept() 系统调用

三、TCP 四次挥手

3.1 为什么 TCP 需要四次挥手

3.2 第一次挥手

3.3 第二次挥手

3.4 第三次挥手

3.5 第四次挥手

3.6 为什么需要 TIME_WAIT

3.7 TIME_WAIT 的等待时间

思考:四次握手和三次挥手

四、TCP 的其他特性与应用

4.1 面向字节流

4.2 粘包问题

4.3 TCP 异常情况

4.4 基于 TCP 的应用层协议

4.5 用 UDP 实现可靠传输(经典面试题)


前言

在前两篇文章中,我们分别从 TCP 报文格式TCP 数据传输机制两个方面,对 TCP 协议进行了深入介绍。

在第一篇文章中,我们从 TCP 报头入手,介绍了 TCP 报文中的各个字段,以及序号、确认序号、窗口大小、标志位等重要字段的作用。

在第二篇文章中,我们进一步研究了 TCP 建立连接之后的数据传输过程,介绍了确认应答、超时重传、滑动窗口、流量控制、拥塞控制、延迟应答、捎带应答等机制,理解了 TCP 是如何在可靠性与传输效率之间进行平衡的。

但是,到目前为止,我们讨论的都是:

TCP 连接建立之后,数据是如何可靠、高效地传输的。

而在真正进行数据传输之前,还有一个问题需要解决:

TCP 连接究竟是如何建立起来的?

TCP 是一个面向连接的传输层协议。通信双方在正式传输数据之前,需要先建立连接;当数据传输完成后,也需要按照一定的流程关闭连接。

因此,本篇文章将作为 TCP 系列的最后一篇,重点介绍 TCP 的连接管理机制,深入理解:

  • TCP 为什么需要三次握手?
  • 三次握手具体做了什么?
  • TCP 连接建立过程中,双方的状态如何变化?
  • TCP 为什么需要四次挥手?
  • 为什么 TCP 关闭连接后还需要等待 TIME_WAIT
  • TCP 的各种连接状态分别代表什么?

通过本篇文章,我们将从连接建立、数据传输到连接关闭,完整串起 TCP 的整个生命周期。

一、为什么 TCP 需要建立连接

1.1 TCP 是面向连接的协议

TCP 是一个面向连接的、可靠的字节流传输协议

所谓面向连接,指的是 TCP 在正式传输数据之前,通信双方需要先建立连接。

例如客户端需要向服务器发送数据:

1客户端                              服务器
2
3        建立 TCP 连接
4        ────────────────→
5        ←────────────────
6        ────────────────→
7
8        TCP 连接建立完成
9
10        发送数据
11        ────────────────→
12        ←────────────────

只有当 TCP 连接建立完成之后,双方才会正式进行数据传输。

而当通信完成之后,双方也不能简单地认为“数据发完了,就结束了”,而是需要按照 TCP 规定的流程关闭连接,释放连接所占用的资源。

因此,TCP 的一次完整通信可以简单理解为:

建立连接 -> 数据传输 -> 关闭连接

这也是 TCP 与 UDP 一个非常重要的区别:UDP 是无连接的。

1.2 连接到底是什么

所谓“面向连接”并不是说 TCP 在通信双方之间建立了一条真实存在的物理线路。而网络中的数据依然是通过 IP 网络进行转发的,中间经过路由器、交换机等网络设备到达目标主机。

直接抛出结论:TCP 所建立的“连接”,本质上是一种由通信双方操作系统维护的、有状态的通信关系。

我们可以先从最简单的角度理解。

假设客户端:

1IP:192.168.1.10
2端口:50000

服务器:

1IP:192.168.1.20
2端口:8080

那么客户端与服务器之间的 TCP 通信可以通过下面四个信息确定:

1 IP       源端口       目的 IP       目的端口
2192.168.1.10  50000      192.168.1.20  8080

这四个信息被称为 TCP 连接的四元组

可以表示为:

1192.168.1.10:50000
2        
3192.168.1.20:8080

通过这个四元组,操作系统能够确定:

哪一台主机的哪个进程,正在与哪一台主机的哪个进程进行 TCP 通信。

但是要注意:

四元组只是标识一条 TCP 连接,并不等于 TCP 连接本身。

因为 TCP 是一个有状态的协议,连接建立之后,操作系统还需要维护大量与连接相关的信息。

例如我们前面已经学习过的:

1TCP 当前状态
2发送序号
3确认序号
4发送窗口
5接收窗口
6重传相关信息
7拥塞控制相关信息
8发送缓冲区
9接收缓冲区
10...

这些信息共同描述了:

当前这条 TCP 通信进行到了什么程度,以及接下来应该如何继续进行。

因此从操作系统的角度来看,一条 TCP 连接实际上对应着内核维护的一组连接状态和相关资源。

后面我们学习 TCP 状态转换时,会看到:

1LISTEN
2   
3SYN_SENT
4   
5ESTABLISHED
6   
7FIN_WAIT_1
8   
9TIME_WAIT
10   
11CLOSED

这些状态实际上就是 TCP 在整个连接生命周期中维护的不同状态。

所以,可以把 TCP 连接理解为:

通信双方围绕一次 TCP 通信建立起来的一套有状态的通信关系。

而连接管理,就是负责维护这套通信关系的生命周期:

1建立
2 
3维护
4 
5关闭

1.3 建立连接时必须要解决的问题

理解了“面向连接”和“TCP 连接”之后,我们再思考一个问题:

为什么 TCP 不能直接开始传输数据,而必须先建立连接?

因为 TCP 是一个可靠的、有状态的字节流协议

在真正发送应用层数据之前,通信双方需要先完成一些必要的信息同步。

1.3.1 确认双方都具备通信条件

首先,客户端需要告诉服务器:

“我想和你建立 TCP 通信。”

服务器收到之后,也需要明确告诉客户端:

“我知道你想和我建立连接,并且我也准备好了。”

客户端还需要进一步确认:

“我知道你已经准备好了。”

只有经过这样的交互,双方才能真正建立起一致的连接状态。

这实际上就是后面三次握手要解决的问题之一。

所以三次握手并不是简单的:“你好 → 你好 → 你好”

而是在通过三次报文交互,让双方逐渐建立起对这条 TCP 连接的共同认知。

1.3.2 同步双方的初始序号

这个问题与上一篇文章中的序号和确认序号直接相关。

TCP 并不是随便给数据编号,而是在建立连接时为这次 TCP 通信确定一个初始序列号(ISN)

例如:

1客户端初始序号:1000
2服务器初始序号:5000

那么客户端后续发送的数据就会从自己的序号开始:

11000
21001
31002
41003
5...

服务器发送的数据则从自己的序号开始:

15000
25001
35002
45003
5...

因此,双方在正式传输数据之前,需要让对方知道自己的初始序号。这个过程就是序列号同步。而这也是三次握手中非常重要的一项工作。

后面我们分析三次握手的时候,会看到:

1客户端  服务器:
2SYN + 序号
3
4服务器  客户端:
5SYN + ACK + 序号 + 确认序号
6
7客户端  服务器:
8ACK + 确认序号

1.3.3 协商 TCP 通信所需要的参数

建立 TCP 连接时,双方还可以通过 TCP 报头中的选项字段交换一些通信参数。

例如:

  • MSS(最大报文段长度)
  • 窗口扩大因子
  • 时间戳等

这些参数会影响后续 TCP 数据传输。

例如我们前面学习过:TCP 的窗口大小与流量控制有关。

如果接收方能够接收的数据比较少,那么它就需要通过窗口相关信息告诉发送方:“我现在只能接收这么多数据。”

因此,在连接建立阶段,双方不仅是在“确认对方存在”,还会为后续的数据传输建立必要的通信参数和初始状态

1.4 小结

所以,TCP 在正式传输数据之前进行连接管理,并不是多此一举。

它需要通过连接建立过程完成:

1              TCP连接建立
2                   
3       ┌───────────┼───────────┐
4                             
5  确认通信双方   同步初始序号   协商通信参数
6                             
7       └───────────┼───────────┘
8                   
9             建立连接状态
10                   
11               数据传输

因此,我们可以把 TCP 建立连接理解为:

通信双方在正式传输数据之前,通过一系列报文交互,确认彼此的通信状态,并同步后续数据传输所需要的信息,从而建立一套双方都认可 的 TCP 连接状态。

那么接下来最关键的问题就是:

TCP 到底是通过什么过程完成这些工作的?为什么偏偏是三次握手,而不是两次或者四次?

二、TCP 三次握手

在上一节中,我们已经知道,TCP 在正式传输数据之前,需要先建立一套双方都认可的连接状态。那么问题来了:TCP 是如何建立这套连接状态的?

答案就是:三次握手

三次握手的宏观理解就是 TCP 在建立连接时,通信双方之间进行的三次 TCP 报文交互。

整个过程可以详细表示为:

2.1 第一次握手

客户端首先向服务器发送一个 SYN 报文,表示:客户端希望与服务器建立 TCP 连接。

注:SYN 报文是指 SYN 标志位被置为 1 的 TCP 报文。

此时 TCP 报头中的:SYN = 1 Seq = x

注:x 是客户端随机选择的初始序列号。

服务器收到客户端发送的 SYN 报文之后,可以明确知道:

1. 有一个客户端希望与自己建立 TCP 连接

2. 客户端的初始序列号是 x

同时,客户端发送 SYN 后,会进入:SYN_SENT 状态。

即表示客户端已经向服务器发送连接请求,等待服务器的应答。

所以第一次握手之后:

客户端:CLOSED -> SYN_SENT 服务器:LISTEN

此时,TCP 连接还没有建立完成,因为服务器收到了客户端的请求,但客户端还不知道:

服务器是否收到了自己的请求,以及服务器是否同意建立连接。

所以还需要第二次握手。

2.2 第二次握手

服务器收到客户端发送的 SYN 后,如果同意建立连接,就会向客户端发送一个 SYN + ACK 报文。

这个报文同时完成两件事情:

1. 确认客户端的 SYN

服务器通过:ACK = x + 1 来告诉客户端:你的 SYN 我已经收到了

2. 告诉客户端服务器自己的初始序列号

服务器在第二次握手中,需要告诉客户端:服务器的初始序列号

假设服务器选择的初始序列号是 y:Seq = y

因此第二次握手的 TCP 报文时:

SYN = 1 ACK = 1 Seq = y + 1 Ack = x + 1

当服务器发送完 SYN + ACK 后,会进入:SYN_RECV 状态

即表示服务器已经收到客户端的连接请求,并且已经向客户端回复了连接请求。

而此时客户端收到第二次握手之后,可以知道:

服务器确实收到了我的 SYN,并且服务器也愿意建立 TCP 连接。

同时,客户端也获得了服务器的初始序列号 y。

但是此时还有一个问题:

服务器并不知道客户端有没有收到自己的 SYN + ACK。

所以还需要第三次握手。

2.3 第三次握手

客户端收到服务器发送的 SYN + ACK 后,需要向服务器发送一个 ACK 报文。

ACK = 1 Seq:x + 1 Ack = y + 1

其中:Ack = y + 1 表示客户端已经收到服务器序号为 y 的 SYN。

当服务器收到这个 ACK 后,服务器就可以确认:

客户端已经收到了我的 SYN + ACK。

此时双方已经完成了必要的信息同步,TCP 连接正式建立。

服务器和客户端均进入:ESTABLISHED 状态

此时 TCP 三次握手完成,就可以开始进行正式的数据传输。

2.4 为什么需要三次握手

对于 TCP 来讲,为什么恰好需要三次握手呢,为什么不是两次以及其他次数呢?

对于这个问题,让我们从双方需要确认的信息来看。

第一次握手:客户端告诉服务器

1客户端  服务器
2
3SYN
4Seq = x

客户端告诉服务器:“我想和你建立连接,我的初始序列号是 x。”

服务器收到后,可以确认:客户端具备发送能力

但是客户端此时并不知道服务器是否收到了自己的 SYN。

第二次握手:服务器告诉客户端

1服务器  客户端
2
3SYN + ACK
4Seq = y
5Ack = x + 1

服务器告诉客户端:“你的 SYN 我收到了,这是我的初始序列号 y。”

此时客户端可以确认:服务器具备接收能力和发送能力。

但是服务器还不知道客户端有没有收到自己的 SYN + ACK。

第三次握手:客户端再次确认

1客户端  服务器
2ACK
3Ack = y + 1

客户端告诉服务器:“你的 SYN + ACK 我收到了。”

此时服务器也可以确认:客户端具备接收能力。

于是双方就完成了确认:

1客户端知道:
2服务器能够接收我的数据
3服务器知道:
4客户端能够接收我的数据

所以三次握手实际上完成了一个非常重要的过程:

让客户端和服务器都确认对方具备正常通信的能力,并完成双方初始序列号的同步。

为什么不能是两次握手过程?

假设只有两次:

1客户端                         服务器
2
3SYN ─────────────────────────→
4   ←──────────────────── SYN + ACK

此时服务器认为:

“我已经把 SYN + ACK 发给客户端了。”

但是服务器无法确认客户端是否真正收到了这个 SYN + ACK

如果客户端因为网络问题根本没有收到第二次握手,那么:

1客户端:我没有建立成功
2服务器:我以为你建立成功了

双方对于连接状态就可能产生不一致。

因此,还需要第三次 ACK,让服务器确认:

客户端已经收到我的 SYN + ACK。

所以:两次握手无法让服务器确认客户端已经成功接收到自己的响应。

这也是三次握手相比两次握手的重要意义。

为什么不能是其他次握手呢?

对于 TCP 三次握手,双方都已经确认对方具备正常通信的能力,并已经完成双方序列号的同步,准备工作已经完成,所以不需要额外的次数来进行 TCP 连接的建立。

思考:对于 TCP 三次握手的过程,难道只建立序列号的共识吗?

对于 TCP 报头中的 16 位窗口字段?选项字段?

2.5 三次握手过程中的 TCP 状态变化

注:每个状态在内核中的实现本质就是一个宏。

结合上图,可以看到,客户端和服务器在三次握手过程中经历了不同的状态。

第一次握手:客户端进入 SYN_SENT

最开始,客户端处于 CLOSED 状态,服务器处于 LISTEN 状态。

当客户端希望与服务器建立连接时,客户端向服务器发送一个 SYN 报文。

发送 SYN 后,客户端不能认为连接已经建立,因为它还没有收到服务器的确认。

因此,客户端进入:SYN_SENT

SYN_SENT 可以理解为:已经发送 SYN,正在等待服务器确认。

此时服务器仍然处于 LISTEN 状态,等待客户端的连接请求。

第二次握手:服务器进入 SYN_RECV

服务器在 LISTEN 状态下收到客户端发送的 SYN 报文。

服务器确认客户端确实希望建立连接后,会向客户端发送 SYN + ACK 报文。

发送之后,服务器同样不能认为连接已经建立,因为它还需要确认:客户端是否收到了自己发送的 SYN + ACK?

所以服务器进入:SYN_RECV

SYN_RECV 可以理解为:已经收到客户端的 SYN,也已经回复 SYN + ACK,正在等待客户端最后的 ACK。

与此同时,客户端收到服务器发送的 SYN + ACK 后,就已经能够确认:服务器收到了我的 SYN,并且同意建立连接。

因此客户端可以进入:ESTABLISHED

也就是说,客户端在收到第二次握手后,就已经认为连接建立成功了。

第三次握手:服务器进入 ESTABLISHED

客户端进入 ESTABLISHED 后,会向服务器发送第三次握手的 ACK。

服务器处于 SYN_RECV 状态,收到这个 ACK 后,就可以确认:

客户端已经成功收到我的 SYN + ACK。

至此,双方关于连接建立所需要确认的信息已经完成。

此时服务器可以进入:ESTABLISHED

最终双方都进入:ESTABLISHED

ESTABLISHED 表示:TCP 连接正式建立,双方可以开始进行数据传输。

整个状态变化过程

因此,三次握手可以从状态变化的角度概括为:

1客户端:
2CLOSED
3    发送 SYN
4SYN_SENT
5    收到 SYN + ACK
6ESTABLISHED
1服务器:
2CLOSED
3    listen()
4LISTEN
5    收到 SYN,发送 SYN + ACK
6SYN_RECV
7    收到 ACK
8ESTABLISHED

最终:

1        三次握手完成
2             
3客户端 ESTABLISHED
4             
5服务器 ESTABLISHED
6             
7        正式传输数据

三次握手的本质不仅仅是三个 TCP 报文的交互,同时也是客户端和服务器 TCP 状态逐步同步的过程。

2.6 listen()、connect()、accept() 系统调用

前面我们从 TCP 协议角度分析了 TCP 三次握手的整个过程,但是在实际的 Linux 网络编程中,我们并不会手动编写代码去发送这三个报文,那么服务器与客户端是如何完成 TCP 三次握手的呢?

对于 TCP 服务器代码的编写,通常会执行:

1socket();
2bind();
3listen();
4accept();

对于 TCP 客户端代码的编写,通常会执行:

1socket();
2connect();

这些系统调用和 TCP 三次握手到底是什么关系呢?本小节将为你揭晓答案。

(1) listen() :让服务器进入监听状态

服务器创建 socket 并完成 bind() 后,还不能直接接收客户端的连接请求。

服务器需要调用:listen(sockfd, backlog);

告诉操作系统:这个 socket 用来监听客户端的 TCP 连接请求。调用 listen() 后,服务器对应的 TCP socket 就会进入监听状态,即 LISTEN。

注:listen() 本身并不是发送 TCP 报文,更不是执行三次握手,它只是让内核知道:这个 socket 是一个监听 socket,可以用来接收连接请求。

(2)connect() :客户端发起连接

当客户端调用:connect(sockfd, ...);

表示:客户端请求与服务器建立 TCP 连接。

**注:connect() 只是应用程序请求内核建立 TCP 连接的入口,而 TCP 的三次握手则由操作系统内核中的 TCP 协议栈自动完成。**这也就是为什么我们写 TCP 客户端代码时,不需要我们手动进行 TCP 三次握手。

(3) accpet() :获取已经建立的连接

当服务器调用:accept(listenfd, ...);

表示:应用层告诉操作系统,把 listenfd 已经建立好的连接交给我。

对于 accept(),它会返回一个新的 socket,这个 socket 负责和某一个已经建立连接的客户端进行数据通信,而 listenfd 继续负责监听新的连接。

注:accpet() 只是应用层用来获取 TCP 连接,它并不参与 TCP 三次握手的过程,即使服务器不进行 accept(),服务器和客户端的连接依旧正常建立。

总结一句话:

listen() 负责让服务器进入监听状态, connect() 负责让客户端请求建立 TCP 连接,而 accept() 负责让服务器应用程序获取已经建立好的 TCP 连接。三次握手由操作系统内核中的 TCP 协议栈自动完成,应用程序并不需要手动参与三个 TCP 报文的发送。

三、TCP 四次挥手

3.1 为什么 TCP 需要四次挥手

先回答一个核心问题:

TCP 为什么不能像 UDP 一样,通信结束后直接关闭,而是需要进行挥手?

因为 TCP 是面向连接的协议,通信双方的操作系统会为 TCP 连接创建相应的数据结构,对 TCP 连接进行管理。而 UDP 是无连接的,操作系统只需要将数据报交给网络协议栈进行发送即可。

因此,当 TCP 通信结束时,通信双方需要通过一定的机制通知对方关闭连接,并释放操作系统中维护的相关资源,这个过程就是TCP 的连接关闭过程,也就是四次挥手

但是,仅仅因为 TCP 是面向连接的,还不能解释为什么需要四次挥手。

这是因为 TCP 是一个全双工通信协议。一条 TCP 连接建立后,客户端和服务器之间实际上存在两个独立的数据传输方向:

1客户端 ─────────────→ 服务器
2       数据传输方向 1
3
4客户端 ←───────────── 服务器
5       数据传输方向 2

因此,TCP 连接的关闭并不是简单地 “一方关闭,整个连接立即关闭”,而是需要分别关闭两个方向的数据传输。

例如,客户端已经没有数据需要发送了,可以先关闭:

客户端 → 服务器

但此时服务器可能还有数据需要发送给客户端:

服务器 → 客户端

所以,服务器不能因为客户端关闭了发送方向,就立即关闭整个 TCP 连接。

这就是 TCP 通常需要通过多次报文交互完成连接关闭的根本原因。

TCP 是全双工的,连接的两个通信方向需要分别关闭,因此 TCP 的连接关闭过程通常需要四次挥手。

整个过程可以详细表示为:

3.2 第一次挥手

客户端和服务器在 TCP 协议中,地位是相同的。本节我们假设客户端主动关闭连接。

当客户端已经没有数据需要发送给服务器时,客户端会向服务器发送一个 FIN 报文。

FIN 报文:FIN 标志位被置为 1 的 TCP 报文。FIN 标志位表示:客户端已经没有数据需要发送,请求关闭 客户端 -> 服务器 这一方向的数据传输。

客户端发送 FIN 报文后,进入:FIN_WAIT_1 状态

FIN_WAIT_1表示:客户端已经发送 FIN 报文,正在等待服务器确认。

注:对于第一次挥手,TCP 连接并没有立即关闭,因为 TCP 是全双工的,此时只是表示:客户端 -> 服务器 这个方向已经关闭,但是 服务器 -> 客户端 这个方向依旧可以传输数据。所以客户端此时仍然需要保持连接。

3.3 第二次挥手

服务器收到客户端发送的 FIN 报文后,首先需要告诉客户端:你发送的 FIN 报文我已经收到。

因此服务器向客户端发送一个 ACK 报文:

1客户端                         服务器
2
3FIN  ───────────────────────→
4
5     ←────────────────────── ACK

服务器收到 FIN 报文后,进入:CLOSE_WAIT 状态

而客户端收到服务器返回的 ACK 后,进入:FIN_WAIT_2 状态

CLOSE_WAIT 表示:客户端 -> 服务器 这个方向已经关闭,即客户端已经没有数据向服务器发送,但 服务器 -> 客户端 这个方向没有关闭,即服务器需要处理完客户端之前发送的数据。

FIN_WAIT_2 表示:正在等待服务器发送 FIN 报文。

对于这种连接状态,被称为半关闭状态。

注:对于第二次挥手,TCP 连接并没有完全关闭,而是处于半关闭状态。客户端发送 FIN 只代表客户端不再向服务器发送数据,但服务器仍可能需要处理已经接收到的数据,或者继续向客户端发送剩余数据,因此服务器不会立即关闭 TCP 连接。

3.4 第三次挥手

当服务器的数据也全部处理和发送完毕,并且服务器操作系统发现:服务器处于 CLOSE_WAIT 状态。此时服务器向客户端发送 FIN 报文:

1客户端                         服务器
2
3FIN  ───────────────────────→
4
5     ←────────────────────── ACK
6
7     ←────────────────────── FIN

服务器发送 FIN 报文后,进入:LAST_ACK 状态

LAST _ACK 表示:服务器已经发送 FIN 报文,等待客户端对这个 FIN 报文进行最后确认。

客户端收到服务器的 FIN 后,说明:服务器 -> 客户端这个方向的数据传输也结束了。

注:对于第三次挥手,TCP 连接依旧没有完全关闭。服务器发送 FIN 报文,只代表服务器已经没有数据需要发送,此时还需要等待客户端发送最后一个 ACK 报文,确认客户端已经收到服务器发送的 FIN 报文。

3.5 第四次挥手

客户端收到服务器的 FIN 后,向服务器发送 ACK:

1客户端                         服务器
2
3FIN  ───────────────────────→
4
5     ←────────────────────── ACK
6
7     ←────────────────────── FIN
8
9ACK  ───────────────────────→

服务器收到这个 ACK 后,进入:CLOSED 状态

注:对于第四次挥手,服务器收到客户端发送的 ACK 后,进入 CLOSED 状态,此时服务器的 TCP 连接正式关闭,操作系统可以释放为该 TCP 连接维护的相关内核数据结构。

而客户端发送最后一个 ACK 报文后,并不会立即进入 CLOSED 状态,而是进入 TIME_WAIT 状态,需要等待一段时间后才会进入 CLOSED 状态。此时,客户端的 TCP 连接才正式关闭,操作系统可以释放为该 TCP 连接维护的相关内核数据结构。

3.6 为什么需要 TIME_WAIT

为什么服务器可以直接关闭,而客户端却需要等待?

这是因为客户端发送的最后一个 ACK 报文,虽然已经发出,但客户端无法确定这个 ACK 报文是否成功到达服务器。

假设最后一个 ACK 报文在网络传输过程中丢失,对于服务器来说,由于在一定时间内没有收到客户端的 ACK,就会重新发送 FIN 报文。

如果客户端发送 ACK 后立即进入 CLOSED 状态,那么当服务器重新发送 FIN 时,客户端已经没有对应的 TCP 连接来处理这个 FIN,也就无法再次向服务器发送 ACK。

而客户端进入 TIME_WAIT 状态后,会在一段时间内继续维护这条 TCP 连接。如果服务器重新发送 FIN,客户端仍然能够接收到该 FIN,并重新发送 ACK,从而保证服务器最终能够正常关闭连接。

如果客户端处于 TIME_WAIT 期间,只要收到服务器重传的 FIN,就会再次 ACK,但是如果网络本身持续异常,导致客户端发送的 ACK 始终无法到达服务器,那么对于客户端来讲,TIME_WAIT时间过后,会直接关闭 TCP 连接,而对于服务器来讲,服务器不可能无限等待,而是重传上限后,TCP 认为当前连接已经无法关闭,此时服务器会放弃重传,并关闭连接。

因此,客户端不能在发送最后一个 ACK 后立即进入 CLOSED,而需要通过 TIME_WAIT 保留一段时间的连接状态,以提高最后一个 ACK 成功到达服务器的可靠性。

除了保证最后一个 ACK 能够被服务器收到之外,TIME_WAIT 还可以避免旧 TCP 连接中的报文影响后续的新连接

TCP 报文在网络中传输时,并不一定能够在连接关闭后立即消失。

例如:

1旧连接
2客户端 ─────────────→ 服务器
3          某个报文
4             
5          网络延迟

如果 TCP 连接刚刚关闭,客户端又立即使用完全相同的四元组建立一条新的 TCP 连接,那么网络中残留的旧报文就有可能被新连接误认为是当前连接的数据。

因此,客户端进入 TIME_WAIT 后等待一段时间,可以让旧连接中残留的报文在网络中自然消失,从而降低对后续连接产生影响的可能性。

所以, TIME_WAIT 主要解决两个问题:

  1. 保证最后一个 ACK 丢失后,客户端仍然能够重新响应服务器的 FIN。
  2. 让旧连接中残留的报文在网络中消失,避免影响后续建立的相同连接。

因此:

TIME_WAIT 并不是 TCP 多余的等待,而是 TCP 为了保证连接能够可靠关闭,并避免旧连接报文影响新连接而设计的状态。

思考:服务器关闭后,为什么不能立即 bind 原来的端口号?

3.7 TIME_WAIT 的等待时间

前面我们已经知道,客户端发送最后一个 ACK 后,并不会立即进入 CLOSED,而是进入 TIME_WAIT 状态。

那么问题来了:TIME_WAIT 到底需要等待多长时间?

TCP 中通常使用 2MSL(Maximum Segment Lifetime,最长报文段寿命) 作为 TIME_WAIT 的等待时间。

这里的 MSL可以简单理解为:一个 TCP 报文在网络中允许存在的最长时间。

为什么是 2MSL,而不是 1MSL?

这是因为客户端发送最后一个 ACK 后,需要考虑一种情况:

1客户端                         服务器
2
3       ←──────────── FIN
4
5ACK ────────────────→
6         
7       网络延迟
8         
9       服务器收到

如果 ACK 正常到达服务器,那么服务器就可以关闭连接。

但是客户端无法直接知道:服务器到底有没有收到这个 ACK。

因此客户端需要等待足够长的时间,使得:

  1. 客户端发送的最后一个 ACK 能够到达服务器;
  2. 如果 ACK 丢失,服务器重新发送的 FIN 也能够到达客户端;
  3. 客户端能够再次发送 ACK。

最极端情况下,可以理解为:

1客户端 ───── ACK ─────→ 服务器
2         最长 MSL
3
4客户端 ←──── FIN ────── 服务器
5         最长 MSL

两段时间加起来就是:

MSL + MSL = 2MSL

思考:四次握手和三次挥手

在理解 TCP 四次挥手之后,可能会产生一个有意思的问题:

既然 TCP 关闭连接需要四次挥手,那么为什么建立连接只需要三次握手,而不是四次握手?

其实,从信息交互的角度来看,TCP 三次握手也可以理解成需要完成四个动作:

1客户端:我想建立连接
2服务器:我收到了你的请求
3服务器:我也想和你建立连接
4客户端:我收到了你的请求

但是,服务器在回复客户端的连接请求时,可以将确认客户端 SYN 的 ACK服务器自己的 SYN放在同一个 TCP 报文中:

1       SYN
2        
3      SYN + ACK
4        
5       ACK

因此,原本需要完成的四个动作,被压缩成了三个 TCP 报文。

所以,三次握手并不是少完成了一次确认,而是第二次握手通过 SYN + ACK 同时完成了两个动作。

那么问题来了:TCP 四次挥手是不是也可以通过这种方式减少一次,变成三次挥手?

答案是:可以

在正常的四次挥手中:

1客户端  服务器:FIN
2客户端  服务器:ACK
3客户端  服务器:FIN
4客户端  服务器:ACK

第二次挥手的 ACK 和第三次挥手的 FIN,在时间上并不一定能够同时发送。

因为服务器收到客户端的 FIN 后,服务器可能还有数据没有发送完成

例如:

1客户端:我不发数据了
2服务器:收到,但是我还有数据要发

所以服务器需要先发送 ACK,继续处理和发送自己的数据,等数据发送完成后,再发送 FIN。

但是,如果服务器收到客户端的 FIN 时:服务器也已经没有数据需要发送。

那么服务器就可以将:

1确认客户端 FIN  ACK
2+
3服务器自己的 FIN

合并到同一个 TCP 报文中:

1客户端                         服务器
2
3FIN ───────────────────────→
4
5     ←────────────────────── FIN + ACK
6
7ACK ───────────────────────→

这样,原本的四次挥手就变成了三次报文交互。

所以可以总结为:

TCP 四次挥手是通常情况下的连接关闭过程,但并不意味着网络中一定会出现四个独立的 TCP 报文。如果服务器收到 FIN 后恰好没有数据需要发送,那么 ACK 和 FIN 可以合并,从而形成“三次挥手”。

四、TCP 的其他特性与应用

4.1 面向字节流

深入理解TCP协议(一):TCP报文格式详解 文章中,我们知道 TCP 报头字段中没有直接描述 TCP 数据长度的字段。

为什么 TCP 报头中不直接增加一个数据长度字段呢?

实际上,TCP 并不是无法知道一个 TCP 报文中携带了多少数据。

TCP 报文封装在 IP 数据报中,IP 层能够提供整个 IP 数据报的长度,而 TCP 报头中的 4 位首部长度字段可以确定 TCP 报头的长度。因此,TCP 可以计算出当前 TCP 报文携带的数据长度。

既然 TCP 能知道 TCP 报文携带的数据长度,那为什么不能像 UDP 那样以数据报的形式交付给应用层,而是以字节流的形式交付给应用层?

深入理解 TCP 协议(二):TCP 可靠传输与高效通信机制 滑动窗口机制中,我们知道 TCP 并不是将应用层一次交付的完整数据直接封装成一个 TCP 报文,而是将发送缓冲区中的连续字节划分成多个 TCP 报文进行传输。

因此,即使 TCP 能够明确知道每一个 TCP 报文携带了多少数据,也无法通过 TCP 报文的边界来确定应用层数据的边界

例如,应用层向 TCP 交付一段完整数据:

HelloWorld

TCP 可能根据当前网络状况将其划分为:

1TCP 报文 1:Hello
2TCP 报文 2:World

但对于接收方应用层而言,它并不会知道 Hello 和 World 分别来自两个 TCP 报文,也不会认为它们是两个独立的数据。

TCP 会将这些数据重新组织成连续的字节流交付给应用层:

HelloWorld

因此,TCP 报文 的边界并不是应用层数据的边界。TCP 只保证字节流能够可靠、有序地传输,而不会保留应用层数据之间的边界,这也是 TCP 面向字节流的本质。

4.2 粘包问题

既然 TCP 不负责维护应用层数据的边界,那么应用层连续发送多个数据,接收方又应该如何区分这些数据呢?

这就是 TCP 粘包问题

注:TCP 粘包问题中的包是指应用层的数据包。

避免粘包问题的核心思想:明确两个数据包的边界。

避免粘包问题的措施:

(1)制定定长的数据包,如果报文数据不足,以特殊字符填充

(2)对于变长的数据包,可以在数据包的起始位置,约定一个数据包总长度的字段

(3)对于变长的数据包,可以在数据包之间添加特殊分隔符

4.3 TCP 异常情况

进程终止:进程终止会自动关闭文件描述符,操作系统对其套接字自动进行四次挥手。

机器重启:和进程终止情况一样。

机器断电或者网线断开:接收端认为连接存在,一旦接收端有写入操作,接收端发现连接对端无响应,就会自动释放该 TCP 连接,即使没有写入操作,TCP 协议也内置了一个保活定时器,会定期询问对方是否存在。

4.4 基于 TCP 的应用层协议

HTTP

HTTPS

SSH

FTP

SMTP

Telnet

当然,还有你自己写的基于 TCP 的应用层协议

对比维度TCPUDP
连接方式面向连接无连接
数据交付方式面向字节流面向数据报
可靠性可靠传输尽最大努力交付,不保证可靠
数据边界不保留应用层消息边界保留数据报边界
是否存在粘包/拆包存在,需要应用层解决消息边界不存在 TCP 意义上的粘包/拆包
连接管理三次握手、四次挥手
传输效率机制较多,开销相对较大首部简单,开销较小
传输速度通常更适合稳定可靠的数据传输通常更适合低延迟、实时性要求高的场景
适用场景HTTP/HTTPS、文件传输、数据库通信、SSH 等DNS、DHCP、实时音视频、在线游戏、直播等

归根结底,TCP 和 UDP 之间没有优缺点,只是 TCP 和 UDP 的特点不同,所以采用什么传输层协议,是需要根据场景而定。

4.5 用 UDP 实现可靠传输(经典面试题)

参考 TCP 的可靠性机制回答:
例如:

引入序列号,保证数据顺序

引入确认应答机制,确认数据是否到达

引入超时重传机制

......

所以面试的时候,可以总结成一句话:

UDP 本身是不可靠的,如果希望基于 UDP 实现可靠传输,可以在应用层引入序列号、确认应答、超时重传、去重、滑动窗口等机制,从而解决 UDP 的丢包、乱序、重复以及传输效率等问题,本质上就是在 UDP 之上实现一套类似 TCP 的可靠传输机制。

不过这里有一个很重要的面试加分点

既然 UDP 上面最终又实现了这么多 TCP 的机制,那为什么不直接使用 TCP?

答案是:UDP + 自定义可靠传输并不一定是为了“替代 TCP”,而是为了在需要可靠性的同时,保留 UDP 的一些特性,例如可以由应用层自己控制重传策略、报文格式以及部分传输行为。


深入理解 TCP 协议(三):连接管理机制 —— 三次握手和四次挥手详解》 是转载文章,点击查看原文


相关推荐


麒麟v10-Orchestrator高可用组件完整部署与使用(从入门到精通)
西部鳞斑响尾猫2026/8/21

环境:MySQL 8.0.35 GTID 一主两从(141 主 + 142/143 从)+ Orchestrator 3.2.6 raft 三节点(141/142/143)+ 元数据库(143:3307 独立实例)+ VIP(192.168.195.200)+ 最小化 SMTP 邮件服务(143:25) 本文覆盖 Orchestrator.pdf 全部内容:架构原理、安装、配置文件逐项讲解、运行、监控、企业级场景模拟、常见故障模拟与解决、邮件与 VIP、知识点补充。 所有命令均注明执行节点,可直


K8s 数据库 Secret 加密实战|密码明文漏洞彻底修复,等保密评双合规(金仓 / 达梦双库适配)
雨辰AI2026/8/8

摘要 90% 的团队第一次上 K8s 都会踩这个致命合规雷:以为 Secret 是加密存储,实则只是 Base64 编码,等于把数据库管理员密码、国密加密密钥明文存在 etcd 里,运维全员可见、配置提交 Git 直接泄露。等保、密评测评时一查一个准,整改一次就要推翻重配。 本文基于政务、金融信创项目合规落地经验,彻底拆解 K8s 数据库密钥的合规风险,输出从轻量到企业级的三套落地方案:SealedSecret 静态加密、国密 KMS 对接、Sidecar 动态零落地注入,覆盖人大金仓 V9


力扣hot100-240.搜索二维矩阵2-单调性剪枝详解
闪电悠米2026/7/30

LeetCode 240. 搜索二维矩阵 II:单调性剪枝详解 1. 算法思想 这题属于: 矩阵搜索 / 单调性剪枝 也常被称为 Z 字形搜索。它不是普通二分查找:每一行、每一列分别有序,但整个矩阵按行展开后并不整体有序。 例如: [ [1, 4, 7], [2, 5, 8], [3, 6, 9] ] 按行展开是 1, 4, 7, 2, 5, 8, 3, 6, 9,其中 7 后面是 2,因此不能把它当一维数组二分。 本题的关键是:从右上角开始,每次比较都能确定排除一整行或一整列。


「寒草呈献」工作六年,是否仍有创造未来的勇气 ✨
寒草2026/7/22

大家好,我是寒草 🌿 封笔多年,这一篇文章,献给自己~ 六年似弹指一瞬 2020 年盛夏至今,我已工作满整整六年,此间经历颇多。 『踌躇』 2020 年下半年,虽步履蹒跚,不知前路何方,仍一边裹着焦虑一边四处探寻。ps:还曾记得我当时为何来到我现在所在的公司,仅是因为董事长所谓『梦想』的感召。 「肆意」 2021 年开始在掘金创作,我自视与众不同,不喜技术输出(认为那是翻来覆去的陈词滥调),更偏爱人文关怀和新奇创意,那年与数不清的业界好友畅谈,好似那一整年的春夏秋冬都是热烈的盛夏。 「探寻


Rust 函数与返回值详解:参数、表达式与返回类型
程序员爱钓鱼2026/7/14

《Rust 编程实战》系列第 9 篇 在前面的文章中,我们已经学习了变量、数据类型、常量和静态变量。 接下来,我们需要解决一个非常重要的问题: 如何把一段功能独立出来,并在程序中的多个地方重复使用? 答案就是:函数(Function)。 函数是组织 Rust 程序最基本的方式之一。无论是命令行工具、Web 服务、桌面软件还是企业级项目,最终都会由大量函数共同组成。 本文将详细介绍: 如何定义函数 如何传递参数 如何声明参数类型 如何返回数据 Rust 中语句和表达式的区


认识 Horizon UI · 15/17:用模板定制控制台
SkyWalking中文站2026/7/6

Horizon UI 系列第十五篇:整个控制台都由可编辑模板驱动。你可以把任意 layer 或 overview 打开成模板,在本地草稿里调整组件、widget 和文案,预览后发布到 OAP 给整个组织使用,并在发布前查看差异,也可以导出和导入。 译自英文原文:Meet Horizon UI · 15/17: Customization — Config-Driven Layer Templates。 这是 Meet Horizon UI 系列的第十五篇,也开启第五幕 make it yours


用视频数据采集 API 构建个人视频搜索引擎:从 C 罗频道到 Elasticsearch 全文检索
硬核科技工作室2026/6/28

一、视频元数据好看,但不好稳定拿 做视频搜索、内容监测或者训练数据准备时,第一步通常不是模型,也不是搜索算法,而是先拿到一批质量稳定的视频元数据。 比如我们想做一个个人视频搜索引擎,输入关键词 Cristiano,系统可以返回相关视频的标题、描述、播放量、时长、上传者和视频链接。听起来很简单,但真正做起来会发现,视频平台页面结构经常变化,不同入口返回的信息也不一样:频道页、搜索页、标签页、播放页,每个页面的数据组织方式都不同。 如果自己做这件事,通常会有几种方案。 第一种是自己写数据采集


AI 能写代码了,为什么我反而开始要求它先写文档?
Avan菜菜2026/6/19

最近在尝试用 AI 参与项目开发。 刚开始我的方式很简单: 提需求 ↓ 让 AI 直接实现 ↓ 不断返工 ↓ 继续补需求 结果非常熟悉: 功能能跑 代码越来越多 需求越来越乱 AI 上下文越来越长 后面谁都不敢接手 尤其是涉及: 前后端联动 权限体系 数据结构变更 API 契约 多阶段迭代 时,问题会迅速放大。 后来我接触到了 GitHub 开源的 Spec Kit。 它让我第一次把 AI 开发从: 直接写代码 变成: 先规格 ↓ 再设计 ↓ 再拆任务 ↓ 最后实现 整个过程开始变


企业智能助手的实践分享(LLM/RAG)
uzong2026/6/11

本文聚焦 AI 技术在企业级智能的实践,剖析项目实施过程中的关键挑战与避坑指南。 1. LLM 智能运维助手 1.1. 助手背景 在企业基础设施建设中,开放平台与基础服务承载着海量业务。随着系统复杂度的增加,日常运行中产生了庞大的日志告警数据。面对这些海量且繁杂的告警信息,传统的人工排查模式不仅耗时费力,且难以在“告警风暴”中迅速抽丝剥茧,成为制约研发效率的瓶颈。 希望助手能力致力于解决两大核心难题:一是应对海量日志告警的干扰,二是大幅缩短告警排查的平均耗时。 1.2. 案例效果 下面是一个案例


Linux shell脚本教程
诸神缄默不语2026/6/4

诸神缄默不语-个人技术博文与视频目录 Linux系统的命令行终端界面就是一个小黑窗,在里面敲命令执行任务。当你想执行一系列复杂的任务(比如连续执行多个命令、有逻辑判断规则等)时,光靠直接敲命令+回车就不够了,这时你就会将一系列任务的执行代码写到一个文本文件中,然后让Linux终端依次执行。这个文本文件就是shell脚本。 本文对Linux系统中的shell脚本进行简单介绍,包括其作用和基本写法。更高级的用法将在以后的教程中介绍。 对Linux系统的整体命令行操作教程,请参考我撰写的另一篇博文:

首页编辑器站点地图

本站内容在 CC BY-SA 4.0 协议下发布

Copyright © 2026 聚合阅读