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

网络层的工作全写在 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 秒实验

  1. 把 proto 改成 17(UDP),看校验和变成多少。
  2. 把目的地址改成 1.1.1.1,看校验和差多少。
  3. 想一下:为什么 IPv6 取消了头部校验和?

常见误区

  1. 以为 IP 保证可靠。 它只做「尽力送达」,不重传、不排序、不确认。可靠性推给传输层。
  2. 以为 IP 头校验和校验的是整个包。 只校验首部,数据由传输层自己校验。
  3. 以为「位域」是现代设计。 它是 1981 年省字节的产物,今天读起来很费劲, IPv6 已经改掉了大部分。
自测三问(点开对答案) 1. IP 头的「版本 + 首部长度」那个字节怎么读? —— **高 4 位是版本,低 4 位是「首部长度 ÷ 4」**。0x45 表示版本 4、首部 20 字节。 2. IP 头校验和能发现什么错误?不能发现什么? —— **能发现单个比特翻转;对某些多位错误无能为力。**而且它只校验首部,不校验数据。 3. 为什么说 IP 是「唯一跨网络」的一层? —— **网络接口层只管一段线,传输层管两端主机**,只有 IP 能跨任意跳数把包送过去。

下一节往上一层:传输层。

相关笔记