TCP 三次握手和四次挥手详解
大家好,我是小北。
我们都知道 TCP 是面向连接(connection.oriented)的一个传输层协议,在发送数据前,客户端和服务器端必须先通过“握手”建立一个连接。
1.1 为什么需要连接?
因为 TCP 提供的是可靠的数据传输服务,所以它需要做到:
可靠性保证
- 可以跟踪每个包的状态
- 知道哪些数据已送达
- 及时处理丢失的数据
状态管理
维护发送和接收的上下文
实现流量控制
处理拥塞控制
资源管理
- 合理分配系统资源
- 防止资源耗尽
- 保证服务质量
而这些机制的实现就构成了“连接”本身,也就是双方系统中记录的各种资源状态。
1.2 什么是 TCP 连接?
提到连接,可能很多同学下意识想到的是像在电路交换网络中的端到端**TDM(时分多路复用)或FDM(频分多路复用)**真实存在的电路,但,实际上该“连接”是一条逻辑连接:
我们在前面有提到过,TCP协议只在端系统中运行,而不在网络中转设备(路由器和链路层交换机)中运行,所以中间的网络设备不会维持TCP连接状态。
路由器看到的不是连接,仅仅是一个个的数据包。
真实的 TCP 连接下如图,是通信双方在两端维护的一些信息集合:
维护状态的这些信息包括:
- 源IP和端口
- 目标IP和端口
- 序列号等状态信息
- 各种资源(缓冲区、控制块等)
这几个要素共同唯一标识了一个TCP连接,可以把它类比为快递单号,通过这些信息就能跟踪和维护这个通信过程。
但是我们通常说唯一确认一个 TCP 连接是四元组:
- 源 IP
- 源端口
- 目标 IP
- 目标端口
其中有任何一个元素不同都是不同的 TCP 连接:
三、TCP 连接的建立:三次握手
TCP 是一个面向连接的、可靠的、基于字节流的传输层通信协议。
因此在TCP通信开始之前,必须先建立连接,这个建立连接的过程就是我们常说的**"三次握手"(Three-way Handshake)**。
用于建立连接的控制消息:SYN 和 ACK:
接下来我们会经常提到 SYN 包、ACK 包、FIN 包等,这些都是 TCP 用来管理连接的控制包,但是 TCP 本身并没有设计特殊的控制消息类型,所有 TCP 消息都使用相同的包格式:
TCP 标头包含多个控制标志位(Control Flags),用于标识数据包的类型和管理 TCP 连接状态。
在 TCP 的所有控制位中,以下三个标志位在三次握手和四次挥手中最重要:
SYN (Synchronize):
同步标志位,用于建立连接时同步双方的序列号
当 SYN=1 时,表示这是一个连接请求或连接接受报文
仅在连接建立阶段的握手报文中置位
ACK (Acknowledgment)
- 确认标志位,用于确认已收到的数据包
- 当 ACK=1 时,确认号字段有效
- 除了最初建立连接时的 SYN 报文外,所有报文的 ACK 都必须置 1
TIPS:
关于除了第一个 SYN 报文 ACK 没有置为 1,其它所有报文都需要置为 1 这个是在 rfc793 中有隐晦规定的,原文是在说明确认号的时候,有提到:
Acknowledgment Number: 32 bits If the ACK control bit is set this field contains the value of the next sequence number the sender of the segment is expecting to receive. Once a connection is established this is always sent.
如果设置了 ACK 控制位,则该字段包含该 segment 的发送方期望接收的下一个序列号的值。一旦建立连接,就会始终发送确认号。
这里说一单连接建立,每个包都会带有确认号,而确认号生效的前提就是 ACK 标志为 1
FIN (Finish)
- 终止标志位,用于释放一个连接
- 当 FIN=1 时,表示该端已经完成数据发送,要求释放连接
- 用于连接终止阶段的四次挥手过程
这些控制标志位是 TCP 实现可靠连接控制的基础,通过不同标志位的组合,可以准确地控制 TCP 连接的建立、维持和释放过程。
通常我们说 SYN/FIN 包就是指对应标志位置为 1 的数据包。
3.1 TCP 为什么需要三次握手?
在理解三次握手的具体过程之前,我们需要先明白为什么 TCP 需要三次握手:
- 确认双方通信能力:通过三次握手可以确认双方都具有收发数据的能力。
- 同步序列号:通过交换各自的序列号(Sequence Number),使双方能够正确地发送和接收数据。
- 防止历史连接:避免历史报文被重新接收,导致连接异常(在后面我们会详细分析这种 Case)。
- 协商参数:双方可以协商窗口大小、最大报文段长度(MSS)等参数。
另外关于为什么是”三次“,简单来说,因为 TCP 是全双工(Full-duplex)协议(全双工是指双方可以同时收发数据)。
所以需要在每个方向都有独立的序列号空间,要建立连接,双方都必须发送 SYN 并从对方接收 ACK 。
因此,从理论上讲,我们需要在客户端和服务端之间传递四个控制消息。
然而,第二次和第三次是可以合并的,通过设置两个相关位(有时称为 SYN + ACK 的消息),将 SYN 之一和 ACK 之一一起发送,所以也就成了三次握手(可以顺带思考下四次挥手为什么不默认合并为三次挥手呢~):
3.2 详细的 TCP 三次握手过程
TIPS:
首先在这里说明一下客户端和服务端这个概念,一般客户端是指主动发起连接的一方,服务端一般指一直监听被动等待连接的一方。
三次握手图解如下 :
PS: 下面三个数据包为了区分主要的字段差异,每一个包的核心字段配色都不同
第一次握手(SYN)
客户端发送 SYN 包:
当客户端需要与服务器建立TCP连接时,它会主动发起第一次握手,客户端会构造一个特殊的TCP报文段(SYN包),开始连接建立的过程。
这个SYN包中携带了以下重要信息:
首先,在TCP控制位中,SYN标志位被设置为1,表明这是一个连接请求报文。其他控制位(包括ACK、URG、PSH、RST和FIN)都设置为0,因为此时还不需要这些功能。
在序列号字段,客户端会随机选择一个初始序列号(ISN),我们通常用 x 表示。
这个随机选择的序列号有助于防止通信链路上旧的重复连接请求干扰新连接的建立(后面会单独文章解释这个问题)。
确认号(ACK)字段此时设置为0,因为这是第一次通信,还没有需要确认的数据。
客户端还会在 TCP 选项字段中携带一些重要的连接参数:
- MSS(Maximum Segment Size,最大报文段长度):告诉对方本机能接收的最大 TCP 报文段大小
- 窗口缩放因子:用于扩展 TCP 窗口大小的范围
- 时间戳:如果双方都支持,可用于计算往返时延和防止序列号回绕
在窗口大小字段,客户端会设置自己的接收窗口大小,告诉服务器自己的接收能力。
发送完 SYN 包后,客户端的状态从 CLOSED 变为 SYN_SENT,表示已发送连接请求,正在等待服务器的响应。
此时客户端会启动一个定时器,如果在指定时间内没有收到服务器的响应,将重传 SYN 包。
简要总结第一个包:
- 标志位:SYN=1, ACK=0
- 序列号:seq=x(客户端随机选择的初始序列号)
- 窗口大小:设置自己的接收窗口大小
- TCP选项:
- MSS(最大报文段长度)
- 窗口缩放因子
- 时间戳(如果使用)
客户端的状态变化: CLOSED → SYN_SENT
第二次握手(SYN + ACK)
服务端回应 SYN+ACK 包:
当服务器收到客户端的 SYN 包后,如果同意建立连接,会回送一个特殊的 TCP 报文段(SYN+ACK包)作为响应。
这个包中包含以下关键信息:
在 TCP 控制位中,同时设置 SYN 和 ACK 标志位为1。SYN=1表示这是一个连接请求响应,ACK=1 表示确认收到了客户端的请求。
服务器会生成自己的初始序列号 y,放在序列号字段中。
同时,确认号(ACK)字段会设置为 x+1(客户端的序列号加1),表示已经收到了客户端的 SYN 包,期望收到序号为 x+1 的数据段。
服务器也会在 TCP 选项字段中设置自己支持的参数:
- 响应客户端提出的 MSS
- 设置自己的窗口缩放因子
- 如果支持时间戳,则回送时间戳值
窗口大小字段设置为服务器的接收窗口大小,告诉客户端自己能处理多少数据。
发送完 SYN+ACK 包后,服务器的状态从 LISTEN 转变为 SYN_RCVD,表示已收到连接请求并发送了响应,正在等待客户端的确认。
这时服务器也会启动定时器,等待客户端的确认,如果服务器没有收到客户端的ACK确认,服务器会启动重传机制。
简要总结第二个包:
- 标志位:SYN=1, ACK=1
- 序列号:seq=y(服务端随机选择的初始序列号)
- 确认号:ack=x+1(客户端序列号+1)
- 窗口大小:设置自己的接收窗口大小
- TCP选项:
- 确认客户端的MSS
- 协商窗口缩放
- 回应时间戳
服务端状态变化: LISTEN → SYN_RCVD
第三次握手(ACK)
客户端发送 ACK 包(可携带数据的 ACK 包):
当客户端收到服务器的 SYN+ACK 包后,会发送一个普通的 ACK 包作为响应,完成连接的建立。
这个 ACK 包的特点是:
在控制位中,只有 ACK 标志位为1,SYN标志位变为0(因为不需要再次发起连接请求)。
序列号字段设置为 x+1,确认号字段设置为 y+1(服务器的序列号加1)。
此时 TCP 选项字段主要用于:
- 确认双方协商的连接参数
- 可能包含时间戳等信息
- 不再包含MSS等连接建立时的特殊选项
TIPS
注意,在第三次握手的 ACK 包中可以携带应用层数据,数据将被放在TCP报文的数据部分
它的优势是可以减少一个独立数据包的传输,节省一个往返时间(RTT)
应用场景:
- HTTP请求:可以在 ACK 包中直接发送 GET/POST 请求
- 小数据传输:适合发送短消息或命令
- 实时应用:可以更快开始数据交换
当客户端发送完这个 ACK 包后,它的状态变为 ESTABLISHED,表示连接已经建立,可以开始发送数据。服务器收到这个 ACK 包后,也将状态改为 ESTABLISHED,双方的连接就正式建立完成。
至此,三次握手完成,双方都验证了对方的收发能力,协商好了通信参数,并同步了序列号,为后续的可靠数据传输做好了准备。
这个过程看起来复杂,但保证了TCP连接建立的可靠性和安全性。
简要总结第三个包:
- 标志位:ACK=1, SYN=0
- 序列号:seq=x+1
- 确认号:ack=y+1
- 窗口大小:可能会根据第二次握手调整
- 数据传输:具备携带数据的能力
- TCP选项:确认协商的参数
状态变化:
- 客户端:SYN_SENT → ESTABLISHED
- 服务端:SYN_RCVD → ESTABLISHED
四、TCP 连接的终止:四次挥手
我们在前面说了 TCP 是全双工协议,所以连接的每一端都可以独立地关闭自己的发送通道。
这种单向关闭的能力,使得 TCP 连接可以在一个方向关闭而在另一个方向保持打开状态(称为半关闭)。
正是这个特性,决定了 TCP 必须使用四次挥手来完成连接的终止(同时关闭等特殊情况除外)。
4.1 TCP四次挥手详细过程
第一次挥手(FIN)
客户端发送 FIN 包:
当应用程序调用 close() 主动关闭连接时,TCP 协议会构造一个特殊的 FIN 包,开始连接终止过程。
这个 FIN 包中携带了以下重要信息: 在 TCP 控制位中,FIN 和 ACK 标志位都被设置为1(FIN=1, ACK=1)。
FIN=1 表示这是一个连接终止请求,ACK=1 是因为在正常通信阶段,所有包都需要对上一个包进行确认(之前提到除了第一个建立连接的 SYN 包不需要 ACK 标志,TCP 其它过程所有包都需要)。
序列号设为 m,这个 m 是已传送数据的最后一个字节的序号加1。确认号则是收到的对方最后一个包的序列号加1。
发送完 FIN 包后,发起方:
- 状态从 ESTABLISHED 变为 FIN_WAIT_1
- 停止发送数据,但仍然可以接收数据
- 启动 FIN_WAIT 定时器,防止对方不回应导致的无限等待
TIPS
注意,发送 FIN 包的一方虽然不再发送数据,但是接收通道仍然保持打开状态。这就是 TCP 的"半关闭"特性
应用场景:
- 文件传输:发送方传输完成后发送FIN,但仍可以接收对方的响应
- 消息队列:生产者完成生产后发送FIN,但仍需要接收消费确认
简要总结第一个包:
- 标志位:FIN=1, ACK=1
- 序列号:seq=m
- 确认号:ack=n(n是之前收到的最后一个包的序列号加1)
- TCP选项:时间戳等
发送方状态变化: ESTABLISHED → FIN_WAIT_1
第二次挥手(ACK)
服务端回应 ACK 包:
当接收方收到 FIN 包后,会回送一个 ACK 包确认收到了关闭请求。这个 ACK 包的构造相对简单:
TCP 控制位中只有 ACK 标志位为1(ACK=1),表示这是一个确认包。
确认号字段设置为收到的序列号加1(ack=m+1),表示已经收到了对方的 FIN 包。序列号保持为 n,即自己当前的发送序号。
这个 ACK 包通常不携带数据,发送完 ACK 包后,接收方:
- 状态从 ESTABLISHED 变为 CLOSE_WAIT
- 通知应用层对方想要关闭连接
- 此时仍然可以发送数据
- CLOSE_WAIT 状态会持续到应用层调用 close()
TIPS
CLOSE_WAIT 状态非常重要,它给了接收方足够的时间来处理剩余的工作:
- 完成剩余数据的发送
- 清理资源
- 做最后的处理
如果应用程序有 bug 没有正确调用 close(),连接会一直停留在 CLOSE_WAIT 状态,造成资源泄漏
简要总结第二个包:
- 标志位:ACK=1
- 序列号:seq=n
- 确认号:ack=m+1
- 通常无数据负载
- TCP选项:时间戳等
接收方状态变化: ESTABLISHED → CLOSE_WAIT
第三次挥手(FIN)
服务端发送 FIN 包:
当接收方的应用层调用 close() 后,会发送自己的 FIN 包,表示数据发送完成,准备关闭连接。
这个 FIN 包的结构: TCP 控制位中同样设置 FIN 和 ACK 都为1(FIN=1, ACK=1)。ACK=1 是因为这时所有包都需要确认。
序列号设为 p,这是服务器当前的发送序号。确认号保持为 m+1,表示仍然在确认之前收到的客户端的 FIN 包。
TCP 选项字段类似于第一个 FIN 包,发送完 FIN 包后:
- 状态从 CLOSE_WAIT 变为 LAST_ACK
- 停止发送数据
- 等待对方的最终确认
- 启动定时器,准备重传机制
TIPS
第三次挥手的 FIN 包表明此端已经完成了所有数据的发送,之后:
- 不能再发送任何数据
- 仍然可以接收对方的 ACK
- 如果超时未收到 ACK,会重传 FIN 包
简要总结第三个包:
- 标志位:FIN=1, ACK=1
- 序列号:seq=p
- 确认号:ack=m+1
- TCP选项:时间戳等
发送方状态变化: CLOSE_WAIT → LAST_ACK
第四次挥手(ACK)
客户端发送最后的 ACK 包:
收到对方的 FIN 包后,需要发送最后一个 ACK 包来确认连接的关闭。这个 ACK 包的特点:
TCP 控制位只有 ACK 为1,表示这是一个纯确认包。
序列号保持为 m+1,确认号设置为收到的序列号加1(ack=p+1)。
这个 ACK 包通常不包含数据,发送完最后的 ACK 后:
- 状态变为 TIME_WAIT
- 启动 2MSL 计时器
- 在 2MSL 时间后彻底关闭连接
2MSL(Maximum Segment Lifetime)是报文最大生存时间的两倍,这个时间是必要的:
- 确保双向数据都能在网络中消失
- 通常是 2-4 分钟
- 可以通过系统参数调整
TIME_WAIT 过多可能导致:
- 端口资源耗尽
- 系统负载增加
解决方案:
- 打开 tcp_tw_reuse(允许复用TIME_WAIT连接)
- 使用长连接(避免频繁建立/断开)
- 使用多个源端口(增加可用端口数)
后面我们会单独重点讨论为什么是 2MSL
简要总结第四个包:
- 标志位:ACK=1
- 序列号:seq=m+1
- 确认号:ack=p+1
- 无数据负载
- TCP选项:最后的时间戳等
状态变化:
- 发送方:FIN_WAIT_2 → TIME_WAIT → CLOSED
- 接收方:LAST_ACK → CLOSED
至此,一个完整的 TCP 连接终止过程就完成了,TCP 四次挥手是过程中有很多细节值得好好讨论,包括 2MSL、TIME_WAIT、CLOSE_WAIT 等都是面试常常会扩展问的,所以后面会单独写一篇文章来讨论~
4.2 为什么 TCP 需要四次挥手?
要理解为什么 TCP 必须使用四次挥手而不是二次或三次,我们需要从 TCP 连接的本质特性出发。
全双工通信的特性
TCP 连接是全双工的,这意味着:
- 连接的每一端都有自己的发送缓冲区和接收缓冲区
- 数据可以在两个方向上独立传输
- 每个方向都需要单独关闭
就像两条独立的单行道,每条都需要单独关闭。
接下来我们会分别分析一下两次挥手和三次挥手各自会有什么问题?
TCP 为什么不能两次挥手?
如果采用两次挥手,流程可能是:

