计算机网络自顶向下方法第九版学习:第六章 6.3.4~6.4.2节:DOCSIS、交换局域网、MAC地址、ARP与以太网 —— 详细中文解析
图2展示了一个典型的机构交换局域网:三个系(电气工程、计算机科学、计算机工程)各连一个二层交换机,再通过主干交换机连到服务器和路由器。交换局域网拓扑(对应图2):互联网路由器主干交换机(10Gbps光纤)/ | \/ | \子交换机1 子交换机2 子交换机3(EE系) (CS系) (CE系)主机 ... 主机 主机 ... 主机 主机 ... 主机MAC 地址(媒体访问控制地址),也叫 LAN 地
目录
1. DOCSIS:有线电视网络的链路层协议
1.1 背景:有线电视接入网
DOCSIS(Data-Over-Cable Service Interface Specifications,有线数据服务接口规范)是家用有线宽带的链路层协议,是前三类多路访问协议的"综合体"。
有线网络架构(图1对应):
┌──────────────────────────────────┐
│ 下行信道 i(CMTS → 用户) │──> 所有家庭收到
CMTS ───────────────┤ │
(头端/机房) │ 上行信道 j(用户 → CMTS) │<── 多用户共享!
└──────────────────────────────────┘
t₁ t₂
├─────────────────────────────────┤
│ mini时隙: │ mini时隙: │
│ 请求帧 │ 被分配的数据帧 │
│(随机竞争) │(CMTS指定,无碰撞) │
1.2 DOCSIS 中三类协议的综合运用
DOCSIS 是一个罕见的在单一网络中融合了三类多路访问协议的例子:
1.3 工作流程(图1详解)
下行方向(CMTS → 家庭,简单):
- CMTS 是唯一的发送方,无多路访问问题
- CMTS 在下行信道广播 MAP 控制消息,告诉各调制解调器在时间区间 [ t 1 , t 2 ] [t_1, t_2] [t1,t2] 内谁可以用哪个 mini 时隙
上行方向(家庭 → CMTS,复杂): - 多个调制解调器共享同一上行信道频率,可能碰撞
- 上行时间区间分为两类 mini 时隙:
上行时隙结构(时间区间 [t₁, t₂]):
├──请求槽──┤ ├──────────────已分配数据槽────────────────┤
│ 调制解调器A │ │ 调制解调器A(CMTS已分配) │ 调制解调器C │ ...
│ 调制解调器B │ (碰撞!)
│ (随机竞争)│
请求槽(随机访问):
- 调制解调器用随机访问方式发送"我有数据要发"的请求帧
- 可能碰撞!但调制解调器无法直接检测碰撞
- 推断碰撞方式:如果下一个 MAP 消息里没有分配到时隙 → 推断发生了碰撞
- 碰撞后用二进制指数退避延迟重发请求
数据槽(集中分配): - CMTS 通过 MAP 消息明确指定每个时隙给谁 → 无碰撞!
总结:DOCSIS 是 FDM + TDM + 随机访问 + 集中分配的完美融合!
2. 交换局域网概述
2.1 什么是交换局域网?
图2展示了一个典型的机构交换局域网:三个系(电气工程、计算机科学、计算机工程)各连一个二层交换机,再通过主干交换机连到服务器和路由器。
交换局域网拓扑(对应图2):
互联网
|
路由器
|
主干交换机(10Gbps光纤)
/ | \
/ | \
子交换机1 子交换机2 子交换机3
(EE系) (CS系) (CE系)
/ | \ / | \ / | \
主机 ... 主机 主机 ... 主机 主机 ... 主机
2.2 交换机 vs 路由器
| 对比项 | 链路层交换机 | 网络层路由器 |
|---|---|---|
| 工作层次 | 第2层(链路层) | 第3层(网络层) |
| 转发依据 | MAC 地址 | IP 地址 |
| 使用的算法 | 自学习(无需路由协议) | OSPF、BGP等路由算法 |
| 地址配置 | 自动(即插即用) | 需要配置 |
交换机对主机来说是透明的——主机不需要知道交换机的存在,交换机默默转发帧。
3. MAC地址
3.1 什么是 MAC 地址?
MAC 地址(媒体访问控制地址),也叫 LAN 地址、物理地址,是**网络接口(适配器/网卡)**的唯一标识。
注意:
- 是**接口(网卡)**有 MAC 地址,不是主机本身
- 一台有多个网卡的主机,有多个 MAC 地址
- 链路层交换机的接口没有 MAC 地址(它透明转发,不需要被寻址)
3.2 MAC 地址的格式
MAC 地址长度:6字节(48位),通常用十六进制表示:
MAC地址示例(图3对应):
1A-23-F9-CD-06-9B ← 主机C的MAC地址
5C-66-AB-90-75-B1 ← 主机B的MAC地址
49-BD-D2-C7-56-2A ← 主机A的MAC地址
88-B2-2F-54-1A-0F ← 路由器接口的MAC地址
格式:XX-XX-XX-XX-XX-XX(每个XX是一个字节,用两位十六进制表示)
├──前24位──┤├──后24位──┤
厂商标识 厂商自定义(保证不重复)
IEEE 如何保证全球唯一性:
- IEEE 统一管理 MAC 地址空间
- 厂商购买一块地址空间( 2 24 2^{24} 224 个地址,即约1600万个)
- IEEE 固定前24位(OUI,组织唯一标识符),厂商自定义后24位
因此全球共有 2 48 ≈ 281 2^{48} \approx 281 248≈281 万亿个可能的 MAC 地址。
3.3 MAC 地址 vs IP 地址(核心区别)
这是一个非常重要的对比:
| 对比项 | MAC 地址 | IP 地址 |
|---|---|---|
| 结构 | 扁平结构(无层次) | 层次结构(网络部分+主机部分) |
| 变化性 | 不变(烧入硬件,全球跟着走) | 随位置变化(换网络就要换IP) |
| 类比 | 身份证号(全球唯一,不变) | 邮政地址(随居住地变化) |
| 有效范围 | 同一子网内 | 跨网络,全球路由 |
类比总结:
- MAC 地址 = 你的身份证号(走到哪都是这个号)
- IP 地址 = 你的家庭住址(搬家就要换地址)
3.4 广播 MAC 地址
当发送方希望局域网内所有接口都接收这个帧时,使用广播地址:
广播 MAC 地址 = FF-FF-FF-FF-FF-FF \text{广播 MAC 地址} = \texttt{FF-FF-FF-FF-FF-FF} 广播 MAC 地址=FF-FF-FF-FF-FF-FF
(48个连续的1,6字节全为0xFF)
4. 地址解析协议 ARP
4.1 为什么需要 ARP?
发送方知道目标的 IP 地址,但发送链路层帧需要目标的 MAC 地址。这就是 ARP 要解决的问题。
问题场景(图4对应):
主机C(IP: 222.222.222.220)想给主机A(IP: 222.222.222.222)发数据
主机C知道:目标IP = 222.222.222.222
主机C不知道:目标MAC = ???
ARP 的任务:已知同一子网内的 IP 地址 → 求对应的 MAC 地址
ARP 和 DNS 的区别:
| 协议 | 输入 | 输出 | 有效范围 |
|---|---|---|---|
| DNS | 主机名(hostname) | IP 地址 | 全球 |
| ARP | IP 地址 | MAC 地址 | 仅限同一子网 |
4.2 ARP 表
每台主机和路由器内存中都有一张 ARP 表(图5对应):
主机 222.222.222.220 的 ARP 表:
┌─────────────────┬───────────────────┬──────────┐
│ IP 地址 │ MAC 地址 │ TTL │
├─────────────────┼───────────────────┼──────────┤
│ 222.222.222.221 │ 88-B2-2F-54-1A-0F │ 13:45:00 │
│ 222.222.222.223 │ 5C-66-AB-90-75-B1 │ 13:52:00 │
└─────────────────┴───────────────────┴──────────┘
TTL(Time-To-Live):表项的有效期,典型值为20分钟
过期后自动删除,需要时重新查询
4.3 ARP 工作流程(即插即用,自动建立)
详细步骤:
步骤1:主机C广播ARP请求
帧目标MAC = FF-FF-FF-FF-FF-FF(广播)
ARP包内容:"谁是 222.222.222.222?请告诉 222.222.222.220"
步骤2:子网内所有主机收到该广播帧
每台主机检查 ARP 包中的目标 IP
步骤3:主机A(IP=222.222.222.222)匹配,单播回复
帧目标MAC = 主机C的MAC(点对点,不用广播)
ARP包内容:"222.222.222.222 的 MAC 是 49-BD-D2-C7-56-2A"
步骤4:主机C更新 ARP 表,发送数据帧
"大喊"类比:ARP 请求就像在一个开放式办公室里大喊:
“222号格子的人,你的工位号是多少?”(所有人都听到)
ARP 响应就像那个人走过来悄悄告诉你(单播)。
ARP 的两个有趣特点:
- 查询帧:广播(所有人都能听到)
- 响应帧:单播(只告诉询问者)
- 即插即用:ARP 表自动建立,无需管理员手动配置
5. 跨子网发送数据报
5.1 问题设定(图6对应)
图6展示了两个子网通过路由器互连:
子网1(111.111.111.0/24) 子网2(222.222.222.0/24)
主机H1 主机H2
IP: 111.111.111.111 IP: 222.222.222.222
MAC: 74-29-9C-E8-FF-55 MAC: 49-BD-D2-C7-56-2A
| |
| 路由器 |
+---------> 接口1 接口2 <---+
IP: 111.111.111.110 IP: 222.222.222.220
MAC: E6-E9-00-17-BB-4B MAC: 1A-23-F9-CD-06-9B
5.2 跨子网发送的完整流程
场景:H1(111.111.111.111)要发数据给 H2(222.222.222.222)
步骤1:H1 查路由表,发现目标在子网2,需要经过路由器
下一跳路由器接口IP = 111.111.111.110
步骤2:H1 用 ARP 查询路由器接口1的 MAC 地址
ARP 查询:谁是 111.111.111.110?
ARP 响应:E6-E9-00-17-BB-4B
步骤3:H1 构造链路层帧,发送到子网1
帧的目标MAC = E6-E9-00-17-BB-4B(路由器接口1)
帧的载荷 = IP数据报(目标IP=222.222.222.222)
步骤4:路由器接口1收到帧
发现目标MAC匹配自己,提取IP数据报
查转发表:222.222.222.222 → 从接口2转发
步骤5:路由器用 ARP 查询 H2 的 MAC 地址
ARP 查询(在子网2广播):谁是 222.222.222.222?
ARP 响应:49-BD-D2-C7-56-2A
步骤6:路由器从接口2发出新帧
帧的目标MAC = 49-BD-D2-C7-56-2A(H2的MAC)
帧的载荷 = 同一个IP数据报(目标IP不变,仍为222.222.222.222)
步骤7:H2 收到帧,提取IP数据报,传给网络层
关键点:每经过一个路由器,链路层帧被重新封装(MAC地址变了),但IP数据报的源IP和目标IP始终不变!
跨子网帧格式变化示意:
子网1中的帧:
┌──────────────────────┬───────────────────────┬────────────────┐
│ 目标MAC: │ 源MAC: │ IP数据报 │
│ E6-E9-00-17-BB-4B │ 74-29-9C-E8-FF-55 │ src:111.111.111.111 │
│ (路由器接口1) │ (H1) │ dst:222.222.222.222 │
└──────────────────────┴───────────────────────┴────────────────┘
子网2中的帧(路由器重新封装):
┌──────────────────────┬───────────────────────┬────────────────┐
│ 目标MAC: │ 源MAC: │ IP数据报 │
│ 49-BD-D2-C7-56-2A │ 1A-23-F9-CD-06-9B │ src:111.111.111.111 │
│ (H2) │ (路由器接口2) │ dst:222.222.222.222 │
└──────────────────────┴───────────────────────┴────────────────┘
↑ IP层信息没变!
6. 以太网
6.1 以太网的历史地位
以太网是有线局域网领域的绝对霸主,就像互联网之于全球网络一样。
以太网拓扑的演进:
总线拓扑时代:
主机A 主机B 主机C 主机D
| | | |
───────┴────────┴────────┴────────┴───── 同轴电缆
广播介质:任何人发送,所有人都收到
用 CSMA/CD 解决碰撞
集线器(Hub)星型拓扑:
主机A 主机B
\ /
\ /
[Hub集线器] ← 物理层设备,位操作,广播所有接口
/ \ 逻辑上仍是广播LAN,仍有碰撞
/ \
主机C 主机D
交换机(Switch)星型拓扑(现代):
主机A 主机B
\ /
\ /
[Switch交换机] ← 链路层设备,帧操作,智能转发
/ \ 无碰撞,全双工!
/ \
主机C 主机D
6.2 以太网成功的原因
以太网能统治局域网市场几十年,主要因为:
- 先发优势:最早广泛部署,管理员熟悉
- 竞争对手更复杂、更贵:令牌环、FDDI、ATM 都比以太网复杂
- 持续进化:每当新技术出现更高速率时,以太网总能跟上(10M→100M→1G→10G→400G)
- 成本极低:以太网芯片已是大宗商品,极便宜
7. 以太网帧结构详解
图7展示了以太网帧的6个字段,从左到右:
以太网帧结构(对应图7):
8字节 6字节 6字节 2字节 46~1500字节 4字节
┌────────┬──────────┬──────────┬──────┬────────────┬─────┐
│前导码 │目标MAC地址│源MAC地址 │类型 │ 数据 │ CRC │
│Preamble│Dest. Addr│Src. Addr │Type │ Data │ │
└────────┴──────────┴──────────┴──────┴────────────┴─────┘
各字段详解
1. 前导码(Preamble,8字节)
前7字节:10101010 10101010 10101010 ... (唤醒接收方,同步时钟)
第8字节:10101011 (最后两位11:提示"重要内容马上来了!")
作用:让接收方适配器"锁定"发送方的时钟频率,因为现实中发送速率不是完全精确的。
2. 目标地址(Dest. Address,6字节)
接收方适配器收到帧后,检查目标 MAC 是否匹配自己:
- 匹配 → 提取数据,向上传递
- 不匹配 → 丢弃(不传给 CPU,减少中断)
FF-FF-FF-FF-FF-FF→ 广播,所有人都接受
3. 源地址(Source Address,6字节)
发送方适配器的 MAC 地址。
4. 类型字段(Type,2字节)
用于多路复用——告诉接收方的网络层,这个帧的载荷应该交给哪个上层协议:
常见类型值:
0x0800 → IP 数据报
0x0806 → ARP 包
0x86DD → IPv6 数据报
类比:就像信封上写"航空邮件"还是"普通邮件",告诉邮局用哪种方式处理。
5. 数据字段(Data,46~1500字节)
- 最大值:1500字节(以太网 MTU)。超过则需要 IP 分片
- 最小值:46字节。不足则需要填充(Stuffing),接收方用 IP 头部的长度字段去除填充
46 ≤ 数据字段长度 ≤ 1500 字节 46 \leq \text{数据字段长度} \leq 1500 \text{ 字节} 46≤数据字段长度≤1500 字节
6. CRC(循环冗余校验,4字节)
用于检测帧在传输过程中的比特错误。接收方计算 CRC,不匹配则直接丢弃(以太网不发送 ACK/NAK)。
7.1 以太网的服务特性
无连接(Connectionless):发帧前不握手,直接发。类似 UDP。
不可靠(Unreliable):
- CRC 通不过 → 直接丢弃,不通知发送方
- 发送方不知道帧是否到达
- 如果上层是 TCP → TCP 会重传(间接实现可靠性)
- 如果上层是 UDP → 数据直接丢失,应用层会看到"空洞"
8. 以太网技术演进
8.1 命名规则解析
以太网标准的命名格式:速率BASE-介质
例子拆解:
100BASE-T
│ │ └─ T = 双绞铜线(Twisted pair copper)
│ └──── BASE = 基带传输(只传以太网信号)
└───────── 100 = 100 Mbps
1000BASE-LX
│ │ └─ LX = 长波光纤(Long wavelength fiber)
│ └───── BASE = 基带传输
└────────── 1000 = 1 Gbps (Gigabit)
10GBASE-T
│ │ └─ T = 双绞铜线
│ └──── BASE = 基带传输
└───────── 10G = 10 Gbps
8.2 各代以太网对比
| 标准 | 速率 | 介质 | 年代 |
|---|---|---|---|
| 10BASE-2 | 10 Mbps | 细同轴电缆 | 1980s |
| 10BASE-5 | 10 Mbps | 粗同轴电缆 | 1980s |
| 10BASE-T | 10 Mbps | 双绞线 | 1990s |
| 100BASE-T | 100 Mbps | 双绞线 | 1990s |
| 1000BASE-T | 1 Gbps | 双绞线 | 2000s |
| 1000BASE-SX/LX | 1 Gbps | 光纤 | 2000s |
| 10GBASE-T | 10 Gbps | 双绞线 | 2010s |
| 40GBASE / 100GBASE | 40/100 Gbps | 光纤 | 2010s+ |
8.3 “统一链路层,多元物理层”(图8对应)
图8展示的核心思想:不管物理层用什么介质(铜线、光纤),MAC 协议和帧格式始终不变:
以太网最不变的东西:帧格式!从1973年发明至今,帧格式从未改变,这是以太网真正的"灵魂"。
8.4 现代交换以太网为什么不再需要 MAC 协议?
总线/Hub时代:
多节点共享介质 → 可能碰撞 → 需要 CSMA/CD
现代交换机时代:
每条链路只有交换机和一个节点(点对点)
交换机一次只向一个接口转发一个帧
交换机支持全双工(同时收发)
→ 没有碰撞 → 不需要 CSMA/CD MAC 协议!
但以太网帧格式保留了下来,用于身份识别和多路复用。
9. C++ 完整代码:ARP表模拟
#include <iostream>
#include <string>
#include <map>
#include <vector>
#include <sstream>
#include <iomanip>
#include <ctime>
#include <stdexcept>
// ============================================================
// ARP 表条目结构体
// ============================================================
struct ARPEntry {
std::string ipAddress; // IP 地址(字符串形式)
std::string macAddress; // MAC 地址(XX-XX-XX-XX-XX-XX格式)
int ttlSeconds; // 距离过期剩余秒数(TTL)
ARPEntry() : ttlSeconds(0) {}
ARPEntry(const std::string& ip, const std::string& mac, int ttl)
: ipAddress(ip), macAddress(mac), ttlSeconds(ttl) {}
};
// ============================================================
// 验证 MAC 地址格式是否合法
// 合法格式:XX-XX-XX-XX-XX-XX,其中XX为两位十六进制
// ============================================================
bool isValidMAC(const std::string& mac) {
if (mac.size() != 17) return false;
for (int i = 0; i < 17; i++) {
if (i % 3 == 2) {
// 第2、5、8、11、14位应是'-'
if (mac[i] != '-') return false;
} else {
// 其他位应是十六进制字符
char c = mac[i];
bool isHex = (c >= '0' && c <= '9') ||
(c >= 'A' && c <= 'F') ||
(c >= 'a' && c <= 'f');
if (!isHex) return false;
}
}
return true;
}
// ============================================================
// ARP 表类:模拟主机/路由器中的 ARP 缓存表
// ============================================================
class ARPTable {
private:
// 用 map 存储 ARP 表:IP地址 → ARP表条目
std::map<std::string, ARPEntry> table;
int defaultTTL; // 默认 TTL(秒),典型值为1200秒(20分钟)
std::string owner; // 拥有此 ARP 表的主机名/IP
public:
// 构造函数
ARPTable(const std::string& ownerName, int ttl = 1200)
: owner(ownerName), defaultTTL(ttl) {}
// ============================================================
// 查询 ARP 表
// 参数:ip - 目标 IP 地址
// 返回:找到返回 MAC 地址,否则返回空字符串
// ============================================================
std::string lookup(const std::string& ip) {
auto it = table.find(ip);
if (it != table.end() && it->second.ttlSeconds > 0) {
return it->second.macAddress; // 命中缓存
}
return ""; // 未命中,需要发 ARP 请求
}
// ============================================================
// 向 ARP 表中插入/更新条目
// 参数:ip - IP 地址
// mac - MAC 地址
// ttl - TTL(秒),默认使用成员变量 defaultTTL
// ============================================================
void insert(const std::string& ip, const std::string& mac, int ttl = -1) {
if (!isValidMAC(mac)) {
std::cerr << "警告:MAC地址格式不合法:" << mac << std::endl;
return;
}
if (ttl < 0) ttl = defaultTTL;
table[ip] = ARPEntry(ip, mac, ttl);
std::cout << "ARP表更新:" << ip << " -> " << mac
<< "(TTL=" << ttl << "秒)" << std::endl;
}
// ============================================================
// 删除指定条目(例如主机断开网络后)
// ============================================================
void remove(const std::string& ip) {
if (table.erase(ip)) {
std::cout << "ARP表:已删除条目 " << ip << std::endl;
}
}
// ============================================================
// 模拟时间推进,减少所有条目的 TTL
// TTL 归零的条目将被删除(模拟 ARP 条目过期)
// 参数:seconds - 推进的秒数
// ============================================================
void advanceTime(int seconds) {
std::vector<std::string> toDelete;
for (auto& kv : table) {
kv.second.ttlSeconds -= seconds;
if (kv.second.ttlSeconds <= 0) {
toDelete.push_back(kv.first);
}
}
for (const auto& ip : toDelete) {
std::cout << "ARP表:条目 " << ip << " 已过期,自动删除" << std::endl;
table.erase(ip);
}
}
// ============================================================
// 打印当前 ARP 表内容
// ============================================================
void print() const {
std::cout << "\n=== " << owner << " 的 ARP 表 ===" << std::endl;
std::cout << std::left
<< std::setw(20) << "IP 地址"
<< std::setw(22) << "MAC 地址"
<< std::setw(10) << "TTL(秒)" << std::endl;
std::cout << std::string(52, '-') << std::endl;
if (table.empty()) {
std::cout << "(表为空)" << std::endl;
} else {
for (const auto& kv : table) {
const ARPEntry& e = kv.second;
std::cout << std::left
<< std::setw(20) << e.ipAddress
<< std::setw(22) << e.macAddress
<< std::setw(10) << e.ttlSeconds << std::endl;
}
}
std::cout << std::endl;
}
};
// ============================================================
// ARP 协议模拟器:模拟 ARP 请求/响应过程
// ============================================================
class ARPSimulator {
public:
// ============================================================
// 模拟 ARP 请求过程(同一子网内)
// 参数:senderIP - 发送方 IP
// targetIP - 目标 IP
// arpTable - 发送方的 ARP 表
// subnetMACs - 子网内 IP→MAC 的映射(模拟其他主机)
// 返回:解析到的目标 MAC 地址
// ============================================================
static std::string sendARPRequest(
const std::string& senderIP,
const std::string& targetIP,
ARPTable& arpTable,
const std::map<std::string, std::string>& subnetMACs) {
// 第一步:先查本地 ARP 表
std::string cachedMAC = arpTable.lookup(targetIP);
if (!cachedMAC.empty()) {
std::cout << "ARP缓存命中!" << targetIP
<< " 的MAC = " << cachedMAC << std::endl;
return cachedMAC;
}
// 第二步:ARP 表中没有,发送广播请求
std::cout << "\n[ARP请求] " << senderIP << " 广播询问:" << std::endl;
std::cout << " \"谁是 " << targetIP << "?"
<< "请告诉 " << senderIP << "\"" << std::endl;
std::cout << " (目标MAC = FF-FF-FF-FF-FF-FF 广播)" << std::endl;
// 第三步:子网内所有主机收到广播,检查IP是否匹配
std::cout << "\n[子网广播] 所有主机检查..." << std::endl;
for (const auto& kv : subnetMACs) {
if (kv.first == senderIP) continue; // 发送方自己不响应
if (kv.first == targetIP) {
// 匹配!单播回复
std::cout << "[ARP响应] " << kv.first
<< " 单播回复 " << senderIP << ":" << std::endl;
std::cout << " \"我是 " << targetIP
<< ",我的MAC = " << kv.second << "\"" << std::endl;
// 第四步:发送方更新 ARP 表
arpTable.insert(targetIP, kv.second);
return kv.second;
} else {
std::cout << " 主机 " << kv.first << " 检查:不匹配,丢弃" << std::endl;
}
}
std::cout << "[ARP错误] 未找到 " << targetIP << "(可能不在同一子网)" << std::endl;
return "";
}
};
// ============================================================
// 演示以太网帧结构
// ============================================================
void printEthernetFrame(
const std::string& destMAC,
const std::string& srcMAC,
const std::string& type,
int dataLen) {
std::cout << "\n=== 以太网帧结构 ===" << std::endl;
std::cout << std::string(70, '=') << std::endl;
// 前导码
std::cout << "| " << std::setw(10) << "前导码"
<< " | 8字节: 10101010x7 + 10101011 (同步时钟) |" << std::endl;
// 目标MAC
std::cout << "| " << std::setw(10) << "目标MAC"
<< " | 6字节: " << std::setw(20) << destMAC
<< " |" << std::endl;
// 源MAC
std::cout << "| " << std::setw(10) << "源MAC"
<< " | 6字节: " << std::setw(20) << srcMAC
<< " |" << std::endl;
// 类型
std::cout << "| " << std::setw(10) << "类型"
<< " | 2字节: " << std::setw(6) << type
<< " (0x0800=IP, 0x0806=ARP) |" << std::endl;
// 数据
std::cout << "| " << std::setw(10) << "数据"
<< " | " << dataLen << "字节 (46~1500字节,不足46字节需填充) |" << std::endl;
// CRC
std::cout << "| " << std::setw(10) << "CRC"
<< " | 4字节: 循环冗余校验(CRC-32) |" << std::endl;
std::cout << std::string(70, '=') << std::endl;
std::cout << "总帧长度(不含前导码):"
<< (6 + 6 + 2 + dataLen + 4) << " 字节" << std::endl;
}
// ============================================================
// 主函数:综合演示
// ============================================================
int main() {
std::cout << "=====================================================" << std::endl;
std::cout << " MAC地址 / ARP 协议 / 以太网帧 综合演示 " << std::endl;
std::cout << "=====================================================" << std::endl;
// ---- 演示1:MAC 地址验证 ----
std::cout << "\n=== 演示1:MAC地址格式验证 ===" << std::endl;
std::vector<std::string> testMACs = {
"1A-23-F9-CD-06-9B", // 合法
"5C-66-AB-90-75-B1", // 合法
"FF-FF-FF-FF-FF-FF", // 合法(广播地址)
"GG-XX-YY-00-00-00", // 非法(非十六进制字符)
"1A:23:F9:CD:06:9B", // 非法(分隔符应为'-')
"1A-23-F9-CD-06" // 非法(长度不足)
};
for (const auto& mac : testMACs) {
std::cout << " " << std::setw(22) << mac
<< " -> " << (isValidMAC(mac) ? "合法" : "非法") << std::endl;
}
// ---- 演示2:ARP 表操作 ----
std::cout << "\n=== 演示2:ARP表操作(对应图5)===" << std::endl;
// 创建主机 222.222.222.220 的 ARP 表
ARPTable arpTable("主机 222.222.222.220", 1200);
// 手动插入初始条目(模拟图5中的ARP表)
arpTable.insert("222.222.222.221", "88-B2-2F-54-1A-0F", 800); // 距过期还有800秒
arpTable.insert("222.222.222.223", "5C-66-AB-90-75-B1", 1100);
arpTable.print();
// 演示查询命中
std::cout << "查询 222.222.222.221:" << std::endl;
std::string result = arpTable.lookup("222.222.222.221");
std::cout << " 结果:" << (result.empty() ? "未找到" : result) << std::endl;
// 演示时间推进导致条目过期
std::cout << "\n模拟时间推进 900 秒..." << std::endl;
arpTable.advanceTime(900); // 800秒TTL的条目会过期
arpTable.print();
// ---- 演示3:ARP 协议完整流程(同一子网)----
std::cout << "\n=== 演示3:ARP完整流程(同一子网,对应图4)===" << std::endl;
// 模拟子网内所有主机的 IP→MAC 映射(真实情况中每台主机自己知道自己的MAC)
std::map<std::string, std::string> subnetMACs = {
{"222.222.222.220", "1A-23-F9-CD-06-9B"}, // 主机C(发送方)
{"222.222.222.222", "49-BD-D2-C7-56-2A"}, // 主机A(目标)
{"222.222.222.223", "5C-66-AB-90-75-B1"}, // 主机B
{"222.222.222.221", "88-B2-2F-54-1A-0F"} // 路由器接口
};
// 主机C想给主机A(222.222.222.222)发帧,但不知道其MAC
ARPTable hostC_ARP("主机C 222.222.222.220");
// ARP表中只有路由器的条目,没有主机A的
hostC_ARP.insert("222.222.222.221", "88-B2-2F-54-1A-0F");
std::cout << "\n主机C 想发帧给 IP:222.222.222.222,查询MAC地址..." << std::endl;
std::string resolvedMAC = ARPSimulator::sendARPRequest(
"222.222.222.220",
"222.222.222.222",
hostC_ARP,
subnetMACs
);
if (!resolvedMAC.empty()) {
std::cout << "\n主机C 获得目标MAC:" << resolvedMAC << std::endl;
std::cout << "现在可以发送以太网帧了!" << std::endl;
}
hostC_ARP.print();
// ---- 演示4:以太网帧结构展示 ----
std::cout << "\n=== 演示4:以太网帧结构(对应图7)===" << std::endl;
// 主机C(AA-AA-AA-AA-AA-AA)发送给主机A(BB-BB-BB-BB-BB-BB)的帧
printEthernetFrame(
"49-BD-D2-C7-56-2A", // 目标MAC(主机A)
"1A-23-F9-CD-06-9B", // 源MAC(主机C)
"0x0800", // 类型:IP
100 // 数据长度:100字节(含IP数据报)
);
// ---- 演示5:广播 ARP 请求帧 ----
std::cout << "\n=== 演示5:ARP 请求帧(广播)===" << std::endl;
printEthernetFrame(
"FF-FF-FF-FF-FF-FF", // 目标MAC:广播地址!
"1A-23-F9-CD-06-9B", // 源MAC(主机C)
"0x0806", // 类型:ARP
28 // ARP包固定28字节(IPv4)
);
return 0;
}
编译与运行:
g++ -std=c++11 -o arp_demo arp_demo.cpp
./arp_demo
预期输出片段:
=== 演示3:ARP完整流程(同一子网,对应图4)===
主机C 想发帧给 IP:222.222.222.222,查询MAC地址...
[ARP请求] 222.222.222.220 广播询问:
"谁是 222.222.222.222?请告诉 222.222.222.220"
(目标MAC = FF-FF-FF-FF-FF-FF 广播)
[子网广播] 所有主机检查...
主机 222.222.222.221 检查:不匹配,丢弃
主机 222.222.222.223 检查:不匹配,丢弃
[ARP响应] 222.222.222.222 单播回复 222.222.222.220:
"我是 222.222.222.222,我的MAC = 49-BD-D2-C7-56-2A"
ARP表更新:222.222.222.222 -> 49-BD-D2-C7-56-2A(TTL=1200秒)
核心知识点汇总
MAC 地址大小:
MAC地址位数 = 6 字节 = 48 位 \text{MAC地址位数} = 6 \text{ 字节} = 48 \text{ 位} MAC地址位数=6 字节=48 位
可能的MAC地址总数 = 2 48 ≈ 2.8 × 10 14 \text{可能的MAC地址总数} = 2^{48} \approx 2.8 \times 10^{14} 可能的MAC地址总数=248≈2.8×1014
以太网帧数据字段约束:
46 ≤ Data字段长度(字节) ≤ 1500 46 \leq \text{Data字段长度(字节)} \leq 1500 46≤Data字段长度(字节)≤1500
ARP vs DNS 的核心区别:
DNS : 主机名 → 全球 IP地址 \text{DNS}: \text{主机名} \xrightarrow{\text{全球}} \text{IP地址} DNS:主机名全球IP地址
ARP : IP地址 → 仅限同一子网 MAC地址 \text{ARP}: \text{IP地址} \xrightarrow{\text{仅限同一子网}} \text{MAC地址} ARP:IP地址仅限同一子网MAC地址
广播 MAC 地址:
广播MAC = FF-FF-FF-FF-FF-FF = 111 … 1 ⏟ 48 个 1 \text{广播MAC} = \texttt{FF-FF-FF-FF-FF-FF} = \underbrace{111\ldots1}_{48\text{个}1} 广播MAC=FF-FF-FF-FF-FF-FF=48个1
111…1
6.4.3 链路层交换机 & 6.4.4 虚拟局域网(VLAN)详解
一、交换机是什么?整体概念
交换机的核心工作:接收帧,决定往哪转发,然后转出去。
关键特性:对主机/路由器透明。主机发帧时,只写目的MAC地址,完全不知道中间有没有交换机,交换机默默完成转发工作。
主机A 交换机 主机B
| | |
|-- 帧(目的=B的MAC) -->| |
| |-- 查表,从接口3转出 ->|
| | |
(主机A根本不知道交换机的存在,就像它不存在一样)
二、转发与过滤(Forwarding & Filtering)
2.1 两个核心概念
- 过滤(Filtering):决定帧该不该转发,如果源和目的在同一接口侧,就直接丢弃,不浪费带宽
- 转发(Forwarding):决定帧该转发到哪个接口,然后送出去
这两个功能都依赖交换表(Switch Table)。
2.2 交换表结构
每条表项包含三个字段:
| MAC地址 | 接口编号 | 记录时间 |
|---|---|---|
| 62-FE-F7-11-89-A3 | 1 | 9:32 |
| 7C-BA-B2-B4-91-10 | 3 | 9:36 |
| … | … | … |
表项含义:“这个MAC地址的设备,在接口X那个方向”。
2.3 转发决策的三种情况
假设目的MAC地址为 DD-DD-DD-DD-DD-DD,从接口 x x x 到达交换机:
情况一:表中没有该MAC地址
→ 广播!把帧复制一份,从除了接口x以外的所有接口发出去
→ 类似"大声喊:谁是DD-DD-DD-DD-DD-DD?"
情况二:表中有该MAC,且对应接口 = x(来的接口)
→ 过滤!直接丢弃这个帧
→ 说明目的设备和源设备在同一个LAN段,帧已经在那个段广播过了,
交换机不需要再转发
情况三:表中有该MAC,且对应接口 = y(y ≠ x)
→ 转发!把帧送到接口y的输出缓冲区
→ 精确投递,只有接口y那侧能收到
用 ASCII 演示三种情况:
[交换机]
/ | \
接口1 接口2 接口3
| | |
主机A 主机B 主机C
62-FE... 01-12... 7C-BA...
场景:主机B 发帧给 主机A(目的=62-FE-F7-11-89-A3)
→ 帧从接口2到达
→ 查表:62-FE... 在接口1
→ 接口2 ≠ 接口1 → 情况三:转发到接口1
场景:主机A 发帧给 主机A(自己发给自己,极端情况,来自接口1)
→ 查表:62-FE... 在接口1
→ 接口1 = 接口1 → 情况二:过滤丢弃
场景:主机B 发帧给 未知MAC
→ 查表:没有该MAC
→ 情况一:广播到接口1和接口3
三、自学习(Self-Learning)
交换机不需要手动配置交换表!它能自动、动态、自主地学习建表。
3.1 自学习过程
三条规则:
- 初始时,交换表为空
- 每收到一个帧,就把源MAC地址 + 来自哪个接口 + 当前时间记入表中
- 如果某条记录超过老化时间(aging time)没有被更新,就删除
时间线演示:
9:39 — 交换机接口2收到一帧,源MAC = 01-12-23-34-45-56
→ 交换表新增:01-12-23-34-45-56 | 接口2 | 9:39
交换表变化:
[before] [after]
62-FE-F7-11-89-A3 | 1 | 9:32 62-FE-F7-11-89-A3 | 1 | 9:32
7C-BA-B2-B4-91-10 | 3 | 9:36 7C-BA-B2-B4-91-10 | 3 | 9:36
01-12-23-34-45-56 | 2 | 9:39
10:32 — 62-FE-F7-11-89-A3 超过60分钟未出现(最后记录9:32)
→ 自动删除该条目
3.2 自学习的好处
- 即插即用(Plug-and-Play):管理员插上网线就能用,不需要任何配置
- 自适应:设备换了网卡(MAC地址变了),旧记录老化删除,新记录自动学习
- 全双工:每个接口都可以同时收发
四、交换机的优点
消除碰撞
在纯交换机网络中(没有集线器Hub),完全没有碰撞!
交换机缓存帧,同一时刻每条链路上最多只有一帧在传输。
交换机总吞吐量 = ∑ i 接口 i 的速率 \text{交换机总吞吐量} = \sum_i \text{接口}_i \text{ 的速率} 交换机总吞吐量=i∑接口i 的速率
例:一台4口交换机,每口1Gbps,总吞吐量可达4Gbps(全双工时8Gbps)。
而集线器(Hub)所有端口共享带宽,就像一条共享的高速公路,大家都在抢,碰撞频发。
支持异构链路
不同接口可以跑不同速率、不同介质:
交换机
├── 接口1: 1Gbps 铜缆(1000BASE-T)
├── 接口2: 1Gbps 铜缆(1000BASE-T)
├── 接口3: 100Mbps 光纤(100BASE-FX)
└── 接口4: 100Mbps 铜缆(100BASE-T)
交换机在接口之间"翻译",屏蔽了速率差异(通过缓冲区)。
便于管理
- 检测到"jabber"(某适配器不停乱发帧)时,可以自动断开那个接口
- 只有一根线断了,只影响那台设备,不像同轴电缆断了全网瘫痪
- 可以统计各接口的带宽使用率、碰撞率,供管理员分析
五、交换机 vs 路由器(Switch vs Router)
对应教材图6.24(协议栈处理层次图):
主机(Host) 交换机(Switch) 路由器(Router)
+-------------+ +-------------+ +-------------+
| Application | | | | |
| Transport | | | | |
| Network | | | | Network | ← 处理到这层
| Link | | Link | ← 处理到这层
| Physical | | Physical | | Physical |
+-------------+ +-------------+ +-------------+
交换机:只看第2层(MAC地址),处理更快
路由器:要看第3层(IP地址),处理稍慢但功能更强
对比表格
| 特性 | 集线器(Hub) | 交换机(Switch) | 路由器(Router) |
|---|---|---|---|
| 流量隔离 | 无 | 有(按MAC) | 有(按IP) |
| 即插即用 | 是 | 是 | 否(需配IP) |
| 最优路由 | 否 | 否(受生成树限制) | 是 |
| 防广播风暴 | 否 | 否 | 是 |
| 处理速度 | 最快(无逻辑) | 快 | 较慢 |
什么时候用交换机?什么时候用路由器?
网络规模小(几百台主机)→ 用交换机
原因:即插即用,配置简单,够用
网络规模大(几千台主机)→ 用路由器(+交换机)
原因:
1. 路由器提供更强的流量隔离
2. 路由器能抑制广播风暴
3. 路由器支持最优路径(不受生成树约束)
交换机的缺点补充
- 生成树限制:为了防止广播帧成环,交换网络的活跃拓扑必须是一棵生成树,某些冗余链路会被关闭,无法利用最优路径
- ARP表膨胀:大型交换网络中,每台主机都需要维护庞大的ARP表,ARP广播流量很大
- 广播风暴:如果一台主机失控,不断发以太网广播帧,交换机会转发所有这些帧,导致全网瘫痪
六、安全话题:交换机投毒攻击(Switch Poisoning)
在交换网络中,正常情况下主机C只能收到发给自己的帧,很难窃听A和B的通信。
攻击方式(Switch Poisoning / MAC Flooding):
攻击者发送大量帧,每帧的源MAC地址都是随机伪造的
→ 交换表被填满(表项有限,通常几千到几万条)
→ 真实MAC地址的表项被挤出
→ 交换机找不到目的MAC,退化为广播
→ 攻击者的嗅探器能接收到所有广播的帧
→ 成功监听网络流量
正常状态: 投毒后:
交换表(少量真实记录) 交换表(全是假记录)
A | 接口1 AA:BB:.. | 接口3
B | 接口2 CC:DD:.. | 接口1
C | 接口3 EE:FF:.. | 接口2
...(几千条假记录)
→ 精准转发,C看不到A→B的帧 → 退化广播,C能看到所有帧
七、VLAN(虚拟局域网)详解
7.1 为什么需要 VLAN?
传统层次化交换网络有三个痛点:
痛点1:流量隔离不彻底
广播帧(如 ARP、DHCP)会传遍整个机构网络,隔壁部门的人可以用 Wireshark 嗅探你的帧。
痛点2:交换机利用率低
10个部门就需要10个一级交换机,即使每个部门只有5个人,也要10台交换机。
痛点3:用户移动管理麻烦
员工换部门,需要重新插网线,非常麻烦。如果某人同属两个部门,问题更复杂。
7.2 VLAN 的解决方案
VLAN 允许在一台物理交换机上划出多个虚拟局域网,每个 VLAN 就是一个独立的广播域。
基于端口的 VLAN(Port-based VLAN)(最常见):
一台16口交换机:
端口1 → 未分配
端口2~8 → EE(电气工程)VLAN
端口9~15 → CS(计算机科学)VLAN
端口16 → 未分配
效果:
EE部门发广播 → 只在端口2~8之间广播,CS那边收不到
CS部门发广播 → 只在端口9~15之间广播,EE那边收不到
ASCII 示意图:
[一台物理交换机,16个端口]
端口: 1 2 3 4 5 6 7 8 | 9 10 11 12 13 14 15 | 16
未 [===== EE VLAN =====] [====== CS VLAN ====] 未
| | | | | | | | | | | |
PC PC PC PC PC PC PC PC PC PC PC PC
(电气工程部门) (计算机科学部门)
EE广播 CS广播
↓↓↓ 只在这里传 ↓↓↓ ↓↓↓ 只在这里传 ↓↓↓
[端口2-8内部] [端口9-15内部]
7.3 VLAN 如何解决三个痛点
| 痛点 | VLAN 解决方案 |
|---|---|
| 流量隔离不彻底 | 广播域隔开,EE的广播到不了CS |
| 交换机利用率低 | 两个部门共用一台交换机,节省硬件 |
| 用户移动麻烦 | 只需软件配置"把端口8改为CS VLAN",不需要重新插网线 |
7.4 VLAN 间通信:需要路由器
问题:EE和CS完全隔离了,如果要相互通信怎么办?
方法:将交换机上一个端口(如端口1)连接到外部路由器,并把该端口配置为同时属于EE和CS两个VLAN。
[路由器]
/ \
EE接口 CS接口
\ /
[VLAN交换机的端口1]
|
[VLAN交换机]
端口2~8: EE 端口9~15: CS
EE主机 → CS主机 的流程:
EE主机发包给CS主机(不同网段)
→ 包先在EE VLAN内到达路由器的EE接口
→ 路由器查IP路由表,从CS接口转发
→ 包在CS VLAN内到达CS主机
现代交换机通常内置了路由功能(三层交换机),不需要外接路由器。
7.5 VLAN Trunk(主干链路)
问题:如果两台VLAN交换机分别在不同楼,要互联怎么办?
笨方法(图a):每个VLAN都用一根专门的线连接两台交换机。有N个VLAN就要N根线。
交换机A 交换机B
|-- EE专用线 ----------->|
|-- CS专用线 ----------->|
(有几个VLAN就要几根线,不可扩展)
聪明方法:VLAN Trunk(图b):两台交换机之间只用一根 Trunk 链路,所有VLAN的帧都走这一根线,但帧上打标签表明自己属于哪个VLAN。
交换机A 交换机B
端口2~8: EE VLAN 端口2,3,6: EE VLAN
端口9~15: CS VLAN 端口4,5,7: CS VLAN
端口16: Trunk口 ----Trunk链路---端口1: Trunk口
EE帧走Trunk时打上"EE标签"
CS帧走Trunk时打上"CS标签"
到达交换机B后,根据标签分发到对应VLAN端口
7.6 802.1Q 帧格式(VLAN标签)
原始以太网帧在 Trunk 上传输时,需要在帧头插入一个 4字节的 VLAN 标签。
原始以太网帧:
+----------+----------+----------+------+------+-----+
| Preamble | Dest MAC | Src MAC | Type | Data | CRC |
+----------+----------+----------+------+------+-----+
802.1Q VLAN帧(插入4字节标签):
+----------+----------+----------+------+------+------+------+-----+
| Preamble | Dest MAC | Src MAC | TPID | TCI | Type | Data | CRC*|
+----------+----------+----------+------+------+------+------+-----+
↑2字节 ↑2字节
81-00 包含VLAN ID(12位)+优先级(3位)
* CRC 需要重新计算(因为帧内容变了)
各字段说明:
| 字段 | 大小 | 说明 |
|---|---|---|
| TPID(Tag Protocol Identifier) | 2字节 | 固定值 0x8100,表示"这是一个VLAN帧" |
| TCI(Tag Control Information) | 2字节 | 包含:3位优先级 + 1位CFI + 12位VLAN ID |
| VLAN ID | 12位(在TCI中) | 2 12 = 4096 2^{12}=4096 212=4096 个VLAN,即最多支持4094个(0和4095保留) |
VLAN ID 范围: 0 ≤ VLAN ID ≤ 4095 0 \leq \text{VLAN ID} \leq 4095 0≤VLAN ID≤4095,有效范围 1 1 1 到 4094 4094 4094。
Trunk 工作流程:
发送端交换机(左) 接收端交换机(右)
收到EE VLAN的帧
→ 在帧头插入VLAN标签(VLAN ID = EE的编号)
→ 从Trunk口发出
--------帧+标签-------->
→ 解析VLAN标签
→ 知道是EE VLAN
→ 去掉标签
→ 转发到EE VLAN的端口
7.7 其他类型的 VLAN
除了基于端口的VLAN,还有:
- 基于MAC地址的VLAN:管理员指定哪些MAC地址属于哪个VLAN,设备插到任何端口都自动归入正确的VLAN
- 基于网络层协议的VLAN:按IPv4、IPv6或其他协议划分
- 跨路由器的VLAN(VXLAN):让VLAN可以跨越更大地理范围,覆盖不同地点的局域网,形成一个全球性的虚拟局域网
八、知识结构总览
九、完整 C++ 模拟代码:交换机自学习 + 转发
#include <iostream>
#include <map>
#include <string>
#include <vector>
#include <ctime>
#include <iomanip>
using namespace std;
// 交换表中的一条记录
struct SwitchEntry {
int interface; // 该MAC地址对应的接口编号
time_t timestamp; // 记录创建/更新时间
};
// 模拟交换机类
class Switch {
private:
// 交换表:MAC地址 -> 表项
map<string, SwitchEntry> table;
int numInterfaces; // 接口数量
int agingTime; // 老化时间(秒)
int currentTime; // 模拟时钟(用整数秒代替真实时间)
// 打印当前交换表
void printTable() const {
cout << "\n当前交换表:\n";
cout << left << setw(22) << "MAC地址"
<< setw(8) << "接口"
<< setw(10) << "记录时间(s)" << endl;
cout << string(40, '-') << endl;
if (table.empty()) {
cout << "(表为空)\n";
}
for (const auto& entry : table) {
cout << left << setw(22) << entry.first
<< setw(8) << entry.second.interface
<< setw(10) << entry.second.timestamp << endl;
}
cout << endl;
}
// 删除超时的表项(老化机制)
void aging() {
vector<string> toDelete;
for (const auto& entry : table) {
// 如果超过老化时间没有更新,标记删除
if (currentTime - entry.second.timestamp > agingTime) {
toDelete.push_back(entry.first);
}
}
for (const auto& mac : toDelete) {
cout << " [老化删除] MAC=" << mac
<< ",距上次更新已超过" << agingTime << "秒\n";
table.erase(mac);
}
}
public:
// 构造函数
// interfaces: 接口数量
// aging: 老化时间(秒)
Switch(int interfaces, int aging)
: numInterfaces(interfaces), agingTime(aging), currentTime(0) {
cout << "交换机初始化:" << interfaces << "个接口,老化时间="
<< aging << "秒\n\n";
}
// 设置当前模拟时间
void setTime(int t) {
currentTime = t;
// 每次时间推进都检查老化
aging();
}
// 处理一个到来的帧
// srcMAC: 帧的源MAC地址
// dstMAC: 帧的目的MAC地址
// inIface: 帧从哪个接口到来
void processFrame(const string& srcMAC, const string& dstMAC, int inIface) {
cout << "=== 时间=" << currentTime << "s,接口" << inIface
<< " 收到帧 ===\n";
cout << " 源MAC: " << srcMAC << "\n";
cout << " 目的MAC: " << dstMAC << "\n";
// ---- 自学习:记录源MAC地址和来源接口 ----
if (table.find(srcMAC) == table.end()) {
// 新MAC,新增记录
cout << " [自学习] 新增记录:" << srcMAC
<< " -> 接口" << inIface << "\n";
} else if (table[srcMAC].interface != inIface) {
// MAC地址换了接口(设备移动了),更新记录
cout << " [自学习] 更新记录:" << srcMAC
<< " 从接口" << table[srcMAC].interface
<< " 移动到接口" << inIface << "\n";
} else {
// 刷新时间戳
cout << " [自学习] 刷新时间戳:" << srcMAC << "\n";
}
table[srcMAC] = {inIface, currentTime};
// ---- 转发/过滤决策 ----
auto it = table.find(dstMAC);
if (it == table.end()) {
// 情况一:目的MAC不在表中 → 广播
cout << " [转发决策] 目的MAC未知 → 广播到除接口"
<< inIface << "以外的所有接口:";
for (int i = 1; i <= numInterfaces; i++) {
if (i != inIface) cout << "接口" << i << " ";
}
cout << "\n";
} else if (it->second.interface == inIface) {
// 情况二:目的MAC在表中,且与来源接口相同 → 过滤
cout << " [转发决策] 目的MAC(" << dstMAC
<< ")与来源在同一接口" << inIface << " → 过滤丢弃\n";
} else {
// 情况三:目的MAC在表中,且接口不同 → 精确转发
cout << " [转发决策] 目的MAC(" << dstMAC
<< ")在接口" << it->second.interface
<< " → 精确转发到接口" << it->second.interface << "\n";
}
printTable();
}
};
int main() {
// 创建一台4接口、老化时间60秒的交换机
Switch sw(4, 60);
// 模拟场景:
// 接口1 <-> 主机A (MAC: AA-AA-AA-AA-AA-AA)
// 接口2 <-> 主机B (MAC: BB-BB-BB-BB-BB-BB)
// 接口3 <-> 主机C (MAC: CC-CC-CC-CC-CC-CC)
// 接口4 <-> 主机D (MAC: DD-DD-DD-DD-DD-DD)
cout << "==== 场景1:主机A发帧给主机B,目的MAC未知 ====\n";
sw.setTime(10);
// A→B:A在接口1,表中无B的记录
sw.processFrame("AA-AA-AA-AA-AA-AA", "BB-BB-BB-BB-BB-BB", 1);
cout << "==== 场景2:主机B回帧给主机A,已知A在接口1 ====\n";
sw.setTime(12);
// B→A:B在接口2,学习B;A已知在接口1,精确转发
sw.processFrame("BB-BB-BB-BB-BB-BB", "AA-AA-AA-AA-AA-AA", 2);
cout << "==== 场景3:主机C发帧给主机A(过滤测试)====\n";
sw.setTime(15);
// 故意让C从接口1发(模拟同一LAN段的情况,目的A也在接口1)
sw.processFrame("CC-CC-CC-CC-CC-CC", "AA-AA-AA-AA-AA-AA", 1);
cout << "==== 场景4:等待老化(时间跳到75秒)====\n";
sw.setTime(75);
// AA-AA那条记录是时间10s加的,75-10=65>60,应该被老化删除
sw.processFrame("DD-DD-DD-DD-DD-DD", "AA-AA-AA-AA-AA-AA", 4);
return 0;
}
预期输出摘要:
场景1: A→B,B未知 → 广播到接口2,3,4;自学习记录A在接口1
场景2: B→A,A已知在接口1 → 精确转发到接口1;自学习记录B在接口2
场景3: C→A,C和A都在接口1 → 过滤丢弃;自学习记录C在接口1
场景4: 时间75s,A的记录(时间10s)超过60s → 老化删除;再次广播
十、VLAN 核心概念速查
| 术语 | 解释 |
|---|---|
| VLAN | 在一台物理交换机上划分多个虚拟局域网,每个VLAN是独立广播域 |
| 基于端口的VLAN | 管理员指定哪些端口属于哪个VLAN |
| Trunk 链路 | 连接两台VLAN交换机的特殊链路,承载多个VLAN的帧 |
| 802.1Q | IEEE标准,定义VLAN标签格式,在以太网帧中插入4字节标签 |
| TPID | Tag Protocol Identifier,固定值0x8100,标识VLAN帧 |
| VLAN ID | 12位,标识帧属于哪个VLAN,最多支持 2 12 − 2 = 4094 2^{12}-2=4094 212−2=4094 个VLAN |
| 三层交换机 | 内置路由功能的交换机,可以在不同VLAN间路由IP数据报 |
| VXLAN | 可扩展虚拟局域网,允许VLAN跨越更大地理范围 |
6.5 链路虚拟化(MPLS & VXLAN)& 6.6 数据中心网络 详解
一、链路虚拟化的核心思想
什么是链路虚拟化?
把一个复杂的底层网络"伪装"成一条简单的链路。上层设备看到的就是一条线,根本不知道下面有多复杂。
现实: 上层看到的:
[A]--路由器--路由器--路由器--[B] [A]==========[B]
复杂的MPLS/IP网络 一条"虚拟链路"
虚拟化的层次演进:
物理线缆(一根铜线)
↓ 抽象
共享介质(多主机共用一根线/无线电频谱)
↓ 抽象
交换以太网(多台交换机组成的复杂基础设施)
↓ 抽象
VLAN(逻辑隔离的虚拟局域网)
↓ 抽象
MPLS(将复杂的路由器网络变成"虚拟链路")
↓ 抽象
VXLAN(通过互联网把不同地点的局域网连成一个虚拟局域网)
每一层,主机都"以为"自己连接的是一条简单的线,不知道下面的复杂性。
二、MPLS(多协议标签交换)
2.1 MPLS 是什么?为什么需要它?
MPLS(Multiprotocol Label Switching,多协议标签交换)诞生于1990年代中后期。
传统IP路由的问题:
每次转发都需要查找目的IP地址,做最长前缀匹配(Longest Prefix Match)。这个操作比较慢,尤其是路由表很大时。
传统IP转发:查找 目的IP(32位) ⏟ 最长前缀匹配,慢 ⇒ 输出接口 \text{传统IP转发:查找} \underbrace{\text{目的IP(32位)}}_{\text{最长前缀匹配,慢}} \Rightarrow \text{输出接口} 传统IP转发:查找最长前缀匹配,慢
目的IP(32位)⇒输出接口
MPLS 的解决方案:
用一个固定长度的标签(Label)来转发,查固定值比做最长前缀匹配快得多。
MPLS转发:查找 标签(固定长度) ⏟ 精确匹配,快 ⇒ 新标签 + 输出接口 \text{MPLS转发:查找} \underbrace{\text{标签(固定长度)}}_{\text{精确匹配,快}} \Rightarrow \text{新标签 + 输出接口} MPLS转发:查找精确匹配,快
标签(固定长度)⇒新标签 + 输出接口
重要:MPLS 不是要替代 IP,而是增强它。MPLS 与 IP 协同工作,在 IP 层和链路层之间插入一个标签头。
2.2 MPLS 帧格式(对应图1)
MPLS 头部插在链路层头(Ethernet/PPP)和网络层头(IP)之间:
+------------------+-------------+----------+---------------------------+
| PPP或Ethernet头 | MPLS 头部 | IP 头部 | 链路层帧其余部分(数据) |
+------------------+-------------+----------+---------------------------+
↓ 展开
+--------+-----+---+-----+
| Label | Exp | S | TTL |
| (20位) |(3位)|(1)|(8位)|
+--------+-----+---+-----+
各字段含义:
| 字段 | 大小 | 说明 |
|---|---|---|
| Label(标签) | 20位 | 转发依据,取值范围 0 0 0 到 2 20 − 1 2^{20}-1 220−1 |
| Exp(实验位) | 3位 | 保留供实验用,可用于服务质量(QoS)标记 |
| S(栈底位) | 1位 | 标识是否是最后一个标签(MPLS支持标签栈,S=1表示栈底) |
| TTL(生存时间) | 8位 | 类似IP的TTL,防止帧无限循环 |
标签栈(Label Stack)概念(了解即可):
[Ethernet头][标签3][标签2][标签1(S=1)][IP头][数据]
↑外层 ↑中间 ↑最内层(栈底)
可以嵌套多层标签,实现更复杂的流量工程(如VPN)。
2.3 标签交换路由器(LSR)
能处理 MPLS 帧的路由器叫做标签交换路由器(Label-Switched Router, LSR)。
LSR 转发的步骤:
- 收到帧,读取 MPLS 标签
- 查自己的转发表(按标签精确匹配)
- 把标签换成新标签,从指定接口发出
这整个过程不需要看 IP 地址,速度很快。
2.4 MPLS 转发示例(对应图2)
网络拓扑:
R5,R6(普通IP路由器)
\
R4(MPLS LSR)---接口0---R3(MPLS LSR)---接口0--- D
\ \---接口1---R1(MPLS LSR)--接口0---A
---接口1---R2(MPLS LSR)---接口0---R1
各路由器转发表(从图2读取):
R4 的转发表:
| 入标签(in) | 出标签(out) | 目的 | 出接口 |
|---|---|---|---|
| (无,R5/R6来的包打上标签) | 10 | A | 接口0→R3 |
| 12 | D | 接口0→R3 | |
| 8 | A | 接口1→R2 |
R3 的转发表:
| 入标签 | 出标签 | 目的 | 出接口 |
|---|---|---|---|
| 10 | 6 | A | 接口1→R1 |
| 12 | 9 | D | 接口0 |
R2 的转发表:
| 入标签 | 出标签 | 目的 | 出接口 |
|---|---|---|---|
| 8 | 6 | A | 接口0→R1 |
R1 的转发表:
| 入标签 | 出标签 | 目的 | 出接口 |
|---|---|---|---|
| 6 | — | A | 接口0(直连A,去掉标签) |
从 R5 发数据到 A 的转发过程:
R5 → R4:普通IP包(目的=A)
R4:打上标签10(或8,有两条路),走接口0→R3(或接口1→R2)
路径一:经由 R3
R4[标签10] → R3:入标签10,换成标签6,走接口1 → R1
R3[标签6] → R1:入标签6,去掉标签,从接口0直接交给A
路径二:经由 R2
R4[标签8] → R2:入标签8,换成标签6,走接口0 → R1
R2[标签6] → R1:入标签6,去掉标签,从接口0直接交给A
关键观察:R4 到 A 有两条 MPLS 路径!传统 IP 路由只能选一条(最短路),而 MPLS 可以让两条路都用,实现流量工程(Traffic Engineering)。
2.5 MPLS 的核心优势
1. 流量工程(Traffic Engineering)
IP 路由只选最短路,MPLS 可以绕开最短路,把流量分发到多条路径:
网络拓扑: IP路由: MPLS流量工程:
A → B 有两条路 只走一条 两条路都用
路1:A-X-Y-B(1Gbps) → 路1(最短) → 路1承担60%流量
路2:A-P-Q-B(1Gbps) → 路2承担40%流量
浪费路2带宽 最大化利用带宽
2. 快速故障恢复
预先计算好备用路径,链路断了立即切换到备用路径,不需要等待路由协议重新收敛(可能要几秒到几十秒)。
3. VPN 实现
ISP 用 MPLS 把企业分散各地的网络连起来,形成虚拟专用网络(VPN),流量在 MPLS 网络内隔离,就像专线一样。
2.6 MPLS 与 SDN 的关系
MPLS 在 SDN 出现之前就已经很成熟了。SDN 的通用转发(OpenFlow)也能实现 MPLS 的很多功能。未来 MPLS 和 SDN 是共存还是 SDN 取代 MPLS,目前尚无定论。
三、VXLAN(可扩展虚拟局域网)
3.1 VLAN 的局限性
传统 802.1Q VLAN 有两大局限:
| 局限 | 原因 | 影响 |
|---|---|---|
| 最多4094个VLAN | VLAN ID 只有12位, 2 12 = 4096 2^{12}=4096 212=4096,去掉0和4095 | 大型数据中心远远不够用 |
| 必须在同一个以太网基础设施内 | VLAN 标签只在第2层传播 | 无法跨越互联网连接不同地点 |
现代数据中心可能有几十万台虚拟机,每个租户需要独立的虚拟网络,4094个远远不够。
3.2 VXLAN 的解决方案
VXLAN(Virtual eXtensible LAN,可扩展虚拟局域网,RFC 7348)的核心思路:
把整个以太网帧塞进UDP包里,通过互联网传输。
VXLAN封装 = IP数据报 [ UDP段 [ VXLAN头 + 原始以太网帧 ⏟ 完整保留 ] ] \text{VXLAN封装} = \text{IP数据报}\left[\text{UDP段}\left[\text{VXLAN头} + \underbrace{\text{原始以太网帧}}_{\text{完整保留}}\right]\right] VXLAN封装=IP数据报[UDP段[VXLAN头+完整保留
原始以太网帧]]
用 VNI(VXLAN Network Identifier)代替 VLAN ID:
VNI 为 24 位 ⇒ 2 24 = 16 , 777 , 216 ≈ 1600 万个虚拟局域网 \text{VNI 为 24 位} \Rightarrow 2^{24} = 16,777,216 \approx 1600\text{万个虚拟局域网} VNI 为 24 位⇒224=16,777,216≈1600万个虚拟局域网
对比: VLAN ID 12位 ⇒ 4094 个 \text{VLAN ID 12位} \Rightarrow 4094 \text{个} VLAN ID 12位⇒4094个,VXLAN 多出约 4000 倍!
3.3 VXLAN 工作原理(对应图3)
场景:Sunnyvale(加州)的主机A 要发以太网帧给 Bangalore(印度)的主机B。
Sunnyvale Bangalore
[主机A] [主机B]
| |
| 以太网帧(目的MAC=主机B的MAC) |
↓ ↓
[VLAN交换机] [VLAN交换机]
| |
[VTEP x]---------------互联网--------------[VTEP y]
(VXLAN隧道端点) VXLAN隧道 (VXLAN隧道端点)
封装过程(VTEP x 做的事):
步骤1:收到主机A发给主机B的原始以太网帧
步骤2:在以太网帧前加上 VXLAN 头(包含VNI)
步骤3:把 [VXLAN头+以太网帧] 放进 UDP 数据段
步骤4:把 UDP段 放进 IP数据报(目的IP = VTEP y 的IP地址)
步骤5:把这个大IP包发送出去,互联网正常传递
最终封装结构:
+----------+----------+----------+-------------+--------------+
| Eth头 | IP头 | UDP头 | VXLAN头(VNI)| 原始以太网帧 |
| 到VTEP y | 到VTEP y | 到VTEP y | 24位VNI |src=A, dst=B |
+----------+----------+----------+-------------+--------------+
外层(互联网传输用) ↑内层(原始帧,完整保留)
解封装过程(VTEP y 做的事):
步骤1:收到IP数据报(互联网正常投递,不知道里面是什么)
步骤2:发现是UDP,发现UDP里是VXLAN格式
步骤3:读取VNI,确认属于哪个虚拟局域网
步骤4:取出内层的原始以太网帧
步骤5:把以太网帧发到Bangalore局域网
步骤6:主机B收到帧,就像本地局域网直接发来的一样
整个过程,主机A 和主机B 完全不知道中间经过了互联网! 对它们来说,就像通过一根局域网线直接连着一样。
3.4 VTEP(VXLAN隧道端点)
VTEP 就是执行封装/解封装的设备,可以是:
- 普通物理交换机或路由器(固件支持VXLAN)
- 数据中心里的虚拟交换机(软件实现)
3.5 VXLAN 的封装层次(理解"网络套网络")
主机A视角: 我在给主机B发一个以太网帧
以太网帧实际经历: 以太网帧 → 被塞进UDP → 被塞进IP → 穿越互联网 → 从IP取出 → 从UDP取出 → 以太网帧到达B
类比: 快递盒(以太网帧)
被装进大盒子(UDP)
被装进更大的盒子(IP数据报)
通过物流网络运输(互联网)
拆开大盒子,取出快递盒,交给收件人
这就是**隧道(Tunneling)**的本质:用一种协议的包来承载另一种协议的包。
四、数据中心网络(Data Center Networking)
4.1 数据中心是什么?
谷歌、微软、亚马逊、阿里巴巴等公司建有巨型数据中心,每个容纳数万到数十万台服务器。
数据中心的三大用途:
- 内容服务:网页、搜索、AI聊天机器人、流媒体视频
- 大规模并行计算:分布式搜索索引、大数据处理
- 云计算:为其他公司提供AWS、Azure、阿里云等服务
数据中心成本构成(2024年):
总成本 = 45 % ⏟ 服务器主机 + 25 % ⏟ 基础设施(电源、冷却) + 15 % ⏟ 电力消耗 + 15 % ⏟ 网络(交换机、路由器、链路) \text{总成本} = \underbrace{45\%}_{\text{服务器主机}} + \underbrace{25\%}_{\text{基础设施(电源、冷却)}} + \underbrace{15\%}_{\text{电力消耗}} + \underbrace{15\%}_{\text{网络(交换机、路由器、链路)}} 总成本=服务器主机 45%+基础设施(电源、冷却) 25%+电力消耗 15%+网络(交换机、路由器、链路) 15%
网络虽然不是最大成本,但网络创新是降低总成本、提升性能的关键。
4.2 数据中心基本组件
服务器(Blade/刀片服务器):
- 像披萨盒形状,包含CPU、内存、磁盘
- 叠放在机架(Rack)中,每个机架通常有20~40台
机架顶部交换机(TOR Switch,Top of Rack): - 每个机架顶部一台交换机
- 连接机架内所有服务器 + 连接其他交换机
- 服务器到TOR的链路:40Gbps 或 100Gbps 以太网
4.3 层次化架构(对应图4)
互联网(Internet)
|
[边界路由器] ← 连接数据中心与公网
/ \
[接入路由器] [接入路由器] ← 可有多个
| \ / |
[负载均衡] [一层交换机]... [负载均衡]
| |
[二层交换机] [二层交换机] ← 汇聚层
/ | | | | \
[TOR][TOR][TOR][TOR][TOR][TOR] ← 接入层(机架顶部)
| | | | | |
机架1 机架2 机架3 机架4 机架5 机架6 ← 服务器机架
**负载均衡器(Load Balancer)**的作用:
- 外部请求发到一个公共IP地址
- 负载均衡器把请求分发到不同的内部服务器
- 实现 NAT 功能:公网IP ↔ 内部IP 转换
- 有时被叫做"4层交换机",因为它看第4层(TCP/UDP端口号)来做决策
- 安全效果:隐藏内部网络结构,客户端无法直接访问服务器
4.4 层次化架构的瓶颈问题
考虑以下场景:
- 服务器到TOR:10Gbps
- 交换机之间:100Gbps
- 机架1中10台服务器各向机架5中对应服务器发流量
- 同时有机架2→6,机架3→7,机架4→8,共40条并发流
40条流共享A-B链路(100Gbps) ⇒ 每条流只能得到 100 Gbps 40 = 2.5 Gbps \text{40条流共享A-B链路(100Gbps)} \Rightarrow \text{每条流只能得到} \frac{100\text{Gbps}}{40} = 2.5\text{Gbps} 40条流共享A-B链路(100Gbps)⇒每条流只能得到40100Gbps=2.5Gbps
这远小于服务器的10Gbps网卡速率,浪费严重!上层链路成为瓶颈。
4.5 解决瓶颈的方案
方案一:用更高速的交换机
把100Gbps换成400Gbps的交换机。代价:高端交换机价格极其昂贵。
方案二:就近部署(减少跨机架通信)
把相互通信频繁的服务放在同一机架或相邻机架。局限:很多应用需要数千台服务器分布各处。
方案三:增加层间连接(对应图5)
让每个TOR交换机连接多个二层交换机,每个二层交换机连接多个一层交换机,形成高度互联的多路径拓扑:
原来:TOR → 1条线 → 二层交换机(瓶颈!)
现在:TOR → 多条线 → 多台二层交换机(多路径!)
如果每个TOR连接2台二层交换机,两台TOR之间就有4条不同路径,总容量变成 4 × 100 Gbps = 400 Gbps 4 \times 100\text{Gbps} = 400\text{Gbps} 4×100Gbps=400Gbps。
Facebook的数据中心:
- 每个TOR连接16台二层交换机
- 每台二层交换机连接16台一层交换机
多路径路由(ECMP):
ECMP(Equal Cost Multi-Path,等价多路径):对多条等价路径随机选择下一跳,实现负载均衡。
4.6 叶脊拓扑(Leaf-Spine,对应图6)
三层层次架构进一步演化为两层叶脊拓扑:
[Spine交换机1][Spine交换机2][Spine交换机3]...[Spine交换机N]
↕每个Leaf都连到每个Spine↕
[Leaf交换机1][Leaf交换机2][Leaf交换机3]...[Leaf交换机M]
| | |
机架1 机架2 机架3
叶脊拓扑的关键特性:
- 每个Leaf连接所有Spine(全连接)
- 数据中心内任意两台服务器通信:恰好经过2次交换跳(Leaf→Spine→Leaf)
- 扩展方式:加Spine节点(横向扩展带宽),加Leaf节点(增加服务器数量)
对比传统三层架构:
| 特性 | 传统三层 | 叶脊拓扑 |
|---|---|---|
| 层数 | 3层(TOR/二层/一层) | 2层(Leaf/Spine) |
| 跳数 | 可变(2~6跳) | 固定(2跳) |
| 扩展性 | 有瓶颈 | 横向扩展方便 |
| 路径数 | 少 | 多(全连接) |
东西流量 vs 南北流量:
南北流量(North-South): 外部用户 ↔ 数据中心内部服务器
流量方向是"上下"(进出数据中心)
东西流量(East-West): 数据中心内部服务器 ↔ 服务器
流量方向是"左右"(内部横向)
现代数据中心东西流量 >> 南北流量
→ 叶脊拓扑更适合(优化内部横向通信)
4.7 Clos 网络
多层、多阶段的交换互联网络统称为 Clos 网络,以 Charles Clos(1953年在电话交换研究中提出)命名。
叶脊拓扑就是一种 Clos 网络的变体,在数据中心和多处理器互联中广泛应用。
五、数据中心网络发展趋势
5.1 SDN 集中控制
谷歌、微软、Facebook 都在拥抱 SDN 式的集中控制:
- 数据平面:简单的商用交换机
- 控制平面:软件控制器(如 Google 的 Orion 平台)
- 大规模自动化配置和状态管理
5.2 虚拟化
虚拟机(VM)和容器使软件与硬件解耦:
- VM 可以在不同物理服务器之间迁移(即使在不同机架)
- 挑战:迁移时要保持网络连接不中断
- 解决方案:把整个数据中心当作一张扁平的第2层网络
- 用类似 DNS 的查询替代 ARP 广播,维护"VM的IP地址→当前连接的物理交换机"的映射
5.3 物理约束
数据中心网络特点:
- 超高带宽(40Gbps、100Gbps 链路已成标配)
- 超低延迟(微秒级)
这给传统协议带来挑战: - TCP 拥塞控制反应太慢(设计之初是为广域网,RTT 是毫秒级)
- 小缓冲区:延迟低但容易丢包
- 解决方案:数据中心专用TCP变体(如 DCTCP)、RDMA(远程直接内存访问)
5.4 能效与碳排放
数据中心能耗( 2024 年) ≈ 400 TWh ≈ 全球电力的 1 % ∼ 2 % \text{数据中心能耗}(2024年)\approx 400\text{ TWh} \approx \text{全球电力的} 1\% \sim 2\% 数据中心能耗(2024年)≈400 TWh≈全球电力的1%∼2%
预计到2030年底升至全球能源的 3 % ∼ 4 % 3\% \sim 4\% 3%∼4%(AI应用驱动的计算需求增长)。
大型数据中心运营商正在追求:
- 能效优化:减少每单位计算的电力消耗
- 碳效率:不只是省电,而是减少碳排放(使用可再生能源、优化工作负载调度)
5.5 硬件模块化与定制化
集装箱式数据中心(MDC):
- 在标准12米集装箱内建造"微型数据中心"
- 每个集装箱:数千台服务器,几十个机架
- 优点:工厂预制,快速部署
- 设计理念:优雅降级——组件逐渐失效时性能缓慢下降,而不是突然崩溃;当失效组件超过阈值,整个集装箱被替换
自研硬件趋势:
谷歌、微软、亚马逊等云巨头越来越多地自己设计网络适配器、交换机、TOR等,使用商用芯片(merchant silicon)而非购买厂商产品。
六、知识结构总览
七、完整 C++ 模拟代码:MPLS 标签交换转发
#include <iostream>
#include <map>
#include <string>
#include <vector>
#include <iomanip>
using namespace std;
// MPLS 转发表中的一条表项
struct MPLSEntry {
int outLabel; // 出标签(-1 表示去掉标签,即到达目的地)
string dest; // 目的节点名称(用于显示)
int outInterface; // 出接口编号
};
// MPLS 数据包(简化版)
struct MPLSPacket {
int label; // 当前 MPLS 标签
string srcIP; // 源IP(模拟)
string dstIP; // 目的IP(模拟)
string payload; // 数据载荷
};
// 标签交换路由器(LSR)
class LabelSwitchedRouter {
private:
string name; // 路由器名称
map<int, MPLSEntry> forwardingTable; // MPLS 转发表:标签 → 表项
public:
LabelSwitchedRouter(const string& n) : name(n) {}
// 添加一条转发表项
// inLabel: 入标签
// outLabel: 出标签(-1 表示弹出标签,即最后一跳)
// dest: 目的节点名称
// outIface: 出接口
void addEntry(int inLabel, int outLabel, const string& dest, int outIface) {
forwardingTable[inLabel] = {outLabel, dest, outIface};
}
// 处理一个 MPLS 包,返回是否成功转发
// packet: 输入数据包(会被修改:标签被替换)
// nextRouter: 输出,下一跳路由器名称
bool forward(MPLSPacket& packet, string& nextRouter) const {
cout << "\n[" << name << "] 收到包,当前标签=" << packet.label
<< ",目的IP=" << packet.dstIP << "\n";
// 在转发表中查找当前标签
auto it = forwardingTable.find(packet.label);
if (it == forwardingTable.end()) {
cout << " [" << name << "] 错误:标签" << packet.label
<< "不在转发表中!丢弃包。\n";
return false;
}
const MPLSEntry& entry = it->second;
if (entry.outLabel == -1) {
// 最后一跳:去掉MPLS标签,把IP包交给目的地
cout << " [" << name << "] 最后一跳!弹出标签,直接交付给目的地:"
<< entry.dest << "(接口" << entry.outInterface << ")\n";
packet.label = -1; // 标签已去掉
nextRouter = entry.dest;
} else {
// 中间跳:换标签,转发到下一个LSR
cout << " [" << name << "] 标签交换:入标签=" << packet.label
<< " → 出标签=" << entry.outLabel
<< ",目的=" << entry.dest
<< ",出接口=" << entry.outInterface << "\n";
packet.label = entry.outLabel; // 替换标签
nextRouter = entry.dest;
}
return true;
}
// 打印当前路由器的转发表
void printTable() const {
cout << "路由器 " << name << " 的MPLS转发表:\n";
cout << left << setw(10) << "入标签"
<< setw(10) << "出标签"
<< setw(8) << "目的"
<< setw(8) << "出接口" << "\n";
cout << string(36, '-') << "\n";
for (const auto& e : forwardingTable) {
cout << left << setw(10) << e.first
<< setw(10) << (e.second.outLabel == -1 ? "弹出" : to_string(e.second.outLabel))
<< setw(8) << e.second.dest
<< setw(8) << e.second.outInterface << "\n";
}
cout << "\n";
}
const string& getName() const { return name; }
};
// MPLS 网络模拟器
class MPLSNetwork {
private:
// 路由器集合:名称 → LSR对象
map<string, LabelSwitchedRouter*> routers;
public:
~MPLSNetwork() {
for (auto& p : routers) delete p.second;
}
// 添加一台路由器
LabelSwitchedRouter* addRouter(const string& name) {
auto* r = new LabelSwitchedRouter(name);
routers[name] = r;
return r;
}
// 模拟一个包从 startRouter 出发,经过 MPLS 网络转发的全过程
// startRouter: 起始路由器名称
// initLabel: 初始打上的标签
// srcIP, dstIP: 源/目的IP(用于显示)
void simulate(const string& startRouter, int initLabel,
const string& srcIP, const string& dstIP) {
cout << "\n====================================\n";
cout << "模拟包从 [" << startRouter << "] 出发\n";
cout << "源IP=" << srcIP << ",目的IP=" << dstIP << "\n";
cout << "初始MPLS标签=" << initLabel << "\n";
cout << "====================================\n";
MPLSPacket pkt;
pkt.label = initLabel;
pkt.srcIP = srcIP;
pkt.dstIP = dstIP;
pkt.payload = "数据载荷";
string currentRouter = startRouter;
int hopCount = 0;
const int MAX_HOPS = 10; // 防止死循环
while (hopCount < MAX_HOPS) {
auto it = routers.find(currentRouter);
if (it == routers.end()) {
// 到达非LSR设备(如目的主机A或D)
cout << "\n包已到达目的地:" << currentRouter << "\n";
cout << "(此设备不是LSR,直接接收IP包)\n";
break;
}
string nextRouter;
bool ok = it->second->forward(pkt, nextRouter);
if (!ok) break;
currentRouter = nextRouter;
hopCount++;
// 如果标签已弹出(-1),说明已到目的地
if (pkt.label == -1) {
cout << "\n包已成功投递给目的地:" << currentRouter << "\n";
break;
}
}
if (hopCount >= MAX_HOPS) {
cout << "错误:超过最大跳数限制,可能有路由环路!\n";
}
}
};
int main() {
MPLSNetwork network;
// 根据图2构建MPLS网络
// 节点:R1, R2, R3, R4(LSR),A, D(目的主机)
auto* R1 = network.addRouter("R1");
auto* R2 = network.addRouter("R2");
auto* R3 = network.addRouter("R3");
auto* R4 = network.addRouter("R4");
// R1 转发表:入标签6 → 弹出标签,交给目的地A(接口0)
R1->addEntry(6, -1, "A", 0);
// R2 转发表:入标签8 → 出标签6,转给R1(接口0)
R2->addEntry(8, 6, "R1", 0);
// R3 转发表:
// 入标签10 → 出标签6,转给R1(接口1)
// 入标签12 → 出标签9,转给D(接口0)
R3->addEntry(10, 6, "R1", 1);
R3->addEntry(12, 9, "D", 0);
// R4 转发表(入口路由器,给来自R5/R6的IP包打标签后转发):
// 入标签10 → 出标签10,转给R3(接口0,经由R3到A)
// 入标签12 → 出标签12,转给R3(接口0,到D)
// 入标签8 → 出标签8,转给R2(接口1,经由R2到A)
R4->addEntry(10, 10, "R3", 0);
R4->addEntry(12, 12, "R3", 0);
R4->addEntry(8, 8, "R2", 1);
// 打印所有路由器的转发表
cout << "=== MPLS 网络各路由器转发表 ===\n\n";
R1->printTable();
R2->printTable();
R3->printTable();
R4->printTable();
// 模拟路径一:R4 经由 R3 → R1 → A,使用标签10
network.simulate("R4", 10, "10.0.0.5", "10.0.0.1");
// 模拟路径二:R4 经由 R2 → R1 → A,使用标签8
network.simulate("R4", 8, "10.0.0.6", "10.0.0.1");
// 模拟路径三:R4 经由 R3 → D,使用标签12
network.simulate("R4", 12, "10.0.0.5", "10.0.0.2");
return 0;
}
预期运行结果摘要:
路径一(标签10,经R3→R1→A):
R4:标签10→10,出接口0→R3
R3:标签10→6,出接口1→R1
R1:标签6弹出,交付给A
路径二(标签8,经R2→R1→A):
R4:标签8→8,出接口1→R2
R2:标签8→6,出接口0→R1
R1:标签6弹出,交付给A
路径三(标签12,经R3→D):
R4:标签12→12,出接口0→R3
R3:标签12→9,出接口0→D
八、核心概念对比总结
| 技术 | 虚拟化对象 | 标识符大小 | 主要用途 |
|---|---|---|---|
| VLAN(802.1Q) | 以太网广播域 | 12位(4094个) | 同一物理交换机内隔离 |
| MPLS | IP路由路径 | 20位标签 | 流量工程、VPN、快速恢复 |
| VXLAN | 以太网局域网 | 24位VNI(1600万个) | 跨地域/大规模虚拟局域网 |
九、数据中心拓扑对比
| 拓扑 | 层数 | 任意两主机跳数 | 扩展方式 | 适用规模 |
|---|---|---|---|---|
| 简单单交换机 | 1 | 2 | 换更大交换机 | 数百台 |
| 传统三层层次 | 3(TOR/二层/一层) | 2~6(可变) | 增加层间连接 | 数万台 |
| 叶脊(Leaf-Spine) | 2 | 固定2跳 | 横向加Spine节点 | 数十万台 |
6.7 一次网页请求的完整旅程
场景:学生 Bob 把笔记本接入学校以太网,打开浏览器访问 www.google.com
零、全局网络信息(来自图6.34)
| 角色 | IP地址 | MAC地址 |
|---|---|---|
| Bob的笔记本 | 68.85.2.101(DHCP分配) | 00:16:D3:23:68:8A |
| 学校网关路由器(接学校侧) | 68.85.2.1 | 00:22:6B:45:1F:1B |
| Comcast DNS服务器 | 68.87.71.226 | — |
| www.google.com服务器 | 64.233.169.105 | — |
| 学校子网块 | 68.85.2.0/24 | — |
| Comcast网络块 | 68.80.0.0/13 | — |
| Google网络块 | 64.233.160.0/19 | — |
图中步骤编号对照:
- 步骤 1~7:DHCP 过程(获取IP地址)
- 步骤 8~13:DNS + ARP 过程(获取Google服务器IP)
- 步骤 14~17:路由到DNS服务器
- 步骤 18~24:TCP三次握手 + HTTP请求/响应
一、阶段一:DHCP —— Bob的笔记本获取IP地址(步骤1~7)
为什么需要DHCP?
Bob刚插上网线,笔记本还没有IP地址,什么网络请求都发不出去。必须先通过DHCP协议向网络申请一个IP地址。
详细步骤
步骤1:笔记本创建DHCP请求,层层封装
DHCP请求消息
→ 放进UDP段(目的端口67=DHCP服务器,源端口68=DHCP客户端)
→ 放进IP数据报(目的IP=255.255.255.255广播,源IP=0.0.0.0,因为还没IP)
→ 放进以太网帧(目的MAC=FF:FF:FF:FF:FF:FF广播,源MAC=00:16:D3:23:68:8A)
步骤2:笔记本把帧发给交换机
步骤3:交换机收到,广播到所有端口(包括连路由器的端口)
步骤4:路由器(MAC=00:22:6B:45:1F:1B)收到帧
→ 提取以太网帧中的IP数据报
→ 发现目的IP=255.255.255.255(广播),自己处理
→ 提取UDP段,再提取DHCP请求
→ DHCP服务器(运行在路由器内)处理请求
步骤5:DHCP服务器准备回复
分配给Bob:
IP地址:68.85.2.101
DNS服务器IP:68.87.71.226
默认网关IP:68.85.2.1
子网掩码:68.85.2.0/24
DHCP ACK消息
→ 放进UDP段
→ 放进IP数据报(目的IP=68.85.2.101)
→ 放进以太网帧(目的MAC=00:16:D3:23:68:8A,源MAC=00:22:6B:45:1F:1B)
步骤6:路由器把帧发给交换机
交换机已经学到了Bob笔记本的MAC地址(步骤1时学的)
→ 精确转发到Bob笔记本的端口
步骤7:Bob笔记本收到DHCP ACK
→ 提取并记录:
自己的IP地址:68.85.2.101
DNS服务器IP:68.87.71.226
默认网关:68.85.2.1 → 写入IP转发表
→ 笔记本网络初始化完成!
协议栈封装示意(DHCP请求):
+-------------------------------+
| DHCP 请求消息 | 应用层
+-------------------------------+
| UDP头 (src:68, dst:67) | 传输层
+-------------------------------+
| IP头 (src:0.0.0.0, | 网络层
| dst:255.255.255.255) |
+-------------------------------+
| 以太网头 (dst:FF:FF:FF:FF:FF:FF| 链路层
| src:00:16:D3:23:68:8A)
+-------------------------------+
二、阶段二:DNS + ARP —— 查询Google的IP地址(步骤8~13)
Bob在浏览器输入 www.google.com
浏览器需要知道 www.google.com 的IP地址才能建立连接,但现在只有域名,没有IP。→ 需要 DNS查询。
步骤8(DNS查询开始):
笔记本创建DNS查询消息(问:www.google.com 的IP是什么?)
→ 放进UDP段(目的端口53=DNS)
→ 放进IP数据报(目的IP=68.87.71.226=DNS服务器)
但是!要把这个IP数据报发出去,需要封装成以太网帧。
目的IP是DNS服务器(在Comcast网络),不在本地子网。
所以需要发给"默认网关"路由器(68.85.2.1)。
但笔记本只知道网关的IP,不知道网关的MAC地址!
→ 需要 ARP 协议来查询网关的MAC地址
步骤9(ARP请求):
笔记本发ARP广播帧:
"谁的IP是68.85.2.1?请告诉我你的MAC地址!"
以太网帧(目的MAC=FF:FF:FF:FF:FF:FF广播)
→ 发给交换机 → 交换机广播到所有端口
步骤10(ARP回复):
网关路由器(IP=68.85.2.1)收到ARP请求
→ 发现目标IP就是自己
→ 回复ARP应答(单播):
"我的MAC是00:22:6B:45:1F:1B,对应IP 68.85.2.1"
→ 发给Bob笔记本
步骤11:
Bob笔记本收到ARP回复
→ 记录:网关MAC=00:22:6B:45:1F:1B
→ ARP缓存更新
步骤12(发出DNS查询帧):
现在终于知道网关的MAC地址了!
把DNS查询封装成以太网帧:
目的MAC = 00:22:6B:45:1F:1B(网关路由器)← 链路层地址,下一跳
目的IP = 68.87.71.226(DNS服务器) ← 网络层地址,最终目的
笔记本 → 交换机 → 网关路由器
步骤13:
网关路由器收到帧,提取IP数据报
→ 查路由表,确定下一跳
→ 转发到Comcast网络
IP地址 vs MAC地址的关键区别(以送快递打比方):
IP地址(68.87.71.226)= 最终收件人地址(DNS服务器在Comcast)
MAC地址(00:22:6B:45:1F:1B)= 当前这段路的下一个中转站地址(网关路由器)
每经过一个路由器,MAC地址会变(换成下一跳的MAC)
但IP地址始终不变(一直是DNS服务器的IP)
三、阶段三:域内路由 —— 数据报在Comcast网络中传播(步骤14~17)
步骤14:
网关路由器查转发表
→ 目的IP 68.87.71.226 属于Comcast网络
→ 转发到Comcast最左边的路由器
→ 封装成适合该链路的帧格式,发出去
步骤15:
Comcast内部路由器收到数据报
→ 查转发表(由OSPF/RIP等域内路由协议填充,以及BGP填充域间路由)
→ 逐跳转发,最终到达DNS服务器
步骤16:
DNS服务器(68.87.71.226)收到DNS查询
→ 查DNS数据库(或缓存)
→ 找到:www.google.com → 64.233.169.105
→ 创建DNS回复消息,原路返回给Bob(68.85.2.101)
步骤17:
DNS回复经过Comcast网络→学校路由器→交换机→Bob笔记本
Bob笔记本提取DNS回复:
www.google.com 的IP = 64.233.169.105
终于知道Google服务器的IP了!
路由转发中的协议分工:
学校内部:由 DHCP 分配的转发表(默认路由)
Comcast内部:由 OSPF/RIP 等域内路由协议维护的转发表
Comcast→Google:由 BGP 维护的域间路由表
四、阶段四:TCP三次握手 + HTTP请求/响应(步骤18~24)
TCP三次握手(步骤18~20)
步骤18(SYN):
Bob笔记本创建TCP套接字,发起连接:
TCP SYN段(目的端口80=HTTP)
→ IP数据报(目的IP=64.233.169.105=Google服务器)
→ 以太网帧(目的MAC=00:22:6B:45:1F:1B=网关路由器)
→ 经过学校网络→Comcast网络→Google网络→Google服务器
步骤19(SYNACK):
Google的www.google.com服务器收到SYN
→ 创建连接套接字(专门用于和Bob通信)
→ 发送TCP SYNACK段
→ 经过Google网络→Comcast网络→学校网络→Bob笔记本
步骤20(ACK):
Bob笔记本收到SYNACK,TCP连接建立!
→ 发送ACK确认
TCP三次握手完成
TCP三次握手示意:
Bob笔记本 Google服务器
| |
|-------- SYN(我想建立连接)----------->|
| |
|<------- SYNACK(好的,我也同意)-------|
| |
|-------- ACK(收到,连接建立)---------->|
| |
|====== TCP 连接建立,可以传数据了 =======|
HTTP请求与响应(步骤21~24)
步骤21(HTTP GET):
Bob浏览器创建 HTTP GET 请求:
GET / HTTP/1.1
Host: www.google.com
→ 写入TCP套接字
→ TCP段→IP数据报→以太网帧→路由转发→Google服务器
步骤22(Google服务器处理):
Google HTTP服务器从TCP套接字读取HTTP GET
→ 准备网页内容(HTML)
→ 创建HTTP响应(200 OK + HTML内容)
→ 写入TCP套接字,发回给Bob
步骤23~24(响应返回):
HTTP响应经过Google→Comcast→学校→Bob笔记本
Bob浏览器读取HTTP响应
→ 解析HTML
→ 渲染显示网页
Bob看到了 www.google.com 的主页!
五、完整流程时序图
六、每个步骤涉及的协议全景
步骤 协议层次 涉及协议
1-7 应用+传输+网络+链路 DHCP / UDP / IP(广播) / Ethernet(广播)
8 应用+传输+网络 DNS查询 / UDP / IP
9-11 链路 ARP(查网关MAC地址)
12-13 应用+传输+网络+链路 DNS/UDP/IP/Ethernet(发往网关)
14-17 网络 OSPF/RIP(域内路由)/ BGP(域间路由)
应用 DNS(解析到64.233.169.105)
18-20 传输 TCP三次握手(SYN/SYNACK/ACK)
21 应用+传输 HTTP GET / TCP
22-24 应用+传输 HTTP响应 / TCP
七、各层地址的变化规律
从Bob(68.85.2.101)到Google(64.233.169.105)
经过每一跳:
IP地址:始终不变
源IP = 68.85.2.101(Bob)
目的IP = 64.233.169.105(Google)
MAC地址:每跳都变!
Bob笔记本→网关路由器:
src MAC = 00:16:D3:23:68:8A(Bob)
dst MAC = 00:22:6B:45:1F:1B(网关)
网关路由器→Comcast路由器:
src MAC = 网关路由器WAN侧MAC
dst MAC = Comcast路由器MAC
... 每跳路由器都换一次MAC地址 ...
最后一跳路由器→Google服务器:
src MAC = Google内部路由器MAC
dst MAC = Google服务器网卡MAC
八、协议栈穿越示意(完整封装)
以HTTP GET请求为例(发出时):
Bob浏览器(应用层):
HTTP GET / HTTP/1.1\r\nHost: www.google.com\r\n\r\n
TCP层封装:
+----------------------------------+
| TCP头(src:随机端口 dst:80) |
| HTTP GET消息 |
+----------------------------------+
IP层封装:
+----------------------------------+
| IP头(src:68.85.2.101 |
| dst:64.233.169.105) |
| TCP段 |
+----------------------------------+
以太网层封装(第一段链路,到网关):
+----------------------------------+
| 以太网头(src:00:16:D3:23:68:8A |
| dst:00:22:6B:45:1F:1B)|
| IP数据报 |
+----------------------------------+
物理层:将上述帧转换为电信号或光信号,发送出去
九、关键数字速查
| 协议 | 端口/特殊地址 | 说明 |
|---|---|---|
| DHCP服务器 | UDP 67 | 客户端发请求到此端口 |
| DHCP客户端 | UDP 68 | 服务器回复发到此端口 |
| DNS | UDP 53 | 域名解析 |
| HTTP | TCP 80 | 网页请求 |
| DHCP请求广播IP | 255.255.255.255 | 全网广播 |
| DHCP请求源IP | 0.0.0.0 | 尚无IP时使用 |
| 广播MAC | FF:FF:FF:FF:FF:FF | 以太网广播 |
十、完整 C++ 模拟代码:一次网页请求的关键协议流程
#include <iostream>
#include <string>
#include <map>
#include <vector>
using namespace std;
// ============================================================
// 数据结构定义
// ============================================================
// 简化版"以太网帧"
struct EtherFrame {
string srcMAC;
string dstMAC;
string payload; // 帧的数据载荷(这里用字符串模拟)
};
// 简化版"IP数据报"
struct IPDatagram {
string srcIP;
string dstIP;
string payload;
};
// 简化版"UDP段"
struct UDPSegment {
int srcPort;
int dstPort;
string payload;
};
// ARP 缓存:IP地址 → MAC地址
map<string, string> arpCache;
// 交换机的 MAC 地址表:MAC → 端口号
map<string, int> switchTable;
// ============================================================
// 工具函数
// ============================================================
// 打印分隔线
void separator(const string& title) {
cout << "\n" << string(60, '=') << "\n";
cout << " " << title << "\n";
cout << string(60, '=') << "\n";
}
// 模拟打印以太网帧
void printFrame(const EtherFrame& f, const string& direction) {
cout << " [以太网帧] " << direction << "\n";
cout << " 源MAC: " << f.srcMAC << "\n";
cout << " 目的MAC: " << f.dstMAC << "\n";
cout << " 载荷: " << f.payload << "\n";
}
// 模拟打印IP数据报
void printDatagram(const IPDatagram& d, const string& note) {
cout << " [IP数据报] " << note << "\n";
cout << " 源IP: " << d.srcIP << "\n";
cout << " 目的IP: " << d.dstIP << "\n";
cout << " 载荷: " << d.payload << "\n";
}
// ============================================================
// 阶段一:DHCP 过程
// ============================================================
string runDHCP(const string& clientMAC) {
separator("阶段一:DHCP —— 获取IP地址");
// 步骤1:客户端发送DHCP Discover(广播)
cout << "步骤1: Bob笔记本发送DHCP请求(广播)\n";
EtherFrame dhcpReq;
dhcpReq.srcMAC = clientMAC;
dhcpReq.dstMAC = "FF:FF:FF:FF:FF:FF"; // 广播
dhcpReq.payload = "UDP(src:68,dst:67)[IP(src:0.0.0.0,dst:255.255.255.255)[DHCP请求]]";
printFrame(dhcpReq, "Bob笔记本 → 交换机(广播)");
// 步骤2~3:交换机广播,路由器收到
cout << "\n步骤2: 交换机广播到所有端口,网关路由器收到\n";
// 步骤4~5:DHCP服务器(在路由器内)分配IP并回复
cout << "\n步骤3: DHCP服务器分配地址,发回DHCP ACK\n";
string allocatedIP = "68.85.2.101";
// 模拟DHCP服务器的回复内容
cout << " DHCP ACK 内容:\n";
cout << " 分配IP: " << allocatedIP << "\n";
cout << " DNS服务器IP: 68.87.71.226\n";
cout << " 默认网关IP: 68.85.2.1\n";
cout << " 子网块: 68.85.2.0/24\n";
EtherFrame dhcpAck;
dhcpAck.srcMAC = "00:22:6B:45:1F:1B"; // 网关路由器MAC
dhcpAck.dstMAC = clientMAC;
dhcpAck.payload = "DHCP ACK(IP=" + allocatedIP + ")";
printFrame(dhcpAck, "网关路由器 → Bob笔记本(单播)");
// 步骤6~7:笔记本记录IP
cout << "\n步骤4: Bob笔记本记录IP地址,初始化完成\n";
cout << " Bob笔记本IP: " << allocatedIP << "\n";
return allocatedIP;
}
// ============================================================
// 阶段二:ARP过程(查询网关MAC地址)
// ============================================================
string runARP(const string& clientMAC, const string& clientIP,
const string& gatewayIP, const string& gatewayMAC) {
separator("阶段二:ARP —— 查询网关MAC地址");
// 检查ARP缓存
if (arpCache.count(gatewayIP)) {
cout << "ARP缓存命中!网关MAC = " << arpCache[gatewayIP] << "\n";
return arpCache[gatewayIP];
}
// 步骤9:发送ARP广播请求
cout << "步骤1: Bob笔记本发送ARP请求(广播)\n";
cout << " \"谁的IP是 " << gatewayIP << "?请告诉 " << clientMAC << "\"\n";
EtherFrame arpReq;
arpReq.srcMAC = clientMAC;
arpReq.dstMAC = "FF:FF:FF:FF:FF:FF";
arpReq.payload = "ARP请求: 谁是" + gatewayIP + "?";
printFrame(arpReq, "Bob笔记本 → 所有设备(广播)");
// 步骤10:网关路由器回复ARP
cout << "\n步骤2: 网关路由器发现目标IP是自己,回复ARP应答\n";
EtherFrame arpReply;
arpReply.srcMAC = gatewayMAC;
arpReply.dstMAC = clientMAC;
arpReply.payload = "ARP回复: " + gatewayIP + " 的MAC是 " + gatewayMAC;
printFrame(arpReply, "网关路由器 → Bob笔记本(单播)");
// 步骤11:更新ARP缓存
arpCache[gatewayIP] = gatewayMAC;
cout << "\n步骤3: Bob笔记本更新ARP缓存\n";
cout << " ARP缓存: " << gatewayIP << " → " << gatewayMAC << "\n";
return gatewayMAC;
}
// ============================================================
// 阶段三:DNS查询(获取Google的IP地址)
// ============================================================
string runDNS(const string& clientIP, const string& gatewayMAC,
const string& dnsServerIP, const string& hostname) {
separator("阶段三:DNS —— 查询 " + hostname + " 的IP地址");
// DNS查询消息封装
cout << "步骤1: 创建DNS查询,封装并发往网关\n";
IPDatagram dnsQuery;
dnsQuery.srcIP = clientIP;
dnsQuery.dstIP = dnsServerIP; // DNS服务器在Comcast
dnsQuery.payload = "UDP(dst:53)[DNS查询: " + hostname + "]";
printDatagram(dnsQuery, "Bob笔记本 → DNS服务器(跨网络)");
cout << " 以太网帧目的MAC = " << gatewayMAC << "(网关,下一跳)\n";
cout << " 注意:IP层目的是DNS服务器,MAC层目的是网关路由器\n";
// 模拟经过Comcast网络路由
cout << "\n步骤2: 数据报经Comcast网络路由转发到DNS服务器\n";
cout << " 经过的路由协议: OSPF(域内)+ BGP(域间)\n";
// DNS服务器查找并返回结果
cout << "\n步骤3: DNS服务器查找 " << hostname << "\n";
string googleIP = "64.233.169.105";
cout << " DNS查询结果: " << hostname << " → " << googleIP << "\n";
cout << "\n步骤4: DNS回复经Comcast网络返回Bob笔记本\n";
cout << " Bob笔记本获得: " << hostname << " = " << googleIP << "\n";
return googleIP;
}
// ============================================================
// 阶段四:TCP三次握手 + HTTP请求/响应
// ============================================================
void runTCPHTTP(const string& clientIP, const string& serverIP,
const string& gatewayMAC) {
separator("阶段四:TCP三次握手 + HTTP请求/响应");
// TCP SYN
cout << "=== TCP 三次握手 ===\n\n";
cout << "步骤1 [SYN]: Bob笔记本发起TCP连接请求\n";
IPDatagram syn;
syn.srcIP = clientIP;
syn.dstIP = serverIP;
syn.payload = "TCP[SYN, seq=0, dst_port=80]";
printDatagram(syn, "Bob → Google(经过学校→Comcast→Google网络)");
cout << " 以太网帧目的MAC = " << gatewayMAC << "(网关,下一跳)\n";
// TCP SYNACK
cout << "\n步骤2 [SYNACK]: Google服务器回应,同意建立连接\n";
IPDatagram synack;
synack.srcIP = serverIP;
synack.dstIP = clientIP;
synack.payload = "TCP[SYN+ACK, seq=0, ack=1]";
printDatagram(synack, "Google → Bob(原路返回)");
// TCP ACK
cout << "\n步骤3 [ACK]: Bob确认,TCP连接建立\n";
IPDatagram ack;
ack.srcIP = clientIP;
ack.dstIP = serverIP;
ack.payload = "TCP[ACK, seq=1, ack=1]";
printDatagram(ack, "Bob → Google");
cout << " *** TCP 三次握手完成,连接建立!***\n";
// HTTP GET
cout << "\n=== HTTP 请求/响应 ===\n\n";
cout << "步骤4 [HTTP GET]: Bob浏览器发送HTTP GET请求\n";
IPDatagram httpGet;
httpGet.srcIP = clientIP;
httpGet.dstIP = serverIP;
httpGet.payload = "TCP[HTTP GET / HTTP/1.1\\r\\nHost: www.google.com]";
printDatagram(httpGet, "Bob → Google");
// HTTP Response
cout << "\n步骤5 [HTTP响应]: Google服务器返回网页内容\n";
IPDatagram httpResp;
httpResp.srcIP = serverIP;
httpResp.dstIP = clientIP;
httpResp.payload = "TCP[HTTP/1.1 200 OK\\r\\n<html>Google首页HTML内容</html>]";
printDatagram(httpResp, "Google → Bob");
cout << "\n步骤6: Bob浏览器接收HTML,渲染显示网页\n";
cout << " *** Bob成功看到了 www.google.com 的首页!***\n";
}
// ============================================================
// 主函数:串联所有阶段
// ============================================================
int main() {
cout << "============================================================\n";
cout << " 一次网页请求的完整旅程\n";
cout << " 场景:Bob访问 www.google.com\n";
cout << "============================================================\n";
// Bob笔记本的MAC地址(出厂就有,不变)
const string BOB_MAC = "00:16:D3:23:68:8A";
// 网关路由器的MAC地址(学校侧接口)
const string GATEWAY_MAC = "00:22:6B:45:1F:1B";
// 网关路由器的IP地址
const string GATEWAY_IP = "68.85.2.1";
// DNS服务器IP(Comcast提供)
const string DNS_IP = "68.87.71.226";
// 访问的目标网站
const string TARGET_HOST = "www.google.com";
// 阶段一:DHCP获取IP地址
string bobIP = runDHCP(BOB_MAC);
// 阶段二:ARP查询网关MAC
string gwMAC = runARP(BOB_MAC, bobIP, GATEWAY_IP, GATEWAY_MAC);
// 阶段三:DNS查询Google的IP
string googleIP = runDNS(bobIP, gwMAC, DNS_IP, TARGET_HOST);
// 阶段四:TCP三次握手 + HTTP请求/响应
runTCPHTTP(bobIP, googleIP, gwMAC);
cout << "\n" << string(60, '=') << "\n";
cout << " 旅程结束!Bob成功访问了 www.google.com\n";
cout << " 涉及协议:DHCP, ARP, DNS, UDP, IP, Ethernet,\n";
cout << " OSPF/BGP(路由), TCP, HTTP\n";
cout << string(60, '=') << "\n";
return 0;
}
https://godbolt.org/z/v1nE9qxfb
============================================================
一次网页请求的完整旅程
场景:Bob访问 www.google.com
============================================================
============================================================
阶段一:DHCP —— 获取IP地址
============================================================
步骤1: Bob笔记本发送DHCP请求(广播)
[以太网帧] Bob笔记本 → 交换机(广播)
源MAC: 00:16:D3:23:68:8A
目的MAC: FF:FF:FF:FF:FF:FF
载荷: UDP(src:68,dst:67)[IP(src:0.0.0.0,dst:255.255.255.255)[DHCP请求]]
步骤2: 交换机广播到所有端口,网关路由器收到
步骤3: DHCP服务器分配地址,发回DHCP ACK
DHCP ACK 内容:
分配IP: 68.85.2.101
DNS服务器IP: 68.87.71.226
默认网关IP: 68.85.2.1
子网块: 68.85.2.0/24
[以太网帧] 网关路由器 → Bob笔记本(单播)
源MAC: 00:22:6B:45:1F:1B
目的MAC: 00:16:D3:23:68:8A
载荷: DHCP ACK(IP=68.85.2.101)
步骤4: Bob笔记本记录IP地址,初始化完成
Bob笔记本IP: 68.85.2.101
============================================================
阶段二:ARP —— 查询网关MAC地址
============================================================
步骤1: Bob笔记本发送ARP请求(广播)
"谁的IP是 68.85.2.1?请告诉 00:16:D3:23:68:8A"
[以太网帧] Bob笔记本 → 所有设备(广播)
源MAC: 00:16:D3:23:68:8A
目的MAC: FF:FF:FF:FF:FF:FF
载荷: ARP请求: 谁是68.85.2.1?
步骤2: 网关路由器发现目标IP是自己,回复ARP应答
[以太网帧] 网关路由器 → Bob笔记本(单播)
源MAC: 00:22:6B:45:1F:1B
目的MAC: 00:16:D3:23:68:8A
载荷: ARP回复: 68.85.2.1 的MAC是 00:22:6B:45:1F:1B
步骤3: Bob笔记本更新ARP缓存
ARP缓存: 68.85.2.1 → 00:22:6B:45:1F:1B
============================================================
阶段三:DNS —— 查询 www.google.com 的IP地址
============================================================
步骤1: 创建DNS查询,封装并发往网关
[IP数据报] Bob笔记本 → DNS服务器(跨网络)
源IP: 68.85.2.101
目的IP: 68.87.71.226
载荷: UDP(dst:53)[DNS查询: www.google.com]
以太网帧目的MAC = 00:22:6B:45:1F:1B(网关,下一跳)
注意:IP层目的是DNS服务器,MAC层目的是网关路由器
步骤2: 数据报经Comcast网络路由转发到DNS服务器
经过的路由协议: OSPF(域内)+ BGP(域间)
步骤3: DNS服务器查找 www.google.com
DNS查询结果: www.google.com → 64.233.169.105
步骤4: DNS回复经Comcast网络返回Bob笔记本
Bob笔记本获得: www.google.com = 64.233.169.105
============================================================
阶段四:TCP三次握手 + HTTP请求/响应
============================================================
=== TCP 三次握手 ===
步骤1 [SYN]: Bob笔记本发起TCP连接请求
[IP数据报] Bob → Google(经过学校→Comcast→Google网络)
源IP: 68.85.2.101
目的IP: 64.233.169.105
载荷: TCP[SYN, seq=0, dst_port=80]
以太网帧目的MAC = 00:22:6B:45:1F:1B(网关,下一跳)
步骤2 [SYNACK]: Google服务器回应,同意建立连接
[IP数据报] Google → Bob(原路返回)
源IP: 64.233.169.105
目的IP: 68.85.2.101
载荷: TCP[SYN+ACK, seq=0, ack=1]
步骤3 [ACK]: Bob确认,TCP连接建立
[IP数据报] Bob → Google
源IP: 68.85.2.101
目的IP: 64.233.169.105
载荷: TCP[ACK, seq=1, ack=1]
*** TCP 三次握手完成,连接建立!***
=== HTTP 请求/响应 ===
步骤4 [HTTP GET]: Bob浏览器发送HTTP GET请求
[IP数据报] Bob → Google
源IP: 68.85.2.101
目的IP: 64.233.169.105
载荷: TCP[HTTP GET / HTTP/1.1\r\nHost: www.google.com]
步骤5 [HTTP响应]: Google服务器返回网页内容
[IP数据报] Google → Bob
源IP: 64.233.169.105
目的IP: 68.85.2.101
载荷: TCP[HTTP/1.1 200 OK\r\n<html>Google首页HTML内容</html>]
步骤6: Bob浏览器接收HTML,渲染显示网页
*** Bob成功看到了 www.google.com 的首页!***
============================================================
旅程结束!Bob成功访问了 www.google.com
涉及协议:DHCP, ARP, DNS, UDP, IP, Ethernet,
OSPF/BGP(路由), TCP, HTTP
============================================================
十一、整体知识总结
十二、本章总结
链路层完整覆盖了以下内容:
| 主题 | 核心内容 |
|---|---|
| 基本服务 | 把网络层数据报封装成帧,在相邻节点间传递 |
| 差错检测 | 奇偶校验、校验和、CRC循环冗余校验 |
| 多路访问 | 信道划分(TDM/FDM)、随机访问(ALOHA/CSMA)、轮转(轮询/令牌环) |
| 链路层寻址 | MAC地址(48位)、ARP协议(IP↔MAC转换) |
| 以太网 | 最成功的有线局域网技术 |
| 交换机 | 自学习、转发/过滤、即插即用 |
| VLAN | 虚拟局域网,隔离广播域 |
| MPLS | 标签交换,流量工程,VPN |
| VXLAN | 跨互联网的虚拟局域网,VNI 24位(1600万) |
| 数据中心网络 | 层次架构→叶脊拓扑,负载均衡,SDN管控 |
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐


所有评论(0)