网络层数据平面:路由器内部结构、IP协议、广义转发、SDN

目录

  1. 复习题解答(R1~R35)
  2. 习题详解(P1~P24)
  3. 附录:C++ 代码演示

一、复习题解答

Section 4.1

R1. 网络层数据包叫什么?路由器和链路层交换机的本质区别是什么?

  • 网络层数据包叫做数据报(datagram)
    • 传输层叫:报文段(segment)
    • 链路层叫:帧(frame)
  • 路由器 vs 链路层交换机:

对比维度路由器(Router)链路层交换机(L2 Switch)
工作层次网络层(第3层)链路层(第2层)
转发依据IP地址MAC地址
转发表路由/转发表(按IP前缀)MAC地址表(按MAC地址)
能否跨网段通信能不能(只在同一网段内)

R2. 数据平面和控制平面的主要功能分别是什么?

平面主要功能实现方式
数据平面(Data Plane)每个路由器内部:决定从哪个输入端口到来的包从哪个输出端口转发出去——即"转发"硬件实现(纳秒级)
控制平面(Control Plane)网络范围:决定数据包从源到目的经过哪条路径——即"路由"软件实现(毫秒级)

简单记忆:数据平面管"怎么走这一步",控制平面管"走哪条路"。
R3. 转发(Forwarding)和路由(Routing)的关键区别?

转发(Forwarding):
  - 局部操作,发生在单个路由器内部
  - 查转发表,决定从哪个输出端口发出
  - 时间尺度:纳秒级(硬件完成)
  - 类比:在十字路口看路牌决定往哪个方向走
路由(Routing):
  - 全局操作,涉及整条路径的规划
  - 运行路由算法(如OSPF、BGP),生成转发表
  - 时间尺度:秒级(软件计算)
  - 类比:出发前规划从北京到上海走哪条高速

R4. 转发表在路由器中的作用?
转发表是路由器进行目的地转发的核心数据结构:

  • 存储"目的IP前缀 → 输出端口"的映射关系
  • 路由器收到数据包后,用目的IP地址查转发表,找到对应输出端口,将包送入交换结构
  • 使用最长前缀匹配规则:当多条前缀都匹配时,选最长(最具体)的那条
    R5. 互联网网络层的服务模型是什么?提供什么保证?
    互联网网络层采用**尽力而为(Best-Effort)**服务模型:
  • 不保证数据包的按序到达
  • 不保证数据包不丢失
  • 不保证端到端时延的上界
  • 不保证最小带宽
    换句话说:互联网网络层什么都不保证,可靠性全部交给上层(TCP)来处理。

Section 4.2

R6. 路由器各组件哪些用硬件实现,哪些用软件实现?

组件实现方式原因
输入端口(Input Port)硬件需要在纳秒级完成查表和转发决策,只有硬件才够快
输出端口(Output Port)硬件同上,高速排队和发送需要硬件
交换结构(Switching Fabric)硬件要在极短时间内将包从输入搬到输出
路由处理器(Routing Processor)软件运行路由协议、管理转发表,对速度要求相对低

对应到数据平面/控制平面:

  • 数据平面(转发)→ 硬件
  • 控制平面(路由计算)→ 软件
    R7. 为什么高速路由器的每个输入端口都存一份转发表的副本(影子拷贝)?
    目的:避免每次查表都访问中央路由处理器,消除瓶颈。
不用影子拷贝的情况:
  输入端口1 ──查表──→ 中央路由处理器 ──→ 结果
  输入端口2 ──查表──→ 中央路由处理器 ──→ 结果  (串行,慢!)
  输入端口N ──查表──→ 中央路由处理器 ──→ 结果
用影子拷贝的情况:
  输入端口1 ──查本地表──→ 结果(无需访问中央处理器)
  输入端口2 ──查本地表──→ 结果(并行,快!)
  输入端口N ──查本地表──→ 结果

每个输入端口保存一份完整转发表的副本,可以并行、独立地完成转发决策,大幅提升吞吐量。
R8. 什么是目的地转发?它与广义转发有何不同?SDN采用哪种?

对比维度目的地转发广义转发
匹配字段只看目的IP地址可以匹配多个字段(源IP、目的IP、端口号、MAC、入端口……)
动作只能转发到某个端口可以转发、丢弃、修改字段、发给控制器等
灵活性低高
实现复杂度低高

SDN 采用广义转发(OpenFlow 流表)。

R9. 数据包匹配到多条转发表规则时,用什么规则决定?
传统目的地转发:使用最长前缀匹配(Longest Prefix Match)。

  • 选择前缀位数最多(最具体)的那条规则
  • 例:目的IP 200.23.18.5 同时匹配 /20 和 /23,选 /23

R10. 三种交换结构及其并行能力?

交换结构 Switching Fabric

经内存交换
Switching via Memory

经总线交换
Switching via Bus

经交叉开关交换
Switching via Crossbar

工作方式: 包存入内存
再由CPU读出发送

并行能力: 不能
内存总线是瓶颈

工作方式: 共享总线
每次只有一个包通过

并行能力: 不能
总线同一时刻只传一个

工作方式: N×N交叉点矩阵
不同输入输出对独立连接

并行能力: 能
不冲突的包可同时传输

只有**交叉开关(Crossbar)**可以在同一时刻并行传输多个不冲突的数据包。
R11. 输入端口如何发生丢包?如何消除?
发生原因: 输入端口队列满,新到达的包无处存放,被丢弃。
更隐蔽的原因——HOL(队头阻塞):

输入端口1队列:[包A→输出3][包B→输出2][包C→输出1]
                    ↑
         包A要去输出3,但输出3被占用
         包B虽然输出2空闲,但被包A挡住,无法发送
         → 包B、C白白等待 = HOL阻塞

消除方法(不用无限缓冲):
使用交叉开关并配合合理的调度策略(如输入排队调度),可以让每个输入端口的包独立向不同输出端口发送,大幅减少甚至消除 HOL 阻塞。
R12. 输出端口如何发生丢包?提升交换结构速度能否防止?
发生原因: 当多个输入端口同时向同一个输出端口发包时,短时间内到达的包超过了输出链路的发送能力,缓冲区溢出。
提升交换结构速度能防止吗?
不能。 交换结构再快,也无法改变输出链路的带宽上限——如果5个输入端口同时各自发来一个满速包,它们都想从同一个输出端口出去,输出端口每次只能发一个,剩下的必须排队,缓冲区终究会满。
R13. HOL阻塞是什么?发生在输入端口还是输出端口?
HOL(Head-of-Line Blocking,队头阻塞) 发生在输入端口:

  • 队列头部的包因目标输出端口被占用而无法前进
  • 它身后的包即使目标输出端口空闲,也被挡住无法发送
  • 导致输入端口带宽利用率严重下降(理论上最坏只有 1 − 1 / e ≈ 58 % 1 - 1/e \approx 58\% 1−1/e≈58%)
    R14. 哪种调度能保证包按到达顺序离开?
    只有 FIFO(先进先出) 保证包严格按到达顺序离开。
  • 优先级队列、轮询(RR)、WFQ 都可能改变离开顺序。
    R15. 举例说明为什么需要给某类包更高优先级?
    例: 企业网络中同时运行视频会议和文件备份:
  • 视频会议是实时流量,延迟超过 150ms 就会出现卡顿,严重影响体验
  • 文件备份是弹性流量,传得慢一点完全可以接受
    若不区分优先级,大量文件备份流量可能将视频包堵在队列里,导致视频通话质量急剧下降。
    设置优先级后,视频包优先发送,保证实时性;文件备份包在空闲时再传,两全其美。
    R16. RR和WFQ的本质区别是什么?何时行为相同?

对比轮询(RR)加权公平队列(WFQ)
服务顺序各类别轮流各发一个包各类别按权重比例分配带宽
灵活性每个类别得到等量服务权重高的类别得到更多带宽

何时行为完全相同: 当所有类别的 WFQ 权重相等时(如都为1),WFQ 退化为 RR,行为完全一致。

Section 4.3

