本文是 NS3 学习笔记的第四章节,主要是对 L2级别协议 的实现
L2 交换机协议实现详细说明文档 - 多交换机拓扑版
目录
1. 项目概述
1.1 项目目的
本项目使用 ns-3 标准的协议栈(Protocol Stack)架构实现多个二层(L2)交换机组成的网络。
- 创建自定义协议类:
L2SwitchProtocol 继承自 Object
- 使用 Helper 模式: 通过
L2SwitchHelper 安装协议
- 协议聚合: 使用
node->AggregateObject() 将协议附加到节点
- 多交换机支持: 每个交换机独立运行 L2SwitchProtocol,数据包通过多跳转发
- 标准协议栈集成: 主机端使用标准的
InternetStackHelper 安装完整的 TCP/IP 协议栈
1.2 核心特性
- ✅ MAC 地址学习: 自动学习源 MAC 地址与端口的映射关系
- ✅ 智能转发: 基于学习到的 MAC 表进行单播转发
- ✅ 泛洪机制: 对未知目的地址和广播帧进行泛洪
- ✅ 防环路: 自动丢弃目的端口等于源端口的数据包
- ✅ 多交换机支持: 支持多个交换机级联,数据包逐跳转发
- ✅ 协议栈架构: 完全符合 ns-3 的对象模型和协议栈设计
- ✅ 自定义转发: 不依赖 ns-3 内置的 BridgeNetDevice
2. 网络拓扑
2.1 拓扑图
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
| Host A Host B Host C
(192.168.1.1) (192.168.1.2) (192.168.1.3)
| | |
| | |
+-----+-----+ +-----+-----+ +-----+-----+
| Switch 0 | | Switch 1 | | Switch 2 |
+-----+-----+ +-----+-----+ +-----+-----+
端口0 端口1 端口0 端口1 端口2 端口0 端口1
| | | | | | |
| +-------------+ | +------------+ |
Host A (SW0-SW1) Host B (SW1-SW2) Host C
详细连接关系:
┌─────────────────────────────────────────────────────────────────────┐
│ │
│ Host A ──── Switch 0 ──── Switch 1 ──── Switch 2 ──── Host C │
│ │ 端口0-1 端口0-2 端口0-1 │ │
│ │ │ │ │
│ 192.168.1.1 Host B 192.168.1.3 │
│ 192.168.1.2 │
│ (端口1) │
│ │
└─────────────────────────────────────────────────────────────────────┘
简化视图:
[Host A] [Host B] [Host C]
│ │ │
│ │ │
┌────┴────┐ ┌────┴────┐ ┌────┴────┐
│ Switch0 │───────────│ Switch1 │───────────│ Switch2 │
└─────────┘ └─────────┘ └─────────┘
2端口 3端口 2端口
数据包转发路径:
- Host A → Host C: Host A → Switch0 → Switch1 → Switch2 → Host C (3跳)
- Host B → Host C: Host B → Switch1 → Switch2 → Host C (2跳)
- Host A → Host B: Host A → Switch0 → Switch1 → Host B (2跳)
|
2.2 连接详情
| 连接 | 端点1 | 端点2 | 链路属性 |
|——|——-|——-|———-|
| Link 1 | Host A | Switch 0 端口0 | 100Mbps, 6.56μs |
| Link 2 | Switch 0 端口1 | Switch 1 端口0 | 100Mbps, 6.56μs |
| Link 3 | Host B | Switch 1 端口1 | 100Mbps, 6.56μs |
| Link 4 | Switch 1 端口2 | Switch 2 端口0 | 100Mbps, 6.56μs |
| Link 5 | Host C | Switch 2 端口1 | 100Mbps, 6.56μs |
2.3 IP 地址分配
- Host A: 192.168.1.1/24
- Host B: 192.168.1.2/24
- Host C: 192.168.1.3/24
- 交换机: 不配置 IP 地址(纯二层设备)
2.4 交换机端口配置
| 交换机 | 端口数 | 端口0连接 | 端口1连接 | 端口2连接 |
|——–|——–|———–|———–|———–|
| Switch0 | 2 | Host A | Switch1 | - |
| Switch1 | 3 | Switch0 | Host B | Switch2 |
| Switch2 | 2 | Switch1 | Host C | - |
3. 核心架构设计
3.1 协议栈模型
在 ns-3 中,协议是通过聚合(Aggregate)到节点的方式实现的:
1
2
3
4
5
6
7
8
9
10
11
12
| // 1. 创建协议对象
Ptr<L2SwitchProtocol> protocol = CreateObject<L2SwitchProtocol>();
// 2. 配置协议
protocol->SetSwitchName("Switch0");
protocol->SetNode(node);
// 3. 聚合到节点 (关键步骤!)
node->AggregateObject(protocol);
// 4. 通过节点获取协议对象
Ptr<L2SwitchProtocol> p = node->GetObject<L2SwitchProtocol>();
|
3.2 类结构
3.2.1 L2SwitchProtocol 类
继承关系: Object ← L2SwitchProtocol
核心职责:
- 监听节点上所有网络设备的数据包
- 学习 MAC 地址与端口的映射关系
- 根据目的 MAC 地址转发数据包
关键成员变量:
1
2
3
4
| std::string m_switchName; // 交换机名称(用于日志)
Ptr<Node> m_node; // 所属节点
std::map<Mac48Address, Ptr<NetDevice>> m_macTable; // MAC 地址表
bool m_initialized; // 初始化标志
|
3.2.2 L2SwitchHelper 类
作用: 简化协议的安装和配置
核心方法:
Install(Ptr<Node> node, const std::string& name): 在单个节点上安装协议
Install(NodeContainer nodes): 在节点容器上批量安装协议
4. 数据包转发流程详解
4.1 数据包生命周期
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
| ┌─────────────────────────────────────────────────────────────┐
│ 数据包到达交换机 │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 步骤 1: 混杂模式回调触发 │
│ ReceiveFromDevice() 被调用 │
│ - 从 NetDevice 接收数据包 │
│ - 提取源 MAC 地址和目的 MAC 地址 │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 步骤 2: MAC 地址学习 │
│ Learn(srcMac, inDevice) │
│ - 检查源 MAC 是否在表中 │
│ - 如果不存在,添加到 MAC 表 │
│ - 如果存在但端口不同,更新端口 │
└─────────────────────────────────────────────────────────────┘
│
▼
是否为广播帧?
/ \
是 否
/ \
▼ ▼
┌───────────────────┐ ┌──────────────────┐
│ 步骤 3a: 广播转发 │ │ 步骤 3b: 查表转发│
│ ForwardBroadcast()│ │ GetLearnedPort() │
│ - 泛洪到所有端口 │ │ - 查找目的 MAC │
│ - 排除入端口 │ └──────────────────┘
└───────────────────┘ │
│
┌────────────┴─────────────┐
│ │
找到端口? 未找到端口?
│ │
▼ ▼
┌─────────────────────┐ ┌──────────────────┐
│ 步骤 4a: 单播转发 │ │ 步骤 4b: 泛洪 │
│ ForwardUnicast() │ │ ForwardBroadcast()│
│ - 发送到特定端口 │ │ - 未知目的地址 │
│ - 检查出入端口不同 │ │ - 泛洪到所有端口 │
└─────────────────────┘ └──────────────────┘
│ │
└────────────┬─────────────┘
▼
┌────────────────────┐
│ 步骤 5: 转发完成 │
│ 返回 true │
└────────────────────┘
|
4.2 详细步骤说明
步骤 1: 数据包接收 (ReceiveFromDevice)
当数据包到达交换机的某个端口时,混杂模式回调被触发:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| bool L2SwitchProtocol::ReceiveFromDevice(
Ptr<NetDevice> inDevice, // 入端口设备
Ptr<const Packet> packet, // 数据包
uint16_t protocol, // 协议类型 (如 0x0800 = IPv4, 0x0806 = ARP)
const Address& from, // 源 MAC 地址
const Address& to, // 目的 MAC 地址
NetDevice::PacketType packetType) // 包类型
{
// 1. 地址转换
Mac48Address srcMac = Mac48Address::ConvertFrom(from);
Mac48Address dstMac = Mac48Address::ConvertFrom(to);
// 2. 日志记录
NS_LOG_DEBUG(m_switchName << ": Received packet from " << srcMac
<< " to " << dstMac << " on device " << inDevice->GetIfIndex());
// ... 后续处理
}
|
关键参数解析:
inDevice: 数据包进入的网络设备(端口)
from 和 to: 以太网帧头中的源和目的 MAC 地址
protocol: 以太网类型字段,表示上层协议
0x0800: IPv4
0x0806: ARP
0x86DD: IPv6
步骤 2: MAC 地址学习 (Learn)
交换机通过学习源 MAC 地址来建立转发表:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| void L2SwitchProtocol::Learn(Mac48Address source, Ptr<NetDevice> inDevice)
{
auto it = m_macTable.find(source);
if (it == m_macTable.end())
{
// 情况 1: 新 MAC 地址 - 添加到表
m_macTable[source] = inDevice;
NS_LOG_INFO(m_switchName << ": Learned " << source
<< " on port " << inDevice->GetIfIndex());
}
else if (it->second != inDevice)
{
// 情况 2: MAC 地址存在但端口变了 - 更新端口
it->second = inDevice;
NS_LOG_INFO(m_switchName << ": Updated " << source
<< " to port " << inDevice->GetIfIndex());
}
// 情况 3: MAC 地址和端口都匹配 - 无需操作
}
|
MAC 表结构:
1
| std::map<Mac48Address, Ptr<NetDevice>> m_macTable;
|
示例:
1
2
3
4
5
6
7
8
| MAC 表内容 (Switch 1):
┌─────────────────────┬──────────┐
│ MAC 地址 │ 端口号 │
├─────────────────────┼──────────┤
│ aa:bb:cc:dd:ee:01 │ 0 │ // Host B
│ aa:bb:cc:dd:ee:02 │ 1 │ // Switch 0
│ aa:bb:cc:dd:ee:03 │ 2 │ // Switch 2
└─────────────────────┴──────────┘
|
步骤 3: 转发决策
3.1 广播帧处理
1
2
3
4
5
| if (dstMac.IsBroadcast()) // 检查是否为 FF:FF:FF:FF:FF:FF
{
NS_LOG_INFO(m_switchName << ": Broadcasting packet from " << srcMac);
ForwardBroadcast(inDevice, packet, protocol, to);
}
|
3.2 单播帧处理
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| else
{
// 查找目的 MAC 地址
Ptr<NetDevice> outDevice = GetLearnedPort(dstMac);
if (outDevice && outDevice != inDevice)
{
// 情况 1: 已知端口且与入端口不同 - 单播转发
ForwardUnicast(outDevice, packet, protocol, to);
}
else if (!outDevice)
{
// 情况 2: 未知端口 - 泛洪
ForwardBroadcast(inDevice, packet, protocol, to);
}
else
{
// 情况 3: 目的端口 = 入端口 - 丢弃(防环路)
NS_LOG_DEBUG(m_switchName << ": Dropping packet");
}
}
|
步骤 4: 数据包转发
4.1 单播转发 (ForwardUnicast)
1
2
3
4
5
6
7
8
9
| void L2SwitchProtocol::ForwardUnicast(
Ptr<NetDevice> outDevice,
Ptr<const Packet> packet,
uint16_t protocol,
const Address& destination)
{
// 通过指定端口发送数据包
outDevice->Send(packet->Copy(), destination, protocol);
}
|
关键点:
- 使用
packet->Copy() 创建数据包副本
- 只向一个端口发送(单播)
- 保持以太网帧头不变
4.2 广播转发 (ForwardBroadcast)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| void L2SwitchProtocol::ForwardBroadcast(
Ptr<NetDevice> inDevice,
Ptr<const Packet> packet,
uint16_t protocol,
const Address& destination)
{
// 向所有端口转发(除了入端口)
uint32_t nDevices = m_node->GetNDevices();
for (uint32_t i = 0; i < nDevices; ++i)
{
Ptr<NetDevice> device = m_node->GetDevice(i);
if (device != inDevice) // 排除入端口(防止回发)
{
device->Send(packet->Copy(), destination, protocol);
}
}
}
|
关键点:
- 遍历所有端口
- 排除入端口(水平分割原则)
- 为每个出端口创建独立的数据包副本
5. 关键类和方法说明
5.1 L2SwitchProtocol 类
5.1.1 初始化方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| void L2SwitchProtocol::Initialize()
{
// 必须在所有设备都添加完毕后调用
uint32_t nDevices = m_node->GetNDevices();
for (uint32_t i = 0; i < nDevices; ++i)
{
Ptr<NetDevice> device = m_node->GetDevice(i);
// 为每个设备注册混杂模式回调
// 这样我们就能监听所有经过这个设备的数据包
device->SetPromiscReceiveCallback(
MakeCallback(&L2SwitchProtocol::ReceiveFromDevice, this));
}
}
|
混杂模式 (Promiscuous Mode):
- 正常模式: NetDevice 只接收目的地址为自己的数据包
- 混杂模式: NetDevice 接收所有经过的数据包(包括不是发给自己的)
- 交换机必须工作在混杂模式,才能转发其他设备的数据包
5.1.2 核心转发方法总览
| 方法 |
功能 |
调用时机 |
ReceiveFromDevice() |
接收和分发数据包 |
数据包到达时自动调用 |
Learn() |
MAC 地址学习 |
每次收到数据包时 |
GetLearnedPort() |
查询 MAC 表 |
转发决策时 |
ForwardUnicast() |
单播转发 |
已知目的端口时 |
ForwardBroadcast() |
泛洪转发 |
广播或未知目的时 |
5.2 L2SwitchHelper 类
5.2.1 安装方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| void L2SwitchHelper::Install(Ptr<Node> node, const std::string& name)
{
// 1. 检查是否已安装
Ptr<L2SwitchProtocol> protocol = node->GetObject<L2SwitchProtocol>();
if (protocol)
{
NS_LOG_WARN("L2SwitchProtocol already installed");
return;
}
// 2. 创建协议对象
protocol = CreateObject<L2SwitchProtocol>();
protocol->SetSwitchName(name);
protocol->SetNode(node);
// 3. 聚合到节点(关键步骤!)
node->AggregateObject(protocol);
}
|
使用示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| // 创建交换机节点
NodeContainer switches;
switches.Create(3);
// 安装 L2 交换协议
L2SwitchHelper switchHelper;
switchHelper.Install(switches.Get(0), "Switch0");
switchHelper.Install(switches.Get(1), "Switch1");
switchHelper.Install(switches.Get(2), "Switch2");
// 初始化协议(在所有设备创建后)
for (uint32_t i = 0; i < switches.GetN(); ++i)
{
Ptr<L2SwitchProtocol> protocol = switches.Get(i)->GetObject<L2SwitchProtocol>();
protocol->Initialize();
}
|
6. MAC 地址学习机制
6.1 学习过程
MAC 地址学习是交换机的核心功能,它通过观察数据包的源 MAC 地址来建立转发表:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
| 时间线:
t=0: MAC 表为空
┌──────────────┐
│ Switch 1 │
│ MAC Table: {} │
└──────────────┘
t=1: Host B (MAC=BB:BB:BB:BB:BB:BB) 发送数据包到 Switch 1 端口 0
ReceiveFromDevice() 被调用:
- from = BB:BB:BB:BB:BB:BB
- inDevice = 端口 0
Learn() 执行:
- m_macTable[BB:BB:BB:BB:BB:BB] = 端口 0
┌─────────────────────────────┐
│ Switch 1 │
│ MAC Table: │
│ BB:BB:BB:BB:BB:BB → 端口 0│
└─────────────────────────────┘
t=2: Switch 0 (MAC=AA:AA:AA:AA:AA:AA) 发送数据包到 Switch 1 端口 1
Learn() 执行:
- m_macTable[AA:AA:AA:AA:AA:AA] = 端口 1
┌─────────────────────────────┐
│ Switch 1 │
│ MAC Table: │
│ BB:BB:BB:BB:BB:BB → 端口 0│
│ AA:AA:AA:AA:AA:AA → 端口 1│
└─────────────────────────────┘
|
6.2 动态更新
如果同一个 MAC 地址从不同端口发送数据(例如主机移动了),MAC 表会自动更新:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| void L2SwitchProtocol::Learn(Mac48Address source, Ptr<NetDevice> inDevice)
{
auto it = m_macTable.find(source);
if (it == m_macTable.end())
{
// 新 MAC - 添加
m_macTable[source] = inDevice;
}
else if (it->second != inDevice)
{
// MAC 存在但端口不同 - 更新
it->second = inDevice;
NS_LOG_INFO("Updated " << source << " to port " << inDevice->GetIfIndex());
}
}
|
6.3 学习表的局限性
当前实现:
- ✅ 支持动态学习
- ✅ 支持端口更新
- ❌ 不支持表项老化(超时删除)
- ❌ 不支持表大小限制
改进方向:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
| // 可以添加表项老化机制
struct MacTableEntry {
Ptr<NetDevice> port;
Time lastSeen; // 最后一次看到的时间
};
std::map<Mac48Address, MacTableEntry> m_macTable;
// 定期清理过期表项
void AgeTable() {
Time now = Simulator::Now();
for (auto it = m_macTable.begin(); it != m_macTable.end(); )
{
if (now - it->second.lastSeen > Seconds(300)) // 5分钟超时
{
it = m_macTable.erase(it);
}
else
{
++it;
}
}
}
|
7. 转发决策逻辑
7.1 决策流程图
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
| 收到数据包
│
▼
┌─────────────────┐
│ 学习源 MAC │
│ Learn(srcMac) │
└─────────────────┘
│
▼
目的 MAC 是广播地址?
/ \
是 否
/ \
▼ ▼
┌──────────┐ 查找 MAC 表
│ 泛洪转发 │ GetLearnedPort(dstMac)
└──────────┘ │
│
┌────────────┴────────────┐
│ │
找到端口 未找到端口
│ │
▼ ▼
出端口 = 入端口? ┌──────────┐
/ \ │ 泛洪转发 │
是 否 └──────────┘
/ \
▼ ▼
┌────────┐ ┌──────────┐
│ 丢弃 │ │ 单播转发 │
└────────┘ └──────────┘
|
7.2 三种转发场景
7.2.1 场景 1: 广播转发
触发条件:
- 目的 MAC 地址为
FF:FF:FF:FF:FF:FF(广播地址)
- 或未知的目的 MAC 地址
转发行为:
1
2
3
4
5
6
7
| ForwardBroadcast(inDevice, packet, protocol, to);
// 实际执行:
for (所有端口 except 入端口)
{
端口->Send(packet->Copy());
}
|
示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| ARP 请求广播:
┌────────────────────────────────────────┐
│ 以太网帧头: │
│ Src MAC: aa:bb:cc:dd:ee:01 (Host A) │
│ Dst MAC: ff:ff:ff:ff:ff:ff (广播) │
│ ARP 内容: │
│ Who has 192.168.1.3? Tell 192.168.1.1│
└────────────────────────────────────────┘
│
▼ 到达 Switch 0 端口 0
│
┌───────────┴──────────┐
│ │
▼ ▼
端口 1 转发 端口 0 不转发
(到 Switch 1) (入端口)
|
7.2.2 场景 2: 单播转发(已知目的)
触发条件:
- MAC 表中存在目的 MAC 地址
- 出端口与入端口不同
转发行为:
1
2
3
4
5
| Ptr<NetDevice> outDevice = GetLearnedPort(dstMac);
if (outDevice && outDevice != inDevice)
{
ForwardUnicast(outDevice, packet, protocol, to);
}
|
示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| 已学习到 Host C (MAC=cc:cc:cc:cc:cc:cc) 在 Switch 2 端口 0
Host A → Host C 的数据包:
┌────────────────────────────────────────┐
│ 以太网帧头: │
│ Src MAC: aa:aa:aa:aa:aa:aa (Host A) │
│ Dst MAC: cc:cc:cc:cc:cc:cc (Host C) │
└────────────────────────────────────────┘
│
▼ 到达 Switch 0 端口 0
│
查表: cc:cc:cc:cc:cc:cc → 端口 1
│
▼ 只从端口 1 转发
│
(到 Switch 1)
|
7.2.3 场景 3: 泛洪转发(未知目的)
触发条件:
- MAC 表中不存在目的 MAC 地址
- 且目的地址不是广播地址
转发行为:
1
2
3
4
5
| Ptr<NetDevice> outDevice = GetLearnedPort(dstMac);
if (!outDevice)
{
ForwardBroadcast(inDevice, packet, protocol, to);
}
|
示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| 第一次发送到 Host C (MAC 表中还没有 Host C):
┌────────────────────────────────────────┐
│ 以太网帧头: │
│ Src MAC: aa:aa:aa:aa:aa:aa (Host A) │
│ Dst MAC: cc:cc:cc:cc:cc:cc (Host C) │
└────────────────────────────────────────┘
│
▼ 到达 Switch 0 端口 0
│
查表: cc:cc:cc:cc:cc:cc → 未找到!
│
▼ 泛洪到所有端口 (除入端口)
┌───────────┴──────────┐
│ │
▼ ▼
端口 1 转发 端口 0 不转发
(到 Switch 1) (入端口)
|
8. 完整的包转发示例
8.1 场景: Host A 向 Host C 发送 UDP 数据包
8.1.1 初始状态
1
2
3
4
5
| 所有交换机的 MAC 表都是空的:
Switch 0: {}
Switch 1: {}
Switch 2: {}
|
8.1.2 步骤 1: Host A 发送 ARP 请求
目的: Host A 需要知道 Host C (192.168.1.3) 的 MAC 地址
1
2
3
4
5
6
7
8
9
10
11
12
13
| ┌──────────────────────────────────────────────┐
│ ARP Request (广播) │
│ Ethernet Header: │
│ Src MAC: AA:AA:AA:AA:AA:AA (Host A MAC) │
│ Dst MAC: FF:FF:FF:FF:FF:FF (广播) │
│ Type: 0x0806 (ARP) │
│ ARP Payload: │
│ Operation: Request (1) │
│ Sender MAC: AA:AA:AA:AA:AA:AA │
│ Sender IP: 192.168.1.1 │
│ Target MAC: 00:00:00:00:00:00 (未知) │
│ Target IP: 192.168.1.3 │
└──────────────────────────────────────────────┘
|
转发路径:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
| 1️⃣ 到达 Switch 0 端口 0
ReceiveFromDevice():
- from = AA:AA:AA:AA:AA:AA
- to = FF:FF:FF:FF:FF:FF
- inDevice = 端口 0
Learn():
- Switch 0 学习: AA:AA:AA:AA:AA:AA → 端口 0
转发决策:
- 目的是广播 → ForwardBroadcast()
- 从端口 1 转发到 Switch 1
Switch 0 MAC 表:
AA:AA:AA:AA:AA:AA → 端口 0
2️⃣ 到达 Switch 1 端口 1
ReceiveFromDevice():
- from = AA:AA:AA:AA:AA:AA
- to = FF:FF:FF:FF:FF:FF
- inDevice = 端口 1
Learn():
- Switch 1 学习: AA:AA:AA:AA:AA:AA → 端口 1
转发决策:
- 目的是广播 → ForwardBroadcast()
- 从端口 0 转发到 Host B
- 从端口 2 转发到 Switch 2
Switch 1 MAC 表:
AA:AA:AA:AA:AA:AA → 端口 1
3️⃣ 到达 Switch 2 端口 1
ReceiveFromDevice():
- from = AA:AA:AA:AA:AA:AA
- to = FF:FF:FF:FF:FF:FF
- inDevice = 端口 1
Learn():
- Switch 2 学习: AA:AA:AA:AA:AA:AA → 端口 1
转发决策:
- 目的是广播 → ForwardBroadcast()
- 从端口 0 转发到 Host C ✓
Switch 2 MAC 表:
AA:AA:AA:AA:AA:AA → 端口 1
4️⃣ Host C 收到 ARP 请求
- 检查 Target IP (192.168.1.3) 是自己 ✓
- 准备 ARP 响应
|
8.1.3 步骤 2: Host C 发送 ARP 响应
1
2
3
4
5
6
7
8
9
10
11
12
13
| ┌──────────────────────────────────────────────┐
│ ARP Reply (单播) │
│ Ethernet Header: │
│ Src MAC: CC:CC:CC:CC:CC:CC (Host C MAC) │
│ Dst MAC: AA:AA:AA:AA:AA:AA (Host A MAC) │
│ Type: 0x0806 (ARP) │
│ ARP Payload: │
│ Operation: Reply (2) │
│ Sender MAC: CC:CC:CC:CC:CC:CC │
│ Sender IP: 192.168.1.3 │
│ Target MAC: AA:AA:AA:AA:AA:AA │
│ Target IP: 192.168.1.1 │
└──────────────────────────────────────────────┘
|
转发路径:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
| 1️⃣ 到达 Switch 2 端口 0
Learn():
- Switch 2 学习: CC:CC:CC:CC:CC:CC → 端口 0
转发决策:
- 查表: AA:AA:AA:AA:AA:AA → 端口 1 ✓
- 单播转发到端口 1
Switch 2 MAC 表:
AA:AA:AA:AA:AA:AA → 端口 1
CC:CC:CC:CC:CC:CC → 端口 0
2️⃣ 到达 Switch 1 端口 2
Learn():
- Switch 1 学习: CC:CC:CC:CC:CC:CC → 端口 2
转发决策:
- 查表: AA:AA:AA:AA:AA:AA → 端口 1 ✓
- 单播转发到端口 1
Switch 1 MAC 表:
AA:AA:AA:AA:AA:AA → 端口 1
CC:CC:CC:CC:CC:CC → 端口 2
3️⃣ 到达 Switch 0 端口 1
Learn():
- Switch 0 学习: CC:CC:CC:CC:CC:CC → 端口 1
转发决策:
- 查表: AA:AA:AA:AA:AA:AA → 端口 0 ✓
- 单播转发到端口 0
Switch 0 MAC 表:
AA:AA:AA:AA:AA:AA → 端口 0
CC:CC:CC:CC:CC:CC → 端口 1
4️⃣ Host A 收到 ARP 响应
- 缓存 Host C 的 MAC 地址
- 现在可以发送 IP 数据包了!
|
8.1.4 步骤 3: Host A 发送 UDP 数据包
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| ┌──────────────────────────────────────────────┐
│ IP Packet (UDP Echo Request) │
│ Ethernet Header: │
│ Src MAC: AA:AA:AA:AA:AA:AA │
│ Dst MAC: CC:CC:CC:CC:CC:CC │
│ Type: 0x0800 (IPv4) │
│ IP Header: │
│ Src IP: 192.168.1.1 │
│ Dst IP: 192.168.1.3 │
│ Protocol: 17 (UDP) │
│ UDP Header: │
│ Src Port: 49153 │
│ Dst Port: 9 (Echo) │
│ Payload: 512 bytes │
└──────────────────────────────────────────────┘
|
转发路径 (现在所有交换机都学习了 MAC 地址):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| 1️⃣ Switch 0 端口 0
- 查表: CC:CC:CC:CC:CC:CC → 端口 1 ✓
- 单播转发到端口 1
2️⃣ Switch 1 端口 1
- 查表: CC:CC:CC:CC:CC:CC → 端口 2 ✓
- 单播转发到端口 2
3️⃣ Switch 2 端口 1
- 查表: CC:CC:CC:CC:CC:CC → 端口 0 ✓
- 单播转发到端口 0
4️⃣ Host C 收到 UDP 数据包
- 应用层处理 (UdpEchoServer)
- 准备 Echo Reply
|
8.1.5 步骤 4: Host C 发送 UDP Echo 响应
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| ┌──────────────────────────────────────────────┐
│ IP Packet (UDP Echo Reply) │
│ Ethernet Header: │
│ Src MAC: CC:CC:CC:CC:CC:CC │
│ Dst MAC: AA:AA:AA:AA:AA:AA │
│ Type: 0x0800 (IPv4) │
│ IP Header: │
│ Src IP: 192.168.1.3 │
│ Dst IP: 192.168.1.1 │
│ Protocol: 17 (UDP) │
│ UDP Header: │
│ Src Port: 9 │
│ Dst Port: 49153 │
│ Payload: 512 bytes (echo back) │
└──────────────────────────────────────────────┘
|
转发路径:
1
2
| Switch 2 → Switch 1 → Switch 0 → Host A
(所有交换机都已学习路径,全程单播转发)
|
8.2 关键观察
- 首次通信需要泛洪: ARP 请求是广播,必须泛洪
- 学习是双向的:
- ARP Request 让所有交换机学习 Host A 的位置
- ARP Reply 让所有交换机学习 Host C 的位置
- 后续通信高效: 一旦学习完成,所有数据包都是单播转发
- 学习过程透明: 应用层无感知,完全由交换机自动处理
9. 运行和测试
9.1 编译和运行
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| # 1. 配置 ns-3 (如果还没配置)
cd /path/to/ns-3.44
./ns3 configure --enable-examples --enable-tests
# 2. 编译程序 (方法一: 使用 ns3 脚本)
./ns3 build scratch/l2-switch-protocol
# 2. 编译程序 (方法二: 使用 cmake)
cmake --build cmake-cache -j4 --target scratch_l2-switch-protocol
# 3. 运行仿真
./build/scratch/ns3.44-l2-switch-protocol-default
# 4. 启用详细日志
export NS_LOG=L2SwitchProtocol=level_all
./build/scratch/ns3.44-l2-switch-protocol-default
|
9.2 预期输出
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
| === Creating Multi-Switch Network Topology ===
Topology: [Host A]-SW0-SW1-SW2-[Host C], [Host B]-SW1
Created link: Host A <-> Switch0
Created link: Switch0 <-> Switch1
Created link: Host B <-> Switch1
Created link: Switch1 <-> Switch2
Created link: Host C <-> Switch2
Installed L2SwitchProtocol on node 3 (Switch0)
Installed L2SwitchProtocol on node 4 (Switch1)
Installed L2SwitchProtocol on node 5 (Switch2)
Switch0: Initializing with 2 devices
Switch0: Registered callback on device 0 (MAC: 00:00:00:00:00:02)
Switch0: Registered callback on device 1 (MAC: 00:00:00:00:00:03)
Switch1: Initializing with 3 devices
Switch1: Registered callback on device 0 (MAC: 00:00:00:00:00:04)
Switch1: Registered callback on device 1 (MAC: 00:00:00:00:00:06)
Switch1: Registered callback on device 2 (MAC: 00:00:00:00:00:07)
Switch2: Initializing with 2 devices
Switch2: Registered callback on device 0 (MAC: 00:00:00:00:00:08)
Switch2: Registered callback on device 1 (MAC: 00:00:00:00:00:0a)
=== IP Addresses ===
Host A: 192.168.1.1
Host B: 192.168.1.2
Host C: 192.168.1.3
=== Starting Simulation ===
At time +1s client sent 512 bytes to 192.168.1.3 port 9
Switch0: Learned 00:00:00:00:00:01 on port 0
Switch0: Broadcasting packet from 00:00:00:00:00:01
Switch1: Learned 00:00:00:00:00:03 on port 0
Switch1: Broadcasting packet from 00:00:00:00:00:03
Switch2: Learned 00:00:00:00:00:07 on port 0
Switch2: Broadcasting packet from 00:00:00:00:00:07
Switch2: Learned 00:00:00:00:00:09 on port 1
Switch2: Unknown destination 00:00:00:00:00:01, flooding
Switch1: Learned 00:00:00:00:00:08 on port 2
Switch1: Unknown destination 00:00:00:00:00:01, flooding
Switch0: Learned 00:00:00:00:00:04 on port 1
Switch0: Forwarding 00:00:00:00:00:04 -> 00:00:00:00:00:01 via port 0
...
At time +1.0033s server received 512 bytes from 192.168.1.1 port 49153
At time +1.0033s server sent 512 bytes to 192.168.1.1 port 49153
...
At time +1.0066s client received 512 bytes from 192.168.1.3 port 9
At time +2s client sent 512 bytes to 192.168.1.3 port 9
At time +2s client sent 512 bytes to 192.168.1.3 port 9
...
=== Simulation Complete ===
|
9.3 验证点
✅ 检查项 1: 多交换机初始化
1
2
3
4
| 每个交换机都应该正确初始化:
- Switch0: 2 个端口 (连接 Host A 和 Switch1)
- Switch1: 3 个端口 (连接 Switch0, Host B, Switch2)
- Switch2: 2 个端口 (连接 Switch1 和 Host C)
|
✅ 检查项 2: MAC 地址学习
1
2
3
4
| 每个交换机独立学习入端口的 MAC 地址:
- Switch0 学习: Host A 的 MAC 在端口 0
- Switch1 学习: Switch0 端口的 MAC 在端口 0, Host B 的 MAC 在端口 1
- Switch2 学习: Switch1 端口的 MAC 在端口 0, Host C 的 MAC 在端口 1
|
✅ 检查项 3: 多跳转发
1
2
3
| - Host A -> Host C: 经过 Switch0 -> Switch1 -> Switch2 (3跳)
- Host B -> Host C: 经过 Switch1 -> Switch2 (2跳)
- 首次通信泛洪,后续通信单播转发
|
✅ 检查项 4: 应用层通信
1
2
3
| - Host A 发送 3 个数据包,全部收到响应
- Host B 发送 2 个数据包,全部收到响应
- 通信成功完成
|
9.4 调试技巧
9.4.1 启用选择性日志
1
2
3
4
5
6
7
8
| # 只看 INFO 级别的日志
export NS_LOG=L2SwitchProtocol=level_info
# 看所有级别的日志
export NS_LOG=L2SwitchProtocol=level_all
# 带时间戳和节点ID
export NS_LOG=L2SwitchProtocol=level_all:prefix_time:prefix_node
|
9.4.2 抓包分析
在代码中添加 PCAP 跟踪:
1
2
3
4
5
| // 在 main() 函数中添加
csma.EnablePcapAll("l2-switch");
// 运行后会生成 .pcap 文件
// 使用 Wireshark 打开查看
|
9.4.3 常见问题排查
问题 1: 数据包丢失
1
2
| 原因: 可能没有调用 Initialize()
解决: 确保在所有设备创建后调用 protocol->Initialize()
|
问题 2: MAC 表学习失败
1
2
| 原因: 混杂模式回调未正确注册
解决: 检查 SetPromiscReceiveCallback() 是否被调用
|
问题 3: 转发环路
1
2
| 原因: 没有正确排除入端口
解决: 检查 ForwardBroadcast() 中的 if (device != inDevice) 条件
|
10. 重要问题:Point-to-Point 链路与 MAC 地址
10.1 问题描述
在最初的实现中,我们使用 PointToPointHelper 创建链路连接主机和交换机。运行仿真时,发现交换机持续泛洪,永远无法学习到目的 MAC 地址,导致网络风暴:
1
2
3
4
5
6
7
| Switch0: Learned 00:00:00:00:00:01 on port 0
Switch0: Unknown destination 00:00:00:00:00:02, flooding
Switch1: Learned 00:00:00:00:00:07 on port 1
Switch1: Unknown destination 00:00:00:00:00:08, flooding
Switch2: Learned 00:00:00:00:00:09 on port 1
Switch2: Unknown destination 00:00:00:00:00:0a, flooding
... (无限循环泛洪)
|
关键观察:
- 交换机学习到的源 MAC 是主机的 MAC(如
00:00:00:00:00:01)
- 但数据包的目的 MAC 是交换机端口自己的 MAC(如
00:00:00:00:00:02)
- 这与真实以太网的行为完全不同!
10.2 根本原因分析
10.2.1 Point-to-Point 链路的工作方式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
| ┌─────────────────────────────────────────────────────────────────────────┐
│ Point-to-Point 链路的 MAC 地址行为 │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ Host A Switch 0 │
│ MAC: 00:00:00:00:00:01 端口0 MAC: 00:00:00:00:00:02 │
│ │
│ ┌──────────────────┐ ┌──────────────────┐ │
│ │ PointToPoint │────────────────│ PointToPoint │ │
│ │ NetDevice │ │ NetDevice │ │
│ └──────────────────┘ └──────────────────┘ │
│ │
│ 当 Host A 发送数据包时: │
│ ┌────────────────────────────────────┐ │
│ │ 原始以太网帧: │ │
│ │ Src MAC: 00:00:00:00:00:01 │ ← Host A 的 MAC │
│ │ Dst MAC: 00:00:00:00:00:05 │ ← 最终目的地(如 Host C) │
│ └────────────────────────────────────┘ │
│ │ │
│ ▼ Point-to-Point 链路发送 │
│ │
│ ┌────────────────────────────────────┐ │
│ │ 到达交换机时的帧: │ │
│ │ Src MAC: 00:00:00:00:00:01 │ ← Host A 的 MAC (不变) │
│ │ Dst MAC: 00:00:00:00:00:02 │ ← 变成了交换机端口的 MAC! │
│ └────────────────────────────────────┘ │
│ │
│ 问题:目的 MAC 被改成了对端设备的 MAC,原始目的地址丢失了! │
│ │
└─────────────────────────────────────────────────────────────────────────┘
|
10.2.2 真实以太网(CSMA)的工作方式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
| ┌─────────────────────────────────────────────────────────────────────────┐
│ CSMA(以太网)的 MAC 地址行为 │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ Host A Switch 0 │
│ MAC: 00:00:00:00:00:01 端口0 MAC: 00:00:00:00:00:02 │
│ │
│ ┌──────────────────┐ ┌──────────────────┐ │
│ │ CSMA NetDevice │────────────────│ CSMA NetDevice │ │
│ └──────────────────┘ 共享介质 └──────────────────┘ │
│ │
│ 当 Host A 发送数据包时: │
│ ┌────────────────────────────────────┐ │
│ │ 原始以太网帧: │ │
│ │ Src MAC: 00:00:00:00:00:01 │ ← Host A 的 MAC │
│ │ Dst MAC: 00:00:00:00:00:05 │ ← 最终目的地(如 Host C) │
│ └────────────────────────────────────┘ │
│ │ │
│ ▼ CSMA 链路发送(广播介质) │
│ │
│ ┌────────────────────────────────────┐ │
│ │ 到达交换机时的帧: │ │
│ │ Src MAC: 00:00:00:00:00:01 │ ← Host A 的 MAC (不变) │
│ │ Dst MAC: 00:00:00:00:00:05 │ ← 目的 MAC 保持不变! ✓ │
│ └────────────────────────────────────┘ │
│ │
│ 正确:目的 MAC 在整个传输过程中保持不变,交换机可以正确学习和转发! │
│ │
└─────────────────────────────────────────────────────────────────────────┘
|
10.2.3 技术差异对比
| 特性 |
Point-to-Point |
CSMA (以太网) |
| 链路类型 |
点对点(两个端点) |
多点接入(共享介质) |
| MAC 地址处理 |
目的 MAC 改为对端设备 MAC |
目的 MAC 保持原始值不变 |
| 帧格式 |
可以不使用标准以太网帧 |
标准以太网帧(802.3) |
| 适用场景 |
路由器间连接、WAN 链路 |
LAN、交换机网络 |
| 二层交换 |
❌ 不适合 |
✅ 完美支持 |
10.3 问题的具体表现
使用 Point-to-Point 链路时的错误行为:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
| 时间线分析:
t=1.0s: Host A (MAC=01) 发送 ARP 请求到 Host C (MAC=05)
┌─────────────────────────────────────┐
│ 原始帧: Src=01, Dst=FF:FF:FF:FF:FF:FF│
└─────────────────────────────────────┘
│
▼ 到达 Switch 0 端口 0
┌─────────────────────────────────────┐
│ 回调收到: from=01, to=02 │ ← 目的变成端口 MAC!
└─────────────────────────────────────┘
Switch0: Learned 01 on port 0 ← 正确学习源 MAC
Switch0: Unknown destination 02 ← 查找 02,永远找不到!
Switch0: Flooding... ← 被迫泛洪
t=1.001s: 泛洪的帧到达 Switch 1
┌─────────────────────────────────────┐
│ 回调收到: from=07, to=08 │ ← 又变成新的端口 MAC!
└─────────────────────────────────────┘
Switch1: Learned 07 on port 1 ← 学习的是错误的 MAC!
Switch1: Unknown destination 08 ← 查找 08,永远找不到!
Switch1: Flooding... ← 继续泛洪
... 无限循环,每个交换机都在学习错误的 MAC 并持续泛洪 ...
|
10.4 解决方案:使用 CSMA 链路 + 自定义 L2SwitchProtocol
正确的解决方案是使用 CSMA(以太网)链路来保持 MAC 地址不变,同时让我们的自定义 L2SwitchProtocol 来实现真正的二层转发功能:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
| #include "ns3/csma-module.h"
// 1. 创建 CSMA 链路(不使用 BridgeNetDevice!)
CsmaHelper csma;
csma.SetChannelAttribute("DataRate", StringValue("100Mbps"));
csma.SetChannelAttribute("Delay", TimeValue(NanoSeconds(6560)));
// 2. 为每个主机创建到交换机的链路
NetDeviceContainer switchDevices;
NetDeviceContainer hostDevices;
for (uint32_t i = 0; i < hosts.GetN(); ++i)
{
NodeContainer link;
link.Add(hosts.Get(i));
link.Add(switchNode.Get(0));
NetDeviceContainer linkDevices = csma.Install(link);
hostDevices.Add(linkDevices.Get(0)); // 主机端
switchDevices.Add(linkDevices.Get(1)); // 交换机端
}
// 3. 安装我们的自定义 L2SwitchProtocol(不使用 BridgeHelper!)
L2SwitchHelper switchHelper;
switchHelper.Install(switchNode.Get(0), "CentralSwitch");
// 4. 初始化协议 - 注册混杂模式回调
Ptr<L2SwitchProtocol> protocol = switchNode.Get(0)->GetObject<L2SwitchProtocol>();
protocol->Initialize();
|
关键点:
- ✅ 使用 CSMA 链路保持 MAC 地址不变
- ✅ 使用自定义
L2SwitchProtocol 实现转发
- ✅ 不依赖 ns-3 内置的
BridgeNetDevice
- ✅ 协议通过混杂模式回调接收所有数据包
- ✅ 协议自主完成 MAC 学习和转发决策
10.5 修复后的正确输出
使用 CSMA + 自定义 L2SwitchProtocol 后的正常运行日志:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
| === Creating Network Topology ===
Created 3 switch ports
Installed L2SwitchProtocol on node 3 (CentralSwitch)
CentralSwitch: Initializing with 3 devices
CentralSwitch: Registered callback on device 0 (MAC: 00:00:00:00:00:02)
CentralSwitch: Registered callback on device 1 (MAC: 00:00:00:00:00:04)
CentralSwitch: Registered callback on device 2 (MAC: 00:00:00:00:00:06)
=== IP Addresses ===
Host A: 192.168.1.1
Host B: 192.168.1.2
Host C: 192.168.1.3
=== Starting Simulation ===
At time +1s client sent 512 bytes to 192.168.1.3 port 9
CentralSwitch: Learned 00:00:00:00:00:01 on port 0 ← Host A
CentralSwitch: Broadcasting packet from 00:00:00:00:00:01 ← 第一次广播(ARP)
CentralSwitch: Learned 00:00:00:00:00:05 on port 2 ← Host C
CentralSwitch: Forwarding 00:00:00:00:00:05 -> 00:00:00:00:00:01 via port 0 ← 单播!
CentralSwitch: Forwarding 00:00:00:00:00:01 -> 00:00:00:00:00:05 via port 2 ← 单播!
At time +1.00715s server received 512 bytes from 192.168.1.1 port 49153
At time +1.00715s server sent 512 bytes to 192.168.1.1 port 49153
CentralSwitch: Broadcasting packet from 00:00:00:00:00:05
CentralSwitch: Forwarding 00:00:00:00:00:01 -> 00:00:00:00:00:05 via port 2
CentralSwitch: Forwarding 00:00:00:00:00:05 -> 00:00:00:00:00:01 via port 0
At time +1.0133s client received 512 bytes from 192.168.1.3 port 9
...后续通信全部使用单播转发...
=== Simulation Complete ===
|
观察到的正确行为:
- ✅ 第一次通信时广播(ARP 请求)
- ✅ 学习到正确的源 MAC 地址
- ✅ 后续通信变为单播转发
- ✅ 通信成功完成
- ✅ 所有转发由自定义 L2SwitchProtocol 完成
10.6 关键教训总结
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
| ┌─────────────────────────────────────────────────────────────────────────┐
│ 关键教训 │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ 1. Point-to-Point ≠ 以太网 │
│ - P2P 是点对点链路,不适合模拟交换机网络 │
│ - P2P 会修改以太网帧的目的 MAC 地址 │
│ │
│ 2. 选择正确的 NetDevice 类型 │
│ - 交换机网络 → CsmaNetDevice(以太网) │
│ - 路由器间连接 → PointToPointNetDevice │
│ - 无线网络 → WifiNetDevice │
│ │
│ 3. 自定义协议实现转发 │
│ - L2SwitchProtocol 通过混杂模式回调接收数据包 │
│ - 协议自主完成 MAC 学习和转发决策 │
│ - 不需要依赖 ns-3 内置的 BridgeNetDevice │
│ │
│ 4. 调试技巧 │
│ - 仔细观察 from 和 to 地址是否正确 │
│ - 检查学习的 MAC 是否与预期一致 │
│ - 持续泛洪通常意味着 MAC 地址传递有问题 │
│ │
└─────────────────────────────────────────────────────────────────────────┘
|
10.7 ns-3 链路类型选择指南
| 场景 |
推荐链路类型 |
NetDevice |
Helper |
| LAN 交换网络 |
CSMA |
CsmaNetDevice |
CsmaHelper + 自定义协议 |
| 路由器间连接 |
Point-to-Point |
PointToPointNetDevice |
PointToPointHelper |
| 数据中心网络 |
CSMA |
CsmaNetDevice |
CsmaHelper |
| 无线局域网 |
WiFi |
WifiNetDevice |
WifiHelper |
| 广域网 |
Point-to-Point |
PointToPointNetDevice |
PointToPointHelper |
10.8 完整的多交换机代码结构
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
| // ========== 使用自定义协议实现多交换机二层网络 ==========
// 1. 只需要 CSMA 模块,不需要 bridge-module
#include "ns3/csma-module.h"
// 2. 创建节点
NodeContainer hosts;
hosts.Create(3); // Host A, Host B, Host C
NodeContainer switches;
switches.Create(3); // Switch 0, Switch 1, Switch 2
// 3. 创建 CSMA 设备
CsmaHelper csma;
csma.SetChannelAttribute("DataRate", StringValue("100Mbps"));
csma.SetChannelAttribute("Delay", TimeValue(NanoSeconds(6560)));
// 4. 创建链路连接
// Host A <-> Switch 0
{
NodeContainer link;
link.Add(hosts.Get(0));
link.Add(switches.Get(0));
NetDeviceContainer devices = csma.Install(link);
hostDevices.Add(devices.Get(0));
}
// Switch 0 <-> Switch 1
{
NodeContainer link;
link.Add(switches.Get(0));
link.Add(switches.Get(1));
csma.Install(link);
}
// Host B <-> Switch 1
{
NodeContainer link;
link.Add(hosts.Get(1));
link.Add(switches.Get(1));
NetDeviceContainer devices = csma.Install(link);
hostDevices.Add(devices.Get(0));
}
// Switch 1 <-> Switch 2
{
NodeContainer link;
link.Add(switches.Get(1));
link.Add(switches.Get(2));
csma.Install(link);
}
// Host C <-> Switch 2
{
NodeContainer link;
link.Add(hosts.Get(2));
link.Add(switches.Get(2));
NetDeviceContainer devices = csma.Install(link);
hostDevices.Add(devices.Get(0));
}
// 5. 在每个交换机上安装自定义 L2SwitchProtocol
L2SwitchHelper switchHelper;
switchHelper.Install(switches.Get(0), "Switch0");
switchHelper.Install(switches.Get(1), "Switch1");
switchHelper.Install(switches.Get(2), "Switch2");
// 6. 初始化每个交换机的协议
for (uint32_t i = 0; i < switches.GetN(); ++i)
{
Ptr<L2SwitchProtocol> protocol = switches.Get(i)->GetObject<L2SwitchProtocol>();
protocol->Initialize();
}
|
多交换机架构的关键点:
- ✅ 每个交换机独立运行 L2SwitchProtocol
- ✅ 每个交换机独立维护自己的 MAC 地址表
- ✅ 数据包通过多跳转发到达目的地
- ✅ 不依赖 ns-3 内置的 BridgeNetDevice
- ✅ 所有转发逻辑由自定义协议实现
11. 总结
11.1 核心要点
| 要点 |
说明 |
| 协议栈架构 |
使用 Object 继承和 AggregateObject() 实现协议 |
| 混杂模式 |
交换机必须在所有端口启用混杂模式 |
| MAC 学习 |
从源 MAC 学习,到目的 MAC 转发 |
| 转发决策 |
广播泛洪,单播查表,未知泛洪 |
| 防环路 |
不向入端口回发数据包 |
| 多交换机 |
每个交换机独立运行协议,数据包逐跳转发 |
11.2 多交换机转发流程
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| ┌─────────────────────────────────────────────────────────────────┐
│ 多交换机数据包转发流程 │
└─────────────────────────────────────────────────────────────────┘
Host A 发送数据包到 Host C:
Host A Host C
│ │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
└──│ Switch0 │──────│ Switch1 │──────│ Switch2 │─────────┘
└─────────┘ └─────────┘ └─────────┘
│ │ │
▼ ▼ ▼
1. 学习源MAC 2. 学习源MAC 3. 学习源MAC
2. 查表/泛洪 2. 查表/泛洪 2. 查表/泛洪
3. 转发到SW1 3. 转发到SW2 3. 转发到Host C
每个交换机独立维护 MAC 表:
- Switch0: 学习 Host A 的 MAC 在端口 0
- Switch1: 学习 Switch0 端口 MAC 在端口 0, Host B 的 MAC 在端口 1
- Switch2: 学习 Switch1 端口 MAC 在端口 0, Host C 的 MAC 在端口 1
|
11.3 与真实交换机的对比
| 特性 |
本实现 |
真实交换机 |
| MAC 学习 |
✅ 支持 |
✅ 支持 |
| 单播转发 |
✅ 支持 |
✅ 支持 |
| 广播泛洪 |
✅ 支持 |
✅ 支持 |
| 多交换机级联 |
✅ 支持 |
✅ 支持 |
| 表项老化 |
❌ 不支持 |
✅ 支持 (300s) |
| VLAN |
❌ 不支持 |
✅ 支持 |
| 生成树协议 |
❌ 不支持 |
✅ 支持 (STP/RSTP) |
| 端口镜像 |
❌ 不支持 |
✅ 支持 |
| QoS |
❌ 不支持 |
✅ 支持 |
11.4 扩展方向
- 添加 VLAN 支持
```cpp
// 为每个端口配置 VLAN
void SetPortVlan(uint32_t port, uint16_t vlanId);
// 只在相同 VLAN 内转发
if (inPort.vlan == outPort.vlan)
{
Forward();
}
1
2
3
4
5
6
7
8
|
2. **实现生成树协议 (STP)**
```cpp
class SpanningTreeProtocol : public Object
{
void ComputeSpanningTree();
void BlockPort(Ptr<NetDevice> port);
};
|
- 添加端口统计
1
2
3
4
5
6
| struct PortStats {
uint64_t rxPackets;
uint64_t txPackets;
uint64_t rxBytes;
uint64_t txBytes;
};
|
- 实现流量控制
1
2
3
4
5
| // 基于优先级的队列
class PriorityQueue {
std::queue<Ptr<Packet>> highPriority;
std::queue<Ptr<Packet>> lowPriority;
};
|
附录
A. 以太网帧格式
1
2
3
4
5
6
7
8
9
10
11
12
13
| ┌──────────────────────────────────────────────────────────┐
│ 以太网帧格式 │
├────────────┬─────────────────────────────────────────────┤
│ 目的 MAC │ 6 字节 (如 aa:bb:cc:dd:ee:ff) │
├────────────┼─────────────────────────────────────────────┤
│ 源 MAC │ 6 字节 │
├────────────┼─────────────────────────────────────────────┤
│ 类型 │ 2 字节 (0x0800=IPv4, 0x0806=ARP) │
├────────────┼─────────────────────────────────────────────┤
│ 负载 │ 46-1500 字节 │
├────────────┼─────────────────────────────────────────────┤
│ FCS │ 4 字节 (帧校验序列) │
└────────────┴─────────────────────────────────────────────┘
|
B. MAC 地址格式
1
2
3
4
5
6
7
| MAC 地址: 48 位 (6 字节)
表示方法: xx:xx:xx:xx:xx:xx (16进制)
特殊地址:
- 单播: 第一个字节的最低位 = 0 (如 00:11:22:33:44:55)
- 组播: 第一个字节的最低位 = 1 (如 01:00:5e:xx:xx:xx)
- 广播: ff:ff:ff:ff:ff:ff
|
C. 常用协议类型
| 类型值 |
协议 |
说明 |
| 0x0800 |
IPv4 |
Internet Protocol version 4 |
| 0x0806 |
ARP |
Address Resolution Protocol |
| 0x86DD |
IPv6 |
Internet Protocol version 6 |
| 0x8100 |
VLAN |
802.1Q VLAN Tagged Frame |
D. 参考资料
- ns-3 文档:
- 以太网标准:
- IEEE 802.3 (Ethernet)
- IEEE 802.1D (Spanning Tree Protocol)
- 相关 RFC:
- RFC 826: Address Resolution Protocol (ARP)
- RFC 1812: Requirements for IP Version 4 Routers
作者: Liu Mengxuan
版本: 2.0 (多交换机拓扑版)
ns-3 版本: 3.44
最后更新: 2025-12-01