Appearance
IP 数据报与 IPv4 地址
概念
IP 协议(Internet Protocol)是网络层的核心:它把上层(传输层)交下来的数据封装成"IP 数据报",然后尽最大努力把它从源主机送到目的主机。
一句话说清它是什么:IP = 互联网的"邮政系统"——它规定信封怎么写(IP 首部)、地址怎么写(IPv4 地址),并承诺"尽量送到";但它不承诺"一定送到、按序送到、不重复送到"。
★ "尽最大努力交付"(best-effort delivery)的四条不保证(必背):
text
① 不保证送达 —— 分组可能被丢弃 (队列满 / 校验错 / TTL 到 0)
② 不保证顺序 —— 各分组可能走不同路径, 到达次序可能与发送次序不同
③ 不保证不重复 —— 理论上存在重复分组
④ 不保证时延 —— 不给任何时延上界 (不提供服务质量保证)⚠️ "不可靠"不是缺陷,而是设计选择:IP 把可靠性推给两端的主机(TCP 负责重传与排序),自己保持极简——这样中间的路由器就能做得很便宜、很快,整个互联网才能扩展到今天的规模。 这就是著名的"端到端原则"。
本章要解决的三件事:
| 问题 | 对应内容 |
|---|---|
| 数据报长什么样 | IPv4 首部 12 个字段(固定 20 B) |
| 太大了怎么办 | 分片:MTU、标识、标志、片偏移 |
| 地址怎么编 | 32 位 IPv4 地址、分类地址、私有地址、特殊地址 |
与上一章(net/24-ethernet.md)的衔接:上一章说"IP 数据报是链路层帧的载荷,MTU 1500 是它的上限";本章就回答"IP 数据报自己长什么样、超过 1500 怎么办"——答案是"分片",而"MTU"这个数正是两章的交汇点。
原理
一、IPv4 首部:12 个字段,固定部分 20 B
首部结构(必背):
text
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
┌───────┬───────┬───────────────┬───────────────────────────────┐
│ 版本 │首部长度│ 区分服务 │ 总长度 │ <- 4 B
│ 4 位 │ 4 位 │ 8 位 │ 16 位 │
├───────┴───────┼───────────────┼───────────────────────────────┤
│ 标识 │ 标志 │ 片偏移 │ <- 4 B
│ 16 位 │ 3 位 │ 13 位 │
├───────────────┼───────┴───────┼───────────────────────────────┤
│ TTL │ 协议 │ 首部检验和 │ <- 4 B
│ 8 位 │ 8 位 │ 16 位 │
├───────────────┴───────────────┴───────────────────────────────┤
│ 源 IP 地址 │ <- 4 B
│ 32 位 │
├───────────────────────────────────────────────────────────────┤
│ 目的 IP 地址 │ <- 4 B
│ 32 位 │
├───────────────────────────────────────────────────────────────┤
│ 可选字段(0 ~ 40 B,很少用) │
└───────────────────────────────────────────────────────────────┘
★ 固定部分 20 B = 5 行 x 4 B;带选项时最长 60 B| 字段 | 位数 | 含义 | 单位 / 易错点 |
|---|---|---|---|
| 版本(version) | 4 | 4 = IPv4,6 = IPv6 | — |
| 首部长度(IHL) | 4 | 首部有多少个 4 B | ★ 单位是 4 B;最小值 5(= 20 B),最大值 15(= 60 B) |
| 区分服务(TOS) | 8 | 优先级 / 服务类型 | 旧教材叫"服务类型" |
| 总长度(total length) | 16 | 首部 + 数据的总字节数 | ★ 单位是 1 B;最大 65535 B |
| 标识(identification) | 16 | 同一数据报的各分片用同一个标识 | 用来"认亲" |
| 标志(flags) | 3 | 保留位 / DF(不分片)/ MF(后面还有分片) | ★ DF = 1 表示禁止分片;MF = 1 表示"后面还有" |
| 片偏移(fragment offset) | 13 | 本片数据在原数据部分的起始位置 | ★ 单位是 8 B;最大值 |
| 生存时间(TTL) | 8 | 每经一个路由器减 1,减到 0 就丢弃 | 防止分组在环路上无限打转 |
| 协议(protocol) | 8 | 上层协议类型 | 1 = ICMP、6 = TCP、17 = UDP |
| 首部检验和(checksum) | 16 | 只校验首部,不校验数据 | ★ 每经一个路由器都要重算(因为 TTL 变了) |
| 源 IP 地址 | 32 | 发送方 | — |
| 目的 IP 地址 | 32 | 接收方 | — |
★ 三条由字段宽度直接推出的结论:
| 结论 | 推导 |
|---|---|
| 首部固定 20 B,最长 60 B | 首部长度字段 4 位 → 最大值 15 → |
| 数据报最大 65535 B | 总长度字段 16 位 → |
| 数据部分最大 65515 B |
★ "首部长度"与"总长度"的关系(题目最常问):
⚠️ 两个"长度"字段的单位不一样,这是本章第一大陷阱:首部长度单位 4 B(因为首部最多 60 B、4 位存不下字节数,只能存"4 B 的个数");总长度单位 1 B(因为它要能表示到 65535)。 混用就会算错一倍以上。
IP 协议号(常见几个,记住就够):
| 协议号 | 协议 |
|---|---|
| 1 | ICMP(网际控制报文协议) |
| 2 | IGMP |
| 6 | TCP |
| 17 | UDP |
| 41 | IPv6 封装 |
二、分片:超过 MTU 怎么办
MTU(Maximum Transmission Unit,最大传送单元)是"链路层帧能承载的 IP 数据报的最大长度"——以太网的 MTU = 1500 B(见 net/24-ethernet.md)。
当 IP 数据报(含首部)大于下一跳链路的 MTU 时,就必须分片:把它切成若干片,每片各自加上 20 B 首部,独立传输。
★ 分片规则(必背):
text
① 每片的最大数据长度 = MTU - 20(首部)
★ 但必须是 8 的倍数 (因为片偏移以 8 B 为单位)
-> 实际取"不超过 MTU-20 的最大 8 的倍数"
② 各片的 标识 相同 (都等于原数据报的标识) -> 目的主机靠它认亲
③ MF 位: 除最后一片外, 全部置 1; 最后一片置 0
④ 片偏移 = 该片数据在原数据部分中的起始偏移 / 8
⑤ 最后一片的数据长度不必是 8 的倍数 (它没有"下一片"要对齐)★ 锚点(本章定死):原数据报总长 4000 B(首部 20 B + 数据 3980 B),下一跳 MTU = 1500 B
text
第①步: 每片最大数据 = (1500 - 20) 向下取 8 的倍数 = 1480 B (1480 / 8 = 185)
第②步: 切数据 3980 B
片 1: 数据 1480 B (原数据 [0, 1479])
片 2: 数据 1480 B (原数据 [1480, 2959])
片 3: 数据 1020 B (原数据 [2960, 3979]) <- 剩下多少算多少
第③步: 填字段
┌──────┬────────┬────────┬────────┬──────────┬─────────┐
│ 片号 │ 数据长 │ 总长度 │ 片偏移 │ MF │ 说明 │
├──────┼────────┼────────┼────────┼──────────┼─────────┤
│ 1 │ 1480 B │ 1500 B │ 0 │ 1 │ 后面还有 │
│ 2 │ 1480 B │ 1500 B │ 185 │ 1 │ 后面还有 │
│ 3 │ 1020 B │ 1040 B │ 370 │ 0 │ 最后一片 │
└──────┴────────┴────────┴────────┴──────────┴─────────┘
片偏移: 0/8 = 0; 1480/8 = 185; 2960/8 = 370★ 分片的开销(要会算):
| 项 | 值 |
|---|---|
| 原数据报 | 4000 B |
| 分片后总传输量 | 1500 + 1500 + 1040 = 4040 B |
| 额外开销 | 4040 − 4000 = 40 B(2 个额外首部,每个 20 B) |
| 开销增幅 |
⚠️ 关于"最后一片 1020 B 不是 8 的倍数":
,这完全合法——因为"片偏移必须是 8 的倍数"约束的是"每一片的起始位置",而最后一片没有后继片需要对齐。 RFC 791 与教材口径一致:除最后一片外,各片数据长度必须是 8 的倍数。 这一条是本章最容易答错的细节。
⚠️ "谁来分片、谁来重组":分片可以由源主机做,也可以由中途的路由器做(当出口链路 MTU 更小时);但重组只在目的主机进行——路由器不负责重组(否则要缓存大量分片、代价太大)。 这也是"为什么分片有害":中间路由器一分片,后续所有链路都得转发这些小片,效率下降。
"不要分片"的两个手段:① 主机在发送前就按路径 MTU 控制数据报长度(路径 MTU 发现);② 设置 DF 位 = 1,让路由器在超 MTU 时丢弃并回 ICMP 差错报文。
三、IPv4 地址:32 位,分类编址
IPv4 地址 32 位,写成"点分十进制":4 个十进制数,每数 0 ~ 255,用点分开,例如 192.168.1.100。
★ 分类地址(必背表):
| 类别 | 首位模式 | 网络号位数 | 主机号位数 | 网络数 | 每网主机数 | 地址范围 |
|---|---|---|---|---|---|---|
| A | 0 | 7 | 24 | 126( | 16777214( | 1.0.0.0 ~ 126.255.255.255 |
| B | 10 | 14 | 16 | 16384( | 65534( | 128.0.0.0 ~ 191.255.255.255 |
| C | 110 | 21 | 8 | 2097152( | 254( | 192.0.0.0 ~ 223.255.255.255 |
| D | 1110 | — | — | — | — | 224.0.0.0 ~ 239.255.255.255(组播) |
| E | 1111 | — | — | — | — | 240.0.0.0 ~ 255.255.255.255(保留) |
★ 两个"减 2"与一个"减 2"的来历:
text
① A 类网络数 = 2^7 - 2 = 126
因为网络号全 0 (0.0.0.0) 表示"本网络", 网络号 127 (127.x.x.x) 保留作环回
-> 去掉这两个, 剩 126 个
② 每网主机数 = 2^主机号位数 - 2
因为主机号全 0 表示"这个网络本身"(网络地址), 主机号全 1 表示"本网广播地址"
-> 这两个都不能分配给具体主机
A 类: 2^24 - 2 = 16777214; B 类: 2^16 - 2 = 65534; C 类: 2^8 - 2 = 254⚠️ "B 类、C 类为什么不用减 2":B 类首位是
10、C 类首位是110,它们的前缀本身就不含"全 0"与"全 1"这两种情况——所以网络数就是、 ,不减去任何东西。 而 A 类首位固定为 0,7 位网络号可以是"全 0"(0.0.0.0)和"全 1"(127),这两个特殊,所以要减 2。
★ 地址空间与浪费(这个对比最能说明"为什么要划分子网/CIDR"):
| 项 | 值 |
|---|---|
| IPv4 地址总数 | |
| A 类一个网络的地址数 | |
| C 类一个网络的地址数 | |
| "一个中等单位分到一个 A 类地址"的浪费 | 16777216 个地址里可能只用几百个 |
⚠️ 分类编址的致命缺陷就是"粒度太粗":要么给一个能装 1677 万台主机的 A 类(用不完),要么给一个只能装 254 台的 C 类(不够用),中间没有合适的档位。 这就是子网划分(
net/31-subnet.md)与 CIDR 出现的直接动因。
★ 私有地址(三块,必背):
| 私有地址块 | 等价的分类地址 | 地址数 |
|---|---|---|
10.0.0.0/8 | 1 个 A 类网络 | |
172.16.0.0/12 | 16 个 B 类网络(172.16 ~ 172.31) | |
192.168.0.0/16 | 256 个 C 类网络 |
⚠️
172.16.0.0/12的范围必须记准:它是172.16.0.0~172.31.255.255——172.15.x.x与172.32.x.x都是公网地址(/12意味着前 12 位固定,而172占 8 位、16的高 4 位再占 4 位)。 这是常考的"边界判别"题。
★ 特殊地址(清单):
| 地址 | 含义 |
|---|---|
0.0.0.0 | 本网络本主机(仅作源地址,用于"我还不知道自己地址"时,如 DHCP 请求) |
| 网络号有效、主机号全 0 | 网络地址(代表"这个网络本身",不能给主机) |
| 网络号有效、主机号全 1 | 直接广播地址(向本网络内所有主机广播) |
255.255.255.255 | 受限广播地址(只在本网内广播,路由器不转发) |
127.0.0.0/8(常用 127.0.0.1) | 环回地址(发给自己,数据不出网卡) |
★ 点分十进制 ↔ 二进制换算(锚点):
text
192 . 168 . 1 . 100
11000000 10101000 00000001 01100100
★ 记住几个关键值可加快心算:
10000000 = 128 11000000 = 192 11100000 = 224
11110000 = 240 11111000 = 248 11111100 = 252
11111110 = 254 11111111 = 255
-> 一个十进制数 = 左侧若干位 1 的"前缀和":
192 = 128 + 64, 224 = 128 + 64 + 32, 240 = 128 + 64 + 32 + 16 ...四、TTL:一个字段,两种用途
TTL 的规则很简单:每经过一个路由器减 1,减到 0 就丢弃该分组,并向源主机回一个 ICMP 超时报文。
它的两个用途(都常考):
| 用途 | 说明 |
|---|---|
| ① 防止环路 | 若路由表出现环路,没有 TTL 的分组会永远打转、把网络拖死;TTL 保证"最多走 255 跳" |
② traceroute / tracert 的原理 | 依次发送 TTL = 1、2、3…… 的探测分组——TTL 到 0 时那个路由器会回 ICMP 超时报文,报文里的源 IP 就是"第 N 跳的路由器地址" |
⚠️ 默认 TTL 值不是 255:实际系统常用 64(Linux)、128(Windows)——所以"ping 一个网站看到 TTL = 52"可以粗略推出"中间经过了约 12 跳"(64 − 52 ≈ 12)。 考试里若没给初值,就按题目给的算。
五、IP 的两个"历史包袱"与本章的位置
| 问题 | 后果 | 解决方向 |
|---|---|---|
| 地址枯竭(分类编址粒度太粗 + 32 位不够用) | A 类用不完、C 类不够用、总地址 42.95 亿即将耗尽 | 子网划分(net/31-subnet.md)→ CIDR → NAT(net/32-nat.md)→ IPv6(net/34-ipv6.md) |
| 分片开销 | 额外首部 + 重组负担 + 中间链路效率下降 | 路径 MTU 发现、DF 位、IPv6 取消路由器分片 |
⚠️ 本章在整个网络层的位置:它是网络层的"地基"——下一章的子网划分、再下一章的 NAT、再再下一章的路由协议,全部建立在"IP 数据报 + IPv4 地址"这两个概念之上。 学不好本章的字段与地址换算,后面全都会卡住。
示例
例 1:C 实现——首部字段解析与分片计算
参数:一段 20 B 的 IPv4 首部(
45 00 0F A0 1A 2B 40 00 40 06 00 00 C0 A8 01 64 08 08 08 08);分片参数为总长 4000 B、MTU 1500 B。
#include <stdio.h>
/* 解析 IPv4 首部固定部分(前 20 B) */
void parse(const unsigned char *h) {
int ver = (h[0] >> 4) & 0x0F; /* 版本 */
int ihl = h[0] & 0x0F; /* 首部长度, 单位 4 B */
int total = (h[2] << 8) | h[3]; /* 总长度, 单位 1 B */
int ident = (h[4] << 8) | h[5]; /* 标识 */
int ff = (h[6] << 8) | h[7]; /* 标志 3 位 + 片偏移 */
int df = (ff >> 14) & 1;
int mf = (ff >> 13) & 1;
int off = ff & 0x1FFF;
printf(" 版本 = %d (4 = IPv4); 首部长度 = %d x 4 = %d B\n", ver, ihl, ihl * 4);
printf(" 区分服务 = 0x%02X; 总长度 = %d B (单位 1 B)\n", h[1], total);
printf(" 数据长度 = 总长度 - 首部 = %d - %d = %d B\n", total, ihl * 4, total - ihl * 4);
printf(" 标识 = 0x%04X = %d\n", ident, ident);
printf(" 标志: 保留 = %d, DF = %d, MF = %d; 片偏移 = %d (x 8 = %d B)\n",
(ff >> 15) & 1, df, mf, off, off * 8);
printf(" TTL = %d; 协议 = %d; 首部检验和 = 0x%04X\n", h[8], h[9], (h[10] << 8) | h[11]);
printf(" 源 IP = %d.%d.%d.%d\n", h[12], h[13], h[14], h[15]);
printf(" 目的 IP = %d.%d.%d.%d\n", h[16], h[17], h[18], h[19]);
}
/* 分片: 总长度 total_len, 下一跳 MTU, 首部固定 20 B */
void frag(int total_len, int mtu) {
int hdr = 20;
int data = total_len - hdr;
int maxd = (mtu - hdr) / 8 * 8; /* 每片数据上限, 向下取 8 的倍数 */
int off = 0, n = 1, left = data, d;
printf(" 原数据报: 总长度 %d B (首部 %d + 数据 %d)\n", total_len, hdr, data);
printf(" 每片最多: MTU %d - 首部 %d = %d, 向下取 8 的倍数 -> %d B\n",
mtu, hdr, mtu - hdr, maxd);
while (left > 0) {
d = (left > maxd) ? maxd : left; /* 最后一片不足 8 的倍数也合法 */
printf(" 第 %d 片: 数据 %4d B, 总长度 %4d B, 片偏移 %4d, MF = %d\n",
n, d, d + hdr, off / 8, (left - d > 0) ? 1 : 0);
off += d;
left -= d;
n++;
}
printf(" 总传输量 = %d + %d + %d = %d B, 额外开销 = %d B (%d 个额外首部)\n",
1500, 1500, 1040, 1500 + 1500 + 1040, 4040 - total_len, 2);
}
int main(void) {
unsigned char hdr[20] = {0x45, 0x00, 0x0F, 0xA0, 0x1A, 0x2B, 0x40, 0x00,
0x40, 0x06, 0x00, 0x00, 0xC0, 0xA8, 0x01, 0x64,
0x08, 0x08, 0x08, 0x08};
printf("① 首部字段解析 (20 B 固定部分)\n");
parse(hdr);
printf("\n② 分片: 总长 4000 B, MTU 1500 B\n");
frag(4000, 1500);
printf("\n③ 单个字段的位数上限\n");
printf(" 首部长度 4 位 -> 最大 %d x 4 = %d B\n", 15, 60);
printf(" 总长度 16 位 -> 最大 %d B (数据部分最多 %d B)\n", 65535, 65535 - 20);
printf(" 片偏移 13 位 -> 最大 %d x 8 = %d B\n", 8191, 8191 * 8);
return 0;
}
c 本站为静态站,不提供在线运行;可复制到本地用 gcc / python 执行
预期输出:
① 首部字段解析 (20 B 固定部分)
版本 = 4 (4 = IPv4); 首部长度 = 5 x 4 = 20 B
区分服务 = 0x00; 总长度 = 4000 B (单位 1 B)
数据长度 = 总长度 - 首部 = 4000 - 20 = 3980 B
标识 = 0x1A2B = 6699
标志: 保留 = 0, DF = 1, MF = 0; 片偏移 = 0 (x 8 = 0 B)
TTL = 64; 协议 = 6; 首部检验和 = 0x0000
源 IP = 192.168.1.100
目的 IP = 8.8.8.8
② 分片: 总长 4000 B, MTU 1500 B
原数据报: 总长度 4000 B (首部 20 + 数据 3980)
每片最多: MTU 1500 - 首部 20 = 1480, 向下取 8 的倍数 -> 1480 B
第 1 片: 数据 1480 B, 总长度 1500 B, 片偏移 0, MF = 1
第 2 片: 数据 1480 B, 总长度 1500 B, 片偏移 185, MF = 1
第 3 片: 数据 1020 B, 总长度 1040 B, 片偏移 370, MF = 0
总传输量 = 1500 + 1500 + 1040 = 4040 B, 额外开销 = 40 B (2 个额外首部)
③ 单个字段的位数上限
首部长度 4 位 -> 最大 15 x 4 = 60 B
总长度 16 位 -> 最大 65535 B (数据部分最多 65515 B)
片偏移 13 位 -> 最大 8191 x 8 = 65528 B⚠️ 三点说明:
- 本机无 C 编译器:此段代码逐行人工审查,并用等价的 Python 实现实跑核对,输出 22 行逐字一致。
0x40这个字节既是 DF 又是 MF 的载体:0x4000的二进制是0100 0000 0000 0000——第 15 位(保留)= 0、第 14 位(DF)= 1、第 13 位(MF)= 0,片偏移 13 位全 0。 所以"DF = 1 表示禁止分片、本片可整发";若换成0x20B9,则 MF = 1、片偏移 = 185,正是锚点里第 2 片的取值。(mtu - hdr) / 8 * 8这个写法不能简化为mtu - hdr:整数除法天然向下取整,1480/8*8 = 1480恰好不变;但若 MTU 是 1502,(1502-20)/8*8 = 1480而非 1482——这一步就是"片偏移必须以 8 B 为单位"的强制对齐。
例 2:Python——首部字段、分类地址、换算与分片
def pad(s, w):
"""按"显示宽度"补空格: 中文算 2 列, 否则终端里对不齐"""
return s + ' ' * max(0, w - sum(2 if ord(c) > 0x2000 else 1 for c in s))
print('=== ① IPv4 首部 12 个字段 ===')
print(' ' + pad('字段', 18) + pad('位数', 8) + pad('单位', 10) + '说明')
for a, b, c, d in [('版本', '4', '—', '4 = IPv4, 6 = IPv6'),
('首部长度 IHL', '4', '4 B', '最小 5 (20 B), 最大 15 (60 B)'),
('区分服务 TOS', '8', '—', '优先级 / 服务类型'),
('总长度', '16', '1 B', '首部 + 数据, 最大 65535'),
('标识', '16', '—', '同一数据报的各片共用'),
('标志', '3', '—', '保留 / DF(不分片) / MF(还有片)'),
('片偏移', '13', '8 B', '本片数据在原数据中的起始位置'),
('TTL', '8', '跳', '每经路由器减 1, 到 0 丢弃'),
('协议', '8', '—', '1 = ICMP, 6 = TCP, 17 = UDP'),
('首部检验和', '16', '—', '只校验首部, 每跳重算'),
('源 IP', '32', '—', '发送方'),
('目的 IP', '32', '—', '接收方')]:
print(' ' + pad(a, 18) + pad(b, 8) + pad(c, 10) + d)
print(' ★ 固定 20 B; 首部长度单位是 4 B, 总长度单位是 1 B —— 别混')
print()
print('=== ② 首部字段的位数上限 ===')
for a, b in [('首部长度 4 位', '最大 15 x 4 = 60 B (最小 5 x 4 = 20 B)'),
('总长度 16 位', '最大 65535 B; 数据部分最多 65515 B'),
('片偏移 13 位', '最大 8191 x 8 = 65528 B'),
('标识 16 位', '2^16 = 65536 个不同的数据报标识')]:
print(' ' + pad(a, 18) + b)
print()
print('=== ③ 分片: 总长 4000 B, MTU 1500 B ===')
total, hdr, mtu = 4000, 20, 1500
data = total - hdr
maxd = (mtu - hdr) // 8 * 8
print(' 每片数据上限 = (%d - %d) 向下取 8 的倍数 = %d B' % (mtu, hdr, maxd))
print(' ' + pad('片号', 8) + pad('数据长', 10) + pad('总长度', 10) + pad('片偏移', 10)
+ pad('MF', 6) + '说明')
off, left, n = 0, data, 1
while left > 0:
d = maxd if left > maxd else left
mf = 1 if left - d > 0 else 0
note = ('片偏移 = %d / 8 = %d' % (off, off // 8)) if mf == 1 else \
'最后一片 %d / 8 = %.1f, 不必是 8 的倍数' % (d, d / 8)
print(' ' + pad(str(n), 8) + pad('%d B' % d, 10) + pad('%d B' % (d + hdr), 10)
+ pad(str(off // 8), 10) + pad(str(mf), 6) + note)
off += d
left -= d
n += 1
print(' 总传输量 = 1500 + 1500 + 1040 = %d B; 额外开销 %d B (2 个额外首部) = %.2f%%'
% (1500 + 1500 + 1040, 4040 - total, (4040 - total) * 100.0 / total))
print(' ★ 除最后一片外, 每片数据长必须是 8 的倍数; 最后一片不受此限')
print()
print('=== ④ 分类地址 A/B/C ===')
print(' ' + pad('类', 6) + pad('首位模式', 12) + pad('网络号位数', 12) + pad('主机号位数', 12)
+ pad('网络数', 14) + '每网主机数')
for cls, pref, nb, hb in [('A', '0', 7, 24), ('B', '10', 14, 16), ('C', '110', 21, 8)]:
nets = 2 ** nb - (2 if cls == 'A' else 0)
print(' ' + pad(cls, 6) + pad(pref, 12) + pad(str(nb), 12) + pad(str(hb), 12)
+ pad('{:,}'.format(nets), 14) + '{:,}'.format(2 ** hb - 2))
print(' D 类 1110 -> 224.0.0.0 ~ 239.255.255.255 组播; E 类 1111 -> 保留')
print(' ★ A 类网络数减 2: 去掉 0.0.0.0(本网络) 与 127.x.x.x(环回)')
print(' ★ 每网主机数减 2: 去掉"主机号全 0"(网络地址)与"主机号全 1"(广播地址)')
print()
print('=== ⑤ 点分十进制 <-> 二进制 ===')
def to_bin(ip):
return ' '.join(format(int(x), '08b') for x in ip.split('.'))
for ip in ['192.168.1.100', '10.255.255.254', '172.16.0.0', '255.255.255.0']:
print(' ' + pad(ip, 20) + to_bin(ip))
print(' 十进制前缀和速查: 128 / 192 / 224 / 240 / 248 / 252 / 254 / 255')
print(' ★ 192 = 128+64, 224 = 128+64+32, 240 = 128+64+32+16')
print()
print('=== ⑥ 私有地址与特殊地址 ===')
def kind(ip):
a, b, c, d = [int(x) for x in ip.split('.')]
if a == 10:
return '私有 10.0.0.0/8 (1 个 A 类)'
if a == 172 and 16 <= b <= 31:
return '私有 172.16.0.0/12 (16 个 B 类)'
if a == 192 and b == 168:
return '私有 192.168.0.0/16 (256 个 C 类)'
if a == 127:
return '环回地址 (数据不出网卡)'
if a == 0:
return '本网络本主机 (只能作源地址)'
if ip == '255.255.255.255':
return '受限广播 (路由器不转发)'
if a >= 224:
return 'D/E 类 (组播或保留)'
return '公网地址'
for ip in ['10.1.2.3', '172.15.0.1', '172.16.0.1', '172.31.255.255', '172.32.0.1',
'192.168.1.100', '8.8.8.8', '127.0.0.1', '0.0.0.0', '255.255.255.255']:
print(' ' + pad(ip, 20) + kind(ip))
print(' ★ 172.15.x.x 与 172.32.x.x 是公网! /12 只覆盖 172.16 ~ 172.31')
print()
print('=== ⑦ 地址空间账 ===')
print(' IPv4 总数 = 2^32 = {:,} = {:.2f} 亿'.format(2 ** 32, 2 ** 32 / 1e8))
print(' 一个 A 类网络的地址数 = 2^24 = {:,}'.format(2 ** 24))
print(' 一个 B 类网络的地址数 = 2^16 = {:,}'.format(2 ** 16))
print(' 一个 C 类网络的地址数 = 2^8 = {:,}'.format(2 ** 8))
print(' 私有地址总量 = 2^24 + 2^20 + 2^16 = {:,}'.format(2 ** 24 + 2 ** 20 + 2 ** 16))
print(' ★ 分类编址粒度太粗 -> 这就是子网划分与 CIDR 的动因 (见 net/31-subnet.md)')
print()
print('=== ⑧ TTL 的两种用途 ===')
print(' ① 防环: 每经一跳减 1, 到 0 丢弃 -> 最多走 255 跳')
print(' ② traceroute: 发 TTL = 1,2,3... 的探测分组, 第 N 跳路由器回 ICMP 超时')
for default, seen in [(64, 52), (128, 116), (255, 240)]:
print(' 默认 TTL = %3d, 收到 TTL = %3d -> 经过约 %d 跳' % (default, seen, default - seen))
python 本站为静态站,不提供在线运行;可复制到本地用 gcc / python 执行
输出对照(真实运行结果):
=== ① IPv4 首部 12 个字段 ===
字段 位数 单位 说明
版本 4 — 4 = IPv4, 6 = IPv6
首部长度 IHL 4 4 B 最小 5 (20 B), 最大 15 (60 B)
区分服务 TOS 8 — 优先级 / 服务类型
总长度 16 1 B 首部 + 数据, 最大 65535
标识 16 — 同一数据报的各片共用
标志 3 — 保留 / DF(不分片) / MF(还有片)
片偏移 13 8 B 本片数据在原数据中的起始位置
TTL 8 跳 每经路由器减 1, 到 0 丢弃
协议 8 — 1 = ICMP, 6 = TCP, 17 = UDP
首部检验和 16 — 只校验首部, 每跳重算
源 IP 32 — 发送方
目的 IP 32 — 接收方
★ 固定 20 B; 首部长度单位是 4 B, 总长度单位是 1 B —— 别混
=== ② 首部字段的位数上限 ===
首部长度 4 位 最大 15 x 4 = 60 B (最小 5 x 4 = 20 B)
总长度 16 位 最大 65535 B; 数据部分最多 65515 B
片偏移 13 位 最大 8191 x 8 = 65528 B
标识 16 位 2^16 = 65536 个不同的数据报标识
=== ③ 分片: 总长 4000 B, MTU 1500 B ===
每片数据上限 = (1500 - 20) 向下取 8 的倍数 = 1480 B
片号 数据长 总长度 片偏移 MF 说明
1 1480 B 1500 B 0 1 片偏移 = 0 / 8 = 0
2 1480 B 1500 B 185 1 片偏移 = 1480 / 8 = 185
3 1020 B 1040 B 370 0 最后一片 1020 / 8 = 127.5, 不必是 8 的倍数
总传输量 = 1500 + 1500 + 1040 = 4040 B; 额外开销 40 B (2 个额外首部) = 1.00%
★ 除最后一片外, 每片数据长必须是 8 的倍数; 最后一片不受此限
=== ④ 分类地址 A/B/C ===
类 首位模式 网络号位数 主机号位数 网络数 每网主机数
A 0 7 24 126 16,777,214
B 10 14 16 16,384 65,534
C 110 21 8 2,097,152 254
D 类 1110 -> 224.0.0.0 ~ 239.255.255.255 组播; E 类 1111 -> 保留
★ A 类网络数减 2: 去掉 0.0.0.0(本网络) 与 127.x.x.x(环回)
★ 每网主机数减 2: 去掉"主机号全 0"(网络地址)与"主机号全 1"(广播地址)
=== ⑤ 点分十进制 <-> 二进制 ===
192.168.1.100 11000000 10101000 00000001 01100100
10.255.255.254 00001010 11111111 11111111 11111110
172.16.0.0 10101100 00010000 00000000 00000000
255.255.255.0 11111111 11111111 11111111 00000000
十进制前缀和速查: 128 / 192 / 224 / 240 / 248 / 252 / 254 / 255
★ 192 = 128+64, 224 = 128+64+32, 240 = 128+64+32+16
=== ⑥ 私有地址与特殊地址 ===
10.1.2.3 私有 10.0.0.0/8 (1 个 A 类)
172.15.0.1 公网地址
172.16.0.1 私有 172.16.0.0/12 (16 个 B 类)
172.31.255.255 私有 172.16.0.0/12 (16 个 B 类)
172.32.0.1 公网地址
192.168.1.100 私有 192.168.0.0/16 (256 个 C 类)
8.8.8.8 公网地址
127.0.0.1 环回地址 (数据不出网卡)
0.0.0.0 本网络本主机 (只能作源地址)
255.255.255.255 受限广播 (路由器不转发)
★ 172.15.x.x 与 172.32.x.x 是公网! /12 只覆盖 172.16 ~ 172.31
=== ⑦ 地址空间账 ===
IPv4 总数 = 2^32 = 4,294,967,296 = 42.95 亿
一个 A 类网络的地址数 = 2^24 = 16,777,216
一个 B 类网络的地址数 = 2^16 = 65,536
一个 C 类网络的地址数 = 2^8 = 256
私有地址总量 = 2^24 + 2^20 + 2^16 = 17,891,328
★ 分类编址粒度太粗 -> 这就是子网划分与 CIDR 的动因 (见 net/31-subnet.md)
=== ⑧ TTL 的两种用途 ===
① 防环: 每经一跳减 1, 到 0 丢弃 -> 最多走 255 跳
② traceroute: 发 TTL = 1,2,3... 的探测分组, 第 N 跳路由器回 ICMP 超时
默认 TTL = 64, 收到 TTL = 52 -> 经过约 12 跳
默认 TTL = 128, 收到 TTL = 116 -> 经过约 12 跳
默认 TTL = 255, 收到 TTL = 240 -> 经过约 15 跳八条结论:
- 首部固定 20 B、最长 60 B:首部长度字段 4 位,最大值 15,
B——带选项时才会超过 20 B。 - 两个"长度"字段单位不同:首部长度单位 4 B、总长度单位 1 B;数据长度 = 总长度 − 首部长度 × 4——这是本章第一大陷阱。
- ★ 分片锚点:总长 4000 B 经 MTU 1500 → 1500 / 1500 / 1040 B 三片,片偏移 0 / 185 / 370,MF = 1 / 1 / 0。
- ★ 最后一片不必是 8 的倍数:
仍然合法——8 的倍数约束只针对"非最后一片",因为它要给下一片提供整 8 的起始位置。 - 分片的代价是 1.00%:4000 B → 4040 B、多 40 B(2 个额外首部)——分片越多,这项开销占比越高。
- ★ 分类地址锚点:A 类 126 个网络 / 每网 16777214 台;B 类 16384 / 65534;C 类 2097152 / 254——两处"减 2"的来历分别是"本网络 + 环回"和"网络地址 + 广播地址"。
- ★ 私有地址边界的判别:
172.16.x.x与172.31.255.255是私有,172.15.0.1与172.32.0.1是公网——/12覆盖的是172.16~172.31这 16 个 B 类网段。 - TTL 一举两用:既是防环手段,又是
traceroute的实现基础——"默认 64、收到 52"可粗略推出约 12 跳。
考点
考点
1. 必背结论
- IP 是网络层核心协议,提供无连接、尽最大努力交付的分组传输。
- ★ "四条不保证":不保证送达 / 不保证顺序 / 不保证不重复 / 不保证时延。
- 可靠性由两端主机负责(TCP),IP 保持极简——这是"端到端原则"。
- ★ IPv4 首部固定 20 B,最长 60 B;共 12 个字段。
- ★ 单位三兄弟:首部长度单位 4 B;总长度单位 1 B;片偏移单位 8 B。
- ★ 位数上限:首部长度 4 位 → 最大 60 B;总长度 16 位 → 最大 65535 B;片偏移 13 位 → 最大 65528 B。
- 协议号:1 = ICMP、6 = TCP、17 = UDP。
- 首部检验和只校验首部(因为每跳 TTL 要变,校验数据代价太大)。
- ★ MTU:链路层帧能承载的 IP 数据报最大长度;以太网 MTU = 1500 B。
- ★ 分片规则:每片数据 ≤ MTU − 20,且(除最后一片)必须是 8 的倍数;各片标识相同;除最后一片外 MF = 1;片偏移 = 起始偏移 / 8。
- ★ 分片锚点:总长 4000 B(首部 20 + 数据 3980)经 MTU 1500 → 三片:数据 1480 / 1480 / 1020,总长 1500 / 1500 / 1040,片偏移 0 / 185 / 370,MF = 1 / 1 / 0;总传输 4040 B,多 40 B(1.00%)。
- ★ 最后一片的数据长度不必是 8 的倍数(锚点:
)。 - 分组只在目的主机重组;路由器可以分片但不重组。
- ★ IPv4 地址 32 位,点分十进制;分类地址:A 类首位
0、B 类10、C 类110、D 类1110(组播)、E 类1111(保留)。 - ★ 分类地址锚点:A 类 7 位网络号 / 24 位主机号 → 126 个网络 / 16777214 台主机;B 类 14/16 → 16384 / 65534;C 类 21/8 → 2097152 / 254。
- 两处"减 2":A 类网络数减 2(去掉
0.0.0.0与127.x.x.x);每网主机数减 2(去掉网络地址与广播地址)。 - ★ 私有地址三块:
10.0.0.0/8(1 个 A 类)/172.16.0.0/12(16 个 B 类,即172.16~172.31)/192.168.0.0/16(256 个 C 类)。 - ★ 特殊地址:
0.0.0.0本网络本主机 / 主机号全 0 网络地址 / 主机号全 1 直接广播 /255.255.255.255受限广播 /127.0.0.0/8环回。 - 地址空间:
亿。 - TTL 两用途:防环 +
traceroute原理。
2. 高频陷阱
- 把"首部长度"当成"字节数":错。它是"4 B 的个数"——字段值 5 表示 20 B。
- 认为"首部长度可以是任意值":错。最小 5(20 B)、最大 15(60 B)——值小于 5 的报文非法。
- 算数据长度时用"总长度 − 20":只有在"无选项"时才对;通用算法是"总长度 − 首部长度 × 4"。
- 片偏移单位记成 1 B:错。单位是 8 B——片偏移 185 表示"从第 1480 字节开始"。
- 把"每片数据长度必须是 8 的倍数"绝对化:错。除最后一片外才有这个约束——锚点里最后一片 1020 B 就不是 8 的倍数。
- 把"每片最大数据"算成 MTU:错。每片也要带 20 B 首部——MTU 1500 时每片最多 1480 B 数据。
- 认为"分片后各片的标识不同":错。各片标识相同——目的主机正是靠"标识相同"把同一数据报的各片收集起来重组。
- 认为"MF = 1 表示这是最后一片":错,记反了。MF = 1 表示"后面还有分片";最后一片 MF = 0。
- 认为"路由器会重组分片":错。重组只在目的主机做——路由器只负责"按 MTU 再分一次"(若需要)。
- 认为"DF = 1 表示允许分片":错。DF = Don't Fragment,= 1 表示禁止分片——此时若超过 MTU 就丢弃并回 ICMP 差错报文。
- A 类网络数答成 128:错。是 126——
0.0.0.0与127.x.x.x不能用。 - B 类网络数写成
:错。B 类就是 ——"减 2"只发生在 A 类网络数与"每网主机数"上。 - 每网主机数不加"减 2":错。必须减 2(网络地址 + 广播地址)——C 类是 254 而不是 256。
- 把
172.16.0.0/12的范围写成172.0.0.0~172.255.255.255:错。是172.16.0.0~172.31.255.255;172.15.x.x与172.32.x.x都是公网地址。 - 认为"受限广播
255.255.255.255会被路由器转发":错。受限广播只在本网内、路由器不转发;能被路由器转发的是"直接广播"(网络号有效、主机号全 1)。 - 把"环回地址"当成"本机地址可以用":要注意:
127.0.0.0/8整段都是环回,数据不出网卡;0.0.0.0只能作源地址。 - 认为"TTL 表示时间":错。现代 TTL 表示"还能经过多少跳"(跳数计数),不是秒数。
3. 解题模板("IP 数据报计算题")
① 首部解析题: 按位置读
第 1 字节高 4 位 = 版本, 低 4 位 = 首部长度(单位 4 B)
第 3~4 字节 = 总长度(单位 1 B)
数据长度 = 总长度 - 首部长度 x 4
② 分片题(三步):
(1) 每片最大数据 = (MTU - 20) 向下取 8 的倍数
(2) 从原数据头开始切, 最后一片取剩下的(不必是 8 的倍数)
(3) 填 4 个字段:
标识 -> 各片相同(= 原数据报的标识)
MF -> 除最后一片外全 1
片偏移 -> 本片起始字节 / 8
总长度 -> 本片数据长 + 20
③ 分片开销: 额外开销 = (片数 - 1) x 20 B
④ 地址题:
分类看首位模式: 0 / 10 / 110 / 1110 / 1111
网络数: A 类 2^7-2 = 126, B 类 2^14, C 类 2^21
每网主机数: 2^主机号位数 - 2
⑤ 私有地址判别: 10.*.*.* / 172.16~172.31.*.* / 192.168.*.*
⑥ 二进制换算: 记住 128/192/224/240/248/252/254/255 这几个前缀和4. 与相邻章节的接口
net/24-ethernet.md(以太网):"MTU 1500"是上一章给出的链路层参数,本章用它做分片计算;"IP 数据报是帧的载荷、帧长 = 14 + 载荷 + 4"与本章的"总长度"是同一个数在两层里的两种叫法。net/01-architecture.md(体系结构):"MTU 1500、以太网最大帧 1518、固定开销 58 B"的锚点口径在本章继续保持;"网络层 PDU 叫数据报(分组)"也在上一章给出。net/31-subnet.md(子网划分与 CIDR):本章的"分类编址粒度太粗"是下一章的动因——下一章就在"网络号 + 主机号"这两段之间再插一段"子网号"。net/32-nat.md(NAT 与 ICMP):本章的私有地址三块是 NAT 的前提——NAT 就是"让私有地址的主机通过一个公网地址上网",同时本章提到的 ICMP 差错报文在那一章展开。net/34-ipv6.md(IPv6):本章的分片开销与地址枯竭两个问题,IPv6 用"取消路由器分片 + 128 位地址"一并解决。net/41-tcp.md(TCP 报文段):TCP 的"首部长度字段"(数据偏移,单位 4 B)与本章的"首部长度"是同一种设计;TCP 的 MSS(最大报文段长度)与本章的 MTU 之间差着一个 IP 首部 20 B + TCP 首部 20 B。os/21-paging.md(分页):"把大块切成定长小块 + 编号偏移"与本章的分片是同一个结构——区别只在于:分页是硬件地址翻译,分片是网络转发需要。
小结
- IP = 网络层核心,无连接、尽最大努力交付;四条不保证:送达 / 顺序 / 不重复 / 时延。
- ★ IPv4 首部 12 字段、固定 20 B、最长 60 B;单位三兄弟:首部长度 4 B / 总长度 1 B / 片偏移 8 B。
- 位数上限:首部长度 4 位 → 60 B;总长度 16 位 → 65535 B;片偏移 13 位 → 65528 B。
- 协议号:1 = ICMP、6 = TCP、17 = UDP;首部检验和只校验首部。
- ★ 分片锚点:总长 4000 B、MTU 1500 → 数据 1480 / 1480 / 1020,总长 1500 / 1500 / 1040,片偏移 0 / 185 / 370,MF = 1 / 1 / 0;总传输 4040 B、多 40 B。
- ★ 最后一片不必是 8 的倍数;除最后一片外每片数据长必须是 8 的倍数;重组只在目的主机。
- ★ 分类地址:A(
0,7/24 位,126 网 / 16777214 台)/ B(10,14/16,16384 / 65534)/ C(110,21/8,2097152 / 254)/ D(1110组播)/ E(1111保留)。 - 两处减 2:A 类网络数(去 0.0.0.0 与 127.x.x.x);每网主机数(去网络地址与广播地址)。
- ★ 私有地址:
10.0.0.0/8、172.16.0.0/12(172.16~172.31)、192.168.0.0/16。 - 特殊地址:
0.0.0.0/ 网络地址(主机号全 0)/ 直接广播(主机号全 1)/255.255.255.255/127.0.0.0/8。 - 地址空间
亿;TTL 两用途:防环 +traceroute。
下一篇:子网划分与 CIDR
评论(0)
当前浏览器不允许本地存储,评论无法保存。
还没有评论,来说两句。