目录

  1. DOCSIS:有线电视网络的链路层协议
  2. 交换局域网概述
  3. MAC地址
  4. 地址解析协议 ARP
  5. 跨子网发送数据报
  6. 以太网
  7. 以太网帧结构详解
  8. 以太网技术演进
  9. C++ 完整代码:ARP表模拟

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 是一个罕见的在单一网络中融合了三类多路访问协议的例子:

DOCSIS 多路访问

FDM
频分复用
下行/上行用不同频率信道

TDM
类时分
上行信道划分为mini时隙

随机访问
请求mini时隙用
随机访问竞争

集中分配
CMTS用MAP消息指定
哪个调制解调器用哪个时隙

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 248281 万亿个可能的 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 工作流程(即插即用,自动建立)

没有

主机C要给IP:222.222.222.222发帧

ARP表中
有该IP的条目?

直接取出MAC地址,封装帧发送

构造ARP请求包

以广播MAC地址FF-FF-FF-FF-FF-FF
发送ARP请求帧
全子网都收到

子网内每台主机检查
IP是否匹配

IP匹配?

丢弃,不响应

主机A单播回复ARP响应
包含自己的MAC地址

主机C收到响应
更新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 以太网的历史地位

以太网是有线局域网领域的绝对霸主,就像互联网之于全球网络一样。
以太网拓扑的演进

1970s-1980s
总线拓扑
共享同轴电缆
广播,有碰撞

1990s
集线器Hub星型
物理星型,逻辑总线
仍有碰撞

2000s至今
交换机Switch星型
无碰撞
全双工

总线拓扑时代

     主机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 协议和帧格式始终不变

应用层

传输层

网络层

链路层
MAC协议 + 帧格式
50年来始终如一!

1000BASE-T
双绞线

1000BASE-SX
短波光纤

1000BASE-LX
长波光纤

1000BASE-LX10
长距离光纤

1000BASE-BX10
单纤双向

1000BASE-CX
铜缆

以太网最不变的东西:帧格式!从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地址总数=2482.8×1014
以太网帧数据字段约束
46 ≤ Data字段长度(字节) ≤ 1500 46 \leq \text{Data字段长度(字节)} \leq 1500 46Data字段长度(字节)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=481 1111

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 自学习过程

三条规则

  1. 初始时,交换表为
  2. 每收到一个帧,就把源MAC地址 + 来自哪个接口 + 当前时间记入表中
  3. 如果某条记录超过老化时间(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 0VLAN ID4095,有效范围 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可以跨越更大地理范围,覆盖不同地点的局域网,形成一个全球性的虚拟局域网

八、知识结构总览

链路层交换机

转发与过滤
Forwarding & Filtering

自学习
Self-Learning

交换机优点

交换机 vs 路由器

VLAN虚拟局域网

交换表
MAC+接口+时间

三种情况
广播/过滤/转发

空表起步

学习源MAC

老化删除

消除碰撞

异构链路

便于管理

Switch: 第2层
基于MAC

Router: 第3层
基于IP

基于端口的VLAN

VLAN间通信
需要路由器

VLAN Trunk
主干链路

802.1Q帧格式
4字节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 2122=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转发:查找最长前缀匹配,慢 目的IP32位)输出接口
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 2201
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 转发的步骤:

  1. 收到帧,读取 MPLS 标签
  2. 查自己的转发表(按标签精确匹配)
  3. 把标签换成新标签,从指定接口发出
    这整个过程不需要看 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,2161600万个虚拟局域网
对比: VLAN ID 12位 ⇒ 4094 个 \text{VLAN ID 12位} \Rightarrow 4094 \text{个} VLAN ID 124094,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 数据中心是什么?

谷歌、微软、亚马逊、阿里巴巴等公司建有巨型数据中心,每个容纳数万到数十万台服务器。
数据中心的三大用途

  1. 内容服务:网页、搜索、AI聊天机器人、流媒体视频
  2. 大规模并行计算:分布式搜索索引、大数据处理
  3. 云计算:为其他公司提供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)而非购买厂商产品。

六、知识结构总览

链路虚拟化与数据中心

MPLS

VXLAN

数据中心网络

帧格式
Label+Exp+S+TTL

标签交换路由器LSR
查标签→换标签→转发

优势

流量工程
多路径

快速故障恢复

VPN实现

解决VLAN局限
4094→1600万

VTEP隧道端点
封装与解封装

封装格式
IP-UDP-VXLAN头-以太网帧

VNI 24位标识符

基本组件
Blade+TOR+Rack

层次化架构
三层拓扑

瓶颈问题
上层链路拥挤

叶脊拓扑
Leaf-Spine

每Leaf连所有Spine

固定2跳
横向扩展

发展趋势

SDN集中控制

虚拟化VM迁移

能效与碳效率

硬件定制化

七、完整 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 的主页!

五、完整流程时序图

Google服务器 64.233.169.105 DNS服务器 68.87.71.226 Comcast网络 学校网关路由器 学校交换机 Bob笔记本 Google服务器 64.233.169.105 DNS服务器 68.87.71.226 Comcast网络 学校网关路由器 学校交换机 Bob笔记本 阶段一:DHCP(步骤1-7) 阶段二:ARP+DNS(步骤8-13) 阶段三:域内路由到DNS(步骤14-17) 阶段四:TCP+HTTP(步骤18-24) 浏览器渲染显示网页! DHCP请求(广播帧) 广播转发 DHCP ACK(IP=68.85.2.101 DNS=68.87.71.226 网关=68.85.2.1) ARP请求(广播)谁是68.85.2.1? 广播转发 ARP回复(MAC=00:22:6B:45:1F:1B) DNS查询(目的IP=68.87.71.226) 转发DNS查询 路由转发 DNS回复(www.google.com=64.233.169.105) 回复到达Bob TCP SYN(目的IP=64.233.169.105) TCP SYNACK TCP ACK(连接建立) HTTP GET / HTTP 200 OK(网页HTML)

六、每个步骤涉及的协议全景

步骤  协议层次                 涉及协议
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
============================================================

十一、整体知识总结

协议

协议

协议

协议

协议

Bob插上网线

DHCP
获取IP地址
步骤1-7

有了IP
知道DNS和网关IP

ARP
查询网关MAC地址
步骤8-11

DNS查询
www.google.com→IP
步骤12-17

TCP三次握手
SYN/SYNACK/ACK
步骤18-20

HTTP GET请求
步骤21

HTTP响应
收到网页HTML
步骤22-24

浏览器渲染
Bob看到网页

DHCP+UDP+IP+Ethernet

ARP+Ethernet

DNS+UDP+IP
OSPF/BGP路由

TCP+IP+Ethernet

HTTP+TCP+IP

十二、本章总结

链路层完整覆盖了以下内容:

主题 核心内容
基本服务 把网络层数据报封装成帧,在相邻节点间传递
差错检测 奇偶校验、校验和、CRC循环冗余校验
多路访问 信道划分(TDM/FDM)、随机访问(ALOHA/CSMA)、轮转(轮询/令牌环)
链路层寻址 MAC地址(48位)、ARP协议(IP↔MAC转换)
以太网 最成功的有线局域网技术
交换机 自学习、转发/过滤、即插即用
VLAN 虚拟局域网,隔离广播域
MPLS 标签交换,流量工程,VPN
VXLAN 跨互联网的虚拟局域网,VNI 24位(1600万)
数据中心网络 层次架构→叶脊拓扑,负载均衡,SDN管控

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