R17. Host B 如何知道把 IP 载荷交给 TCP 而不是 UDP?
靠 IPv4 数据报头部的协议号(Protocol)字段:

  • 值为 6 → 交给 TCP
  • 值为 17 → 交给 UDP
  • 值为 1 → 交给 ICMP
    这个字段就是网络层和传输层之间的"胶水"。
    R18. IP头部哪个字段可以限制包最多经过N个路由器?
    TTL(Time-to-Live,存活时间)字段:
  • 发送方设置 TTL = N
  • 每经过一个路由器,TTL 减 1
  • TTL 减到 0 时,路由器丢弃该包并返回 ICMP 错误消息
  • 这样包最多经过 N 个路由器
    R19. 传输层校验和和IP层校验和是否覆盖相同的字节?
    不覆盖相同的字节:
  • IP 头部校验和:只校验 IP 头部(20字节),不包含数据载荷
  • TCP/UDP 校验和:校验 整个 TCP/UDP 报文段(头部+数据),外加一个伪头部(包含源/目的IP地址等)
    两者唯一的"公共部分"是伪头部中的IP地址字段,但伪头部本身不是IP数据报的一部分,只是计算校验和时临时使用的。
    因此两个校验和几乎不覆盖完全相同的字节。
    R20. 大数据报分片后,在哪里重组?
    在目的主机处重组,不在中间路由器处重组。
  • IPv4 允许中间路由器分片,但重组只在目的端进行
  • IPv6 根本不允许中间路由器分片(源端如果发现包太大,必须自己分;路由器直接丢包并返回"Packet Too Big")
    R21. 路由器有IP地址吗?有几个?
    有。路由器每个接口(interface)都有一个IP地址。
    一台有 N 个接口的路由器就有 N 个 IP 地址。(IP地址与接口绑定,不与设备绑定。)
    R22. 223.1.3.27 的32位二进制是什么?
223  = 11011111
  1  = 00000001
  3  = 00000011
 27  = 00011011
结果:11011111 00000001 00000011 00011011

R23. DHCP 获取信息(概念说明)
DHCP 客户端通常可以获取并查看以下信息(以 Linux 为例用 ip addr 和 ip route 命令查看):

信息项示例值
IP 地址192.168.1.105
子网掩码255.255.255.0 (/24)
默认网关(第一跳路由器)192.168.1.1
本地 DNS 服务器地址192.168.1.1(或运营商DNS如 8.8.8.8)

R24. 经过3个路由器,包经过多少个接口和转发表?

源主机 → 路由器1 → 路由器2 → 路由器3 → 目的主机
接口(interfaces):
  源主机出口 = 1
  路由器1入口 + 出口 = 2
  路由器2入口 + 出口 = 2
  路由器3入口 + 出口 = 2
  目的主机入口 = 1
  总计:1 + 2 + 2 + 2 + 1 = 8 个接口
转发表查询次数:
  每个路由器查一次 → 3 次(源主机和目的主机不查转发表)

R25. 数据报中头部开销占比是多少?
应用数据:40 字节
IP头部:20 字节,TCP头部:20 字节,总头部:40 字节
总数据报大小 = 40 + 40 = 80  字节 \text{总数据报大小} = 40 + 40 = 80 \text{ 字节} 总数据报大小=40+40=80 字节
头部占比 = 40 80 = 50 % \text{头部占比} = \frac{40}{80} = 50\% 头部占比=8040​=50%
应用数据占比 = 40 80 = 50 % \text{应用数据占比} = \frac{40}{80} = 50\% 应用数据占比=8040​=50%
R26. 无线路由器后面5台电脑的IP如何分配?用NAT吗?

  • ISP 给无线路由器分配一个公网IP(通过DHCP)
  • 无线路由器内置DHCP服务器,给5台电脑各分配一个私有IP(如 192.168.1.x)
  • 无线路由器使用NAT:5台电脑共享那一个公网IP与外网通信
    使用 NAT 的原因:ISP 只给了一个公网IP,但5台设备都需要访问互联网,NAT 用端口号区分各设备的连接。
    R27. 路由聚合(Route Aggregation)是什么?为什么有用?
    路由聚合(也叫地址聚合/路由汇总): 将多个连续的小地址块合并成一个大地址块来宣告。
不聚合时(8条路由):
  200.23.16.0/23
  200.23.18.0/23
  200.23.20.0/23
  ...(共8条)
聚合后(1条路由):
  200.23.16.0/20 → 涵盖上述所有8个/23块

好处:

  • 减少互联网骨干路由器的路由表条目数量(路由表爆炸是互联网扩展性的重大挑战)
  • 加快路由查找速度
  • 减少路由更新消息的传播
    R28. 什么是"即插即用"或"零配置"协议?
    即插即用(Plug-and-Play)/ 零配置(Zeroconf)协议 = 设备接入网络后不需要人工配置,自动完成所有网络参数的获取。
    DHCP 是典型代表:设备插上网线(或连接WiFi),自动获取IP地址、子网掩码、默认网关、DNS服务器,无需管理员手动配置。
    R29. 什么是私有网络地址?私有地址应该出现在公共互联网上吗?
    私有地址: 仅在内部网络(如家庭网、企业网)内有意义的IP地址,全球有无数网络使用相同的私有地址段:

私有地址块用途
10.0.0.0/8大型企业内网
172.16.0.0/12中型网络
192.168.0.0/16家庭/小型网络

不应该出现在公共互联网上。 因为:

  • 全球有无数网络都在用 192.168.1.x,路由器无法区分哪个是哪个网络的 192.168.1.1
  • 私有地址在互联网上不可路由,含私有地址的包会被丢弃
    NAT 的职责就是在包进出边界时替换私有地址和公网地址。
    R30. IPv4 和 IPv6 头部字段对比

字段IPv4IPv6备注
版本号4位,值=44位,值=6相同位置,不同值
服务类型/流量类别TOS(8位)Traffic Class(8位)功能类似
TTL / 跳数限制TTL(8位)Hop Limit(8位)功能相同,名字不同
协议号 / 下一头部Protocol(8位)Next Header(8位)功能相同
源/目的地址各32位各128位IPv6大幅扩展
头部长度有(4位)无(固定40字节)IPv6固定头部,不需要
总长度有(16位,含头部)负载长度(16位,不含头部)计算方式不同
校验和有无IPv6删除,提升速度
分片字段有无(端系统分片)IPv6删除,提升速度
可选项有(可变长)无(改用Next Header链)IPv6固定头部
流标签无有(20位)IPv6新增

R31. IPv6 通过IPv4隧道传输时,是否把IPv4当作链路层协议?
同意这个说法。
原因类比:

  • 在正常协议栈中,链路层为网络层提供传输服务(如以太网帧承载IP包)
  • 在IPv6隧道中,IPv4为IPv6提供传输服务:IPv6整个数据报被封装在IPv4的数据字段里
正常情况:
  [以太网帧头] + [IPv4数据报] = 链路层承载网络层
隧道情况:
  [IPv4头部] + [IPv6数据报] = IPv4承载IPv6
                                ↑
                          相当于IPv4在这里扮演了"链路层"的角色

IPv6 节点看到的是:把 IPv4 当作一条"虚拟链路",对端是隧道出口的 IPv6 节点。

Section 4.4

R32. 广义转发与目的地转发的区别?
见 R8,简要重述:

  • 目的地转发:只匹配目的IP,只能转发
  • 广义转发:匹配多个字段(跨多个协议层),可以执行丰富的动作(转发、丢弃、修改字段……)
    R33. 传统转发表与 OpenFlow 流表的区别?

对比传统转发表OpenFlow 流表
匹配字段只有目的IP前缀12个字段(IP、MAC、端口号……)
动作只有"转发到端口X"转发、丢弃、修改字段、发给控制器
计数器通常没有每条规则有匹配计数和时间戳
来源路由算法在本地计算远程控制器集中计算并下发
灵活性低高(可实现路由、防火墙、NAT、负载均衡…)

R34. 匹配+动作操作是什么?目的地转发中匹配什么、动作是什么?SDN中呢?
匹配+动作(Match-Plus-Action): 对数据包头部字段进行匹配,根据匹配结果执行对应动作。
目的地转发:

  • 匹配:目的IP地址(最长前缀匹配)
  • 动作:转发到指定输出端口
    SDN(OpenFlow)可匹配的三个字段举例:
  • 源IP地址
  • 目的IP地址
  • 入端口(Ingress Port)
    SDN 可执行的三个动作举例:
  • 转发到指定端口
  • 丢弃数据包
  • 修改头部字段(如改写目的IP,用于NAT)
    R35. OpenFlow 1.0 中可以匹配和不可以匹配的IP头部字段各举3个?
    可以匹配(3个):
  1. 源IP地址(IP Src)
  2. 目的IP地址(IP Dst)
  3. IP协议号(IP Protocol)
    不可以匹配(3个):
  4. TTL(存活时间)
  5. 数据报总长度(Datagram Length)
  6. 头部校验和(Header Checksum)

二、习题详解

P1. 路由器A的转发表

题意: 有一个网络拓扑,需要设计路由器A的转发表。
a. 所有去往H3的流量走接口3:

