你在浏览器里敲下 example.com,不到一百毫秒,一台可能远在另一个大洲的机器就把网页送了回来。中间隔着你家路由器、运营商的机房、海底光缆,没有任何一条线是从你这里直接连到它那里的。这篇回答一个问题:一个包凭什么能一跳一跳地找到路?答案会从”两台机器用一根线连起来”开始,每次规模变大、原来的办法失效,就引出一层新的设计。

前置:信息的表示(二进制、字节序)

分层:每一层只解决一个问题

先想最朴素的情形:两台电脑用一根网线直连。要通信,只需要约定电信号怎么表示 0 和 1,再约定一段比特从哪里开始、到哪里结束。这就是物理层和链路层的工作。

现在把一栋楼的几十台电脑连起来。每个包得写明”给谁”,于是有了链路层地址(MAC)。再往上,把全世界几十亿台设备连起来,MAC 就不够用了(下文会讲为什么),于是有了网络层的 IP 地址和路由。IP 只负责”尽力把包送到那台机器”,可能丢、可能乱序;一台机器上又同时跑着浏览器、微信、ssh,于是传输层用端口区分进程,TCP 还负责把丢掉的包补回来。最上面是应用层,HTTP、DNS 这些协议定义”内容是什么意思”。

层解决的问题地址典型协议 / 设备
应用层内容的含义域名、URLHTTP、DNS、SSH
传输层交给哪个进程;可靠性端口(16 位)TCP、UDP
网络层跨网络送到哪台机器IP 地址(IPv4 32 位)IP、ICMP;路由器
链路层同一段链路上交给谁MAC 地址(48 位)以太网、Wi-Fi;交换机
物理层比特怎么变成信号—网线、光纤、无线电

这就是常说的 TCP/IP 五层模型。分层的价值在于每层只依赖下一层提供的服务,而不关心它怎么实现:TCP 不在乎下面是 Wi-Fi 还是光纤,以太网也不在乎上面装的是 IP 还是别的东西。

分层在数据上的体现叫封装。应用把数据交给 TCP,TCP 在前面加上自己的头(端口、序号),交给 IP;IP 再加上源和目的 IP 地址,交给以太网;以太网再套上 MAC 地址和校验,变成线上的一帧:

| 以太网头 14B | IP 头 20B | TCP 头 20B | HTTP 数据 ... | FCS 4B |
  dst MAC, src MAC,   src IP, dst IP,  src port, dst port,
  EtherType=0x0800   TTL, protocol=6   seq, ack, flags

每个头里都有一个字段说明”里面装的是什么”:以太网的 EtherType 0x0800 表示 IPv4,IP 头的 protocol 6 表示 TCP、17 表示 UDP,TCP 的端口号说明交给哪个应用。接收方按这些字段一层层拆开,就像拆快递时外箱上写着”内有小盒”。

以太网一帧的载荷上限默认是 1500 字节,叫 MTU。去掉 IP 头和 TCP 头各 20 字节,一个 TCP 段最多带 1460 字节应用数据。这个数字后面讲 TCP 时还会出现。

停一下,检查理解单选

路由器转发一个 HTTP 请求包时,通常会读取并改写哪些头部?

链路层:在一根线上找到对方

早期以太网所有电脑挂在同一根同轴电缆上,后来是集线器(hub):一个口进来的信号原样复制到所有其他口。这意味着所有人共享一条”走廊”,两台机器同时说话信号就会叠在一起。解决办法是 CSMA/CD:先听线上有没有人说话,没人再发;发的同时继续听,发现冲突就停下,随机等一会儿重来,冲突次数越多等待的随机范围越大(二进制指数退避)。

共享介质的问题是规模一大冲突就失控,而且谁都能听到别人的包。交换机(switch)改变了这一点。它也有很多口,但它会记下”哪个 MAC 地址出现在哪个口”:

  1. 收到一帧,把(源 MAC,入口)记进 MAC 表。这一步叫自学习,不需要任何配置。
  2. 查目的 MAC:表里有就只从那个口发出;没有就向除入口外的所有口泛洪。
  3. 目的地址是广播 ff:ff:ff:ff:ff:ff 的帧,一律泛洪。

每个口独立通信,冲突基本消失。现代有线以太网都是全双工点对点链路,CSMA/CD 实际上已经不再触发;Wi-Fi 因为还是共享空气,用的是避免冲突的变体 CSMA/CA。

