“三次握手,四次挥手”你真的懂吗?
Category(分类): NET Protocol
Status: 未知
本文主要介绍 TCP 连接建立、数据传输和连接关闭的基本过程,并结合 Linux 连接队列和 Redis 抓包示例进行说明。
文中使用“三次握手”和“四次挥手”描述最常见的过程。实际 TCP 实现还可能出现 SYN 携带数据、ACK 与 FIN 合并、同时关闭以及 SYN Cookie 等情况,因此报文数量并不绝对固定。
一、什么是“三次握手,四次挥手”
TCP 是一种面向连接、可靠的字节流协议,通常用于单播通信。在发送数据前,通信双方必须先建立连接。这里的“连接”并不是物理线路,而是通信双方在内存中维护的一组状态信息,例如本地和远端的 IP 地址、端口号、序列号、确认号、窗口等。这些信息通常保存在 TCP 控制块(TCB)中。
TCP 可以看成一种字节流服务。IP 层可能出现丢包、重复、乱序和数据损坏等情况,TCP 通过序列号、确认号、校验和以及重传机制,为上层应用提供可靠、有序的字节流:
- 序列号用于标识字节位置,帮助 TCP 检测重复和乱序;
- 累积确认用于告知发送方已经连续收到的字节范围;
- 校验和用于检测报文损坏;
- 超时重传、快速重传等机制用于恢复丢失的数据。
TCP 采用三次握手建立连接,通常采用四次挥手关闭连接。建立连接时,双方需要同步初始序列号(ISN);关闭连接时,两个方向的数据流需要分别结束。
二、TCP 服务模型
2.1 TCP 连接和四元组
一个 TCP 连接通常由以下四元组唯一标识:
源 IP、源端口、目的 IP、目的端口
例如:
192.168.1.10:53000 -> 192.168.1.20:6379
TCP 连接通常可以分为三个阶段:
- 连接建立;
- 数据传输;
- 连接关闭。
2.2 全双工和字节流
TCP 是全双工协议。连接建立后,两个方向都可以独立地发送和接收数据。应用程序可以选择双向通信,也可以只使用其中一个方向。
TCP 不保留应用层的消息边界。例如,应用程序连续调用两次 send(),接收方可能一次 recv() 读到两次发送的数据,也可能分多次读到。因此,应用层协议需要自己设计消息边界,例如固定长度、分隔符或长度字段。
2.3 累积确认
当 TCP 接收到另一端的数据时,通常会发送确认,但确认可能因为延迟确认策略而稍后发送。TCP 的 ACK 通常是累积的:确认号为 N,表示序列号小于 N 的数据已经按序接收,接收方下一步期望收到序列号为 N 的字节。
例如:
ACK = 1001
表示序列号 0 到 1000 的字节已经按序收到,接收方期待的下一个字节是 1001。
累积确认的好处是,即使某一个 ACK 丢失,后续 ACK 也可能覆盖之前已经确认的范围。
2.4 序列号和乱序处理
IP 协议本身不负责消除重复,也不保证报文按照发送顺序到达。TCP 使用序列号识别数据位置,可以丢弃重复报文,并将乱序到达的数据暂存在接收缓冲区中。
TCP 是字节流协议,不会将乱序数据直接交给应用程序。接收方通常要等缺失的小序列号数据到达后,才能把连续的数据交付给上层。
三、TCP 头部