转发表(简化):
┌────────────────────┬──────────┐
│  目的地址/前缀      │  出接口   │
├────────────────────┼──────────┤
│  H3的IP前缀        │   3      │
│  (其他)           │  默认接口 │
└────────────────────┴──────────┘

b. H1→H3走接口3,H2→H3走接口4——能做到吗?
不能!这是陷阱题。
传统目的地转发只看目的IP地址(H3的IP),不看源地址,因此无法区分"来自H1"还是"来自H2"——相同目的地只能对应一个出接口。
要实现这个需求,必须用广义转发(OpenFlow),同时匹配源IP和目的IP。

P2. 交换结构并行传输分析

场景: 两个包同时到达不同输入端口。
a. 共享总线(Shared Bus),目的是不同输出端口——能并行?
不能。 共享总线同一时刻只能被一个包使用,两个包必须串行通过总线。
b. 经内存交换(Switching via Memory),目的是不同输出端口——能并行?
不能。 经内存交换需要 CPU 将包从输入端口读入内存,再写到输出端口,内存总线是瓶颈,同样是串行操作。
c. 交叉开关(Crossbar),目的是相同输出端口——能并行?
不能。 即使是交叉开关,同一个输出端口在同一时刻只能接收一个输入,两个包都要去同一出口,产生竞争,只能串行。(如果是不同输出端口,交叉开关才能并行。)

P3. 不同交换结构的最大输入排队延迟

条件: 交换速率 R s w i t c h = R l i n e R_{switch} = R_{line} Rswitch​=Rline​(即交换结构速度等于线路速度),所有包目的是不同输出端口,包传输时间为 D D D。
a. 经内存交换(Memory):
内存总线是瓶颈,同一时刻只能处理一个包。N个输入端口中最坏情况是所有包排队等待前面的包处理完,最大排队延迟:
D m a x = ( N − 1 ) ⋅ D D_{max} = (N-1) \cdot D Dmax​=(N−1)⋅D
b. 共享总线(Bus):
与内存类似,总线同一时刻只能传一个包:
D m a x = ( N − 1 ) ⋅ D D_{max} = (N-1) \cdot D Dmax​=(N−1)⋅D
c. 交叉开关(Crossbar):
当所有包目的是不同输出端口时,交叉开关可以并行处理,不需要等待:
D m a x = 0 D_{max} = 0 Dmax​=0
(无排队延迟)

P4. 时隙调度(HOL vs 最优)

题意: 交叉开关,同步时隙,每时隙传一个包,求最少和最多时隙数。
通用分析框架(以常见教材例题为例):

假设输入端口和目标输出端口的映射如下(教材给出具体图):
最优调度(无HOL约束,自由选择):
  - 每个时隙,选择不冲突的包集合(每个输出端口至多被一个包选中)同时传输
  - 利用匈牙利算法或贪心方法求最小时隙数
最差调度(故意制造最多等待):
  - 每个时隙只传一个包,其余全部等待
  - 最多时隙数 = 包的总数

最少时隙数的计算原则: 每个时隙选出尽量多的不冲突包(每个输出端口最多一个)同时传输,直到所有包传完。

P5. WFQ 权重 0.5、0.25、0.25 的服务顺序

a. 三个类别都有大量包时,WFQ的服务顺序?
权重比为 0.5 : 0.25 : 0.25 = 2 : 1 : 1 0.5 : 0.25 : 0.25 = 2:1:1 0.5:0.25:0.25=2:1:1,即类别1获得 50 % 50\% 50% 带宽,类别2和3各获得 25 % 25\% 25%。
可能的服务序列:1 2 1 3 1 2 1 3 …(类别1每2个时隙服务一次,类别2和3各每4个时隙服务一次)
或更精确地:1 1 2 3 1 1 2 3 … 等价序列,只要长期比例满足 2 : 1 : 1 2:1:1 2:1:1 即可。
b. 只有类别1和2有包(类别3队列空),服务顺序?
权重按有包的类别重新归一化: w 1 = 0.5 , w 2 = 0.25 w_1 = 0.5, w_2 = 0.25 w1​=0.5,w2​=0.25,比值 = 2 : 1 = 2:1 =2:1。
服务序列:1 2 1 1 2 1 1 2 … 或等价地 1 1 2 1 1 2 …
类别3的份额被"回收",当类别3没有包时,WFQ会立即跳过它(work-conserving原则)。

P6. 四种调度策略对比(时隙分析)

本题要求对给定的包到达时间图,分别计算FIFO、优先级、轮询、WFQ下各包的离队时间和延迟。
解题框架(适用于所有四种调度):

已知条件(教材图):
  - 12个包,每个包占用1个时隙传输
  - 每个包有到达时间(从图中读取)
  - 链路同一时刻只能传一个包
通用步骤:
  Step1: 列出每个包的到达时刻
  Step2: 按调度策略,逐时隙决定哪个包被发送
  Step3: 记录每个包的发送开始时刻(= 离队时刻)
  Step4: 计算延迟 = 发送开始时刻 - 到达时刻
  Step5: 求平均延迟

关键结论(e小问):
无论采用哪种调度策略(FIFO、RR、优先级、WFQ),所有12个包的平均延迟都相同!
原因:调度策略只改变包的服务顺序,不改变"总工作量"。在单服务器模型中,总处理时间固定,平均等待时间由总负载决定,与调度顺序无关。调度策略影响的是不同类别包之间的公平性和延迟分布,不影响整体平均。

P8. 最长前缀匹配转发表(32位地址)

给定目的地址范围,需要找出匹配的CIDR前缀:
接口0: 地址范围 11100000 00000000 ... 到 11100000 00111111 ...

起始: 11100000 00000000 00000000 00000000
终止: 11100000 00111111 11111111 11111111
前18位相同:11100000 00(后面不同)
→ 前缀:11100000 00 = 224.0.0.0/10 ?
更仔细分析:
起始到终止,第3字节从00000000到00111111(即0~63),
第3字节前2位固定为00,后6位可变
→ 精确前缀:11100000 00 xxxxxx = 224.0.0.0/10
验证:
  11100000 00 = 前10位
  a.b.c.d/10 notation: 224.0.0.0/10
接口0前缀:224.0.0.0/10

接口1: 11100000 01000000 00000000 00000000 到 11100000 01000000 11111111 11111111

第1、2字节固定,第3字节 = 01000000(固定),第4字节可变
→ 前24位固定:11100000 01000000 01000000
→ 前缀:224.64.64.0/24 ? 
重新看:
  11100000 01000000 = 前16位(224.64)
  第3字节: 00000000(固定)
  → 前24位固定: 11100000 01000000 00000000
  → 接口1前缀:224.64.0.0/24

接口2: 11100000 01000001 00000000 00000000 到 11100001 01111111 11111111 11111111

这个范围跨越了224.65.x.x 到 225.127.x.x,需要多个前缀覆盖,
或使用一个较短前缀覆盖(精确分析在实际题目图中确认)。

a. 转发表(使用 a.b.c.d/x 记法):

前缀(点分十进制/x)接口
224.0.0.0/100
224.64.0.0/241
224.64.1.0/242(部分)
(其余)3

b. 判断三个目的地址走哪个接口:

11001000 10010001 01010001 01010101 = 200.145.81.85
  → 前缀不匹配接口0、1、2 → 接口3
11100001 01000000 11000011 00111100 = 225.64.195.60
  → 前11位: 11100000 0 匹配/10 ? 
    11100001 ≠ 11100000 → 不匹配接口0
  → 检查是否匹配接口2的范围 → 属于接口2
11100001 10000000 00010001 01110111 = 225.128.17.119
  → 不匹配接口0、1、2 → 接口3

P9. 8位地址空间,最长前缀匹配

给定前缀表:

前缀接口
000
0101
0111
102
113

各接口对应的地址范围(8位地址):
接口0(前缀 00):

00000000 ~ 00111111 = 0 ~ 63,共 64 个地址

接口1(前缀 010 或 011):

010xxxxx = 64 ~ 95(32个)
011xxxxx = 96 ~ 127(32个)
共 64 个地址(64 ~ 127)

接口2(前缀 10):

10000000 ~ 10111111 = 128 ~ 191,共 64 个地址

接口3(前缀 11):

11000000 ~ 11111111 = 192 ~ 255,共 64 个地址

P10. 8位地址,另一组前缀表


前缀接口
10
101
1112
otherwise3

分析(最长前缀匹配优先):
接口3(otherwise,即以 0 开头的地址):

0xxxxxxx = 0 ~ 127,共 128 个地址

接口0(前缀 1,但需排除被更长前缀覆盖的部分):

