NS3学习笔记04

NS3实现L2协议转发

Posted by Liu Mengxuan on December 2, 2025

本文是 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 类

继承关系: ObjectL2SwitchProtocol

核心职责:

  • 监听节点上所有网络设备的数据包
  • 学习 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: 数据包进入的网络设备(端口)
  • fromto: 以太网帧头中的源和目的 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 关键观察

  1. 首次通信需要泛洪: ARP 请求是广播,必须泛洪
  2. 学习是双向的:
    • ARP Request 让所有交换机学习 Host A 的位置
    • ARP Reply 让所有交换机学习 Host C 的位置
  3. 后续通信高效: 一旦学习完成,所有数据包都是单播转发
  4. 学习过程透明: 应用层无感知,完全由交换机自动处理

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 扩展方向

  1. 添加 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. 添加端口统计
    1
    2
    3
    4
    5
    6
    
    struct PortStats {
     uint64_t rxPackets;
     uint64_t txPackets;
     uint64_t rxBytes;
     uint64_t txBytes;
    };
    
  2. 实现流量控制
    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. 参考资料

  1. ns-3 文档:
  2. 以太网标准:
    • IEEE 802.3 (Ethernet)
    • IEEE 802.1D (Spanning Tree Protocol)
  3. 相关 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