From 271ab1c1f45669e4c782bb207a7de42a0eabe6ff Mon Sep 17 00:00:00 2001
From: hhs <386998068@qq.com>
Date: Wed, 27 May 2026 23:01:37 +0800
Subject: [PATCH] vault backup: 2026-05-27 23:01:37
---
hhs/NETWORK/02-链路层/04-Switch与路由器.md | 486 +++++++++++++++---
hhs/NETWORK/02-链路层/05-VLAN与Trunk.md | 143 ++++--
.../03-网络层/01-IPv4首部与分段重组.md | 110 +++-
hhs/NETWORK/03-网络层/02-CIDR与子网划分.md | 195 +++++--
.../03-网络层/03-IPv6地址与扩展头部.md | 193 ++++++-
hhs/Redis/02-核心数据类型.md | 99 ++++
hhs/Redis/08-SortedSet精解.md | 82 ++-
7 files changed, 1128 insertions(+), 180 deletions(-)
diff --git a/hhs/NETWORK/02-链路层/04-Switch与路由器.md b/hhs/NETWORK/02-链路层/04-Switch与路由器.md
index ca34b40..2b896ed 100644
--- a/hhs/NETWORK/02-链路层/04-Switch与路由器.md
+++ b/hhs/NETWORK/02-链路层/04-Switch与路由器.md
@@ -1,5 +1,5 @@
---
-tags: [计算机网络, Switch, Hub, Router, VLAN]
+tags: [计算机网络, Switch, Hub, Router, VLAN, STP, RSTP, EtherChannel, L3Switch, MAC, CEF, ARP, IGMP, DAI, BPDU]
create time: 2026-05-17 23:20
---
@@ -14,7 +14,7 @@ create time: 2026-05-17 23:20
| OSI 层级 | 物理层 L1 | 数据链路层 L2 | 网络层 L3 |
| 寻址依据 | 无(全量复制) | MAC 地址 | IP 地址 + 路由表 |
| 冲突域 | 全部共享 | 每个端口独立 | 每个接口独立 |
-| 广播域 | 不隔离 | 不隔离(同 VLAN) | **天然隔离** |
+| 广播域 | 全部共享(一个广播域) | 不隔离(同 VLAN 内) | **天然隔离** |
| 性能 | 极低(半双工) | 高(全双工并行) | 依赖 CPU/ASIC |
| 现代使用 | ❌ 已淘汰 | ✅ 局域网核心 | ✅ 网关/边界 |
@@ -26,10 +26,10 @@ Hub 本质上就是一个多端口的中继器(Repeater)。收到任何端
```mermaid
flowchart LR
- A["PC-A 发送"] -->|"信号"| H[Hub]
- H -->|"原样复制"| B["PC-B (收到!)"]
- H -->|"原样复制"| C["PC-C (碰撞! 🔥)"]
-
+ A["PC-A 发送"] -->|"信号"| H["Hub"]
+ H -->|"原样复制"| B["PC-B 收到"]
+ H -->|"原样复制"| C["PC-C 碰撞"]
+
style H fill:#FF6B6B,color:#fff
```
@@ -40,54 +40,94 @@ flowchart LR
3. **无法隔离故障**:一台 PC 中毒疯狂发包会影响所有人
4. **安全性差**:所有流量对所有端口可见
+> [!question] 思考
+> Hub 已经被淘汰了,为什么我们还要了解它?因为 **Switch 的泛洪行为本质上就是退化成了 Hub**——理解 Hub 的缺陷,才能理解后面 CAM 表攻击的危害。
+
+### 冲突域 vs 广播域
+
+这两个概念贯穿整个 L2 学习,必须清晰区分:
+
+```mermaid
+flowchart TD
+ subgraph "冲突域 Collision Domain"
+ CD1["同一时刻只能有一方发送, 否则碰撞"]
+ end
+
+ subgraph "广播域 Broadcast Domain"
+ BD1["一条广播帧能到达的所有设备集合"]
+ end
+
+ Hub["Hub: 所有端口 = 1 个冲突域, 1 个广播域"]
+ SW["Switch: 每端口 = 1 个冲突域, 同 VLAN = 1 个广播域"]
+ RT["Router: 每接口 = 既隔离冲突域, 也隔离广播域"]
+
+ style CD1 fill:#FF6B6B,color:#fff
+ style BD1 fill:#87CEEB,color:#000
+```
+
+> [!question] 思考
+> 交换机能隔离冲突域,但不能隔离广播域(VLAN 内)——那广播风暴的防护靠什么?答案是 **VLAN 分割广播域** + **STP 防环**,这也是后面两篇笔记的核心主题。
+
## Switch(交换机)——现代 LAN 核心
### 工作原理
-Switch 为每个端口维护一张独立的 MAC 表,只将帧转发到目标端口,实现"点对点"通信。
+Switch 维护一张全局 MAC 表(CAM 表),记录"哪个 MAC 地址在哪个端口"的映射关系。收到帧时查表,只将帧转发到目标端口,实现"点对点"通信。
```mermaid
flowchart TD
- S["Switch
MAC 表: mac_A→Port1, mac_B→Port2"]
-
- PC_A["PC-A → Port1"] -->|"发给 Mac_B"| S
- S -->|"仅从 Port2 发出"| PC_B["PC-B"]
-
- PC_D["PC-D → Port4"] -->|"未知目标"| S
- S -->|"泛洪到 Port1,2,3"| N["Port1~3 都收到
只有正确的会处理"]
-
+ S["Switch MAC表 mac_A→Port1, mac_B→Port2"]
+
+ A["PC-A Port1"] -->|"发给 mac_B"| S
+ S -->|"仅从 Port2 发出"| B["PC-B"]
+
+ D["PC-D Port4"] -->|"未知目标"| S
+ S -->|"泛洪到 Port1,2,3"| N["Port1到3都收到, 只有正确的会处理"]
+
style S fill:#98FB98,color:#000
```
+> [!question] 思考
+> Switch 的泛洪和 Hub 的复制有什么区别?Hub 是**无差别地复制所有帧**,而 Switch 只在 **MAC 表中找不到目标时**才泛洪。一旦学到地址,就走精确转发——这就是"智能"的地方。
+
### MAC 表学习流程
```mermaid
sequenceDiagram
participant SW as Switch
- participant A as PC-A (mac_A)
- participant B as PC-B (mac_B)
-
- A->>SW: Frame(mac_A→mac_B) from Port1
- SW->>SW: Learn: mac_A on Port1
- SW->>B: Flood (mac_B unknown)
-
- B->>SW: Reply Frame(mac_B→mac_A) from Port2
- SW->>SW: Learn: mac_B on Port2
-
- A->>SW: Next Frame(mac_A→mac_B) from Port1
- SW->>SW: Forward directly → Port2 only ✅
+ participant A as PC-A mac_A
+ participant B as PC-B mac_B
+
+ A->>SW: Frame mac_A→mac_B from Port1
+ SW->>SW: Learn mac_A on Port1
+ SW->>B: Flood mac_B unknown
+
+ B->>SW: Reply Frame mac_B→mac_A from Port2
+ SW->>SW: Learn mac_B on Port2
+
+ A->>SW: Next Frame mac_A→mac_B from Port1
+ SW->>SW: Forward directly to Port2
```
+> [!info] MAC 地址表的老化
+> 交换机的 CAM 表条目不是永久的。如果某个 MAC 地址在**老化时间**(默认通常 300 秒 / 5 分钟)内没有再出现,该条目会被自动清除。这保证了表项不会被已离线设备永久占据,但也是 MAC 泛洪攻击能生效的前提——攻击者必须持续发送伪造帧来维持表满状态。
+
### Switch 的关键优势
| 特性 | 说明 |
|------|------|
| **每个端口一个冲突域** | 端口间互不影响 |
| **全双工通信** | 收发光纤分开,可同时对发对收 |
-| **硬件转发** | ASIC 芯片查 MAC 表,延迟 < 1ms |
+| **硬件转发** | ASIC 芯片查 MAC 表,延迟 < 1μs |
| **背板带宽** | 交换机内部总线,决定交换能力上限 |
| **VLAN 支持** | 802.1Q 实现逻辑分割 |
+> [!tip] 关于全双工
+> 全双工 = 收发同时进行,物理上收发走不同线对(或波长)。这意味着**冲突域的概念在全双工下已不存在**——不需要 CSMA/CD。
+
+> [!info] IGMP Snooping:多播流量的守护者
+> 交换机默认会把多播帧像广播一样泛洪到所有端口。**IGMP Snooping** 让交换机监听 IGMP 报文,学习哪些端口有多播组成员,只将多播流量转发到有接收者的端口。典型场景:IPTV 组播、视频会议。没有 IGMP Snooping 的交换机在多播场景下会严重浪费带宽。
+
### 常见 Switch 规格参数
| 参数 | 说明 | 典型值 |
@@ -98,6 +138,222 @@ sequenceDiagram
| 包转发率 | PPS (packets per second) | 10G 端口满速 ≈ 14.88M PPS |
| 缓存 | SRAM 缓冲丢包 | 几 MB ~ 几百 MB |
+> [!question] 思考
+> 10G 端口满速为什么恰好是 **14.88M PPS**?因为最小以太网帧 64 字节 + 8 字节前导码 + 12 字节帧间距 = 84 字节,10Gbps / (84×8 bits) ≈ 14.88M。记住这个数字,面试常考。
+
+### 交换模式:帧怎么被转发的?
+
+交换机在转发帧时,有三种读取策略,它们在**延迟**和**可靠性**之间做出不同的取舍:
+
+| 模式 | 行为 | 延迟 | 错误检测 | 适用场景 |
+|------|------|------|---------|---------|
+| **Cut-through** 直通转发 | 读完目标 MAC 就立刻开始转发 | 最低 | ❌ 不校验 | 对延迟极端敏感的高频交易 |
+| **Store-and-Forward** 存储转发 | 完整接收帧 → CRC 校验 → 无误再转发 | 较高 | ✅ 完整校验 | **现代交换机默认模式** |
+| **Fragment-Free** 无碎片转发 | 读取前 64 字节后转发(过滤冲突碎片) | 中等 | ⚠️ 部分 | 早期 Catalyst 交换机 |
+
+> [!info] 为什么 Fragment-Free 检查恰好 64 字节?
+> 以太网规定最小帧长 64 字节。**冲突产生的碎片(runt frame)一定小于 64 字节**——如果一个帧在传输前 64 字节期间没发生冲突,后续碰撞的概率极低。Fragment-Free 通过检查前 64 字节,用较低代价过滤掉了绝大部分冲突碎片,是 Cut-through 和 Store-and-Forward 之间的折中方案。
+
+> [!tip] 为什么 Store-and-Forward 成为主流?
+> 早期 ASIC 算力不够,Cut-through 有延迟优势。但现代 ASIC 做 CRC 校验几乎零开销,Store-and-Forward 的可靠性优势(能丢弃损坏帧、避免错误帧浪费带宽)远大于那点微秒级延迟差异。
+
+### STP:交换机环路的终结者
+
+当交换机之间存在冗余链路时(高可用设计的标配),帧可能在环路中无限循环:
+
+```mermaid
+flowchart LR
+ SW1["SW1 根桥"] -->|"转发"| SW2["SW2"]
+ SW1 -->|"转发"| SW3["SW3"]
+ SW2 -->|"广播帧循环"| SW3
+ SW3 -->|"广播帧循环"| SW2
+
+ style SW1 fill:#98FB98,color:#000
+ style SW2 fill:#FFD700,color:#000
+ style SW3 fill:#FFD700,color:#000
+```
+
+**环路会导致三大灾难:**
+
+1. **广播风暴**:广播帧被无限复制,瞬间耗尽带宽
+2. **MAC 表抖动**:同一帧从不同端口到达,交换机不断更新 MAC 表导致转发表不稳定
+3. **重复帧接收**:终端收到多份相同帧,协议栈混乱
+
+**STP(Spanning Tree Protocol, 802.1D)的解决思路:** 通过选举算法,逻辑上"阻塞"某些端口,让拓扑变成一棵无环的树。
+
+```mermaid
+flowchart TD
+ SW1["SW1 根桥 Root Bridge"] -->|"DP"| SW2["SW2"]
+ SW1 -->|"DP"| SW3["SW3"]
+ SW2 -->|"DP"| SW3
+ SW3 -.->|"BLK 阻塞"| SW2
+
+ style SW1 fill:#98FB98,color:#000
+ style SW2 fill:#DDA0DD,color:#000
+ style SW3 fill:#FFD700,color:#000
+```
+
+> [!info] 图中端口角色
+> - **DP(Designated Port)**:每个网段上负责转发的端口,由到根桥路径开销最小的交换机拥有。**根桥的所有端口都是 DP**(图中 SW1 的三个端口、SW2→SW3 链路上 SW2 的端口均为 DP)。
+> - **RP(Root Port)**:每个非根桥上有且仅有一个到达根桥的最优端口。图中 **SW2 面向 SW1 的端口** 和 **SW3 面向 SW1 的端口** 就是各自 RP。比较规则:路径开销最小 → 对端 Bridge ID 最小 → 对端 Port ID 最小。
+> - **BLK(Blocked)**:逻辑阻塞,不转发数据帧,仅接收 BPDU 以防拓扑变化。图中 **SW3 面向 SW2 的端口被阻塞**,从而打破环路。
+
+STP 选举规则:
+
+1. **选根桥**:Bridge ID(优先级 + MAC 地址)最小的交换机
+2. **选根端口(RP)**:每个非根桥选一个到根桥"最优路径"的端口(比较路径开销 → 对端 Bridge ID → 对端 Port ID)
+3. **选指定端口(DP)**:每个网段选一个负责转发的端口(通常是离根桥更近的一方)
+4. **阻塞其余端口**:不转发数据帧,只接收 BPDU
+
+> [!tip] STP vs RSTP
+> 经典 STP 收敛需要 30~50 秒(Blocking → Listening → Learning → Forwarding),这在现代网络中不可接受。**RSTP(802.1w)** 将收敛时间缩短到**秒级**,是当前生产环境的标准选择。
+
+### STP 安全增强机制
+
+STP 本身没有认证,恶意设备可以伪造 BPDU 抢占根桥或制造环路。以下三个机制是生产环境必备:
+
+| 机制 | 作用 | 应用位置 |
+|------|------|---------|
+| **BPDU Guard** | 如果接收到 BPDU,立即 shutdown 端口 | 连接终端的 Access 端口(防止用户私接交换机) |
+| **Root Guard** | 如果收到更优的 BPDU,将端口置为 root-inconsistent | 非信任方向端口(防止非法设备抢占根桥) |
+| **Loop Guard** | 如果单向链路故障导致收不到 BPDU,将端口置为 loop-inconsistent | RP 和 Alternate 端口(防止单向故障引起环路) |
+
+> [!question] 思考
+> BPDU Guard 和 Root Guard 看起来很像,区别在哪?**BPDU Guard 是"绝不允许 BPDU 出现"**——一旦收到就关停,适用于接入层终端端口。**Root Guard 是"允许 BPDU 但不允许更优的"**——保护根桥地位不被篡改,适用于汇聚/核心层非信任方向端口。
+
+### EtherChannel:STP 的补丁
+
+STP 阻塞冗余端口来防环,但代价是**浪费了那条链路的带宽**。EtherChannel(LACP/PAgP)将多条物理链路捆绑成一条逻辑链路,解决这个问题:
+
+```mermaid
+flowchart LR
+ SW1["SW1"] -->|"物理链路 x4 捆绑为 1 条逻辑链路"| SW2["SW2"]
+ Note1["STP 只看到 1 条逻辑链路, 不会阻塞"] -.-> SW1
+```
+
+| 特性 | 说明 |
+|------|------|
+| **带宽叠加** | 4 条 1G 链路 = 1 条 4G 逻辑链路 |
+| **STP 透明** | STP 将捆绑组视为单条链路,不阻塞任何成员 |
+| **故障切换** | 某条物理链路断开,流量自动分配到剩余链路 |
+| **协商协议** | LACP(802.3ad,开放标准)/ PAgP(Cisco 私有) |
+
+> [!info] 负载均衡哈希算法
+> EtherChannel 的"负载均衡"并非逐包轮询,而是**基于流(per-flow)哈希**——同一对源/目的通信始终走同一条物理链路,保证帧顺序不乱。哈希输入字段的选择直接影响均衡效果:
+>
+> | 哈希模式 | 哈希字段 | 适用场景 |
+> |---------|---------|---------|
+> | `src-mac` | 源 MAC | 服务器下行(多客户端 → 一服务器) |
+> | `dst-mac` | 目的 MAC | 多服务器场景 |
+> | `src-dst-ip`(推荐) | 源 + 目的 IP | 大多数场景,均衡效果最佳 |
+> | `src-dst-mac` | 源 + 目的 MAC | 二层环境 |
+>
+> ```bash
+> # Cisco 配置哈希算法
+> Switch(config)# port-channel load-balance src-dst-ip
+> ```
+
+> [!question] 思考
+> 既然 EtherChannel 这么好,为什么不把所有链路都捆绑?因为捆绑的成员链路必须连接相同的两端设备、相同速率/双工,且有数量上限(通常 8 条)。更重要的是——基于流的哈希意味着**单条流仍然只能走一条物理链路**,不能突破单链路速率上限。如果只有少数大流量流,可能出现"看似 4G 实际只用 1G"的不均衡。
+
+### CAM 表安全:当 Switch 退化成 Hub
+
+交换机的 CAM 表(MAC 表)容量是有限的,通常为数千到数万条。攻击者可以利用这一点:
+
+**MAC 泛洪攻击原理:**
+
+```mermaid
+sequenceDiagram
+ participant ATK as Attacker
+ participant SW as Switch CAM 8192 entries
+ participant V as Victim
+
+ ATK->>SW: 伪造 10万+ 个随机源 MAC 帧
+ SW->>SW: CAM 表溢出
+ Note over SW: 无法学习新 MAC, 退化为 Hub 模式, 所有未知流量泛洪到所有端口
+ V->>SW: 正常通信帧
+ SW->>ATK: 泄漏, 帧被泛洪到攻击者端口
+```
+
+**防御措施:**
+
+| 措施 | 原理 |
+|------|------|
+| **端口安全 (Port Security)** | 限制每个端口学习的最大 MAC 数,超限则触发违规动作(见下表) |
+| **DHCP Snooping** | 构建 IP-MAC 绑定表,过滤非法 DHCP 响应 |
+| **Dynamic ARP Inspection (DAI)** | 基于 DHCP Snooping 表校验 ARP 报文的合法性 |
+| **802.1X 认证** | 端口级准入控制,未认证设备无法通信 |
+
+> [!info] 端口安全的三种违规模式
+>
+> | 模式 | 超限行为 | 是否发送 SNMP Trap | 是否递增 Violation 计数 | 端口状态 |
+> |------|---------|-------------------|----------------------|---------|
+> | **Protect** | 静默丢弃违规帧 | ❌ | ❌ | 保持 UP(最容易被忽视) |
+> | **Restrict** | 丢弃违规帧 | ✅ | ✅ | 保持 UP |
+> | **Shutdown**(默认) | 丢弃违规帧 | ✅ | ✅ | **err-disabled**,需手动 `shutdown` / `no shutdown` 恢复 |
+>
+> 生产环境推荐 **Shutdown** 模式——故障暴露最明显,能迫使运维介入排查。如果担心影响可用性,可配合 `errdisable recovery` 自动恢复。
+
+> [!question] 思考
+> 为什么 MAC 泛洪攻击能让 Switch 退化成 Hub?因为 CAM 表满后,交换机无法学习新的 MAC-端口 映射关系,对于所有"未知目标"的帧只能泛洪——这恰恰就是 Hub 的行为。**理解 Hub 的工作方式,才能理解这个攻击的本质。**
+
+### ARP 欺骗:另一种 L2 攻击
+
+MAC 泛洪是让交换机"退化",而 ARP 欺骗则是"欺骗"——攻击者直接篡改 ARP 映射关系,将流量劫持到自己手中。
+
+**ARP 欺骗原理:**
+
+```mermaid
+sequenceDiagram
+ participant ATK as Attacker
+ participant GW as Gateway 192.168.1.1
+ participant V as Victim 192.168.1.10
+
+ ATK->>GW: 伪造 ARP Reply: 192.168.1.10 is at MAC_ATK
+ GW->>GW: ARP 表被污染, 192.168.1.10 → MAC_ATK
+
+ ATK->>V: 伪造 ARP Reply: 192.168.1.1 is at MAC_ATK
+ V->>V: ARP 表被污染, 网关 → MAC_ATK
+
+ Note over V,GW: 所有流量经过 Attacker, 实现中间人攻击
+```
+
+> [!info] 为什么 ARP 协议如此脆弱?
+> ARP 协议设计之初没有认证机制——**任何设备都可以主动发送 ARP Reply(免费 ARP),接收方不会验证其真实性**。在没有防护的网络中,攻击者可以随意声称"我就是网关"。这也是为什么 ARP 欺骗至今仍是内网渗透中最常用的手法之一。
+
+**ARP 欺骗 vs MAC 泛洪对比:**
+
+| 维度 | MAC 泛洪 | ARP 欺骗 |
+|------|---------|---------|
+| 攻击目标 | 交换机 CAM 表 | 终端 ARP 缓存 |
+| 攻击效果 | Switch 退化成 Hub(泛洪) | 流量劫持到攻击者(精准定向) |
+| 流量截获 | 被动嗅探泛洪流量 | 主动中间人,可篡改/重放 |
+| 防御措施 | Port Security | **DAI + DHCP Snooping**(核心组合) |
+
+> [!tip] DAI + DHCP Snooping 的防御逻辑
+> **DHCP Snooping** 在交换机上建立一张"IP-MAC-端口"可信绑定表(信任表)。**DAI(Dynamic ARP Inspection)** 则拦截所有 ARP 报文,对照这张绑定表校验:如果 ARP 声称的 IP-MAC 映射不在表中,直接丢弃。两道防线配合,几乎可以完全封堵 ARP 欺骗。详见 [[hhs/NETWORK/08-网络安全/01-DDoS与MITM防御]]。
+
+### 风暴控制:流量的最后一道闸门
+
+MAC 泛洪攻击针对的是 CAM 表容量,而**广播风暴**针对的是带宽——当广播/多播帧因环路或恶意行为失控时,网络瞬间瘫痪。STP 防环是根本方案,但 **Storm Control** 是每个端口上的最后一道保险:
+
+| 参数 | 说明 |
+|------|------|
+| **监控对象** | 广播帧、多播帧、未知单播帧(可分别配置) |
+| **阈值方式** | 带宽百分比 / 帧速率(pps) / 绝对速率(bps) |
+| **超限动作** | 阻塞(shutdown)/ 仅丢弃超限帧(drop)/ 发送 SNMP Trap 告警 |
+
+```bash
+# Cisco IOS 配置示例
+interface GigabitEthernet0/1
+ storm-control broadcast level 20 # 广播流量超过带宽 20% 时触发
+ storm-control multicast level pps 1000 # 多播超过 1000 pps 时触发
+ storm-control action shutdown # 超限直接 shutdown 端口
+```
+
+> [!question] 思考
+> STP 已经防环了,为什么还需要 Storm Control?因为 STP 只防"环路导致的"广播风暴,**无法防护恶意终端主动发送大量广播帧**。两者是不同层级的防护:STP 是拓扑级(防环),Storm Control 是端口级(限速),在生产环境中应同时启用。
+
## Router(路由器)——跨网段互联
### 工作原理
@@ -106,23 +362,23 @@ Router 运行在 OSI 第三层,根据 **IP 路由表**做出转发决策,不
```mermaid
flowchart TD
- subgraph "LAN 1 — 192.168.1.0/24"
+ subgraph "LAN 1 - 192.168.1.0/24"
A["PC-A 192.168.1.10"]
S["Local Switch"]
end
-
+
subgraph "Router Gateway"
- R["Router eth0: .1 / eth1: .1"]
+ R["Router eth0 .1, eth1 .1"]
end
-
- subgraph "LAN 2 — 192.168.2.0/24"
+
+ subgraph "LAN 2 - 192.168.2.0/24"
B["PC-B 192.168.2.10"]
S2["Local Switch"]
end
-
+
A --> S --> R
R --> S2 --> B
-
+
style A fill:#DDA0DD,color:#000
style B fill:#DDA0DD,color:#000
style R fill:#98FB98,color:#000
@@ -137,45 +393,159 @@ sequenceDiagram
participant A as PC-A
participant R as Router
participant B as PC-B
-
- Note over A: 判断:192.168.2.10 不在本地子网?
→ 走默认网关
- A->>R: Frame{SrcMac=A, DstMac=Gateway}
IP(Src=192.168.1.10,
Dst=192.168.2.10)
- Note over R: Step 1: Strip Ethernet Header
Step 2: 校验 IP Checksum
Step 3: 查路由表 → 下一跳 via eth1
Step 4: ARP 找 B 的 MAC
Step 5: Rebuild Ethernet Header
- R->>B: Frame{SrcMac=Gateway, DstMac=B}
IP(Src=192.168.1.10,
Dst=192.168.2.10)
- Note over R: ← MAC 改了 ← IP 没变
+
+ Note over A: 判断 192.168.2.10 不在本地子网, 走默认网关
+ A->>R: Frame SrcMAC=A, DstMAC=Gateway, IP Src=192.168.1.10, Dst=192.168.2.10
+ Note over R: Step1 Strip Ethernet Header, Step2 校验 IP Checksum, Step3 TTL -1, 若 TTL=0 则丢弃并回 ICMP Time Exceeded, Step4 查路由表 via eth1, Step5 ARP 找 B 的 MAC, Step6 Rebuild Ethernet Header
+ R->>B: Frame SrcMAC=Gateway, DstMAC=B, IP Src=192.168.1.10, Dst=192.168.2.10
+ Note over R: MAC 改了, IP 没变
```
+> [!info] TTL 的作用:防止环路中的无限循环
+> 路由器每转发一个包,TTL 减 1。当 TTL 降为 0 时,路由器**丢弃该包并向源 IP 发送 ICMP Time Exceeded 消息**——这就是 `traceroute` 的工作原理。每一跳路由器都会返回一条 ICMP 消息,从而揭示完整路径。
+
### 关键规则:IP 不变,MAC 逐跳改
```mermaid
flowchart LR
- A["原始帧
SrcMAC=A DstMAC=R1-E0
SrcIP=A DstIP=B"] -->|"通过 Router1"| M1["帧改写
SrcMAC=R1-E1 DstMAC=R2-E0
SrcIP=A DstIP=B ← 不变!"]
- M1 -->|"通过 Router2"| M2["帧改写
SrcMAC=R2-E1 DstMAC=B
SrcIP=A DstIP=B"]
-
+ A["原始帧 SrcMAC=A, DstMAC=R1-E0, SrcIP=A, DstIP=B"] -->|"通过 Router1"| M1["帧改写 SrcMAC=R1-E1, DstMAC=R2-E0, IP不变"]
+ M1 -->|"通过 Router2"| M2["帧改写 SrcMAC=R2-E1, DstMAC=B, IP不变"]
+
style A fill:#DDA0DD,color:#000
style M2 fill:#98FB98,color:#000
```
-> [!tip] 一句话记住区别
-> **Switch 看 MAC 转发,Router 看 IP 转发。** Switch 是"同城快递",Router 是"跨省物流"。
+> [!question] 思考
+> 为什么要这样设计?因为 **IP 地址是端到端的逻辑标识**(告诉你"最终目的地"),而 **MAC 地址是逐跳的物理标识**(告诉你"下一跳该发给谁")。就像寄快递:收件人地址(IP)全程不变,但每个中转站的转运标签(MAC)都会换。
+
+### Router 的路由功能
+
+路由器的核心能力是**路由**——根据路由表决定数据包的去向。路由来源分三种:
+
+| 路由类型 | 说明 | 配置方式 |
+|---------|------|---------|
+| **直连路由** | 接口 UP 后自动学习 | 无需配置 |
+| **静态路由** | 管理员手动指定 | `ip route 10.0.0.0/8 192.168.1.1` |
+| **动态路由** | 协议自动交换(OSPF/BGP) | 启动路由协议进程 |
+
+```bash
+# Linux 查看路由表
+$ ip route show
+default via 192.168.1.1 dev eth0
+192.168.1.0/24 dev eth0 proto kernel scope link src 192.168.1.10
+10.0.0.0/8 via 192.168.1.254 dev eth0 # 静态路由
+
+# Cisco IOS 查看路由表
+Router# show ip route
+S 10.0.0.0/8 [1/0] via 192.168.1.254 # S = Static
+C 192.168.1.0/24 is directly connected # C = Connected
+```
+
+> [!tip] 路由表中的"最长前缀匹配"
+> 当多条路由都匹配目标 IP 时,路由器选择**掩码最长**的那条。例如同时有 `10.0.0.0/8` 和 `10.1.0.0/16`,发往 `10.1.2.3` 的包会走 `/16` 这条——越精确的路由越优先。
+
+> [!info] 管理距离(AD)和度量值(Metric)
+> 当同一条路由从不同来源学到时(如 OSPF 和 RIP 都宣告了 `10.0.0.0/16`),路由器用两个维度做选择:
+> 1. **管理距离 AD(Administrative Distance)**:衡量路由来源的可信度。直连 = 0,静态 = 1,OSPF = 110,RIP = 120——**AD 越小越优先**。
+> 2. **度量值 Metric**:同一协议内比较路径优劣。OSPF 看带宽(Cost),RIP 看跳数——**Metric 越小越优先**。
+>
+> 选择顺序:**最长前缀匹配 → AD 最小 → Metric 最小**。这个三步判定是路由选路的核心逻辑,面试和排障都会用到。
+
+### Router 的其他关键功能
+
+路由转发只是路由器的一部分能力,现代路由器实际上身兼数职:
+
+| 功能 | 说明 | 典型场景 |
+|------|------|---------|
+| **NAT** | 将私有 IP 转换为公有 IP | 家庭路由器、企业出口 |
+| **ACL** | 基于五元组的包过滤 | 禁止外部访问内网特定端口 |
+| **QoS** | 流量优先级调度 | VoIP 优先于普通数据 |
+| **DHCP Server** | 自动分配 IP 地址 | 小型网络无需独立 DHCP 服务器 |
+| **VPN** | 加密隧道 | 远程办公接入企业内网 |
+
+> [!question] 思考
+> Switch 和 Router 的功能边界正在模糊——**三层交换机**可以做 VLAN 间路由,**路由器**也能做交换。在你的网络架构中,什么场景用纯 L2 Switch、什么场景用 L3 Switch、什么场景用 Router?带着这个问题继续学习后面的 VLAN 和路由章节。
+
+### L3 交换机:Switch 与 Router 的融合
+
+传统的 L2 Switch 只看 MAC 转发,跨 VLAN 通信必须绕道路由器——这在大型企业网中成为性能瓶颈。**L3 交换机**将路由功能集成到交换机的 ASIC 硬件中,实现"线速路由":
+
+```mermaid
+flowchart TD
+ subgraph "传统方案 Router-on-a-Stick"
+ VA1["Host-A VLAN10"] -->|"所有跨VLAN流量上行"| TR["Trunk 链路"]
+ TR --> R["Router 子接口"]
+ R -->|"回传下行"| TR2["Trunk 链路"]
+ TR2 --> VB1["Host-B VLAN20"]
+ end
+
+ subgraph "L3 交换机方案"
+ VA2["Host-A VLAN10"] --> L3["L3 Switch 内部 SVI 路由"]
+ L3 --> VB2["Host-B VLAN20"]
+ end
+```
+
+| 对比 | Router-on-a-Stick | L3 交换机 |
+|------|-------------------|-----------|
+| 转发方式 | CPU 软件路由 | ASIC 硬件路由 |
+| 性能 | 受路由器 CPU 限制 | 线速转发 |
+| 拓扑 | 需要 Trunk 到路由器 | 内部完成,无需外部设备 |
+| 适用场景 | 小型网络 | 中大型企业网核心 |
+
+> [!tip] SVI(交换虚拟接口)
+> L3 交换机通过 **SVI** 实现 VLAN 间路由——为每个 VLAN 创建一个虚拟三层接口(如 `interface Vlan10`,配置 IP `192.168.10.1`),充当该 VLAN 的网关。数据包在交换机内部直接完成路由,无需离开设备。
+
+> [!info] L3 交换机为什么比路由器快?
+> 关键在于转发路径不同:
+> - **传统路由器**:收到包 → CPU 查路由表 → CPU 执行 ARP → CPU 封装新帧 → 发出。每一步都经过 CPU,性能受主频限制。
+> - **L3 交换机**:预先从路由表构建硬件转发表(**无需等待第一个包触发**),**所有数据包由 ASIC 直接在硬件层面完成查表 + 改写 MAC + 转发**,完全绕过 CPU。这个通用理念称为"**一次路由,多次交换**"。Cisco 的实现叫 **CEF(Cisco Express Forwarding)**,基于 **FIB(Forwarding Information Base)** + **Adjacency Table** 双表架构——FIB 由路由表预计算而来,存储目标前缀 → 下一跳映射;Adjacency Table 存储每一跳对应的 L2 封装头(源/目的 MAC),两者配合实现"查一次表,改两处数据,直接发出"。华为对应叫 **快速转发**,同样基于 FIB 硬件表。核心思路一致:把软件查表的结果预计算到硬件表中,实现线速转发。
+>
+> 打个比方:路由器像一个每次都走审批流程的公司,L3 交换机像一个把常规流程固化成自动化的公司——第一次走流程建模板,之后同类请求直接自动执行。
+
+> [!warning] FIB vs LFIB 易混淆
+> **FIB** 用于标准 IP 转发(本节讨论的场景);**LFIB(Label FIB)** 是 MPLS 标签转发的概念,两者不要混淆。
## 三者的协作关系
现实网络中,三者通常协同工作:
+```mermaid
+flowchart TD
+ PC["终端 PC"] --> H["Hub 旧时代遗留, 冲突域大, 已淘汰"]
+ PC --> SW["Switch 局域网核心, MAC 转发表驱动, 全双工"]
+ SW --> RT["Router 网关, IP 路由表驱动, 隔离广播域"]
+ RT --> ISP["ISP Router 接入 Internet"]
+ PC --> WAP["Wireless AP 接入 Switch, AP 本身不转发"]
+
+ style H fill:#FF6B6B,color:#fff
+ style SW fill:#98FB98,color:#000
+ style RT fill:#DDA0DD,color:#000
+ style ISP fill:#87CEEB,color:#000
```
-PC ──┬── Hub(旧时代遗留) ← 冲突域大,已淘汰
- ├── Switch(局域网核心) ← MAC 转发表驱动,全双工
- │ │
- │ └── Router(网关) ← IP 路由表驱动,隔离广播域
- │ │
- │ └── ISP Router → Internet
- │
- └── Wireless AP → Switch (AP 本身不转发)
-```
+
+## 排障速查
+
+| 操作 | Switch | Router | Linux 对等命令 |
+|------|--------|--------|---------------|
+| 查看 MAC 表 | `show mac-address-table` | — | `bridge fdb show` |
+| 查看路由表 | — | `show ip route` | `ip route show` |
+| 查看 ARP 表 | `show arp` | `show arp` | `ip neigh show` |
+| 查看接口状态 | `show interfaces status` | `show ip interface brief` | `ip link show` |
+| 端口安全配置 | `switchport port-security maximum 2` | — | 无直接对等命令(可用 `ebtables` / `tc` 限制) |
+
+> [!tip] Linux 网络排障组合拳
+> 快速定位 L2/L3 问题的三板斧:
+> ```bash
+> ip link show # L1/L2: 接口是否 UP?速率/双工?
+> ip neigh show # L2: ARP 解析是否正常?MAC 是否正确?
+> ip route get <目标IP> # L3: 走哪条路由?下一跳是谁?
+> ```
## 关联笔记
-- [[hhs/NETWORK/MAC地址与广播域]] — Switch 如何管理 MAC 表和广播
-- [[hhs/NETWORK/VLAN与Trunk]] — 三层交换机如何实现 VLAN 间路由
-- [[hhs/NETWORK/静态路由与默认网关]] — Router 的路由表配置
+- [[hhs/NETWORK/02-链路层/03-MAC地址与广播域]] — Switch 如何管理 MAC 表和广播
+- [[hhs/NETWORK/02-链路层/05-VLAN与Trunk]] — 三层交换机如何实现 VLAN 间路由
+- [[hhs/NETWORK/03-网络层/04-ARP协议完整流程]] — ARP 如何解析 MAC 地址
+- [[hhs/NETWORK/03-网络层/06-静态路由与默认网关]] — Router 的路由表配置
+- [[hhs/NETWORK/03-网络层/08-NAT原理与应用]] — Router 的 NAT 功能详解
+- [[hhs/NETWORK/08-网络安全/01-DDoS与MITM防御]] — MAC 泛洪等 L2 攻击防御
diff --git a/hhs/NETWORK/02-链路层/05-VLAN与Trunk.md b/hhs/NETWORK/02-链路层/05-VLAN与Trunk.md
index f5d53f0..656cc8e 100644
--- a/hhs/NETWORK/02-链路层/05-VLAN与Trunk.md
+++ b/hhs/NETWORK/02-链路层/05-VLAN与Trunk.md
@@ -29,41 +29,48 @@ VLAN(Virtual Local Area Network,虚拟局域网)允许在一台物理交
### 带 802.1Q Tag 的帧
```
-┌──────────┬──────────┬──────┬───┬──────┬─────────────┬──────┐
-│ Dest MAC │ Src MAC │Type │TPID │ TCI │ Payload │ FCS │
-│ 6 bytes │ 6 bytes │ 2B │2B │ 2B │ 46-1500 B │ 4 B │
-│ │ │0x0800│8100 │ VID │ │ │
-└──────────┴──────────┴──────┴───┴──────┴─────────────┴──────┘
- ←── 14 ──→ │←──4──→│ ←──MTU──→ ←── 4 ──→
- ↑ 新增 4-byte Tag
+┌──────────┬──────────┬──────┬──────┬──────────┬─────────────┬──────┐
+│ Dest MAC │ Src MAC │ TPID │ TCI │Org.EType │ Payload │ FCS │
+│ 6 bytes │ 6 bytes │ 2B │ 2B │ 2B │ 46-1500 B │ 4 B │
+│ │ │0x8100│ VID │ 0x0800 │ │ │
+└──────────┴──────────┴──────┴──────┴──────────┴─────────────┴──────┘
+ ←── 14 ──→ │←4 Tag→│ ←───── 4+MTU ─────→ ←── 4 ──→
+ ↑ 新增 4-byte Tag
```
+> [!important] TPID 替换了 EtherType
+> TPID(0x8100)**占据**了原帧中 EtherType 的位置,原 EtherType(如 0x0800)被后移到 TCI 之后、Payload 之前。交换机通过识别 TPID=0x8100 来判断这是一帧 802.1Q 标记帧。
+
### TCI (Tag Control Information) 位图
```
-Bit 15 Bit 13-12 Bit 11-0
-────────────────────────────────────
-CFI | PCP (优先级) | VID (VLAN ID)
-(1bit) | (3 bits) | (12 bits)
+Bit 15-13 Bit 12 Bit 11-0
+─────────────────────────────────────────
+PCP (优先级) | DEI/CFI | VID (VLAN ID)
+ (3 bits) | (1 bit) | (12 bits)
```
| 字段 | 大小 | 取值范围 | 说明 |
|------|------|---------|------|
-| CFI | 1 bit | 0 或 1 | Canonical Format Indicator,传统以太网固定为 0 |
| PCP | 3 bit | 0–7 | Priority Code Point,QoS 优先级(见下表) |
-| VID | 12 bit | 0–4095 | VLAN Identifier,**实际可用 1–4094** |
+| DEI | 1 bit | 0 或 1 | Drop Eligible Indicator,指示该帧在拥塞时可被丢弃。在 802.1Q-2011 前称为 CFI(Canonical Format Indicator),传统以太网固定为 0 |
+| VID | 12 bit | 0–4095 | VLAN Identifier,**实际可用 1–4094**(0 仅用于优先级帧,4095 保留) |
### PCP 优先级对照
| PCP | 用途 | 典型场景 |
|-----|------|---------|
-| 0 | Best Effort | 普通数据流量 |
| 1 | Background | 后台低优先级的备份任务 |
-| 2–3 | Excellent Effort | 文件传输等中等业务 |
-| 4 | Critical Applications | 视频流会议 |
-| 5 | Voice | 语音通话 VoIP(最高优先) |
-| 6 | Network Control | 网络管理协议 |
-| 7 | Reserved | 保留 |
+| 0 | Best Effort(默认) | 普通数据流量,**大多数业务的默认值** |
+| 2 | Spare | 预留,可用于自定义业务 |
+| 3 | Excellent Effort | 关键业务数据(如 ERP、数据库同步) |
+| 4 | Video | 视频流(≤100ms 延迟要求) |
+| 5 | Voice | 语音通话 VoIP(≤10ms 延迟,**流控标记**) |
+| 6 | Internetwork Control | 路由协议等网络控制报文 |
+| 7 | Network Control | STP、LLDP 等二层协议,**不能被丢弃** |
+
+> [!question] 为什么 PCP 0(Best Effort)不是最低优先级?
+> 这是 IEEE 802.1Q 历史遗留设计。PCP 1 的 "Background" 比默认更"低",用于非紧急的大块传输(如备份),避免抢占默认业务的带宽。实际部署中 PCP 0 和 1 的区别不大,重点应关注 PCP 4–7。
## VLAN 端口类型
@@ -101,6 +108,30 @@ flowchart LR
style SW2 fill:#98FB98,color:#000
```
+## Native VLAN(本征 VLAN)
+
+Trunk 端口上有一个特殊概念:**Native VLAN**,默认为 VLAN 1。
+
+```mermaid
+flowchart LR
+ UH["不带标签的主机"] -->|"裸帧(无 Tag)"| TR["Trunk 端口"]
+ TH["带 Tag 的主机"] -->|"Tag 帧(VID=10)"| TR
+ TR -->|"识别为 Native VLAN"| NV["VLAN 1 域"]
+ TR -->|"按 VID 转发"| V10["VLAN 10 域"]
+
+ style NV fill:#FFD700,color:#000
+ style V10 fill:#87CEEB,color:#000
+```
+
+**工作规则:**
+- **入方向**:Trunk 端口收到**不带标签的帧**时,自动归入 Native VLAN
+- **出方向**:属于 Native VLAN 的帧在发出时**剥离标签**(对端看到的是裸帧)
+
+> [!warning] Native VLAN 必须两端一致
+> 如果 Trunk 两端交换机的 Native VLAN 设置不一致(一端是 VLAN 1,另一端是 VLAN 2),会导致:
+> 1. 流量被错误转发到非预期 VLAN
+> 2. 成为 VLAN Hopping 攻击的入口(见下一节)
+
## Router-on-a-Stick(单臂路由)
当路由器只有一个物理接口时,可以通过子接口 + VLAN 实现多个 VLAN 间路由:
@@ -152,29 +183,46 @@ $ ip -d link show type vlan
### Docker 中利用 VLAN
+> [!warning] Docker bridge 网络 ≠ VLAN
+> `docker network create --driver bridge` 创建的是**隔离的 Linux 网桥**,不会在帧上打 802.1Q Tag。它在逻辑上类似"独立广播域",但不是真正的 VLAN。
+> 要让容器接入真实 VLAN,需使用 `macvlan` 或 `ipvlan` 驱动:
+
```yaml
-# docker-compose.yml
+# docker-compose.yml — 容器直接接入物理 VLAN
services:
- web:
+ web-vlan10:
+ image: nginx
networks:
- frontend: # VLAN 10
+ office_net:
ipv4_address: 192.168.10.10
- backend: # VLAN 20
+
+ app-vlan20:
+ image: app:latest
+ networks:
+ server_net:
ipv4_address: 192.168.20.10
networks:
- frontend:
- driver: bridge
+ office_net:
+ driver: macvlan
+ driver_opts:
+ parent: eth0.10 # 宿主机 VLAN 子接口
ipam:
config:
- subnet: 192.168.10.0/24
- backend:
- driver: bridge
+ gateway: 192.168.10.1
+ server_net:
+ driver: macvlan
+ driver_opts:
+ parent: eth0.20
ipam:
config:
- subnet: 192.168.20.0/24
+ gateway: 192.168.20.1
```
+容器通过 macvlan 驱动直接收发带 802.1Q 标签的帧,与物理网络中的 VLAN 设备无缝互通。
+
## VLAN 边界与跨 Switch
```mermaid
@@ -191,6 +239,39 @@ sequenceDiagram
**核心要点:** VLAN 信息随 802.1Q Tag 穿过 Trunk 链路传递到下一台交换机。每台交换机根据自己的 VLAN 数据库独立处理。
+## VLAN Hopping 攻击与防御
+
+VLAN Hopping 是通过协议漏洞非法访问其他 VLAN 的二层攻击,主要有两种方式:
+
+### 攻击一:Double Tagging(双标签攻击)
+
+```mermaid
+sequenceDiagram
+ participant A as 攻击者 (VLAN 10)
+ participant S as Switch
+ participant V as VLAN 20 目标主机
+
+ A->>S: 外层 Tag VID=1 (Native VLAN)
内层 Tag VID=20 (目标 VLAN)
+ S->>S: 剥离外层 Tag (Native VLAN=1)
+ S->>V: 帧到达 VLAN 20!(内层 Tag 未被检查)
+```
+
+**原理**:攻击者发送双标签帧,外层使用 Native VLAN ID。Switch 剥离外层 Tag 后,内层 Tag 暴露,帧被转发到目标 VLAN。此攻击**单向**,且需要攻击者在 Native VLAN 中。
+
+> [!important] 防御:将 Native VLAN 设为未使用的 VLAN(如 VLAN 999),并确认所有 Trunk 端口都显式允许所需的 VLAN(不要用 `allowed vlan all`)
+
+### 攻击二:DTP 欺骗
+
+若端口启用了 DTP(Dynamic Trunking Protocol),攻击者可伪装成交换机,诱使端口切换为 Trunk 模式,从而访问所有 VLAN。
+
+**防御**:在所有接入端口上关闭 DTP 自动协商:
+```bash
+# Cisco: 关闭 DTP
+switchport nonegotiate
+# Huawei: 关闭自动协商
+undo negotiation auto
+```
+
## 最佳实践
| 项目 | 建议 |
@@ -199,10 +280,10 @@ sequenceDiagram
| Native VLAN | 修改默认 native VLAN(避免用 1),防 VLAN Hopping |
| VLAN 数量 | 单台交换机支持最大 4094 个 VLAN |
| Inter-VLAN 路由 | >10 个 VLAN 用三层交换机而非单臂路由 |
-| 安全 | 未使用的端口划入"黑洞 VLAN"(无 IP 段),关闭 DTP 自动协商 |
+| 安全 | 未使用的端口划入"黑洞 VLAN"(无 IP 段),**关闭 DTP**(Dynamic Trunking Protocol,Cisco 私有自动协商协议)|
## 关联笔记
-- [[hhs/NETWORK/Ethernet帧结构]] — 802.1Q Tag 插入在 Ethertype 位置之前
-- [[hhs/NETWORK/Switch与路由器]] — Switch 如何基于 VLAN 隔离广播域
-- [[hhs/NETWORK/MAC地址与广播域]] — VLAN 缩小了广播域的规模
+- [[hhs/NETWORK/02-链路层/01-Ethernet帧结构]] — 802.1Q Tag 的 TPID 替换了原始帧的 EtherType 位置
+- [[hhs/NETWORK/02-链路层/04-Switch与路由器]] — Switch 如何基于 VLAN 隔离广播域
+- [[hhs/NETWORK/02-链路层/03-MAC地址与广播域]] — VLAN 缩小了广播域的规模
diff --git a/hhs/NETWORK/03-网络层/01-IPv4首部与分段重组.md b/hhs/NETWORK/03-网络层/01-IPv4首部与分段重组.md
index 5bef9fa..f533fbf 100644
--- a/hhs/NETWORK/03-网络层/01-IPv4首部与分段重组.md
+++ b/hhs/NETWORK/03-网络层/01-IPv4首部与分段重组.md
@@ -1,5 +1,5 @@
---
-tags: [计算机网络, IPv4, IP首部, TTL, MTU, 分片]
+tags: [计算机网络, IPv4, IP首部, TTL, MTU, 分片, ECN, PMTUD, IP Options]
create time: 2026-05-17 23:40
---
@@ -12,19 +12,19 @@ IPv4 数据包是互联网的基本传输单元。理解其首部结构,有助
## IPv4 首部格式(32-bit fields)
```
-Version(4b) │ IHL(4b) │ DSCP/ECN(8b) │ Total Length(16b) │
-├─────────────────────────────────────────────────────────────────┤
-│ Identification(16b) │DF│ MF │Offset(13b)│
-├─────────────────────────────────────────────────────────────────┤
-│ TTL(8b) │ Protocol(8b) │ Header Checksum │
-├─────────────────────────────────────────────────────────────────┤
-│ Source Address (32b) │
-├─────────────────────────────────────────────────────────────────┤
-│ Destination Address (32b) │
-├─────────────────────────────────────────────────────────────────┤
-│ Options (variable) │
-│ ...padding... │
-└─────────────────────────────────────────────────────────────────┘
+Version(4b) │ IHL(4b) │ DSCP(6b) │ ECN(2b) │ Total Length(16b) │
+├───────────────────────────────────────────────────────────────────────┤
+│ Identification(16b) │Rsv│DF│MF│ Offset(13b) │
+├───────────────────────────────────────────────────────────────────────┤
+│ TTL(8b) │ Protocol(8b) │ Header Checksum(16b) │
+├───────────────────────────────────────────────────────────────────────┤
+│ Source Address (32b) │
+├───────────────────────────────────────────────────────────────────────┤
+│ Destination Address (32b) │
+├───────────────────────────────────────────────────────────────────────┤
+│ Options (variable, 0-40B) │
+│ ...padding... │
+└───────────────────────────────────────────────────────────────────────┘
```
### 各字段详解
@@ -33,9 +33,11 @@ Version(4b) │ IHL(4b) │ DSCP/ECN(8b) │ Total Length(16b) │
|------|------|------|
| **Version** | 4 bit | 版本号,IPv4 = `0100` |
| **IHL** (Internet Header Length) | 4 bit | 首部长度,单位 4 bytes。**最小值 5**(20 bytes),最大 15(60 bytes) |
-| **DSCP** (Differentiated Services Code Point) | 6 bit | QoS 优先级标记;前 3 bit 为 ECN(Explicit Congestion Notification)|
+| **DSCP** (Differentiated Services Code Point) | 6 bit | QoS 优先级标记,替代早期的 ToS 字段前 6 位 |
+| **ECN** (Explicit Congestion Notification) | 2 bit | 显式拥塞通知。末 2 bit 用于在不丢包的情况下通知端主机网络拥塞(`00`=不支持, `10`/`01`=支持 ECN, `11`=拥塞已经历) |
| **Total Length** | 16 bit | 整个 IP 包总长(含首部),最大 65535 字节 |
| **Identification** | 16 bit | 唯一标识符。同一原始包的分片共享此 ID |
+| **Reserved** | 1 bit | 保留位,必须为 0 |
| **DF** (Don't Fragment) | 1 bit | 置 1 则禁止分片;路由器不能分片的包如果 DF=1,直接丢弃并返回 ICMP "Fragmentation Needed" |
| **MF** (More Fragments) | 1 bit | 置 1 表示后面还有分片;最后一个分片 MF=0 |
| **Fragment Offset** | 13 bit | 该分片在原始数据中的偏移量(以 8 字节为单位) |
@@ -55,11 +57,46 @@ Version(4b) │ IHL(4b) │ DSCP/ECN(8b) │ Total Length(16b) │
受限于链路 MTU: 通常 1500B Payload → 最大常用包 1520 bytes
```
+> [!tip] 为什么 IHL 最大值是 15?
+> IHL 以 4 字节为单位,最大值 15 × 4 = 60 bytes。去掉固定首部 20 bytes,Options 最多 **40 bytes**。这就是为什么 Options 字段上限是 40B。
+
+### Options 字段(可选,0-40 bytes)
+
+Options 是 IPv4 首部中最容易被忽略的部分,但在网络诊断和特殊场景中依然重要:
+
+| 选项类型 | 说明 | 典型用途 |
+|---------|------|---------|
+| **Record Route** | 记录包经过的每一跳路由器 IP | 路由追踪(类似 traceroute) |
+| **Timestamp** | 每个路由器记录到达时间戳 | 测量延迟、时钟同步分析 |
+| **Strict Source Route** | 指定完整路径,逐跳严格匹配 | 诊断路由问题(现代网络几乎禁用) |
+| **Loose Source Route** | 指定必须经过的中间节点,其余自由路由 | 绕路测试 |
+
+> [!warning] 安全注意
+> 现代路由器和防火墙通常**丢弃包含 Options 的 IP 包**,因为 Source Route 曾被用于 IP 欺骗攻击。这也是为什么正常流量中你几乎看不到 Options。
+
+### Header Checksum 逐跳重算
+
+与 L2 CRC 和 TCP/UDP Checksum 不同,IP Header Checksum **每经过一个路由器都要重新计算**——因为 TTL 会递减。
+
+```mermaid
+flowchart LR
+ A["路由器收到包"] --> B["TTL -= 1"]
+ B --> C["重新计算 Checksum"]
+ C --> D["转发到下一跳"]
+
+ style A fill:#87CEEB,color:#000
+ style D fill:#98FB98,color:#000
+```
+
+> [!question] 为什么 IP Checksum 只校验首部?
+> 早期设计为减轻路由器负担——只校验 20 bytes 首部远比校验整个 64KB 包高效。数据完整性由上层协议(TCP/UDP Checksum)保证。这也是 IP 被称为"尽力而为"协议的原因之一。
+
## TTL 深度解析
### TTL 的常见误解
-很多人以为 TTL 的单位是秒。其实在 IPv4 中它是**跳数计数器**——每经过一个路由器(三层转发节点)减 1,不是按时间递减。
+> [!question] TTL 是时间吗?
+> 很多人以为 TTL 的单位是秒——这其实是 IPv6 的 Hop Limit 的前身历史遗留误解。在 IPv4 中 TTL 是**跳数计数器**——每经过一个路由器(三层转发节点)减 1,不是按时间递减。名字容易误导,但行为很简单:减到 0 就丢弃。
```mermaid
flowchart LR
@@ -109,10 +146,10 @@ MTU(Maximum Transmission Unit)是链路层允许的最大 IP 包载荷大小
|----------|---------|-------------|
| Ethernet | 1500 bytes | 9000 bytes |
| Wi-Fi (802.11n/ac/ax) | 1500 | ❌ 不支持 |
-| PPPoE (宽带拨号) | 1492 (预留 8B PPPoE头) | — |
-| IPv6 | 1280 (硬性最小) | 可选 |
-| GRE 隧道 | 1476 (预留 24B GRE头) | — |
-| VXLAN | 1450 (预留 50B VxLAN头) | — |
+| PPPoE (宽带拨号) | 1492 (8B PPPoE 头开销) | — |
+| IPv6 | 1280 (硬性最小 MTU) | 可选 |
+| GRE 隧道 | 1476 (24B GRE 头开销) | — |
+| VXLAN | 1450 (50B VXLAN 头开销) | — |
### 分片规则
@@ -138,10 +175,39 @@ MTU(Maximum Transmission Unit)是链路层允许的最大 IP 包载荷大小
| — | 攻击者可以利用分片做碎片攻击(Teardrop 等) |
| — | IPv6 已取消中间路由器分片,改由端到端 PMTUD |
+### Wireshark 中识别分片
+
+实战排查时,抓包识别分片是最常见需求:
+
+```bash
+# tcpdump 过滤分片包(offset > 0 或 MF=1 的包)
+$ tcpdump -i eth0 'ip[6:2] & 0x3fff != 0' -nn
+
+# Wireshark 显示过滤器
+# ip.frag_offset > 0 → 非首片
+# ip.flags.mf == 1 → 还有后续分片
+# ip.id == 0x1234 → 按 Identification 追踪同组分片
+```
+
+> [!tip] 分片重组技巧
+> Wireshark 默认会自动重组分片。如果需要查看原始分片:`Edit → Preferences → IPv4 → 去勾选 "Reassemble fragmented IPv4 datagrams"`。
+
+### 常见分片攻击
+
+| 攻击手法 | 原理 | 防御 |
+|---------|------|------|
+| **Teardrop** | 构造重叠偏移的分片,重组时内核缓冲区溢出 | 更新内核补丁(已修复) |
+| **Ping of Death** | 分片重组后 IP 包 > 65535 bytes,触发整数溢出 | 现代系统已防御 |
+| **Rose Attack** | 发送大量不完整的分片,消耗重组缓冲区 | 设置重组超时、限制并发分片数 |
+| **Fragment Overlap** | 后续分片覆盖前片数据,绕过基于首片的防火墙规则 | 防火墙需做完整分片重组后再检测 |
+
### PMTUD(Path MTU Discovery)
现代网络推荐使用 **路径 MTU 发现**而非分片:
+> [!warning] PMTUD Black Hole
+> 如果中间路由器**静默丢弃**了 DF=1 的大包,但不返回 ICMP,源主机会一直重传失败——这就是 PMTUD Black Hole。常见于错误配置的防火墙(拦截了 ICMP "Fragmentation Needed")。TCP 的对策是 `net.ipv4.tcp_mtu_probing`(值设为 2),主动探测合适的 MSS。
+
```mermaid
sequenceDiagram
participant Src as 源主机
@@ -152,8 +218,8 @@ sequenceDiagram
Note over Src: DF(Do Not Fragment)=1
R->>R: MTU=1400 < 1500, DF=1 → 无法分片!
R->>Src: ICMP Type 3 Code 4 "Fragmentation Needed"
- Note over Src: Path MTU = 1400 - IP头(20) - Eth(14) = 1366
- Src->>Dst: IP Packet (size=1366, DF=1) ✅
+ Note over Src: Path MTU = 1400, Max IP Payload = 1400 - 20(IP头) = 1380
+ Src->>Dst: IP Packet (size=1400, DF=1) ✅
Dst-->>Src: ACK ✅
```
diff --git a/hhs/NETWORK/03-网络层/02-CIDR与子网划分.md b/hhs/NETWORK/03-网络层/02-CIDR与子网划分.md
index 2376889..a6b8db3 100644
--- a/hhs/NETWORK/03-网络层/02-CIDR与子网划分.md
+++ b/hhs/NETWORK/03-网络层/02-CIDR与子网划分.md
@@ -22,7 +22,7 @@ CIDR(Classless Inter-Domain Routing,无类别域间路由)是现代 IP 寻
### 常见子网对照表
-| CIDR | 子网掩码 | 主机数 ( usable) | 典型用途 |
+| CIDR | 子网掩码 | 主机数 (usable) | 典型用途 |
|------|---------|-----------------|---------|
| /30 | 255.255.255.252 | 2 | 点对点链路 |
| /29 | 255.255.255.248 | 6 | 小型办公室 |
@@ -31,16 +31,30 @@ CIDR(Classless Inter-Domain Routing,无类别域间路由)是现代 IP 寻
| /26 | 255.255.255.192 | 62 | 小型部门 |
| /25 | 255.255.255.128 | 126 | 中型部门 |
| /24 | 255.255.255.0 | 254 | **最常用**,标准办公室 VLAN |
-| /23 | 255.255.255.254 | 510 | 大型部门 |
-| /22 | 255.255.255.252 | 1022 | 数据中心租户 |
-| /21 | 255.255.255.248 | 2046 | 大规模部署 |
-| /16 | 255.255.255.0 | 65534 | 大型企业内网 |
+| /23 | 255.255.254.0 | 510 | 大型部门 |
+| /22 | 255.255.252.0 | 1022 | 数据中心租户 |
+| /21 | 255.255.248.0 | 2046 | 大规模部署 |
+| /16 | 255.255.0.0 | 65534 | 大型企业内网 |
> [!tip] 快速计算可用主机数
> $N_{usable} = 2^h - 2$,其中 $h$ 是主机位数量($h = 32 - \text{prefix}$)
> - `-2` 因为网络地址和广播地址不可用
> - 对于 /31(点对点链路),RFC 3021 允许使用 2 个地址(无网络/广播位)
+## CIDR vs 分类地址:为什么需要 CIDR?
+
+CIDR(1993 年,RFC 1518/1519)诞生之前,IP 地址按 A/B/C 三类硬性划分,带来两个致命问题:
+
+| 对比项 | 分类地址(Classful) | CIDR(Classless) |
+|--------|-------------------|-----------------|
+| 前缀长度 | A=/8, B=/16, C=/24 固定三档 | 任意 /0 ~ /32 |
+| 分配灵活性 | 差——中等规模网络要么浪费 B 类(/16 多余),要么 C 类(/24)不够 | 精确按需分配 |
+| 路由聚合 | 不支持,每个网络单独一条路由 | 支持超网聚合,压缩路由表 |
+| 路由表规模 | 随网络数线性增长 | 聚合后显著缩小 |
+
+> [!QUESTION] 分类地址到底浪费有多严重?
+> 一个需要 2000 台主机的机构,在分类体系下只能申请 B 类地址(/16,65534 个 IP),实际使用率仅 **3%**——剩下 63000+ 个地址被白白锁死。CIDR 只需分配 /21(2046 个 IP),利用率直接拉满。
+
## 子网划分的核心算法
### 示例:从 /24 划分为 4 个子网
@@ -64,11 +78,11 @@ Subnet 3: 192.168.1.192/26 — 范围: .192~.255 — 可用: .193~.254 — Bca
```mermaid
flowchart LR
- S0["192.168.1.0/26
.1-.62"] -->|"PCs / Servers"| SW0[Switch-A]
- S1["192.168.1.64/26
.65-.126"] -->|"IP Phones"| SW1[Switch-B]
- S2["192.168.1.128/26
.129-.190"] -->|"Guest WiFi"| AP1[AP-C]
- S3["192.168.1.192/26
.193-.254"] -->|"IoT Devices"| GW1[Gateway .1]
-
+ S0["192.168.1.0/26
.1-.62"] -->|"PCs, Servers"| SW0["Switch-A"]
+ S1["192.168.1.64/26
.65-.126"] -->|"IP Phones"| SW1["Switch-B"]
+ S2["192.168.1.128/26
.129-.190"] -->|"Guest WiFi"| AP1["AP-C"]
+ S3["192.168.1.192/26
.193-.254"] -->|"IoT Devices"| GW1["Gateway .1"]
+
style S0 fill:#DDA0DD,color:#000
style S1 fill:#FFD700,color:#000
style S2 fill:#98FB98,color:#000
@@ -77,17 +91,39 @@ flowchart LR
### VLSM(可变长子网掩码)
-不同子网可以用不同长度的掩码——这就是**可变长子掩码**。
+不同子网可以用不同长度的掩码——这就是**可变长子网掩码**(VLSM)。在 CIDR 出现之前,同一网络内所有子网必须等长,造成大量地址浪费。VLSM 允许按需"量体裁衣"。
-场景:公司有 4 个部门,需要不同规模:
-- 研发部:200 台 → 需要至少 202 → /24(254 台)
-- 市场部:50 台 → 需要至少 52 → /26(62 台)
-- 行政部:10 台 → 需要至少 12 → /28(14 台)
-- 互联链:2 台 → 需要 2 → /30(2 台)
+场景:公司申请了 `192.168.0.0/22`(1022 个可用 IP),4 个部门需求如下:
-总计借用:32 - 24 = 8 位主机位
-- 研发 /24: 借回 0 位子网位 → 用掉 1 个大块
-- 剩余给其他 3 个 /26, /28, /30...
+| 部门 | 主机数 | 需要 ≥ | 分配掩码 | 可用 IP |
+|------|--------|-------|---------|--------|
+| 研发部 | 200 | 202 | /24 | 254 |
+| 市场部 | 50 | 52 | /26 | 62 |
+| 行政部 | 10 | 12 | /28 | 14 |
+| 互联链路 | 2 | 2 | /30 | 2 |
+
+**分配原则:从大到小,依次切割**(先给需求最大的部门分配,避免碎片化):
+
+```
+总地址池: 192.168.0.0/22(含 .0.0 ~ .3.255,共 1024 地址)
+
+① 研发部 → 192.168.0.0/24 用掉 256 地址(.0.0 ~ .0.255)
+ 剩余: 192.168.1.0 ~ 192.168.3.255
+
+② 市场部 → 192.168.1.0/26 用掉 64 地址(.1.0 ~ .1.63)
+ 剩余: 192.168.1.64 ~ 192.168.3.255
+
+③ 行政部 → 192.168.1.64/28 用掉 16 地址(.1.64 ~ .1.79)
+ 剩余: 192.168.1.80 ~ 192.168.3.255
+
+④ 互联链 → 192.168.1.80/30 用掉 4 地址(.1.80 ~ .1.83)
+ 剩余: 192.168.1.84 ~ 192.168.3.255(留给未来扩展)
+```
+
+> [!tip] VLSM 规划要点
+> - **先大后小**:优先满足最大子网需求,剩余空间再切分,避免碎片
+> - **注意边界对齐**:每个子网起始地址必须是其掩码块大小的整数倍(例如 /26 块大小 64,起址必须是 64 的倍数)
+> - 实际操作中常用 Excel 或 `ipcalc`/`sipcalc` 工具辅助规划
## 实用计算工具
@@ -112,10 +148,35 @@ $ python3 -c "import ipaddress; print(list(ipaddress.ip_network('192.168.10.0/26
| 步骤 | 操作 |
|------|------|
-| 1 | 将最后一个 octet 转换为二进制 |
-| 2 | 前 prefix%32 位为网络位,其余为主机位 |
-| 3 | 网络地址 = 全置 0;广播地址 = 全置 1 |
-| 4 | 可用范围 = 网络地址+1 ~ 广播地址-1 |
+| 1 | 将 IP 地址与子网掩码都转为二进制 |
+| 2 | **网络地址** = IP **按位 AND** 掩码(两列都为 1 才写 1) |
+| 3 | **广播地址** = 网络地址的主机位全部置 1 |
+| 4 | **可用范围** = 网络地址+1 ~ 广播地址-1 |
+
+> [!example] 动手练:求 `172.16.50.100/18` 的网络地址、广播地址、可用范围
+>
+> **步骤 1** — 写出第三字节的二进制(/18 意味着前两个字节 + 第三字节前 2 位是网络位):
+> ```
+> IP 第三字节: 50 = 00110010
+> 掩码第三字节: 192 = 11000000 ← /18 对应 255.255.192.0
+> ```
+>
+> **步骤 2** — 按位 AND:
+> ```
+> IP: 00110010
+> Mask: 11000000
+> AND: 00000000 → 网络地址第三字节 = 0
+> 网络地址 = 172.16.0.0
+> ```
+>
+> **步骤 3** — 主机位全 1(第三字节后 6 位 + 第四字节 8 位 = 14 位主机位):
+> ```
+> 00111111.11111111 = 63.255
+> 广播地址 = 172.16.63.255
+> ```
+>
+> **步骤 4** — 可用范围:
+> `172.16.0.1 ~ 172.16.63.254`,共 $2^{14} - 2 = 16382$ 个可用 IP
## 私有地址空间(RFC 1918)
@@ -132,6 +193,38 @@ $ python3 -c "import ipaddress; print(list(ipaddress.ip_network('192.168.10.0/26
> - 多机房、分布式 → `10.0.0.0/8` 留有余量
> - Docker 默认使用 `172.17.0.0/16`
+## 特殊地址段速查
+
+除了 RFC 1918 私有地址,还有几个常考的特殊地址段:
+
+| 地址段 | 名称 | 用途 |
+|--------|------|------|
+| `127.0.0.0/8` | 环回地址(Loopback) | 本机测试,最常用 `127.0.0.1` |
+| `169.254.0.0/16` | 链路本地地址(Link-local) | DHCP 失败时自动分配(APIPA) |
+| `224.0.0.0/4` | 组播地址(Multicast) | OSPF 用 `224.0.0.5`/`224.0.0.6` |
+| `0.0.0.0/8` | 本网络 | 表示「当前网络」,路由表中代表默认路由 |
+
+> [!QUESTION] 为什么 `127.0.0.1` 能 Ping 通自己?
+> 发送到 127.x.x.x 的数据包不会离开主机——操作系统内核的网络栈直接将它**环回**到传输层,不经过任何物理网卡。这也是它作为服务健康检查首选地址的原因。
+
+## 最长前缀匹配(Longest Prefix Match)
+
+CIDR 引入了一个关键的路由查找规则:当一个目标 IP 匹配多条路由时,**前缀越长(掩码越精确)的路由优先**。
+
+```
+路由表:
+ 192.168.0.0/16 → 下一跳 A
+ 192.168.1.0/24 → 下一跳 B
+ 192.168.1.0/26 → 下一跳 C
+
+目标 IP: 192.168.1.50
+ 匹配 /16 ✅ /24 ✅ /26 ✅
+ 最长前缀 → /26 → 选择下一跳 C
+```
+
+> [!tip] 面试高频题
+> 最长前缀匹配是路由器硬件(TCAM)的核心算法。理解它,就理解了为什么 CIDR 能让路由表既精简又灵活——聚合路由用短前缀覆盖大范围,精确路由用长前缀处理例外。
+
## 超网聚合(Supernetting / CIDR Aggregation)
将多个连续的小网合并为一个更大的网:
@@ -145,7 +238,7 @@ $ python3 -c "import ipaddress; print(list(ipaddress.ip_network('192.168.10.0/26
共同前缀: 11000000.10101000.000000XX → /22
聚合后: 192.168.0.0/22 包含 1024 个 IP
- (注意:实际只用了其中两个 /24 块,浪费了中间两个)
+ (注意:实际只用了其中 3 个 /24 块,浪费了 192.168.0.0/24 这 1 个)
```
路由聚合减少了全球 BGP 路由表的大小。没有 CIDR 聚合,BGP 路由表会膨胀到百万级别。
@@ -153,30 +246,48 @@ $ python3 -c "import ipaddress; print(list(ipaddress.ip_network('192.168.10.0/26
## Go 中的子网判断
```go
-import "net"
+package main
-ip := net.ParseIP("192.168.1.50")
-_, cidr, _ := net.ParseCIDR("192.168.1.0/26")
+import (
+ "fmt"
+ "net"
+)
-if cidr.Contains(ip) {
- fmt.Println("IP 在子网范围内 ✅")
-}
+func main() {
+ ip := net.ParseIP("192.168.1.50")
+ _, cidr, _ := net.ParseCIDR("192.168.1.0/26")
-// 遍历所有可用 IP
-for ip := cidr.IP.Mask(cidr.Mask); cidr.Contains(ip); inc(ip) {
- if ip.Equal(cidr.IP) || ip.Equal(lastIP) {
- continue // 跳过网络地址和广播地址
+ // 判断 IP 是否在子网内
+ if cidr.Contains(ip) {
+ fmt.Println("IP 在子网范围内 ✅")
+ }
+
+ // 计算广播地址(网络地址 | ^子网掩码)
+ broadcast := make(net.IP, len(cidr.IP))
+ for i := range cidr.IP {
+ broadcast[i] = cidr.IP[i] | ^cidr.Mask[i]
+ }
+
+ // 遍历所有可用 IP(跳过网络地址和广播地址)
+ for ip := cidr.IP.Mask(cidr.Mask); cidr.Contains(ip); inc(ip) {
+ if ip.Equal(cidr.IP) || ip.Equal(broadcast) {
+ continue
+ }
+ fmt.Println(ip) // 处理每个可用 IP
+ }
+}
+
+// IP 地址自增(逐字节进位)
+func inc(ip net.IP) {
+ for i := len(ip) - 1; i >= 0; i-- {
+ ip[i]++
+ if ip[i] > 0 { break }
}
- // 处理每个可用 IP
}
-func inc(ip net.IP) { for i := len(ip) - 1; i >= 0; i-- {
- ip[i]++
- if ip[i] > 0 { break }
-}}
```
## 关联笔记
-- [[hhs/NETWORK/IPv4首部与分段重组]] — IP 地址在 IPv4 包中的位置
-- [[hhs/NETWORK/IPv6地址与扩展头部]] — IPv6 同样使用 CIDR 表示法
-- [[hhs/NETWORK/NAT原理与应用]] — NAT 通常映射到私有地址空间
+- [[hhs/NETWORK/03-网络层/01-IPv4首部与分段重组]] — IP 地址在 IPv4 包中的位置
+- [[hhs/NETWORK/03-网络层/03-IPv6地址与扩展头部]] — IPv6 同样使用 CIDR 表示法
+- [[hhs/NETWORK/03-网络层/08-NAT原理与应用]] — NAT 通常映射到私有地址空间
diff --git a/hhs/NETWORK/03-网络层/03-IPv6地址与扩展头部.md b/hhs/NETWORK/03-网络层/03-IPv6地址与扩展头部.md
index 5acd9a9..f3712e0 100644
--- a/hhs/NETWORK/03-网络层/03-IPv6地址与扩展头部.md
+++ b/hhs/NETWORK/03-网络层/03-IPv6地址与扩展头部.md
@@ -1,5 +1,5 @@
---
-tags: [计算机网络, IPv6, SLAAC, 地址空间]
+tags: [计算机网络, IPv6, SLAAC, 地址空间, ICMPv6, NDP, 扩展头部]
create time: 2026-05-18 00:20
---
@@ -44,8 +44,30 @@ IPv4映射: 0000:0000:0000:0000:0000:ffff:c0a8:0101
| `::1/128` | 环回 (Loopback) | `::1` | 本机 |
| `fc00::/7` | ULA (唯一本地地址) | `fd00::1` | 私有地址,等价 RFC1918 |
| `fe80::/10` | 链路本地 (Link-Local) | `fe80::1` | 仅同链路可用,自动配置 |
-| `ff00::/8` | 组播 (Multicast) | `ff02::1` | 多播组 |
-| — | 未指定地址 | `::` | 类似 IPv4 的 0.0.0.0 |
+| `ff00::/8` | 组播 (Multicast) | `ff02::1` | 多播组(作用域由第二段决定) |
+| — | 未指定地址 | `::` | 仅用于 DAD、默认路由等,不可作为目标地址 |
+| `2000::/3` 中分配 | 任播 (Anycast) | 与单播地址格式相同 | 一组节点共享同一地址,报文送达最近的一个 |
+
+> [!question] IPv6 地址空间到底有多大?
+> 128 位 = 2¹²⁸ ≈ 3.4 × 10³⁸ 个地址。对比:IPv4 仅 2³² ≈ 43 亿,全球沙粒总数约 7.5 × 10¹⁸。IPv6 的地址数足以给地球上每一粒沙子分配约 4.5 × 10¹⁹ 个地址。实际上,全球单播地址仅使用 `2000::/3`(约 1/8 空间),但已绰绰有余。
+
+> [!tip] 链路本地地址 (Link-Local) 是强制的
+> 每个启用 IPv6 的接口**必须**自动配置一个 `fe80::` 开头的链路本地地址——即使没有路由器、没有全球前缀。因为 NDP(邻居发现)、RA 接收、DAD 等基础协议都依赖它运作。
+
+### 组播地址作用域
+
+IPv6 组播地址格式为 `ff0s::/8`,其中 `s` 是作用域标志:
+
+| 第二段 (scope) | 前缀 | 作用域 | 常见地址 |
+|:-:|:-:|:-:|:-:|
+| 1 | `ff01::/16` | 本机 (Interface-Local) | `ff01::1` 所有节点 |
+| 2 | `ff02::/16` | 链路 (Link-Local) | `ff02::1` 所有节点, `ff02::2` 所有路由器 |
+| 5 | `ff05::/16` | 站点 (Site-Local) | `ff05::1:3` 所有 DHCP 服务器 |
+| 8 | `ff08::/16` | 组织 (Organization) | 企业级范围 |
+| e | `ff0e::/16` | 全球 (Global) | 跨互联网组播 |
+
+> [!question] IPv6 去掉了广播,用什么替代?
+> IPv4 有广播(`255.255.255.255`),IPv6 **完全取消广播**,用组播 + 任播替代。`ff02::1`(所有节点组播)在功能上等价于链路广播,但更精准——只有加入该组的节点才处理。
## IPv6 地址组成
@@ -56,11 +78,16 @@ IPv4映射: 0000:0000:0000:0000:0000:ffff:c0a8:0101
```
MAC: aa:bb:cc:dd:ee:ff
插入 ff:fe → aa:bb:cc:ff:fe:dd:ee:ff
-翻转第七位(UD bit) → ab:bb:cc:ff:fe:dd:ee:ff
+翻转第七位(U/L bit) → a8:bb:cc:ff:fe:dd:ee:ff
+ ↑ aa=10101010 → 第7位取反 → 10101000=a8
-结果: fe80::ab:bb:cc:ff:fe:dd:ee:ff (link-local)
+结果: fe80::a8bb:ccff:fedd:eeff (link-local)
```
+> [!warning] EUI-64 的隐私隐患与 RFC 4941
+> EUI-64 将 MAC 地址嵌入 IPv6 地址,意味着你的设备在全球可被追踪(MAC 不变 → IPv6 接口 ID 不变)。
+> **RFC 4941 隐私扩展**:主机周期性生成**随机临时地址**用于对外通信(如上网浏览),EUI-64 地址仅用于入站连接。现代 OS(Linux/Windows/macOS)默认启用此机制。
+
### SLAAC( Stateless Address Autoconfiguration )
主机无需 DHCP 服务器,通过 Router Advertisement(RA)消息自行配置:
@@ -70,24 +97,45 @@ sequenceDiagram
participant Host as 主机
participant R as 路由器
- R->>Host: Router Advertisement (每 200s~60min)
+ R->>Host: Router Advertisement (每 200s~1800s)
Note over Host: RA 中包含:
Note over Host: - 前缀 (如 2001:db8:abcd::/64)
Note over Host: - prefix length
Note over Host: - 默认网关
Note over Host: - DNS (RFC 6106 / RDNSS)
+ Note over Host: - M/O 标志位
Host->>Host: 自构 IPv6 地址
- Host->>R: NDP Neighbor Solicitation (查重)
- R-->>Host: (无冲突 → 无回复)
+ Host->>Host: DAD: 以 :: 为源地址发送 NS
+ Host->>R: NDP Neighbor Solicitation (目标=候选地址)
+ R-->>Host: (无冲突 → 无 NA 回复)
Host->>Host: ✅ 分配 2001:db8:abcd::
```
-### DUID-DHCPv6
+> [!info] RA 中的 M/O 标志位决定地址获取方式
+> | 标志 | 含义 | 行为 |
+> |------|------|------|
+> | **M** (Managed) | 管理地址配置 | 主机**必须**使用 DHCPv6 获取地址 |
+> | **O** (Other) | 其他配置 | SLAAC 获取地址 + DHCPv6 获取 DNS 等信息 |
+> | M=0, O=0 | 纯 SLAAC | 前缀 + 接口标识符,无 DHCPv6 |
-DHCPv6 服务器分配地址时需要客户端标识:
-- **DUID-LLT**: DUID + Link-Layer Time + Link-Layer Address
-- **DUID-UUID**: DUID + UUID (持久不变)
+> [!tip] DAD(重复地址检测)为什么用 `::` 作为源地址?
+> 因为发送 DAD 时,该地址还未正式归属于主机——正在验证中。所以用未指定地址 `::` 作为源,避免"自己宣布了一个还没确认的地址"的矛盾。
+
+### DHCPv6 与 DUID
+
+DHCPv6 相比 SLAAC 提供更多控制(DNS、NTP、域名等),适用于企业环境。客户端标识使用 DUID(DHCP Unique Identifier):
+
+| DUID 类型 | 组成 | 特点 |
+|:-:|:-:|:-:|
+| **DUID-LLT** | MAC + 时间戳 | 最常用,基于链路层地址+生成时间 |
+| **DUID-LL** | 仅 MAC | 无时间戳,适合无持久存储设备 |
+| **DUID-UUID** | UUID | 持久不变,虚拟机场景 |
+
+> [!info] SLAAC vs DHCPv6 速查
+> - **SLAAC**: 零配置、去中心化,适合家庭/小型网络
+> - **DHCPv6 Stateful**: 完整地址分配+配置管理,适合企业
+> - **DHCPv6 Stateless**: SLAAC 分配地址 + DHCPv6 仅下发 DNS 等信息(最常见组合)
## IPv6 首部 vs IPv4 首部
@@ -102,6 +150,42 @@ DHCPv6 服务器分配地址时需要客户端标识:
| 首部 Checksum | ✅ | ❌ **已删除!** |
| 分片 | ID/DF/MF/Offset | 移到扩展头(由源端分片) |
+IPv6 固定首部仅 40 字节,结构如下:
+
+```mermaid
+block-beta
+ columns 4
+ block:header:4
+ columns 4
+ A["Version 4bit"]:1
+ B["Traffic Class 8bit"]:1
+ C["Flow Label 20bit"]:2
+ end
+ block:body:4
+ columns 4
+ D["Payload Length 16bit"]:1
+ E["Next Header 8bit"]:1
+ F["Hop Limit 8bit"]:1
+ G["reserved"]:1
+ end
+ block:src:4
+ columns 1
+ H["Source Address 128bit"]
+ end
+ block:dst:4
+ columns 1
+ I["Destination Address 128bit"]
+ end
+
+ style header fill:#e8f4fd,stroke:#2196f3
+ style body fill:#f3e5f5,stroke:#9c27b0
+ style src fill:#e8f5e9,stroke:#4caf50
+ style dst fill:#fff3e0,stroke:#ff9800
+```
+
+> [!question] IPv6 固定首部 40 字节 vs IPv4 最小 20 字节,更大反而更好?
+> IPv6 首部虽然更大,但**字段更少且固定**(无 Options),路由器处理时无需条件分支判断——这就是"简化首部"的设计哲学。用空间换时间,线速转发更容易实现。
+
### IPv6 为什么去掉 Checksum?
- TCP/UDP/ICMPv6 自带端到端校验
@@ -112,27 +196,68 @@ DHCPv6 服务器分配地址时需要客户端标识:
新引入的 20-bit 字段,用于标识属于同一"流"的数据包序列。中间网络设备可据此做 QoS 或负载均衡——无需深度解析载荷。
+典型场景:同一 TCP 连接的所有包携带相同的 Flow Label,路由器据此快速分类转发路径,避免逐包查表。结合 SRv6 使用效果更佳。
+
## IPv6 扩展头部(Extension Headers)
IPv6 将可选功能移出固定首部,放在扩展头链中,由 Next Header 字段串联:
+```mermaid
+graph LR
+ FH["Fixed Header"]
+ HBH["Hop-by-Hop Options"]
+ DO["Destination Options"]
+ RT["Routing"]
+ FR["Fragment"]
+ AH["AH"]
+ ESP["ESP"]
+ UL["Upper Layer TCP/UDP/ICMPv6"]
+
+ FH -->|"Next Header=0"| HBH
+ FH -->|"Next Header=60"| DO
+ HBH -->|"Next Header=43"| RT
+ RT -->|"Next Header=44"| FR
+ FR -->|"Next Header=51"| AH
+ AH -->|"Next Header=50"| ESP
+ ESP -->|"Next Header=6/17/58"| UL
+ DO -->|"Next Header=6/17/58"| UL
```
-Fixed Header → Hop-by-Hop Options (可选) → Destination Options → Routing → Fragment → Authentication (AH) → Encapsulating Security Payload (ESP) → Upper Layer (TCP/UDP/ICMPv6)
-```
+
+> [!info] 扩展头的顺序是有规定的
+> RFC 8200 建议的推荐顺序:Hop-by-Hop → Destination(路由头之前) → Routing → Fragment → AH → ESP → Destination(上层头之前)。但中间路由器**只需处理** Hop-by-Hop Options,其余扩展头在到达最终目的地时才解析——这对转发性能非常友好。
| 扩展头 | 类型值 | 用途 |
|--------|-------|------|
| Hop-by-Hop Options | 0 | 逐跳选项(极少使用) |
-| Routing | 43 | 路由类型 0 (SRv6 前身)、Type 2 (Mobile IPv6) |
+| Routing | 43 | Type 2 (Mobile IPv6);~~Type 0 已废弃~~(见下方警告) |
| Fragment | 44 | 分片信息(ID/offset/MF),仅出现在原始包中 |
| Authentication (AH) | 51 | IPsec 认证(较少独立使用) |
| Encapsulating Security Payload (ESP) | 50 | IPsec 加密封装 |
-| Destination Options | 60 | 终点选项(MIO 移动 IPv6、Jumbo Payload) |
+| Destination Options | 60 | 终点选项(MIPv6、Jumbo Payload) |
| Mobility Header | 135 | Mobile IPv6 |
+> [!danger] Routing Header Type 0 (RH0) 已被废弃
+> RFC 5095 (2007) **禁止使用 RH0**。原因是攻击者可在 RH0 中列出多个中间节点,让数据包在两个路由器之间反复弹跳(Amplification Attack),造成 DDoS。现代路由器收到 RH0 会直接丢弃。SRv6(Segment Routing over IPv6)使用新的 SRH 扩展头(Type 4)来实现类似功能,且设计上避免了此安全问题。
+
> [!warning] Path MTU Discovery in IPv6
> IPv6 规定**路由器不允许对 IP 包分片**。如果包超过链路 MTU,路由器直接丢弃并返回 ICMPv6 "Packet Too Big"。这就是 PMTUD 在 IPv6 中成为强制要求的原因。
+## ICMPv6:IPv6 的"万能胶水"
+
+ICMPv6(Next Header = 58)在 IPv6 中的角色远超 IPv4 的 ICMP——它同时承载了 IPv4 中 ARP、IGMP 的功能:
+
+| 功能 | IPv4 中的协议 | IPv6 中统一由 ICMPv6 承担 |
+|------|:--:|:-:|
+| 错误/信息报告 | ICMP | ICMPv6 |
+| 地址解析(MAC↔IP) | **ARP** | **NDP** (Neighbor Discovery Protocol) |
+| 组播组管理 | **IGMP** | **MLD** (Multicast Listener Discovery) |
+| 路由器发现 | 无标准协议 | **RA/RS** (Router Advertisement/Solicitation) |
+| 重复地址检测 | 无 | **DAD** (Duplicate Address Detection) |
+| Path MTU 发现 | ICMP "Fragmentation Needed" | ICMPv6 "Packet Too Big" |
+
+> [!question] 为什么 IPv6 去掉了 ARP?
+> ARP 是一个独立的 L2/L3 之间的"怪胎"协议(既不是 IP 也不是 TCP/UDP),有广播泛洪、无认证等先天缺陷。IPv6 用 NDP(基于 ICMPv6 组播)替代它——地址解析只发到目标节点所在的组播组,而非广播全网,更高效也更安全。
+
## IPv4 到 IPv6 的过渡机制
| 技术 | 原理 | 适用场景 |
@@ -144,22 +269,42 @@ Fixed Header → Hop-by-Hop Options (可选) → Destination Options → Routing
## Go 中的 IPv6 支持
+Go 的 `net` 包天然支持 IPv6,`"tcp6"` 和 `"tcp4"` 区分协议族:
+
```go
-import "net"
-
-// 创建双栈监听器(同时支持 v4/v6)
-ln, err := net.Listen("tcp6", ":8080")
-// 设置 IPv6Only = false 可同时接受 IPv4 连接 (DualStack)
-
-// 解析 IPv6 地址
-ip := net.ParseIP("2001:db8::1") // net.IP 天然支持 v4/v6
+import (
+ "context"
+ "net"
+ "syscall"
+)
+// 解析与判断
+ip := net.ParseIP("2001:db8::1") // net.IP 同时支持 v4/v6
_, cidr, _ := net.ParseCIDR("2001:db8::/32")
fmt.Println(cidr.Contains(ip)) // true
+
+// Dual Stack 监听(同时接受 v4/v6 连接)
+// 注意:Linux 默认 IPv6 socket 也接受 v4(IPV6_V6ONLY=0)
+ln, _ := net.Listen("tcp", ":8080")
+
+// 仅 IPv6 监听(不接受 v4 映射地址)
+lc := net.ListenConfig{
+ Control: func(network, address string, c syscall.RawConn) error {
+ return c.Control(func(fd uintptr) {
+ syscall.SetsockoptInt(int(fd), syscall.IPPROTO_IPV6,
+ syscall.IPV6_V6ONLY, 1) // 强制 IPv6 only
+ })
+ },
+}
+ln6, _ := lc.Listen(context.Background(), "tcp6", ":8080")
```
+> [!tip] 实战建议
+> 生产环境建议使用 `"tcp"` (Dual Stack) 监听,让 OS 同时处理 v4/v6。仅在需要明确区分协议族时才使用 `tcp4`/`tcp6`。
+
## 关联笔记
- [[hhs/NETWORK/IPv4首部与分段重组]] — IPv4 和 IPv6 首部的差异对比
- [[hhs/NETWORK/DNS原理与优化]] — AAAA 记录解析 IPv6 地址
- [[hhs/NETWORK/CIDR与子网划分]] — CIDR 在 IPv6 中的应用方式相同
+- [[hhs/NETWORK/02-网络层/02-ARP与ICMP]] — ARP 协议(被 NDP 替代)
diff --git a/hhs/Redis/02-核心数据类型.md b/hhs/Redis/02-核心数据类型.md
index 8f86618..a6559af 100644
--- a/hhs/Redis/02-核心数据类型.md
+++ b/hhs/Redis/02-核心数据类型.md
@@ -43,6 +43,90 @@ Redis 提供五种基础数据类型,用一句话总结各自的核心价值
> 想深入了解? [[02-核心数据类型/02-5-SDS|SDS]] · [[02-核心数据类型/02-1-ziplist与listpack|ziplist 与 listpack]] · [[02-核心数据类型/02-2-skiplist|skiplist]] · [[02-核心数据类型/02-3-hashtable|hashtable]] · [[02-核心数据类型/02-4-quicklist|quicklist]]
+### key → value 映射全景
+
+> [!tip] 五种类型,同一个套路
+> 无论哪种类型,Redis 的 key 永远是**一个字符串**(底层 SDS)。区别只在于 **value 的内部组织方式**。
+
+```mermaid
+flowchart LR
+ K["Redis Key 空间, 永远是 SDS"] --> V1["String: 一坨不可分割的值"]
+ K --> V2["Hash: 多个 field-value 对"]
+ K --> V3["List: 有序可重复序列"]
+ K --> V4["Set: 无序不重复集合"]
+ K --> V5["ZSet: member-score 对, 按 score 排序"]
+ classDef key fill:#e1f5fe,stroke:#2196f3
+ classDef val fill:#fff3e0,stroke:#ff9800
+ class K key
+ class V1,V2,V3,V4,V5 val
+```
+
+每个类型的 value 具体长什么样——用实际业务数据来感受:
+
+**String**:value 就是一个原子值,Redis 不关心里面是 JSON 还是纯文本。
+
+```
+SET "session:abc123" "eyJhbGciOi..."
+┌──────────────────┐ ┌──────────────────────┐
+│ key │ │ value │
+│ "session:abc123" │ ──→ │ "eyJhbGciOi..." │
+│ │ │ 就是一个字符串,没了 │
+└──────────────────┘ └──────────────────────┘
+```
+
+**Hash**:value 是一张"小表格",field 活在 value 内部,不是 Redis key。
+
+```
+HSET "user:1001" name "alice" age "25" email "a@x.com"
+┌──────────────────┐ ┌───────────────────────────┐
+│ key │ │ value │
+│ "user:1001" │ ──→ │ name → "alice" │
+│ │ │ age → "25" │
+│ │ │ email → "a@x.com" │
+│ │ │ field 不是 key,仅限 value 内│
+└──────────────────┘ └───────────────────────────┘
+```
+
+**List**:value 是一条有序队列,元素没有名字,只有位置。
+
+```
+LPUSH "unread:1001" "notif:501" "notif:500" "notif:499"
+┌──────────────────┐ ┌─────────────────────────────────┐
+│ key │ │ value │
+│ "unread:1001" │ ──→ │ [0]notif:499 [1]notif:500 [2]... │
+│ │ │ ← 旧 新 → │
+│ │ │ 元素可以重复,有顺序 │
+└──────────────────┘ └─────────────────────────────────┘
+```
+
+**Set**:value 是一个不重复的袋子,没有顺序、没有分数。
+
+```
+SADD "online:chat42" "user:1001" "user:2002" "user:3003"
+┌──────────────────┐ ┌──────────────────────────────┐
+│ key │ │ value │
+│ "online:chat42" │ ──→ │ { user:1001, user:2002, │
+│ │ │ user:3003 } │
+│ │ │ 去重无序,SADD 重复元素只保留一份 │
+└──────────────────┘ └──────────────────────────────┘
+```
+
+**ZSet**:value 是一组 (member, score) 对,member 唯一、score 可重复。
+
+```
+ZADD "leaderboard" 100 "alice" 200 "bob" 150 "carol"
+┌──────────────────┐ ┌──────────────────────────────┐
+│ key │ │ value │
+│ "leaderboard" │ ──→ │ bob → 200 ← 最高 │
+│ │ │ carol → 150 │
+│ │ │ alice → 100 ← 最低 │
+│ │ │ 按 score 排序,member 不可重复 │
+└──────────────────┘ └──────────────────────────────┘
+```
+
+> [!question] Hash 的 field 和 ZSet 的 member 都"像 key",它们到底是不是 key?
+> **都不是。** 它们只存在于所属 key 的 value 内部。`HGET user:1001 name` 必须带上 key `user:1001` 才能访问——你不能用 `name` 全局查找。同理,ZSet 的 `member` 也不能脱离 `leaderboard` 独立存在。它们的作用域**仅限于各自 key 的 value 内部**。
+
我们先从最简单的 String 开始,逐个击破。
## 一、String(字符串)
@@ -318,6 +402,21 @@ skiplist 天然有序,擅长范围查询("给我分数在 100~200 之间的
> [!warning] ZSet 性能陷阱
> 不要往同一个 ZSet 里塞超过百万级别的元素——skiplist 虽然 O(log N),但百万级的维护成本不小。可以按分片拆成多个 ZSet。
+## 同一业务场景:五种类型怎么选?
+
+假设你在做一个"用户主页",需要缓存以下信息:
+
+| 信息 | 最佳类型 | key 设计 | value 存什么 |
+|------|---------|---------|-------------|
+| 用户 Token | **String** | `"token:uid1001"` | `"eyJhbG..."` (一个字符串) |
+| 用户资料 | **Hash** | `"user:1001"` | `{name, age, bio...}` (多个字段,可部分更新) |
+| 最近浏览记录 | **List** | `"browsed:1001"` | `["商品A","商品B","商品C"]` (有序可重复) |
+| 用户标签 | **Set** | `"tags:1001"` | `{"Go","Redis","后端"}` (去重无序) |
+| 全站积分排名 | **ZSet** | `"rank:global"` | `{uid1001:5200, uid2002:4800...}` (按分数排序) |
+
+> [!summary] 一句话记忆
+> **key 是门牌号,value 是房间里的东西。** String 放一坨、Hash 放一张表、List 放一条队列、Set 放一个袋子、ZSet 放一个排行榜。
+
## 快速对照表
| 数据类型 | 编码 | 最佳场景 | 时间复杂度 |
diff --git a/hhs/Redis/08-SortedSet精解.md b/hhs/Redis/08-SortedSet精解.md
index 919996a..357eb1d 100644
--- a/hhs/Redis/08-SortedSet精解.md
+++ b/hhs/Redis/08-SortedSet精解.md
@@ -7,7 +7,25 @@ create time: 2026-05-15 18:14
## 概述
-Sorted Set(有序集合)是 Redis 中**功能最丰富、场景最广泛**的数据结构。底层采用双编码策略:小集合(默认 ≤128 个元素,Redis 7.0+ 使用 listpack 替代 ziplist)用连续内存紧凑存储;超过阈值后自动切换为 **skiplist + hashtable** 组合,天然支持按分数排序和 O(log N) 的范围查询。本节聚焦实战中常用的高级模式。
+> [!TIP] 一句话理解 Sorted Set
+> 想象一个**自动按分数排序的排行榜**:你往里扔一个"成员 + 分数"的组合,Redis 帮你从小到大排好。你随时可以问"第 N 名是谁""分数在 80~100 之间的有哪些""某个成员排第几"——全部 O(log N) 搞定。
+>
+> 这就是 Sorted Set 的本质:**每个成员都有一个分数(score),Redis 自动按分数排序,且成员不重复**。
+
+Sorted Set(有序集合)是 Redis 中**功能最丰富、场景最广泛**的数据结构。底层采用双编码策略:小集合(默认 ≤128 个元素,Redis 7.0+ 使用 listpack 替代 ziplist)用连续内存紧凑存储;超过阈值后自动切换为 **skiplist + hashtable** 组合,天然支持按分数排序和 O(log N) 的范围查询。
+
+```mermaid
+graph LR
+ A["Sorted Set"] --> B["score 排序"]
+ A --> C["member 唯一"]
+ A --> D["O(log N) 范围查询"]
+ B --> E["排行榜"]
+ B --> F["延迟队列"]
+ C --> G["去重计数"]
+ D --> H["Top-N 聚合"]
+```
+
+本节聚焦实战中常用的高级模式——你会发现,很多看似不同的业务需求,本质上都是「**带分数的排行榜**」。
## 排行榜系统
@@ -64,18 +82,40 @@ flowchart TD
### 多维度排行榜技巧
-同一份数据需要按不同维度排名时,不要用多个 ZSet——用 **score = base + fraction**:
+> [!QUESTION] 思考一下
+> 游戏排行榜需要先按**胜场**排名,胜场相同时再按**击杀数**排。Sorted Set 只有一个 score 字段,怎么同时表示两个维度?
+
+答案是:把两个维度"打包"成一个浮点数——**score = 主维度 × 基数 + 次维度**。
```
总分 = 胜场 × 10000 + 击杀数
例: 5胜3杀 → score = 50003
-→ 先比胜率,再比击杀数,完美映射为单浮点数
+ ↑高位决定主排序 ↑低位决定次排序
```
+为什么能行?因为浮点数比较是**先比高位再比低位**的——这和我们日常说"先看总分,总分一样看小分"是一回事。只要次维度的最大值不超过基数(这里是 10000),就不会"进位"干扰主维度。
+
+> [!TIP] 基数怎么选?
+> 基数 = 次维度的最大可能值 + 1。比如击杀数最大 9999,基数就取 10000。如果有第三个维度,可以再嵌套:`(主 × 10000 + 次) × 10000 + 第三`。
+
## 延迟队列
### 原理
+> [!QUESTION] 思考一下
+> 你有 1000 个定时任务分散在不同时间点需要执行——用户注册 5 分钟后发短信、订单 30 分钟后自动取消……怎么用 Redis 保证它们按时执行?
+
+思路很直观:**把"什么时候执行"存进 score,把"执行什么"存进 member**。消费者不断从 ZSet 里弹出 score 最小(即时间最早)的元素,如果它的 score ≤ 当前时间,说明到期了,执行它;否则放回去继续等。
+
+```mermaid
+flowchart LR
+ A["生产者: 提交任务"] -->|"ZADD score=执行时间"| B["ZSet: 延迟队列"]
+ B -->|"score最小的到期了?"| C{"消费者检查"}
+ C -->|"到期"| D["执行任务"]
+ C -->|"未到期"| B
+ D --> E["ZREM 移除"]
+```
+
利用 ZSet 的 score 字段存储执行时间戳,通过 `BZPOPMIN`(阻塞弹最低分)定时拉取到期的任务:
```bash
@@ -124,6 +164,14 @@ LPUSH processing_queue
## 滑动窗口去重计数
+> [!QUESTION] 思考一下
+> 统计"最近 1 分钟内有多少独立用户访问了首页"——你能想到几种方案?为什么 ZSet 是其中最简单直接的一种?
+
+核心思路可以用一句话概括:**把 ZSet 当成一个带时间戳的签到本**。每个用户来访时"签到"(score = 当前时间戳,member = 用户 ID),然后把超过 1 分钟的签到记录撕掉,剩下的就是窗口内的独立访客数。
+
+> [!TIP] 为什么 ZSet 天然去重?
+> 因为 ZSet 的 **member 是唯一的**——同一个用户多次访问,score 会被更新但不会产生重复记录。这比 List 或 Stream 省去了额外的去重逻辑。
+
ZSet 天然适合做**固定时间窗口内的去重计数**(如每分钟独立访客):
```bash
@@ -151,6 +199,24 @@ count, _ := rdb.ZCard(ctx, "page:home:uv").Result()
## 滑动窗口限流器
+> [!QUESTION] 思考一下
+> 限流器的核心问题:「在最近 N 秒内,这个用户最多只能发 100 个请求」。如果用**固定窗口**(按整秒切分),在窗口边界会发生什么?
+
+```mermaid
+graph TB
+ subgraph FW["固定窗口 — 按秒硬切"]
+ F1["0.9s 发 100 个"] --> F2["1.0s 窗口重置"]
+ F2 --> F3["1.1s 再发 100 个"]
+ F4["结果: 0.2s 内通过 200 个!"]
+ end
+ subgraph SW["滑动窗口 — 时间平滑"]
+ S1["任何时候都只看最近 1s"] --> S2["窗口内永远 ≤ 100"]
+ end
+ FW -->|"问题"| SW
+```
+
+这就是固定窗口的**边界突刺**问题:用户在窗口交界处 0.2 秒内发出 200 个请求,实际速率远超限制。滑动窗口通过"只看最近 N 秒"的方式平滑地解决了这个问题。
+
用 `ZREMRANGEBYSCORE` + `ZCARD` 实现经典的**固定窗口 / 滑动窗口限流**:
```bash
@@ -239,6 +305,11 @@ top20, _ := rdb.ZRevRangeWithScores(ctx, "global:top20", 0, 19).Result()
## Geo — 地理位置
+> [!QUESTION] 思考一下
+> 老板说:「加个附近 5 公里奶茶店的功能」。你手头只有 Redis,怎么做?
+
+其实 Geo 就是 **Sorted Set 的语法糖**——底层把经纬度编码成一个 52 位 GeoHash 数字作为 score,成员是地点名称。既然 score 是个数字,自然就能排序、比较距离、查找附近范围。所以 Geo 不是新数据结构,而是**把「找附近」这个常见需求包装成了更友好的 API**。
+
### 基本操作
```bash
@@ -290,6 +361,11 @@ SINTER interests:user:1 interests:user:2
### 加权推荐(ZSet 方案)
+> [!QUESTION] 思考一下
+> 两个人兴趣标签有重叠,但重叠程度不同。如果想给用户推荐「最志同道合」的人,怎么**量化匹配度**?
+
+思路:把每个标签赋予一个权重分数(比如用户对该话题的关注度),然后**把两个用户的兴趣 ZSet 做分数求和**——共同标签的分数会叠加,非共同标签保持原分。叠加后的分数越高,说明匹配度越强。
+
当每个标签有**权重分数**时,把标签转为 ZSet,利用 `ZUNIONSTORE` 计算匹配度:
```bash