TCP 头部中的源端口和目的端口用于确定通信双方的应用进程。序列号表示当前报文段中第一个数据字节的序列号;确认号表示发送方期望接收的下一个序列号。确认号字段只有在 ACK 标志位有效时才有意义。
3.1 序列号、确认号和控制位
当建立一个新连接时,客户端发送的第一个 SYN 报文段会携带本端的初始序列号 ISN。如果 SYN 的序列号为 x,那么后续数据通常从 x + 1 开始。SYN 会消耗一个序列号;ACK 本身不携带数据,也不消耗序列号。
FIN 同样会消耗一个序列号。更准确地说,TCP 的报文段长度由数据长度加上 SYN 和 FIN 所占的序列空间组成。
常见控制位如下:
- ACK:确认号字段有效;
- RST:立即重置连接,常见的
connection reset by peer与此有关; - SYN:同步序列号,用于建立连接;
- FIN:发送方没有更多数据需要发送;
- PSH:提示接收方尽快将数据交给应用程序;
- URG:紧急指针字段有效;
- ECE/CWR:与 ECN 拥塞通知有关。
3.2 头部长度
TCP 头部中的数据偏移(Data Offset)以 32 位字为单位,也就是以 4 字节为单位。该字段占 4 位,取值范围为 5~15,因此:
- 最小 TCP 头部长度:
5 × 4 = 20字节; - 最大 TCP 头部长度:
15 × 4 = 60字节。
可选项为空时,TCP 头部通常为 20 字节;使用 MSS、时间戳、窗口扩大等选项时,头部会变长。
在最常见的 TCP 建连和关闭过程中,SYN、ACK、FIN 报文通常只携带 TCP 头部和选项、不携带应用数据。但这不是绝对规则,例如 TCP Fast Open 可以让 SYN 携带数据,FIN 也可以与最后一段数据一起发送。
四、TCP 状态转换
TCP 的建立、传输和关闭会经历不同状态,例如 LISTEN、SYN-SENT、SYN-RECEIVED、ESTABLISHED、FIN-WAIT-1、FIN-WAIT-2、CLOSE-WAIT、LAST-ACK 和 TIME-WAIT。

几个常见状态的含义如下:
LISTEN:服务器等待连接请求;SYN-SENT:主动发起连接后,等待对方响应;SYN-RECEIVED:收到 SYN 并发送 SYN-ACK,等待客户端确认;ESTABLISHED:连接已建立,可以传输数据;FIN-WAIT-1:本端已经发送 FIN,等待 ACK 或对端 FIN;FIN-WAIT-2:本端 FIN 已被确认,等待对端 FIN;CLOSE-WAIT:收到对端 FIN,等待本地应用关闭;LAST-ACK:本端 FIN 已发送,等待最后的 ACK;TIME-WAIT:主动关闭的一方等待一段时间,避免旧报文影响后续连接。
五、为什么是“三次握手”
5.1 三次握手的目的
客户端和服务端通信前要先建立连接。三次握手的主要目的包括:
- 确认双方都能发送和接收 TCP 报文;
- 交换并确认双方的初始序列号;
- 协商 MSS、窗口扩大、SACK、时间戳等 TCP 选项;
- 让双方进入
ESTABLISHED状态。
“三次握手可以确认双方收发能力正常”是便于理解的概括。它验证的是 TCP 报文在两个方向上的可达性和序列号同步,并不等价于应用进程一定正常或网络路径绝对可靠。
5.2 三次握手的过程
假设客户端的初始序列号为 x,服务端的初始序列号为 y:

- 第一次握手:客户端发送 SYN
客户端 -> 服务端:SYN,Seq = x
服务端收到后可以确认:客户端的发送能力正常,服务端的接收能力正常。 - 第二次握手:服务端发送 SYN-ACK
服务端 -> 客户端:SYN + ACK,Seq = y,Ack = x + 1
服务端发送自己的 SYN,同时确认收到了客户端的 SYN。客户端收到后可以确认:服务端能够接收客户端的报文,也能够向客户端发送报文;客户端自己的接收能力也正常。 - 第三次握手:客户端发送 ACK
客户端 -> 服务端:ACK,Seq = x + 1,Ack = y + 1
服务端收到这个 ACK 后可以确认:客户端已经收到服务端的 SYN-ACK,服务端的发送能力和客户端的接收能力均正常。至此,双方完成初始序列号同步,通常进入ESTABLISHED状态。

5.3 为什么不能只握手两次
如果只有两次握手:
客户端 -> 服务端:SYN
服务端 -> 客户端:SYN + ACK
服务端还没有收到客户端对第二个报文的确认,因此无法确定客户端是否真的收到了自己的 SYN-ACK。第三次 ACK 正是用来让服务端确认客户端已经收到第二次握手的响应。
因此,三次握手不是简单地“多发一个包”,而是完成双方序列号同步和双向可达性确认所需的最小流程。
六、为什么通常是“四次挥手”
TCP 连接是全双工的,两个方向的数据流可以独立关闭。当一方不再发送数据时,它可以发送 FIN,但这只表示“我不再发送”,并不代表它不能继续接收数据。
假设客户端先关闭连接,典型过程如下:

