📚 第六章 TCP与UDP
📄 一句话概括

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 秒实验

  1. 算一下:数据偏移是 8 时,首部多少字节?选项区多大?
  2. 想一下:为什么 TCP 要「可变的头长」,而 UDP 不需要?
  3. 想一下:0x18 是哪两个标志一起置?

常见误区

  1. 以为「标志位只能有一个」。 可以同时置多个,SYN 加 ACK 就是 0x12。
  2. 以为「TCP 头固定 20 字节」。 最小 20,最多 60,因为选项区可以加长。
  3. 以为「紧急指针很常用」。 几乎没人用,实现还有争议。
自测三问(点开对答案) 1. TCP 首部最小多少字节?最大多少? —— **最小 20,最大 60**,差的是最多 40 字节的选项区。 2. 数据偏移是 5 意味着什么? —— **首部有 5 个 32 位字,也就是 20 字节。** 3. SYN 加 ACK 的十六进制是多少? —— **0x12**,0x02 或 0x10。

第六章到这里结束。下一章进入路由协议。

相关笔记