一台电脑要把文件交给远处的服务器,最直接的办法是拉一根线。但如果每两台机器都要单独连接,
这就引出了计算机网络的几个核心问题:数据怎样被拆开和转发?共享资源不足时怎么办?每个节点只知道局部信息,怎样共同找到一条正确的路径?当网络规模继续增长,又怎样避免每台路由器都记住每台机器?
这是 CS Revisited / Computer Networks 的第一篇。Computer Networks 会按主题持续分篇,本篇先建立架构与路由的基础。假设读者熟悉基本的数据结构、图和概率;协议术语会在用到时引入。本文覆盖 Berkeley CS 168 (UC Berkeley CS 168, 2026) 第 1–4 周的七讲,按问题之间的依赖组织,也补充一些推导和协议细节。
从一条链路到一张网络
先把要传输的数据切成 packet。每个 packet 有 payload,以及告诉接收方如何处理它的 header。例如,目的地址回答“交给谁”,长度帮助划定边界;它们的意义并不依赖 payload 是图片还是文件。两个端点之间的一组相关 packet 常被称为 flow,具体如何识别取决于协议和讨论场景。
链路把 bit 从一个接口送到另一个接口。中间节点收到 packet 后,决定从哪个接口继续发送,这叫 forwarding。如果它按 IP 目的地址跨网络转发,我们称它为 router。它不用理解文件内容,但必须有一张“目的地 → 下一跳”的表。怎样获得这张表,是后面要解决的 routing。
逐步阅读这个包的旅程
- A 准备发送:跟踪 payload 1 对应的一个包。目的地址是 B;发送之前,R1 的转发表已经由 routing 建立。
- A → R1:开始发送 bit:A 把包编码成链路上的 bit 串。最前面的 bit 已在传播,后面的 bit 仍可能留在 A;不是整个包瞬间跳到 R1。
- R1 收到完整的包:本例采用 store-and-forward:R1 从入口 1 收齐这个包。发送时间与传播时间不同,步骤间隔不代表真实耗时。
- R1 读取目的地址 B:R1 从 header 读出目的地址 B,用它查询转发表;不需要理解 payload 的文件内容。
- 命中表项:B → R2,出口 2:表项给出下一跳 R2 和本机出口 2。R2 是下一台设备,B 仍是这个包的最终目的地。
- R1 → R2:从出口 2 发送:R1 将包交给出口 2,在下一条链路上重新发送 bit。出口 3 通向 R3,本次没有被选中。
- R2 再次作转发决定:R2 收到包,也要查自己的转发表。R1 只决定上一跳怎样到达 R2,并没有替 R2 完成查表。
- R2 → B:最后一跳:R2 选择通向 B 的接口,把包的 bit 送到 B。每一跳都包含链路传输,router 处还包含本地的转发决定。
- B 收到这个包:B 收到了 payload 1 对应的包。其余包也要传输;更高层负责后续处理。收到一个包不等于完整文件已经交付。
Internet 的特别之处还在于:这些链路和路由器不属于同一个管理员。校园网、运营商和云网络有不同技术、成本和策略,却需要互相传递数据。因而协议不仅要能运行,还要允许独立部署、局部升级,以及在部分设备失效时继续工作。Internet 是连接这些网络的基础设施;Web 只是运行在它上面的一类应用。
为什么不能像升级一个程序那样升级 Internet
先考虑一个校园网:管理员可以选择交换机、分配地址,统一升级设备。再把校园网接到运营商,运营商又和别的运营商互联。此时没有谁能替所有人决定设备、成本或内部路由,但每一段都必须交付别人送来的包。这种 federation 的关键是:共同约定边界行为,内部实现保留自治。(UC Berkeley CS 168, 2026b)
互联因此同时是技术问题与协调问题。两个运营者可能竞争同一批客户,却必须交换流量;一条物理上很短的路径,也可能因商业关系而不被采用。新协议即使在实验室更好,也必须回答“只升级一部分参与者时还能不能工作”。后面的 IP 窄腰、域内与域间路由分工,都在缩小必须共同协调的范围。
另一个约束是反馈必然落后。取两端距离 4,000 km、信号速度约
当一个回复迟迟不来,发送者不能仅凭沉默区分“消息丢了”“对方故障了”“回复还在排队”。Timeout 是根据等待时间作出的行动判断,不是对远处状态的绝对证明。设计协议时必须同时考虑正常推进、重复消息和失效恢复;不能只写一条成功时的 send/receive 流程。
Bandwidth 与 latency 是两件事
先看一个容易画清楚的例子:一个 4 kbit 的包,通过 1 kbit/s 的链路发送;信号走完链路需要 3 s。发送端每秒送入 1 kbit,已经上链路的数据则继续向接收端移动。到了第 4 秒,发送端刚把整个包送上线,接收端却只收到了开头的 1 kbit。
假设链路的发送速率为
这叫 transmission delay,也常叫 serialization delay。发出去之后,信号还得走完距离
从开始发送到最后一个 bit 到达,对一条空闲链路有
提高
如果路径包含三条这样的链路,且 router 必须收齐包才开始转发,即 store-and-forward,单包就需要三次 serialization:
这里写
分包怎样形成流水线
如果沿途 router 都必须等完整文件收齐才继续发送,那么前一个发送者忙完,后一个才能开始,链路利用率很低。分包之后,第一个包进入第二条链路时,第二个包已经可以进入第一条链路。
设有
在上面的三跳例子中,100 个包需要
链路上同时能容纳多少 bit?发送端在一个传播时间内可以送入
讨论发送窗口时,经常改用 带宽 × RTT。这是另一段时间:发送端要持续发送直到最早的数据得到反馈。若反馈时间约为 40 ms,要维持 100 Mbit/s,未确认数据量就得达到约 500 kB。两种 BDP 都有意义,但必须说明用的是单程传播时间还是反馈往返时间。
练习:更高的带宽什么时候才值得?
链路 A 为 10 Mbit/s、10 ms propagation delay;链路 B 为 1 Mbit/s、1 ms。令两种发送时间相等:
解得
共享链路:为什么需要 queue
现在让三个用户共享 10 Mbit/s 链路,每人活跃时需要 4 Mbit/s,但并不一直发送。如果给每人永久预留峰值,总共需要 12 Mbit/s,第三人无法加入。可是三个人轮流活跃时,实际需求从来没有超过 4 Mbit/s。
区别在于:各自峰值之和,不等于合并后的峰值。若第
不等式可以取等号;只有峰值没有同时出现时,才有共享收益。Statistical multiplexing 利用的就是这种错开,而不是凭空增加容量。(UC Berkeley CS 168, n.d.)
这里可以先问一个反例:如果三个用户始终各发 3 Mbit/s,总需求恒为 9 Mbit/s,那么预留与逐包共享都能满足它们,细粒度复用并没有额外创造收益。共享更有价值的地方是各用户的需求会变,并且变化不完全同步。容量规划还得选择容忍多少超载,而不能把长期平均值当作每个瞬间的保证。
Circuit switching 与 packet switching
一种做法是在通信开始时申请资源,沿途建立 circuit,结束后释放。Circuit 可以用时隙、频段等资源承载,并不意味着一定要给每个用户铺一根物理线。准入成功后容量更可预测,但保留的份额可能在用户沉默时闲置,而且建立、拆除和故障恢复需要管理对应状态。
另一种做法是有包就发,链路一次为一个 packet 服务,即 packet switching。它在更细的粒度上共享容量,适合突发流量;代价是没有预留时,用户可能同时争用资源,需要排队甚至丢包。每包 header 和调度也有开销。
Circuit switching 也可以在“哪些会话此刻活跃”的层面复用资源;packet switching 则进一步利用一个会话内部的空闲间隔。这里比较的是两种典型设计,reservation 与 packet 的格式并非逻辑上互斥,分组网络也能实现资源预留。
故障恢复同样不同。普通 IP forwarding 可以在路由收敛后让后续包走新路径,不必为每个 flow 重新建立沿途 reservation。它并不保证恢复期间不丢包,也不保证应用不会因超时而终止。
把 reservation 真正实现出来,还会遇到分布式状态管理:沿途前三台 router 已经预留,第四台拒绝怎么办?成功回复丢失后,发送端重试会不会预留两份?发送端突然离线,谁释放那些份额?这些问题需要撤销、去重或租约等机制,资源申请本身也有启动延迟。(UC Berkeley CS 168, 2026c) 如果流很短,建立 circuit 的协调成本可能比传数据本身更显著。
Packet switching 把这些逐 flow 的容量承诺从基本接口里拿掉,让下一包直接使用当前转发表。换路时,主要更新的是共享的路由状态;需要 reservation 的设计还得恢复受影响会话的承诺。这解释了两者在大规模故障下的工作量差别,也解释了为何“更可预测的服务”和“更容易恢复的基础设施”未必由同一种设计同时得到。
平均负载低,仍然可能丢包
继续用三个用户的例子。假设在某个观察时刻,每个人以概率
这只是瞬时超载概率,不是丢包率。是否丢包还取决于超载持续多久、已有 backlog 以及 buffer 大小。独立性也是假设:同一热点事件可以让很多用户同时活跃。
为了吸收短暂超载,可以把来不及发送的 bit 放入 queue。设一个时间步内先加入
它直接表达守恒:上一时刻没发完的,加上新来的,减去能服务的,剩下的不能为负。真实系统的到达与服务连续交错,但同一个收支关系仍然成立。
若输入连续 20 ms 保持 12 Mbit/s,输出为 10 Mbit/s,从空 queue 开始就会积累
突发末尾到达的包需要等待约
因此 queue 能把短期速率不匹配转换为延迟,不能解决长期容量不足。可靠传输负责发现和补回缺失数据,congestion control 则负责控制送入网络的速率。只重传而不控制负载,会把更多工作塞进已经忙不过来的链路。
这也暴露了两个不同的资源问题:buffer 决定暂时存多少,scheduler 决定接下来服务谁。增大 buffer 能吸收更长突发,却也允许更长等待;如果没有限制,前面的积压会持续伤害需要低 latency 的应用。FIFO 按到达顺序服务,也不自动等价于每个用户公平获得容量。
发送端独立选择速率,结果却在共享 queue 里相互影响:A 加速,不仅可能让自己的包丢失,也可能挤掉 B 的包。可靠性解决“哪些数据还欠着”,拥塞控制解决“大家共同能承受多快”,调度解决“拥塞点先发谁”。(UC Berkeley CS 168, 2026d) 把三个目标分开,后面才不会把任何一种 ACK 都误读成容量许可。
包长与链路速率不同,怎样精确算排队?
考虑 A → B → C,两条链路的速率分别为
第二个包要在 B 开始发送,必须同时满足两个条件:它已经完整到达 B;第一个包已经离开 B 的发送接口。前者发生在
后者发生在
所以第二包的发送起点是
在 B 的等待时间便是
即使两个链路速率相同,大包后面紧跟一个小包,也可能有等待:小包很快收齐,但前面的大包还没有发完。因此“没有交叉流量”不足以保证零排队。
推广到多个包,只需反复使用同一条递推:第
如果 C 收齐请求后立即发送响应,RTT 则要另加响应沿返回路径的 serialization 与 propagation。只有路径、速率和包长等条件对称时,才可以直接把单程时间乘二。
为什么 utilization 接近 100% 时,平均延迟可能快速增加?
一个便于计算的参照是 M/M/1 queue:packet 按 Poisson 过程到达,到达率为
相邻状态之间的稳态流量需要平衡:从
把一段长时间内所有包在系统里停留的时间相加,也等于对系统内包数作时间积分。因此在稳定条件下,平均包数等于到达率乘平均停留时间,得到
它包括等待和服务。这里变大的原因是随机突发越来越难被剩余容量消化;不能只用平均输入小于容量,就推出延迟一定小。对于固定包长、相关到达或有限 buffer,具体公式不同。
Layering:每一层承诺什么
发送 bit、在一个局部网络内交付、跨网络交付、给应用提供可靠数据,这些任务可以分开。分层的价值是让上层依赖一个较稳定的 service interface,而不用知道下层每一个实现细节。(UC Berkeley CS 168, n.d.b)
| 层 | 它要解决的问题 | 例子 |
|---|---|---|
| Application,L7 | 数据代表什么操作或内容? | HTTP、DNS |
| Transport,L4 | 数据交给哪个通信端点,需要怎样的交付语义? | TCP、UDP |
| Network,L3 | 怎样跨越多个独立网络找到目的地址? | IP |
| Link,L2 | 怎样在当前链路或局部网络中交付 frame? | Ethernet、Wi-Fi |
| Physical,L1 | 怎样在介质上传输信号? | 电、光、无线电 |
这里保留常用 OSI 层号,所以 L4 后面直接是 L7;session 和 presentation 的功能并非消失了,而是未必由单独的协议层实现。
Protocol 和 interface 也不同。 Interface 是同一台机器上相邻层之间的服务约定;protocol 是不同节点上的同层实体怎样交换消息的约定。后者不仅规定 byte 格式,还规定状态与动作:收到请求、重复包、错误消息或超时,下一步分别做什么。只定义一个 header struct,还没有定义完整协议。
例如 HTTP 客户端逻辑上与服务器的 HTTP 实体对话,但真实 byte 必须从客户端逐层向下、经过介质,再在服务器逐层向上。TCP 只需为上层提供约定的数据流接口,不必理解一份 HTTP 消息表示读取还是上传。这个抽象边界也限制了 TCP 能作出的承诺:它能确认传输的数据,不能替 HTTP 应用确认业务完成。
分层的收益在独立变化时最明显。把 Ethernet 换成 Wi-Fi,应用不必重新设计自己的消息格式;换一个应用,也不必重新造 NIC。不过“下层实现可替换”有前提:上层不能偷偷依赖接口之外的性质,例如假设每次发送立即送达,或把一次 socket write 当成一个接收端可见的消息边界。模块的独立性来自明确的契约,不是来自图上画了五层。
一个包经过 router 时,什么会变化
假设主机 A 通过 Ethernet 把一个 TCP segment 交给 router,再通过另一条链路送往主机 B。应用数据在发送端向下封装:TCP 加入端口与序号等信息,IP 加入地址,Ethernet 再加入当前局部交付需要的 header 和 trailer。
router 先处理入站链路封装,再根据 IP 目的地址选出下一跳,最后添加适用于出站链路的新封装。对一次 IP hop,frame 的目的地址是下一跳接口,不一定是最终服务器;中间如果有多个 L2 switch,它们可以继续在同一个局部网络里转发该 frame。跨 L3 hop 才重新组织相应的链路封装。(UC Berkeley CS 168, n.d.b)
IP header 也不是永远原样保留:例如 IPv4 router 转发时会递减 TTL,并相应更新 IPv4 header checksum。(Baker, 1995) 这里假设没有 NAT、隧道或 proxy,源和目的 IP 地址保持不变。普通转发不需要终止 TCP 连接;router 运行自己的管理和路由协议时,则也会作为通信端点使用高层协议。
到达 B 之后,OS 需要把数据分给正确的 socket。Transport port 是逻辑编号,不是 router 上插网线的物理 port。多个 TCP 连接可以共用服务器的 443 端口,连接还要结合两端地址与端口区分;不能把“端口号”简单等同于某个全局唯一进程。
Header 不只告诉我们去哪里,还告诉我们怎样继续解析
收齐一个 Ethernet frame 后,主机怎么知道里面是 IPv4 还是 IPv6?EtherType 字段选择下一层处理者。选中 IPv4 后,其 Protocol 字段再选择 TCP、UDP 等。选中 TCP 后,OS 才利用地址与端口定位连接。这个过程叫 demultiplexing:发送端把许多上层使用者复用到同一个下层,接收端依据 header 把它们分回来。(UC Berkeley CS 168, n.d.d)
以两个客户端连接同一 HTTPS 服务为例,服务器的目的 IP 和 443 都相同,两个 established TCP socket 仍可由不同的源 IP、源 port 区分。普通 router 只为当前转发需要读取相关字段;终点则继续拆解直到找到正确的上层实体。字段解释必须依照协议推进,不能在未知 payload 中搜索一个“像端口号”的数。
L2 与 L3 的边界也不由物理跳数决定。一个 Ethernet 局部网络可以有多台 switch,frame 在其中经过多条链路;IP next hop 是这个局部网络对面的 router 接口。连接两个独立管理的 Ethernet 网络时,即使技术相同,也可以用 L3 分隔地址管理与策略。反过来,两个 router 直接相连,只是局部交付退化为一条链路。(UC Berkeley CS 168, 2026e)
为什么中间只约定 IP
如果每一种应用都得认识每一种底层链路,两侧的组合会迅速膨胀。让所有参与者共同支持 IP 的交付接口,就可以把问题拆开:新应用建在 IP 之上,新链路提供承载 IP 的方式。
这就是 narrow waist。它用一个共同服务把多种上层和多种下层接起来,而这个共同服务是 best-effort datagram delivery:允许丢失、重排和重复,不承诺固定带宽或交付期限。TCP 可以在这个基础上提供可靠有序 byte stream;UDP 保留 datagram 接口,不替应用实现可靠交付。(UC Berkeley CS 168, n.d.d)
Best-effort 不是“不用认真传”,而是接口不作那些保证。把所有应用都强制放进可靠、有序、固定速率的服务,可能给实时数据增加不需要的等待,也使底层接入门槛变高。相应代价是上层必须承担更多工作。
为什么要支持更多应用,却选择较弱的共同服务?考虑文件上传与实时语音:上传需要补齐每个 byte;语音中已经错过播放期限的 packet,即使后来补齐也未必有用。若底层一律等待缺失数据再交付,上传得到所需语义,语音却被迫承担不想要的等待。若一律承诺低延迟与固定容量,又会排除无法满足这些条件的链路。
较小的共同承诺留下了组合空间:同一 datagram 服务之上,文件可以加上序号、确认和重传;实时应用可以选择有限重传、丢弃迟到数据或调整编码速率。所谓通用性,来自让不同应用构造自己的语义,而非把最严格的所有要求都塞进网络层。(UC Berkeley CS 168, 2026e)
Best-effort 还不承诺“每个失败都会通知发送端”。ICMP 可以提供部分错误反馈,但错误消息也可能丢失或被过滤。一个 send 调用成功,通常只能证明本地系统接受了这次发送,不能证明远处接收。上层必须明确自己怎样确认成功、怎样处理沉默。
共同接口也有升级成本:改变网络层需要多个独立参与者配合。IPv4 和 IPv6 是 IP 的两个版本,不能把“窄腰”理解为现实里只有唯一一种 header。
End-to-End:正确性应在哪个边界成立
文件上传的目标如果是“服务器已经保存了完整文件”,那么逐跳确认是否足够?
即使每条链路都能确认收包,最后一个 router 后面的主机仍可能在写文件时失败。即使 TCP 已经确认 byte 到达,对端应用也可能还没完成校验或持久化。低层确认停在它能观察的边界,不能自动证明更高层的目标。
所以应用需要定义自己的成功条件,例如校验最终内容,并在持久化成功后返回结果。这是 Saltzer、Reed 和 Clark 的 end-to-end argument:如果某个功能的完整正确性需要端点掌握的信息,那么低层实现不能替代端点的责任。(Saltzer et al., 1984)
这并不排斥链路重传。设一个教学模型有 10 跳,每次尝试独立地以概率
要区分三种判断:“低层能否独立把功能做完整”“端点能做时低层是否还必要”“低层实现能否带来值得的性能收益”。它们可能给出不同结论。拥塞点上的优先级调度需要控制那个拥塞点;不能仅因偏爱端到端设计,就假设发送端能替沿途 router 完成调度。
可以把功能放到这三个问题里逐一检查。文件完整性需要覆盖源数据、传输、接收端拷贝与存储;链路 checksum 只覆盖其中一段,最终检查仍由应用承担。无线链路局部重传不消除这项责任,却可能减少昂贵的整条路径重传,因此是否加入要看性能收益。
Priority forwarding 则不同:发送端可以声明某个 packet 很紧急,但只有拥塞接口的 scheduler 能把它排到另一包之前。至于寻路,源主机理论上可以获知拓扑并在包里携带路径;这只是把收集地图、处理变化和路径长度开销转移到别处。把 routing 放在网络内,可以让共享基础设施承担这份工作。End-to-end 是分析信息、控制权与成本的论证,并不能直接推出“所有功能都移到端点”。
另一条相关原则是 fate sharing:把通信赖以继续的关键状态放在依赖它的端点,使中间 router 的故障不必毁掉会话状态。端点若保存未确认数据,换路之后仍有机会继续传输。这里说的是关键会话状态,不是“router 没有状态”;forwarding table、queue 和邻接关系都是状态。Clark 对 Internet 的历史设计分析把故障后的继续通信放在很高的优先级,成本、易接入与资源核算则排在较后。(Clark, 1988)
具体地看,A 已发送 P1,但还没收到确认。若 A 保留 P1 和确认进度,R1 故障使网络改经 R2 后,A 仍可重新发送 P1。如果只有 R1 保存那份无法重建的会话进度,端点虽然都还活着,通信也可能因 R1 的状态消失而无法继续。
这里应区分可由协议重建的路由信息与通信继续所必需且无法从别处恢复的状态。前者丢失后可以重新学习;后者应放在通信依赖的实体上。Fate sharing 减少的是额外的致命故障依赖,并不消除恢复时间,也不把所有应用状态都变成只需内存保存。若应用要求主机崩溃后仍保留文件,它的持久性目标还需要自己的存储设计。
这些选择不是所有时代的唯一答案。防火墙、NAT 和 proxy 反映了运营者的控制需求,也重新引入路径中的状态依赖。理解架构,要同时看它保住了什么性质,以及把哪些责任留给了别人。
目标排序会改变整套架构
Clark 的设计回顾 (Clark, 1988) 先以互联既有网络为总目标,再依次强调故障后继续通信、多种服务、多种网络、分布式管理、成本、易接入与资源核算。这个顺序会影响遇到冲突时牺牲什么:如果故障后的会话连续性优先,就有理由让网络重新寻路、端点保留进度;如果接入异构网络优先,就很难要求每一种链路先承诺相同的可靠性和时延。
换一种排序,答案可能改变。若资源核算优先,系统可能要求每次通信先建立可计费身份与状态;若运营者首先要限制攻击流量,就需要在它能控制的网络边界执行检查。防火墙、NAT、proxy 因此也反映了“谁提出目标、谁有权执行”的变化。原始架构擅长开放互联,不代表它已经内建认证、公平性、隐私或抗攻击保证。
这样看,decentralized control、best-effort、narrow waist 和端点承担可靠性并非几个孤立口号:它们共同缩小全网必须协调的承诺,减少路径上的会话依赖,把应用变化留给端点,同时把负载适配与成功判断交回上层。
补充阅读:从“可靠”追问到具体的成功条件
End-to-End Arguments in System Design 适合带着文件、消息去重和确认边界一起读;本文引用 1984 年期刊版本。Clark 的 1988 年论文 则解释这些选择背后的目标排序。读它们时可以改变应用的成功条件,重新判断哪些工作必须留在端点。
Routing:局部决定如何组成全局路径
现在假设 packet 的目的地址已经确定。每个 router 只做一次 table lookup,就把包交给邻居。但所有 router 的选择拼在一起,真的能把包送到目的地吗?
把网络记为有限图
先试两种不需要表的办法。每到一个 router 就随机选出口,偶尔能到达,但可能反复绕行;复制到所有出口,则可能找到目标,却会在有环网络里不断产生副本,消耗与这次交付无关的容量。要让普通 unicast 的工作量可控,就需要可复用的 forwarding state。
另一个办法是由源主机把完整路径写进 packet,即 source routing。它把每个中间节点的动作交代得很明确,但源端要先获得新鲜路径,header 也随路径增长。在这里使用的 destination-based 模型中,包只需带目的地,router 自己决定下一跳。(UC Berkeley CS 168, 2026f) 代价是整条路径由许多局部决定共同拼成,不能只验证源端选择。
注意该模型的限制:在固定表下,同一个 router 对去往同一目的地的包作出相同选择,不看源地址、此前经过哪里或应用类型。后面“路径相遇后合并”的性质正来自这项限制;真实系统的策略路由、ECMP 或隧道需要另行说明。
先谈有效,再谈最短
如果 A 认为 B 知道路,而 B 认为 A 知道路,两个局部表项看起来都合法,合起来却产生 loop。如果某个节点没有下一跳,就形成 dead end。到达目的节点后正常停止不算 dead end。
在固定拓扑、固定单下一跳表、无传输丢失的模型中,对能到达
把每个节点指向自己的 next hop,得到一棵指向
因此控制平面要解的并非“替 A 找一条路”这么小的问题,而是替同一目的地协调一组表:A 相信 B,B 必须真的能把包继续交出去。对一个全部可达、包含
更进一步,若 A 把通往 D 的包交给 B,A 后面的路径就被 B 的选择决定。对于可加代价的最短路,后缀也必须是从 B 到 D 的最短路,否则替换后缀就能改善 A 的路径。这种 optimal substructure,才使“先付到邻居的 cost,再接上邻居的距离”成为可能。
有效路径不一定代价最小。假设下面所有边双向、代价对称,D 是目的节点。
A 可以直接以代价 7 到 D,也可以经 B、C,以
例如一条两跳路径的容量是 100 与 10 Mbit/s,它不会因相加而拥有 110 Mbit/s 的持续吞吐;没有交叉流量时,瓶颈容量至多为
一个能排除环的量
设
由于 cost 严格为正,沿每一跳都有
但这个证明用的是同一张图上的真实距离。故障期间,不同 router 保存的可能是不同时间的估计;不能看到每台机器都执行“最短路算法”,就认定整个系统此刻无环。
Control plane 与 data plane
Data plane 负责把当前 packet 按表转发,工作发生在每次收包时。Control plane 负责发现邻居、交换可达性、计算路由并更新表,时间尺度由事件与定时器驱动。静态路由由管理员配置,directly connected route 可以从接口配置获得,动态路由再传播其他可达性。
这个分离不只是代码分类。以 100 Gbit/s 接口、1,000 byte packet 为例,平均每 80 ns 就可能到来一个包,不能每次都先询问全网拓扑再求最短路。快速路径读取当前已安装的结果;路由进程在后台通过消息更新它。代价是故障到新表安装之间,data plane 仍可能按旧状态工作。
路由消息本身也经过会丢失、会延迟的链路。邻居发现与邻接通信提供学习的起点,不需要先拥有完整的远端路由;随后协议必须处理消息未到达、邻居重启和旧报告。Convergence 表示拓扑停止变化、通信与处理满足相应条件后,表项最终达到兼容状态;它不等于故障发生的瞬间仍始终有效,也不等于永远不再发送周期消息。
这里主要讨论 intra-domain routing,一个管理者可以约定共同 metric。跨管理域的 inter-domain routing 还涉及商业关系和出口策略,不能简单化成全球统一最短路。BGP 属于后一类问题的主要协议,也能用于域内场景;它的 path-vector 思想与下面的 distance-vector 有联系,但选择目标不同。
Distance Vector:只告诉邻居“我离目的地多远”
如果 A 不知道整张图,能不能仍然作出正确决定?假设每个邻居愿意报告它到 D 的距离。A 要经 B 去 D,必须先支付 A–B 的代价,再承担 B 后面的路程。对所有邻居比较,便得到 Bellman equation:
这不是要求 A 拿到完整路径;A 只需要邻居的距离,以及自己到邻居的 cost。每个目的地重复一次,就是一个 distance vector。Distributed Bellman–Ford 让 router 不断交换估计并更新它们。(UC Berkeley CS 168, n.d.b)
在同一张图上算一遍
先用同步轮次方便观察:初始化时只有 D 知道自己的距离为 0,其他节点为
| 轮次 | A | B | C | D |
|---|---|---|---|---|
| 0 | 0 | |||
| 1 | 7,直达 D | 5,直达 D | 2,直达 D | 0 |
| 2 | 6,经 B | 3,经 C | 2,直达 D | 0 |
| 3 | 4,经 B | 3,经 C | 2,直达 D | 0 |
例如第二轮 A 比较
同步轮次 0 1 / 4
只有 D 知道自己距离为 0。虚线是物理链路;箭头表示当前选择的 next hop。
完整静态演算记录
| 步骤 | 状态(cost / via) | 解释 |
|---|---|---|
| 1:同步轮次 0 | A=∞/—;B=∞/—;C=∞/—;D=0/— | 只有 D 知道自己距离为 0。虚线是物理链路;箭头表示当前选择的 next hop。 |
| 2:同步轮次 1 | A=7/D;B=5/D;C=2/D;D=0/— | D 的 0 传到直接邻居:A=7,B=5,C=2。数据箭头指回 D。 |
| 3:同步轮次 2 | A=6/B;B=3/C;C=2/D;D=0/— | B 使用 C 上一轮的 2,得到 1+2=3;A 只能使用 B 上一轮的 5,得到 6。 |
| 4:同步轮次 3 | A=4/B;B=3/C;C=2/D;D=0/— | A 收到 B 的 3,得到 1+3=4。信息向外传播,数据沿 A→B→C→D 交付。 |
为什么这种迭代能得到最短路?初始化只知道零条边的路径。做一次邻居扩展,就能考虑多一条边的路径。因此第
真实网络没有全局轮次。消息可能延迟、乱序或丢失,router 收到更新就可以计算。静态拓扑下,从上述初始化出发,若邻居更新最终持续得到传递、节点持续处理,信息仍会逐步传开;但“
为什么不能只接受更短的路
现在让 C–D 断开。B 原来选择 C,距离为 3。如果只接受更小的值,C 报告自己已不可达时,B 会因为
因此必须保存 route 的来源:当前 next hop 的更新,即使变差,也必须影响当前路线。 一种完整状态模型是保存每个邻居对每个目的地的最新有效报告
这样原路线变差时,还能比较缓存的其他候选路线。节省状态的教学实现可以只存当前最佳 next hop、cost 和 expiry time:收到更优路线就替换,收到当前 next hop 的报告则接受变好或变差;换路时未缓存的候选要等邻居再次通告。两种实现不要混成同一套存储假设。
消息本身也可能丢失,所以只在变化时发送还不够:triggered update 缩短等待,periodic update 提供再次传播的机会。若一直没有收到支撑当前路线的新报告,就让 route 过期;这个 route timer 与数据包的 IP TTL 是两个不同机制。
过期后若只静默删除,依赖自己的邻居还会继续转发一段时间。可以主动通告该目的地距离为
坏消息为什么会兜圈子
另看一个更小的网络:D–A–B,两条边 cost 都是 1,没有其他出口。收敛后 A 到 D 为 1,B 经 A 到 D 为 2。
假设没有 split horizon,且断链消息尚未送达 B。D–A 断开后,A 收到 B 先前发出的“我到 D 的距离是 2”,于是把 B 当作备用出口,更新为 3。但 B 的那条路本来就依赖 A。
| 事件 | A 的估计 | B 的估计 | 实际发生的事 |
|---|---|---|---|
| 故障前 | 1,经 D | 2,经 A | 可以到达 |
| A 失去 D,收到 B 的旧报告 | 3,经 B | 2,经 A | A–B 成环 |
| B 收到 A 的 3 | 3,经 B | 4,经 A | 距离增加,环未消失 |
| A 收到 B 的 4 | 5,经 B | 4,经 A | 继续自我支撑 |
问题不在加法,而在报告缺少路径依赖信息。“B 有一条长为 2 的路线”,没有告诉 A 这条路线经过 A 自己。于是旧信息可以被不断加工成新通告,形成 count to infinity。
这就是“好消息快、坏消息慢”的来源。新出现的较短路径提供一个外部事实,每次传播都可以改善估计;路径消失后,残留的有限估计却可能互相支撑。在这个二节点例子里,每次通告都带一个更新后的数,所以 periodic update 不能自动解决问题;它甚至持续给错误依赖续命。Timeout 能发现沉默,不能识别一条一直有人通告却自我循环的路线。
Split horizon 与 poison reverse
既然 B 的路线来自 A,就没有理由把它作为 A 的备选出口。Split horizon 让 B 不再向 A 通告这条路线。Poison reverse 更明确:B 仍向 A 通告,但报告
这与 route poisoning 不同:前者是按接收邻居调整通告,B 自己仍有路;后者是自己的 route 已失效。显式 poison reverse 还能比静默等待 expiry 更快地拆掉某些已有的二节点相互依赖。
但它们没有携带完整路径。若依赖是 A 经 B、B 经 C、C 经 A,每个人只对自己的 next hop 报
RIP 把 metric 16 定义为不可达,允许的有限 hop count 最多为 15。这样坏消息计数到一个有限上限就停止;它同时限制了能表示的路径长度。16 是 RIP 的工程取舍,不是所有 DV 协议共享的数学常数,更不是 packet TTL。
故障前 1 / 17
D—A—B,两条链路 cost=1。B 经 A 到 D,距离为 2。
完整静态演算记录
| 步骤 | 状态(cost / via) | 解释 |
|---|---|---|
| 1:故障前 | D=0/—;A=1/D;B=2/A | D—A—B,两条链路 cost=1。B 经 A 到 D,距离为 2。 |
| 2:A 检测到 D—A 断开 | D=0/—;A=∞/—;B=2/A | A 已经失去直达路线;B 尚未收到坏消息,仍保留旧路线。 |
| 3:B→A 通告 2 | D=0/—;A=3/B;B=2/A | A 得到 1+2=3,选择 B。报告仍然依赖接收者自己,距离增加却没有新出口。 |
| 4:A→B 通告 3 | D=0/—;A=3/B;B=4/A | B 得到 1+3=4,选择 A。报告仍然依赖接收者自己,距离增加却没有新出口。 |
| 5:B→A 通告 4 | D=0/—;A=5/B;B=4/A | A 得到 1+4=5,选择 B。报告仍然依赖接收者自己,距离增加却没有新出口。 |
| 6:A→B 通告 5 | D=0/—;A=5/B;B=6/A | B 得到 1+5=6,选择 A。报告仍然依赖接收者自己,距离增加却没有新出口。 |
| 7:B→A 通告 6 | D=0/—;A=7/B;B=6/A | A 得到 1+6=7,选择 B。报告仍然依赖接收者自己,距离增加却没有新出口。 |
| 8:A→B 通告 7 | D=0/—;A=7/B;B=8/A | B 得到 1+7=8,选择 A。报告仍然依赖接收者自己,距离增加却没有新出口。 |
| 9:B→A 通告 8 | D=0/—;A=9/B;B=8/A | A 得到 1+8=9,选择 B。报告仍然依赖接收者自己,距离增加却没有新出口。 |
| 10:A→B 通告 9 | D=0/—;A=9/B;B=10/A | B 得到 1+9=10,选择 A。报告仍然依赖接收者自己,距离增加却没有新出口。 |
| 11:B→A 通告 10 | D=0/—;A=11/B;B=10/A | A 得到 1+10=11,选择 B。报告仍然依赖接收者自己,距离增加却没有新出口。 |
| 12:A→B 通告 11 | D=0/—;A=11/B;B=12/A | B 得到 1+11=12,选择 A。报告仍然依赖接收者自己,距离增加却没有新出口。 |
| 13:B→A 通告 12 | D=0/—;A=13/B;B=12/A | A 得到 1+12=13,选择 B。报告仍然依赖接收者自己,距离增加却没有新出口。 |
| 14:A→B 通告 13 | D=0/—;A=13/B;B=14/A | B 得到 1+13=14,选择 A。报告仍然依赖接收者自己,距离增加却没有新出口。 |
| 15:B→A 通告 14 | D=0/—;A=15/B;B=14/A | A 得到 1+14=15,选择 B。报告仍然依赖接收者自己,距离增加却没有新出口。 |
| 16:A→B 通告 15 | D=0/—;A=15/B;B=∞/— | B 的候选达到 RIP 的不可达值 16,记为 ∞。这是 metric 上限,不是 packet TTL。 |
| 17:B→A 通告 ∞ | D=0/—;A=∞/—;B=∞/— | A 收到 ∞,也撤销依赖 B 的路线。二者都已不可达。 |
Poison reverse 对照:B 向 A 报 ∞,A 没有可用备用路线;A 再向 B 报 ∞ 后,B 也撤销路线。它只排除这次二节点相互依赖,不保证任意拓扑无环。
试着先播放普通 DV,再切换到 poison reverse:两边的物理故障完全相同,改变的只是 B 向 A 声明什么。这个对照说明 poison reverse 不是在数据包经过环时补救,而是在建立路由依赖时阻止一类错误备选。
同一故障中有三个不同的“寿命”:route timer 约束本地报告多久算新鲜;metric 上限限制坏消息计数多远;IP TTL/Hop Limit 限制一个已经进入环的 packet 最多继续转发多少跳。TTL 最终让包停止,仍可能有新的包不断进入同一坏路,因而它不能替代路由收敛。
Triggered update 为什么还需要记住“向谁说过什么”
有新消息就把整张表发出去,容易形成额外工作:相同的报告从邻居传来,本地结果没有变,却继续触发所有邻居发送。周期发送负责修补丢失;事件发送应该聚焦真正改变的通告。可以缓存“这个目的地上次向这个邻居报告的值”,只在将要发送的值变化时触发增量更新。(UC Berkeley CS 168, 2026g)
缓存必须按邻居区分。假设 B 到 D 的 cost 保持 3,但 next hop 从 C 换为 A:B 向 A 原先可以报 3,现在使用 poison reverse 必须报 ∞;向 C 原先报 ∞,现在可以报 3。只比较本地 cost 会漏掉这次变化。新邻居加入时也没有旧历史可依赖,需要先发送它应知道的当前状态。
这类优化必须放在正确性机制之后:direct route 提供起点,邻居更新修正估计,重发处理丢消息,expiry 处理沉默,poison 与按邻居改写处理部分坏依赖,再考虑如何减少重复流量。完整路径信息能识别更多依赖,但会付出更大的状态与通告开销,这也为后续 path-vector 留下了动机。
把机制装回一个事件驱动的 router
对每个目的地维护 next hop、cost 与过期时间,并钉住自己的直连信息。按以下事件处理。(UC Berkeley CS 168, 2026g)
- 收到通告:加上到发送邻居的链路 cost。若没有有效路线、候选更优,或消息来自当前 next hop,则更新相应状态;按协议规则处理不可达值和有效报告的 timer。
- 链路失效或路线过期:使受影响路线失效,触发 poison 通告;若保存了有效备选,可以重新选择。
- 路线改变:尽快触发更新。向每个邻居分别应用 split horizon 或 poison reverse,不能把同一份未处理的向量无条件发给所有邻居。
- 定时重发:即使没有新变化,也给丢失的消息再次传播的机会。过期、保留 poison 和最终垃圾回收是不同阶段。
这是一份机制级描述。RIP 的 timer、触发更新节流和垃圾回收见 RFC 2453,不能用一个无限保留所有旧信息的 cache 代替协议规定的 freshness 管理。
Link State:先共享地图,再各自算路
DV 隐藏了路径细节。如果每个 router 改为通告“我和谁相邻、代价是多少”,所有节点最终就能拼出拓扑。然后每个节点独立计算最短路,把第一跳写入自己的 forwarding table。这是 link-state 的基本分工。(UC Berkeley CS 168, n.d.e)
怎样获得一张不会被旧消息覆盖的地图
先用 hello 或接口状态发现邻居;若长期失联,按协议规则宣告邻接变化。接着把本地 link state 发给邻居,邻居再继续扩散,即 flooding。
直接转发每个收到的副本会在有环图里不断放大消息,因此需要让一个节点知道“这个版本我已经处理过了”。给通告加入 origin 和该 origin 的 sequence number,接收方保存已知版本,只在收到更新版本时替换并继续传播。不同 origin 的 sequence number 不需要互相比较,也不需要一只全网同步的时钟。
然而“只传播新版本”也带来问题:某个邻居丢了第一次发送,后面重复包又被抑制,它如何补齐?因此可靠 flooding 还需要确认、重传或数据库同步;周期刷新和 age 则处理长期失效信息。OSPF 的 LSA 具有类型、标识、发布者、sequence number、checksum 和 age 等字段,邻接建立时也有数据库交换。(Moy, 1998) 它比“给每条边配一个全局时间戳”更具体。
在图中,B 既从 A 收到通告,也从 C 收到同一个通告。保留物理冗余有利于故障恢复,因此不能只为消除重复就要求物理拓扑无环;接收方需要在协议状态里记住已经看过的版本。无丢失的简单模型中,每个节点对一个版本最多扩散一次,发送量因而与链路数同阶;实际重传与数据库同步另计。
版本信息也不能从 DV 直接照搬过来。LS 报告是某个 origin 对自己邻接的事实,具有明确的发布者;DV 的“我到 D 有多远”是多台 router 报告加工后的估计。给 B 的估计加上 B 自己递增的序号,并不会告诉 A 这条估计是否依赖 A,因而不会单独消除 count to infinity。
重启与序号回绕同样不能忽略:如果节点重启后从 0 开始,而别人保存着旧的大序号,新消息会被误判为旧版本。实际协议必须规定状态恢复、老版本清除与版本生命周期。
Dijkstra 为什么每次固定最小的候选
有了地图,从 A 开始求最短路。初始只有 A 的距离确定为 0;它的邻居得到候选距离 B=1、D=7。现在选候选距离最小的 B。为什么 B 可以直接定下来?
任何尚未探索的替代路径,都得先离开已经确定的集合。如果它的第一处未确定节点的候选距离已经不小于 B,后面再加非负 cost,也不可能绕一圈得到更便宜的 B。于是最小候选可以固定,然后用它去改善邻居的候选距离。
| 刚固定的节点 | B 的候选 | C 的候选 | D 的候选 |
|---|---|---|---|
| A,距离 0 | 1,经 A | 7,经 A | |
| B,距离 1 | 已固定 | 2,经 B | 6,经 B |
| C,距离 2 | 已固定 | 已固定 | 4,经 C |
| D,距离 4 | 已固定 | 已固定 | 已固定 |
最终 A 到 D 的路径仍是 A–B–C–D,第一跳为 B。前驱用于还原路径;forwarding table 不必保存整条路径。
固定 A,距离 0 1 / 4
Relax 邻接边:B=0+1=1;D=0+7=7。实心节点已固定;箭头表示从 A 出发的前驱树方向,不是所有节点去 D 的转发表。
完整静态演算记录
| 步骤 | 状态(cost / via) | 解释 |
|---|---|---|
| 1:固定 A,距离 0 | A=0/—;B=1/A;C=∞/—;D=7/A | Relax 邻接边:B=0+1=1;D=0+7=7。实心节点已固定;箭头表示从 A 出发的前驱树方向,不是所有节点去 D 的转发表。 |
| 2:固定 B,距离 1 | A=0/—;B=1/A;C=2/B;D=6/B | Relax 邻接边:C=1+1=2;D=1+5=6。实心节点已固定;箭头表示从 A 出发的前驱树方向,不是所有节点去 D 的转发表。 |
| 3:固定 C,距离 2 | A=0/—;B=1/A;C=2/B;D=4/C | Relax 邻接边:D=2+2=4。实心节点已固定;箭头表示从 A 出发的前驱树方向,不是所有节点去 D 的转发表。 |
| 4:固定 D,距离 4 | A=0/—;B=1/A;C=2/B;D=4/C | 所有节点已固定。实心节点已固定;箭头表示从 A 出发的前驱树方向,不是所有节点去 D 的转发表。 |
两种树的方向要分清:DV 图固定目的地 D,画每台 router 去 D 的 next hop;这里固定源 A,Dijkstra 一次算出 A 到所有节点的前驱树。A 要安装去 C 或 D 的下一跳,沿前驱回溯到 A 后取路径上的第一个邻居 B,而不是把 D 的前驱 C 当成 A 的直接下一跳。其他 router 要以自己为源运行相应计算。
用邻接表和 binary heap,一次单源计算的常见复杂度为
地图一致,不代表更新同时完成
假设 A–B cost 为 1,B–D 为 1,A–D 为 5。故障前 A 经 B 到 D;B–D 断开后,B 先获知故障,选择经 A 到 D。但 A 还在用旧表,把 D 的包交给 B,于是暂时出现 A–B loop。
B 在新图上、A 在旧图上,各自都算对了最短路,但它们拼起来不对。即使控制数据库已经收敛,不同时间安装新 forwarding table 也可能产生类似的 microloop。收敛至少经历故障检测、信息传播、本地计算和表项安装,不能只计 Dijkstra 的运行时间。
在统一拓扑、共同 metric 和正 cost 下,每个合法最短路 next hop 都使真实剩余距离下降,因此不会有环。相同 tie-breaking 有利于得到可复现的单路径选择;但对这个正权条件下的无环证明,并不要求所有节点以同一方式打破平局。
若支持 equal-cost multipath(ECMP),一个目的地可以对应多个合法 next hop,delivery tree 相应推广成 DAG。这里仍要求每条可选边都使距离严格下降;只有“每个节点觉得这条路不错”不够。零代价边会破坏严格下降,需要额外约束。
| 比较维度 | Distance Vector | Link State |
|---|---|---|
| 传播的信息 | 到目的地的距离估计 | 本地邻接及其状态 |
| 每个节点知道什么 | 邻居报告,通常没有完整路径 | 所在范围的拓扑数据库 |
| 本地计算 | 经各邻居比较候选路线 | 在地图上运行最短路算法 |
| 变化如何扩散 | 邻居重新估计,再逐级通告 | 先 flood 状态,再独立计算 |
| 主要困难 | 陈旧依赖、计数到无穷、失效传播 | 可靠 flooding、版本生命周期、更新期间不一致 |
LS 不会因为“共享地图”就变成集中式算法,也不是没有环、没有协议状态的免费方案。两类方法最终都要在有限带宽、消息丢失与网络变化之间维护可用的路由。
一个更有用的比较是共享事实,还是共享计算结果:LS 先把本地事实扩散,再让每个人计算;DV 让每个人把自己的计算结果交给邻居继续使用。前者花更多状态保存地图,后者需要面对估计的隐藏依赖。LS 可以在各处并行重算,但完成时间仍受传播与安装影响;DV 的某些更新很快,某些失效却可能经过许多轮计数。不能把“LS 一定快、DV 一定慢”当成与拓扑和实现无关的结论。
这些地图和向量也都有作用范围。让每台 router 保存全球细节,不仅有内存与更新成本,也会要求运营者公开内部拓扑。自治边界与地址聚合把细节限制在较小范围,正好连接到下面的层级寻址。(UC Berkeley CS 168, n.d.e)
补充阅读:从抽象算法走到协议
CS 168 的 DV 教材 可以用来逐项复查上面的事件规则;再读 RIP v2,RFC 2453 §3,看 timer、poison 和 triggered update 如何协同。
OSPF v2,RFC 2328 §13、§16 分别展开 flooding 和路由计算。这里需要带着两个不同问题读:数据库如何保持新鲜,以及从数据库怎样导出可安装的路由。它们不是同一个算法步骤。
Addressing:不要让每台 router 记住每台主机
到目前为止,A、B、C、D 都是短标签。如果把每个真实接口都当作独立目的地,forwarding table 会随所有目的地增长,每次加入或离开也可能带来远处更新。一个自然的压缩办法是:去往同一个区域、具有相同转发动作的目的地,共用一条记录。
因此地址最好带有可聚合的结构。名字回答“我想找谁”,地址提供网络交付所需的定位信息;DNS 负责名字到地址等记录的解析。IP 地址通常绑定接口,一个设备可以有多个地址;服务也能通过多个地址提供,不应把地址当成永不变化的个人身份。
先用 区域.主机 这样的两级标签思考。外部 router 看 4.* 就把数据交给区域 4 的入口;进入区域后,内部 router 再区分 4.1、4.2。若 4.6 离开,而区域 4 的入口与聚合仍有效,外部的 4.* 不需要跟着变化。这既压缩表项,也隐藏内部 churn。(UC Berkeley CS 168, n.d.a)
假设有
收益来自地址与转发方向的共同结构。如果一个前缀中的主机随机散布到不同出口,仅仅把它们编号为连续整数,也不能凭空合并动作。主机移动后换用新网络的地址,有利于聚合;若要保留原地址,就可能需要例外路由、隧道或其他定位机制。身份稳定性与位置聚合是两个目标。
只有一个上游出口的末梢主机还可以进一步压缩:保留当前 subnet 的直连判断,其余目的地使用 default route。它不需要知道世界上每个远端前缀;上游会作更细的决定。默认路由是在局部把“其余目的地使用同一动作”写成一条记录,并不代表所有 router 都能只用默认项。
从固定边界到 CIDR
IPv4 地址有 32 bit。如果固定用前 8 bit 表示网络,剩下 24 bit 表示网络内部位置,最多只能划出 256 种前缀,每个块又很大。Classful addressing 曾用不同类别提供不同尺寸:Class A、B、C 的传统边界分别是 /8、/16、/24,其开头的类别 bit 决定解释方式。
但一个需要约 450 个接口地址的网络,/24 太小,/16 又浪费很多。CIDR 直接在地址旁标明 prefix length,允许边界落在任意 bit,而不是只在预设类别之间选择。一个 /23 有
例如 192.0.2.0/23 固定前 23 bit,netmask 为 255.255.254.0,覆盖 192.0.2.0 到 192.0.3.255。地址
其中
聚合需要地址对齐,也需要转发语义一致
192.0.2.0/24 和 192.0.3.0/24 的前 23 bit 相同,刚好是同一个 /23 的两个子块;在相同转发动作下,可以用一条 /23 代替。192.0.3.0/24 与 192.0.4.0/24 虽然数字相邻,却不是同一个 /23 的两个子块,不能按同样方法合并。
地址分配因此常呈层级:较大地址块向下分配给运营者和站点,站点再分配给 subnet。外部只需知道聚合前缀,内部的每次主机变化便不必传播到外部。不过,前缀、物理地点和 Autonomous System 不是一一对应的;多出口和策略会产生例外。CIDR 的作用同时包括减少地址浪费和支持路由聚合。(Fuller & Li, 2006)
为什么用 longest prefix match
假设一张 forwarding table 如下:
| 目的前缀 | 动作 |
|---|---|
0.0.0.0/0 | 默认出口 |
192.0.2.0/23 | 出口 P |
192.0.3.0/24 | 出口 Q |
192.0.3.42/32 | 出口 R |
192.0.2.8 走 P;192.0.3.9 走 Q;192.0.3.42 走 R。它们都匹配默认路由,但更具体的范围表达了对较大聚合的修正,故采用 longest prefix match(LPM),即固定 bit 最多的匹配项。
这允许一个站点在大聚合之外,因 multihoming 再发布更具体的路线。代价是更具体前缀增多,聚合压缩效果下降。LPM 是 data-plane lookup 规则;同一前缀的多个候选 route 怎样按策略、metric 选择,是 control-plane 问题。前缀更长也不等于路径更短。
192.0.3.42 → 出口 R(匹配中选择 /32)
| 前缀 | 动作 | 本次 lookup |
|---|---|---|
| 0.0.0.0/0 | 默认出口 | 匹配 |
| 192.0.2.0/23 | 出口 P | 匹配 |
| 192.0.3.0/24 | 出口 Q | 匹配 |
| 192.0.3.42/32 | 出口 R | 选中 |
可以输入不同地址,再撤回 /24 观察结果。地址 192.0.3.42 同时匹配四项,选择 /32;改成 192.0.3.9,它不再匹配 /32,于是选择 /24;撤回 /24,相同地址才回落到 /23。没有特例时复用聚合,有特例时用更细范围覆盖,这就是 LPM 与聚合能共存的原因。
二进制表示让对齐条件更直观:第三字节 2 是 00000010,3 是 00000011,共享前 7 bit;加上前两个字节的 16 bit,得到 /23。3 与 4(00000100)不共享同样的前 7 bit,不能因十进制相邻就合并为一个恰好覆盖二者的 /23。
聚合还需要处理“洞”。如果 P 宣告整个 /23,但其中某个 /24 已没有有效内部路线,不能盲目把这些包再按默认路由送回提供该聚合路线的上游,否则可能循环。发布聚合的 router 需要相应的 discard/reject 行为,让没有更具体有效路线覆盖的聚合内部地址停止转发;不能把宣告大块范围当成对其中每个地址都可达的证明。
IPv6 改变了容量,没有取消层次
IPv6 把地址扩展到 128 bit,以十六进制组表示。例如 2001:db8:0:1::42,其中 :: 压缩一段连续的全零组,同一地址中只能这样压缩一次。(Hinden & Deering, 2006) 前缀仍然用 /n 表示,最长前缀匹配和层次聚合仍有意义。它也有自己的 packet header 和 Hop Limit;不能只把 IPv4 地址字段直接拉长。(Deering & Hinden, 2017)
常见 IPv6 LAN 使用 /64。SLAAC 下,主机可以根据 router advertisement 提供的可用前缀与本地生成的 interface identifier 形成地址,并进行 duplicate address detection。它并不让主机随意挑选一个全球可路由前缀;地址空间巨大也不取消分配与可达性管理。(Thomson et al., 2007)
过渡的困难来自协议与部署:端点、沿途网络和应用支持必须配合。Dual stack 让节点同时运行 IPv4、IPv6;tunneling 可以把一种协议承载在另一种上;NAT64 等机制则在特定场景中进行转换。(Bagnulo et al., 2011) 转换存在,并不意味着两个协议天然兼容或所有通信都能无条件透明转换。
练习:聚合之后怎样处理例外?
若上表中的 /24 被撤回,192.0.3.9 会回落到 /23,走 P。192.0.3.42 仍走 R,因为 /32 还在。
这只是 lookup 的答案。P 是否真的能把包交付,要继续看它内部的可达性;如果没有有效路线而只有覆盖该地址的本地 discard aggregate,包会被丢弃。把“匹配到了一个前缀”和“端到端一定可达”分开,才能正确分析故障。
Traceroute:从返回的错误消息观察路径
我们已经知道 forwarding table 决定下一跳,但普通主机看不到沿途的表。能否只发一些 packet,间接知道它们走到了哪里?
IPv4 TTL 在转发时减少,耗尽后 router 丢弃 packet,通常发回 ICMP Time Exceeded。于是可以让第一个 probe 的 TTL=1,在第一跳到期;第二组设为 2,在第二跳到期;再逐渐增大。每次回复的外层源 IP 提供一个回应接口的地址,往返时间来自 probe 发出到对应回复到达的时间。(Baker, 1995)
经典 UDP traceroute 向目的端一个预计未开放的端口发送 probe。若抵达目的端并得到 ICMP Destination Unreachable / Port Unreachable(IPv4 type 3、code 3),就可以识别终点。其他 unreachable code 可能表示沿途失败,不能一概视为成功抵达。TCP 或 ICMP probe 的结束条件不同。CS 168 前四周的 Traceroute 项目还要求处理延迟、重复和无关回复。(UC Berkeley CS 168, 2026i)
| Probe TTL | 理想路径 A → R1 → R2 → B 的响应 |
|---|---|
| 1 | R1 的 Time Exceeded |
| 2 | R2 的 Time Exceeded |
| 3 | B 的 Port Unreachable,假设该 UDP 端口关闭 |
为了把回复归属到正确 probe,不能只看回复源地址。ICMP 错误会引用触发错误的原始 IP header 和一部分 payload;解析其中的协议、目的地址和 UDP 端口等字段,可以匹配本轮 probe。IPv4 header 长度由 IHL 指定,不能把“总是 20 byte”写死。还要先验证实际收到的长度与消息类型,忽略截断、重复、其他目的地以及前一轮迟到的回复。
一个稳健的探测循环是在每个 TTL 下发送带可识别信息的 probes,接收至本轮 deadline,只记录匹配且尚未记录的响应,然后再进入下一轮。用 deadline 而非“必须凑齐三个回复”,才能在丢包时结束等待;也不能让任意迟到消息重置无限等待。
怎样读一次 traceroute,而不从输出推断过多?
在自己的网络环境可以运行 traceroute -n example.com,或使用系统提供的等价工具。输出需要谨慎解释:
*表示在等待期限内没有收到匹配回复。可能是过滤、限速、回复丢失或不回应,不直接证明 data plane 无法转发;后面仍可能出现正常回复。- 每一行是独立 probes 的结果,路径可能随时间或 flow hash 变化,不一定能把每行任取一个地址连成一条真实路径。
- 每个时间是到该响应点再返回的 RTT,回复未必沿原路回来。相邻行 RTT 相减,不能直接当作那条链路的传播延迟,甚至可能得到负数。
- ICMP 回应地址不一定就是你想象中的入站接口地址;看到的是协议暴露出的观察结果,而不是完整物理拓扑。
这些限制不使 traceroute 失去价值,反而规定了它能支持哪些判断。可以结合重复测量、终点可达性和本机路由信息继续定位问题。
把一个包的旅程接起来
现在回到开头的文件传输。应用通过名字找到地址,选定 transport endpoint,把数据交给传输层。传输层按自身语义组织数据;IP 为跨网络交付提供共同格式;每一跳的 link layer 再为当前局部传输封装 frame。
链路发送耗费 serialization 和 propagation 时间,共享链路还会排队。router 用当前 forwarding table 选择下一跳;这些表来自静态配置、直连信息和路由协议。DV 交换距离估计,LS 交换邻接状态,二者都必须面对陈旧信息。层级地址把许多目的地折叠成前缀,而更具体的前缀保留必要例外。
到达服务器之后,网络交付的成功还要继续向上解释:byte stream 完整了吗,文件校验通过了吗,内容持久保存了吗?这里自然接上后续的 reliable transport、congestion control 和应用协议。想先看这些机制如何影响 QUIC、WebRTC 与 RPC,可以继续读现代通信协议。
课程覆盖与继续阅读
这里的周次按 CS 168 Fall 2026 calendar 核对;部分 PDF 沿用旧版页眉,因此下面用 FA26 发布的讲次定位,不把其每页文字当作新的协议标准。
| 课程范围 | 本文对应内容 |
|---|---|
| Week 1,Lecture 1:Architecture and Protocols | 互联与自治、规模与故障、protocol、best-effort |
| Week 2,Lecture 2:Bottom-up | packet、链路时延、BDP、forwarding/routing、资源共享 |
| Week 2,Lecture 3:Top-down | queue、可靠性与拥塞、layering、端口、封装 |
| Week 3,Lecture 4:Architectural Principles | end-to-end、fate sharing、目标排序与边界 |
| Week 3,Lecture 5:Routing Principles | 图模型、有效状态、delivery tree、least-cost、域内与域间 |
| Week 4,Lecture 6:Distance-Vector | Bellman–Ford、当前 next-hop 更新、timer、poison、split horizon、计数到无穷、逐邻居增量通告 |
| Week 4,Lecture 7:Link-State, Addressing | flooding、Dijkstra、一致性、层级地址、classful/CIDR、LPM、IPv6 |
| Discussions 1–3;Project 1A/1B;Project 2 的增量通告 | header 变化、架构取舍、时延与复用算例、逐事件 DV、Traceroute 与异常回复 |
延伸推导包括分包流水线、突发积压、M/M/1 的假设、距离下降的无环证明、LS 更新期间的 microloop,以及聚合空洞。这里停在网络架构与基础路由;Week 5 以后的 router hardware、BGP 细节和 TCP 算法尚未展开。