1xxxxxxx 共 128 ~ 255
但其中 10xxxxxx 被接口1覆盖,111xxxxx 被接口2覆盖
→ 接口0只剩:100xxxxx 和 110xxxxx
  100xxxxx = 128 ~ 159(32个)
  110xxxxx = 192 ~ 223(32个)
  共 64 个地址

接口1(前缀 10):

10xxxxxx = 128 ~ 191(排除被更长前缀覆盖的)
但 10 只有2位,接口1前缀最长,无需再排除
→ 128 ~ 191,共 64 个地址
(注意:接口0的1前缀比接口1的10短,所以10xxxxxx匹配接口1)

接口2(前缀 111):

111xxxxx = 224 ~ 255,共 32 个地址

汇总:

接口地址范围数量
0128~159, 192~223(前缀1,排除10和111)64
1128~191(前缀10)64
2224~255(前缀111)32
30~127(otherwise)128

P11. 三个子网的地址分配

前提: 都使用前缀 223.1.17.0/24,在此基础上划分子网。
需求:子网1 ≥ 60接口,子网2 ≥ 90接口,子网3 ≥ 12接口。
/24 有8位主机位,共 2 8 = 256 2^8 = 256 28=256 个地址。
子网2(最大需求90,需要至少7位主机位, 2 7 − 2 = 126 ≥ 90 2^7-2=126 \geq 90 27−2=126≥90):
223.1.17.0 / 25 (主机位7位,可用126个地址,满足90) 223.1.17.0/25 \quad \text{(主机位7位,可用126个地址,满足90)} 223.1.17.0/25(主机位7位,可用126个地址,满足90)
子网1(需求60,需要至少6位, 2 6 − 2 = 62 ≥ 60 2^6-2=62 \geq 60 26−2=62≥60):
剩余地址从 223.1.17.128 开始:
223.1.17.128 / 26 (主机位6位,可用62个地址,满足60) 223.1.17.128/26 \quad \text{(主机位6位,可用62个地址,满足60)} 223.1.17.128/26(主机位6位,可用62个地址,满足60)
子网3(需求12,需要至少4位, 2 4 − 2 = 14 ≥ 12 2^4-2=14 \geq 12 24−2=14≥12):
剩余地址从 223.1.17.192 开始:
223.1.17.192 / 28 (主机位4位,可用14个地址,满足12) 223.1.17.192/28 \quad \text{(主机位4位,可用14个地址,满足12)} 223.1.17.192/28(主机位4位,可用14个地址,满足12)
答案汇总:

子网地址可用主机数
子网2223.1.17.0/25126 ≥ 90
子网1223.1.17.128/2662 ≥ 60
子网3223.1.17.192/2814 ≥ 12

P14. 子网划分

给定: ISP 拥有 128.119.40.64/26,要划分4个等大小子网。
/26 有6位主机位,共 2 6 = 64 2^6 = 64 26=64 个地址,划分4个子网:
每个子网 64 / 4 = 16 64/4 = 16 64/4=16 个地址,即 /28(4位主机位)。
子网1:  128.119.40.64 / 28 ( 64 ∼ 79 ) \text{子网1: } 128.119.40.64/28 \quad (64 \sim 79) 子网1: 128.119.40.64/28(64∼79)
子网2:  128.119.40.80 / 28 ( 80 ∼ 95 ) \text{子网2: } 128.119.40.80/28 \quad (80 \sim 95) 子网2: 128.119.40.80/28(80∼95)
子网3:  128.119.40.96 / 28 ( 96 ∼ 111 ) \text{子网3: } 128.119.40.96/28 \quad (96 \sim 111) 子网3: 128.119.40.96/28(96∼111)
子网4:  128.119.40.112 / 28 ( 112 ∼ 127 ) \text{子网4: } 128.119.40.112/28 \quad (112 \sim 127) 子网4: 128.119.40.112/28(112∼127)

P17. MP3 分片数计算

条件: 最大数据报 1500 字节,IP头 20 字节,MP3 文件 5,000,000 字节。
每个数据报最大载荷: 1500 − 20 = 1480 1500 - 20 = 1480 1500−20=1480 字节
需要的数据报数 = ⌈ 5,000,000 1480 ⌉ = ⌈ 3378.38 ⌉ = 3379  个数据报 \text{需要的数据报数} = \left\lceil \frac{5{,}000{,}000}{1480} \right\rceil = \left\lceil 3378.38 \right\rceil = 3379 \text{ 个数据报} 需要的数据报数=⌈14805,000,000​⌉=⌈3378.38⌉=3379 个数据报

P18. NAT 转换表

条件: 路由器公网IP 24.34.112.235,内网 192.168.1.0/24,5台主机,每台2个TCP连接到 128.119.40.86:80。
a. 内网地址分配(示例):

设备内网IP
路由器内网接口192.168.1.1
主机1192.168.1.2
主机2192.168.1.3
主机3192.168.1.4
主机4192.168.1.5
主机5192.168.1.6

b. NAT转换表(6条,每台主机2个连接,只列3台示例):

内网 IP:端口公网 IP:端口
192.168.1.2:300124.34.112.235:5001
192.168.1.2:300224.34.112.235:5002
192.168.1.3:300124.34.112.235:5003
192.168.1.3:300224.34.112.235:5004
192.168.1.4:300124.34.112.235:5005
192.168.1.4:300224.34.112.235:5006

P20. OpenFlow 流表设计(s2)

需求:

  • 接口1来自h5/h6,目的h1/h2 → 从接口2发出
  • 接口2来自h1/h2,目的h5/h6 → 从接口1发出
  • 接口1或2,目的h3 → 接口3
  • 接口1或2,目的h4 → 接口4
  • h3和h4互通
s2 流表:
┌────────────────────────────────────────────────┬──────────────┐
│  匹配条件                                        │  动作         │
├────────────────────────────────────────────────┼──────────────┤
│  入端口=1, 源IP=10.3.*.*, 目的IP=10.1.*.*       │  转发(接口2)  │
│  入端口=2, 源IP=10.1.*.*, 目的IP=10.3.*.*       │  转发(接口1)  │
│  目的IP=10.2.0.3                                │  转发(接口3)  │
│  目的IP=10.2.0.4                                │  转发(接口4)  │
└────────────────────────────────────────────────┴──────────────┘

P23. 防火墙流表设计(四种情形)

情形1:只允许h1和h6的流量到达h3/h4,阻断h2/h5

┌──────────────────────────────────────────────┬──────────────┐
│  源IP=10.1.0.1(h1), 目的IP=10.2.0.3          │  转发(接口3)  │
│  源IP=10.1.0.1(h1), 目的IP=10.2.0.4          │  转发(接口4)  │
│  源IP=10.3.0.6(h6), 目的IP=10.2.0.3          │  转发(接口3)  │
│  源IP=10.3.0.6(h6), 目的IP=10.2.0.4          │  转发(接口4)  │
│  (其余:无规则 → 丢弃)                        │              │
└──────────────────────────────────────────────┴──────────────┘

情形2:只允许TCP流量到达h3/h4,阻断UDP

┌──────────────────────────────────────────────┬──────────────┐
│  协议=TCP, 目的IP=10.2.0.3                    │  转发(接口3)  │
│  协议=TCP, 目的IP=10.2.0.4                    │  转发(接口4)  │
│  (UDP等:无规则 → 丢弃)                       │              │
└──────────────────────────────────────────────┴──────────────┘

情形3:只允许到h3的流量,阻断到h4的所有流量

┌──────────────────────────────────────────────┬──────────────┐
│  目的IP=10.2.0.3                              │  转发(接口3)  │
│  目的IP=10.2.0.4                              │  丢弃(无动作)│
└──────────────────────────────────────────────┴──────────────┘

情形4:只允许h1发给h3的UDP流量,其他全阻断

┌──────────────────────────────────────────────┬──────────────┐
│  源IP=10.1.0.1(h1), 协议=UDP,目的IP=10.2.0.3 │  转发(接口3)  │
│  (其余:无规则 → 丢弃)                        │              │
└──────────────────────────────────────────────┴──────────────┘

P24. ICMP 是网络层协议还是传输层协议?

ICMP(Internet Control Message Protocol)的层次位置:
ICMP 应该被认为是网络层协议,理由如下:

  1. 封装位置: ICMP 消息被封装在 IP 数据报的数据字段中(协议号 = 1),和 TCP(6)、UDP(17)一样,是 IP 数据报的载荷
  2. 但与TCP/UDP不同: ICMP 不为应用程序提供服务,而是为 IP 协议本身提供差错报告和控制功能(如 TTL 过期、目的不可达、Ping 功能)
  3. 功能归属: ICMP 服务于网络层的操作和诊断,不提供进程到进程的通信