这样做的问题在于:
- 缺乏对 FIN 的确认机制,无法确保 FIN 包成功到达
- 双方无法分别确认对方的数据是否已经发送完成
- 可能导致数据丢失
两次可以说是完全不能符合我们 TCP 可靠传输这个特性,都不需要太多讨论。
那,这句话的意思是 三次挥手还相对靠谱一点?
TCP 为什么不能三次挥手?
如果仿照三次握手的模式,可能的流程是:

这样做的问题在于:
- 服务器收到 FIN 后,可能还有数据未发送完成
- 强制服务器立即响应 FIN 会导致数据传输中断
- 无法支持半关闭状态
所以 TCP 不能三次握手的核心原因是在于说我们要将发送 FIN 的权利给到应用层自己决定,只有应用才知道它到底发送结束没有。
总结一下 TCP 为什么不能两次和三次挥手:
当然,这里我们是从理论层面上分析因为需要支持半打开连接,所以不能合并 第二个 ACK 和第三个 FIN,实际上 Linux 系统也可以通过某些设置,达到合并为三次的行为。
为什么 TCP 握手可以是三次而 TCP 挥手需要四次?
这是最关键的问题。三次握手和四次挥手的本质区别在于:
- 建立连接时
- 服务器收到 SYN 后,可以直接确定自己的初始序列号
- 可以把 ACK 和 SYN 合并在一个包中发送
- 此时双方都还没有数据需要发送
- 释放连接时
- 收到对方的 FIN 后,仅能说明对方没有数据要发了
- 自己这边可能还有数据需要继续发送
- 所以 ACK 和 FIN 包不能合并
- 必须等到所有数据发送完毕才能发 FIN
这就像是:
- 建立连接时,双方都是"准备好了就可以开始"
- 释放连接时,双方是"我这边完事了,但你那边不一定"
四次挥手的必要性
四次挥手的每一次都有其不可替代的作用:
第一次挥手(FIN)
- 发起方表示:"我的数据发送完了,不会再发送了"
- 此时接收方仍然可以发送数据
第二次挥手(ACK)
- 接收方说:"好的,我知道你不会再发数据了"
- 但接收方自己还有数据要发送
第三次挥手(FIN)
- 接收方说:"我的数据也发送完了"
- 表明这一方也准备关闭连接
第四次挥手(ACK)
- 发起方说:"我知道了,你也发送完了"
- 进入 TIME_WAIT,确保最后的 ACK 能到达