- 客户端发送 FIN,表示客户端没有更多数据要发送;
- 服务端发送 ACK,确认收到客户端的 FIN。此时客户端不能再发送数据,但仍然可以接收服务端的数据;
- 服务端应用程序处理完剩余数据后,服务端发送 FIN;
- 客户端发送 ACK,确认收到服务端的 FIN。
用序列号表示:
客户端 -> 服务端:FIN,Seq = u
服务端 -> 客户端:ACK,Ack = u + 1
服务端 -> 客户端:FIN,Seq = v,Ack = u + 1
客户端 -> 服务端:ACK,Ack = v + 1
6.1 为什么 FIN 通常不能像 SYN-ACK 一样一次完成
服务端收到客户端的 FIN 时,只能知道客户端不再发送数据了,但服务端应用程序可能还有数据没有发送完。服务端是否发送 FIN,要由服务端应用程序决定,因此服务端通常先发送 ACK,稍后再发送 FIN。
这就是建立连接常见为三次握手,而关闭连接常见为四次挥手的主要原因:
- 建立连接时,服务端的 SYN 和 ACK 通常可以放在一个报文中;
- 关闭连接时,ACK 可以立即发送,但 FIN 往往要等应用程序确认没有更多数据后再发送。
6.2 四次挥手并不是绝对固定四个报文
“四次挥手”是最常见的讲法,但实际报文数量可能变化:
- 如果服务端已经准备好关闭,ACK 和 FIN 可以合并发送;
- 如果双方同时发送 FIN,可能进入
CLOSING状态; - FIN 可以与最后一段应用数据一起发送。
主动关闭的一方通常会进入 TIME-WAIT。该状态用于确保对端收到最后一个 ACK,并避免旧连接中的延迟报文影响后续使用相同四元组建立的新连接。
七、三次握手和四次挥手的报文细节
7.1 三次握手
- 客户端发送一个 SYN 段,并指明客户端的初始序列号
ISN(c); - 服务端发送自己的 SYN 段作为应答,并指明
ISN(s)。为了确认客户端的 SYN,确认号为ISN(c) + 1; - 客户端为了确认服务端的 SYN,将
ISN(s) + 1作为 ACK 的值。
每发送一个 SYN,都会消耗一个序列号。如果 SYN 丢失,TCP 会根据重传机制重新发送。
7.2 四次挥手
假设客户端当前序列号为 K,服务端当前序列号为 L:
- 客户端发送 FIN,序列号为
K,并携带对服务端最近数据的 ACK; - 服务端将
K + 1作为 ACK,表示已经收到客户端的 FIN; - 服务端应用程序处理完数据后,发送 FIN,序列号为
L,确认号为K + 1; - 客户端发送 ACK,确认号为
L + 1。
八、ISN:初始序列号
三次握手的一个重要功能是客户端和服务端交换 ISN(Initial Sequence Number),以便双方知道接下来如何根据序列号组织和确认数据。
早期规范中常用下面的形式描述 ISN 生成算法:
ISN = M + F(localip, localport, remoteip, remoteport, secretkey)
其中:
M是随时间单调增加的计时器,经典规范中约每 4 微秒增加一次;F()是基于连接四元组和秘密信息计算的伪随机函数;secretkey用于避免外部攻击者根据已经观察到的序列号推算其他连接的 ISN。
这个公式用于解释设计思想,不应直接当成所有操作系统的实际源代码。现代系统会结合内核版本、随机密钥和其他安全策略生成 ISN。
ISN 的设计同时考虑了两个问题:
- 避免旧连接的延迟报文被误认为属于新连接;
- 降低攻击者猜测序列号并伪造 TCP 报文的概率。
良好的 ISN 不能替代 TLS、IPsec 或 TCP-AO 等真正的身份认证机制。
九、TCP 序列号回绕
TCP 序列号字段只有 32 位,序列号空间为 0 到 2^32 - 1,超过最大值后会从 0 重新开始。这叫作序列号回绕(sequence wraparound)。
序列号回绕不是因为随机 ISN 超过 2^31 - 1,而是因为 32 位序列号空间按 2^32 取模循环。TCP 不能简单使用无符号整数的大小比较,而要使用序列号空间中的相对顺序。通常只有在两个序列号相距小于 2^31 时,这种比较才是有意义的。
Linux 内核中常见的比较思路如下:
/*
* The next routines deal with comparing 32 bit unsigned ints
* and worry about wraparound (automatic with unsigned arithmetic).
*/
static inline int before(__u32 seq1, __u32 seq2)
{
return (__s32)(seq1 - seq2) < 0;
}
#define after(seq2, seq1) before(seq1, seq2)
__u32 表示 32 位无符号整数,__s32 表示 32 位有符号整数。通过将相减结果解释为有符号数,可以在一定范围内正确判断回绕后的先后关系。
为了便于理解,可以用 8 位序列号做一个类比。假设序列号从 0 到 255 循环:
seq1 = 255
seq2 = 1
255 - 1 = 254
如果将 8 位结果 254 解释为有符号 8 位整数,它等于 -2。这说明在序列号回绕的语义下,255 可以被判断为位于 1 之前。真实 TCP 使用的是 32 位序列号,并且必须遵守半序列号空间的限制,不能直接照搬这个 8 位例子进行任意比较。
十、SYN Flood 攻击
最基本的 DoS(Denial of Service,拒绝服务)攻击之一,是利用大量请求占用服务端资源,使合法用户无法得到及时响应。SYN Flood 就是针对 TCP 建连过程的一类 DoS 攻击。
10.1 攻击原理
攻击者向服务器端口发送大量 SYN 报文。服务器收到 SYN 后,通常需要保存一个待完成连接的状态,向客户端回复 SYN-ACK,并进入 SYN-RECEIVED 状态,等待客户端最后的 ACK。
大量半连接会占用连接队列、定时器和内存资源。如果攻击者伪造源 IP,使服务器回复的 SYN-ACK 无法到达真正的发送者,服务器还需要等待重传超时,资源会被占用更久。
不同操作系统的具体实现不同。Linux 通常会使用较轻量的 request_sock 保存半连接请求,并可以通过 SYN Cookie 等机制减少为每个请求分配完整 TCB 的需求,因此不应把某个固定的 TCB 字节数当成普遍结论。
10.2 常见防护方法
无效连接的超时释放
系统可以通过重传次数和超时机制清理长时间没有完成的半连接。这种方法简单,但对攻击连接和正常连接一视同仁,不能单独解决大规模 SYN Flood。
SYN Cache
系统只在专用的数据结构中保存完成握手所必需的少量信息,收到正确 ACK 后再创建完整连接控制块。这样可以降低半连接状态的内存开销。
SYN Cookie
SYN Cookie 不为每一个半连接保存完整的服务器端状态,而是将部分必要信息编码进服务端发送的初始序列号中。收到客户端 ACK 后,服务端重新计算并验证该 ACK,验证通过后再创建连接。
SYN Cookie 可以显著降低半连接队列压力,但会受到序列号空间和 TCP 选项空间的限制,具体行为取决于实现。
SYN Proxy 防火墙
SYN Proxy 通常会在客户端和后端服务器之间分别建立两个 TCP 连接:
- 防火墙先与客户端完成 TCP 握手;
- 防火墙验证客户端的 ACK 后,再向内部服务器发起新的 SYN;
- 后续数据由防火墙在两条连接之间转发。
这种方式可以将大量不完整连接挡在后端服务器之外,但防火墙需要维护两条连接的状态,并可能需要处理两端的序列号转换。
十一、连接队列
在外部连接请求到达后、被服务程序通过 accept() 取走前,连接可能处于 SYN-RECEIVED 或 ESTABLISHED 状态。Linux 服务端通常可以从两个队列的角度理解这个过程:

- 半连接队列:保存处于
SYN-RECEIVED状态、等待客户端最终 ACK 的请求; - 全连接队列(accept 队列):保存已经完成握手、等待应用程序
accept()的连接。
这两个队列的具体实现和参数会随操作系统、内核版本以及 SYN Cookie 是否启用而变化。
11.1 半连接队列
Linux 中,tcp_max_syn_backlog 表示每个监听 socket 能够记住的、尚未收到客户端确认的连接请求数量。请求通常处于 SYN-RECEIVED 状态。
服务器收到 SYN 后,会创建轻量级的请求状态并回复 SYN-ACK;当收到客户端 ACK 后,连接才会进入已完成连接队列。

tcp_synack_retries 控制服务端 SYN-ACK 的重传次数。不同内核、初始 RTO 和配置会影响实际等待时间,因此不应简单地把重试时间固定为 31 秒。可以通过系统参数查看当前配置:
sysctl net.ipv4.tcp_synack_retries
sysctl net.ipv4.tcp_max_syn_backlog
11.2 全连接队列
当服务端收到第三次握手的 ACK 后,连接会进入监听 socket 的完成队列,等待应用程序调用 accept()。

如果完成队列满了,Linux 的行为受到 tcp_abort_on_overflow 等参数影响:
- 默认情况下,内核可能暂时丢弃 ACK,并在稍后重传 SYN-ACK,等待客户端再次确认;
- 如果启用
tcp_abort_on_overflow,内核可能直接发送 RST,让客户端收到connection reset by peer; - 客户端也可能因为长时间收不到有效响应而出现连接超时。
相关参数可以这样查看:
sysctl net.ipv4.tcp_abort_on_overflow
cat /proc/sys/net/core/somaxconn
服务端并不是简单地“重传 SYN 和 ACK”,而是重传一个 SYN-ACK 报文,目标是客户端,而不是服务端自己。
十二、相关命令
查看 TCP 统计信息:
netstat -s | grep -Ei 'listen|overflow'
部分系统也可以使用:
ss -s
ss -lnt
ss -lnt 中常见字段含义如下:
State Recv-Q Send-Q Local Address:Port Peer Address:Port
LISTEN 0 128 0.0.0.0:6379 0.0.0.0:*
对于监听 socket,Recv-Q 和 Send-Q 的具体含义取决于系统版本;通常可以将 Send-Q 理解为监听队列上限,将 Recv-Q 理解为当前等待应用程序处理的连接数量。
十三、Redis 抓包实例分析
下面保留原文中的 Redis 抓包示例,用于分析 TCP 三次握手和 Redis PING/PONG 数据。
13.1 抓取 6379 端口数据
在 dev 机器上部署 Redis 服务,端口号为 6379。在 dev2 机器上使用 redis-cli 访问 dev:6379。
使用 tcpdump 抓包:
tcpdump -i any -nn -s 0 -w /tmp/a.cap 'port 6379'
参数说明:
-i any:监听所有可用网卡,具体接口也可以替换为eth0等;-nn:不解析主机名和端口服务名;-s 0:使用当前 tcpdump/libpcap 版本的默认快照长度,现代版本通常足以抓取完整报文;-w:将原始数据包写入文件;'port 6379':只捕获与 6379 端口相关的报文。
-S 与抓取长度无关,它用于让 tcpdump 显示绝对 TCP 序列号:
tcpdump -r /tmp/a.cap -nn -S
读取并分析捕获文件:
tcpdump -r /tmp/a.cap -nn -A -x | vim -
其中,-A 以 ASCII 形式显示部分数据,-x 以十六进制形式显示数据。-n 和 -nn 功能有重叠,实际使用一个 -nn 即可。
13.2 IP 数据包和 TCP 数据
抓到的是 IP 数据包。对于 IPv4,IP 数据包通常由 IPv4 头部和 IP 数据部分组成;当上层协议是 TCP 时,IP 数据部分就是 TCP 头部加 TCP 数据。