从协议栈角度看:
  [应用层]  HTTP, SMTP, FTP...
  [传输层]  TCP, UDP ← 为应用提供进程间通信
  [网络层]  IP + ICMP ← ICMP为IP服务,同属网络层
  [链路层]  以太网, WiFi...

结论: ICMP 虽然从"封装关系"看像传输层(在IP之上),但从"功能"角度看,它服务于网络层,是网络层协议的一部分。

三、附录:C++ 代码演示

WFQ 调度模拟与延迟计算(对应 P5/P6)

#include <iostream>
#include <vector>
#include <queue>
#include <string>
#include <iomanip>
#include <algorithm>
#include <numeric>
// ─────────────────────────────────────────────────────────────────────────────
// 数据包描述
// ─────────────────────────────────────────────────────────────────────────────
struct Packet {
    int id;          // 包编号(1~12)
    int arriveSlot;  // 到达时隙
    int classId;     // 所属类别(0 或 1)
};
// ─────────────────────────────────────────────────────────────────────────────
// FIFO 调度模拟
// 返回每个包的发送开始时隙(即离队时隙)
// ─────────────────────────────────────────────────────────────────────────────
std::vector<int> simulateFIFO(const std::vector<Packet>& pkts) {
    // 按到达时间排序(FIFO:谁先到谁先走)
    std::vector<Packet> sorted = pkts;
    std::sort(sorted.begin(), sorted.end(),
              [](const Packet& a, const Packet& b) {
                  return a.arriveSlot < b.arriveSlot;
              });
    std::vector<int> departSlot(pkts.size());
    int currentSlot = 0;  // 当前链路空闲的时隙
    for (const auto& p : sorted) {
        // 如果链路空闲时间 < 包到达时间,等到包到达才开始传输
        currentSlot = std::max(currentSlot, p.arriveSlot);
        // 记录这个包在原始数组中的位置
        departSlot[p.id - 1] = currentSlot;
        currentSlot++;  // 传输一个包需要一个时隙
    }
    return departSlot;
}
// ─────────────────────────────────────────────────────────────────────────────
// 优先级队列调度模拟
// classId=0 为高优先级,classId=1 为低优先级
// ─────────────────────────────────────────────────────────────────────────────
std::vector<int> simulatePriority(const std::vector<Packet>& pkts) {
    std::vector<int> departSlot(pkts.size(), -1);
    int currentSlot = 0;
    // 找到所有包中最大到达时间,作为模拟终止条件
    int maxArrival = 0;
    for (const auto& p : pkts) maxArrival = std::max(maxArrival, p.arriveSlot);
    // 两个优先级队列(高优先级和低优先级)
    std::queue<int> highQ, lowQ;  // 存包的索引
    int pktIdx = 0;               // 下一个要检查是否到达的包
    // 按到达时间排序
    std::vector<Packet> sorted = pkts;
    std::sort(sorted.begin(), sorted.end(),
              [](const Packet& a, const Packet& b) {
                  return a.arriveSlot < b.arriveSlot;
              });
    int processed = 0;
    int slot = 0;
    while (processed < (int)pkts.size()) {
        // 将当前时隙到达的包加入对应队列
        for (auto& p : sorted) {
            if (p.arriveSlot == slot && departSlot[p.id - 1] == -1) {
                if (p.classId == 0) highQ.push(p.id);
                else                lowQ.push(p.id);
            }
        }
        // 选择最高优先级的包发送
        int toSend = -1;
        if (!highQ.empty()) {
            toSend = highQ.front(); highQ.pop();
        } else if (!lowQ.empty()) {
            toSend = lowQ.front(); lowQ.pop();
        }
        if (toSend != -1) {
            departSlot[toSend - 1] = slot;
            processed++;
        }
        slot++;
        if (slot > maxArrival + (int)pkts.size()) break;  // 安全退出
    }
    return departSlot;
}
// ─────────────────────────────────────────────────────────────────────────────
// 打印调度结果
// ─────────────────────────────────────────────────────────────────────────────
void printResult(const std::string& name,
                 const std::vector<Packet>& pkts,
                 const std::vector<int>& departSlots) {
    std::cout << "\n=== " << name << " ===\n";
    std::cout << std::setw(6) << "包ID"
              << std::setw(10) << "到达时隙"
              << std::setw(10) << "发送时隙"
              << std::setw(10) << "延迟" << "\n";
    std::cout << std::string(36, '-') << "\n";
    double totalDelay = 0;
    for (const auto& p : pkts) {
        int delay = departSlots[p.id - 1] - p.arriveSlot;
        totalDelay += delay;
        std::cout << std::setw(6) << p.id
                  << std::setw(10) << p.arriveSlot
                  << std::setw(10) << departSlots[p.id - 1]
                  << std::setw(10) << delay << "\n";
    }
    std::cout << "平均延迟 = " << std::fixed << std::setprecision(2)
              << totalDelay / pkts.size() << " 时隙\n";
}
// ─────────────────────────────────────────────────────────────────────────────
// 主函数:演示FIFO和优先级调度的延迟计算
// 使用教材P6题的典型到达时间序列
// ─────────────────────────────────────────────────────────────────────────────
int main() {
    // 教材图P6:12个包的到达时隙(根据题图读取,这里用典型示例值)
    // 包ID:     1  2  3  4  5  6  7  8  9  10  11  12
    // 到达时隙(示例,实际值从题图读取)
    std::vector<int> arrivals = {0, 0, 1, 2, 2, 3, 5, 5, 6,  8,  9,  9};
    // P6b: 奇数包为高优先级(classId=0),偶数包为低优先级(classId=1)
    std::vector<Packet> pkts;
    for (int i = 0; i < 12; ++i) {
        pkts.push_back({i + 1, arrivals[i], (i % 2 == 0) ? 0 : 1});
    }
    std::cout << "============================================================\n";
    std::cout << "  数据包调度延迟分析(对应习题 P6)\n";
    std::cout << "============================================================\n";
    // FIFO
    auto fifoResult = simulateFIFO(pkts);
    printResult("FIFO 调度", pkts, fifoResult);
    // 优先级(奇数高优先级)
    auto prioResult = simulatePriority(pkts);
    printResult("优先级调度(奇数包高优先级)", pkts, prioResult);
    // ── WFQ 简化演示(虚拟完成时间法)────────────────────────────────────────
    std::cout << "\n=== WFQ 调度(权重 class1=2, class2=1)===\n";
    // 奇数包=类别1(权重2),偶数包=类别2(权重1)
    std::vector<double> vt = {0.0, 0.0};       // 每个类别的当前虚拟时间
    std::vector<double> weights = {2.0, 1.0};  // 权重
    // 用 (虚拟完成时间, 包ID) 的最小堆模拟
    using PII = std::pair<double, int>;
    std::priority_queue<PII, std::vector<PII>, std::greater<PII>> pq;
    std::vector<Packet> wfqPkts = pkts;
    std::sort(wfqPkts.begin(), wfqPkts.end(),
              [](const Packet& a, const Packet& b) {
                  return a.arriveSlot < b.arriveSlot;
              });
    std::vector<int> wfqDepart(12, -1);
    int slot = 0;
    int nextPkt = 0;  // 下一个未入队的包索引
    int processed = 0;
    while (processed < 12) {
        // 将本时隙到达的包加入WFQ堆
        while (nextPkt < 12 && wfqPkts[nextPkt].arriveSlot <= slot) {
            const Packet& p = wfqPkts[nextPkt];
            int cid = p.classId;
            // 虚拟完成时间 = max(当前虚拟时间, 包到达时刻) + 1/权重
            double ft = std::max(vt[cid], (double)p.arriveSlot) + 1.0 / weights[cid];
            vt[cid] = ft;
            pq.push({ft, p.id});
            nextPkt++;
        }
        if (!pq.empty()) {
            auto [ft, pid] = pq.top();
            pq.pop();
            wfqDepart[pid - 1] = slot;
            processed++;
            std::cout << "时隙 " << slot << ": 发送包#" << pid
                      << " (虚拟完成时间=" << std::fixed << std::setprecision(2)
                      << ft << ")\n";
        }
        slot++;
        if (slot > 30) break;
    }
    double totalDelay = 0;
    for (const auto& p : pkts) {
        totalDelay += wfqDepart[p.id - 1] - p.arriveSlot;
    }
    std::cout << "WFQ 平均延迟 = " << totalDelay / 12 << " 时隙\n";
    std::cout << "\n结论:三种调度的平均延迟应非常接近(验证 P6-e 的结论)\n";
    return 0;
}

四、总结速查表