这里的 MAC 地址是 48 位,写成 a4:83:e7:12:34:56 这样的形式,前 24 位是厂商编号,原则上出厂即固定(如今手机为隐私会随机化)。它是”身份”而不是”位置”:一个 MAC 地址不会告诉你这台设备在世界的哪个角落。这正是 MAC 不能用来做全球寻址的原因。如果路由器要按 MAC 转发,它就得记住全世界每一块网卡在哪个方向,表项数以百亿计,而且设备一移动就全部失效。

那么同一个局域网里,我知道对方的 IP(比如网关 192.168.1.1),怎么知道它的 MAC?用 ARP:

我 → 广播 ff:ff:ff:ff:ff:ff:  谁是 192.168.1.1?请告诉 192.168.1.23
网关 → 单播给我:             192.168.1.1 在 a4:83:e7:xx:xx:xx

结果缓存在本机的 ARP 表里(arp -a 可以看),过几分钟过期。ARP 完全基于信任,谁回答都信,同一局域网的攻击者可以冒充网关(ARP 欺骗)。

停一下,检查理解单选

交换机刚上电、MAC 表为空时,主机 A 给主机 B 发了一帧。会发生什么?

IP 地址:把”身份”换成”位置”

MAC 失败的教训是:全球寻址需要可聚合的地址,让一个表项代表一大片机器,就像邮编 100 开头的都往北京送,邮局不需要认识北京每一户。IPv4 地址是 32 位,写成四个十进制字节 192.168.1.77,并被切成两段:前缀表示”哪个网络”,剩下的表示”网络里的哪台主机”。

前缀有多长,用 CIDR 记法 /n 或者等价的子网掩码说明。192.168.1.0/24 表示前 24 位是网络号,掩码 255.255.255.0,这个网络有 2^8 = 256 个地址,其中全 0 的 .0 是网络地址、全 1 的 .255 是广播地址,剩下 254 个给主机。判断一个地址是否在某网络内,就是把它和掩码按位与,看结果是否等于网络号。Python 标准库的 ipaddress 可以直接验证:

import ipaddress as ip
net = ip.ip_network("192.168.1.0/24")
print(net.netmask, net.num_addresses, net.broadcast_address, ip.ip_address("192.168.1.77") in net)
print(ip.ip_address("192.168.1.77").is_private, ip.ip_address("8.8.8.8").is_private)
255.255.255.0 256 192.168.1.255 True
True False

早年地址按 A/B/C 类固定切成 /8、/16、/24,浪费严重,1993 年起 CIDR 才允许任意长度的前缀。

一台主机发包前要做的第一个判断就基于掩码:目的 IP 和我在不在同一个子网?在,就 ARP 问对方的 MAC 直接发;不在,就把包交给默认网关(通常是家里的路由器),ARP 问的是网关的 MAC。注意这时帧上的目的 MAC 是网关,而 IP 头里的目的地址仍然是最终服务器,这两个地址从此分道扬镳:IP 地址端到端不变(NAT 除外,见后文),MAC 地址每过一跳就换一次。

8.8.8.8 输出 is_private=False,因为 RFC 1918 留出了三段私有地址:10.0.0.0/8、172.16.0.0/12、192.168.0.0/16。它们可以在任何内网重复使用,但不能出现在公网上。为什么要这样?因为 32 位只有约 43 亿个地址,早已分完。私有地址加上后面的 NAT 是 IPv4 续命的主要手段,真正的解决方案是 128 位地址的 IPv6,它的地址空间大到可以给每台设备分配全局唯一地址,但迁移至今仍未完成。

停一下,检查理解单选

主机 10.1.2.7/24 要给 10.1.3.9 发包,以太网帧的目的 MAC 应该填谁?

路由:每一跳只做一次查表

包到了路由器,路由器怎么决定往哪个口发?它有一张路由表,每一项是”前缀 → 下一跳”。目的地址可能同时匹配好几项,规则是最长前缀匹配:选前缀最长、也就是最具体的那一项。0.0.0.0/0 匹配一切,所以它就是默认路由,兜底用。

用 ipaddress 写一个四行的路由器:

import ipaddress as ip
table = [("0.0.0.0/0","默认网关"),("10.0.0.0/8","公司 VPN"),("10.1.0.0/16","机房 A"),("10.1.2.0/24","GPU 集群")]
def route(dst):
    d = ip.ip_address(dst)
    hits = [(ip.ip_network(n), h) for n, h in table if d in ip.ip_network(n)]
    return max(hits, key=lambda x: x[0].prefixlen)   # 越具体越优先
for dst in ["10.1.2.7","10.1.9.9","10.200.0.1","8.8.8.8"]:
    n,h = route(dst); print(f"{dst:<12} -> {str(n):<12} {h}")
