← 全部笔记

前置知识 · 计算机网络

TCP 三次握手:三个包,交换两个初始序列号

建立连接时,双方各报一次自己的起始编号,再各确认一次对方的编号。读完这一页,你能:

约 15 分钟 · 截至 2026-11-04,Docker Engine 29.x · 抓包实验状态未记录 · 依据 RFC 9293(Docker 版本为课程基线)

问题

发数据之前,两端要先确认两件事

第一,对方在线,而且能收也能发。第二,对方从哪个编号开始给字节编号。TCP 给每个字节编一个号,叫序列号(Sequence Number, seq);起始编号叫初始序列号(Initial Sequence Number, ISN)。

三次握手总览 客户端发 SYN 报出自己的起始编号 x;服务器回 SYN+ACK,确认收到 x 并报出自己的起始编号 y;客户端回 ACK 确认收到 y。 客户端 服务器 ① SYN:我要连接,我从 x 开始编号 ② SYN+ACK:收到 x;我从 y 开始编号 ③ ACK:收到 y
图 1 三个包里,每一方各报一次自己的起始编号,各确认一次对方的。箭头向下倾斜,表示包在路上要花时间。

先别往下看:只看图 1,你觉得第 ③ 个包是必需的吗?如果去掉它,谁会“不知道”什么?

去掉 ③,服务器就不知道客户端是否真的收到了 y,也分不清这个 SYN 是新的还是网络里滞留很久的旧包。第 4 节会用对比图细讲。

机制

ack 的意思是“下一个请从这里开始”

每个包有两个关键数字。seq 是这个包第一个字节的编号。确认号(Acknowledgment Number, ack)是“我已收到之前所有字节,下一个期待的编号”。SYN 包本身不带数据,但规定它占用一个编号,所以确认 SYN 时 ack = 对方 seq + 1。

下图用 1000 和 5000 当初始序列号,方便看清楚。真实连接里的 ISN 是难以预测的 32 位数。点“下一步”逐包看。

三次握手逐包时序图 客户端 SYN seq=1000;服务器 SYN+ACK seq=5000 ack=1001;客户端 ACK ack=5001;随后客户端发送第一个数据字节,seq=1001。客户端状态 CLOSED、SYN_SENT、ESTABLISHED;服务器状态 LISTEN、SYN_RCVD、ESTABLISHED。 客户端 服务器 CLOSED LISTEN SYN_SENT SYN_RCVD ESTABLISHED ESTABLISHED SYN seq=1000 SYN+ACK seq=5000 ack=1001 ACK ack=5001 数据 seq=1001 第一个数据字节
  1. 客户端发出 SYN(同步)包,带上自己的初始序列号 1000,进入 SYN_SENT。猜猜:服务器回的包里,ack 是多少?
  2. 服务器回 SYN+ACK。ack=1001 的意思是“1000 收到了,下一个从 1001 开始”。它同时带上自己的初始序列号 5000,进入 SYN_RCVD。客户端接下来回什么?ack 是多少?
  3. 客户端收到 SYN+ACK 就进入 ESTABLISHED,并回 ACK:ack=5001,确认收到服务器的 5000。
  4. 服务器收到这个 ACK 才进入 ESTABLISHED。现在双方都确认了对方能收能发,也都知道了对方的 ISN。客户端发的第一个数据字节,seq 是多少?
  5. 第一个数据字节的编号是 1001,不是 1000:编号 1000 已经被 SYN 用掉了。
图 2 每一方都用 ack = 对方 seq + 1 来确认对方的 SYN;客户端比服务器早一个包进入 ESTABLISHED。

用一两句话解释:确认 SYN 时,为什么 ack 是对方 seq + 1,而不是 seq 本身?

参考答案:ack 表示“我期待收到的下一个编号”。SYN 虽然不带数据,但规定占用一个编号,所以收到 seq=x 的 SYN 后,下一个期待的编号就是 x+1。

  • 说出 ack 表示“下一个期待的编号”,而不是“我收到的编号”
  • 说出 SYN 占用一个序号

动手

在自己的电脑上抓到这三个包

开三个终端窗口,都在同一台 Linux 机器上(虚拟机或 WSL 也行)。抓包需要 root 权限。

# 终端 1:在 8080 端口起一个最简单的 HTTP 服务
python3 -m http.server 8080 --bind 127.0.0.1
# 终端 2:抓回环网卡上 8080 端口的包。-n 不解析域名,-S 显示原始序列号,-t 不打印时间戳
sudo tcpdump -t -nS -i lo port 8080
# 终端 3:发起一次连接
curl -s http://127.0.0.1:8080/ > /dev/null
# 终端 2 的前三行(数字每次都不同):
IP 127.0.0.1.52314 > 127.0.0.1.8080: Flags [S], seq 2193340411, win 65495, options [mss 65495,sackOK,TS val 3051 ecr 0,nop,wscale 7], length 0
IP 127.0.0.1.8080 > 127.0.0.1.52314: Flags [S.], seq 3780412906, ack 2193340412, win 65483, options [mss 65495,sackOK,TS val 3051 ecr 3051,nop,wscale 7], length 0
IP 127.0.0.1.52314 > 127.0.0.1.8080: Flags [.], ack 3780412907, win 512, options [nop,nop,TS val 3051 ecr 3051], length 0