第4章复习题与习题

4.1节 基本概念

4.2节 路由器内部

4.3节 IP协议

4.4节 广义转发

数据报 / 帧 / 报文段
数据平面 vs 控制平面
转发 vs 路由

硬件 vs 软件实现
HOL阻塞
三种交换结构

四种调度:
FIFO/优先级/RR/WFQ
平均延迟相同

协议号区分TCP/UDP
TTL限跳数
DHCP四步握手

私有地址不出公网
NAT转换表
IPv6过渡:隧道

多字段匹配
三类动作
流表设计

防火墙:无规则则丢弃
负载均衡:同目的不同路径
P4语言


题号核心考点
R1数据报;路由器vs交换机工作层次
R3转发=局部/硬件;路由=全局/软件
R8目的地转发只看目的IP;SDN用广义转发
R11HOL阻塞在输入端口;交叉开关缓解
R14只有FIFO保证按到达顺序离开
P1b陷阱题:传统路由不能按源IP区分路径
P5WFQ权重0.5:0.25:0.25 → 服务序列2:1:1
P6e四种调度平均延迟相同(只影响分布)
P8最长前缀匹配找最具体前缀
P11子网划分:从大需求开始分配
P17 ⌈ 5000000 / 1480 ⌉ = 3379 \lceil 5000000/1480 \rceil = 3379 ⌈5000000/1480⌉=3379 个数据报
P23防火墙:白名单+默认丢弃
P24ICMP是网络层协议(服务于IP)

网络层:控制平面 详细解析

本文对应教材第5章,用尽可能通俗的语言解释控制平面的核心知识点。

目录

  1. 控制平面概述
  2. 路由算法基础:图模型
  3. 链路状态算法 LS(Dijkstra)
  4. 距离向量算法 DV(Bellman-Ford)
  5. LS 与 DV 算法对比

1. 控制平面概述

1.1 什么是控制平面?

网络层分为两个平面:

平面职责比喻
数据平面按照表格转发数据包快递员按地址送包裹
控制平面计算并维护转发表规划哪条路最快到达目的地

控制平面负责回答一个核心问题:数据包从源主机到目的主机,应该走哪条路?

1.2 两种控制方式

方式一:每个路由器单独控制(Per-router Control)
路由器A          路由器B          路由器C
+--------+      +--------+      +--------+
|路由算法|<---->|路由算法|<---->|路由算法|
|转发表  |      |转发表  |      |转发表  |
+--------+      +--------+      +--------+
  • 每台路由器自己运行路由算法
  • 路由器之间互相交换信息
  • OSPF 和 BGP 都采用这种方式
方式二:逻辑集中控制(Logically Centralized Control / SDN)
              +--------------------+
              |   中央控制器        |
              |  (SDN Controller)  |
              +--------------------+
               /        |        \
              /         |         \
        +----+      +----+      +----+
        | CA |      | CA |      | CA |   <- 控制代理(只听命令)
        +----+      +----+      +----+
        路由器A     路由器B     路由器C
  • 一个"大脑"统一计算所有转发表
  • 路由器上只有控制代理(CA),负责执行命令
  • CA 之间不直接通信,这是与 Per-router 最大的区别
  • SDN(软件定义网络)采用此方式,2020年已在数据中心(64%)、广域网(58%)广泛部署

2. 路由算法基础:图模型

2.1 用图来描述网络

把网络抽象成一个图 G = ( N , E ) G = (N, E) G=(N,E):

  • N N N:节点集合,代表路由器
  • E E E:边集合,代表物理链路
  • 每条边 ( x , y ) (x, y) (x,y) 有一个代价 c ( x , y ) c(x, y) c(x,y)
    代价可以表示:链路长度、带宽、经济成本等。
    若两节点之间没有直接链路,则 c ( x , y ) = ∞ c(x, y) = \infty c(x,y)=∞。

2.2 最小代价路径

路径代价 = 路径上所有边的代价之和:
c ( x 1 , x 2 ) + c ( x 2 , x 3 ) + ⋯ + c ( x p − 1 , x p ) c(x_1, x_2) + c(x_2, x_3) + \cdots + c(x_{p-1}, x_p) c(x1​,x2​)+c(x2​,x3​)+⋯+c(xp−1​,xp​)
路由目标:找到从源节点到目的节点代价最小的路径。

2.3 路由算法的三种分类

分类一:集中式 vs 分散式

类型特点代表算法
集中式需要全局网络信息链路状态(LS)
分散式只知道相邻节点信息,迭代计算距离向量(DV)

分类二:静态 vs 动态

类型特点
静态路由路径变化慢,通常需人工干预
动态路由随网络拓扑/流量自动调整,但可能出现路由震荡

分类三:负载敏感 vs 负载无关

  • 负载敏感:链路代价随拥塞动态变化(早期 ARPAnet 使用,问题较多)
  • 负载无关:链路代价固定(现代 RIP、OSPF、BGP 均采用此方式)

3. 链路状态算法 LS(Dijkstra)

3.1 核心思想

LS 算法需要每个节点都知道整个网络的拓扑和所有链路代价,然后运行 Dijkstra 算法求最短路。
信息如何获取?每个节点向全网广播自己的链路状态包(包含与自己相连的所有链路及其代价)。广播结束后,每个节点都有完整的网络视图。

3.2 算法符号定义

符号含义
D ( v ) D(v) D(v)从源节点到节点 v v v 的当前已知最小代价
p ( v ) p(v) p(v)在当前最小代价路径上, v v v 的前驱节点
N ′ N' N′已确定最小代价路径的节点集合

3.3 算法伪代码(含详细注释)

初始化阶段:
    N' = {u}                          // 只把源节点 u 放入已确定集合
    对所有节点 v:
        if v 是 u 的邻居:
            D(v) = c(u, v)            // 直接相连,代价即链路代价
        else:
            D(v) = ∞                  // 不相连,代价为无穷大
