📚 第二章 TCP/IP基础知识
📄 一句话概括

首部的本质是「把必要的信息写在明面上」,因为对端没有别的渠道知道。 TCP 的 20 个字节里,序号与确认号各占 4 字节——可靠性是有重量的。

📝 本节和前面几节的关系

2.4 讲完了四层各自管什么,2.5 用一次完整通信把它们串起来。 这一节先看首部,下一节看发送、接收。

一、为什么要有首部

因为「上下文」不能共享,只能每次带上:

首部要回答的 写在哪 不写会怎样
给哪个程序 端口号 不知道交给谁
第几个字节 序号 乱序了没法拼
收到没有 确认号 不知道要不要重传
还能装多少 窗口 发太快会撑爆对端

这四行解释了「为什么 TCP 头是 20 字节而不是 4 字节」:可靠性不是免费的,它要占字段。 UDP 只保留第一行(端口),所以只要 8 字节——它不打算回答后面三个问题。 第四行那个「窗口」是最容易被忽略的: 它是「流量控制」的全部机制——接收方通过窗口字段告诉发送方「我还能收多少」, 发送方按这个数调整发送速度。 没有它,快发送方会撑爆慢接收方。

二、TCP 头逐字段

九个字段,正好 20 字节:

字段 位(十六进制) 本 Demo 的值
源端口 4 40000
目的端口 4 80
序号 8 1000
确认号 8 5000
数据偏移 + 保留 2 0x50
标志位 2 0x18
窗口 4 65535
校验和 4 0x0000
紧急指针 4 0

第二列的单位是「十六进制位数」,两位是一个字节: 所以序号占 8 位十六进制,等于 4 字节、32 位。 九个字段加起来 40 位十六进制,等于 20 字节。 第三、四行那两个 4 字节是「可靠性」的成本: 序号让你能拼回顺序,确认号让对端知道该重传哪一段。 两者合起来占了头部 40%,这就是「可靠传输要付的字节价」。 第五行那个 0x50 也有讲究:高 4 位是「数据偏移」,单位是 4 字节,5 乘 4 等于 20, 表示头部长度 20 字节。 所以 TCP 头最长可以到 60 字节(15 乘 4),多出来的 40 字节是选项(时间戳、窗口缩放等)。

三、标志位是八个开关

一个字节里挤八个开关,每个管一件事:

位 名字 含义
0x01 FIN 我要关了
0x02 SYN 我要建连
0x04 RST 立刻重置
0x08 PSH 别缓存,立刻上交
0x10 ACK 确认号有效
0x20 URG 紧急指针有效

第四行那个 PSH 是「实时性」的开关:默认 TCP 会攒一点数据再发(Nagle 算法), 加了 PSH 就表示「这段别攒,立刻交上去」。 所以「聊天软件按键立刻显示」靠的就是它。 这八个开关能组合:0x18 就是 ACK 加 PSH, 表示「我确认你的数据,同时请你立刻上交我这批」。 0x12 是 SYN 加 ACK,就是「握手第二步」那个包。 读标志位必须按位与,不能当整数比:flags & 0x02 判断是不是 SYN, 写成 flags == 0x02 就会漏掉 0x12 这种情况——这是解析网络包最常犯的错之一。

四、动手 Demo:把 TCP 头 20 个字节拆开

造一个 TCP 头,逐字段读出来。 注意「十六进制位数」这一列:两位就是一个字节。

import struct, _netsim as N

seg = N.tcp4h(40000, 80, seq=1000, ack=5000, flags=0x18)
sp, dp, sq, ak = struct.unpack('!HHII', seg[:12])
print('源端口   %d' % sp)
print('目的端口 %d' % dp)
print('序号     %d' % sq)
print('确认号   %d' % ak)
print('数据偏移 0x%02X' % seg[12])
print('标志位   0x%02X' % seg[13])
print('窗口     %d' % struct.unpack('!H', seg[14:16])[0])
print('校验和   0x%04X' % struct.unpack('!H', seg[16:18])[0])
print('紧急指针 %d' % struct.unpack('!H', seg[18:20])[0])

