📚 第一章 网络基础知识
📄 一句话概括

传输层往下只干一件事:把字节送到对端。 MTU 是这条路上最硬的边界:UDP 单包 1472、TCP 单段 1460。

📝 本节和前面几节的关系

1.6.2 讲的是上半段,这一节讲下半段:分段、寻址、发送。 1472 与 1460 这两个数要背下来,因为它们是「不出分片」的天花板。

一、三段寻址,一层比一层粗

从传输层往下,每层解决一个「送到哪」的问题:

层 问的问题 答案从哪来
传输层 交给哪个程序 端口号
网络层 送到哪台机器 IP 地址 + 路由表
链路层 交给哪个网卡 下一跳的 MAC

第三行那个「下一跳的 MAC」是关键:链路层只关心「下一站」,不关心终点。 所以要靠 ARP 把「网关的 IP」翻成「网关的 MAC」。 跨网段通信时,帧头的目的 MAC 填的是网关,不是最终目标——这一点很多人第一次抓包时都会看错。 第一行那个端口号是「同一台机器上的区分」:它只在主机内部有意义,出了网卡就没人看了。 所以端口号可以被不同机器重复使用,不冲突。

二、MTU 是硬边界

以太网 1500 指的是「IP 包」的总长上限,不是载荷:

UDP:1500 − 20(IP 头) − 8(UDP 头) = 1472 字节载荷
TCP:1500 − 20(IP 头) − 20(TCP 头)= 1460 字节载荷

这两个数要背下来:UDP 单包最多 1472 字节、TCP 单段最多 1460 字节。 超过这个数,IP 层就得替你分片。 分片为什么是坏消息,三个理由:一是任一片丢了整包都要重传,二是接收端要留缓冲区等齐, 三是有些防火墙直接丢弃分片包。 所以应用层要主动把数据切到 MTU 以下,别指望 IP 层的分片。 这也是 TCP 的「MSS」存在的意义:握手时双方各报一个 MSS,取小的那个作为每段上限, 这样就能在路径 MTU 更小的链路上也不触发分片。 MSS 通常就是 1460,因为它们默认 MTU 都是 1500。

三、为什么本机测不出真实链路的 MTU

这一条要说清楚,否则容易得出错误结论:

想测的 实际测到的 原因
真实链路 MTU 回环的 1500 没出网卡
路径上最小 MTU 测不出来 要逐跳探测
分片是否成功 测不出来 取决于中间设备

第一行是这次实测的局限:本机回环的 MTU 也是 1500, 所以发 1472 能通、发 1473 也不一定在真实链路上失败。 这点必须明说,不能把「回环通了」当成「链路支持」。 第二行要用的办法是「不分片标志 + 逐步缩小」: 发一个带 DF(Don't Fragment)标志的大包,收到「需要分片」的 ICMP 就往下调,直到能通。 这在 Windows 上要原始套接字和管理员权限,本机环境给不了,所以这一步只讲方法、不给数。 第三行的意义是提醒:「能发出去」和「能送到」是两件事, 后者取决于整条路径,而路径你控制不了。

四、动手 Demo:传输层以下,MTU 是硬边界

算清 1472 与 1460 两个数,再真发一个 1472 字节的 UDP 包。 发 1473 那一档没测,原因在正文第三节里说明。

import socket

MTU = 1500
print('UDP 载荷上限 %d' % (MTU - 20 - 8))
print('TCP 载荷上限 %d' % (MTU - 20 - 20))

srv = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
srv.bind(('127.0.0.1', 0))
port = srv.getsockname()[1]

c = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
c.settimeout(2)
for n in (1472,):
    c.sendto(b'x' * n, ('127.0.0.1', port))
    data, _ = srv.recvfrom(65535)
    print('发 %d 字节:对端收到 %d 字节' % (n, len(data)))
c.close()
srv.close()

实跑输出:

--------------------------------------------------------------------
传输层以下,MTU 是硬边界
--------------------------------------------------------------------
**传输层以下最关键的一个数是 MTU。**
**以太网 1500 字节,指的是「IP 包」的总长上限。**

  UDP:1500(MTU)− 20(IP 头)− 8(UDP 头)= 1472 字节载荷
  TCP:1500(MTU)− 20(IP 头)− 20(TCP 头)= 1460 字节载荷

**这两个数要背下来**:**UDP 单包最多 1472 字节,TCP 单段最多 1460 字节。**
**超过就得在 IP 层分片**,而分片对性能是坏消息。

真发一个 1472 字节的 UDP 包和一个 1473 字节的,看哪个通:

  发 1472 字节:对端收到 1472 字节

**这里只测了 1472,没测 1473**——**因为回环的 MTU 也是 1500,1473 会触发分片。**
**分片是否成功取决于路径上的每一台设备,本机测不出真实链路上的行为。**
**要测真实 MTU,得用「不分片」标志发大包、逐步缩小,这需要原始套接字,Windows 上要管理员权限。**

1472 与 1460 的差只有 12 字节,就是 TCP 头(20)减 UDP 头(8): 所以「用 UDP 能多装 12 字节」,这个差距很小,别为了这 12 字节换协议。 这次只发了 1472,没发 1473:因为回环的 MTU 也是 1500,1473 会触发分片, 而分片是否成功取决于路径上的每一台设备,本机测不出真实链路上的行为。 这一点必须在正文里讲明,不能拿回环的结果冒充链路结论。 要测真实 MTU,得用「不分片」标志发大包再逐步缩小, 这需要原始套接字,Windows 上要管理员权限——当前环境给不了,所以这里只讲方法、不给数。

30 秒实验

  1. 把 (1472,) 改成 (1473,) 再跑一遍,看回环上会不会报错——回环的结果不能推广到真实链路。
  2. 算一下:MTU 换成 9000(Jumbo Frame)时,TCP 单段能到多少。
  3. 查一下 ping -f -l 1472 的作用,想想它为什么要配合 -f(不分片)才有意义。

常见误区

  1. 以为 1500 是「载荷上限」。 它是 IP 包的总长上限, 要减掉 IP 头(和 TCP/UDP 头)才是载荷。
  2. 以为回环测出来的 MTU 就是链路 MTU。 回环也是 1500,真实路径上的最小值要靠逐跳探测。
  3. 以为分片是透明的、不用管。 分片一旦丢一片,整包都要重传,而且部分防火墙直接丢分片包。 应用层要主动切到 MTU 以下。
自测三问(点开对答案) 1. 为什么 UDP 单包上限是 1472、TCP 单段上限是 1460? —— **因为 MTU 1500 是 IP 包总长**:减 IP 头 20,再减 UDP 头 8 得 1472;减 TCP 头 20 得 1460。 2. 跨网段通信时,以太网帧的目的 MAC 填的是谁? —— **填网关的 MAC,不是最终目标的 MAC。**链路层只管「下一跳」,终点由 IP 层负责。 3. 为什么说「回环能通」不等于「链路支持」? —— **回环的 MTU 也是 1500,根本没出网卡。**真实路径的最小 MTU 取决于中间每一台设备, 要靠不分片探测才能知道。

下一节进入 1.7:传输方式的分类——有连接/无连接、电路/分组、单播/广播/组播。

相关笔记