10.1.2.7     -> 10.1.2.0/24  GPU 集群
10.1.9.9     -> 10.1.0.0/16  机房 A
10.200.0.1   -> 10.0.0.0/8   公司 VPN
8.8.8.8      -> 0.0.0.0/0    默认网关

10.1.2.7 同时落在四个前缀里,最长的 /24 胜出。这个规则让聚合和例外可以共存:一个运营商对外宣告一个大前缀,内部某个客户需要特殊路径时再加一条更长的前缀覆盖它。真实路由器面对几十万条公网路由、每秒上亿个包,用的是 trie(前缀树)或专用的 TCAM 硬件,但语义和上面这段代码相同。

每一跳路由器还会把 IP 头里的 TTL(Time To Live)减 1,减到 0 就丢包,并回一个 ICMP “超时”消息给源地址。TTL 的本意是防止路由配置错误时包在环路里永远打转。traceroute 巧妙地利用了它:依次发 TTL = 1、2、3 … 的包,第 n 跳路由器会因为 TTL 归零而回 ICMP,于是源头就知道了路径上每一跳是谁、往返多久。ping 用的则是 ICMP 的回显请求和回显应答。

路由表从哪里来?小网络里手工配置就行。大网络里路由器之间运行路由协议互相告知可达性。一个自治系统(AS,例如一个运营商或一所大学)内部常用 OSPF 这类链路状态协议:每台路由器把自己的邻居和链路代价泛洪给所有人,每台都拼出整张图,再各自用 Dijkstra 算最短路(算法本身见 算法设计)。另一类是距离向量协议(如 RIP):每台只告诉邻居”我到各目的地的距离”,大家迭代做 Bellman-Ford,实现简单但收敛慢。AS 之间则用 BGP,它交换的是”经过哪些 AS 能到这个前缀”,选路主要看商业策略而非最短距离。互联网的连通性最终就是几万个 AS 用 BGP 拼起来的。

路由器只做尽力而为的转发:队列满了就丢,不做补救,可靠性留给端上的 TCP,这就是端到端原则。

停一下,检查理解多选

关于路由与 TTL,下列哪些说法正确?

NAT 与 DHCP:一个公网地址,一屋子设备

家里的手机、电脑、电视都是 192.168.x.x 这样的私有地址,可它们都能上网。中间做手脚的是家用路由器上的 NAT(Network Address Translation),更准确地说是 NAPT,因为它同时改端口:

内网包:  192.168.1.23:52011  →  93.184.x.x:443
NAT 改写:203.0.113.5:40001   →  93.184.x.x:443     并记录 40001 ↔ 192.168.1.23:52011
回包:    93.184.x.x:443      →  203.0.113.5:40001
NAT 还原:93.184.x.x:443      →  192.168.1.23:52011

(203.0.113.0/24 是 RFC 5737 留给文档用的示例地址。)NAT 把一个公网 IP 的六万多个端口分给内网的连接用,靠一张转换表记住谁是谁。代价是它破坏了”IP 地址端到端不变”的假设:外面的机器无法主动连进来,因为 NAT 表里没有对应条目。这顺带提供了一点安全性,却让 P2P、视频通话这类需要直连的应用很头疼,它们得靠 STUN 之类的”打洞”技术,或者干脆经过中继服务器。运营商为省地址还可能再套一层 NAT(CGNAT)。

新设备接入网络时还没有 IP,它怎么知道自己该用哪个地址、网关和 DNS 是谁?靠 DHCP。设备用源地址 0.0.0.0 广播一个 Discover,DHCP 服务器(一般也是家用路由器)回 Offer,设备 Request,服务器 Ack,四步之后设备拿到 IP、掩码、默认网关、DNS 服务器地址,还有一个租期。租期到了要续约,不续就收回给别人用。

停一下,检查理解单选

家里一台笔记本(192.168.1.23)在跑一个 Web 服务,朋友在外网直接访问家里的公网 IP 却连不上。最根本的原因是什么?

DNS:名字到地址的分布式查表

到这里,只要知道目的 IP,包就能送到。但人记的是 example.com。DNS 负责把名字翻译成 IP。最朴素的做法是一个全球文件记录所有名字,早期的 hosts.txt 正是这样,主机一多就无法维护。DNS 的解法和 IP 地址一样:按层级切分,把管理权一级级下放。

名字从右往左读:根 . 之下是顶级域 com,之下是 example,再之下可以是 www。根服务器只知道各顶级域归谁管,.com 的服务器只知道 example.com 归谁管,最后由 example.com 自己的权威服务器给出答案。你的电脑并不亲自走这条路,它把问题交给一个递归解析器(运营商的、或者 8.8.8.8、1.1.1.1 这类公共的),由解析器去问根、问顶级域、问权威,再把结果和 TTL 一起缓存起来。

