网络层的工作全写在 IP 头的 20 个字节里:寻址、路由、分片。 「版本 + 首部长度」挤在一个字节里,这种位域是省字节的老做法。
2.4.3 讲的是「一段线内」,这一节讲「跨网段怎么走」。 这一节会把 IP 头逐字段拆开,并真算一次校验和。
一、20 个字节决定一切
IP 头最短 20 字节,11 个字段:
| 偏移 | 字段 | 值(本 Demo) |
|---|---|---|
| 0 | 版本 + 首部长度 | 0x45 |
| 1 | 区分服务 | 0x00 |
| 2 | 总长度 | 0x0019 |
| 4 | 标识 | 0x0000 |
| 6 | 标志 + 片偏移 | 0x0000 |
| 8 | 生存时间(TTL) | 0x40 |
| 9 | 协议 | 0x06 |
| 10 | 首部校验和 | 0x80C4 |
| 12 | 源地址 | 192.168.3.152 |
| 16 | 目的地址 | 93.184.216.34 |
第一行那个 0x45 要拆开看:高 4 位是版本(4),低 4 位是首部长度按 4 字节算的个数(5)。
5 乘 4 等于 20 字节,正好是最小长度。
这种「一个字节塞两个字段」的做法叫位域,在 1981 年省字节很重要,但今天读起来很费劲。
第六行那个 TTL 是防环的:每过一个路由器减一,减到 0 就丢弃并回 ICMP 超时。
这也是 traceroute 的原理——故意发 TTL 很小的包,看每一跳是谁回的错。
第七行那个「协议」是 6,表示里面装的是 TCP:17 是 UDP、1 是 ICMP。
这 1 个字节就是「IP 能承载任意上层协议」的关键,加新协议不用改 IP 头。
二、校验和怎么算
三步,全是整数运算:
1. 把首部按 16 位分组,当成一串整数
2. 全部相加,进位移到低位再加(回卷)
3. 结果取反,填进校验和字段
第二行那个「回卷」是这个算法的特点:16 位加法会溢出,标准要求把溢出的进位再加回最低位。 所以它不是普通的求和,是「反码求和」。 接收端做同样的运算,如果结果是全 1,就认为没错。 这个校验和的强度不高:它只能发现「单个比特翻转」这类错误,对某些多位错误无能为力。 而且它只校验首部,不校验数据——数据由传输层(TCP/UDP)自己校验。 所以「IPv4 头有校验和、数据没有」这件事,是分层的结果。 IPv6 干脆把头部的校验和取消了:因为链路层与传输层都有校验,再算一遍是重复劳动。 这也说明 IPv4 那个设计是「当年链路不可靠」的产物。
三、为什么它叫「互联网层」
因为它是唯一「跨网络」的一层:
| 层 | 管到哪 | 地址 |
|---|---|---|
| 网络接口层 | 一段线 | MAC |
| 互联网层 | 任意跳数 | IP |
| 传输层 | 两端主机 | 端口 |
中间那一行是「internet」这个词的技术来源:它把无数个互不相干的网络连成一个「网际网」, 每一段可以自己定技术,只要都跑 IP。 这是 IP 最重要的设计选择。 代价是它必须「假设底下什么都不可靠」:所以 IP 自己不做重传、不做排序、不做确认, 只做「尽力送达」(best effort)。 可靠性全部推给传输层。 这个「把复杂性推到边缘」的选择,是互联网能扩展的根本原因。
四、动手 Demo:IP 头逐字段,校验和真算
真造一个 IP 头,把每个字段拆出来看,再改一个字节对比校验和。 「改一个字节校验和全变」是这次要验证的核心。
import struct, _netsim as N
pkt = N.ip4h('192.168.3.152', '93.184.216.34', b'hello',
ttl=64, proto=6)
hdr = pkt[:20]
print('版本+首部长度 0x%02X' % hdr[0])
print('总长度 0x%04X' % struct.unpack('!H', hdr[2:4])[0])
print('TTL 0x%02X' % hdr[8])
print('协议 0x%02X' % hdr[9])
print('校验和 0x%04X' % struct.unpack('!H', hdr[10:12])[0])
pkt2 = N.ip4h('192.168.3.152', '93.184.216.34', b'hello',
ttl=63, proto=6)
print('TTL 改成 63 后 0x%04X'
% struct.unpack('!H', pkt2[10:12])[0])
实跑输出:
--------------------------------------------------------------------
IP 头逐字段,校验和真算
--------------------------------------------------------------------
**网络层的工作全写在这 20 个字节里。**下面真造一个 IP 头,再把每个字段拆出来看。
偏移 字段 值 原始字节
-----------------------------------------------------------
0 版本 + 首部长度 0x45 45
1 区分服务 0x00 00
2 总长度 0x0019 00 19
4 标识 0x0000 00 00
6 标志 + 片偏移 0x0000 00 00
8 生存时间 0x40 40
9 协议 0x06 06
10 首部校验和 0x80C4 80 c4
12 源地址 192.168.3.152 c0 a8 03 98
16 目的地址 93.184.216.34 5d b8 d8 22
**第一行那个字段是「版本 + 首部长度」挤在一个字节里**:
**高 4 位是版本(4),低 4 位是首部长度按 4 字节算的个数(5,即 20 字节)。**
**这叫「位域」,是省字节的老做法**——**代价是取值要移位再掩码,代码里容易写错。**
**校验和是这么算的**:**把首部按 16 位分组求和、进位回卷、再取反。**
**改任何一个字节,校验和就会变**——**下面改一下 TTL 再算一遍验证。**
**TTL 64 时校验和 0x80C4,改成 63 后变成 0x81C4**——**只差一个字节,校验和完全不同。**
0x45 那个字节:高 4 位 4 是版本,低 4 位 5 是「首部长度 ÷ 4」:
5 乘 4 等于 20,所以首部长 20 字节。
这种「除以 4」是为了用 4 位表示 60 字节(15 乘 4),因为 4 位最大只能表示 15。
总长度 0x0019 等于 25:20 字节头加 5 字节数据(hello),正好 25。
TTL 从 0x40(64)改成 0x3F(63)后,校验和从 0x80C4 变成 0x81C4:只差 0x0100。
这不是巧合——反码求和里,某一项减 1,总和就减 1,取反后结果的对应位就翻。
所以校验和的变化是可以预测的,这也是它能被硬件快速算出的原因。
30 秒实验
- 把
proto改成 17(UDP),看校验和变成多少。 - 把目的地址改成
1.1.1.1,看校验和差多少。 - 想一下:为什么 IPv6 取消了头部校验和?
常见误区
- 以为 IP 保证可靠。 它只做「尽力送达」,不重传、不排序、不确认。可靠性推给传输层。
- 以为 IP 头校验和校验的是整个包。 只校验首部,数据由传输层自己校验。
- 以为「位域」是现代设计。 它是 1981 年省字节的产物,今天读起来很费劲, IPv6 已经改掉了大部分。
自测三问(点开对答案)
1. IP 头的「版本 + 首部长度」那个字节怎么读? —— **高 4 位是版本,低 4 位是「首部长度 ÷ 4」**。0x45 表示版本 4、首部 20 字节。 2. IP 头校验和能发现什么错误?不能发现什么? —— **能发现单个比特翻转;对某些多位错误无能为力。**而且它只校验首部,不校验数据。 3. 为什么说 IP 是「唯一跨网络」的一层? —— **网络接口层只管一段线,传输层管两端主机**,只有 IP 能跨任意跳数把包送过去。下一节往上一层:传输层。
- ☐ (本节完,下一节 2.4.5 传输层)