循环阶段(重复直到 N' = N):
    从不在 N' 中的节点里,找到 D(w) 最小的节点 w
    将 w 加入 N'                      // w 的最短路径已确定
    对 w 的每个邻居 v(v 不在 N' 中):
        D(v) = min(D(v), D(w) + c(w, v))
        // 更新 v 的代价:要么保持原来的,要么经过 w 到达 v

3.4 C++ 完整实现(Dijkstra 算法)

#include <iostream>
#include <vector>
#include <climits>
#include <string>
// 节点数量(对应教材图5.3中的 u,v,w,x,y,z 共6个节点)
const int N = 6;
// 用 INT_MAX/2 表示无穷大,防止加法溢出
const int INF = INT_MAX / 2;
// 节点名称(索引 0~5 对应 u,v,w,x,y,z)
std::string nodeName = "uvwxyz";
// 打印结果:从源节点到每个节点的最短路径和代价
void printResult(int dist[], int prev[], int src) {
    std::cout << "\n=== 从节点 " << nodeName[src] << " 出发的最短路径 ===\n";
    for (int i = 0; i < N; i++) {
        if (i == src) continue;
        std::cout << nodeName[src] << " -> " << nodeName[i]
                  << "  代价: " << dist[i] << "  路径: ";
        // 反向追踪路径
        std::vector<int> path;
        int cur = i;
        while (cur != -1) {
            path.push_back(cur);
            cur = prev[cur];
        }
        // 逆序打印(从源到目的)
        for (int j = path.size() - 1; j >= 0; j--) {
            std::cout << nodeName[path[j]];
            if (j != 0) std::cout << " -> ";
        }
        std::cout << "\n";
    }
}
// Dijkstra 算法
// graph: 邻接矩阵,graph[i][j] 表示节点 i 到 j 的链路代价
// src:   源节点编号
void dijkstra(int graph[N][N], int src) {
    int dist[N];   // dist[i] = 从 src 到 i 的当前最短代价(对应算法中的 D(v))
    int prev[N];   // prev[i] = 最短路径上 i 的前驱节点(对应算法中的 p(v))
    bool inSet[N]; // inSet[i] = true 表示 i 已加入 N'
    // 初始化阶段
    for (int i = 0; i < N; i++) {
        dist[i]  = graph[src][i]; // 直接相连取链路代价,否则为 INF
        prev[i]  = (graph[src][i] < INF) ? src : -1; // 前驱节点
        inSet[i] = false;
    }
    // 源节点加入已确定集合
    inSet[src] = true;
    dist[src]  = 0;
    prev[src]  = -1;
    // 打印初始化状态
    std::cout << "\n--- 初始化 ---\n";
    std::cout << "N' = {" << nodeName[src] << "}\n";
    for (int i = 0; i < N; i++) {
        if (i == src) continue;
        if (dist[i] < INF)
            std::cout << "D(" << nodeName[i] << ") = " << dist[i] << "\n";
        else
            std::cout << "D(" << nodeName[i] << ") = INF\n";
    }
    // 循环阶段:共执行 N-1 次,每次确定一个节点的最短路径
    for (int iter = 0; iter < N - 1; iter++) {
        // 第一步:从不在 N' 中的节点里找 D(w) 最小的节点 w
        int w = -1;
        for (int i = 0; i < N; i++) {
            if (!inSet[i] && (w == -1 || dist[i] < dist[w])) {
                w = i;
            }
        }
        if (dist[w] == INF) break; // 剩余节点都不可达
        // 第二步:将 w 加入 N'
        inSet[w] = true;
        std::cout << "\n--- 第 " << iter + 1 << " 次迭代,加入节点 "
                  << nodeName[w] << " (代价=" << dist[w] << ") ---\n";
        // 第三步:更新 w 的邻居节点的代价
        for (int v = 0; v < N; v++) {
            if (!inSet[v] && graph[w][v] < INF) {
                // 经过 w 到达 v 的代价 = D(w) + c(w, v)
                int newDist = dist[w] + graph[w][v];
                if (newDist < dist[v]) {
                    dist[v] = newDist; // 更新最小代价
                    prev[v] = w;       // 更新前驱节点
                    std::cout << "  更新 D(" << nodeName[v] << ") = "
                              << dist[v] << ",前驱 = " << nodeName[w] << "\n";
                }
            }
        }
    }
    // 打印最终结果
    printResult(dist, prev, src);
}
int main() {
    // 教材图5.3的邻接矩阵
    // 节点:u=0, v=1, w=2, x=3, y=4, z=5
    int graph[N][N] = {
    //   u    v    w    x    y    z
        {0,   2,   5,   1,   INF, INF}, // u
        {2,   0,   3,   2,   INF, INF}, // v
        {5,   3,   0,   3,   1,   5  }, // w
        {1,   2,   3,   0,   1,   INF}, // x
        {INF, INF, 1,   1,   0,   2  }, // y
        {INF, INF, 5,   INF, 2,   0  }  // z
    };
    std::cout << "教材图5.3 链路状态(LS/Dijkstra)算法演示\n";
    std::cout << "节点:u=0, v=1, w=2, x=3, y=4, z=5\n";
    // 从源节点 u(编号0)运行 Dijkstra
    dijkstra(graph, 0);
    return 0;
}

编译运行:

g++ -o ls_dijkstra ls_dijkstra.cpp && ./ls_dijkstra

3.5 图5.3 网络拓扑示意

        u
       /|\
    2 / | \ 5
     /  |1  \
    v   x    w
     \  |\ /|
    3 \ |1\ |3
       \|  \|
        y---z
         2
(括号内数字为链路代价)

3.6 算法执行过程(Table 5.1 可视化)

步骤  N'          D(v)  D(w)  D(x)  D(y)  D(z)
 0    {u}          2,u   5,u   1,u   INF   INF
 1    {u,x}        2,u   4,x   --    2,x   INF
 2    {u,x,y}      2,u   3,y   --    --    4,y
 3    {u,x,y,v}    --    3,y   --    --    4,y
 4    {u,x,y,v,w}  --    --    --    --    4,y
 5    {u,...,z}    --    --    --    --    --

最终最短路径:

  • u → v u \to v u→v:代价 2,路径 u → v u \to v u→v
  • u → w u \to w u→w:代价 3,路径 u → x → y → w u \to x \to y \to w u→x→y→w
  • u → x u \to x u→x:代价 1,路径 u → x u \to x u→x
  • u → y u \to y u→y:代价 2,路径 u → x → y u \to x \to y u→x→y
  • u → z u \to z u→z:代价 4,路径 u → x → y → z u \to x \to y \to z u→x→y→z

3.7 算法复杂度

在最朴素的实现中,每次迭代都要扫描所有未确定节点:
总比较次数 = n + ( n − 1 ) + ( n − 2 ) + ⋯ + 1 = n ( n + 1 ) 2 \text{总比较次数} = n + (n-1) + (n-2) + \cdots + 1 = \frac{n(n+1)}{2} 总比较次数=n+(n−1)+(n−2)+⋯+1=2n(n+1)​
因此时间复杂度为 O ( n 2 ) O(n^2) O(n2)。若使用堆(优先队列)优化,可以降低到 O ( ( n + ∣ E ∣ ) log ⁡ n ) O((n+|E|)\log n) O((n+∣E∣)logn)。

3.8 LS 算法的震荡问题

当链路代价随流量变化时,可能出现路由震荡:

第1轮计算结果    第2轮计算结果    第3轮计算结果
  顺时针路由  ->  逆时针路由  ->  顺时针路由  -> ...

解决方法:让每个路由器随机化发送链路状态广播的时间,避免所有路由器同步执行算法。

4. 距离向量算法 DV(Bellman-Ford)

4.1 核心思想

DV 算法是分散式、迭代式、异步的:

  • 分散:每个节点只和直接相连的邻居交换信息
  • 迭代:不断重复计算,直到没有更新为止
  • 异步:不需要所有节点同步执行
    关键思路:“告诉我你到每个目的地的代价,我来决定经过你走是否更划算。”

4.2 Bellman-Ford 方程

设 d x ( y ) d_x(y) dx​(y) 为节点 x x x 到节点 y y y 的最小代价路径代价,则:
d x ( y ) = min ⁡ v { c ( x , v ) + d v ( y ) } d_x(y) = \min_v \{ c(x, v) + d_v(y) \} dx​(y)=vmin​{c(x,v)+dv​(y)}
其中 v v v 遍历 x x x 的所有邻居节点。
直观理解:从 x x x 到 y y y,必须先经过某个邻居 v v v,总代价 = x x x 到 v v v 的链路代价 + v v v 到 y y y 的最短路径代价。取所有邻居中的最小值即为答案。
验证(图5.3,从 u u u 到 z z z):
d u ( z ) = min ⁡ { c ( u , v ) ⏟ 2 + d v ( z ) ⏟ 5 ,    c ( u , x ) ⏟ 1 + d x ( z ) ⏟ 3 ,    c ( u , w ) ⏟ 5 + d w ( z ) ⏟ 3 } = min ⁡ { 7 , 4 , 8 } = 4 d_u(z) = \min\{ \underbrace{c(u,v)}_{2} + \underbrace{d_v(z)}_{5},\; \underbrace{c(u,x)}_{1} + \underbrace{d_x(z)}_{3},\; \underbrace{c(u,w)}_{5} + \underbrace{d_w(z)}_{3} \} = \min\{7, 4, 8\} = 4 du​(z)=min{2 c(u,v)​​+5 dv​(z)​​,1 c(u,x)​​+3 dx​(z)​​,5 c(u,w)​​+3 dw​(z)​​}=min{7,4,8}=4

4.3 每个节点维护的信息

每个节点 x x x 保存:

  1. 直接链路代价 c ( x , v ) c(x, v) c(x,v):到每个邻居的代价
  2. 自己的距离向量 D x = [ D x ( y ) : y ∈ N ] D_x = [D_x(y) : y \in N] Dx​=[Dx​(y):y∈N]:对全网各节点的代价估计
  3. 邻居的距离向量 D v D_v Dv​:每个邻居发来的距离向量

4.4 DV 算法伪代码(含注释)

节点 x 的算法:
初始化:
    对所有目的节点 y:
        Dx(y) = c(x, y)        // 邻居设为链路代价,否则设为 INF
    对所有邻居 w:
        Dw(y) = 未知            // 邻居的距离向量暂时未知
    向所有邻居发送自己的距离向量 Dx
循环(永久执行):
    等待以下任一事件:
        a) 检测到某条直连链路代价变化
        b) 收到某个邻居的新距离向量
    对所有目的节点 y:
        Dx(y) = min_v { c(x,v) + Dv(y) }   // Bellman-Ford 更新
    如果 Dx 有任何变化:
        向所有邻居发送新的 Dx               // 通知邻居

4.5 C++ 完整实现(距离向量算法)

