2. TCP连接建立(三次握手):过程详解与状态变迁

TCP 是面向连接的协议。这话听起来有点抽象,说白了就是:在正式收发数据之前,双方得先打个招呼,确认对方在线、愿意通信、并且准备好了缓冲区。这个打招呼的过程,就是大名鼎鼎的「三次握手」。

我刚开始学网络的时候,觉得三次握手不就是发三个包嘛,背下来就行了。后来在项目里排查过几次连接超时的问题,才真正体会到——握手里的每一个状态、每一个标志位,都可能成为线上故障的根源。咱们今天就把这个过程彻底拆开来看。

2.1 三次握手:逐包拆解

假设客户端 A 想和服务器 B 建立连接。整个过程分三步走:

  1. 第一次握手(SYN):客户端 A 发送一个 SYN 报文,告诉服务器 B:「我想跟你建立连接,这是我的初始序列号 x。」此时 A 进入 SYN_SENT 状态。
  2. 第二次握手(SYN + ACK):服务器 B 收到后,回复一个 SYN+ACK 报文,意思是:「收到你的请求,我同意。这是我的初始序列号 y,同时确认收到了你的 x(确认号 = x+1)。」B 进入 SYN_RCVD 状态。
  3. 第三次握手(ACK):客户端 A 再发一个 ACK 报文,确认收到了 B 的 SYN(确认号 = y+1)。此时 A 进入 ESTABLISHED 状态。B 收到这个 ACK 后,也进入 ESTABLISHED 状态。

核心要点:三次握手的本质是「双方各自确认对方的接收能力和发送能力都正常」。第一次握手证明 A 能发;第二次握手证明 B 能收能发;第三次握手证明 A 能收。缺任何一次,都无法保证双向通信的可靠性。

这里有个细节我提醒一下:SYN 报文本身不携带应用层数据,但它会消耗一个序列号。ACK 报文如果不携带数据,则不消耗序列号。这个区别在后续的可靠性传输中很关键。

2.2 状态变迁:从 CLOSED 到 ESTABLISHED

TCP 的状态机是面试常客,也是排查问题的利器。我建议你把这个状态变迁图记在脑子里,线上看 netstat 或者 ss 的时候,一眼就能看出连接卡在哪一步。

我的习惯:在服务器上排查连接问题时,我第一件事就是看 ss -tna 的状态列。如果大量连接卡在 SYN_RCVD,那多半是遭受了 SYN Flood 攻击;如果卡在 CLOSE_WAIT,那就是应用层忘了关 socket。

下面是三次握手涉及的状态变迁表:

状态 所属端 含义 触发动作
CLOSED 双方 初始状态,无连接 —
LISTEN 服务器 等待客户端连接 调用 listen()
SYN_SENT 客户端 已发送 SYN,等待 SYN+ACK 调用 connect()
SYN_RCVD 服务器 已收到 SYN,已回复 SYN+ACK,等待 ACK 收到 SYN 后自动进入
ESTABLISHED 双方 连接已建立,可以传输数据 收到第三次握手 ACK

你想想看,如果客户端发完 SYN 后一直没收到 SYN+ACK,它会怎么样?内核会启动超时重传,默认重试 5 次,每次间隔翻倍(1s、2s、4s、8s、16s),最后放弃并回到 CLOSED。这个超时时间在线上调优时经常需要调整。

2.3 为什么是三次,不是两次或四次?

这个问题几乎每次面试都会问到。我直接说结论:两次不够,四次浪费。

咱们先看「两次握手」有什么问题。假设只有两次握手:客户端发 SYN,服务器回 SYN+ACK,然后服务器就直接认为连接建立了。但万一客户端的 SYN 报文在网络中延迟了,客户端超时重传了一个新的 SYN,服务器处理了新的 SYN 并建立了连接。等数据传输完毕关闭后,那个延迟的旧 SYN 才到达服务器。服务器以为这是一个新连接,又分配资源进入 ESTABLISHED 状态,但客户端根本不会理它——这就造成了半开连接,白白浪费服务器资源。

三次握手就能解决这个问题:服务器必须等到客户端的第三次 ACK,才能确认这个连接是「活」的、不是历史重传。如果第三次 ACK 没来,服务器就知道这个 SYN 可能是过期的,直接丢弃。

我曾经踩过的坑:有一次线上服务器内存飙升,查了半天发现是 tcp_syncookies 没开,大量 SYN_RCVD 状态的半连接占满了 backlog 队列。后来我养成了一个习惯——新上线的服务,第一件事就是检查 net.ipv4.tcp_syncookies = 1 有没有配好。

那「四次握手」呢?理论上当然可以:客户端发 SYN,服务器回 ACK,服务器再发 SYN,客户端回 ACK。但仔细想想,服务器的 ACK 和 SYN 完全可以合并成一个报文发送,没必要拆成两次。合并之后就是三次,不多不少刚刚好。

说白了,三次握手是 TCP 设计者在可靠性和效率之间找到的最佳平衡点。两次不可靠,四次没必要。

2.4 三次握手流程图

下面我用一张 SVG 图把整个过程串起来。这张图我建议你多看几遍,把状态和报文对应上,以后看抓包就轻松了。

TCP 三次握手流程与状态变迁 客户端 (A) 服务器 (B) 状态: CLOSED 状态: LISTEN ① SYN (seq=x) → SYN_SENT ② SYN+ACK (seq=y, ack=x+1) → SYN_RCVD ③ ACK (ack=y+1) → ESTABLISHED → ESTABLISHED 关键说明 • 第一次握手:客户端证明自己能发 • 第二次握手:服务器证明自己能收能发 • 第三次握手:客户端证明自己能收,防止历史重传

2.5 几个容易忽略的细节

  • 初始序列号 ISN 不是从 0 开始的。现代 TCP 实现会使用一个随时间变化的随机值,防止旧连接的数据包干扰新连接。我记得 Linux 内核里有一个 tcp_isn() 函数,基于时钟和哈希生成。
  • 第三次握手丢了怎么办? 服务器收不到 ACK,会一直停留在 SYN_RCVD 状态,然后超时重传 SYN+ACK(默认重试 5 次)。如果客户端此时已经进入 ESTABLISHED 并开始发数据,数据包里的 ACK 标志会顺便把第三次握手的确认带过去。
  • 同时打开(Simultaneous Open):两边同时发 SYN,这种情况极少见,但 TCP 规范支持。双方都会进入 SYN_SENT,收到对方的 SYN 后进入 SYN_RCVD,最后各自发 ACK 进入 ESTABLISHED。嗯,这个场景我在实际项目中一次都没遇到过。

避坑指南:如果你用抓包工具(比如 tcpdump 或 Wireshark)看三次握手,注意观察 SYN 报文的 MSS、Window Scale、SACK Permitted 等选项。这些选项只在握手阶段协商,一旦连接建立就固定了。我曾经遇到过因为 MSS 不匹配导致 MTU 分片过多的性能问题,排查了很久才发现是中间设备改了 MSS 值。

好了,三次握手的内容就讲到这里。记住这张状态变迁图,理解每一步的目的,后面讲四次挥手和状态机的时候,你会发现很多东西都是对称的。