TCP 序列号和确认号是怎么工作的?为什么说 TCP 面向字节流?
简化版
TCP 的序列号不是按“第几个包”编号,而是按“字节”编号。Seq 表示本报文段携带数据的第一个字节序号,Ack 表示接收方期望收到的下一个字节序号。因为 TCP 面向字节流,应用层写入的多次数据可能被合并或拆分成不同报文段,接收方只关心连续字节是否到达。
详细版
假设发送方从序列号 1000 开始发送 500 字节,那么报文段可以写成:
Seq = 1000
Len = 500
覆盖字节范围 = [1000, 1499]
接收方确认 = Ack 1500
Ack=1500 的含义是:“1500 之前的字节我都连续收到了,下次请从 1500 开始发。” 如果中间丢了字节,接收方通常会重复确认缺口位置,触发发送方重传。SYN 和 FIN 虽然不携带应用数据,但会消耗 1 个序列号。
完整版教学
一、Seq 不是报文编号
很多人第一次看 TCP 会把序列号理解成“第 1 个包、第 2 个包”。这是错的。TCP 序列号标识的是字节流里的位置。
应用写入 1000 字节
TCP 可以拆成 400 + 600
也可以拆成 300 + 300 + 400
无论怎么拆,字节本身在流里的编号不变。
二、确认号 Ack 的语义
Ack 表示“我期望收到的下一个字节序号”。它是累计确认,而不是确认某一个包。
| 已连续收到的字节 | 返回 Ack | 含义 |
|---|---|---|
[1000,1499] | 1500 | 1500 之前都收到了 |
[1000,1199] | 1200 | 还缺 1200 开始的字节 |
[1000,1199] 和 [1300,1499] | 1200 | 中间有洞,累计确认停在 1200 |
关键点:累计 ACK 只确认“连续前缀”,乱序到达的后段不能让累计确认越过缺口。
三、为什么说 TCP 面向字节流
TCP 不保留应用层消息边界。应用调用两次 write,接收方可能一次 read 读到;应用一次 write,接收方也可能分多次 read 读到。
write("hello")
write("world")
接收端可能读到 "helloworld"
也可能读到 "hel"、"lowor"、"ld"
这就是粘包/拆包问题的根源:TCP 只交付有序字节流,不知道你的业务消息边界。
四、SYN 和 FIN 为什么消耗序列号
SYN 用来建立连接,FIN 用来关闭方向上的字节流。它们不携带应用数据,但属于 TCP 状态机中的重要控制事件,因此各消耗 1 个序列号。
SYN Seq=x
对端 ACK=x+1
FIN Seq=y
对端 ACK=y+1
这样控制事件也能被可靠确认和重传。
五、乱序到达时发生什么
如果接收方先收到 [1000,1199],又收到 [1400,1599],中间 [1200,1399] 缺失,它可以缓存乱序数据,但累计 ACK 仍返回 1200。
已收: 1000..1199
缺口: 1200..1399
已缓存: 1400..1599
Ack: 1200
发送方看到重复 ACK,可能触发快速重传。
六、和可靠传输的关系
序列号用于排序、去重、识别缺口;确认号用于告诉发送方哪些字节已经连续到达。再配合重传计时器、快速重传、滑动窗口,TCP 才能实现可靠、有序传输。
序列号:定位字节
确认号:累计确认
窗口:控制可发送范围
重传:补齐缺口
七、常见误区与追问
- 误区:Seq 是包编号。 Seq 是字节序号,报文段只是承载一段字节。
- 误区:Ack 确认的是当前收到的包。 Ack 表示期望的下一个连续字节。
- 误区:SYN/FIN 不带数据就不占序列号。 它们各消耗 1 个序列号。
- 追问:乱序包到了 Ack 会怎么变? 累计 Ack 不越过缺口,仍确认缺失位置。
- 追问:为什么 TCP 会有粘包? 因为 TCP 是字节流,不保留应用层消息边界。
- 追问:序列号如何帮助去重? 重传数据若序号范围已接收,接收方可识别并丢弃重复字节。
八、加强记忆
TCP 序列号和确认号的核心是“按字节编号、按连续前缀确认”。Seq 是本段第一个字节,Ack 是下一个想要的字节。只要把 TCP 看成一条有序字节流,而不是一串业务消息,粘包、乱序、累计确认都会变得自然。