#include <iostream>
#include <vector>
#include <climits>
#include <string>
const int N = 3;   // 教材图5.6 三节点示例:x=0, y=1, z=2
const int INF = INT_MAX / 2;
std::string nodeName = "xyz";
// 每个节点维护的路由表:dist[i][j] 表示节点 i 认为到节点 j 的最短代价
int dist[N][N];
// 直连链路代价矩阵(对称,不变)
int link[N][N];
// 打印当前所有节点的距离向量
void printDistVectors(int round) {
    std::cout << "\n=== 第 " << round << " 轮 ===\n";
    for (int i = 0; i < N; i++) {
        std::cout << "节点 " << nodeName[i] << " 的距离向量: [";
        for (int j = 0; j < N; j++) {
            if (dist[i][j] == INF)
                std::cout << "INF";
            else
                std::cout << dist[i][j];
            if (j < N - 1) std::cout << ", ";
        }
        std::cout << "]\n";
    }
}
// 执行一轮 DV 更新,返回是否有任何节点的距离向量发生变化
bool dvUpdate() {
    bool changed = false;
    // 临时保存新的距离向量,防止同轮更新互相影响(模拟同步更新)
    int newDist[N][N];
    for (int i = 0; i < N; i++)
        for (int j = 0; j < N; j++)
            newDist[i][j] = dist[i][j];
    // 对每个节点 x
    for (int x = 0; x < N; x++) {
        // 对每个目的节点 y
        for (int y = 0; y < N; y++) {
            if (x == y) {
                newDist[x][y] = 0;
                continue;
            }
            // Bellman-Ford: Dx(y) = min_v { c(x,v) + Dv(y) }
            int minCost = INF;
            for (int v = 0; v < N; v++) {
                // v 是 x 的邻居(直连链路代价不为 INF)
                if (link[x][v] < INF && dist[v][y] < INF) {
                    int cost = link[x][v] + dist[v][y];
                    if (cost < minCost) {
                        minCost = cost;
                    }
                }
            }
            if (minCost < newDist[x][y]) {
                newDist[x][y] = minCost;
                changed = true;
            }
        }
    }
    // 将新结果写回 dist
    for (int i = 0; i < N; i++)
        for (int j = 0; j < N; j++)
            dist[i][j] = newDist[i][j];
    return changed;
}
int main() {
    // 教材图5.6 三节点网络
    // x=0, y=1, z=2
    // 链路代价:c(x,y)=2, c(y,z)=1, c(x,z)=7
    link[0][0] = 0;   link[0][1] = 2;   link[0][2] = 7;
    link[1][0] = 2;   link[1][1] = 0;   link[1][2] = 1;
    link[2][0] = 7;   link[2][1] = 1;   link[2][2] = 0;
    // 初始化距离向量(只知道直连链路代价)
    for (int i = 0; i < N; i++)
        for (int j = 0; j < N; j++)
            dist[i][j] = link[i][j]; // 非邻居初始化为 INF(link 矩阵中已设置)
    std::cout << "距离向量(DV / Bellman-Ford)算法演示\n";
    std::cout << "节点:x=0, y=1, z=2\n";
    std::cout << "链路代价:c(x,y)=2, c(y,z)=1, c(x,z)=7\n";
    printDistVectors(0); // 打印初始状态
    // 迭代直到收敛
    int round = 1;
    while (true) {
        bool changed = dvUpdate();
        printDistVectors(round);
        if (!changed) {
            std::cout << "\n算法已收敛!\n";
            break;
        }
        round++;
    }
    return 0;
}

编译运行:

g++ -o dv_bellman dv_bellman.cpp && ./dv_bellman

预期输出(对应图5.6):

=== 第 0 轮(初始)===
节点 x 的距离向量: [0, 2, 7]
节点 y 的距离向量: [2, 0, 1]
节点 z 的距离向量: [7, 1, 0]
=== 第 1 轮 ===
节点 x 的距离向量: [0, 2, 3]   <- x到z由7变为3(经过y)
节点 y 的距离向量: [2, 0, 1]
节点 z 的距离向量: [3, 1, 0]   <- z到x由7变为3(经过y)
=== 第 2 轮 ===  (无变化,收敛)

4.6 DV 算法执行过程演示(图5.6)

初始状态:
  节点x知道:到y代价2,到z代价7
  节点y知道:到x代价2,到z代价1
  节点z知道:到x代价7,到y代价1
第1轮交换后,x收到y的距离向量[2,0,1]:
  x计算到z:min(c(x,y)+Dy(z), c(x,z)+Dz(z))
           = min(2+1, 7+0) = min(3, 7) = 3
  --> x更新 Dx(z) = 3 (经过y)
第2轮:无变化,算法收敛。

4.7 链路代价变化:好消息与坏消息

好消息传播快

当某条链路代价降低时(如 y → x y \to x y→x 从4降到1):

t0: y 检测到代价变化,更新 Dy(x)=1,通知 z
t1: z 收到更新,计算 Dz(x)=min(1+1, 50)=2,通知 y
t2: y 收到更新,Dy(x) 不变,算法收敛

只需 2 次迭代,好消息迅速传播。

坏消息传播慢:无穷计数问题

当某条链路代价增加时(如 y → x y \to x y→x 从4增到60):

初始:Dy(x)=4, Dz(x)=5(z经过y到x)
t0: y 检测代价变成60,计算新路径:
    Dy(x) = min(60+0, 1+Dz(x)) = min(60, 1+5) = 6  <-- 错!z的路径实际经过y
    但 y 不知道 z 的路径是经过自己的!
t1: y 发送 Dy(x)=6 给 z
    z 计算:Dz(x) = min(50, 1+6) = 7
t2: z 发送 Dz(x)=7 给 y
    y 计算:Dy(x) = min(60, 1+7) = 8
... 如此反复,需要 44 次迭代才能收敛!

这就是无穷计数问题(Count-to-Infinity)。

迭代轮次  Dy(x)  Dz(x)
   0        6      5
   1        6      7
   2        8      7
   3        8      9
   ...     ...    ...
   44       51     50  <- z 最终发现直连代价50更小,切换路由

4.8 毒性逆转(Poisoned Reverse)

思路:如果 z z z 经过 y y y 到达 x x x,则 z z z 告诉 y y y 它到 x x x 的代价是无穷大(说谎),防止 y y y 以为可以经过 z z z 到 x x x。

正常情况:
  z 路由到 x 经过 y
  z 告诉 y:Dz(x) = INF (毒性逆转)
当 c(y,x) 从 4 变为 60:
  t0: y 计算 Dy(x) = 60(只能直连,因为 z 说自己到 x 是 INF)
  t1: y 通知 z:Dy(x)=60
  t2: z 改走直连 z->x(代价50),通知 y:Dz(x)=50
  t3: y 更新 Dy(x)=51(经过z),对 z 施加毒性逆转:告诉 z Dy(x)=INF
  --> 快速收敛!

局限性:毒性逆转只能解决两节点之间的路由环路,涉及三个及以上节点的环路无法被检测。

5. LS 与 DV 算法对比

5.1 三个维度的比较

消息复杂度

算法消息量说明
LS O ( ∣ N ∣ ⋅ ∣ E ∣ ) O(|N| \cdot |E|) O(∣N∣⋅∣E∣)每个节点需广播链路状态给所有节点
DV只和邻居通信消息量少,但收敛慢

收敛速度

算法收敛速度问题
LS(Dijkstra) O ( n 2 ) O(n^2) O(n2)可能出现震荡
DV可能很慢存在无穷计数问题、路由环路

健壮性

场景LSDV
路由器发送错误信息只影响该节点自己的转发表错误信息会扩散到全网
历史案例较隔离1997年一个小 ISP 的故障导致大面积断网数小时

5.2 LS vs DV 决策树

需要选择路由算法?
         |
    +---------+
    | 网络规模 |
    +---------+
    /           \
  小型           大型
  网络           网络
    |               |
  DV可接受      考虑LS
                    |
             +------------+
             | 对健壮性    |
             | 要求高吗?  |
             +------------+
             /             \
           是               否
           |                |
       优选LS            DV也可以
  (错误影响范围小)   (消息量小,实现简单)

5.3 现实应用


协议类型算法基础应用场景
OSPF链路状态LS / Dijkstra单个 ISP 内部路由
BGP路径向量DV 的变种互联网 AS 之间路由(“胶水”)
RIP距离向量DV / Bellman-Ford小型网络,已逐渐淘汰

总结

控制平面
├── 控制方式
│   ├── Per-router(OSPF, BGP)
│   └── 集中控制(SDN)
│
└── 路由算法
    ├── LS(链路状态)
    │   ├── 需要全局信息
    │   ├── Dijkstra 算法,O(n²)
    │   ├── 收敛快,但可能震荡
    │   └── 代表:OSPF
    │
    └── DV(距离向量)
        ├── 只需邻居信息
        ├── Bellman-Ford 方程迭代
        ├── 可能有无穷计数问题
        ├── 毒性逆转可部分解决
        └── 代表:RIP, BGP

两种算法没有绝对的优劣之分,互联网中两者都在使用。

更多推荐