IPv4 头部最小为 20 字节,存在选项时会变长。IPv6 的基本头部长度和格式不同,不能直接套用下面的 IPv4 抓包解析。

TCP 头部同样是 20 字节固定部分加可选项部分:

TCP 选项位于 TCP 头部末尾,常见选项包括 MSS、SACK、时间戳和窗口扩大:

13.3 三次握手分析
原始抓包中的第一段类似下面这样:
10:55:45.662077 IP dev2.39070 > dev.6379: Flags [S], seq 4133153791, win 29200, options [mss 1460,sackOK,TS val 2959270704 ecr 0,nop,wscale 7], length 0
0x0000: 4500 003c 08cf 4000 3606 14a5 0ab3 b561
0x0010: 0a60 5cd4 989e 18eb f65a ebff 0000 0000
0x0020: a002 7210 872f 0000 0204 05b4 0402 080a
0x0030: b062 e330 0000 0000 0103 0307
第一个包是 dev2 向 dev 发送的 SYN 请求:
dev2 -> dev
SYN = 1
Seq(c) = 4133153791
第二个包是服务端响应:
SYN = 1
ACK = 4133153792
Seq(s) = 4264776963
服务端的 ACK = Seq(c) + 1,表示已经收到客户端的 SYN。服务端自己的 SYN 序列号为 4264776963。
第三个包是客户端确认:
Seq = 4133153792
ACK = 4264776964
这里的 ACK = Seq(s) + 1,说明客户端已经收到服务端的 SYN-ACK。至此,TCP 三次握手完成,双方可以开始传输应用数据。
需要注意,tcpdump 默认可能显示相对序列号。使用 -S 才会显示绝对序列号;原始十六进制数据中看到的是实际的 32 位序列号。
13.4 Redis PING 数据分析
第四个包是客户端向服务端发送 Redis PING 请求:
10:55:48.090073 IP dev2.39070 > dev.6379: Flags [P.], seq 1:15, ack 1, win 229, options [nop,nop,TS val 2959273132 ecr 3132256230], length 14
0x0000: 4500 0042 08d1 4000 3606 149d 0ab3 b561
0x0010: 0a60 5cd4 989e 18eb f65a ec00 fe33 5504
0x0020: 8018 00e5 4b5f 0000 0101 080a b062 ecac
0x0030: bab2 6fe6 2a31 0d0a 2434 0d0a 7069 6e67
0x0040: 0d0a
TCP 首部长度为 32 字节,其中可选项长度为 12 字节。IP 报文总长度为 66 字节,IPv4 头部为 20 字节,因此 TCP 数据部分长度为:
66 - 20 - 32 = 14 字节
数据部分为:
2a31 0d0a 2434 0d0a 7069 6e67 0d0a
对应的 ASCII 内容为:
0x2a31 -> *1
0x0d0a -> \r\n
0x2434 -> $4
0x0d0a -> \r\n
0x7069 0x6e67 -> ping
0x0d0a -> \r\n
这正是 Redis 协议中的:
*1\r\n$4\r\nping\r\n
第四个包的 TCP 数据序列号为 4133153792,因此 tcpdump 使用相对序列号显示为 seq 1:15。
13.5 ACK 和 PONG 数据分析
第五个包是客户端对 PING 数据的确认:
Seq = 4264776964
ACK = 4133153806
其中:
4133153806 = 4133153792 + 14
说明服务端已经可以继续从 4133153806 开始接收客户端数据。
第六个包是服务端返回的 PONG:
Seq = 4264776964
ACK = 4133153806
TCP 数据长度为 7 字节,数据内容为:
2b50 4f4e 470d 0a
翻译为:
+PONG\r\n
至此,Redis 客户端和服务端的 PING/PONG 数据交换分析完成。TCP 三次握手已经在前三个包中完成。如果完整抓包中还有第七个纯 ACK,应将其作为服务端 PONG 的确认单独说明,不能把它再次称为三次握手。
十四、小结
TCP 三次握手和四次挥手可以概括为:
- 三次握手:交换初始序列号,确认双向可达性,建立连接;
- 数据传输:使用序列号、ACK、窗口和重传机制提供可靠的字节流;
- 四次挥手:两个方向分别关闭,通常需要 FIN、ACK、FIN、ACK 四个报文;
- 序列号:用于按序交付、重复检测、丢包恢复和连接安全;
- SYN Flood:利用半连接状态消耗服务端资源,可通过 SYN Cookie、SYN Proxy、队列调优和流量清洗等方式缓解。
相关资料: