1. TCP协议基础:从物理信号到可靠连接

说实话,TCP 协议是我职业生涯里打交道最多的协议,没有之一。无论是写高并发服务器,还是排查线上故障,最后总能绕回 TCP 的状态机。这一章,我们就从最底层开始,把 TCP 的骨架搭起来。

1.1 TCP/IP 协议栈:数据是怎么「包」起来的?

你想想看,你在浏览器里输入一个网址,数据是怎么从你的电脑跑到服务器上的?这背后就是 TCP/IP 协议栈在干活。它分四层,每一层只关心自己的事,上层依赖下层。

层级 典型协议 核心职责
应用层 HTTP, FTP, SMTP 用户交互,数据格式
传输层 TCP, UDP 端到端通信,可靠性
网络层 IP, ICMP 路由寻址,分组转发
网络接口层 以太网, Wi-Fi 物理介质,帧传输

我个人习惯把协议栈想象成一个「俄罗斯套娃」。应用层的数据,到了传输层会被包上 TCP 头,到了网络层再包上 IP 头,最后在接口层加上帧头和帧尾。每一层只认自己的头,拆包的时候也是逐层剥开。

核心原则:对等层通信。你的 TCP 层只和对方的 TCP 层对话,中间的网络层只负责转发,不关心 TCP 头里的内容。

1.2 TCP 报文格式:头里藏着什么秘密?

TCP 报文头是 20 字节的固定长度(选项字段可以扩展)。我当年面试的时候,面试官让我手写 TCP 头结构,写不出来就挂了。现在想想,其实记住几个关键字段就够了。

 0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|         源端口号          |       目的端口号           |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                       序列号(Sequence Number)                 |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                    确认号(Acknowledgment Number)              |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 数据偏移 | 保留  |U|A|P|R|S|F|       窗口大小                  |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|         校验和            |        紧急指针          |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

这里我重点说几个你一定会碰到的字段:

  • 序列号(SEQ):每个字节都有一个编号,用来保证数据顺序。我第一次抓包分析时,看到序列号是随机初始化的,还以为是 bug,后来才知道这是安全考虑。
  • 确认号(ACK):告诉对方「我收到你发的第 N 号字节之前的所有数据了」。说白了就是期待下一个字节的编号。
  • 标志位:URG、ACK、PSH、RST、SYN、FIN。这六个位控制着连接的生命周期。我最常用的是 SYN、ACK、FIN,RST 一般出现在异常断开时。
  • 窗口大小:流量控制的关键。告诉对方「你一次最多发这么多,别撑着我」。我曾经遇到过窗口为 0 的情况,客户端一直在等,服务端也不发,典型的死锁场景。

避坑指南:我曾经在生产环境遇到过一个问题,客户端发数据特别慢,抓包一看,每次发送后都要等 ACK。原因是窗口太小,导致发送方必须等确认才能继续发。后来调整了接收缓冲区大小,问题就解决了。

1.3 三次握手:为什么是三次,不是两次或四次?

三次握手是 TCP 建立连接的经典过程。说白了,就是双方互相确认「我能发,你能收」。我刚开始学的时候觉得这很简单,直到有一次排查一个连接超时问题,才发现握手过程里藏着很多细节。

  1. 第一次握手(SYN):客户端发送 SYN 包,序列号设为 x(随机值)。客户端进入 SYN_SENT 状态。
  2. 第二次握手(SYN+ACK):服务端收到后,回复 SYN+ACK,确认号设为 x+1,自己的序列号设为 y(随机值)。服务端进入 SYN_RCVD 状态。
  3. 第三次握手(ACK):客户端收到后,发送 ACK,确认号设为 y+1。客户端进入 ESTABLISHED 状态。服务端收到后也进入 ESTABLISHED。

为什么必须是三次?我举个例子你就明白了。假设只有两次握手:客户端发 SYN,服务端回 SYN+ACK,然后服务端就认为连接建立了。但如果这个 SYN 是网络延迟重发的旧包呢?服务端就会白白浪费资源等一个不存在的客户端。三次握手就是为了防止「历史重复连接」的干扰。

关键点:第三次握手可以携带数据。也就是说,客户端在发送 ACK 的同时,可以直接把应用层数据带过去。这叫「捎带确认」,能减少一次网络往返。

1.4 四次挥手:好聚好散的艺术

断开连接比建立连接复杂。因为 TCP 是「全双工」的,双方都可以独立地关闭自己的发送通道。所以需要四次挥手。

  1. 第一次挥手(FIN):主动关闭方发送 FIN,表示「我这边没数据要发了」。进入 FIN_WAIT_1 状态。
  2. 第二次挥手(ACK):被动关闭方回复 ACK,表示「知道了」。进入 CLOSE_WAIT 状态。主动方收到后进入 FIN_WAIT_2。
  3. 第三次挥手(FIN):被动关闭方发送 FIN,表示「我这边也没数据了」。进入 LAST_ACK 状态。
  4. 第四次挥手(ACK):主动关闭方回复 ACK,进入 TIME_WAIT 状态。被动方收到后关闭。

这里有个坑:TIME_WAIT 状态会持续 2 个 MSL(最大报文段生存时间),一般是 1-4 分钟。我遇到过很多次,高并发短连接的服务端,端口被 TIME_WAIT 占满,导致新连接无法建立。解决办法是调整内核参数或者复用端口。

注意:TIME_WAIT 不是 bug,它是为了确保最后一个 ACK 能被对方收到。如果 ACK 丢了,对方会重发 FIN,而 TIME_WAIT 状态下的主机还能响应这个 FIN。直接关闭的话,对方会一直收不到确认,导致资源泄漏。

1.5 TCP 状态转换图:一张图看懂所有状态

TCP 的状态机是面试高频考点,也是排查网络问题的利器。下面这张图是我自己画的,把关键状态和转换路径都标出来了。

TCP 状态转换图 TCP 状态转换图 CLOSED SYN_SENT LISTEN SYN_RCVD ESTABLISHED FIN_WAIT_1 CLOSE_WAIT FIN_WAIT_2 LAST_ACK TIME_WAIT 主动打开 被动打开 收到 SYN+ACK 收到 SYN 收到 ACK 主动关闭 收到 FIN 收到 ACK 收到 FIN+ACK 关闭 收到 FIN 收到 ACK 超时 说明:实线为正常转换,虚线为超时或异常转换。TIME_WAIT 持续 2MSL 后自动回到 CLOSED。

这张图我建议你保存下来。排查网络问题时,用 netstat -anp 或者 ss -tunap 看连接状态,对照这张图就能快速定位问题出在哪一步。比如你看到大量 CLOSE_WAIT,那一定是服务端代码忘了调用 close()。我遇到过不止一次,都是因为异常处理没写好,导致连接泄漏。

实战技巧:用 tcpdump -i eth0 'tcp[tcpflags] & (tcp-syn|tcp-ack) != 0' 可以只抓握手包。我曾经用这个命令帮同事定位了一个跨机房连接延迟问题,发现是中间防火墙把 SYN 包丢了。