这条链路上的每一级都可以查。下面用 Cloudflare 的 DNS-over-HTTPS 接口逐级查询:

curl -s -H 'accept: application/dns-json' 'https://cloudflare-dns.com/dns-query?name=com&type=NS'
curl -s -H 'accept: application/dns-json' 'https://cloudflare-dns.com/dns-query?name=example.com&type=NS'
curl -s -H 'accept: application/dns-json' 'https://cloudflare-dns.com/dns-query?name=example.com&type=A'
com          NS  a.gtld-servers.net.  b.gtld-servers.net. ...   TTL 169758
example.com  NS  hera.ns.cloudflare.com.  elliott.ns.cloudflare.com.   TTL 79575
example.com  A   104.20.23.154  172.66.147.243   TTL 167

(输出从 JSON 里摘出了关键字段。)三个 TTL 差了三个数量级:顶级域的 NS 记录几乎不变,缓存两天;具体的 A 记录可能随负载均衡变动,只缓存几分钟。缓存是 DNS 撑住全球查询量的关键,一个热门名字的查询绝大多数在解析器的缓存里就结束了,根服务器很少被打扰。代价是改了记录之后,旧答案要等 TTL 过期才会在各处消失。

常见记录类型有 A、AAAA、CNAME(别名)、NS、MX。DNS 查询通常走 UDP 53 端口,一问一答各一个包,省掉了建立连接的开销;答案太大时才退回 TCP。

DNS 默认是明文的,而且路径上的任何人都能伪造回答。在写这篇的机器上,系统默认解析器的结果是这样的:

$ dig +noall +answer example.com A
example.com.		1	IN	A	198.18.0.208

198.18.0.0/15 是保留给网络测试的地址段,不属于任何真实网站。这台机器开着代理软件,它在本地截获 DNS 查询,返回一个假地址,再把发往这个假地址的连接转进代理隧道(所谓 fake-IP 模式)。这正好演示了 DNS 的脆弱:谁控制了你的解析路径,谁就能决定 example.com 指向哪里。DNSSEC 给记录加签名来防伪造,DoH/DoT 把查询装进加密通道来防窃听和篡改,上面的 curl 查询就是 DoH。

停一下,检查理解单选

把一个网站的 A 记录从旧 IP 改到新 IP 后,部分用户几小时内仍然访问旧 IP。最可能的原因是什么?

串起来:开机到发出第一个包

把这些拼在一起,看笔记本连上 Wi-Fi 后访问 example.com 的前半程:

  1. DHCP:广播请求,拿到本机 IP 192.168.1.23/24、网关 192.168.1.1、DNS 服务器地址。
  2. ARP:DNS 服务器不在本子网,要经过网关,于是先 ARP 拿到网关的 MAC。
  3. DNS:把查询 example.com A? 装进 UDP,目的 IP 是 DNS 服务器,目的 MAC 是网关,发出去。家用路由器做 NAT 改写源地址,包沿着最长前缀匹配一跳跳到达解析器,解析器从缓存或逐级查询拿到答案返回。
  4. 发往服务器:现在有了服务器 IP。构造 TCP 段,外面套 IP 头(目的是服务器),再套以太网头(目的 MAC 仍是网关),发出去。从这里开始是传输层的故事。

没有哪一层知道整条路径,路径是每一跳的局部决策拼出来的。至于包丢了怎么办、怎么不把网络挤爆、HTTPS 怎么防窃听,交给 网络:TCP 与应用层。

记住这三件事

  1. 分层让每层只解决一个问题:MAC 管同一链路内的下一跳,每跳都换;IP 管端到端的目的地,一路不变(NAT 除外)。
  2. IP 地址可聚合,路由器只按目的地址做最长前缀匹配、TTL 减 1,尽力而为,不保证送达。
  3. DNS 和路由一样靠层级委派加缓存扩展到全球,代价是更新要等 TTL,而且默认不防伪造。

延伸阅读

  • Stanford CS144 Introduction to Computer Networking 课程讲义与视频(cs144.github.io)
  • Kurose & Ross, Computer Networking: A Top-Down Approach,第 4–6 章(网络层与链路层)
  • RFC 791(IPv4)、RFC 826(ARP)、RFC 1918(私有地址)、RFC 1034/1035(DNS)
  • Julia Evans, How DNS Works 漫画与 dig 系列文章(jvns.ca)
  • Cloudflare Learning Center:What is BGP / What is DNS