怎么读:Flags [S] 是 SYN,[S.] 是 SYN+ACK(点号表示 ACK 标志),[.] 是纯 ACK。后面还有 HTTP 数据和关闭连接的包,先只看前三行。

深入一点:不加 -S 会怎样?

tcpdump 默认在握手之后改用“相对序列号”,把 ISN 当作 0 来显示,所以第三行会显示 ack 1。加 -S 才能看到上面这样的原始数字,方便验证 ack = seq + 1。

换一组数字。客户端 ISN = 3000,服务器 ISN = 7000。把四个空填完整。

② SYN+ACK:seq ,ack

③ ACK:ack

客户端第一个数据字节:seq

服务器的 seq 是它自己的 ISN 7000;它确认客户端的 SYN,所以 ack = 3000 + 1。客户端确认服务器的 SYN,ack = 7000 + 1。SYN 用掉了 3000,所以数据从 3001 开始。

你抓到一个包:Flags [S.], seq 2408150375, ack 913477602。客户端的初始序列号是多少?

这是 SYN+ACK 自己的 seq,也就是服务器的 ISN。

这是 ack,表示“下一个期待的编号”,比客户端的 ISN 大 1。

这是之后客户端回 ACK 时会用的 ack 值,确认的是服务器的 ISN。

[S.] 是服务器发的 SYN+ACK。它的 ack 确认的是客户端的 SYN,等于客户端 ISN + 1,所以客户端 ISN = 913477602 − 1 = 913477601。

为什么是三次

第三个包让服务器能识别“过期的连接请求”

网络里的包可能滞留很久才到达。设想客户端很早以前发过一个 SYN,后来超时放弃了,这个旧 SYN 现在才到服务器。RFC 9293 指出,三次握手的主要原因就是防止这种旧的重复连接请求造成混乱。

如果规则是“服务器回完 SYN+ACK 就算连接建立”(两次握手),那个迟到的旧 SYN 会导致什么?

服务器会为一个早已没人要的连接分配资源,然后一直等客户端发数据。客户端根本不认识这个连接,不会再发任何东西。三次握手里,服务器在收到第三个包前只处于 SYN_RCVD,客户端看到对不上的 SYN+ACK 会回 RST(重置),服务器随即丢弃这个半连接。

资源被白占假如只有两次

客户端服务器 旧 SYN(早已放弃) ESTABLISHED? SYN+ACK 不认识,丢弃 一直空等

及时清理实际的三次

客户端服务器 旧 SYN(早已放弃) SYN_RCVD SYN+ACK RST ack 对不上 丢弃半连接
图 3 同一个迟到的旧 SYN:两次握手会让服务器为它建立连接;三次握手里服务器只停在 SYN_RCVD,客户端回 RST 后就能清理掉。

下面每一项,属于客户端还是服务器?

  • 经历 SYN_SENT
  • 经历 LISTEN
  • 经历 SYN_RCVD
  • 先进入 ESTABLISHED
  • 发出第一个 SYN
  • 发出 SYN+ACK
客户端(主动打开)
服务器(被动打开)

主动发起连接的一方叫“主动打开”,经历 SYN_SENT;先调用 listen() 等待的一方叫“被动打开”,经历 LISTEN 和 SYN_RCVD。客户端比服务器早一个包进入 ESTABLISHED。

边界情况

只发 SYN 不回 ACK,连接会卡在半路

服务器每收到一个 SYN,都要记下这个半连接,等第三个包。攻击者可以伪造大量源地址只发 SYN,服务器发出的 SYN+ACK 没人回应,半连接越积越多,正常用户就连不上了。这叫 SYN 洪泛(SYN flood)。

# 查看处于半连接状态的 TCP 连接(ss 把 SYN_RCVD 显示为 SYN-RECV)
ss -tan state syn-recv
# 查看是否开启了 SYN cookies(1 = 半连接队列满时启用)
sysctl net.ipv4.tcp_syncookies
net.ipv4.tcp_syncookies = 1

服务器遭受 SYN 洪泛时,大量堆积的是哪种状态的连接?

要进入 ESTABLISHED,服务器得收到第三个包。攻击者恰恰不发它。

LISTEN 是监听套接字本身的状态,每个端口只有一个,不会因为连接请求增多而堆积。

SYN_SENT 是发起方(客户端)的状态,出现在攻击者那边,不在服务器上。

服务器回完 SYN+ACK 进入 SYN_RCVD,等第三个包。伪造的源地址永远不会回 ACK,这些半连接就一直停在 SYN_RCVD,直到超时。Linux 的 SYN cookies 让服务器在队列满时先不保存状态,而是把信息编码进自己的 ISN,等合法的 ACK 回来再还原。

回顾

要记住的

来源

  1. RFC 9293, Transmission Control Protocol (TCP), §3.4 序列号、§3.5 建立连接 — https://www.rfc-editor.org/rfc/rfc9293
  2. RFC 6528, Defending against Sequence Number Attacks(ISN 生成) — https://www.rfc-editor.org/rfc/rfc6528
  3. tcp(7) Linux 手册页:tcp_syncookies — https://man7.org/linux/man-pages/man7/tcp.7.html
  4. tcpdump(1) 手册页:-S、-n、-t 选项 — https://www.tcpdump.org/manpages/tcpdump.1.html