实跑输出:

--------------------------------------------------------------------
把 TCP 头 20 个字节拆开
--------------------------------------------------------------------
**数据包首部到底长什么样,拆一遍最清楚。**下面造一个 TCP 头,逐字段读出来。

  字段                十六进制位数     本 Demo 的值
  --------------------------------------------------
  源端口              4                40000
  目的端口            4                80
  序号                8                1000
  确认号              8                5000
  数据偏移 + 保留     2                0x50
  标志位              2                0x18
  窗口                4                65535
  校验和              4                0x0000
  紧急指针            4                0

**第二列的单位是「十六进制位数」,两位是一个字节**:**所以序号占 8 位十六进制 = 4 字节 = 32 位。**
**九个字段加起来正好 40 位十六进制 = 20 字节。**
**校验和那一行是 0x0000,因为这里没算**:**TCP 校验和要把「伪首部」(含源与目的 IP)一起算进去**,
**本 Demo 只造了 TCP 头、没传 IP,所以留了 0。**
**这也说明「传输层校验」其实依赖网络层的信息**——**分层是逻辑上的,实现时层与层之间仍有耦合。**
**第六行那个 0x18 是两个标志位叠在一起**:**0x10 是 ACK、0x08 是 PSH**,
**所以这次既确认了对端、又要求立刻上交数据。**
**标志位是「一个字节里挤八个开关」**,**读的时候要按位与,不能当整数比。**
**序号与确认号都是 32 位,所以最大约 43 亿**:
**超过就回绕到 0**——**这就是为什么高速长连接的序号回绕是个真问题(PAWS 就是治它的)。**

九个字段读出来的值,和构造时传进去的完全一致:40000、80、1000、5000、0x18、65535。 这说明「手工造包」这条路是通的——协议不是什么神秘的东西,就是按位置填字节。 数据偏移是 0x50,高 4 位是 5:5 乘 4 等于 20,所以头部 20 字节。 读这个字段要先右移 4 位,再去掉高位的版本信息。 校验和是 0x0000,因为这里没算:TCP 校验和要把「伪首部」(含源与目的 IP)一起算进去, 本 Demo 只造了 TCP 头、没传 IP,所以留了 0。 这也说明「传输层校验」其实依赖网络层的信息——分层是逻辑上的,实现时层与层之间仍有耦合。

30 秒实验

  1. 把 flags 改成 0x12,看输出变成什么(那是握手第二步的标志)。
  2. 把 seq 改成 2 的 32 次方减 1,看会不会报错——想想序号回绕。
  3. 算一下:如果头部全填满选项(60 字节),数据偏移字段应该是多少?

常见误区

  1. 以为首部是「额外开销」。 它是「必要的信息」,没有它对方根本不知道该怎么处理这些字节。
  2. 以为标志位是「一个整数」。 它是八个独立开关,必须按位与判断,不能整体比较。
  3. 以为 TCP 头固定 20 字节。 最短 20,最长 60,多出来的是选项(时间戳、窗口缩放)。
自测三问(点开对答案) 1. TCP 头里哪两个字段占了 40% 的空间?为什么值得? —— **序号与确认号,各 4 字节。**序号能拼回顺序、确认号能指示重传,这就是可靠性的字节代价。 2. 0x18 这个标志位表示什么? —— **ACK(0x10)加 PSH(0x08)**:既确认对端的数据,又要求立刻上交我这批数据。 3. 为什么读标志位不能写成 `flags == 0x02`? —— **因为标志位可以组合**:0x12 也含 SYN,写等于就会漏掉。必须用按位与 `flags & 0x02`。

下一节看这些首部是怎么被一层层加上去的。

相关笔记