TCP 首部固定 20 字节、8 个字段,比 UDP 多 12 字节:多的全是可靠性要用的。 第 13 个字节里塞了两样东西:高 4 位是首部长度,低 8 位是标志位。
6.6 拆了 UDP 的 8 字节,这一节拆 TCP 的 20 字节。 6.4.2 的序列号、6.4.6 的窗口、5.8.4 的 ECN,在这里都能找到对应的位。
一、八个字段
| 字段 | 字节 | 作用 |
|---|---|---|
| 源端口 | 0~1 | 谁发的 |
| 目的端口 | 2~3 | 发给谁 |
| 序列号 | 4~7 | 字节偏移 |
| 确认号 | 8~11 | 收到了多少 |
| 数据偏移加标志 | 12~13 | 头多长加控制位 |
| 窗口 | 14~15 | 我还能收多少 |
| 校验和 | 16~17 | 整段校验 |
| 紧急指针 | 18~19 | 带外数据的位移 |
前两行和 UDP 一样,后面六个都是 UDP 没有的: 序列号和确认号管「顺序和可靠」,窗口管「别发太快」,校验和管「数据对不对」, 紧急指针管「带外数据」。 最后一行几乎没人用:「带外数据」的意思是「这段数据要插队」, 典型用法是「按 Ctrl+C 中断一个正在传输的命令」。 它的实现有争议,很多系统只用 1 个字节,所以实际很鸡肋。
二、第 13 个字节
这个字节里塞了两样东西:高 4 位是「数据偏移」(首部有几个 32 位字),低 8 位是标志位。 注意「4 加 8 等于 12」,超过了 8 位的字节—— 所以严格说它是「第 12 字节的低 4 位加第 13 字节」,总共 12 位。 数据偏移最小是 5,5 乘 4 等于 20 字节,正好是没有选项时的最小首部。 最大是 15,也就是 60 字节,所以「选项区最多 40 字节」。 这个「用 4 位表示「有几个 32 位字」」的做法,比 UDP 的「没有这个字段」灵活得多。 它让「首部可以变长」:SACK、时间戳、窗口缩放这些扩展,都能塞进选项区。
三、八个标志位
标志位是「可以同时置多个」的,所以它不叫「类型」而叫「标志」。
| 标志 | 位值 | 含义 |
|---|---|---|
| URG | 0x20 | 紧急指针有效 |
| ACK | 0x10 | 确认号有效 |
| PSH | 0x08 | 快把数据交给应用 |
| RST | 0x04 | 直接重置连接 |
| SYN | 0x02 | 请求建立连接 |
| FIN | 0x01 | 请求关闭连接 |
SYN 和 ACK 一起置就是 0x12:这就是三次握手的第二个包。 所以「抓包看到 flags=[SYN, ACK]」不用奇怪——它就是一个字节里的两个位。 还有两个位没列在表里:ECE 和 CWR,它们是 0x40 和 0x80,就是 5.8.4 讲的 ECN。 所以「一个字节 8 位,8 个标志」刚好用满——这也是「想加新标志就得从别处挤」的原因。
四、动手 Demo:TCP 首部 20 个字节,8 个标志位
构造一个 SYN 包的首部,逐字段看值,再把标志位逐位展开。 注意 0x5002 这个数怎么拆。
import struct
off_flags = (5 << 12) | 0x002 # 首部 5 个 32 位字,SYN
hdr = struct.pack('!HHIIHHHH', 50000, 443, 1000, 0,
off_flags, 64240, 0, 0)
print('首部长度', len(hdr), '第 12~13 字节 0x%04x' % off_flags)
print('数据偏移', off_flags >> 12, '-> 首部', (off_flags >> 12) * 4, '字节')
print('标志位 0x%02x' % (off_flags & 0xFF))
实跑输出:
--------------------------------------------------------------------
TCP 首部 20 个字节,8 个标志位
--------------------------------------------------------------------
**TCP 首部固定 20 字节,比 UDP 多 12 字节**,**多出来的全是「可靠性」要用的字段。**
字段 字节位置 本包的值
------------------------------------------------
**源端口** **0~1** **50000**
**目的端口** **2~3** **443**
**序列号** **4~7** **1000**
**确认号** **8~11** **0**
**数据偏移加标志** **12~13** **0x5002**
**窗口** **14~15** **64240**
**校验和** **16~17** **0**
**紧急指针** **18~19** **0**
**第 13 个字节里塞了两样东西**:**高 4 位是「数据偏移」(首部有几个 32 位字)**,
**低 8 位是标志位。**
**这里数据偏移是 5,5 乘 4 等于 20 字节**,**正好是没有选项时的最小首部。**
标志 对应位 本包 含义
----------------------------------------------
FIN 0x01 0 没开
SYN 0x02 1 **这一位是开的**
RST 0x04 0 没开
PSH 0x08 0 没开
ACK 0x10 0 没开
URG 0x20 0 没开
ECE 0x40 0 没开
CWR 0x80 0 没开
**只有 SYN 是 1,所以这是一个「连接请求」**:**三次握手的第一步就是它。**
**标志位是「可以同时置多个」的**,
**所以握手第二步的 SYN+ACK 就是「0x12」**——**两位一起开,表示「我要建连接,同时确认你的」。**
**ECE 和 CWR 就是 5.8.4 讲的 ECN**:
**ECE 表示「我收到过拥塞标记」,CWR 表示「我已经减过窗口了」。**
**这两个位在 20 年里一直是「可选」,所以抓包看到它们常年是 0 也很正常。**
0x5002 拆开就是「5」和「0x02」:高 4 位 5 表示「20 字节首部」,低 8 位 0x02 表示「SYN」。 所以一个数里塞了两件事,读的时候要按位拆。 「0x12 是 SYN 加 ACK」也可以验证:0x10 或 0x02 等于 0x12, 按位或就是把「两个开关都打开」。 所以协议里的「组合状态」通常不是「新编号」,而是「几个位一起置」。 最后一个观察:这 20 字节里,有 8 个字节是「序号和确认号」,占了 40%。 所以「TCP 头的开销主要在可靠性上」这句话,在字节分布上也能看出来。
30 秒实验
- 算一下:数据偏移是 8 时,首部多少字节?选项区多大?
- 想一下:为什么 TCP 要「可变的头长」,而 UDP 不需要?
- 想一下:0x18 是哪两个标志一起置?
常见误区
- 以为「标志位只能有一个」。 可以同时置多个,SYN 加 ACK 就是 0x12。
- 以为「TCP 头固定 20 字节」。 最小 20,最多 60,因为选项区可以加长。
- 以为「紧急指针很常用」。 几乎没人用,实现还有争议。
自测三问(点开对答案)
1. TCP 首部最小多少字节?最大多少? —— **最小 20,最大 60**,差的是最多 40 字节的选项区。 2. 数据偏移是 5 意味着什么? —— **首部有 5 个 32 位字,也就是 20 字节。** 3. SYN 加 ACK 的十六进制是多少? —— **0x12**,0x02 或 0x10。第六章到这里结束。下一章进入路由协议。
- ☐ (本章完,下一节 7.1.1 IP地址与路由控制)