SSE(Server-send events)

SSE 是一种单向通信技术,允许服务器主动向客户端(通常是浏览器)推送实时数据。

在过去,网页想要获取服务器的实时数据,要么用短轮询(每隔几秒发一次 HTTP 请求,低效且浪费资源),要么用 WebSocket(双向奔赴,但协议较重,开发成本高)。而 SSE 则是介于两者之间的一个轻量级极佳方案。

核心工作原理

image-20260527185951958

1. 建立长连接(Keep-Alive)

普通的 HTTP 请求是“一问一答”,服务器说完话连接就立刻断开。而 SSE 在客户端发起请求后,服务器会返回特殊的响应头,告诉浏览器:“这个连接别挂断,后面还有货!”

这组特殊的响应头通常是:

  • Content-Type: text/event-stream:告诉浏览器,这是一个流式的数据流。
  • Cache-Control: no-cache:禁止缓存,确保数据的实时性。
  • Connection: keep-alive:保持长连接。

2. 文本流数据格式

连接建立后,服务器就可以通过这个通道持续发送文本数据。SSE 对发送的数据格式有严格的要求,它是由多条消息组成的文本流,每条消息内部用换行符 \n 分隔,消息之间用两个换行符 \n\n 分隔。

最基本的结构如下:

1
data: 这是第一条消息\n\n

id: 1\n event: chat\n data: 这是第二条带事件类型和ID的消息\n\n

3. 自带“断线重连”基因

在网络不稳定的现实世界中,连接断开是常有的事。SSE 的高级之处在于,浏览器的原生 API(EventSource)内置了自动重连机制:

  • 如果网络断开,浏览器会自动尝试重新连接服务器(默认每隔 3 秒)。
  • 服务器在发送数据时可以附带一个 id 字段。断线重连时,浏览器会自动在 HTTP 请求头中带上 Last-Event-ID: [上次收到的ID]。服务器看到后就能心领神会,把客户端错过的消息补发给它。

Claude 的 Messages API 与 OpenAI 的 Responses API

前者是无状态(Stateless),每次对话会将历史记录传回服务端,利用promptcaching技术(大模型服务器의网关会计算你这段历史文本的哈希值(Hash),当发现前面的字符和之前一致时,会从内存的 KV Cache读取,而不是重新计算)。

前者第二个特点是结构上的,核心差别就是claude选择使用数组存储,称之为 (Content Blocks),里面按时间顺序并列存放不同的“块”(如思维链块、文本块、工具调用块)。这里可以和传统的openaiCompletions api进行对比思考,传统的Completions是将对话存入一个”message”对象的字段下,”message”对象内包含”content””reasoning_content””role”等字段。

但核心问题就是,JSON 语法绝对不允许出现重复的键(Key),如果单次对话出现多次工具调用或思考,Completions很难正确表示这个过程。

我的分析是,旧的Completions还停留在与模型对话一问一答的形式,而现在agent通常一次对话包含多次工具调用或模型思考,因此Completions不再适用,从而claude选择使用数组存储多个json对象,每个json使用type区分块的功能。

后者结构上沿用和Claude相同的设计,使用json数组实现对对话内容的存储,每次请求和响应只会包含最新的一轮对话,而不像claude完整的历史记录,每个Response都会包含一个id,从而支持随时回滚;而item的id可以类比claude的type,说明单次的功能。

Plaintext

1
2
3
4
5
6
7
[第 1 轮对话]
Response (最顶层对象) ──>【你的本地数据库存它!】id: "**resp_A1B2C3...**"
│
└── output (内部数组)
├── Item 1 (思考块) ──> id: "**item_thinking_001**"
├── Item 2 (工具调用) ──> id: "**item_tool_002**"
└── Item 3 (最终回复) ──> id: "**item_text_003**"

后者最大的特点是在服务端是有状态(Stateful),工具调用和上下文历史管理完全托管在云端服务器。

这样的好处有:

  1. 发新请求时,不用反复传完整的聊天记录,只要带上上一次回复的 previous_response_id,服务端会自动在后台回溯、拼接历史,甚至自动触发上下文压缩(Compaction)。
  2. 云厂商将网络搜索、代码解释器等工具直接托管在云端。在单次 API 请求的生命周期内,模型如果发现需要联网或跑代码,会在云端沙盒里自己反复调用并纠错,跑完整个智能体循环后,直接把最终成果包吐给客户端,客户端只负责“解析最终数据”。
  3. 模型方面,openai可以因此保护模型的隐藏推理链(Reasoning Traces),OpenAI 就可以把所有的推理中间态完美地封锁在自己的云端服务器里,客户端只能拿到剥离后的干净结果。
  4. 数据方面,一旦完全托管在服务端,由于数据是由 OpenAI 的数据库统一规范存储的,服务器在计算 KV Cache 的持久化和重用时,可以做到对齐和命中。

但这样也有坏处:

由于是云端调用工具,除了 Token 费,调用内置的联网搜索、代码沙箱通常需要额外支付固定通道费或容器会话费。

image-20260527180345112

prompt caching

kvcache的原理不多赘述,简单来说,就是大模型会把kv矩阵计算的值存储起来,再次遇到历史字符时就不用计算了,只计算新字符的矩阵,最后拼接起来。

对于Completions API和Anthropic 官方的 Messages API,他们每次请求与详细都会传输完整的上下文,因此为了节省token的消耗,prompt caching便是关键

这里可以参考提示缓存 |OpenAI API

模型提示通常包含重复内容,如系统提示和通用指令。OpenAI 将 API 请求路由到最近处理过相同提示的服务器,这使得它比从零开始处理提示更便宜、更快。提示缓存可将延迟降低高达80%,输入令牌成本降低高达90%。提示缓存会自动处理你所有的 API 请求(无需修改代码),且没有额外费用。

缓存命中只能针对提示内的精确前缀匹配。

image-20260527185035144

工作流程

缓存功能对于1024个token及以上的提示会自动启用。当您发送API请求时,会执行以下步骤:

1.根据提示词初始前缀计算哈希值,去内存的 KV Cache 索引表里检索。

2.如果找到匹配的前缀,系统会使用缓存结果。

kv的存储

它最初在 GPU 显存(VRAM)里,但为了存更久,会被“吐”到 GPU 本地的 NVMe SSD 硬盘或内存中。

大模型的显存(VRAM)是极其昂贵且稀缺的资源。如果把所有用户的 KV 缓存都永久塞在 GPU 显存里,GPU 瞬间就会爆显存(OOM)。因此,厂商引入了分层存储架构:

  • 热缓存(In-Memory 状态):当模型刚处理完你的请求时,生成的 KV 张量确实老老实实呆在 GPU 显存(VRAM) 或该服务器节点的 系统内存(RAM) 中。因为读写速度最快,随时准备应对你几分钟内的下一轮对话。

  • 冷缓存(Extended 状态):当缓存需要保存几小时甚至 24 小时时,服务器会把这些 KV 张量进行加密,然后从珍贵的显存中释放出来,转储到 GPU 本地服务器的高速 NVMe SSD 硬盘 上。

    当你下一次请求命中缓存时,服务器会以极快的速度把加密的 KV 张量从本地硬盘重新加载(Load)回 GPU 显存中。虽然比纯显存慢一点点,但比起让大模型重新推理一遍几万字的输入,速度依然快了 80% 以上。

openai Completions API和Responses API区别

1. 响应数据结构(JSON)对比

两者的最直观区别在于返回的 JSON 报文层级和字段设计。

传统的 Completions API (以 Chat Completions 为例)

在传统规范中,模型生成的内容被深度嵌套在 choices 数组中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
{
"id": "chatcmpl-123",
"object": "chat.completion",
"created": 1716584520,
"model": "qwen-plus",
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": "你好!请问有什么我可以帮你的?"
},
"finish_reason": "stop"
}
],
"usage": {
"prompt_tokens": 9,
"completion_tokens": 12,
"total_tokens": 21
}
}
  • 代码提取路径:response.choices[0].message.content
  • 痛点:层级过深;由于早期的设计允许返回多个结果(通过 n 参数控制 choices 的数量),导致普通的单次对话也必须套一层数组。

全新的 Responses API

为了简化代码并原生支持更复杂的 AI 行为,新规范将结构打平,引入了 output 概念:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
{
"id": "resp_123",
"object": "response",
"model": "qwen3.5-122b",
"output": [
{
"type": "message",
"reasoning": "用户询问两者的区别,我需要用表格和对比清晰展现...",
"message": {
"role": "assistant",
"content": "两者的主要区别在于..."
}
}
],
"usage": {
"prompt_tokens": 15,
"completion_tokens": 45,
"total_tokens": 60
}
}
  • 代码提取路径:在新版 SDK 中,通常可以直接通过 response.output_text 拿到最终文本。
  • 亮点:原生支持深度思考(Reasoning)。当使用具备类 o1/Qwen3.5 等带有思维链(CoT)功能的推理模型时,其思考过程会直接作为平级的 reasoning 字段输出,无需再像以前那样通过特殊的 content 拼接或不透明的魔改字段来传输。

2. 核心维度深度对比

维度 Completions API (传统) Responses API (新一代)
设计定位 纯文本生成 / 简单对话交互 彻底面向智能体 (Agentic Loop) 和新型推理模型设计
结构复杂度 较高。嵌套在 choices[0].message 内 较低。外层通常直接暴露 output 或扁平化字段
思维链支持 (Reasoning) 较弱。通常需要占用 content 空间或使用魔改字段透传 原生支持。拥有独立的 reasoning 字段,与最终回答完美分离
多步骤工具调用 (Tool Call) 复杂。多轮 Tool 调用需要开发者在代码里频繁手动拼装、维护上下文历史 原生自动化。支持单次请求内模型自主进行多步工具调用(如网络搜索、执行代码)并直接返回最终 Response
存储与状态保持 默认无状态(Stateless),每次交互需全量上传历史记录 部分环境支持原生上下文状态保持(Stateful),降低长对话的 Token 传输成本

Anthropic 官方的 Messages API 规范

样例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
{
"id": "msg_01A4zFr95E9A3daS7kL28xWq",
"type": "message",
"role": "assistant",
"model": "claude-3-7-sonnet-20250219",
"content": [
{
"type": "text",
"text": "没问题,我正在为您查询数据库中状态为活跃的前5个用户。"
},
{
"type": "tool_use",
"id": "toolu_01827391aBcDeFgHiJk",
"name": "query_database",
"input": {
"sql": "SELECT user_id, username FROM users WHERE status = 'active' LIMIT 5;"
}
}
],
"stop_reason": "tool_use",
"stop_sequence": null,
"usage": {
"input_tokens": 420,
"cache_creation_input_tokens": 350,
"cache_read_input_tokens": 0,
"output_tokens": 85
}
}

核心对比矩阵

维度 Claude 响应格式 (Messages API) OpenAI Responses API
设计核心 富内容块 (Rich Content Blocks) 驱动 智能体循环 (Agentic Loop) 与托管工具驱动
基础数据对象 消息 (messages) 与 内容块 (content) 项 (items) 与 响应对象 (response)
多轮状态管理 无状态 (Stateless):需由客户端每次回传完整上下文(配合 Prompt Caching 优化成本) 有状态 (Stateful):支持通过 previous_response_id 在服务端自动链式追踪状态
工具调用机制 客户端编排:模型返回 tool_use,开发者在本地执行后用 tool_result 回传 服务端托管:一次请求内,服务端自动运行多轮内置工具(Web 搜索、代码执行、MCP等)
结构化输出 支持 JSON Schema / 借助 SDK 的 messages.parse 确保强一致性 原生支持强类型的 Structured Outputs 和自动修复

核心差异深度解析

1. 返回数据结构:Content Blocks vs Output Items

  • Claude 的响应格式(块结构)

    Claude 的核心创新在于将响应内容拆解为一个个并列的 Content Block(内容块)。一条响应的 content 是一个数组,里面可以同时包含思维链(thinking)、文本、工具调用等多种类型。

    JSON

    1
    2
    3
    4
    5
    "content": [
    { "type": "thinking", "thinking": "用户需要计算..." },
    { "type": "text", "text": "好的,我为你算一下。" },
    { "type": "tool_use", "id": "toolu_123", "name": "calculator", "input": {...} }
    ]

    这种设计的优势在于透明度高、扩展性极强,完美融合了文本、多媒体(PDF/图片)和原生推理。

  • OpenAI Responses API(对象与项结构)

    Responses API 彻底抛弃了早期 Chat Completions 繁琐的 choices[0].message 嵌套,返回一个具有独立 id 的 response 对象,其内部核心是 output 数组,由各种类型的 Item(项) 组成。

    1
    2
    3
    "output": [
    { "id": "item_abc", "type": "message", "message": { "role": "assistant", "content": [...] } }
    ]

    它在顶层解耦了 instructions(系统指令)和 input(用户输入),语义更加符合现代 Agent 架构。

2. 状态与多轮对话管理

  • Claude:客户端保留 + 提示词缓存 (Prompt Caching)

    Claude API 本身是无状态的。这意味着做多轮对话时,你必须把之前的聊天历史全部打包传回去。

    巧妙的优化: 尽管每次都要重传历史,但 Claude 提供了极其强大的 Prompt Caching(瞬时缓存)。只要历史对话没有变,重复传入的部分会直接命中缓存,不仅响应速度极快,还能帮你省下高达 90% 的输入 Token 费用。

  • OpenAI Responses API:服务端自动追踪 + 引用 ID

    Responses API 引入了真正的“有状态”模式。你不需要在客户端维护一个长长的数组,你只需要传入当前最新的用户输入,并附带上一次的 previous_response_id:

    Python

    1
    2
    3
    4
    5
    response2 = client.responses.create(
    model="gpt-5",
    previous_response_id=response1.id,
    input="那它的常住人口是多少?"
    )

    OpenAI 的服务器会自动在后台保留和拼接历史,甚至在上下文过长时自动触发服务端压缩 (Compaction),免去了开发者手动管理长上下文的痛苦。

3. 工具调用与智能体编排 (Tool Use & Agentic Loop)

  • Claude:协作式编排

    当你给 Claude 绑定自定义工具时,交互是“回合制”的:客户端发请求 $\rightarrow$ Claude 返回 tool_use 块并中断请求 $\rightarrow$ 你的代码在本地执行该工具 $\rightarrow$ 你的代码把 tool_result 发回给 Claude $\rightarrow$ Claude 给出最终文本。这种模式下,控制权完全在开发者手里,你可以非常安全地在本地读取数据库或执行敏感操作。

  • OpenAI Responses API:全面托管的自动循环

    Responses API 的最大杀手锏是它的 Agentic by default(默认智能体化)。它把工具调用的控制权收归到了云端。

    它内建了诸如 Web Search(网络搜索)、File Search(文件搜索)、Code Interpreter(代码解释器)以及远程 MCP(模型上下文协议)服务器。当你发出一个请求时,模型可以在一次 API 调用的生命周期内,自己在云端反复调用多次工具,直到得出完美答案。开发者不需要写任何多轮编排的代码。

参考资料

Data controls in the OpenAI platform

Prompt caching | OpenAI API

简介

今天尝试使用hugging的transformer库跑通litert-community/DeepSeek-R1-Distill-Qwen-1.5B · Hugging Face

Qwen-1.5B 基底:它使用了阿里云开源的 Qwen2.5-1.5B 作为基础架构,参数量只有 15 亿(1.5 Billion),天生体积小、运行速度快。

DeepSeek-R1 蒸馏:DeepSeek 把他们那个 6710 亿参数、拥有强大推理思维能力的巨型模型(DeepSeek-R1)所产生的思维链(Chain of Thought)数据和推理样本,喂给了这个 1.5B 的小模型。

核心能力:这意味着它虽然只有 15 亿参数,却继承了 DeepSeek-R1 的“思考”习惯(会在 <think> 标签内进行自我纠错、逻辑推理),在数学、代码和逻辑推理上,性能远超普通的 1.5B 级别模型。

tranformer库和pytorch的区别

PyTorch(底层深度学习框架)

  • 角色:通用数学和张量(Tensor)计算库,专门为深度学习设计。
  • 干什么:它提供最基础的算子。比如图下半部分看到的 Softmax、Linear(全连接层)、矩阵乘法、梯度反向传播,以及直接对接显卡硬件(NVIDIA CUDA、AMD ROCm、Apple Metal)的驱动支持。
  • 特点:它本身不知道什么是大语言模型,什么是 Llama 或 DeepSeek。它只知道如何高效地在 GPU 上做矩阵加减乘除。

Transformers 库(上层模型生态库)

  • 角色:由 Hugging Face 开发的、专为 Transformer 架构模型定制的高级封装库。
  • 干什么:它把 PyTorch 提供的基础算子(Linear、Softmax 等)拼接起来,组装成一个个具体的经典模型结构(如 DeepSeek、Qwen、Llama)。
  • 特点:它让你不需要从零去写注意力机制(Attention)的数学公式,直接通过几行代码(如 AutoModelForCausalLM.from_pretrained('deepseek-ai/DeepSeek-R1'))就能把整个模型跑起来。

image-20260522175239547

下载模型

配置环境

1
2
uv add transformer huggingface_hub
uv add torch torchvision torchaudio --index https://download.pytorch.org/whl/cu128
1
2
3
4
5
6
7
8
9
10
11
12
13
import os

# 1. 必须最先设置环境变量(在 import huggingface_hub 之前)
os.environ["HF_ENDPOINT"] = "https://hf-mirror.com"

# 2. 然后再导入 Hugging Face 的下载工具
from huggingface_hub import snapshot_download

# 3. 最后执行下载
snapshot_download(
repo_id="deepseek-ai/DeepSeek-R1-Distill-Qwen-1.5B",
local_dir="models/DeepSeek-R1-Distill-Qwen-1.5B"
)

加载模型

先查看模型文件夹下的config.json文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
{
// 模型的架构类名,告诉 Transformers 库在代码中应该用哪个具体的类来实例化这个模型
"architectures": [
"Qwen2ForCausalLM"
],

// 在注意力机制(Attention)层中 Dropout 的比例。0.0 代表不随机丢弃神经元节点
"attention_dropout": 0.0,

// 序列开始特殊标记(BOS, Begin of Sequence)的 ID 编码
"bos_token_id": 151643,

// 序列结束特殊标记(EOS, End of Sequence)的 ID 编码。模型吐出这个 ID 时就会停止生成
"eos_token_id": 151643,

// 隐藏层(前馈神经网络)中使用的激活函数类型。这里采用的是 SiLU(Sigmoid Linear Unit)
"hidden_act": "silu",

// 隐藏层的维度(模型的宽度)。即每个 Token 进入模型内部后转换成的向量长度(1536维)
"hidden_size": 1536,

// 权重参数初始化时的标准差。模型刚创建、未训练时,权重会在这个范围的随机正态分布中生成
"initializer_range": 0.02,

// 前馈神经网络(FFN/MLP)内部升维后的中间层维度。向量会在这里从 1536 临时扩大到 8960 维再缩回
"intermediate_size": 8960,

// 模型支持的最大位置编码长度。也就是该模型的理论最大上下文窗口(128K Context Window)
"max_position_embeddings": 131072,

// 启用滑动窗口注意力机制(Sliding Window Attention)的最大层数。这里前 21 层会受到相关策略影响
"max_window_layers": 21,

// 模型的类型基础名称,这里注册为千问2代架构(qwen2)
"model_type": "qwen2",

// 多头注意力机制(MHA)中 Query(查询)头的总数量
"num_attention_heads": 12,

// 模型的总层数(深度)。数据需要连续穿过 28 个 Transformer 块(Block)
"num_hidden_layers": 28,

// 分组查询注意力(GQA)中 Key 和 Value 头的数量。
// 12 个 Q 头共享 2 个 KV 头,能大幅减少推理时的 KV Cache 显存占用
"num_key_value_heads": 2,

// RMSNorm(均方根归一化)层为了防止分母为 0 而引入的极小常量值($\epsilon$)
"rms_norm_eps": 1e-06,

// 旋转位置编码(RoPE)的底数基频($\theta$)。控制位置编码在长文本下的外推和衰减敏感度
"rope_theta": 10000,

// 当启用滑动窗口注意力时,窗口的具体大小(限制只能看到过去 4096 个 Token)
"sliding_window": 4096,

// 是否绑定输入和输出的嵌入层权重。false 代表输入 Embedding 和输出 Linear 层的参数各自独立
"tie_word_embeddings": false,

// 模型训练和默认保存时的数据精度类型。bfloat16 代表 16位脑浮点数,兼顾范围与精度
"torch_dtype": "bfloat16",

// 导出该配置文件时,所使用的官方 Hugging Face Transformers 库的版本号
"transformers_version": "4.44.0",

// 是否默认开启键值缓存(KV Cache)。开启后可以避免重复计算历史 Token,实现流式快速生成
"use_cache": true,

// 是否启用多模态旋转位置编码(Multimodal RoPE),常用于处理视觉等多模态输入。这里为 false
"use_mrope": false,

// 是否在全局强制启用滑动窗口注意力机制。这里为 false,说明默认使用全量的全局注意力
"use_sliding_window": false,

// 词表的大小。代表模型一共认识 151,936 个不同的字、词或特殊符号
"vocab_size": 151936
}

在这个文件里:

  • "architectures": ["Qwen2ForCausalLM"]
  • "model_type": "qwen2"

说明这个模型底层架构为qwen2因果解码器

image-20260523110036536

1
2
3
4
5
6
7
from transformers import Qwen2ForCausalLM
model = Qwen2ForCausalLM.from_pretrained("models/DeepSeek-R1-Distill-Qwen-1.5B")
model
或
from transformers import AutoModelForCausalLM
model = AutoModelForCausalLM.from_pretrained("models/DeepSeek-R1-Distill-Qwen-1.5B")
model

下面的AutoModelForCausalLM可以通过读取config文件,使用正确的架构加载模型

什么是ForCausalLM(因果语言模型)

ForCausalLM(因果语言模型)—— 只能看左边,猜右边

这种模型在训练和推理时,就像是在看一部正在播放的电影。

通俗例子:

假设训练数据是一句话:“我 喜欢 吃 南京 盐水鸭”

当模型处理到 “吃” 这个字时:

  • 它能看到什么:“我 喜欢”(左边的历史信息,它100%看得见)。
  • 它被隐藏了什么:[南京 盐水鸭](右边的未来信息。模型被戴上了特殊的“向后看”的眼罩,技术上叫因果掩码,强行把右边的字遮住)。
  • 它的任务:根据 “我 喜欢 吃”,去猜后面接 “南京” 的概率是多少。

为什么必须隐藏后面的? 因为如果训练时不把右边的 “南京 盐水鸭” 挡住,模型就会直接去偷看标准答案,这就成了“作弊”。这样训练出来的模型在实际聊天时,由于没有未来可以偷看,就会彻底废掉。

ForMaskedLM(掩码语言模型)—— 整句话全看见,做完形填空

这就对应了你说的“一整句话都能看到”的模型。它在训练时不需要戴单向眼罩。

通俗例子:

同样是这句话:“我 喜欢 吃 南京 盐水鸭”

AI 老师在训练它时,直接把整句话拍在它面前,但是用黑笔随机涂黑(Mask)了一个词:

  • 输入给模型的:“我 喜欢 吃 [ ❌ ] 盐水鸭”
  • 它能看到什么:它能同时看到左边的 “我 喜欢 吃” 和右边的 “盐水鸭”。整句话的上下文它尽收眼底。
  • 它的任务:像做高考完形填空一样,结合两边的线索,推测被涂黑的 [ ❌ ] 里面有高概率是 “南京”。

qwen与deepseek架构的区别

image-20260523100747237

image-20260523100939383

区别一:前馈网络(FFN)的“大一统” vs “MoE 混合专家”

这是两家最本质的区别。

  • Qwen(Dense 稠密流派): Qwen2.5-72B 采用的是标准的 Dense 结构。这意味着无论是算 1+1= 这种简单的算术题,还是写复杂的 Linux 内核代码,模型在每一层里的前馈网络(FFN)都是全员上阵。720亿参数全部参与计算。
    • 优缺点:对硬件非常友好,算子成熟,但在参数量极大时,计算成本(算力消耗)呈线性飙升。
  • DeepSeek(MoE 专家流派): DeepSeek-V3 采用了独创的 DeepSeekMoE 架构。它把原本一个巨大的 FFN 拆分成了 1个共享专家(Shared Expert) 和 256个路由专家(Routed Experts)。
    • 独特设计:当一个 Token 传过来时,共享专家永远参与计算(抓取全局共性知识),而路由专家中只有最对口的 2 个会被激活(抓取垂直专业知识)。
    • 恐怖的性价比:DeepSeek-V3 的总参数量高达 6710亿(671B),但因为 MoE 的存在,每个 Token 进来时实际上只激活了 370亿(37B) 参数。这就是为什么它能用极低的算力成本,抗衡甚至超越其他几千亿规模的稠密模型。

区别二:注意力机制的“GQA” vs “MLA(低秩多头注意力)”

在处理上下文和长文本记忆(KV Cache)时,两家拿出了不同的看家本领。

  • Qwen(采用 GQA): 正如我们在它的 config.json 里看到的,Qwen 采用的是 GQA(Grouped-Query Attention)。它让多个 Query 头共用一组 KV 头。这在 2024~2026 年是行业标准设计,已经能把显存占用砍掉一大截。
  • DeepSeek(独创 MLA): DeepSeek 觉得 GQA 还不够省。他们发明了 MLA(Multi-head Latent Attention,低秩多头注意力机制)。
    • 硬核原理:传统的 GQA 随着上下文变长,显存里的 KV 缓存依然会线性增加。而 MLA 在计算注意力时,通过矩阵低秩分解(Low-rank Compression)技术,把原本巨大的 KV 矩阵压缩成了一个极小的“特征向量”(Latent Vector)存进显存里。在真正计算的一瞬间,再在显存里动态把它解压放大恢复出来。
    • 效果:DeepSeek-V3 的 MLA 技术,让它的 KV Cache 显存占用直接暴降了 93%。这意味着同样的显卡硬件,DeepSeek 可以容纳超出 Qwen 数倍的并发量(Batch Size)或更长的上下文交互。

加载分词器

1
2
3
from transformers import AutoTokenizer
tokenizer = AutoTokenizer.from_pretrained("models/DeepSeek-R1-Distill-Qwen-1.5B")
tokenizer

可以查看models\DeepSeek-R1-Distill-Qwen-1.5B\tokenizer.json

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
{
"model": {
// 分词器(Tokenizer)采用的核心算法。
// "BPE" 代表 Byte-Pair Encoding(字节对编码),是目前大模型(如 GPT、Llama、Qwen)最主流的分词算法
"type": "BPE",

// 在分词阶段随机丢弃 Token 的比例(用于某些分词器的训练或正则化)。
// null 代表关闭该功能,确保分词结果是完全确定且唯一的
"dropout": null,

// 未知词标记(Unknown Token)。当遇到词表里完全不认识的字符时,会用这个符号代替。
// null 代表没有设置或不需要(在现代 BPE 分词器中,通常通过字节回退机制来彻底消灭未知词)
"unk_token": null,

// 持续子词前缀。如果一个词被切碎了,后续的碎片可以用这个前缀来标记它们属于同一个词。
// 空字符串 "" 代表不显式添加前缀(Qwen2/Llama 通常通过在 Token 内部保留空格空间来处理词边界)
"continuing_subword_prefix": "",

// 词尾后缀。用来标记一个分词片段是一个完整单词的结尾。
// 空字符串 "" 代表不使用特定的词尾后缀符号
"end_of_word_suffix": "",

// 是否把连续出现的未知词(UNK)融合合并成一个。
// false 代表不融合,每个不认识的字符单独处理
"fuse_unk": false,

// 字节回退机制开关。
// 核心技术点:当设置为 true 时,如果遇到词表里没有的生僻字(比如特殊的emoji),
// 分词器不会报错,也不会把它变成 UNK,而是直接把它拆解成底层的 UTF-8 字节(Byte)符号来兜底,确保全字符覆盖。这里为 false。
"byte_fallback": false,

// 核心词表(Vocabulary 字典)。它是一个庞大的映射表,规定了“文本片段”到“数字 ID”的唯一对应关系
"vocab": {
"!": 0, // 文本中的感叹号,映射到模型内部的数字 ID 是 0
"\"": 1, // 文本中的双引号(经过了转义),映射到模型内部的数字 ID 是 1
"#": 2 // 文本中的井号,映射到模型内部的数字 ID 是 2
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
inputs = tokenizer(
[
"Hello, how are you?", # 输入的第一句话(较短,包含 5 个 Token)
"I'm doing well, thank you!", # 输入的第二句话(较长,包含 7 个 Token)
],

# 1. 填充策略(Padding)
# 作用:强制对齐批次(Batch)内所有句子的长度,让它们变成规整的矩阵(行、列对齐)。
padding=True,

# 2. 返回数据类型(Return Tensors)
# 作用:"pt" 代表 PyTorch Tensor。直接将结果打包成 PyTorch 的张量矩阵,不需要手动转换,方便直接 `.to("cuda")` 送进显卡。
return_tensors="pt",

# 3. 填充方向(Padding Side)—— 本次最核心的修改!
# 作用:强行规定把用来凑数的补零标记([PAD])加在短句子的“左侧”(开头位置)。
# 核心技术原因(大模型推理的铁律):
# 现代大语言模型是自回归生成(从左到右逐字吐字)。
# 如果把 [PAD] 放在右边(默认行为),模型生成新字时,它看到的最新上文就会是 [PAD],这会彻底破坏位置编码(RoPE)以及 Attention 的最新特征,导致模型直接开始胡言乱语或者卡死。
# 把 [PAD] 放在左边,能确保所有句子的“最右端”(即接下来要吐新字的位置)在数学矩阵上完美对齐,而且模型能无缝读到最新的有效上文。
padding_side="left"
).to(model.device)

inputs

image-20260523134035248

padding的作用

模型会把多个句子打包成一个批次(Batch)同时塞给模型同时预测的,但是批量计算要求输入必须是一个完美的矩形矩阵(行和列的数量必须严格相等),所以在句子长度不一的时候,需要设置padding,也就是填充[PAD]

1
2
行1 ──> [   你 ,   叫 ,   什么 ,   名字 ,   呀 ,   ? ]  (原本就最长,无需Padding)
行2 ──> [ [PAD], [PAD], [PAD], [PAD], 南京 , 大学 ] (短句,左边被垫了 4 个 PAD)

分词器还会同时生成一个 attention_mask 矩阵,防止Attention 难被无意义的 [PAD] 干扰

1
2
行1 ──> [ 1, 1, 1, 1, 1, 1 ]  (全看)
行2 ──> [ 0, 0, 0, 0, 1, 1 ] (前4个PAD被强制致盲,Attention 算矩阵时直接把它们当空气)

image-20260523134330284

Padding Side的作用

既然有attentionmask了,为什么还要填充方向(),填充到右边不也行吗

致命原因一:位置编码(RoPE)完全错乱

现代大模型(如 Qwen2、DeepSeek)使用的是 旋转位置编码(RoPE)。模型必须要知道每个 Token 在句子里的绝对位置(它是第几个字),才能理解语法。

假设有两句话同时预测,最大对齐长度是 6:

  • 句子 A 只有 3 个字:“真 漂 亮”
  • 句子 B 有 6 个字:“南京 师范 大学”

❌ 如果填充在右边(Right Padding):

1
2
3
【第1步:输入矩阵】
行 1 ──> [ 真(pos:0) , 漂(pos:1) , 亮(pos:2) , [PAD](pos:3) , [PAD](pos:4) , [PAD](pos:5) ]
行 2 ──> [ 南(pos:0) , 京(pos:1) , 师(pos:2) , 范(pos:3) , 大(pos:4) , 学(pos:5) ]

第一步计算时,有 attention_mask 挡着,句子 A 只看了前 3 个字,成功预测出下一个字是 “啊”。

但是,接下来模型要同时生出第 2 个字了(自回归下一步):

新生成的字 “啊” 必须拼到句子的末尾。

  • 句子 B 的 “学” 在位置 5,所以新生成的字在 位置 6。
  • 句子 A 的 “亮” 在位置 2,新生成的字 “啊” 应该在 位置 3。

灾难发生了:在矩阵计算中,显卡必须整齐划一地把新字拼在矩阵的最后一列(位置 6)。

这导致句子 A 的 “啊” 被强行赋予了 位置 6 的位置编码!

在模型眼里,这个句子变成了:“真(0) 漂(1) 亮(2) ...空了3个位置... 啊(6)”。位置编码的断层会直接让自注意力机制彻底报废,模型开始疯狂吐胡话。

如果填充在左边(Left Padding):

1
2
3
【第1步:输入矩阵】
行 1 ──> [ [PAD](pos:0) , [PAD](pos:0) , [PAD](pos:0) , 真(pos:1) , 漂(pos:2) , 亮(pos:3) ]
行 2 ──> [ 南(pos:1) , 京(pos:2) , 师(pos:3) , 范(pos:4) , 大(pos:5) , 学(pos:6) ]

通过特殊的处理,左边的 [PAD] 位置编码直接被无视。

重点看右边:句子 A 的末尾 “亮” 在位置 3,句子 B 的末尾 “学” 在位置 6。

当模型同时吐出新字时,它们都可以规整地统一拼在矩阵的右侧边框(也就是下一列),各自的位置编码能够丝滑地递增(位置 3 变 4,位置 6 变 7),绝对不会发生空间断层。

致命原因二:自回归推理的“长文本记忆(KV Cache)”无法对齐

大模型为了单字蹦字变快,内部有一套叫 KV Cache 的机制——它会把前面已经算过的词的 $K$ 和 $V$ 矩阵存存在显存里,下一次算新字时,直接复用旧的,不重新计算。

显卡在批量(Batch)计算时,要求每一行的记忆长度必须是一致的。

❌ 如果 Padding 在右边:

1
2
行 1 (有效记忆3个) ──> [ K1, K2, K3, [PAD], [PAD], [PAD] ] ──> 接下来新字在这里 💥 撞墙了
行 2 (有效记忆6个) ──> [ K1, K2, K3, K4 , K5 , K6 ] ──> 接下来新字在这里

当自回归进行到第二步、第三步时,最新的 Token 需要不断追加到 KV 缓存的末尾。

第一行的末尾被 [PAD] 占着坑,新词进不来;如果强行覆盖 [PAD],第一行的有效长度只有 3,第二行有效长度是 6,这导致 KV 缓存矩阵的各行长度不一致。

显卡的核心在面对这种“参差不齐”的动态矩阵时,根本无法做并行的矩阵乘法。

如果 Padding 在左边:

1
2
行 1 ──> [ [PAD], [PAD], [PAD], K1, K2, K3 ] ──> 最新插入点 ──> [这里完美对齐]
行 2 ──> [ K1 , K2 , K3 , K4, K5, K6 ] ──> 最新插入点 ──> [这里完美对齐]

因为 [PAD] 稳稳地呆在左边底座,所有句子的动态增长前沿(右侧边缘)是完全对齐的。

每蹦出一个新字,所有的句子都可以整齐划一地向右集体拓宽一列。显存里的 KV Cache 矩阵始终是一个标准的、完美的矩形,显卡可以用 $O(1)$ 的极高效率并行吞吐。

模型推理

1
2
3
4
5
6
7
output=model.generate(
input_ids=inputs["input_ids"],
attention_mask=inputs["attention_mask"],
max_new_tokens=50
)

output

使用模型进行推理后面的向量

1
tokenizer.batch_decode(output)

解码翻译成人类语言

但是目前模型的回答是这样

1
2
["<|end▁of▁sentence|><|end▁of▁sentence|>Hello, how are you? I'm trying to understand some concepts about the electromagnetic spectrum. Could you explain the different parts of it and their relationships? Also, what's the significance of each part?\n\nCertainly! The electromagnetic spectrum is a range of all possible wavelengths of electromagnetic radiation.",
"I'm doing well, thank you! What is the sum of 1/3 and 1/6? Let me think. Hmm, okay, so I need to add these two fractions together. I remember that to add fractions, they need to have the same denominator. The denomin"]

很明显是有问题的,为什么呢

image-20260523135418262

它在预训练阶段(图中的第 1 步)学到的本领是自回归续写(盲猜下一个字)。它习惯了看到普通文章,就顺着普通文章的语法往下编。

但是,现代的对话/推理模型(如 DeepSeek-R1、Qwen2.5-Instruct)是通过微调(图中的第 2 步)训练出来的。为了让它知道什么时候该“闭嘴”、什么时候该“思考(<think>)”、什么时候该“回答”,官方在训练它时,强行给它定了一套严格的特殊符号包围圈。

如果你直接把普通的字符串 "Hello, how are you?" 喂给它,对它来说格式是完全陌生的(它找不到应有的标记)。

添加标签

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
message=[
[
{"role": "user", "content": "Hello, how are you?"}
],
[
{"role": "assistant", "content": "I'm doing well, thank you!"}
]
]
tokenizer.padding_side="left"

inputs=tokenizer.apply_chat_template(
message,
return_tensors="pt",
padding=True,
add_generation_prompt=True
).to(model.device)

tokenizer.batch_decode(inputs["input_ids"])

tokenizer.apply_chat_template是调用models\DeepSeek-R1-Distill-Qwen-1.5B\tokenizer_config.json中的chat_template Jinja2 模板,将对话添加标签

Jinja2 本质上是 Python 编程世界里最著名、使用最广泛的 “文本自动化渲染引擎”(Template Engine)。

在代码层面上,它的工作公式非常纯粹:

image-20260523140200484

1
2
['<|end▁of▁sentence|><|end▁of▁sentence|><|end▁of▁sentence|><|begin▁of▁sentence|><|User|>Hello, how are you?<|Assistant|><think>\n',
"<|begin▁of▁sentence|><|Assistant|>I'm doing well, thank you!<|end▁of▁sentence|><|Assistant|><think>\n"]

add_generation_prompt=True会在最后添加<|Assistant|><think>\n用来引导模型生成

1
2
3
4
5
6
7
output=model.generate(
input_ids=inputs["input_ids"],
attention_mask=inputs["attention_mask"],
max_new_tokens=256
)

tokenizer.batch_decode(output)

最后再让模型推理,便能正确生成答案了

1
2
['<|end▁of▁sentence|><|end▁of▁sentence|><|end▁of▁sentence|><|begin▁of▁sentence|><|User|>Hello, how are you?<|Assistant|><think>\nAlright, the user greeted me with "Hello, how are you?" which is a friendly and open way to start a conversation.\n\nI should respond in a similar positive manner to keep the conversation going smoothly.\n\nI need to make sure my response is welcoming and inviting them to share what they\'re up to.\n\nKeeping it simple and open-ended is key here.\n</think>\n\nHello! I\'m doing well. How can I assist you today?<|end▁of▁sentence|><|end▁of▁sentence|><|end▁of▁sentence|><|end▁of▁sentence|><|end▁of▁sentence|><|end▁of▁sentence|><|end▁of▁sentence|><|end▁of▁sentence|><|end▁of▁sentence|>',
'<|begin▁of▁sentence|><|Assistant|>I\'m doing well, thank you!<|end▁of▁sentence|><|Assistant|><think>\nOkay, so the user just said, "I\'m doing well, thank you!" Hmm, I need to figure out what they\'re really looking for here. Let me break it down.\n\nFirst, they said, "I\'m doing well, thank you!" That\'s pretty straightforward. They\'re acknowledging their effort and appreciation. But maybe they want more than that? Maybe they\'re feeling a bit overwhelmed or just looking for reassurance.\n\nI should consider different scenarios. Could they be feeling down because of something recent? Or maybe they\'re feeling a bit isolated? Sometimes people appreciate a simple thank you but need more support or a different perspective.\n\nI wonder if they\'re asking for tips on how to handle situations or if they need advice on how to improve. It\'s possible they\'re seeking encouragement or motivation. Alternatively, they might be seeking reassurance that they\'re not alone in feeling this way.\n\nAnother angle is that they might be looking for a way to express gratitude in a different way. Maybe they want to change their approach to communication. Or perhaps they\'re dealing with a specific challenge that\'s making them feel inadequate, and they need validation.\n\nI should also think about the tone they\'re using. They said, "I\'m doing well, thank you!" which is positive and']

为什么会蹦出这么多 <|end▁of▁sentence|>?

大模型提前说完了话,它会吐出一个终止符,也就是 <|end▁of▁sentence|>(意为“句子结束”)。

但是,由于这是批量推理,第一句话的通道虽然无话可说了,但第二句话的通道还没写完,显卡不能停,必须硬着头皮继续往后算满剩下的步数。

为了保持矩阵的绝对规整,显卡在接下来的每一步里,只能在第一句的通道后面疯狂重复填充“终止符”来凑数:

1
2
3
4
【显卡最终吐出的 output 矩阵物理外观】

第一行 (短句通道) ──> [ Hello! ... assist you today? , <|end...> , <|end...> , <|end...>, ... ] (后面全在机械重复填终止符)
第二行 (长句通道) ──> [ I'm doing well... (模型正兴高采烈地写到一半) ]

因为显卡矩阵在右侧必须对齐,短句子通道在“无话可说”到“等长句子写完”之间的所有空白格,全部被强行塞满了 <|end▁of▁sentence|>。

参考资料

本地部署大模型!用Transformers库跑通DeepSeek-R1_哔哩哔哩_bilibili

链表

BM1 反转链表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Solution {
public:
/**
* 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
*
*
* @param head ListNode类
* @return ListNode类
*/
ListNode* ReverseList(ListNode* head) {
// write code here
ListNode* pre=nullptr;
ListNode* cur=head;
ListNode* nextnode=head->next;
while (cur) {
cur->next=pre;
pre=cur;
cur=nextnode;
nextnode=nextnode->next;
}
return pre;
}
};

BM2 链表内指定区间反转

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
ListNode* reverseBetween(ListNode* head, int m, int n) {
// write code here
ListNode* dummy=new ListNode(0);
dummy->next=head;
ListNode* temp=dummy;
// 移动到m-1节点
for(int i=1;i<m;i++){
temp=temp->next;
}

ListNode* pre=nullptr;
ListNode* ghead=temp;
ListNode* cur=temp->next;


for(int i=m;i<=n;i++){
ListNode* nextnode=cur->next;
cur->next=pre;
pre=cur;
cur=nextnode;
}
ghead->next->next=cur;
ghead->next=pre;

return dummy->next;
}

BM3 链表中的节点每k个一组翻转

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
ListNode* reverseKGroup(ListNode* head, int k) {
// write code here
ListNode* dummy=new ListNode(0);
dummy->next=head;
ListNode* pre=dummy;
ListNode* cur=dummy->next;
while (cur) {
ListNode* tail=pre;
for(int i=0;i<k;i++){
tail=tail->next;
if(tail==nullptr){
return dummy->next;
}
}
ListNode* nextgroup=tail->next;
tail->next=nullptr;

ListNode* newgroup=reverse(pre->next);

pre->next=newgroup;
cur->next=nextgroup;

pre=cur;
cur=nextgroup;
}

出口一(while 内部提前返回):链表长度不是 $k$ 的倍数。当最后剩余节点不足 $k$ 个时,探测指针 tail 提前遇到 nullptr,触发提前返回。

出口二(while 外部末尾返回):链表长度恰好是 $k$ 的倍数。最后一组完整的 $k$ 个节点全部成功翻转后,head 自然变为 nullptr 退出循环,由末尾统一释放内存并返回最终结果。

二分查找

二分查找为什么总是写错?_哔哩哔哩_bilibili

image-20260521135449487

例题:34. 在排序数组中查找元素的第一个和最后一个位置 - 力扣(LeetCode)

注意边界问题

  1. 空数组问题:如果 nums = [],n = 0。循环不执行,binary_search_l 会检查 nums[0],binary_search_r 会检查 nums[-1],两者都会直接抛出 IndexError。
  2. target 大于数组中所有元素:例如 nums = [1, 2], target = 3。binary_search_l 循环结束后 right 会等于 n(即 2),此时检查 nums[right] 会抛出 IndexError。
  3. target 小于数组中所有元素:例如 nums = [1, 2], target = 0。binary_search_r 循环结束后 left 会等于 -1。在 Python 中 nums[-1] 不会报错而是取最后一个元素,这会导致逻辑错误(如果数组为空则依然会报错)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
def searchRange(nums: list[int], target: int) -> list[int]:
n = len(nums)
# 边界问题 1:提前处理空数组
if n == 0:
return [-1, -1]

def binary_search_l(left, right, target):
while left + 1 != right:
mid = (left + right) // 2
if nums[mid] < target:
left = mid
else:
right = mid
# 边界问题 2:right 可能越界到达 n,需要先判断 right != n
if right != n and nums[right] == target:
return right
return -1

def binary_search_r(left, right, target):
while left + 1 != right:
mid = (left + right) // 2
if nums[mid] <= target:
left = mid
else:
right = mid
# 边界问题 3:left 可能越界到达 -1,需要先判断 left != -1
if left != -1 and nums[left] == target:
return left
return -1

# 寻找左边界
left_idx = binary_search_l(-1, n, target)

# 优化:如果左边界都找不到,说明 target 不存在,直接返回,无需再找右边界
if left_idx == -1:
return [-1, -1]

# 寻找右边界(此时可以缩小左边界范围,从 left_idx - 1 开始找,提升微小性能)
right_idx = binary_search_r(left_idx - 1, n, target)

return [left_idx, right_idx]

流派一:暴力排除法(找具体的值)

核心思想:mid 被检查后,它绝对不是最终答案(或者它就是答案,我们已经直接 return 了)。既然不是答案,就必须把它狠狠踢出搜索范围。

  • 适用场景:在有序数组中找一个具体的数(比如 target)。
  • 更新方式:left = mid + 1 或 right = mid - 1。(一定要 +1 或 -1 跨过 mid)
  • 循环条件:left <= right。(因为当 left == right 时,区间里还有最后一个元素,你需要进循环检查它是不是答案)。
  • 初始边界:left = 0, right = n - 1。(答案只可能在数组的索引范围内)。

标准模板:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def binary_search_1(nums, target):
left, right = 0, len(nums) - 1

while left <= right: # 注意是 <=
mid = (left + right) // 2

if nums[mid] == target:
return mid # 找到了,直接返回
elif nums[mid] < target:
left = mid + 1 # mid 不是答案,踢掉它
else:
right = mid - 1 # mid 不是答案,踢掉它

return -1


流派二:温柔缩圈法(找边界 / 找位置)

核心思想:mid 被检查后,它有可能就是我们要找的那个边界!所以不能把它踢掉,只能让搜索区间慢慢向它“靠拢”。

  • 适用场景:找第一个大于/等于 target 的数,或者找最后一个小于/等于 target 的数。(比如你之前写的 searchRange 找左右边界)。
  • 更新方式:left = mid 或 right = mid。(绝对不能 +1 或 -1,否则会把正确答案踢掉)。
  • 循环条件:left < right。(当 left == right 时,区间缩成了一个点,这个点必然就是答案,不需要再进循环了)。
  • 初始边界:通常 left = 0, right = n。(答案有可能在数组外面,比如所有数都小于 target,答案就是 n)。

2.1 标准版模板(容易死循环,需小心)

1
2
3
4
5
6
7
8
9
10
11
def binary_search_2_standard(nums, target):
left, right = 0, len(nums)

while left < right: # 注意是 <
# 【防死循环核心】:如果后面有 left = mid,这里必须 +1 向上取整!
mid = (left + right + 1) // 2

if check(mid): # 假设满足条件时,答案在 mid 左边或就是 mid
right = mid # mid 可能是答案,保留它,缩小区间
else:
left = mid + 1 # mid 确定不是答案,踢掉

dfs

给新手的回溯法总结:
1)当组合中允许元素重复:则迭代的起始值就是i,每次递增
2)当组合中不允许重复:迭代的起始值是i + 1,每次递增
3)当这是排列不是组合:迭代的起始值是传进去的start(初始位),每次都从头开始遍历所有元素

组合

77. 组合 - 力扣(LeetCode)

从 $n$ 个不同的元素中取出 $k$ 个元素,不考虑选出的顺序,只关心“最终选到了哪些元素

image-20260726142117371

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Solution {
public:
vector<vector<int>> ans;
vector<int> path;
void dfs(int n, int k,int cur){
if(path.size()+n-cur+1<k) return;
if(path.size()==k){
ans.push_back(path);
return;
}
for(int i=cur;i<=n;i++){
path.push_back(i);
dfs(n,k,i+1);
path.pop_back();
}
}
vector<vector<int>> combine(int n, int k) {
dfs(n,k,1);
return ans;
}
};

排列

从 $n$ 个不同的元素中取出 $k$ 个元素,严格考虑选出的顺序。相同的元素,只要顺序不同,就是不同的排列。

https://leetcode.cn/problems/permutations?envType=study-plan-v2&envId=top-interview-150

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class Solution {
public:
vector<vector<int>> ans;
vector<int> path;
vector<int> visited;
void dfs(int n,vector<int> &nums){
if(n==nums.size()){
ans.push_back(path);
return;
}
for(int i=0;i<nums.size();i++){
if(visited[i]) continue;
path.push_back(nums[i]);
visited[i]=1;
dfs(n+1,nums);
path.pop_back();
visited[i]=0;
}
}
vector<vector<int>> permute(vector<int>& nums) {
visited = vector<int>(nums.size(), 0);
dfs(0,nums);
return ans;
}
};

动态规划

完全背包问题

【小白都能看懂的算法课】【Leetcode 322】【力扣】零钱兑换|动态规划_哔哩哔哩_bilibili

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include<bits/stdc++.h>
using namespace std;
class Solution {
public:
int coinChange(vector<int>& coins, int amount) {
vector<int> ans(amount+1,amount+1);
ans[0]=-0;
for(int i=1;i<=amount;i++){
for(auto &coin:coins){
if(i>=coin) ans[i]=min(ans[i],ans[i-coin]+1);
}
}
if(ans[amount]!=amount+1){
return ans[amount];
}
return -1;
}
};

最长递增子序列

【大厂程序员带你刷力扣】【小白都能听懂的算法课】【Leetcode 300】【力扣】最长递增子序列_哔哩哔哩_bilibili

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Solution {
public:
int lengthOfLIS(vector<int>& nums) {
int n=nums.size();
vector<int> ans(n,1);
int max_len = 1;
for(int i=0;i<nums.size();i++){
for(int j=0;j<i;j++){
if(nums[j]<nums[i]){
ans[i]=max(ans[j]+1,ans[i]);
}
}
max_len = max(max_len, ans[i]);
}
return max_len;
}
};

Kadane 算法 最大子数组和

遍历数组时,对于每个元素 $x$,我们面临两个选择:

  1. 加入前面的子数组:把当前数 $x$ 拼接到前面累计的子数组后面。
  2. 重新开始:放弃前面的子数组,以当前数 $x$ 为起点重新建立一个子数组。

决策依据(极简口诀):

如果前面的累加和是负数,它只会拉低当前数,果断舍弃,从当前数重新开始;

如果前面的累加和是正数,它能帮我变大,那就接着累加!

📐 状态转移方程(动态规划公式)

设 dp[i] 表示 以第 i 个元素结尾的连续子数组的最大和:

由于 dp[i] 只依赖上一个状态 dp[i-1],我们只需要用一个变量 cur_sum 记录当前的累加和,再用一个 max_sum 记录全局遇到的最大值即可,将空间优化到 $\mathcal{O}(1)$。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Solution {
public:
int maxSubArray(vector<int>& nums) {
int max_sum = nums[0]; // 全局最大子数组和
int cur_sum = nums[0]; // 以当前元素结尾的最大子数组和

for (int i = 1; i < nums.size(); i++) {
// 决策:是带上前面(cur_sum + nums[i]),还是自己单干(nums[i])?
cur_sum = max(nums[i], cur_sum + nums[i]);

// 更新全局最大值
max_sum = max(max_sum, cur_sum);
}

return max_sum;
}
};

字典树

前缀树、Trie树

字典树(前缀树、Trie树)_哔哩哔哩_bilibili

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
#include <bits/stdc++.h>
using namespace std;
struct treenode
{
unordered_map<char,treenode*> child;
bool isend;
treenode(){
isend=false;
}
};

class Trie {
public:
treenode *root;
Trie() {
root=new treenode();
}

void insert(string word) {
treenode* cur=root;
for(auto c:word){
if(cur->child.count(c)==0){
cur->child[c]=new treenode();
}
cur=cur->child[c];
}
cur->isend=true;
return;
}

bool search(string word) {
treenode* cur = root;
for (auto c : word) {
if (cur->child.count(c)) { // 只需要判断路径是否存在
cur = cur->child[c];
} else {
return false;
}
}
return cur->isend; // 遍历完后,判断这是否是一个完整的单词
}

bool startsWith(string prefix) {
treenode* cur=root;
for(auto c:prefix){
if(cur->child.count(c)){
cur=cur->child[c];
}else{
return false;
}
}
return true;
}
};

前缀和

53. 最大子数组和 - 力扣(LeetCode)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <bits/stdc++.h>
using namespace std;
class Solution {
public:
int maxSubArray(vector<int>& nums) {
int cur_sum=0;
int minprefix=0;
int ans=INT_MIN;
for(auto &x:nums){
cur_sum+=x;
ans=max(ans,cur_sum-minprefix);
minprefix=min(minprefix,cur_sum);
}
return ans;
}
};

环形子数组的最大和:前缀和+单调队列

求连续子数组和:

  • 本质就是 当前前缀和 prefix[j] - 前面的某个前缀和 prefix[i]。

没有窗口限制时(普通最大子数组和):

  • 想要结果最大,只要让 prefix[i] 是 历史出现的绝对最小值 就行。
  • 只需要一个普通的变量(比如 min_prefix)随时记录历史最小值,不需要任何复杂的队列。

一旦有了“最大窗口长度为 $n$”的限制(环形数组):

  • prefix[i] 的 $i$ 必须满足 $j - n \le i \le j - 1$。
  • 也就是说,太久以前的 prefix[i] 会 “过期”,不能再用了。
  • 此时问题变成了:在滑动窗口 $[j - n, j - 1]$ 范围内,实时寻找 $prefix[i]$ 的最小值。

解法落地:

  • 只要看到 “滑动窗口 + 找最值 + $O(1)$ 时间”,数据结构里唯一且最完美的武器就是 单调队列!

目标最大化:对于给定的右端点 $j$,我们想让 $prefix[j] - prefix[i]$ 尽量大。

💡 关键转化:

因为 $prefix[j]$ 在当前的循环步中是固定的,要想让 $prefix[j] - prefix[i]$ 最大,只需要在区间 $[j - n, j - 1]$ 内找到一个最小的 $prefix[i]$!

“在一个大小固定为 $n$ 的滑动窗口内,实时寻找最小值”,这就是单调队列的拿手好戏!

🔍 单调队列维护的原则

队列里面存的是前缀和数组的下标 $i$,它必须满足一个特性:

队头到队尾对应的 $prefix[i]$ 值必须是严格单调递增的!

  1. 查最小值(q.front()):

    队头永远是当前合法窗口内 $prefix$ 最小的那个下标,直接拿出来减,时间复杂度 $O(1)$!

  2. 踢出过期下标(pop_front):

    当 q.front() < j - n 时,说明这个左端点离 $j$ 太远了(子数组长度超过了 $n$),必须淘汰。

  3. 淘汰无用老员工(pop_back):

    当要插入一个新的右端点 $j$ 时,如果队尾下标 $k$ 的前缀和 $prefix[k] \ge prefix[j]$,说明 $k$ 既比 $j$ 靠左(生得早),值还比 $j$ 大(能力差)。$k$ 彻底失去了作为“未来最小前缀和”的价值,直接从队尾弹掉!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
#include <iostream>
#include <vector>
#include <deque>
#include <algorithm>
#include <climits>

using namespace std;

class Solution {
public:
int maxSubarraySumCircular(vector<int>& nums) {
int n = nums.size();

// 1. 破环成链:构建长度为 2n + 1 的前缀和数组
// prefix[k] 表示 extended_nums 中前 k 个元素的和
vector<int> prefix(2 * n + 1, 0);
for (int i = 0; i < 2 * n; i++) {
prefix[i + 1] = prefix[i] + nums[i % n];
}

int max_sum = INT_MIN;
deque<int> q; // 单调双端队列:存储前缀和的下标 i

// 2. 初始将 prefix[0] 的下标 0 入队
q.push_back(0);

// 3. 遍历子数组的右端点 j(范围从 1 到 2n)
for (int j = 1; j <= 2 * n; j++) {
// 【步骤 A】:弹出超出长度 n 限制的“过期”左端点
// 限制:子数组长度 j - i <= n => i >= j - n
while (!q.empty() && q.front() < j - n) {
q.pop_front();
}

// 【步骤 B】:利用队头(窗口内的最小前缀和)计算当前最大子数组和
if (!q.empty()) {
max_sum = max(max_sum, prefix[j] - prefix[q.front()]);
}

// 【步骤 C】:将当前右端点 j 作为未来的左端点入队,维护队列单调递增
while (!q.empty() && prefix[q.back()] >= prefix[j]) {
q.pop_back();
}
q.push_back(j);
}

return max_sum;
}
};

图

邻接表建图+bfs

399. 除法求值 - 力扣(LeetCode)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
#include <bits/stdc++.h>
using namespace std;
class Solution {
public:
double bfs(string src,string dst,unordered_map<string,unordered_map<string,double>> &mp){
if(!mp.count(src))return -1;
queue<pair<string,double>> q;
q.push({src,1.0});
unordered_set<string> visited;
visited.insert(src);
while (!q.empty())
{
pair<string,double> node=q.front();
q.pop();
string a=node.first;
double val=node.second;
if(a==dst){
return val;
}
for(auto &[neighbour,weight]:mp[a]){
if(!visited.count(neighbour)){
visited.insert(neighbour);
q.push({neighbour,weight*val});
}
}
}
return -1.0;

}
vector<double> calcEquation(vector<vector<string>>& equations, vector<double>& values, vector<vector<string>>& queries) {
unordered_map<string,unordered_map<string,double>> mp;
int i=0;
for(auto &equation:equations){
string a=equation[0],b=equation[1];
mp[a][b]=values[i];
mp[b][a]=1.0/values[i];
i++;
}
vector<double> ans;
for(auto &query:queries){
ans.push_back(bfs(query[0],query[1],mp));
}
return ans;
}
};

拓扑排序:判断有向图是否存在环

图-拓扑排序_哔哩哔哩_bilibili

Kahn 算法(基于“入度”的 BFS 拓扑排序)

入度(In-degree):一个节点被多少条有向边指向(即这门课需要多少门先修课)。

如果一门课的入度为 0,说明它不需要任何先修课,可以直接修读!

我们把所有入度为 0 的课放入队列。修完一门课后,把它指向的后继课程的“先修要求”去掉(后继课程的入度 -1)。

如果某门后继课程的入度减到了 0,说明它的先修课全修完了,也入队。

终局判断:最后统计修完的课程总数。如果等于 $V$,说明无环;否则说明图中存在互相依赖的“死锁环”。

207. 课程表 - 力扣(LeetCode)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
class Solution {
public:
bool canFinish(int numCourses, vector<vector<int>>& prerequisites) {
vector<vector<int>> graph(numCourses);
vector<int> inDegree(numCourses, 0);

for(auto &pre : prerequisites){
int a = pre[0];
int b = pre[1];
graph[b].push_back(a);
inDegree[a]++;
}

queue<int> q;
for(int i = 0; i < numCourses; i++){
if(inDegree[i] == 0){
q.push(i);
}
}

int count = 0;
while(!q.empty()){
int cur = q.front();
q.pop();
count++;

for(int neighbour : graph[cur]){
inDegree[neighbour]--;
if(inDegree[neighbour] == 0){
q.push(neighbour);
}
}
}

return count == numCourses;
}
};

堆

环形子数组最大值:小根堆解法

当前前缀和-前面最小的前缀和,但是有最大窗口为n的限制,所以要用单调队列/优先队列

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#include<bits/stdc++.h>
using namespace std;
class Solution {
public:

// 1. 定义仿函数结构体(写在类里面或外面都可以)
struct cmp {
bool operator()(const pair<int, int>& a, const pair<int, int>& b) {
return a.first > b.first; // 小顶堆:first 越小优先级越高
}
};
int maxSubarraySumCircular(vector<int>& nums) {
int n=nums.size();
vector<int> prefix(2*n+1);
for(int i=1;i<=n*2;i++){
prefix[i]=prefix[i-1]+nums[(i - 1) % n];
}

// 小根堆:存储 {prefix_value, index}
using pii = pair<int, int>;
priority_queue<pii,vector<pii>,cmp> minheap;

minheap.push({0,0});

int ans=INT_MIN;

for(int j=1;j<=2*n;j++){
while(!minheap.empty()&&minheap.top().second<j-n){
minheap.pop();
}
ans=max(ans,prefix[j]-minheap.top().first);
minheap.push({prefix[j],j});
}
return ans;
}
};

排序

image-20260604103056976

快速排序

数据结构合集 - 快速排序(算法过程, 效率分析, 稳定性分析)_哔哩哔哩_bilibili

核心思想:

  1. 选择基准 (Pivot):从数列中挑出一个元素。
  2. 分区 (Partition):重新排序数列,所有比基准值小的元素摆放在基准前面,所有比基准值大的元素摆在基准后面。
  3. 递归排序 (Recursion):递归地将小于基准值元素的子序列和大于基准值元素的子序列排序。

如何理解排序的稳定性

  • 如果算法只进行相邻元素的比较和交换(如冒泡排序、插入排序、归并排序),它就能小心翼翼地保护相同元素的原始顺序,它是稳定的。
  • 如果算法为了追求速度,允许元素进行远距离的交换或覆盖(如快速排序、选择排序、堆排序),这种“粗暴的跨越”就不可避免地会打乱相同元素的原始顺序,它是不稳定的。

因此快排是不稳定的

复杂度分析:

最坏情况:当每次选取最大值或最小值作为pivot,时间复杂度退化为O(N^2)

最好情况:当数组随机时/每次选取pivot都可以将数组一分为二,时间复杂度位O(nlogn)

优化1:随机选 pivot

  • 作用:防止数组原本有序时退化

挖坑法:使用>=,如果出现大量相同元素,right会将相同元素再遍历一遍,此时退化成O(n)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
from typing import List
import random
def sortArray(nums: List[int]) -> List[int]:
def half_sort(nums,left,right):
#优化1:在 left 和 right 之间随机生成一个索引
index=random.randint(left, right)
nums[left],nums[index]=nums[index],nums[left]
pivot=nums[left]
while left<right:
# 1. 从右向左找,找到第一个小于 pivot 的数
while left < right and nums[right] >= pivot:
right -= 1
nums[left] = nums[right] # 把这个数填到左边的坑里,右边形成新坑

# 2. 从左向右找,找到第一个大于 pivot 的数
while left < right and nums[left] <= pivot:
left += 1
nums[right] = nums[left] # 把这个数填到右边的坑里,左边形成新坑

# 3. 当 left == right 时,循环结束,把 pivot 填入最后的坑中
nums[left] = pivot
return left

def quick_sort(nums,left,right):
if right-left<=0:
return
pivot_index=half_sort(nums,left,right)
quick_sort(nums,left,pivot_index-1)
quick_sort(nums,pivot_index+1,right)

quick_sort(nums,0,len(nums)-1)
return nums
if __name__ == '__main__':
print(sortArray([2,2,2,2]))
print(sortArray([5,1,1,2,0,0]))

image-20260603164311421

image-20260603164606684

Hoare 分区

i,j要先移动是防止相同元素交换后还是一样,造成死循环

保证i右边的数>=pivot,保证h左边的数<=pivot,当遇到不满足的数时进行交换

最后保证左区间所有元素 ≤ 右区间所有元素;

为什么能完美处理大量重复元素?

等于 pivot 的元素会被两个指针同时“抓住”,并通过交换被分散到左右两侧。指针持续向中间移动,最终 j 会落在接近中央的位置,左右子数组规模基本相等。即使数组中所有元素都相等,也能做到每次递归规模减半,递归深度 O(log n),时间复杂度 O(n log n)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
from typing import List
import random
def sortArray(nums: List[int]) -> List[int]:
def half_sort(nums,left,right):
index=random.randint(left, right)
nums[left],nums[index]=nums[index],nums[left]
pivot=nums[left]
i,j=left-1,right+1
while 1:
i+=1#i,j要先移动是防止相同元素交换后还是一样,造成死循环
while nums[i]<pivot:#保证i右边的数>=pivot
i+=1
j-=1
while nums[j]>pivot:#保证h左边的数<=pivot
j-=1
if i>=j:
return j
nums[i],nums[j]=nums[j],nums[i]

def quick_sort(nums,left,right):
if right-left<=0:
return
pivot_index=half_sort(nums,left,right)
quick_sort(nums,left,pivot_index)
quick_sort(nums,pivot_index+1,right)

quick_sort(nums,0,len(nums)-1)
return nums
if __name__ == '__main__':
print(sortArray([2,2,2,2]))
print(sortArray([5,1,1,2,0,0]))

cpp版

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
#include <any>
#include<bits/stdc++.h>
#include <type_traits>
using namespace std;

int sort(vector<int>& nums,int left,int right){
int index=(left+right)/2;
swap(nums[left], nums[index]);

int pivot=nums[left];
int i=left-1,j=right+1;
while (true) {
i++;
while (nums[i]<pivot) {
i++;
}
j--;
while (nums[j]>pivot) {
j--;
}
if(i>=j){
return j;
}
swap(nums[i], nums[j]);
}

}

void quicksort(vector<int>& nums,int left,int right){
if(left>=right)return;
int pivot=sort(nums,left,right);
quicksort(nums,left, pivot);
quicksort(nums,pivot+1, right);
}

int main(){
int n;
cin>>n;
vector<int> a;
for(int i=0;i<n;i++){
int temp;
cin>>temp;
a.push_back(temp);
}
quicksort(a, 0, n-1);
for(int i=0;i<n;i++){
cout<<a[i]<<" ";
}
}

归并排序

数据结构合集 - 归并排序(非递归与递归算法过程, 效率分析, 稳定性分析)_哔哩哔哩_bilibili

image-20260604104858667

归并排序的核心思想就是

  1. 分 (Divide):将数组从中间一分为二,分别对左右两个子数组进行排序。
  2. 治 (Conquer):递归地将子数组排序。
  3. 合 (Combine):将两个已排序的子数组合并成一个有序数组。

空间复杂度 O(n),时间复杂度 O(n log n),稳定。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
from typing import List
import random
def sortArray(nums: List[int]) -> List[int]:
ans=[0]*len(nums)
def merge_sort(nums,left,right):
if left>=right:return

mid=(left+right)//2
merge_sort(nums,left,mid)
merge_sort(nums,mid+1,right)
index=left
i,j=left,mid+1
while i<=mid and j<=right:
if nums[i]>nums[j]:
ans[index]=nums[j]
index+=1
j+=1
else:
ans[index]=nums[i]
index+=1
i+=1
if i<=mid:
ans[index:right+1]=nums[i:mid+1]
else:
ans[index:right+1]=nums[j:right+1]
nums[left:right+1]=ans[left:right+1]
merge_sort(nums,0,len(nums)-1)

return ans

问:代码中的 nums[i] < pivot 和 nums[i] > pivot 能否改成 nums[i] <= pivot 和 nums[i] >= pivot?

答:这个做法会在子数组所有元素相同时,划分后的 j 是子数组最后一个元素的下标,是最不均匀划分,算法会退化至 O(n**2)。

问:代码中的 i <= j 能否改成 i < j?

答:这会算错。来看一个例子 nums=[2,1,3],pivot=2。左指针 i=1 移动到 i=2,右指针 j=2 因为不满足 i < j 的条件,无法移动。此时我们交换 2 和 nums[j]=3,得到 [3,1,2],返回 j=2。然而 j=2 左侧有大于 pivot=2 的元素,划分失败。

如果写成 i <= j,那么最终 i=2,j=1。此时我们交换 2 和 nums[j]=1,得到 [1,2,3],返回 j=1。这样的划分就是正确的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
#include <any>
#include<bits/stdc++.h>
#include <type_traits>
#include <vector>
using namespace std;

void merge(vector<int>& nums, int left, int mid, int right, vector<int>& tmp) {
int i = left, j = mid + 1;
int k = left;
while (i <= mid && j <= right) {
if (nums[i] <= nums[j]) {
tmp[k] = nums[i];
k++;
i++;
} else {
tmp[k] = nums[j];
k++;
j++;
}

}
while (i <= mid) {
tmp[k] = nums[i];
k++;
i++;
}
while (j <= right) {
tmp[k] = nums[j];
k++;
j++;
}
for (int ii = left; ii <= right; ii++) {
nums[ii] = tmp[ii];
}
}

void mergesort(vector<int>& nums, int left, int right, vector<int>& tmp) {
if (left >= right)return;
int mid = (left + right) / 2;
mergesort(nums, left, mid, tmp);
mergesort(nums, mid + 1, right, tmp);

merge(nums, left, mid, right, tmp);
}

int main() {
int n;
cin >> n;
vector<int> a;
for (int i = 0; i < n; i++) {
int temp;
cin >> temp;
a.push_back(temp);
}
vector<int> tmp(n);
mergesort(a, 0, n - 1, tmp);
for (int i = 0; i < n; i++) {
cout << a[i] << " ";
}
}

链表的归并

148. 排序链表 - 力扣(LeetCode)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
using namespace std;
class Solution {
public:
ListNode* sortList(ListNode* head) {
if(!head)return nullptr;
if(!head->next){
return head;
}
ListNode* slow=head;
ListNode* fast=head;
ListNode* prev=nullptr;

while (fast&&fast->next)
{
prev=slow;
fast=fast->next->next;
slow=slow->next;
}

prev->next=nullptr;

ListNode* left=sortList(head);
ListNode* right=sortList(slow);

return merge(left,right);
}

ListNode* merge(ListNode* left,ListNode* right){
ListNode* dummy=new ListNode();
ListNode* cur=dummy;
while (left&&right)
{
if(left->val<right->val){
cur->next=left;
left=left->next;
}else{
cur->next=right;
right=right->next;
}
cur=cur->next;
}
if(left)cur->next=left;
if(right)cur->next=right;
return dummy->next;

}
};

堆排序

数据结构合集 - 堆与堆排序(算法过程, 效率分析, 稳定性分析)_哔哩哔哩_bilibili

image-20260623120621616

image-20260623121353399

不稳定

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class Solution:
def sortArray(self, nums: List[int]) -> List[int]:

def heap(index,nums,n):
#维护操作,比较左右节点是否都小于根节点,然后递归
left=index*2+1
right=index*2+2
maxindex=index

# if left<n and nums[left]>nums[index]:
# nums[left],nums[index]=nums[index],nums[left]
# heap(left,nums)
# 先找左右节点哪个最大
if left<n and nums[left]>nums[maxindex]:
maxindex=left
if right<n and nums[right]>nums[maxindex]:
maxindex=right
if maxindex!=index:
#说明有变化,换位置,然后递归
nums[maxindex],nums[index]=nums[index],nums[maxindex]
heap(maxindex,nums,n)
def heap_sort(nums):
n=len(nums)
#建堆,从最后一个非叶子节点开始,往下维护大根堆
for i in range(n//2-1,-1,-1):
heap(i,nums,n)
#排序,建堆后,第一个位置一定最大,移动到最后,然后再对第一个建堆维护
for i in range(n-1,0,-1):
nums[0],nums[i]=nums[i],nums[0]
heap(0,nums,i)
return nums

return heap_sort(nums)

堆排序就是反复建堆的过程是吗,但是最小堆的数据结构,不需要维护数组有序,只要保证第一个元素是最小的就行,大根堆就是维护第一个元素最大

  • 堆的性质:你理解得很对,局部有序,全局无序,只保堆顶。
  • 堆排序的本质:建一次堆(O(N)) + 反复调整堆(N次 O(log N)) = 总时间复杂度 O(N log N)。

堆的方法

  • 上浮 (Sift Up) 专门用于:插入新元素 (heappush)
  • 下沉 (Sift Down) 专门用于:删除堆顶 (heappop) 和 批量建堆 (heapify)

heappush是在列表后面新加一个元素,也就是加一个叶子节点,这个叶子节点要以此查看自己的父亲节点,维护堆的性质,称之为上浮

heappop是弹出最大/最小元素,其实是弹出arr[0],,但左右子树依旧是保持着堆的性质,那其实思路就是和最后一个叶子节点交换位置,然后利用数组的pop弹出,再进行一次下沉操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
class minheap:
def __init__(self,arr=None):
self.heap=[]
if arr:
self.heapify(arr)

def __heapdown(self,index:int,n:int):
left=index*2+1
right=index*2+2
minindex=index
if right<n and self.heap[right]<self.heap[minindex]:
minindex=right
if left<n and self.heap[left]<self.heap[minindex]:
minindex=left
if minindex!=index:
self.heap[minindex],self.heap[index]=self.heap[index],self.heap[minindex]
self.__heapdown(minindex,n)

def heapify(self,arr:list):
self.heap=arr[:]
n = len(self.heap)
# 从最后一个非叶子节点开始,逆序遍历,依次下沉
for i in range(n // 2 - 1, -1, -1):
self.__heapdown(i, n)

def heappop(self):
if not self.heap: # ✅ 添加空堆检查
return None

if len(self.heap) == 1: # ✅ 只有一个元素时直接弹出
return self.heap.pop()
#先和最后一个元素交换,然后pop,再下沉操作
n=len(self.heap)-1
self.heap[0],self.heap[n]=self.heap[n],self.heap[0]
#下沉操作
self.__heapdown(0,n)# ✅ 修正:传入 n 而不是 n-1,这个n是数量,不是下标

#返回最大值并弹出
return self.heap.pop()
def heappush(self,num:int):
self.heap.append(num)
#进行上浮操作,以此比较父亲节点
index=len(self.heap)-1
while index>0:
parent=(index-1)//2
if self.heap[index]<self.heap[parent]:
self.heap[index],self.heap[parent]=self.heap[parent],self.heap[index]
index=parent
else:
break

c++

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
#include <any>
#include<bits/stdc++.h>
#include <type_traits>
#include <vector>
using namespace std;

void heapify(vector<int>& nums, int n, int i) {
while (true) {
int largest = i;
int left = 2 * i + 1;
int right = 2 * i + 2;

if (left < n && nums[left] > nums[largest]) {
largest = left;
}
if (right < n && nums[right] > nums[largest]) {
largest = right;
}
if (largest == i) break;
swap(nums[i], nums[largest]);
i=largest;
}

}

void heapsort(vector<int>& nums) {
int n=nums.size();
for(int i=n/2-1;i>=0;i--){
heapify(nums, n,i);
}

for(int i=n-1;i>0;i--){
swap(nums[0],nums[i]);
heapify(nums, i, 0);
}
}

int main() {
int n;
cin >> n;
vector<int> a;
for (int i = 0; i < n; i++) {
int temp;
cin >> temp;
a.push_back(temp);
}
// vector<int> tmp(n);
heapsort(a);
for (int i = 0; i < n; i++) {
cout << a[i] << " ";
}
}

冒泡排序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
int main() {
int n;
cin >> n;
vector<int> a;
for (int i = 0; i < n; i++) {
int temp;
cin >> temp;
a.push_back(temp);
}
// vector<int> tmp(n);
for(int i=0;i<n;i++){
bool flag=true;
for(int j=0;j<n-1-i;j++){
if(a[j]>a[j+1]){
swap(a[j],a[j+1]);
flag=false;
}

}
if(flag)break;
}
for (int i = 0; i < n; i++) {
cout << a[i] << " ";
}
}

二叉树

前中后序遍历

遍历方式 所属搜索类型 核心数据结构 遍历顺序特征 典型应用场景
前序遍历 DFS 栈 (Stack) / 递归 根 $\rightarrow$ 左 $\rightarrow$ 右 复制树、序列化、生成前缀表达式
中序遍历 DFS 栈 (Stack) / 递归 左 $\rightarrow$ 根 $\rightarrow$ 右 二叉搜索树(BST)的排序输出
后序遍历 DFS 栈 (Stack) / 递归 左 $\rightarrow$ 右 $\rightarrow$ 根 删除树、计算目录大小、生成后缀表达式
层序遍历 BFS 队列 (Queue) 从上到下,从左到右按层访问 求树的最小深度、Z字形打印、寻找最短路径
  • 前序(非递归):用栈。根节点入栈 →→ 弹出并访问 →→ 右子节点入栈 →→ 左子节点入栈(保证左先出)。
  • 中序(非递归):用栈。一路向左将节点压入栈 →→ 弹出并访问 →→ 转向右子树。
  • 后序(非递归):用栈。通常需要记录上一个访问的节点,或者使用“根 →→ 右 →→ 左”的顺序入栈,最后将结果数组反转,得到“左 →→ 右 →→ 根”。

前序遍历

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class TreeNode:
def __init__(self, val=0, left=None, right=None):
self.val = val
self.left = left
self.right = right
class Solution:
#递归版
def preorderTraversal(root: Optional[TreeNode]) -> List[int]:
ans=[]
def dfs(node:TreeNode):
if not node:
return
ans.append(node.val)
if node.left:dfs(node.left)
if node.right:dfs(node.right)
dfs(root)
return ans
#栈
def preorderTraversal_stack(root: Optional[TreeNode]) -> List[int]:
ans=[]
stack=[]
stack.append(root)
while stack:
node:TreeNode=stack.pop()
if not node:
continue
ans.append(node.val)
if node.right:stack.append(node.right)#注意和dfs的差别
if node.left:stack.append(node.left)
return ans

中序遍历栈的写法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class Solution {
public:
vector<int> inorderTraversal(TreeNode* root) {
vector<int> result;
stack<TreeNode*> st;
TreeNode* curr = root;

// 当 curr 不为空 或 栈不为空 时继续
while (curr != nullptr || !st.empty()) {

// 第1步:一路向左,全部压栈
while (curr != nullptr) {
st.push(curr);
curr = curr->left;
}

// 第2步:弹出栈顶(最左节点),访问它
curr = st.top();
st.pop();
result.push_back(curr->val); // 访问节点

// 第3步:转向右子树
curr = curr->right;
}

return result;
}
};

层序遍历

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Solution:
def levelOrder(root: Optional[TreeNode]) -> List[List[int]]:
ans=[]
queue=deque()
if not root:return ans
queue.append(root)
while queue:
n=len(queue)
temp=[]
for _ in range(n):
node:TreeNode=queue.popleft()
if not node:continue
temp.append(node.val)
if node.left:queue.append(node.left)
if node.right:queue.append(node.right)
ans.append(temp)
return ans

二叉树的构造(前序中序、后序中序、层序中序构造二叉树)

数据结构合集 - 二叉树的构造(前序中序、后序中序、层序中序构造二叉树)_哔哩哔哩_bilibili

image-20260605101610954

image-20260605130441466

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Solution:
def buildTree(preorder: List[int], inorder: List[int]) -> Optional[TreeNode]:
if not preorder and not inorder:
return None
hashmap={}
for index,val in enumerate(inorder):
hashmap[val]=index
n=len(preorder)
def dfs(in_left,in_right,pre_left,pre_right):
if in_right-in_left<0:
return None
pivot_index=hashmap[preorder[pre_left]]
node=TreeNode()
node.val=preorder[pre_left]
size=pivot_index-in_left
node.left=dfs(in_left,pivot_index-1,pre_left+1,pre_left+size)
node.right=dfs(pivot_index+1,in_right,pre_left+size+1,pre_right)

return node
root:TreeNode=dfs(0,n-1,0,n-1)


return root

二叉搜索树

对于树中的任意一个节点,必须同时满足以下三个条件:

  1. 左小:它左子树上的所有节点值,都严格小于它自己的值。
  2. 右大:它右子树上的所有节点值,都严格大于它自己的值。
  3. 递归:它的左子树和右子树,也必须分别是二叉搜索树。
1
2
3
4
5
6
7
    8
/ \
3 10
/ \ \
1 6 14
/ \ /
4 7 13

考点 1:中序遍历 = 升序数组(最重要!)

对 BST 进行中序遍历(左 -> 根 -> 右),得到的结果一定是一个从小到大排好序的数组!

  • 上面那棵树的中序遍历:1, 3, 4, 6, 7, 8, 10, 13, 14。
  • 应用:很多题目让你找 BST 的第 K 小元素、验证是不是 BST、找两个节点的差值最小,全部用中序遍历解决。

考点 2:查找/插入/删除不需要遍历全树

普通二叉树找东西要遍历所有节点($O(N)$),但 BST 只需要顺着“左小右大”的路径走,不需要回溯,一路走下去就行($O(H)$,H是树高)。

考点 3:警惕“退化成链表”

如果插入的数据本来就是有序的(比如 1, 2, 3, 4, 5),BST 会变成这样:

1
2
3
4
5
6
7
1
\
2
\
3
\
4

这时候它退化成了链表,查找速度变成了 $O(N)$。
(为了解决这个问题,大佬们发明了“平衡二叉搜索树”,如 AVL树、红黑树。C++ 里的 set 和 map 底层就是红黑树。)

完全二叉树

类型 定义 形状 节点数公式
满二叉树 (Full/Perfect) 每一层都填满了,最后一层也是满的。 完美的三角形 $2^h - 1$
完全二叉树 (Complete) 上面都满了,最后一层可以不满,但必须靠左。 缺了右下角的三角形 无固定公式,但在 $2^{h-1}$ 到 $2^h - 1$ 之间

C++ 里的 priority_queue(优先队列),底层就是用完全二叉树实现的,而且它通常不用指针(TreeNode*)来存,而是用数组(vector)来存!

数组下标的魔法公式(假设根节点下标为 i):

  • 左孩子的下标是:2 * i + 1
  • 右孩子的下标是:2 * i + 2
  • 父节点的下标是:(i - 1) / 2

image-20260627113832645

模式匹配

kmp

最浅显易懂的 KMP 算法讲解_哔哩哔哩_bilibili

帮你把KMP算法学个通透!(理论篇)_哔哩哔哩_bilibili

nextval 数组是 KMP 算法的究极优化体。它通过在预处理时加入一步判断,消除了模式串中连续重复字符导致的无意义回溯。当字符发生错配时,nextval 能让指针一步跳跃到真正有效的下一个截然不同的待匹配字符,将最坏情况下的回溯开销降到了极致。”

28. 找出字符串中第一个匹配项的下标 - 力扣(LeetCode)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
#include<bits/stdc++.h>
using namespace std;
class Solution {
public:
vector<int> next;
void getnext(string s){
int j=0;//前缀指针
for(int i=1;i<s.size();i++){
//找匹配的前缀
while (j>0&&s[j]!=s[i])
{
j=next[j-1];//看0到j-1已经匹配的
}
if(s[i]==s[j]){
j++;
}
next[i]=j;


}
}
int strStr(string haystack, string needle) {
next=vector(needle.size(),0);
getnext(needle);
int i=0,j=0;
while (i<haystack.size()&&j<needle.size())
{
if(haystack[i]==needle[j]){
// 分支 1:匹配成功,i 和 j 一起愉快地右移
i++;
j++;
} else if (j > 0) {
// 分支 2:匹配失败,但 j 还能回退 -> i 保持不变!j 查表跳转
j = next[j - 1];
} else {
// 分支 3:匹配失败,且 j 已经在起点 0 了 -> i 只能无奈右移
i++;
}
}


if(j==needle.size()){
return i-needle.size();
}
return -1;
}
};

python

字符串

split()

将字符串按照指定的分隔符“切割”,并返回一个列表(List)

1
str.split(sep=None, maxsplit=-1)
  • sep:分隔符(字符串)。如果不传,默认按空白字符(空格、换行符 \n、制表符 \t 等)分割。
  • maxsplit:最大分割次数。如果不传,默认 -1(表示不限制次数,有多少切多少)。

join()

是把列表(或任何可迭代对象)“拼”成字符串

1
2
3
4
chars = ['H', 'e', 'l', 'l', 'o']
word = "".join(chars)
print(word)
# 输出: Hello

isdigit()

判断一个字符串是否“完全由数字字符组成”。 如果是,返回 True;只要包含一个非数字字符(或者字符串为空),就返回 False。

栈,队列

deque

deque 是 double-ended queue(双端队列)的缩写

deque 专门为两端的高效操作进行了优化,在头部和尾部添加、删除元素的速度都非常快,都是 O(1) 的时间复杂度。

1
from collections import deque
  • append(x):从右边(尾部) 添加元素。
  • appendleft(x):从左边(头部) 添加元素。
  • pop():从右边(尾部) 移除并返回元素。
  • popleft():从左边(头部) 移除并返回元素。

random 模块

random.randint(a, b):生成一个 [a, b] 范围内的随机整数(包含 a 和 b)。

堆

heapq 是 Python 的内置标准库模块,用于实现堆(Heap)\数据结构,特别适合做*优先队列*。

  1. 默认是最小堆(Min Heap):堆顶永远是最小元素
  2. 基于列表实现:直接在普通 list 上操作
  3. 时间复杂度:插入和删除都是 O(log n)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
import heapq
# 方法1:空列表 + heappush
heap = []
heapq.heappush(heap, 3)
heapq.heappush(heap, 1)
heapq.heappush(heap, 4)
print(heap) # 输出: [1, 3, 4] 堆顶是最小值 1

# 方法2:heapify 快速建堆
nums = [3, 1, 4, 1, 5, 9, 2, 6]
heapq.heapify(nums) # 原地转换,O(n)

# 弹出最小值 O(log n)
min_val = heapq.heappop(heap)
函数 说明 时间复杂度
heappush(heap, item) 插入元素 O(log n)
heappop(heap) 弹出最小值 O(log n)

cpp

排序

1
2
3
4
5
6
7
8
vector<int> nums = {5, 2, 8, 1, 9};

// 1. 默认升序(从小到大)
sort(nums.begin(), nums.end());
// 结果: [1, 2, 5, 8, 9]

// 2. 降序(从大到小):传入内置的 greater<int>()
sort(nums.begin(), nums.end(), greater<int>());

结构体 (Struct / Class) 的排序

1
2
3
4
5
6
7
8
9
10
11
12
13
struct Student {
string name;
int score;

// 重载 < 运算符
bool operator<(const Student& other) const {
return score > other.score; // 按分数降序(分数高的排前面)
}
};

// 使用时,直接调用 sort 即可自动生效
vector<Student> students = {{"Alice", 85}, {"Bob", 92}};
sort(students.begin(), students.end());

自定义函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 1. 定义比较函数
// 规则:当 a 应该排在 b 前面时返回 true
bool compareIntervals(const vector<int>& a, const vector<int>& b) {
if (a[0] != b[0]) {
return a[0] < b[0]; // 第一维升序
}
return a[1] > b[1]; // 第二维降序
}

int main() {
vector<vector<int>> intervals = {{1, 3}, {2, 6}, {1, 5}, {8, 10}};

// 2. 调用 sort,直接传入函数名 compareIntervals(注意:不加括号)
sort(intervals.begin(), intervals.end(), compareIntervals);

return 0;
}

堆

在 C++ 中,建立最小堆(Min-Heap)最常用的工具是标准库 <queue> 中的 priority_queue(优先队列)。

默认情况下,priority_queue 是最大堆(大的元素在顶端)。要变成最小堆,你需要修改它的模板参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <queue>
#include <vector>
#include <functional> // 包含 greater

// 语法:priority_queue<数据类型, 容器类型, 比较方式>
priority_queue<int, vector<int>, greater<int>> minHeap;

// 1. 定义一个比较结构体
struct cmp {
// 注意:这里的参数类型要和堆里存的一样,是 ListNode*
// 返回 true 表示 a 的优先级比 b 低(a 应该沉下去,b 浮上来)
bool operator()(ListNode* a, ListNode* b) {
return a->val > b->val;
}
};

// 2. 声明最小堆
// 参数1: 存的数据类型 (ListNode*)
// 参数2: 底层容器 (vector<ListNode*>)
// 参数3: 刚才写的比较规则 (cmp)
priority_queue<ListNode*, vector<ListNode*>, cmp> minHeap;

struct cmp {
bool operator()(const pair<int, int>& a, const pair<int, int>& b) {
return a.first > b.first; // 小顶堆:first 越小优先级越高
}
};

万能头文件

1
#include <bits/stdc++.h>

结构体

1
2
3
4
5
6
7
struct node
{
int val;
int index1;
int index2;
node(int v,int i1,int i2):val(v),index1(i1),index2(i2){}
};

{}的用法:初始化对象 / 容器;函数传参(上下文推导)

1
2
3
4
5
6
7
8
9
10
// 1. 容器
vector<int> v = {1, 2, 3}; // 列表初始化
map<string, int> m = {{"A", 1}}; // 嵌套花括号初始化键值对

// 2. 结构体 / 聚合类(所有成员 public)
struct Point { int x; int y; };
Point p = {10, 20}; // ✅ 完美匹配

// 3. std::pair / std::tuple
pair<int, string> p = {1, "hello"}; // ✅ 合法,编译器自动转成 pair
1
2
3
4
5
6
7
8
9
10
queue<pair<TreeNode*, int>> q;

// ❌ 错误:不能直接 auto 接收
auto temp = {root, root->val};

// ✅ 正确:push 函数要求 pair,编译器看到目标类型,自动把 {} 转换成 pair
q.push({root, root->val});

// ✅ 正确:make_pair 明确要求返回 pair
auto temp = make_pair(root, root->val);

auto用法

拯救冗长的 STL 类型

1
2
3
4
5
6
7
priority_queue<pair<int, pair<int, int>>, vector<...>, cmp> pq;

// 不用 auto(折磨):
pair<int, pair<int, int>> top_element = pq.top();

// 用 auto(清爽):
auto top_element = pq.top(); // 编译器自动推导

遍历容器

1
2
3
4
5
6
vector<string> names = {"Alice", "Bob"};

// 爽!直接遍历
for (auto name : names) {
cout << name << endl;
}

结构化绑定

1
2
3
4
5
queue<pair<TreeNode*, int>> q;
q.push({root, root->val});

// 爽!直接解包(注意这里是方括号 [],不是花括号 {})
auto [node, current_sum] = q.front();

哈希表

unordered_map(键值对哈希表)

相当于 Python 的 dict。用来存 Key -> Value 的映射。

1
2
3
4
5
6
7
#include <unordered_map>
using namespace std;

// 语法:unordered_map<键的类型, 值的类型> 变量名;
unordered_map<int, string> myMap; // 键是 int,值是 string
unordered_map<string, int> wordCount; // 键是 string,值是 int
unordered_map<char, vector<int>> charMap; // 键是 char,值是 vector

增 & 改(最常用)

使用 [] 运算符是最简单直接的方式。

1
2
3
4
5
unordered_map<int, int> map;

map[1] = 100; // 新增:键为 1,值为 100
map[2] = 200; // 新增:键为 2,值为 200
map[1] = 999; // 修改:键 1 已存在,值被覆盖为 999

查(判断键是否存在)

这是刷题时最高频的操作,主要有两种写法:

写法 A:使用 count()(最推荐,简单直观)
count(key) 会返回 1(存在)或 0(不存在)。

1
2
3
4
5
if (map.count(1)) {
cout << "键 1 存在!" << endl;
} else {
cout << "键 1 不存在!" << endl;
}

写法 B:使用 find()(返回迭代器)
find(key) 会返回一个迭代器。如果找不到,它会返回 map.end()。

1
2
3
if (map.find(1) != map.end()) {
cout << "键 1 存在!" << endl;
}

删

1
2
3
// 删除指定 key
size_t count = mp.erase("apple");
// 返回值 count:删除了 1 个元素;如果 key 不存在则返回 0

遍历操作

在 C++ 中,遍历 unordered_map 通常使用范围 for 循环(配合 auto)。
遍历时,每一个元素都是一个 pair(键值对),可以通过 .first 获取键,.second 获取值。

1
2
3
4
5
6
unordered_map<string, int> scores = {{"Alice", 90}, {"Bob", 85}, {"Charlie", 95}};

// 推荐写法:使用 const auto& 避免拷贝,提高效率
for (const auto& pair : scores) {
cout << "姓名: " << pair.first << ", 分数: " << pair.second << endl;
}

(注意:unordered_map 是无序的,遍历出来的顺序不一定是你插入的顺序!)

vector

insert()

1
vec.insert(插入位置的迭代器, 插入的数量, 要插入的值);

初始化

1
2
3
4
vector<类型> 变量名(元素个数, 初始值);
vector<int> v(10, 0);

vector<int> v = {1, 2, 3, 4, 5};

单调队列

核心特性只有八个字:队内元素,单调有序。

  • 单调递增队列:队头到队尾的元素依次变大(队头始终是当前窗口的最小值)。
  • 单调递减队列:队头到队尾的元素依次变小(队头始终是当前窗口的最大值)。

为什么需要单调队列?

假设给你一个大小为 $N$ 的数组,让你求所有长度为 $K$ 的滑动窗口里的最小值:

  • 暴力做法:每移动一步,遍历窗口里的 $K$ 个元素找最小值,时间复杂度为 $\mathcal{O}(N \times K)$。
  • 优先队列(大/小顶堆):可以 $O(\log K)$ 拿到最值,但堆不支持任意删除过期元素,时间复杂度 $\mathcal{O}(N \log K)$。
  • 单调队列:能以 $\mathcal{O}(1)$ 均摊时间 实时维护并获取窗口的最值!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
deque<int> q;
// 1. 【过期校验】队头元素太老了,踢出窗口外
while (!q.empty() && 队头超出了窗口范围) {
q.pop_front();
}

// 2. 【维护单调性】队尾元素比新元素劣质,全部从队尾淘汰
while (!q.empty() && 队尾元素 >= 新元素) {
q.pop_back();
}

// 3. 【新元素入队】
q.push_back(新元素);

// 此时:q.front() 必定是当前窗口内的最佳最值!

互联网的基础与组成

核心概念

路由器 (Router):

  • 功能:连接两个或多个不同的计算机网络(如将家庭网络连接到外部互联网)。
  • 作用:它是实现“互联网”连接的关键设备,负责在不同网络之间转发数据。

交换机 (Switch):

  • 功能:将多个网络节点(如电脑、电视、手机)连接起来,组成一个局部的计算机网络。
  • 应用场景:常用于家庭、公司或学校内部组建局域网。

image-20260512092631854

ISP

全称:Internet Service Provider(互联网服务提供商)。

通俗理解:它是连接普通用户与互联网的“中介”或“运营商”。如果没有 ISP,我们的手机和电脑就无法访问广域网。

组成部分

image-20260512094913137

功能

image-20260512095248915

电路交换和报文交换

1. 电路交换 (Circuit Switching)

这是一种类似于传统打电话的交换方式,必须先“占线”才能通信。

  • 优点:
    • 传输速率高、数据直达: 通信前会建立一条端到端的专用物理通路。
    • 适用场景: 更适合低频次、大量的数据传输(就像打电话一样,拨号花点时间,但一旦接通,长时间通话体验很好)。
  • 缺点:
    • 时间开销大: 建立连接和释放连接都需要消耗额外的时间。
    • 利用率低: 在整个通信期间,线路被双方独占,即使中间有一方没在发数据,别人也用不了这根线。
    • 灵活性差: 线路分配不够灵活,且非常不适合计算机之间常见的“突发式”通信(即高频次、少量的数据传输)。

2. 报文交换 (Message Switching)

这种方式不需要事先占线,而是把整个数据块(报文)传给相邻节点,节点存下来之后再找下一步的路,也就是“存储转发”。

  • 优点:
    • 无需建立连接: 随时有数据随时发,省去了建立连接的时间。
    • 线路利用率高、分配灵活: 用户无需独占整条线路,节点采用“存储转发”的方式动态分配线路。
    • 支持差错控制: 交换节点可以通过校验技术检查数据是否传错。
  • 缺点:
    • 管理不便: 报文的长度是不固定的,这给节点的存储和转发管理带来了麻烦。
    • 资源和时间开销大: 如果报文很长(比如图里手写的 80GB 传到 8bit 链路上),节点需要极大的缓存空间来接收它,并且整体的存储转发时间开销很大。
    • 重传代价高: 长报文如果在传输中出现了一点点错误,整个长报文都需要重新传输,代价极其高昂

分组交换

image-20260512101440274

  1. 发送端:拆分与装箱 (拆)

当用户(如小帅 H1)要发送一份很大的“用户数据”给接收者(如小美 H5)时:

  • 切割:数据不会整块发送,而是被切成一个个更小的单位,称为分组 (Packet)。
  • 加首部:每个分组前面都会加上“首部”(类似快递单),上面写着源 IP 地址和目的 IP 地址。
  • 网络中:存储转发 (送)

图中绿色的“分组交换机”(本质上就是路由器)起到了中转站的作用:

  • 存储:路由器先接收整个分组并暂时存入缓存。
  • 查表:读取分组首部的目的地址,查询自己的“路由表”。
  • 转发:决定走哪条路(比如 A→B 或 A→C)最快,然后把分组发往下一个路由器。
  • 特点:每个分组独立选择路径,不一定要走同一条路。
  • 接收端:还原与合并 (合)

当所有分组到达目的地(如小美 H5)后:

  • 剥离:去掉每个分组的首部(撕掉快递单)。
  • 排序重组:按照分组编号,把散乱的数据重新拼装成原始的“用户数据”。
  1. 发送端:拆分与装箱 (拆)

当用户(如小帅 H1)要发送一份很大的“用户数据”给接收者(如小美 H5)时:

  • 切割:数据不会整块发送,而是被切成一个个更小的单位,称为分组 (Packet)。
  • 加首部:每个分组前面都会加上“首部”(类似快递单),上面写着源 IP 地址和目的 IP 地址。
  • 网络中:存储转发 (送)

图中绿色的“分组交换机”(本质上就是路由器)起到了中转站的作用:

  • 存储:路由器先接收整个分组并暂时存入缓存。
  • 查表:读取分组首部的目的地址,查询自己的“路由表”。
  • 转发:决定走哪条路(比如 A→B 或 A→C)最快,然后把分组发往下一个路由器。
  • 特点:每个分组独立选择路径,不一定要走同一条路。
  • 接收端:还原与合并 (合)

当所有分组到达目的地(如小美 H5)后:

  • 剥离:去掉每个分组的首部(撕掉快递单)。
  • 排序重组:按照分组编号,把散乱的数据重新拼装成原始的“用户数据”。

一、 分组交换的核心优点

  • 无需建立连接:发送数据前不需要像打电话(电路交换)那样先拨号,数据直接发出,实时性好。
  • 线路利用率高:多个用户的数据可以同时在一条物理线路上交替传输,不会独占资源,非常省钱高效。
  • 灵活分配与容错:路由器根据网络拥堵情况动态选择路径。如果某条路断了,分组可以自动绕道,且支持差错控制,发现坏数据能及时纠正。

二、 横向对比:为什么比“报文交换”好?

报文交换是把整封邮件发出去,而分组交换是拆成小件发。

  • 管理更方便:分组有固定长度上限,路由器更容易管理内存和缓存。
  • 速度更快:由于分组小,路由器处理单个分组的存储转发时间极短,减少了排队等待。
  • 容错代价低:如果传输中出错了,只需要重传出错的那一小块(分组),而不需要重传整个大数据包。

三、 存在的缺点与挑战

  • 额外开销大:每个小分组都要贴上“快递单”(控制信息/首部),当数据被拆得太碎时,这些首部信息的占比会显著增加,降低了纯数据的传输效率。
  • 存在时延:每个路由器都要执行“存储-检查-转发”的过程,这会产生一定的处理时延。
  • 处理复杂性:
    • 失序:小件快递走不同路径,可能导致“后发的先到”。
    • 丢失:某个分组可能在路上弄丢了。
    • 重复:可能收到两个一样的分组。
    • *注:这些问题都需要接收端的主机通过复杂的协议逻辑来重新排序和处理

计算机网络的分类


网络类型 全称 覆盖范围 典型应用场景 特点/技术
广域网 (WAN) Wide Area Network 几十 ~ 几千公里 跨省、跨国、跨洲 覆盖范围最广,是互联网的核心
城域网 (MAN) Metropolitan Area Network 几千米 ~ 几十千米 一个或几个相邻城市 常采用以太网技术,常并入局域网讨论
局域网 (LAN) Local Area Network 几十米 ~ 几千米 学校、企业、家庭 范围较小,通常使用以太网技术
个域网 (PAN) Personal Area Network 几十米以内 个人设备连接 常通过无线技术连接(如蓝牙),又称WPAN

image-20260512105302052

性能指标

image-20260512112200680

1. 信道 (Channel)

  • 概念: 指向某一个方向传送信息的通道。
  • 核心要点: 信道 $\neq$ 通信线路。一条物理上的通信线路(比如你家插在路由器上的网线),在逻辑上通常包含一条发送信道和一条接收信道。这也是为什么我们测速时,会有“上行速率”和“下行速率”两个不同的指标。

2. 速率 (Speed / Data Rate)

  • 概念: 网络节点在信道上每秒传输的数据量,也叫数据率或比特率。
  • 单位: bps (bit/s 或 b/s)。
  • ⚠️ 超级易错点(单位与进位):
    • B 与 b 的区别: 大写 B 代表字节 (Byte),小写 b 代表比特 (bit)。$1\text{B} = 8\text{b}$。
    • 网络领域的进位(十进制): 在计算机网络中衡量速率时,前缀 k、M、G、T 的进位是 $10^3$。
      • $1 \text{ kbps} = 10^3 \text{ bps}$
      • $1 \text{ Mbps} = 10^6 \text{ bps}$
    • 体系结构/操作系统的进位(二进制): 在衡量内存、硬盘等存储容量时,前缀 K、M、G、T 的进位是 $2^{10}$。
      • $1 \text{ KB} = 2^{10} \text{ B} = 1024 \text{ B}$

结合幻灯片的例子: 你家办理了“千兆宽带”,运营商口中的“千兆”指的是 $1000 \text{ Mbps}$(也就是 $10^9 \text{ bps}$)。用迅雷下载文件时,软件显示的单位通常是 MB/s (兆字节/秒)。

所以理论最高下载速度 = $1000 \text{ Mbps} \div 8 = 125 \text{ MB/s}$。

3. 带宽 (Bandwidth)

带宽这个词在不同学科里有两个截然不同的含义:

  • 在《计算机网络》中(数字信号):
    • 含义: 指某信道所能传送的最高数据传输速率(即速率的上限)。
    • 单位: bps (bit/s)。
  • 在《通信原理》中(模拟信号):
    • 含义: 指某信道允许通过的信号频带范围(最高频率与最低频率之差)。
    • 单位: Hz (赫兹)。

1. 时延 (Delay) - 【计算题重灾区】

数据从网络一端传送到另一端所需的总时间。它由四个部分拼凑而成:总时延 = 发送时延 + 传播时延 + 处理时延 + 排队时延。

  • 发送时延 (也叫传输时延): 把数据从节点“推”到信道上花的时间。
    • 公式: 数据长度 (bit) ÷ 发送速率 (bit/s)
    • 比喻: 就像火车(数据)驶出站台(网卡)所需的时间。火车越长,驶出站台的时间越久。
  • 传播时延: 数据在物理信道上“跑”完这段距离花的时间。
    • 公式: 信道长度 (m) ÷ 电磁波传播速度 (m/s)
    • 比喻: 火车完全驶出站台后,在铁轨上开到终点站的时间。只和铁轨有多长、火车开多快有关。
  • ⚠️ 超级易混点: 考试时一定要分清这两个!提高网速(带宽)只能减少“发送时延”,无论你办千兆还是万兆宽带,信号在光缆里的“传播时延”(光速)是物理定律,改变不了的。
  • (注:处理时延和排队时延一般在路由器内发生,受网络拥堵影响,基础计算题中如无特别说明常忽略不计。)

image-20260512133345314

2. 时延带宽积 (Delay-Bandwidth Product)

  • 公式: 传播时延 × 带宽
  • 物理含义: 想象信道是一根圆柱形的空心管道,传播时延是管道的“长度”,带宽是管道的“截面积”。时延带宽积就是这个管道的体积。
  • 通俗理解: 它代表发送端连续发送数据时,第一个比特刚好到达接收端时,链路上“正在飞”的最大比特数(即管道被塞满时能装多少数据)。

3. 往返时延 (RTT, Round-Trip Time)

  • 含义: 从发送方发送完数据,到发送方收到接收方的“确认回复”,总共经历的时间。我们在电脑上 ping 某个网站弹出的时间就是 RTT。
  • 包含哪些部分:
    • 去程的:单向传播时延
    • 接收方的:处理时延 + 发送时延 (把确认包推上信道)
    • 回程的:传播时延 (确认包跑回来的时间)
  • ⚠️ 易错陷阱: RTT 不包含 发送方发送业务数据本身的“发送时延” ($t_1$)!它只从数据“发完”的那一刻开始掐表。

image-20260512134726893

4. 信道利用率

  • 含义: 信道有多大比例的时间是在传数据的(没有空闲)。
  • 辩证关系:
    • 太低:修了八车道高速公路却没几辆车,浪费资源。
    • 太高:容易导致网络拥塞。就像节假日的高速公路,利用率接近 100% 时,车会堵死,排队时延会急剧增加。

分层结构

1. 数据的传输视角:水平与垂直

理解网络体系结构,需要同时具备“水平”和“垂直”两种思维:

  • 水平视角(逻辑通信): 每一层的实体(比如两台电脑的应用层)都认为自己在和对端的同一层进行直接通信。它们之间交流的规则就是“协议”。为了让对端看懂自己的意图,发送方会在数据前加上专门的“首部”(有时还有“尾部”)。
  • 垂直视角(物理实现): 数据真正在主机内部的流动是自上而下(发送方)和自下而上(接收方)的。发送方每往下一层,就套上一层新的“首部”(这叫封装,像套娃一样);到了最底层的物理层,所有数据变成单纯的 0 和 1 比特流在网线里跑;接收方则逐层剥离首部(这叫解封装),把原始数据还原给上层。
    • 注意图中的小细节: 只有第2层(数据链路层)不仅加了首部 $H_2$,还加了尾部 $T_2$。

image-20260512143350054

image-20260512143200180

2. 三个重要的数据单元名词 (SDU, PCI, PDU)

这是考试中最爱考概念辨析的地方,你需要记住它们的全称和包含关系:

  • SDU (Service Data Unit, 服务数据单元): 上一层传下来的、需要本层帮忙传输的原始数据(Payload)。
  • PCI (Protocol Control Information, 协议控制信息): 本层为了实现协议功能而自己添加的“首部/尾部”信息。
  • PDU (Protocol Data Unit, 协议数据单元): 本层最终打包好的、准备传给下一层的完整数据块。

核心换算公式:

第 $n$ 层的完整数据块,等于本层加的控制信息加上层传下来的数据:

$n\text{-PDU} = n\text{-PCI} + n\text{-SDU}$

第 $n$ 层打包好的数据,到了第 $n-1$ 层眼里,统统变成了需要传输的原始数据:

$n\text{-PDU} = (n-1)\text{-SDU}$

image-20260512143215280

3. 协议的三要素

网络协议是水平的,它约束了通信双方必须遵守的规则。一个完整的协议必须包含以下三个要素:

  1. 语法 (Syntax): 规定了数据和控制信息的结构与格式。比如:报文的首部占多少个字节?第1个字节代表什么意思?
  2. 语义 (Semantics): 规定了通信双方需要“做什么动作”或“表达什么意思”。比如:这串代码是代表“请求建立连接”,还是代表“数据接收成功(ACK)”?
  3. 同步/时序 (Synchronization/Timing): 规定了事件实现的顺序和条件。比如:发送方发完数据后,如果在 10 秒内没收到对方的确认,就需要重新发送

OSI 七层参考模型

image-20260512154222954

层次 (从下到上) 英文简称 数据传输单位 核心任务与功能 典型网络设备
1. 物理层 Physical 比特 (Bit) 实现相邻节点间比特流的透明传输;定义接口的物理/电气特性(如电压代表0还是1)。 集线器 (Hub)、中继器
2. 数据链路层 Data Link 帧 (Frame) 确保相邻节点间的逻辑无差错;进行差错控制(校验、纠错/丢弃)、流量控制。 交换机 (Switch)、网桥
3. 网络层 Network 分组/数据报 (Packet) 将分组从源节点路由转发到目的节点;维护路由表、拥塞控制、网际互联。 路由器 (Router)
4. 传输层 Transport 报文段 (Segment) 实现端到端(进程到进程,认准“端口”)的通信;复用与分用;全局差错/流量控制。 -
5. 会话层 Session 报文 (Message) 管理进程间会话;使用“检查点”机制实现断点续传。 -
6. 表示层 Presentation 报文 (Message) 处理数据格式转换(如编码转换、数据压缩/解压、加密/解密)。 -
7. 应用层 Application 报文 (Message) 实现特定的网络应用(如微信、浏览器等)。 主机 (端系统)

⚠️ 易混淆的“重灾区”提醒:

在这几张图中,老师用红笔重点圈出的几个细节,是考试最喜欢挖坑的地方:

1. 差错控制:数据链路层 vs 传输层

  • 数据链路层的差错控制是“局部”的,它只保证这段网线(相邻两个节点A到B)传的数据没错,错了就扔掉。
  • 传输层的差错控制是“全局”的,它要保证从发送方主机一路经过无数路由器,最终到达接收方主机的数据是完整、有序、不丢失的。

2. 设备的工作层次(图2)

  • 路由器是“三层设备”:它最高只能理解到网络层(拆开看分组首部里的 IP 地址)。
  • 交换机是“二层设备”:它最高只能理解到数据链路层(拆开看帧首部里的 MAC 地址)。
  • 集线器是“一层设备”:它就是个无脑的物理放大器,只管电信号的 0 和 1。
  • 只有你使用的手机和电脑(主机)才拥有完整的 1 到 7 层。

3. 通信范围的本质区别

  • 网络层(IP):解决的是“主机到主机”的通信(帮你把包裹送到收件人的小区)。
  • 传输层(端口):解决的是“进程到进程”的通信(不仅送到小区,还要具体交给收件人本人,比如是给微信的还是给 QQ 的)。

image-20260512154120967

TCP/IP 模型

image-20260512164948826

1. 模型结构对比

  • OSI 参考模型(7层):应用层、表示层、会话层、传输层、网络层、数据链路层、物理层。
  • TCP/IP 模型(4层):应用层、传输层、网络层、网络接口层。
  • 层级映射关系:
    • TCP/IP 的应用层整合了 OSI 的应用层、表示层(数据格式转换)和会话层(会话管理)。如果应用需要格式转换或会话管理,由应用自身实现。
    • TCP/IP 的网络接口层整合了 OSI 的数据链路层和物理层。TCP/IP 并没有具体规定这一层的功能和协议,而是交由网络设备商自由发挥。

2. 各层的数据传输单位与核心功能

  • 应用层(单位:报文):实现特定的网络应用。
  • 传输层(单位:报文段):实现端到端(进程到进程)的通信。负责复用和分用、差错控制、流量控制、连接管理等。
  • 网络层(单位:数据报/分组):把分组从源结点转发到目的结点。核心功能是路由选择和分组转发。
  • 数据链路层(单位:帧):确保相邻节点之间的链路逻辑上无差错(主要负责差错控制、流量控制)。
  • 物理层(单位:比特):实现相邻节点之间的比特流传输,定义电路接口参数、电气特性等。

3. TCP/IP 模型中的“背锅”机制(网络层与传输层的协作)

课件通过生动的比喻(“摆烂”和“背锅”)解释了 TCP/IP 的设计理念:

  • TCP/IP 的网络层(摆烂):只提供“尽最大努力交付”,数据传输是不可靠的。它只管路由和转发,不负责差错控制和流量控制。
  • TCP/IP 的传输层(背锅):因为网络层不可靠,所以保证数据传输正确性和可靠性的重任就交给了传输层。传输层必须负责差错控制、流量控制和可靠传输管理。

4. 核心考点总结:OSI vs TCP/IP 服务的差异

最后一张图片总结了两个模型在网络层和传输层提供服务时的关键区别,这是考试/面试的重点:

比较维度 OSI 参考模型 TCP/IP 模型
网络层服务 可提供无连接不可靠服务(数据报),也可提供有连接可靠服务(虚电路)。 仅提供无连接不可靠的服务(数据报/尽最大努力交付)。
传输层服务 仅向应用层提供有连接的可靠服务。 可提供有连接可靠服务(TCP协议),也可提供无连接不可靠服务(UDP协议)。

总结一句话: OSI 模型在设计时希望网络层能做更多可靠性的工作,而传输层相对单纯;TCP/IP 模型则极其务实,让底层的网络层“轻装上阵”(不可靠、无连接),将复杂的可靠性保障全部推给了上面的传输层去实现。

物理层

通讯原理基本概念

1. 信号的分类

  • 数字信号:信号的值是离散的(如常见的方波,用高低电平表示 0 和 1)。
  • 模拟信号:信号的值是连续的(如连续波动的正弦波)。

QQ20260512-164906

2. 通信系统的基本模型(五大要素)

  • 数据:信息的实体(如文字、声音、图像),在计算机内部通常以二进制表示。
  • 信号:数据的载体。数据必须转换为信号才能在信道中传输。
  • 信源:信号的来源(即数据的发送方,如计算机 A)。
  • 信宿:信号的“归宿”(即数据的接收方,如路由器或计算机 B)。
  • 信道:信号传输的通道。注意:一条物理线路通常包含两条信道(发送信道和接收信道),以实现双向通信。

QQ20260512-164922

3. 码元(Symbol)的概念

  • 定义:在一个“信号周期”内出现的某一种特定状态的信号,称为一个码元。
  • 多进制码元:
    • 如果一个周期内信号有 4 种可能的取值(如 4 种不同的电压:2V, 1V, -1V, -2V),则称为 4进制码元,每个码元可以携带 2 个比特(2 bit)的数据(对应 00, 01, 10, 11)。
    • 同理,如果信号有 8 种状态,则是 8进制码元,每个码元携带 3 bit 数据。
  • 模拟信号设计:不仅数字信号可以设计多进制,模拟信号也可以通过改变波的特征(如不同的频率或相位组合)来设计出 2进制、4进制甚至更多进制的码元。
  • 优缺点:采用多进制码元的优点是每个信号周期能传输更多信息,提升传输效率;代价是需要更强的信号功率,且对信道质量(抗干扰能力)要求更高。

QQ20260512-165615

4. 核心公式:码元与比特的关系

这是计算网络传输速率的重要考点,描述了一个码元可以携带多少比特的数据:

  • 公式:如果一个信号周期内可能出现 $K$ 种信号(即 $K$ 进制码元),则:

  • 举例:

    • 4 种信号 ($K=4$):$\log_2 4 = 2 \text{ bit}$
    • 8 种信号 ($K=8$):$\log_2 8 = 3 \text{ bit}$
    • 32 种信号 ($K=32$):$\log_2 32 = 5 \text{ bit}$

奈氏准则(无噪声情形)和香农定理(有噪声情形)

1. 带宽(Bandwidth)的两种语境

虽然本质都是指“信道传输数据的能力”,但在不同学科中定义和单位有所不同:

  • 在《计算机网络》中:表示信道所能通过的“最高数据率”。单位是 bps(比特/秒)。
  • 在《通信原理》中:表示信道允许通过的信号频带范围。单位是 Hz(赫兹)。

2. 噪声与信噪比(SNR)

  • 噪声:会对信道产生干扰,从而影响数据的传输效率。
  • 信噪比:信号平均功率与噪声平均功率的比值。有两种表示方式:
    • 无单位记法:$S/N = \frac{\text{信号的功率}}{\text{噪声的功率}}$
    • 分贝(dB)记法:$\text{信噪比(dB)} = 10 \log_{10}(S/N)$

3. 奈奎斯特定理(奈氏准则)—— 针对“无噪声”信道

该定理用于计算在理想低通信道(没有噪声、带宽有限)下的极限传输速率。

  • 极限波特率(码元传输速率):$2W$ (单位:波特/Baud,即 码元/秒)

    • $W$ 是信道的频率带宽(单位:Hz)。
  • 极限比特率(数据传输速率):结合“1个码元可携带 $\log_2 K$ bit 数据”的公式,推导出极限比特率为:

    • $K$ 是一个“信号周期”内可能出现的信号状态种数(即多少进制的码元)。

4. 香农定理 —— 针对“有噪声”信道

该定理用于计算在实际信道(有噪声、带宽有限)下的极限传输速率。

  • 极限比特率公式:

    • $W$ 是信道的频率带宽(单位:Hz)。
    • $S/N$ 是信噪比。
  • ⚠️ 核心易错点提示:在使用香农定理计算时,公式中的信噪比 $S/N$ 必须使用“无单位记法”的比值形式。如果题目给出的是分贝(dB),必须先通过公式逆运算换算成无单位的比值后,才能代入香农公式计算。

编码与调制

QQ20260512-222444

image-20260519103945343

QQ20260512-221124

image-20260519104003426

传输介质

image-20260513084325475

image-20260513084342463

image-20260513084358459

image-20260513084435913

物理层设备

1. 中继器(Repeater)的工作原理

  • 核心功能:中继器只有两个端口。它的主要作用是接收衰减、失真的信号,将其整形再生(恢复成标准的电平信号),然后再转发到另一个端口,从而延长物理传输距离。
    • 示例:如果标准低电平是 0.5~1.5V,中继器收到失真的低电平信号后,会将其重新整形为标准的 1V 再输出。
  • 通信限制:仅支持半双工通信,即两端的节点不能同时发送数据,否则会导致信号“冲突”。
  • 网段概念:中继器的两个端口对应两个不同的“网段”。

2. 集线器(Hub)与冲突域

  • 本质:集线器可以看作是一个“多端口的中继器”。
  • 冲突域(碰撞域):连接在同一个集线器上的所有主机处于同一个冲突域。这意味着如果两台主机同时发送数据,信号就会在集线器内部发生冲突。
  • 信道争用:处于同一冲突域的主机,在发送数据前必须进行“信道争用”(抢占发送权)。

3. 集线器组网的关键特性

  • 拓扑结构差异:
    • 物理上是“星形”拓扑:各台主机通过网线汇聚连接到中心的集线器上。
    • 逻辑上是“总线型”拓扑:数据在集线器内部是“广播式”传输的(发给一个节点,其他节点都能收到),工作机制与早期共享一根总线的网络完全一样。
  • 共享带宽:集线器连接的所有设备共享该集线器的总带宽。
    • 示例:如果一个集线器的总带宽是 10Mbps,连接了 8 台主机同时工作,那么每台主机平均只能分到 1.25Mbps 的带宽。

4. 中继器与集线器的通用限制

  • 不能“无限串联”:为了保证信号的有效传输和冲突检测,串联数量有严格限制。
    • 5-4-3 原则(以 10Base5 网络为例):最多只能串联 5 个网段,中间使用 4 个集线器(或中继器),并且这 5 个网段中只有 3 个网段可以挂接计算机。
  • 速率向下兼容:如果一个集线器连接了不同速率的网段(或网卡),会导致整个网络的所有网段强制“向下兼容”,以最低的那个速率运行。
  • 介质兼容性:集线器两端可以连接不同的传输介质,这意味着它连接的网段在物理层面的接口特性(物理层协议)是可以不同的。

image-20260513085749075

数据链路层

功能

1. 数据链路层所处的地位

  • 核心作用:数据链路层位于网络层和物理层之间,负责将网络层交下来的IP数据报(分组)加上首部和尾部,封装成帧。
  • 服务关系:
    • 向下:使用物理层提供的“比特传输”服务。
    • 向上:为网络层提供服务,负责将封装好的帧传输给下一个相邻结点(如从主机传到路由器)。
  • 两个核心概念辨析:
    • 物理链路:由底层传输介质(0层)和物理层(1层)共同实现,是相邻结点间单纯的物理连接线路。
    • 逻辑链路(数据链路):数据链路层基于“物理链路”,通过相关协议实现了相邻结点间逻辑上无差错的数据传输通道。

2. 数据链路层的功能(五大核心)

第二张图以思维导图的形式概括了数据链路层必须完成的具体任务:

  • 封装成帧(组帧)
    • 帧定界:确保接收方能够准确识别出一个完整帧的界限(哪里开始,哪里结束)。
    • 透明传输:接收方的链路层能从收到的帧内无损恢复原始数据(SDU),让上方的网络层“感受不到”底层曾把数据切分并封装成帧的过程。
  • 差错控制
    • 专门用于发现并解决帧内部的“位错”(即具体的0、1比特发生翻转)。
    • 两种解决思路:一是接收方检错后丢弃该帧,让发送方重传(需检错编码);二是由接收方直接发现并自行纠正错误(需纠错编码)。
  • 可靠传输
    • 专门用于发现并解决宏观的“帧错”,包含三种典型情况:
      • 帧丢失:例如发 1234,只收到 124。
      • 帧重复:例如发 1234,收到了 12334。
      • 帧失序:例如发 1234,收到了 1324。
  • 流量控制
    • 控制发送方发送帧的速率,防止发送过快导致接收方“来不及”接收和处理。
  • 介质访问控制
    • 广播信道:必须实现此功能。因为广播信道逻辑上是总线型拓扑,多个结点需要“争抢”传输介质的使用权,必须有机制来协调。
    • 点对点信道:通常不需要此功能,因为两点之间有专属的传输介质,不存在信道争用问题。

QQ20260513-090143

组帧

1. 字符计数法

  • 原理:在每个帧的开头,使用一个固定长度的数字字段来表示该帧的总长度(注意:帧长 = 计数字段长度 + 数据部分长度)。
  • 最大缺陷(健壮性差):如果传输过程中由于干扰导致计数字段出错(例如原本长度是7变成了3),接收方就会找错边界,不仅当前帧报废,还会导致后续所有帧都无法正确定界,产生连锁反应。

image-20260513092318404

2. 字节填充法(特殊字符法)

  • 帧定界:使用特殊的控制字符来标记帧的开始(如 SOH - Start of Header)和结束(如 EOT - End of Transmission)。
  • 透明传输:如果帧的数据部分恰好包含了与 SOH 或 EOT 相同的比特序列,发送方会在这些特殊字符前面插入一个“转义字符 ESC”。接收方在读取时如果遇到 ESC,就会知道紧跟其后的字符是普通数据,并在处理时将 ESC 删掉(逆向处理)。

image-20260513092345270

3. 零比特填充法

  • 帧定界:使用特殊的比特串 01111110 作为标记帧开始和结束的标志。
  • 透明传输(核心规则):
    • 发送方:对数据部分进行扫描,每当遇到连续的 5 个 1,就强制在后面填充一个 0。这样就绝对避免了数据段出现连续 6 个 1(即伪造的定界符)。
    • 接收方:对收到的数据部分进行逆处理,每当遇到连续的 5 个 1,就自动删掉紧随其后的 1 个 0,从而恢复原始数据。
  • 应用:HDLC协议和PPP协议均采用此方法。

image-20260513092358876

4. 违规编码法

  • 原理:利用物理层传输中“不合法的(违规)”信号电平来表示帧的开头和结尾。这种方法需要物理层的配合。
  • 示例:在标准的曼彻斯特编码中,每个比特周期的中间必须有电平跳变(上跳表示0,下跳表示1)。如果人为制造一个“周期中间不跳变”的信号,这就是一个违规信号,接收方就可以用这个特殊的不跳变信号来作为帧的定界符

image-20260513092412375

检错编码

这些图片是一份关于计算机网络数据链路层差错控制(检错编码)\的详细教学课件,重点讲解了两种常见的检错码:**奇偶校验码**和*循环冗余校验码(CRC)*。

以下是核心知识点的详细总结:

1. 奇偶校验码 (Parity Check)

  • 基本原理:在原有的“有效信息位”附加 1 位的“奇偶校验位”,使得整个校验码中数字“1”的个数满足奇数或偶数的要求。
    • 奇校验:整个校验码中“1”的个数为奇数。
    • 偶校验:整个校验码中“1”的个数为偶数。
  • 硬件实现(以偶校验为例):将各信息位进行“异或(模2加)”运算(即相同为0,相异为1),得到的结果就是偶校验位的值。
  • 致命缺陷:如果传输过程中发生位错的比特数是偶数个(比如2位、4位同时翻转),奇偶性不会改变,此时无法检测出错误。

image-20260513110906416

image-20260513110915033

2. 循环冗余校验码 (CRC, Cyclic Redundancy Check)

CRC 是一种检错能力极强的编码方式,广泛应用于实际网络中。

  • 核心思想:发送方和接收方事先约定一个“除数”。发送方在数据后面添加若干位“校验位”(即余数),使得拼接后的完整数据能够被这个约定的“除数”整除。接收方收到数据后,用同样的“除数”去除,如果余数为 0,说明传输无误;若余数非 0,说明数据出错。
  • 运算规则:CRC 的计算使用的是“模2除法”,其本质是按位进行异或运算(不发生进位和借位)。

💡 CRC 计算的四大步骤(以课件例题为例):

例:生成多项式 $G(x) = x^3 + x^2 + 1$,信息码为 101001

  1. 确定“除数”和位数 $R$:
    • 将生成多项式的系数提取出来,转换为二进制“除数”。$x^3+x^2+1$ 对应二进制为 1101。
    • 确定校验位的长度 $R$。$R$ 等于生成多项式的最高次幂,即 $R = 3$(除数位数减1)。
  2. 移位(补零):
    • 将信息码左移 $R$ 位,即在信息码后面补上 $R$ 个 0,作为“被除数”。本例中变为 101001000。
  3. 相除(模2除法):

    • 用被除数 101001000 模2除以除数 1101。
    • 经过一步步异或运算,最后得到的 $R$ 位余数即为 CRC 校验码(FCS)。本例中余数为 001。4

    image-20260513110952903

  4. 生成发送帧与检错:

    • 发送方:将余数拼接在原始信息后,实际发送的数据为 101001001。

    • 接收方:收到数据后,用 1101 进行模2除法,若最终余数为 000,则代表没有出错。

image-20260513111052010

3. CRC 的检错能力与特点

  • 理论上,只要生成多项式选择得当,CRC 可以检测出:所有奇数个错误、所有双比特错误、以及所有长度小于等于校验位长度的连续错误。
  • 在特定条件下(如 $2^R \ge K+R+1$),CRC 码可以具备纠正 1 位错的能力,但在计算机网络的实际应用中,CRC 一般只用来“检错”,发现错误直接丢弃重传,而不进行复杂的纠错

image-20260513111109881

海明校验码(Hamming Code)

1. 海明码的核心设计思路与长度计算

  • 设计原理:将信息位进行分组,并为每组提供一个偶校验位。通过多个校验位的组合,不仅能检测出错误,还能直接“指引”出发生错误的具体位置。

  • 校验位数量公式:假设有 $n$ 个信息位,需要添加 $k$ 个校验位。为了让这 $k$ 个校验位能指示出所有 $(n+k)$ 个位置的错误状态以及 1 种正确状态,必须满足不等式:

    (例如:课件中 4位信息位 $n=4$,代入公式计算得出至少需要 $k=3$ 个校验位)

2. 海明码的求解步骤(以信息位 1010 为例)

构建海明码主要分为三步:

  • Step 1:确定位置分布
    • 校验位 $P_i$ 必须放在海明位号为 $2^{i-1}$ 的位置上(即 $H_1, H_2, H_4, H_8 \dots$)。
    • 信息位 $D_i$ 按照顺序填入剩下的空缺位置(如 $H_3, H_5, H_6, H_7$)。
  • Step 2:确定校验分组
    • 每个校验位负责校验海明位号二进制表示中特定位为 1 的集合。
    • $P_1$ 负责校验位号二进制最低位为 1 的位置($H_1, H_3, H_5, H_7$)。
    • $P_2$ 负责校验位号二进制次低位为 1 的位置($H_2, H_3, H_6, H_7$)。
    • $P_3$ 负责校验位号二进制第三位为 1 的位置($H_4, H_5, H_6, H_7$)。
  • Step 3:计算校验位的值(偶校验)
    • 利用异或运算($\oplus$)求解,确保每组内的 1 的个数为偶数。
    • 计算得出 $P_1 = 0$, $P_2 = 1$, $P_3 = 0$。拼接得到最终的 7位海明码。

3. 海明码的检错与纠错机制

接收方收到数据后,需要计算校验方程(Syndrome) $S_1, S_2, S_3$:

  • 计算方法是将接收到的各分组数据再次进行异或运算。
  • 判定规则:
    • 如果 $S_3 S_2 S_1 = 000$,说明传输无错误。
    • 如果 $S_3 S_2 S_1 \neq 000$,其二进制值直接对应出错的海明位号。(例如:计算出 010,对应的十进制是 2,说明第 $H_2$ 位出错了,只需将该位翻转即可纠正)。

4. 补充考点:全校验位(解决 1位错与 2位错的区分)

标准的海明码具有“纠 1位错,检 2位错”的能力,但仅凭 $S_3 S_2 S_1$ 无法区分到底是发生了 1位错还是 2位错。

  • 解决方案:在最高位(如 $H8$)增加一个“全体偶校验位”($P{\text{全}}$),对前面的所有位进行整体偶校验。
  • 综合判定逻辑:
    1. $S_3 S_2 S_1 = 000$ 且 整体校验成功 $\to$ 无错误。
    2. $S_3 S_2 S_1 \neq 000$ 且 整体校验失败 $\to$ 有 1位错(直接用 $S$ 的值定位并纠正)。
    3. $S_3 S_2 S_1 \neq 000$ 且 整体校验成功 $\to$ 有 2位错(此时海明码失效,无法纠正,必须要求重传)

QQ20260513-100924

QQ20260513-101157

滑动窗口机制

QQ20260513-112016

QQ20260513-112118

停止等待协议

1. 停止-等待协议的核心机制 (图1)

该协议的运行依赖于四大机制:

  • 滑动窗口机制:发送窗口大小 $W_T=1$,接收窗口大小 $W_R=1$。即“发送方发一个,等一个;接收方收一个,确认一个”。
  • 确认机制:如果接收方收到 $i$ 号帧且没有检测出差错,必须给发送方返回一个确认帧($ACK_i$)。
  • 重传机制(超时重传):发送方发出数据后会启动计时器。如果超时仍未收到对应的 $ACK_i$,则认为数据丢失,会重新发送 $i$ 号帧。
  • 帧编号:为了区分不同的帧,需要给帧编号。在 S-W 协议中,由于窗口大小都是 1,仅需 1 bit(即 0 和 1 交替使用)即可满足编号要求。

2. 帧的交互与序号概念 (图2)

  • 发送方与接收方交替使用 0 和 1 作为帧序号(如 Data0, Data1, Data0…)。
  • 帧结构:数据帧包含首部、可长可短的数据部分和尾部;而确认帧(ACK帧)通常只有首尾的控制信息(如帧序号、帧类型),数据部分非常短甚至为空。
  • 正常交互过程:发送方发送 Data0,接收方收到后回复 ACK0;接着发送方再发送 Data1,以此类推。

QQ20260513-112732

3. 异常处理:超时重传 (图3)

图片演示了网络中出现异常的情况:

  • 如果发送方发送了帧 C(序号为0),但在计时器超时前没有收到对应的确认帧(可能是数据帧在路上丢失,也可能是ACK帧丢失或迟到)。
  • 发送方的时间一到,就会触发“超时重传”机制,重新发送一遍序号为 0 的帧 C。

4. 核心探讨:为什么要给帧编号? (图4)

这是该协议的一个重要考点:

  • 如果没有帧序号会发生什么?

    如果发生超时重传,接收方再次收到同一个帧时,将无法判别这是一个新到达的数据帧,还是一个“重复帧”(即由于之前自己发的ACK丢了,发送方重新发来的老数据)。

  • 为什么 1 bit 就够了?

    因为在 S-W 协议中,接收窗口和发送窗口的距离不超过 1,新帧和旧帧只会在相邻的两次传输中产生混淆,因此用 0 和1 交替表示(1 bit)就已经足够区分了。

后退 N 帧协议(Go-Back-N, 简称 GBN 协议)

1. GBN 协议的核心机制

  • 滑动窗口大小:发送窗口 $W_T > 1$(可以连续发送多个数据帧而无需等待确认),接收窗口 $W_R = 1$(接收方只能按顺序依次接收一个帧)。
  • 累积确认(Cumulative ACK):这是 GBN 的特殊规则。接收方不需要每收到一个帧就发一个确认,它可以连续收到多个数据帧后,只发送最后一个正确按序到达帧的确认帧(ACK_i)。ACK_i 的含义是:“序号 $i$ 及其之前的所有帧我都已经正确收到了”。
  • 超时重传(后退 N 帧):如果发送方的计时器超时未收到 ACK_i,发送方不仅要重传第 $i$ 号帧,还必须重传第 $i$ 号帧之后已经发送出的所有帧。这就是“后退 N 帧”名字的由来。

QQ20260514-082558

2. 异常情况示例与处理

  • 数据帧丢失 / 失序到达:

    • 假设发送方连续发送了 0、1、2 号帧,但 1 号帧在路上丢失了。
    • 接收方正确收到了 0 号帧(接收窗口向前移动,期待 1 号帧)。接着接收方收到了 2 号帧。
    • 由于 2 号帧超出了当前 $W_R = 1$ 的接收窗口(接收方只想要 1 号),2 号帧被视为“非法帧”并被直接丢弃。
    • 接收方会再次向发送方返回 ACK0(目前已按序接收的最后一个正确帧),提醒发送方:“我还在等 1 号帧”。
    • 最终发送方超时,被迫“后退”,重新发送 1 号帧和 2 号帧。

    image-20260514083851115

  • 确认帧(ACK)丢失:

    • 如果前面的 ACK 丢失,但后续的 ACK 成功到达,由于“累积确认”机制,发送方不会重传。
    • 如果一段时间内没有任何 ACK 到达导致超时,发送方会重置计时器,并重传该窗口内未被确认的所有数据帧。

3. 窗口尺寸的强制约束

为了让接收方能正确区分新帧和旧帧(避免重复帧被当成新帧接收),帧序号的比特数 $n$ 与窗口大小必须满足严格的数学关系:

  • 公式:$W_T + W_R \le 2^n$
  • 反面探讨:如果采用 2bit 编号(序号为 0, 1, 2, 3,即 $2^n = 4$),在 $W_R = 1$ 的情况下,发送窗口 $W_T$ 最大只能是 3。如果强行设置 $W_T = 4$,当发送方发出 0, 1, 2, 3 号帧且接收方全部收到后,接收方期待下一个新 0 号帧。若此时 ACK 丢失导致发送方超时重传旧 0 号帧,接收方会将其错误地当作新数据接收,导致严重的数据错乱。

QQ20260514-083228

4. GBN 协议的优缺点

  • 优点:相比停止-等待协议,允许连续发送多个帧,提高了传输效率。
  • 缺点:如果信道误码率很高或接收方处理慢,一旦中间某个帧出错,发送方必须把后面哪怕已经正确传达的帧也全部重传一遍,导致传输效率大幅下降。

QQ20260514-083247

选择重传协议(Selective Repeat, 简称 SR 协议)

1. SR 协议的核心机制与革新

  • 滑动窗口机制:发送窗口 $W_T > 1$,接收窗口 $W_R > 1$。这是与 GBN 最大的区别。接收窗口大于 1 意味着接收方可以缓存失序到达的帧,而不会像 GBN 那样直接丢弃。
  • 确认机制(一帧一确认):SR 协议不再使用累积确认。接收方收到哪个帧,就单独为那个帧发送确认帧($ACK_i$)。
  • 否定确认(NAK):如果接收方发现某个帧有差错(如校验和错误),不仅会丢弃它,还会主动向发送方返回一个否定确认帧($NAK_i$),要求发送方立即重传该帧,而不需要死等计时器超时。
  • 选择重传:当发送方发生超时或收到 $NAK_i$ 时,仅需重传那一个出错或丢失的 $i$ 号帧,无需重传后续已经发送的帧。

2. 异常情况处理示例

  • 数据帧丢失:发送方发出 0~5 号帧,其中 5 号帧丢失,6、7、0 号帧失序到达接收方。由于接收窗口够大,接收方会缓存 6、7、0 号帧并分别返回 ACK。当发送方的 5 号帧超时后,发送方仅重传 5 号帧。
  • 数据帧出错(被丢弃):5 号帧在传输中发生位错,接收方检测出差错后将其丢弃,并主动发送 NAK 5。发送方收到 NAK 5 后,立即请求重传 5 号帧(无需等待超时)。

QQ20260514-085126

  • 确认帧(ACK)丢失:如果某个帧的 ACK 丢失导致发送方超时重传,接收方会再次收到该帧。接收方发现这是一个落在当前接收窗口之外(或者在窗口内但已被标记接收)的重复帧,接收方会直接丢弃该帧,但必须重新返回一次该帧的 ACK,以让发送方能向前滑动窗口。

3. 窗口尺寸的强制约束

为了保证发送方和接收方能正确识别新帧和旧帧(避免重复帧落入新窗口被错误接收),SR 协议对窗口大小有严格的数学要求:

  1. 最大序号限制:$W_T + W_R \le 2^n$ ($n$ 为帧序号的比特数)。
    • 课件探讨:如果用 3bit 编号(0~7),若 $W_T=5, W_R=4$(不满足公式),当 0~4 号帧的 ACK 全部丢失,发送方超时重传旧的 0 号帧时,接收方窗口已经滑动到了等待新 0 号帧的位置,此时接收方会错误地将“旧 0 号帧”当成“新 0 号帧”接收。
  2. 收发窗口比例:通常要求 $W_R \le W_T$(接收窗口不能大于发送窗口,因为没必要)。在实际应用中,通常取两者的最大允许值,即 $W_T = W_R = 2^{n-1}$。

QQ20260514-085845

协议信道利用率

1. 核心概念与符号定义

在计算信道利用率时,一个完整的“发送-确认”周期(Cycle)包含以下几个时间分量:

  • $T_D$:数据帧的传输时延(发送时延)。即把数据比特流推送到链路上所需的时间。
  • $RTT$:往返传播时延。等于 $2 \times$ 单向传播时延,即信号在物理介质中跑一个来回的时间。
  • $T_A$:确认帧(ACK)的传输时延(发送时延)。在很多考题中,由于 ACK 帧很短,这个时间常被忽略(视为 0)。

一个完整的周期时间 = $T_D + RTT + T_A$

2. S-W 协议的信道利用率

  • 工作机制:发送方每发送 1 个数据帧,就必须停下来等待确认,收到 ACK 后才能发送下一个帧。

  • 理想利用率公式:

  • 物理意义:在一个长长的周期时间里,信道只有在 $T_D$ 这段时间是在真正传输有效数据的,其余时间都在“空等”,因此信道利用率往往非常低。

  • 课件示例:$U = \frac{4}{4 + 2 \times 7 + 1} \approx 21\%$

QQ20260514-090835

3. GBN 与 SR 协议的信道利用率(滑动窗口协议)

  • 工作机制:发送方拥有大小为 $N$ 的发送窗口,可以连续不断地向信道中“注入” $N$ 个数据帧,而无需等待每个帧的 ACK。

  • 理想利用率公式:

  • 物理意义:由于连续发送了 $N$ 个帧,在一个周期内的有效数据发送时间变长了($N \cdot T_D$),因此信道利用率得到了大幅提升。注意:信道利用率的极限是 1(即 100%),不能超过这个值。

  • 课件示例:$N=4$ 时,$U = \frac{4 \times 4}{4 + 2 \times 7 + 1} \approx 84\%$

QQ20260514-091433

4. 易考点与术语补充

  • 窗口大小对利用率的影响:对于采用 $n$ 比特进行帧编号的系统,必须满足 $W_T + W_R \le 2^n$。
    • GBN 协议的接收窗口 $W_R = 1$。
    • SR 协议的接收窗口 $W_R > 1$。
    • 因此,在相同的比特位 $n$ 下,GBN 协议可以配置出更大的发送窗口 $W_T$,在理论上其所能达到的最大信道利用率会比 SR 协议更高。
  • 术语辨析:
    • 滑动窗口协议:特指 GBN 或 SR 协议。
    • 连续 ARQ 协议:也是指 GBN 或 SR 协议。
    • ARQ 协议:是 S-W、GBN、SR 协议的总称。

QQ20260514-092437

介质访问控制(MAC)

1. 介质访问控制(MAC)的概念

  • 问题背景:当多个节点(如A, B, C, D, E)共享同一个“总线型”广播信道时(例如早期的同轴电缆局域网,或现在的 WiFi、5G 等无线通信),如果多个节点同时发送数据,就会发生“信号冲突”。
  • 核心目的:介质访问控制(MAC)就是一套规则,用来控制各个节点对传输介质的访问权限,从而减少甚至避免冲突。

2. 频分复用(FDM, Frequency Division Multiplexing)

  • 原理:将共享信道的总频带(总带宽,Hz)在频率维度上划分成多个独立的“子频带”。每个子频带作为一个子信道,分配给一对用户专属使用。
  • 技术细节:发送端通过“复用器”将各个节点发出的信号复合到共享信道上;接收端通过“分用器”将不同子频带的信号分离开来。为了防止子信道之间相互干扰,子频带之间必须留有“隔离频带”。
  • 优缺点:
    • 优点:各节点可以同时发送信号,互不干扰,充分利用了信道带宽。
    • 缺点:主要用于模拟信号的传输。

QQ20260514-102253

3. 波分复用(WDM, Wavelength Division Multiplexing)

  • 原理:本质上就是光的频分复用。根据物理公式 $C = \lambda f$(光速 = 波长 × 频率),光的频率与波长成负相关。波分复用就是将不同波长的光信号复合在一起,在同一根光纤中传输。
  • 特点:由于光信号的频带范围(带宽)极其庞大,一根光纤在逻辑上可以被拆分成海量的子信道,极大地提升了传输容量。

4. 时分复用(TDM, Time Division Multiplexing)

  • 原理:将共享信道的传输时间划分为等长的“TDM帧”,每个TDM帧内部再划分为等长的 $m$ 个“时隙”(Time Slot)。将这 $m$ 个时隙固定分配给 $m$ 对用户使用。
  • 特点:用户在分配给自己的时隙内,独占整个信道的频率带宽;但在时间上,各用户是轮流使用信道的。
  • 致命缺点:分配是静态的、固定的。如果某个节点暂不发送数据,分配给它的“时隙”就会一直空着(闲置),导致信道利用率很低。并且每个节点最多只能分到信道总带宽的 $1/m$。

5. 统计时分复用(STDM, Statistic Time Division Multiplexing)

为了解决 TDM 效率低下的问题,引入了 STDM(也称异步时分复用)。

  • 原理:在 TDM 的基础上,不再固定分配时隙,而是根据统计到的各个节点对信道的使用需求,动态按需分配时隙。
  • 优点:
    • 如果某个节点不发数据,就不会分给它时隙,从而避免了时隙闲置,信道利用率更高。
    • 在极端情况下(比如只有节点 A 需要大量发数据),节点 A 可以在一段时间内连续占用所有的时隙,从而瞬间获得所有的信道带宽资源。

QQ20260514-100531

码分复用(Code Division Multiplexing, 简称 CDM)

1. 码分复用(CDM)的核心设计思想

与时分(分时间)、频分(分频率)不同,码分复用允许多个节点在同一时间使用同一频带进行通信,它是通过一种数学编码手段来区分不同信号的。

  • 专属码片序列:网络中的每个节点都会被分配一个独一无二的“码片序列”,通常包含 $m$ 个码片。这可以看作是一个 $m$ 维向量(分量通常取 $1$ 或 $-1$)。
  • 正交要求(核心前提):为了让信号混合后还能被成功分离,分配给各节点的 $m$ 维向量必须相互正交。即任意两个不同节点的向量进行内积计算,结果必须为 $0$(如 $\vec{a} \cdot \vec{b} = 0$)。

2. 发送方如何发送数据?

当节点需要发送二进制比特流时,遵循以下规则:

  • 发送比特 1:节点发出与自身专属“码片序列”完全相同的 $m$ 个信号值(即原向量 $\vec{a}$)。
  • 发送比特 0:节点发出与自身专属“码片序列”完全相反的 $m$ 个信号值(即取反向量 $-\vec{a}$)。

3. 信号的传输与“叠加”

  • 当多个节点(如节点 A 和节点 B)同时向信道发送数据时,它们发出的电磁波信号会在物理信道中产生叠加。
  • 在数学上,这种物理叠加本质上就是多个 $m$ 维向量的线性加法(如 $\vec{a} + \vec{b}$ 或 $\vec{a} + (-\vec{b})$)。

4. 接收方如何“分离”并接收数据?

接收方收到的是混合后的“叠加信号”。由于接收方知道网络中所有节点的专属码片序列,它可以利用正交特性进行数学分离:

  • 规格化内积:将收到的“叠加信号”与目标发送方的“码片序列”进行规格化内积运算(即向量点乘后除以维数 $m$)。
  • 数学消除:由于不同节点的向量相互正交,在计算内积时,其他节点的干扰信号会被消去(乘积为 $0$),只留下目标节点的信号。
  • 解码判定结果:
    • 如果规格化内积结果为 $1$,表示该发送方发送了比特 1。
    • 如果规格化内积结果为 $-1$,表示该发送方发送了比特 0。
    • 如果结果为 $0$,表示该发送方在此刻没有发送数据。

QQ20260514-104017

随机访问介质访问控制

image-20260518164351616

上半层:LLC(逻辑链路控制)—— 转运中心的“调度文员”

  • 位置: 紧贴着网络层(第 3 层)。
  • 核心职责:
    • 识别与复用(Multiplexing): 拆开从下层送上来的包裹,看看里面的数据到底是要交给 IP 协议、ARP 协议,还是其他网络层协议。(这就是它“逻辑”的体现,它负责软件层面的对接)。
    • 提供服务接口: 根据需求提供无确认的无连接服务、面向连接的服务等(虽然在现代以太网中,这部分功能被弱化了)。
  • 特点: 它完全是个纯软件逻辑概念,与具体的物理传输介质(光纤、铜线、空气)毫无关系。

下半层:MAC(介质访问控制)—— 转运中心的“车队队长”

  • 位置: 紧贴着物理层(第 1 层)。
  • 核心职责:
    • 组装包裹(成帧): 给 LLC 交下来的数据加上“源 MAC 地址”和“目的 MAC 地址”(我们在局域网里认的就是这个硬件地址),并在末尾加上我们前面算过的 CRC 校验码(FCS)。
    • 交通指挥(介质访问): 决定这一秒钟,网卡到底能不能往物理线路上发脉冲信号(也就是我们前面讨论的 CSMA 机制)。
  • 特点: 它和物理硬件深度绑定。你换一块 Wi-Fi 网卡和插一根网线,用的就是完全不同的 MAC 协议。

QQ20260514-175134

1. ALOHA 协议家族

这是最基础的随机访问协议,核心特点是“想发就发”,缺乏对信道状态的探测。

  • 纯 ALOHA (Pure ALOHA):
    • 规则:节点只要准备好数据帧,就立刻发送到信道上。
    • 冲突处理:如果发送后超时未收到确认(ACK),说明发生了冲突。节点会随机等待一段时间后再次尝试重传。
    • 缺点:由于完全不关心信道是否被占用,多节点发送时极易发生帧在时间上的重叠(冲突),信道利用率极低。
  • 时隙 ALOHA (Slotted ALOHA):
    • 规则:将时间划分为等长的“时隙”(大小等于传输一个最大帧的时间)。节点只能在每个时隙的开始时刻才能发送数据。
    • 优点:通过强制时间同步,避免了用户发送数据的随意性。帧要么完全不冲突,要么完全重叠冲突,消除了一半的冲突情况,相比纯 ALOHA 降低了冲突概率,提高了信道利用率。

image-20260514112359990

QQ20260514-105359

2. CSMA 协议(载波监听多路访问)

CSMA 的英文全称是 Carrier Sense Multiple Access,中文通常翻译为载波监听多路访问

为了弥补 ALOHA 协议“不听就发”的缺陷,引入了 CSMA 协议。

  • 核心改进(先听后发):在发送数据之前,必须先监听信道是否空闲。只有在信道空闲时,才会尝试发送。

根据监听策略和发送时机的不同,CSMA 衍生出三种不同的具体协议:

① 1-坚持 CSMA (1-Persistent CSMA)

  • 规则:
    • 如果信道空闲,立刻发送数据(发送概率为 1)。
    • 如果信道忙碌,节点会“坚持”监听信道,直到信道变得空闲,然后立刻发送。
  • 优缺点:信道利用率高(一旦空闲马上被利用)。但冲突概率大:如果多个节点都在等待信道空闲,一旦信道释放,它们会同时发送数据,必然导致冲突。

image-20260514112458260

② 非坚持 CSMA (Non-Persistent CSMA)

  • 规则:
    • 如果信道空闲,立刻发送数据。
    • 如果信道忙碌,节点放弃监听信道,随机推迟一段时间后,再重新尝试监听。
  • 优缺点:节点通过随机推迟“错开”了发送时间,大大降低了冲突概率。但由于节点盲目等待,可能信道已经空闲了节点还在等待,导致信道利用率降低。

image-20260514112514738

③ p-坚持 CSMA (p-Persistent CSMA)

  • 规则:
    • 如果信道忙碌,节点“坚持”监听。
    • 如果信道空闲,节点以概率 p 立刻发送数据,以概率 1-p 推迟一段特定的时间(通常是一个端到端传播时延)再重新监听。
  • 优缺点:这是 1-坚持和非坚持方案的折中。既保留了“坚持监听”以提高利用率的优点,又通过“概率发送”降低了多个节点同时发送导致的冲突概率。

image-20260514112524276

QQ20260514-110951

CSMA/CD 协议(带冲突检测的载波监听多路访问)

QQ20260514-171125

1. CSMA/CD 协议的核心口诀

  • 先听后发:发送数据前先监听信道是否空闲(基于 1-坚持 CSMA,即若忙碌则坚持监听,一旦空闲立即发送)。
  • 边听边发:在发送数据的同时,持续监听信道,检测发出的信号是否与其他节点的信号发生碰撞。
  • 冲突停发:一旦检测到冲突,立即停止发送,以节省信道资源。
  • 随机重发:发生冲突后,根据特定的算法随机等待一段时间再重新尝试发送。

QQ20260514-171758

2. “争用期”与无冲突确认

  • 定义:争用期是指一个节点发出数据后,最多需要多久才能确信自己成功“占领地盘”(即确认没有发生冲突)。
  • 计算公式:$\text{争用期} = 2 \times \text{最大单向传播时延}$。这考虑了最极端的情况:两端距离最远的节点 A 和 B,A 发出的信号刚要到达 B 时,B 也开始发送,冲突信号传回 A 刚好需要两倍的传播时延。
  • 重要结论:CSMA/CD 协议没有 ACK 机制。如果节点在经过一个“争用期”后依然没有检测到冲突,就可以断定这次发送一定成功,后续就不可能再发生冲突了。

QQ20260514-173505

3. “最短帧长”的限制

这是为了保证“边听边发”机制有效而设立的硬性规定。

  • 原因:节点只有在发送数据的过程中才能检测冲突。如果帧太短,节点很快就发完了,但此时冲突信号还在路上(还没传回节点)。当冲突信号到达时,节点已经发完了,它会“误以为”发送成功,从而导致错误。

  • 计算公式:

    (注:以太网规定最短帧长为 64B 即 512bit)。

  • 处理规则:如果实际要发的数据很少,必须“填充”无用数据至合法长度后再发送。接收方若收到小于最短帧长的数据,会直接视为因冲突而中断的无效帧(非法帧)。

QQ20260514-174654

4. 冲突后的退避:截断二进制指数退避算法

当检测到冲突后,节点需要决定等待多久再重发,这里采用的是动态调整等待范围的算法:

  • 退避时间 = $r \times \text{争用期}$,其中 $r$ 是从指定区间内随机抽取的一个整数。
  • 区间计算逻辑:设冲突次数为 $k$。
    1. 当 $k \le 10$ 时,从 $[0, 2^k - 1]$ 区间内随机取整数 $r$。随着冲突次数增加,区间成指数级扩大,意味着节点可能等待的时间越来越长,以此来疏散拥挤的信道。
    2. 当 $10 < k \le 15$ 时,区间不再扩大,固定为 $[0, 2^{10} - 1]$(即 $[0, 1023]$)。
    3. 分水岭:当第 16次冲突发生时,协议会直接“躺平”,放弃传输该帧,并向上级网络层报告错误。

CSMA/CA 协议(带冲突避免的载波监听多路访问)

1. 为什么 WiFi 不用 CSMA/CD?

课件首先对比了有线网的 CSMA/CD 和无线网的 CSMA/CA,解释了无线网络不能使用“冲突检测(CD)”的两个致命原因:

  • 硬件实现极其困难:在无线环境中,设备自身发出的信号强度往往远大于接收到的微弱信号(因为信号在空气中衰减极快)。“边发边听”会导致自己的声音掩盖了别人的声音,根本无法有效检测出冲突。
  • “隐藏站”问题:以太网的碰撞检测是假定了所有的站点都能够听到其他站点是否在发送数据。但在无线局域网的工作环境中,这个假定是不能成立的。在无线通信中,并非所有节点都能互相听见。比如节点 A 和 C 都离中心热点(AP)很近,但 A 和 C 彼此距离很远(超出了信号范围)。此时 A 监听信道会觉得“空闲”,C 也觉得“空闲”,如果它们同时向 AP 发送数据,就会在 AP 处发生严重的冲突。对 A 而言,C 就是一个看不见的“隐藏站”。

QQ20260514-180412

2. CSMA/CA 的核心要点(先发制人,尽量避免)

既然无法在发送中检测冲突,CSMA/CA 的思路就是在发送前想尽一切办法避免冲突。

  • 先听后发:
    • 若信道空闲,必须再等待一个DIFS(分布式协调功能帧间间隔)的时间后,才能发送帧。一旦开始发送,就一口气发完,中途不检测冲突。
    • 若信道忙碌,则强制进入“随机退避”状态。
  • “随机退避”原理:
    • 使用二进制指数退避算法设定一个随机倒计时。
    • 冻结机制:节点在倒计时期间必须保持监听。只有当信道“听起来”空闲时,倒计时才扣减;一旦信道忙碌,倒计时立即冻结。当倒计时归零时,信道必然是空闲的,节点立即发送数据。
  • 确认机制(ACK):因为无法检测冲突,发送方必须依靠接收方返回的 ACK 帧 来确认是否发送成功。如果超时未收到 ACK,发送方只能认为发生了冲突,再次进入“随机退避”并重传(类似于停止-等待协议)。

3. 信道预约机制(解决“隐藏站”的杀手锏)

为了彻底解决隐藏站问题,CSMA/CA 提供了一个可选的信道预约功能(RTS/CTS):

  1. RTS(请求发送):发送方(如节点 A)在发数据前,先向 AP 广播一个极短的控制帧 RTS,里面包含了预计要占用信道的时间。
  2. CTS(允许发送):AP 收到 RTS 后,向四周广播一个 CTS 帧作为回应,里面同样包含预计占用时间。
  3. 虚拟载波监听(禁言机制):关键在于这一步。周围所有的节点(包括 A 的隐藏站 C)一旦收到了 AP 发出的 CTS 帧,就会自动读取里面的时长信息,并设置一个内部计时器(NAV),在此期间自觉保持“禁言”,绝对不发送任何数据。
  4. 安全发送:节点 A 收到 CTS 后,就可以安心地发送冗长的数据帧了,因为此时整个网络都已被 AP “清场”。

4. 帧间间隔(IFS, InterFrame Space)

为了区分不同类型帧的优先级,协议规定了三种长度的等待时间:

  • SIFS(最短):留给接收方处理数据并立即回复(如发送 ACK 或 CTS)的时间,优先级最高。
  • PIFS(中等):用于 PCF 协调功能(考研通常不要求深入)。
  • DIFS(最长):普通的“帧事务”开始前必须等待的时间,优先级最低,为了让高优先级的控制帧先行。

QQ20260514-175254

QQ20260515-095412

QQ20260515-095750

局域网的基本概念和体系结构

QQ20260515-085544

1. 局域网(LAN)的分类与特性

  • 三大特性:覆盖较小的地理范围;具有较低的时延和误码率;局域网内的各节点之间以“帧”为单位进行传输。
  • 传输方式:支持单播(1对1,如A发给B)、广播(1对全体,如A发给所有人)、多播/组播(1对部分特定节点,如A发给B,D,E)。
  • 两大分类:
    • 有线局域网:如以太网(IEEE 802.3)、早期已被淘汰的令牌环网。
    • 无线局域网:如 WiFi(IEEE 802.11),采用星形拓扑(1个AP+多台移动设备),使用 CSMA/CA 协议。

2. 以太网(Ethernet)的发展演进

课件详细梳理了以太网传输介质和物理拓扑的演进路线:

  • 早期同轴电缆以太网(10Base5):物理上和逻辑上都是总线形,采用 CSMA/CD 协议争抢信道。
  • 双绞线以太网(10BaseT等):
    • 使用集线器(Hub):物理上是星形,但逻辑上依然是总线形,工作在半双工模式,必须使用 CSMA/CD 协议。
    • 使用交换机(Switch):物理和逻辑上都是星形。交换机支持全双工通信,此时各节点不再需要争抢信道,不需要使用 CSMA/CD 协议(这是一个重要考点)。
  • 光纤以太网(10BaseF等):主要用于扩大局域网覆盖范围。如果是点对点连接且用两条光纤实现全双工通信,同样不需要使用 CSMA/CD。

3. 网络适配器(网卡 / NIC)的核心工作原理

网卡是连接计算机主机与外部局域网的桥梁(硬件架构包含以太网适配器和WiFi适配器)。

  • MAC 地址(物理地址):全球唯一,长 48 bit。
    • 高 24 bit 由 IEEE 统一分配给不同的网卡制造厂商。
    • 低 24 bit 由厂商自行分配给生产出的每一块网卡。
    • MAC 地址固化在网卡的 ROM(只读存储器)中。
  • 网卡的五大核心要点(功能):
    1. 封装与解封装:负责把网络层交下来的 IP 数据报封装成“帧”发送到局域网;或从局域网接收“帧”并提取出 IP 数据报。
    2. 中断机制与差错处理:从局域网接收信号,如果收到正确的帧,就使用“中断”通知 CPU 提取数据;如果发现是差错帧,网卡会直接将其静默丢弃,不打扰 CPU。
    3. 协议实现:网卡内部依靠硬件实现了数据链路层和物理层的标准功能。
    4. 串 / 并行转换:网卡与主板(CPU/内存)之间通过 I/O 总线进行并行通信,而与外部局域网之间进行串行通信,网卡负责这两种数据流格式的转换。
    5. 帧缓冲:网卡内部带有 RAM(随机存取存储器),用于在发送或接收数据时对“帧”进行缓存。

以太网(Ethernet)的物理层标准、MAC 帧格式

1. 以太网标准与双工模式

不同传输介质对通信模式(半双工/全双工)的支持情况不同:

  • 同轴电缆(如 10BASE5、10BASE2):物理特性决定了它们只能工作在半双工模式。
  • 双绞线(如 10BASE-T):
    • 做题时的默认潜规则:如果连接的是集线器(Hub),只能是半双工(必须使用 CSMA/CD 协议防冲突)。
    • 如果连接的是交换机(Switch),默认支持全双工(各走各的道,不需要使用 CSMA/CD 协议)。
  • 光纤(如 10BASE-F):只支持全双工。
  • 高速以太网演进:当以太网速率达到 10Gbps(万兆)时,规定只工作在全双工模式,这也标志着 CSMA/CD 协议在现代高速网络中已经彻底退出历史舞台。

QQ20260515-085819

2. V2 版以太网 MAC 帧格式(重点记忆)

课件给出了一个非常实用的结构记忆口诀:“6 6 2 N 4,收发协议数验”。

  • 目的地址(6字节):接收方的 MAC 地址。如果是广播帧,该地址的 48 bit 全为 1(即 FF-FF-FF-FF-FF-FF)。
  • 源地址(6字节):发送方的 MAC 地址。
  • 类型(2字节):指明封装在内部的网络层使用的是什么协议(如 IPv4 或 IPv6)。
  • 数据(N = 46~1500字节):即网络层交下来的 IP 数据报。如果太短必须“填充”凑够 46 字节,太长则在网络层就要“分片”。
  • FCS 校验码(4字节):使用之前学过的 CRC(循环冗余校验)来检测位错。
  • 易错细节:为了让接收方对齐时钟节奏,物理层会在这个 MAC 帧前面强行插入 8 字节的“前导码”(7B 前同步码 + 1B 帧开始定界符)。但这 8 字节不算作 MAC 帧本身的长度。MAC 帧的总长度被严格限制在 64B ~ 1518B 之间。

QQ20260515-090348

3. MAC 地址的归属与设备分级

  • 核心理念:MAC 地址是数据链路层(L2)的概念。
  • 结论:工作在网络层的路由器、工作在链路层的交换机内部都有数据链路层的功能,因此它们都有 MAC 地址。而工作在物理层(L1)的集线器(Hub)\只负责无脑放大电信号,它*没有 MAC 地址*。

4. 冲突域与广播域的隔离(必考核心)

这是构建和分析网络拓扑时最关键的原则,决定了广播风暴和冲突的范围:

  • 集线器(Hub):最底层的物理层设备。不隔离冲突域,也不隔离广播域。连在上面的所有电脑“一荣俱荣,一损俱损”,只要有两人同时发数据就冲突,一人发广播所有人必听。
  • 交换机(Switch):数据链路层设备。隔离冲突域,但不隔离广播域。它的每个端口就是一个独立的冲突域(所以连在不同端口的设备可以同时发数据不冲突),但如果有人发广播帧,交换机依然会复制并群发给所有端口。
  • 路由器(Router):网络层设备。既隔离冲突域,也隔离广播域。路由器是广播帧的“终结者”。当路由器收到目的地址全为 1 的广播 MAC 帧时,它会直接拦下丢弃,绝不会将其转发到其他网络段。

QQ20260515-091828

VLAN(虚拟局域网)

1. VLAN 的引入与核心概念

  • 面临的问题:如果一个局域网非常大(连接了大量主机和交换机),整个网络就是一个巨大的“广播域”。一旦有设备发送广播帧,所有设备都会收到,容易引发“广播风暴”,浪费网络资源,且不利于信息安全。
  • VLAN 的作用:在逻辑上将一个大型的物理局域网,分割成若干个较小的虚拟局域网(VLAN)。
  • 核心准则:一个 VLAN 就是一个独立的广播域。 同一个 VLAN 内的主机可以直接通信,不同 VLAN 之间的主机即使连在同一个交换机上,也不能直接进行二层(MAC层)通信,从而隔离了广播。
  • 标识:每个 VLAN 都有一个专属的编号,称为 VID (VLAN ID)。

2. 划分 VLAN 的三种方式

交换机内部会维护一张映射表,将数据帧归入不同的 VLAN 中:

  • ① 基于接口:最简单常用的方式。交换机直接记录“端口号”与“VID”的映射关系(例如:插在 1、2 号口的电脑属于 VLAN 10)。
  • ② 基于 MAC 地址:记录“主机的 MAC 地址”与“VID”的映射关系。优点是即使用户把电脑拔下来换插到交换机的另一个端口,它依然属于原来的 VLAN。
  • ③ 基于 IP 地址:记录“网络层 IP 地址”与“VID”的映射关系。这种方式甚至允许 VLAN 的范围跨越路由器。

3. IEEE 802.1Q 帧(重点考点)

为了让跨越多个交换机的同一 VLAN 主机能够通信,交换机之间通过“干线链路(Trunk 链路)”相连。干线链路需要传输多个不同 VLAN 的数据,因此必须对数据帧进行“打标签”以示区分。

  • 传输规则:
    • 主机与交换机之间:传输的是无标签的标准以太网 MAC 帧(即之前背的口诀:6 6 2 N 4)。
    • 交换机与交换机之间(干线链路):传输的是打了标签的 802.1Q 帧。交换机在发往另一台交换机前会“插入”标签,另一台交换机收到后,在发给目标主机前会“剥除”标签。
  • 802.1Q 帧的结构演变:
    • 在标准 MAC 帧的“源地址(6B)”和“类型(2B)”之间,强行插入了 4 字节的 VLAN 标签。
    • 新口诀:6 6 4 2 N 4,收、发、V(VLAN标签)、协、数、验。
  • 4 字节 VLAN 标签的内部构造:
    • 前 2 个字节:固定为 0x8100,交换机一看到这个数字就知道这是一个打了标签的 802.1Q 帧。
    • 后 2 个字节:包含 4 位的无用信息和 12 位的 VID。这 12 位的 VID 明确指出了该帧属于哪一个具体的 VLAN。

QQ20260515-102011

QQ20260515-102025

IEEE 802.11 无线局域网

一、 802.11 无线局域网基本概念

802.11 无线局域网采用星形拓扑结构,其中心节点称为接入点(AP),也可称为无线接入点(WAP)。

  • 基本服务集 (BSS): 由 1 个基站(AP)和 N 个移动站组成。
    • 通俗理解:1 个 WiFi 热点连了很多台手机或电脑。
    • SSID (服务集标识符): 无线局域网的名字(WiFi名称),最长不超过 32 字节。
    • BSA (基本服务区): 一个基本服务集能够覆盖的地理范围(即站在哪里能搜到这个 WiFi)。
  • 扩展服务集 (ESS): 将多个 AP 连接到同一个分配系统(DS),组成一个更大的服务集。
    • 通俗理解:想象成家里的“全屋 WiFi”系统。
  • 漫游 (Roaming): 移动站从一个基本服务集切换到另一个基本服务集,且保持通信连续不中断。
    • 通俗理解:拿着手机走动时,丝滑切换不同的 WiFi 热点。
  • 门户 (Portal): 充当“网桥”的角色,可将 802.11 无线局域网接入 802.3 有线以太网,将两类局域网连接成更大的局域网。

QQ20260515-103344

二、 硬件架构与通信原理

  • 家用路由器架构: 一个普通的家用路由器实际上是三者的结合体:家用路由器 = 路由器 + 以太网交换机 + AP。
  • 通信限制: 在 802.11 无线局域网内,两个移动站之间不能直接通信,必须通过基站(AP)进行转发。
  • 链路区别: AP 与移动站之间通过无线链路传输;AP 与 AP、AP 与路由器、AP 与交换机之间通常使用有线链路。
  • 格式转换: AP 通常具备“帧格式转换”功能,能在无线链路的 802.11 帧与有线链路的以太网帧之间互相转换。
  • 介质访问控制: 802.11 无线局域网使用 CSMA/CA 协议来实现介质访问控制(包含 RTS 和 CTS 帧的碰撞避免机制)。

三、 802.11 帧的分类

802.11 帧主要分为以下三种类型:

  1. 数据帧: 承载实际传输的数据。
  2. 控制帧: 用于辅助数据传输,如 ACK(确认)、RTS(请求发送)、CTS(允许发送)。
  3. 管理帧: 用于管理无线网络连接,如探测请求/探测响应帧。
    • 通俗理解:主要用于“发现 WiFi”。

四、 802.11 数据帧格式与地址解析

这部分是重难点,尤其涉及 MAC 首部中的地址字段(重点关注地址 1、2、3)以及帧控制位中的“去往 AP”和“来自 AP”的标志位。

核心记忆口诀:

  • 10 表示“去往 AP”(移动站发往 AP):
    • 口诀:去往 AP 中起止
    • 地址 1:中转(接收方,即 AP 的 MAC 地址)
    • 地址 2:起点(发送方,即源移动站 A 的 MAC 地址)
    • 地址 3:止点(最终目的地,即目标移动站 B 的 MAC 地址)
  • 01 表示“来自 AP”(AP 发往移动站):
    • 口诀:来自 AP 止中起
    • 地址 1:止点(接收方,即目标移动站 B 的 MAC 地址)
    • 地址 2:中转(发送方,即 AP 的 MAC 地址)
    • 地址 3:起点(最初源头,即源移动站 A 的 MAC 地址)

(注:地址 4 仅在固定基站的网络互联中使用,一般情况下较少关注。)

QQ20260515-104739

QQ20260515-104952

广域网与ppp协议

一、 广域网 (WAN) 基本概念

  • 特点与任务: 覆盖范围广(几十到几千公里),主要任务是长距离运送主机发送的数据,并将分布在各地的局域网互联起来。
  • 硬件构成: 局域网通过路由器连接到广域网。广域网内部由节点交换机(可理解为广域网专用的专业路由器)通过链路连接而成。
  • 核心对比(重要考点):
    • 广域网 (WAN): 各节点之间通常使用点对点高速链路,数据链路层常使用 PPP 协议。
    • 局域网 (LAN): 传统以太网常使用总线型链路,数据链路层常使用 CSMA/CD 协议。

二、 PPP 协议核心拆解

PPP(点对点协议)是广域网链路层最常用的协议,主要包含三大组成部分及其相关机制:

1. 三个组成部分

  • LCP (链路控制协议): 用于建立、配置和测试数据链路连接(例如:协商 MTU 大小、身份认证协议)。
  • NCP (网络控制协议): 为网络层协议建立和配置逻辑连接。每个不同的网络层协议需要相应的 NCP(例如:为 IP 协议分配 IP 地址的叫 IPCP)。
  • PPP 帧格式: 定义了将网络层数据(如 IP 数据报)封装成帧的格式。

2. PPP 帧格式详解

  • 标志字段 (F): 首尾各有一个,固定为 0x7E (01111110),作为帧定界符。
  • 地址 (A) 与控制 (C): 固定数值,无实际意义。
  • 协议: 2字节,用于指明“信息部分”携带的是什么协议的数据(如:IP 数据报、LCP 数据或 NCP 数据)。
  • 信息部分: 实际携带的有效载荷,不超过 1500 字节。
  • FCS (帧检验序列): 2字节,采用 CRC 循环冗余校验,若出错则直接丢弃该帧。

3. 透明传输机制

为了防止信息部分出现与帧定界符 0x7E 相同的比特组合导致误判,PPP 采用以下两种填充方式:

  • 字符填充(用于异步传输): 逐个字符传送。以 0x7D 作为转义字符来替换数据中出现的 0x7E 等特殊控制字符。
  • 零比特填充(用于同步传输): 连续比特流传送。发送端在发现连续 5 个 1 时,自动填入一个 0(接收端反向删除),确保数据中不会出现 01111110。

4. PPP 协议的主要特点

  • 仅支持点对点、全双工链路。
  • 不可靠: 只保证无差错(通过 CRC 检错丢弃),但不提供确认(没有 ACK 机制)。
  • 支持多种网络层协议(如 IPv4、IPv6)。
  • 面向字节,帧长度是字节的整数倍。

QQ20260515-111957


三、 PPP 协议工作过程(以早期拨号上网为例)

当你使用老电脑连接 ISP(网络服务提供商)的接入服务器时,PPP 的工作状态流转如下:

  1. 物理链路建立: 硬件设备连接,建立底层的物理层连接。
  2. LCP 链路建立: 双方通过发送 LCP 请求/应答帧,协商网络参数(如 MTU、认证方式)。
  3. LCP 链路鉴别: 用户发送账号密码进行身份验证(认证成功或失败)。
  4. NCP 链路建立: 鉴别通过后,发送 NCP 请求,ISP 服务器为你分配一个 IP 地址。
  5. 网络连通: LCP 和 NCP 链路均建立完毕,开始使用 PPP 帧携带 IP 数据报愉快地上网。

补充延伸: 随着技术发展,“拨号上网”时代过去后,为了在现代以太网中使用 PPP 协议的认证等优秀特性,演变出了 PPPoE 协议(PPP over Ethernet),其本质就是在以太网 MAC 帧的数据部分套娃携带了 PPP 帧。

以太网交换机

QQ20260516-100605

一、 以太网交换机的基本特点

  • 本质: 交换机相当于一个多端口网桥(支持全双工通信)。
  • 工作层次: 工作在数据链路层。
  • 转发依据: 交换机根据数据帧的目的 MAC 地址来决定如何转发数据帧。

二、 核心机制:自学习功能(支持即插即用)

交换机内部维护着一张交换表,记录了【MAC 地址】与【端口号】的对应关系。该表初始为空,通过“自学习”不断完善:

  1. 学习(记录“从哪来”):
    • 每当交换机收到一个帧,就会读取该帧的源 MAC 地址(发送方),并将其与接收该帧的端口号记录/更新到交换表中。
  2. 转发(决定“往哪去”):
    • 情况 A(未知目标): 如果交换表中找不到“目的 MAC 地址”(接收方)对应的端口,交换机会将该帧广播到除接收端口之外的所有其他端口。
    • 情况 B(已知目标): 如果交换表中存在“目的 MAC 地址”的对应端口,交换机会精准地将帧转发到该端口(单播)。
  3. 老化机制(应对网络拓扑变化):
    • 思考题指出:如果节点物理位置发生移动(例如拔掉网线换到另一个端口),旧的记录会导致转发错误。
    • 解决方案: 交换表中的每个表项都设有“有效时间”。如果一段时间内没有收到来自该 MAC 地址的帧,该表项就会过期自动作废,从而保证交换机能学习到节点最新的位置。

QQ20260516-100427

三、 两种数据交换方式对比

在决定从哪个端口转发之前,交换机处理数据帧的具体方式分为两种(结合 V2 标准以太网 MAC 帧格式:6 6 2 N 4,即目的地址、源地址、类型、数据、FCS校验):

  • 1. 直通交换 (Cut-through)
    • 工作原理: 交换机接收到帧时,只读取前 6 个字节(目的 MAC 地址),一旦查表确定了输出端口,就立刻开始转发。
    • 优点: 转发时延非常低。
    • 缺点: 无法进行差错检测,也不适用于需要速率匹配或协议转换的线路。
  • 2. 存储转发交换 (Store-and-forward)
    • 工作原理: 交换机会先将整个数据帧完整地接收并放入内部的高速缓存中。接着进行 FCS 差错检测等必要处理。如果帧正确,再根据交换表将其转发出去;如果出错,则直接丢弃。
    • 优点: 能够检测出错误帧并丢弃,同时适用于需要速率匹配、协议转换或差错检测的线路。
    • 缺点: 转发时延较高。

QQ20260516-100952

网络层

网络层的功能

一、 IP地址基础概念

  • 表示方法:IP地址用32位(32bit)二进制数表示。
  • 有点分十进制记法:常以8位(8bit)为一组进行划分,每组转换并记录为十进制数。
    • 具体示例:二进制形式 00111011 10101111 01100001 10011001 对应转换后的点分十进制简记为 59.175.49.153。
  • 数值合法范围:由于每组由8位二进制组成,显然每个部分的合法范围都是 0~255。

二、 网络层的功能

1. 异构网络互联

  • 如何理解“异构”:指每个网络的拓扑结构不同、物理层 & 链路层的实现不同、主机类型也各不相同。
  • 重要互联设备:路由器(Router)。
  • 别称注脚:在TCP/IP文献中,路由器也常被称为网关(Gateway)。

2. 路由与转发

  • 路由(Routing):
    • 各个路由器之间相互配合,规划IP数据报(分组)的最佳转发路径。
    • 各个路由器需要运行“路由协议”,最终生成各自的“路由表”。
  • 转发(Forwarding):
    • 一台路由器根据自己的“转发表”,将收到的IP数据报从合适的接口转发出去。
    • 精简关系:转发表 = 精简版路由表。更精简的数据结构有助于实现快速检索。

3. 拥塞控制

  • 产生原因:网络上出现过量分组,导致超负荷,进而引起网络性能下降。
  • 表现现象:网络上的分组数增加,但吞吐量反而降低。
  • 形象类比:节假日路上的车辆增多到一定程度时,收费站的吞吐量反而降低。
  • 拥塞控制方法:
    • 开环控制(静态的方法):在部署网络时,就提前设计好预防拥塞的方法。一旦网络系统开始运行,就不再修改。
    • 闭环控制(动态的方法):
      • 动态监视网络状态,及时发现哪里发生拥塞,并将拥塞信息传递给相关路由器(如:通过ICMP协议)。
      • 相关路由器在接收到信息后,会及时调整其“路由表”。

2

ipv4分组

image-20260608120731396

一、 各种协议之间的服务关系

整个网络体系结构中,上层协议依赖下层协议提供的服务:

  • 应用层 $\rightarrow$ 传输层:
    • 基于TCP协议的有:SMTP、POP3(电子邮件)、HTTP(万维网)、FTP(文件传输)。
    • 基于UDP协议的有:DNS(域名系统)、DHCP(动态主机配置)。
  • 传输层 $\rightarrow$ 网络层:TCP和UDP协议的数据段最终都要交由 IP协议 封装成 IP分组(数据报)。IP协议是互联网的核心。
  • 网络层辅助协议:
    • ARP协议:用于查询同一网络中的 <主机IP地址, MAC地址> 之间的映射关系。
    • ICMP协议:用于在网络层实体之间相互通知“异常事件”。
    • IGMP协议:用于实现IP组播。
  • 网络层 $\rightarrow$ 数据链路层 & 物理层:IP分组下发后,由具体的物理网络协议承载传输,如以太网(802.3,使用CSMA/CD)、无线局域网(802.11,使用CSMA/CA)、令牌环网(802.5)。

3

二、 IP数据报(IP分组)的格式

IP数据报由 首部 和 数据部分 组成。首部包含固定部分(20字节)和可变部分(0~40字节)。

核心字段解析

  1. 版本(4bit):区分IP协议版本(如 IPv4、IPv6)。
  2. 首部长度(4bit):以 $4\text{B}$ 为单位。
    • 由于4bit最大能表示15,所以首部最大长度为 $15 \times 4\text{B} = 60\text{B}$。
    • 固定首部为20B,所以该字段的最小值为5($5 \times 4\text{B} = 20\text{B}$)。
  3. 总长度(16bit):以 $1\text{B}$ 为单位。涵盖首部和数据部分的总和,最大理论值为 $65535\text{B}$。
  4. 标识(16bit):由源主机生成,通常是一个自增序列。同一个原始数据报被分片后,所有分片的标识都相同。
  5. 标志(3bit):
    • DF (Don’t Fragment):次低位。$\text{DF}=1$ 表示不允许分片;$\text{DF}=0$ 表示允许分片。
    • MF (More Fragment):最低位。$\text{MF}=1$ 表示后面还有分片;$\text{MF}=0$ 表示这是最后一个分片。
  6. 片偏移(13bit):表示该分片的数据部分在“被分片前”的原始数据部分中的相对位置。以 $8\text{B}$ 为单位。
  7. 生存时间 TTL(8bit):数据报在网络中可通过的路由器数量的最大值(跳数)。
  8. 协议(8bit):指出此数据报携带的数据使用何种协议(例如:6表示TCP,17表示UDP)。
  9. 首部检验和(16bit):每个路由器仅校验首部,不对数据部分进行校验。

4

image-20260608120547811

image-20260608120635007

三、 IP数据报的“分片”问题

1. 根本原因与MTU

  • 数据链路层数据帧能承载的最大数据量称为 最大传输单元(MTU)(例如以太网的 $\text{MTU} = 1500\text{B}$)。
  • 当一个IP数据报的总长度超出了下一段链路的MTU时,就需要对该数据报进行分片。

2. 分片的核心规则

  • 发生位置:分片可能在源主机或传输路径上的任何一个路由器中发生。
  • 重组位置:只有目的主机才会对分片进行“重组”。
  • 传输特性:各分片作为独立IP数据报转发,可能通过不同路径乱序到达目的主机。
  • 长度约束(核心考点):由于片偏移字段以 $8\text{B}$ 为单位,因此除了最后一个分片外,其他每个分片的数据部分长度必须是 $8\text{B}$ 的整数倍。

QQ20260605-181757

3. 经典分片计算实例

已知条件:

原始IP数据报:总长度 = $4000\text{B}$(首部 $20\text{B}$,有效数据 $3980\text{B}$),标识 = 777,$\text{MF}=0$。

下一段链路的 $\text{MTU} = 1500\text{B}$。

  • 计算每个分片的最大数据载荷:

    由于每个分片需要扣除 $20\text{B}$ 的IP首部,剩余可传输数据空间为 $1500 - 20 = 1480\text{B}$。

    检查 $1480$ 是否能被 $8$ 整除:$1480 \div 8 = 185$(符合 $8\text{B}$ 整数倍规则)。

  • 分片拆分结果:

分片编号 总长度 数据部分长度 数据区间 标识 MF DF 片偏移 (数据起始字节 ÷8)
分片 1 $1500\text{B}$ $1480\text{B}$ $0 \sim 1479$ 777 1 0 $0 \div 8 = \mathbf{0}$
分片 2 $1500\text{B}$ $1480\text{B}$ $1480 \sim 2959$ 777 1 0 $1480 \div 8 = \mathbf{185}$
分片 3 $1040\text{B}$ $1020\text{B}$ ($3980-1480\times2$) $2960 \sim 3979$ 777 0 0 $2960 \div 8 = \mathbf{370}$

image-20260608120520741

四、 生存时间 TTL (Time To Live) 工作机制

  • 初始化:TTL的初始值通常由源主机设置。
  • 递减过程:数据报在网络中每经过一个路由器,路由器在转发前就会将 TTL 减 1。
  • 异常处理:
    • 如果路由器发现 TTL 减到 0,就会直接 丢弃该分组,不再进行转发。
    • 丢弃的同时,路由器会向源主机发送一个 ICMP 报文(超时差错报告),以此通知源主机发生了异常事件,防止无用分组在网络中无休止地循环。

image-20260608120711912

ip地址

为您整理的计算机网络核心笔记(关于IP地址分类、特殊IP地址、以及IP分组转发与网关配置):

一、 分类IP地址方案(Classful Addressing)

早期互联网将32位(32bit)IP地址划分为两级结构:IP地址 = <网络号>, <主机号>。通过检查IP地址的前几个比特(最高前缀),即可推断出网络号所占的位数。

  • A类地址(1 ~ 126):
    • 最高位固定为 0。
    • 前8位为网络号,后24位为主机号。
  • B类地址(128 ~ 191):
    • 最高位固定为 10。
    • 前16位为网络号,后16位为主机号。
  • C类地址(192 ~ 223):
    • 最高位固定为 110。
    • 前24位为网络号,后8位为主机号。
  • D类地址(224 ~ 239):最高位固定为 1110,用作多播地址(组播)。
  • E类地址(240 ~ 255):最高位固定为 1111,保留供以后使用。

💡 核心常识:

  1. 处于同一个物理网络的所有主机和路由器接口,其IP地址中的“网络号”必须完全相同。
  2. 路由器与路由器连接的对等接口可以不分配IP地址(无编号链路);但路由器与普通网络节点连接的接口必须分配IP地址。
  3. 当一台新主机接入网络时,除了分配唯一的IP地址,还必须配置“默认网关”。

QQ20260608-123837

二、 特殊用途的IP地址

下表中的特殊IP地址不能指派给网络中的任何一台普通主机或路由器接口进行“私用”:

网络号 主机号 可作源地址? 可作目的地址? 代表的含义与实际用途
Y 全0 ❌ ❌ 表示整个网络本身(只能用于路由表、转发表中作为网络前缀)。
Y 全1 ❌ ✔️ 直接广播地址。向网络号为 Y 的外部网络中所有主机广播IP分组。
0 Y ✔️ ❌ 表示本网络中主机号为 Y 的特定主机。
全0 全0 ✔️ ❌ 表示本网络上的本主机(常在主机刚启动、通过 DHCP 协议获取动态IP时作为源地址使用)。
全1 全1 ❌ ✔️ 受限广播地址。向当前本物理网络内的所有主机广播分组,路由器不会转发此广播。
127 任何数(非全0/1) ✔️ ✔️ 环回自检地址(Loopback)。表示主机本身,流量不流向网络,常用于本地网络软件环回测试。

📌 重要数量结论:

由前两行可知,在具体的分类网络中,主机号全0(网络号)和主机号全1(广播地址)被特殊保留。因此,如果一个网络中主机号占 $N\text{ bit}$,那么该网络最多支持的可用主机&路由器接口数为:

三、 IP分组转发与网关配置实例

结合学校与公司的拓扑图,IP分组在网络层的转发表现为两种场景:

1. 本地直接交付(同网段通信)

  • 场景:主机 $H1(166.1.0.1)\rightarrow H6(166.1.4.4)$
  • 机制:$H1$ 检查目的IP,发现两者的网络号都是 166.1(同属学校的B类网段)。
  • 流程:不经过路由器,直接在本地通过 ARP协议 查询 $H6$ 的 MAC 地址,并在数据链路层封装成帧直接发送。

2. 间接交付(跨网段通信与默认网关)

  • 场景:主机 $H1(166.1.0.1)\rightarrow H7(200.1.1.2)$
  • 默认网关的作用:$H1 \sim H6$ 必须配置默认网关为 166.1.0.5(即学校路由器的内网接口 B2)。
  • 转发流程:
    1. $H1$ 发现目的IP 200.1.1.2 属于外网,无法直接送达。
    2. $H1$ 将该IP分组打包,并在底层通过网关的 MAC 地址,把分组投递给默认网关(学校路由器)。
    3. 学校路由器剥离数据帧后提取IP分组,查阅自身的转发表。
    4. 转发表匹配到目的网络号 200.1.1.0 对应的下一跳转发接口为 B0,随后将分组送往公司路由器 $C0$ 接口,最终由公司路由器交付给 $H7$。

QQ20260609-132829

子网掩码和子网划分

根据您上传的这一组关于子网划分、默认路由及数据报端到端发送/转发流程的课件,我为您提炼并整理了一份逻辑清晰、重点突出的复习笔记:

一、 子网划分技术与子网掩码

QQ20260609-151404

1. 原理与结构演变

  • 两级结构 $\rightarrow$ 三级结构:
    • 划分前:IP地址 = <网络号>, <主机号>
    • 划分后:IP地址 = <网络号>, <子网号>, <主机号>
  • 划分机制:从原本属于主机号的 $n\text{ bit}$ 中抠出 $k\text{ bit}$ 作为子网号,剩余的 $n-k\text{ bit}$ 继续作为主机号。这样能划分出 $2^k$ 个大小相等的子网。
  • 特殊地址约束:每个子网地址中,主机号部分不能分配为全 0 或全 1。
    • 全 0:代表该子网网络号本身。
    • 全 1:代表该子网的广播地址。

2. 子网掩码(Subnet Mask)

  • 核心作用:将子网掩码与IP地址进行“逐位相与(AND)”,即可快速算出 <网络号, 子网号>(即网络前缀)。只有网络前缀完全相同的IP地址,才归属于同一个子网。
  • 默认子网掩码(未进行子网划分时的传统分类网络):
    • A类:255.0.0.0
    • B类:255.255.0.0
    • C类:255.255.255.0
  • 配置规范:
    1. 进行了子网划分的网络内部,每台主机、每个路由器接口都必须同时配置:IP地址、默认网关、子网掩码。
    2. 路由器的转发表项必须升级为三元组:<目的网络号, 子网掩码, 转发接口>。

二、 默认路由(Default Route)

  • 表项设置:目的网络号全 0(0.0.0.0),子网掩码全 0(0.0.0.0)。
  • 匹配机制:在路由器查转发表时,如果路由表中所有的常规表项都无法匹配成功,数据报最终就会命中默认路由并从指定的默认接口转发出去。

三、 IP 数据报端到端的传输全流程

QQ20260609-212156

1. 主机发送端处理逻辑(间接交付 vs 直接交付)

当主机准备发送一个 IP 数据报时:

  1. 网络前缀匹配:使用本机配置的子网掩码,分别与“本机IP”和“目的IP”进行逐位相与,检查两者的网络前缀是否相同。
  2. 决策路径:
    • 若相同(属于同一网络):进行本地直接交付。通过 ARP协议 询问得到目的主机的 MAC 地址,直接在数据链路层封装成帧,通过交换机发送给目的主机。
    • 若不同(不属于同一网络):进行间接交付。通过 ARP协议 询问得到默认网关(路由器接口)的 MAC 地址,将帧发送给默认网关,由路由器帮其转发。

QQ20260609-212225

2. 路由器转发端处理逻辑

当路由器某个接口收到 IP 数据报时:

  1. 校验提取:对 IP 数据报首部进行校验,从中提取出目的 IP 地址。
  2. 查转发表:依次将目的 IP 地址与转发表中各个表项的“子网掩码”进行逐位相与,看计算结果是否等于该表项的“目的网络号”。
  3. 执行转发:根据匹配成功的表项,将数据报从对应的转发接口送出。
    • 注:如果匹配出来的“转发接口”恰好与该数据报进入路由器的入口相同,则路由器直接就地交付,不用再将其转回。

四、 拓扑案例实战深度解析

QQ20260609-151353

结合课件中的学校拓扑图(B类地址段 166.1.x.x),我们可以观察到两种经典的通信轨迹:

案例 A:跨网段通信(间接交付)

  • 场景:主机 $H1(166.1.0.1) \rightarrow H3(166.1.128.2)$,网络子网掩码配置为 255.255.128.0。
  • 路径解析:
    1. $H1$ 发现目的 IP 经掩码相与后得到的网络号是 166.1.128.0,与自己的网络号 166.1.0.0 不同。
    2. $H1$ 将数据包转交给默认网关 166.1.0.5,通过接口 B3 进入学校路由器。
    3. 学校路由器查表,命中表项 166.1.128.0 | 255.255.128.0 | B2,数据包最终从接口 B2 送出,成功到达 $H3$。

QQ20260609-210411

案例 B:同网段通信(本地直接交付)

  • 场景:主机 $H3(166.1.128.2) \rightarrow H6(166.1.150.1)$,子网掩码为 255.255.128.0。
  • 路径解析(核心考点):
    • 将 $H3$ 和 $H6$ 的第三字节分别与掩码的 128 ($10000000_2$) 相与:
      • $128 \text{ AND } 128 = 128$
      • $150(10010110_2) \text{ AND } 128(10000000_2) = 128$
    • 结论:两者的网络前缀相同(同属 166.1.128.0 网段)。因此,$H3$ 到 $H6$ 的数据帧直接通过本地交换机完成内部直接交付(如蓝线所示),根本不需要通过网关中转到路由器内部!

QQ20260609-210059

无分类编址(CIDR)Classless Inter-Domain Routing

QQ20260609-215026

一、 无分类编址 CIDR 核心概念

1. 时代背景与定义

  • 背景:1993年提出,消出了传统的 A/B/C/D/E 类网络划分,大幅提升了IP地址的利用率,缓解了IPv4地址枯竭危机。

  • 两级结构:

    其中网络前缀的长度是可变的。

  • 斜线记法(CIDR记法):如 128.14.32.153/30,表示网络前缀占 $30\text{ bit}$,主机号占 $2\text{ bit}$。

2. 实例拆解:128.14.32.0/21 地址块

  • 主机号位数:$32 - 21 = 11\text{ bit}$。

  • 最大可分配主机数:$2^{11} - 2 = 2046$ 台(可满足某单位2000台主机的联网需求)。

  • 子网掩码:前21位全为1,后11位全为0。

  • 地址块范围计算:

    保持前21位不变,将后11位主机号分别置为全0(起点)和全1(终点):

    • 起始地址:128.14.32.0(主机号全0,代表本网络号)
    • 结束地址:128.14.39.255(主机号全1,代表广播地址)

QQ20260609-213405

二、 子网划分方案对比:定长 vs 变长

1. 定长子网划分(FLSM)

  • 原理:将固定长度的主机号前 $k\text{ bit}$ 抠出来作为子网号,划分出 $2^k$ 个大小完全相同的子网。
  • 缺陷:极不灵活。当各个物理网络实际需要的主机数量差距悬殊时,会导致大网不够用、小网严重浪费,IP地址利用率极低。

2. 变长子网划分(VLSM)

  • 原理:在CIDR地址块内部动态划分物理网络时,子网号的长度不固定。
  • 优势:根据每个子网的实际需求“量体裁衣”分配对应大小的地址块(如一、二级ISP之间的层层动态下发)。

三、 CIDR 子网划分的硬性约束

  • 主机号禁用规则:在任何一个划分出的子网中,主机号全0(网络本身)和主机号全1(广播)的IP地址绝对不能分配给具体的节点私用。

  • 最小子网限制(点对点链路):

    对于只有两台设备互联的“点对点链路”(如路由器与路由器、或路由器与单台主机“狗剩”连接):

    • 结论:主机号至少需要保留 $2\text{ bit}$。
    • 原因:如果只留 $1\text{ bit}$,则只能表示 0 和 1,全部被全0/全1规则抹杀,导致可用IP为0。保留 $2\text{ bit}$ 时,可选有 00, 01, 10, 11,去掉 00 和 11 后,刚好剩下 01 和 10 两个合法IP分配给链路两端。因此,点对点链路对应的最高效CIDR前缀为 /30。

四、 核心解题技巧:二叉树划分法

在面对复杂的变长子网划分题型时,可以利用“从根到叶构造二叉哈夫曼树”的技巧快速解题:

1. 构造规则

  1. 根节点:代表原始获得的最大的 CIDR 地址块(假设其可自由分配的主机号占 $h\text{ bit}$)。
  2. 分支规则:向下每分裂一次代表借用 $1\text{ bit}$ 作子网号。规定左分支写 0,右分支写 1(反之亦可)。
  3. 叶子节点:树的末端叶子结点即对应一个划分出的子网。
  4. 路径锁定:从根节点到达该叶子结点的路径二进制编码,紧跟在原始网络前缀后面,组成该子网新的网络前缀。

2. 实战演练(根据拓扑图)

假设咸鱼电信获得原始块:128.14.32.128/27(共32个IP,可分配主机位 $5\text{ bit}$)。

QQ20260609-214621

路由聚合

路由聚合(Route Aggregation / 构成超网)

1. 概念与原理

  • 定义:在一个路由器转发表中,如果几条路由表项的转发接口相同,且它们的部分网络前缀也相同,就可以将这几条路由表项压缩、合并为一条表项。这种地址的合并技术称为路由聚合,也叫构成超网(Supernetting)。
  • 聚合方法:将各网络的网络前缀转换成二进制,寻找它们共同的前缀部分。共同的位数即为聚合后的新网络前缀长度。

2. 课件实例解剖

县里的电信路由器原本有三条去往不同子网的路由,由于转发接口都是 G1,可以进行聚合:

  • 表项① $\rightarrow$ 128.14.32.128/28:10000000.00011110.00100000.10000000
  • 表项② $\rightarrow$ 128.14.32.144/29:10000000.00011110.00100000.10010000
  • 表项③ $\rightarrow$ 128.14.32.152/30:10000000.00011110.00100000.10011000

🔍 找共同点:可以看到,前27位(即第四字节的前三位 100)完全相同。

聚合后的表项④ $\rightarrow$ 128.14.32.128/27,转发接口仍为 G1。

QQ20260609-221846

3. 优缺点对比

  • 优点:表更小,查询更快。能极大地减少路由表的大小,降低路由器的内存消耗和查表延迟。
  • 缺点:可能会引入额外的无效地址。
    • 例如:聚合后的 /27 地址块涵盖了 128.14.32.156/30。如果这个地址块在实际中是空闲的/无效的,那么当有去往该地址的数据报时,也会被错误地派发到 G1 接口,从而带来了额外的无效流量。

二、 最长前缀匹配原则(Longest Prefix Match)

1. 核心思想

在使用 CIDR 编制时,由于路由聚合的存在,转发表中的一条目的 IP 地址可能会同时匹配上多个不同的路由表项。

  • 最长前缀匹配原则:当有多个表项都能匹配成功时,路由器会选择网络前缀最长(即掩码中1的个数最多)的那个表项。
  • 深层逻辑:网络前缀越长,说明其划分的子网范围越小,路由就越具体(Specific)、越精确。

2. 经典查表匹配实例

已知场景:县里的电信路由器收到一个数据报,其目的 IP 地址 = 128.14.32.131。

目的 IP 对应的二进制:10000000.00011110.00100000.10000011

将目的 IP 与路由转发表中的各项逐一进行前缀比对:

  1. 比对表项①(128.14.32.128/27 | 接口 G1):
    • 检查前 27 位:10000000.00011110.00100000.100...
    • 结果:匹配成功(前27位完全一致)。
  2. 比对表项②(128.14.32.128/28 | 接口 G3,对应铁柱网吧):
    • 检查前 28 位:10000000.00011110.00100000.1000...
    • 结果:匹配成功(前28位完全一致)。
  3. 比对表项③(0.0.0.0/0 | 接口 G0,默认路由):
    • 默认路由匹配长度为 0 位。
    • 结果:匹配成功(作为兜底选项)。

3. 最终转发决策

  • 匹配成功的表项有:表项①(27位)、表项②(28位)、表项③(0位)。
  • 根据最长前缀匹配原则,表项②的 28 位匹配长度最长。
  • 结论:最终路由器命中表项②,将该 IP 数据报从 G3 接口 转发出去,精准送达铁柱网吧。

QQ20260609-222212

网络地址转换NAT

QQ20260609-231437

一、 NAT(网络地址转换)核心概念

1. 私有 IP 地址(内网 IP)

  • 划分网段:
    • 10.0.0.0 ~ 10.255.255.255
    • 172.16.0.0 ~ 172.31.255.255
    • 192.168.0.0 ~ 192.168.255.255
  • 应用规则:只允许分配给局域网内部的节点,不允许直接分配给互联网上的节点。
  • 复用特性:私有 IP 地址是可复用的。只需在同一个局域网内保持唯一即可,不需要全球唯一。

2. 全球 IP 地址(外网 IP)

  • 来源:通常由因特网服务提供商(ISP)提供。
  • 特性:全球唯一。是局域网与外界互联网通信时真正使用的公共 IP 地址。

二、 NAT 路由器 vs 普通路由器

设备类型 核心工作层级 是否改变 IP/端口 核心机制 / 功能
普通路由器 仅包含网络层及以下功能 ❌ 绝不改变 仅根据转发表中的目的网络号,规划路径并原封不动地转发 IP 数据报。
NAT 路由器 包含传输层功能(需识别端口) ✔️ 主动更改 在转发 IP 数据报时,进行内网 IP/端口 与 外网 IP/端口 的相互转换。

📌 NAT 表的记录格式:

NAT 表维护着动态的转换映射关系:〈内网 IP : 端口号〉 ↔ 〈外网 IP : 端口号〉

三、 NAT 路由器转换规则

  • 从内网转发到外网(Outbound):
    • 路由器会更改数据报的 源 IP 地址 和 源端口号(将内网私有地址改写为路由器的外网公共地址与新分配的端口)。
  • 从外网转发到内网(Inbound):
    • 路由器会更改数据报的 目的 IP 地址 和 目的端口号(根据外网端口反查 NAT 表,还原为对应的内部主机私有地址与进程端口)。

四、 实战轨迹拆解:手机1 ↔ 手机2 微信通信全流程

结合课件中的“训练二”(手机1向手机2发送微信图片),我们通过数据报在不同阶段的首部字段变化来闭环理解 NAT 转换:

1. 角色基本配置

  • 发送端(右侧局域网):
    • 手机1 内网 IP:192.168.3.48,微信进程端口:9855
    • 右侧 NAT 路由器外网 IP:59.175.49.153
    • 右侧 NAT 表项:59.175.49.153 : 7788 ↔ 192.168.3.48 : 9855
  • 接收端(左侧局域网):
    • 手机2 内网 IP:192.168.3.74,微信进程端口:6666
    • 左侧 NAT 路由器外网 IP:66.211.88.55
    • 左侧 NAT 表项:192.168.3.74 : 6666 ↔ 66.211.88.55 : 4096

2. 数据报三阶段字段演变

阶段一:数据报刚离开手机1,在右侧内网中传输时

此时数据报尚未经过右侧 NAT 路由器:

  • IP 首部:源 IP = 192.168.3.48,目的 IP = 66.211.88.55(指向接收端的公网地址)
  • TCP 首部:源端口 = 9855,目的端口 = 4096(指向接收端在公网映射的端口)

QQ20260609-235934

阶段二:数据报经过右侧 NAT 转换,在互联网(Internet)中传输时

右侧 NAT 路由器根据自身 NAT 表,将源信息改写为外网公共信息:

  • IP 首部:源 IP = 59.175.49.153(被改写),目的 IP = 66.211.88.55
  • TCP 首部:源端口 = 7788(被改写),目的端口 = 4096

QQ20260610-000026

阶段三:数据报到达左侧 NAT 路由器,转换后进入左侧内网时

左侧 NAT 路由器收到外网数据报,查阅自身 NAT 表,将目的信息还原为左侧内网特定主机的进程信息:

  • IP 首部:源 IP = 59.175.49.153,目的 IP = 192.168.3.74(被还原)
  • TCP 首部:源端口 = 7788,目的端口 = 6666(被还原)
  • 最终结果:手机2 的微信进程(6666端口)成功收到由公网 59.175.49.153:7788 投递过来的图片数据。

QQ20260610-000107

地址解析协议(ARP)Address Resolution Protocol

ARP 协议基础概念

QQ20260610-003006

1. 硬件地址与网络层地址的关系

  • MAC 地址(48bit):是网络适配器(网卡)出厂时分配好的硬件地址,具有全球唯一性。
    • 一台主机或节点至少拥有一个网络适配器,因此至少有一个 MAC 地址。
    • 路由器作为一个三层设备,拥有多个用于数据转发的独立接口,每个接口背后都是一个独立的网络适配器,因此路由器拥有多个不同的 MAC 地址。
  • ARP 协议的核心作用:在同一个局域网(LAN)内部,动态地建立并查询一个已知的 IP 地址与对应的硬件 MAC 地址之间的映射关系。

2. ARP 表(ARP 缓存)

  • 数据结构:每台主机和路由器的每个转发接口都维护着自己独立的 ARP 高速缓存表,专门记录 〈IP地址 ↔ MAC地址〉 的映射对。
  • 生存特性:为了适应网络拓扑的动态变化(如设备更换、IP 重新分配),ARP 表项需要定期进行自动更新和老化清除。

ARP 交互工作流程(请求与响应机制)

当主机 $A$ 需要向本局域网内的目标主机 $B$ 发送数据,但其本地 ARP 表中缺失 $B$ 的 MAC 地址时,将触发以下双向交互流程:

1. ① ARP 请求分组(ARP Request)—— 广播发送

  • 具体报文内容:
    • 我是谁? $\rightarrow$ 我的 IP 地址是 $X$,我的 MAC 地址是 $Y$。
    • 我想找谁? $\rightarrow$ 我想找的那个家伙,IP 地址是 $Z$。
  • 链路层封装:封装入 MAC 帧时,帧目的地址设为全 1(广播)。
  • 传输特性:作为广播帧发送,局域网内的所有主机和路由器接口均能接收并解封装该帧,并读取里面的 ARP 请求。

2. ② ARP 响应分组(ARP Reply)—— 单播回传

  • 具体报文内容:
    • 自我认领 $\rightarrow$ “你好,我就是你要找的那个靓仔,我的 IP 地址是 $Z$,我的 MAC 地址是 $V$。”
  • 链路层封装:封装入 MAC 帧时,帧目的地址显式设为 $Y$(原请求方的 MAC 地址),源地址设为 $V$。
  • 传输特性:作为单播帧精准回传给请求方,网络内其他无关主机在链路层发现目的 MAC 不匹配时会自动丢弃该帧。

拓扑实战训练案例推导

结合课件中的网络拓扑图,该网络使用了 CIDR 划分方案,划分为两个大子网(网络前缀为 /17):

  • 接口 B3:166.1.0.5/17(覆盖 $H1, H2$ 等,范围 166.1.0.1 ~ 166.1.127.255)
  • 接口 B2:166.1.128.1/17(覆盖 $H3 \sim H6$ 等,范围 166.1.128.1 ~ 166.1.255.255)
    • 关键推导:$H3$(166.1.128.2)与 $H6$(166.1.150.1)经掩码计算后,网络前缀均为 166.1.128.0,因此它们处于同一个局域网物理网段内。

QQ20260610-082047

假设 $H3$ 想给默认网关($166.1.128.1$)发送 IP 数据报,分析 ARP 工作过程

  1. 判断路径:$H3$ 发现目的 IP 166.1.128.1 与自己在同一网段,属于本地直接交付,但本地无网关的 MAC 地址。
  2. H3 发起请求:$H3$ 产生一个 ARP 请求分组。
    • 内容:我是谁:166.1.128.2 , AA:BB:CC:DD:EE:33;我找谁:166.1.128.1。
    • 封装:MAC 帧目的地址 = FF-FF-FF-FF-FF-FF(全1广播),源地址 = ...EE:33。
  3. 路由器接口响应:学校路由器的 B2 接口收到广播并提取匹配,发现寻找的是自己,于是向 $H3$ 单播回传一个 ARP 响应分组。
    • 内容:166.1.128.1 对应的 MAC 地址是 AA:BB:CC:DD:EE:B2。
    • 封装:MAC 帧目的地址 = ...EE:33(单播),源地址 = ...EE:B2。
  4. 状态学习:$H3$ 收到响应后,将其写入自己的 ARP 表中。

这份为您精心整理的动态主机配置协议 DHCP (Dynamic Host Configuration Protocol) 核心网络笔记,严格基于您提供的课件图解进行结构化提炼,直击考点与底层协议字段变化:

动态主机配置协议 DHCP Dynamic Host Configuration Protocol

一、 DHCP 基本概念与体系结构

1. 核心作用

  • 为刚刚接入网络的计算机动态分配 IP 地址。
  • 自动配置主机的核心网络参数:默认网关、子网掩码、DNS 服务器地址等。

2. 架构模式:客户端/服务器模型 (C/S)

  • DHCP 客户端:新接入网络、希望获得 IP 地址等配置信息的主机。
  • DHCP 服务器:负责管理 IP 地址池并分配 IP 地址的专用主机或设备。
    • 注:在家庭网络中,DHCP 服务器通常由家庭路由器兼职;在大型企业/校园网络内,可以同时存在多台专用 DHCP 服务器。

3. 协议层级与端口分配

  • DHCP 属于 应用层协议。
  • 底层传输依赖 UDP 协议 进行不可靠传输。
    • 客户端 UDP 端口号 = 68
    • 服务器 UDP 端口号 = 67

QQ20260610-082550

二、 DHCP 协议数据的层层封装流程

当 DHCP 报文从应用层向下传递时,会历经操作系统的网络栈进行层层头部包装:

  1. 应用层:生成原始的 DHCP 报文(包含配置载荷数据 $H_5$)。
  2. 传输层:添加 UDP 首部($H_4$),填入源/目的端口号,封装为 UDP 数据报。
  3. 网络层:添加 IP 首部($H_3$),填入源/目的 IP 地址,封装为 IP 数据报。
  4. 数据链路层:添加 MAC 帧头($H_2$)和帧尾($T_2$),封装为可直接在物理链路上传输的 MAC 帧。
  5. 物理层:将 MAC 帧转换为纯粹的 二进制比特流(10100...)在介质中发射。

三、 DHCP 四步交互工作原理 (DORA 过程)

当新主机 $H_3$(MAC 地址为 AA:BB:CC:DD:EE:33)初次接入网络,与网络中的 DHCP 服务器(IP 为 166.1.128.8,MAC 为 AA:BB:CC:DD:EE:08)交互时,会完整经历以下四个阶段:

QQ20260610-094110

1. ① DHCP 发现报文 (DHCP DISCOVER) —— 客户端 $\rightarrow$ 服务器

  • 核心语义:客户端“初来乍到”,在网络中大喊:“有谁能给我发个 IP 地址吗?我的 MAC 地址是 … ”。
  • 各层头部字段配置:
    • 传输层 (UDP):源端口 = 68,目的端口 = 67
    • 网络层 (IP):源 IP = 0.0.0.0(此时主机尚无 IP),目的 IP = 255.255.255.255(本地广播,发送给网络内的所有人)
    • 数据链路层 (MAC):源 MAC = ...EE:33,目的 MAC = FF-FF-FF-FF-FF-FF(广播帧)

2. ② DHCP 提供报文 (DHCP OFFER) —— 服务器 $\rightarrow$ 客户端

  • 核心语义:DHCP 服务器收到广播后,从地址池预留一个 IP,对客户端说:“我有空闲 IP 给你,这个 166.1.128.2 租给你用 24 小时,掩码和网关也给你准备好了。”
  • 各层头部字段配置:
    • 传输层 (UDP):源端口 = 67,目的端口 = 68
    • 网络层 (IP):源 IP = 166.1.128.8,目的 IP = 255.255.255.255(广播数据报)
    • 数据链路层 (MAC):源 MAC = ...EE:08,目的 MAC = AA:BB:CC:DD:EE:33(单播帧)
    • 深度思考:为什么此时 IP 层用广播而 MAC 层可以用单播?因为客户端网卡能识别自己的硬件 MAC 地址,从而在链路层精准接收该单播帧;但由于客户端在网络层还没有正式绑定合法的 IP,无法作为常规 IP 单播终点,故网络层依然设置为广播形式。

3. ③ DHCP 请求报文 (DHCP REQUEST) —— 客户端 $\rightarrow$ 服务器

  • 核心语义:客户端向全网宣布:“我正式接受来自服务器 166.1.128.8 提供的 IP 地址 166.1.128.2!”

  • 各层头部字段配置:

    • 传输层 (UDP):源端口 = 68,目的端口 = 67

    • 网络层 (IP):源 IP = 0.0.0.0(正式确立前仍暂用0),目的 IP = 255.255.255.255(本地广播)

    • 数据链路层 (MAC):源 MAC = ...EE:33,目的 MAC = FF-FF-FF-FF-FF-FF(广播帧)

    • 核心考点:为什么这一步明明已经有了心仪的 IP,却还要用广播?

      原因是为了通知全网所有的 DHCP 服务器。如果网络中存在多台 DHCP 服务器且它们都发了 OFFER,客户端只能选一个。通过广播 REQUEST,既能明确告知被选中的服务器“我要用你的 IP”,也能让其他未被选中的服务器获知消息,及时释放它们此前为该客户端预留的备用 IP 地址。

4. ④ DHCP 确认报文 (DHCP ACK) —— 服务器 $\rightarrow$ 客户端

  • 核心语义:选中的 DHCP 服务器做出最终批复:“没问题,一锤定音,这地址你正式拿去用吧!”
  • 各层头部字段配置:
    • 传输层 (UDP):源端口 = 67,目的端口 = 68
    • 网络层 (IP):源 IP = 166.1.128.8,目的 IP = 255.255.255.255(广播数据报)
    • 数据链路层 (MAC):源 MAC = ...EE:08,目的 MAC = AA:BB:CC:DD:EE:33(单播帧)
  • 后续动作:客户端收到 ACK 后,正式将 166.1.128.2 配置到本地网卡上,此时主机才真正拥有了在网络中合法合规进行跨网段双向通信的能力。

网际控制报文协议 ICMP(Internet Control Message Protocol)

QQ20260610-094952

一、 ICMP 概述与封装格式

1. 协议定位与核心功能

  • 协议层级:ICMP 属于 网络层协议。
  • 核心作用:由于 IP 协议本身提供的是不可靠、无连接的尽力交付服务,ICMP 专门用于让主机或路由器互相报告网络中发生的差错和异常情况,以提高交付成功的机会。

2. 底层封装格式

  • 层层嵌套:ICMP 报文并不是直接交付给数据链路层,而是作为数据部分封装在 IP 数据报中。
  • IP 首部相关关键字段:
    • 协议(Protocol)字段 = 1:明确指示该 IP 数据报的数据部分是 ICMP 报文。
    • 携带源 IP 地址、目的 IP 地址、TTL、首部校验和等信息。
  • ICMP 报文自身结构:其首部的前 8 bit 为 Type(类型)字段,用于在底层直接指明该 ICMP 报文的具体功能类型。

QQ20260610-095619

二、 ICMP 报文分类与深度解析

ICMP 报文主要分为两大类:差错报告报文 和 询问报文。

1. 差错报告报文(Error Reporting)

① 终点不可达 (Destination Unreachable)

当路由器或目的主机无法交付数据报时发送。根据触发主体的不同分为两种常考场景:

  • 路由器发送(网络/主机不可达):当路由器找不到去往目的网络的路由线索时(道路不通),丢弃数据报并向源主机报告。
  • 目的主机发送(端口不可达):数据报已经成功送达目的主机,但目的主机发现传输层对应的目的端口号不存在(本地没有对应的监听进程),由目的主机向源主机报告。

QQ20260610-100642

② 时间超过 (Time Exceeded)

  • 路由器发送(传输途中超时):数据报在网络中传输时,每经过一个路由器其 TTL 减 1。一旦路由器发现 $\text{TTL} = 0$,则直接丢弃该数据报,并向源主机发送时间超过报文(说明路程太远或出现路由环路)。
  • 目的主机发送(分片组装超时):当一个大 IP 数据报被分片传输时,如果部分分片已到达目的主机,但在规定时间内其余分片未能齐备,目的主机将无法重组,只能全部丢弃已到分片,并向源主机发送时间超过报文。

③ 参数问题 (Parameter Problem)

  • 发送主体:路由器或目的主机。
  • 触发条件:当发现收到的 IP 数据报首部中的某些参数不合法,或者首部校验和出现差错时,丢弃该数据报并向源主机报告。

④ 改变路由 / 重定向 (Redirect)

  • 发送主体:局域网内的路由器。
  • 触发场景:主机 $A$ 本应将去往目的地 $C$ 的数据报直接发送给更近的路径,但却错误地发给了默认路由器。默认路由器在帮其转发的同时,会向 $A$ 发送重定向报文,提示:“对于这个目的网络,下次你直接让另一台路由器帮你转发,路径会更短”,以此让主机动态更新路由表。

注:源点抑制报文(用于通知源端网络拥塞、请求降低发送速率)在 2012年后已被废弃。

2. 询问报文(Query)

① 回送请求与回答 (Echo Request / Reply)

  • 交互逻辑:源主机向目的主机发送回送请求(“在吗?请回答我”),目的主机收到后必须原样回传回送回答(“在!”)。
  • 用途:用于测试目的站是否可达以及了解其有关状态。

② 时间戳请求与回答 (Timestamp Request / Reply)

  • 交互逻辑:请求当前网络的绝对时间。
  • 用途:用于进行网络时钟同步和测量传输时延。

三、 不必反馈 ICMP 差错报告的特殊情况(高频考点)

为了防止网络中出现 ICMP 报文无限循环并导致信道拥塞,以下 4 种情况绝对不发送 ICMP 差错报告报文:

  1. 差错中的差错不报:如果出错的数据报本身就是携带着 ICMP 差错报告报文 的 IP 数据报,则不再反馈差错。
  2. 分片只报第一个:如果一个 IP 数据报被分片传输,那么只有第一个分片出错时才会触发反馈,后续的其余分片出错时一律不予反馈。
  3. 特殊目的地址不报:对具有多播(组播)地址或广播地址的 IP 数据报,一律不发送差错报告。
  4. 特殊源地址不报:如果 IP 数据报的源地址是特殊地址(如环回自检地址 127.x.x.x 或未知源地址 0.0.0.0),即便发生异常也不反馈。

四、 ICMP 的典型应用场景

1. ping 命令

  • 底层机制:直接基于 ICMP 询问报文中的 回送请求(Echo Request) 和 回送回答(Echo Reply) 实现。
  • 工作层级:ping 属于应用层直接使用网络层 ICMP 的典型例子,不经过传输层(不使用 TCP 或 UDP)。

2. traceroute (Windows 下为 tracert) 命令

  • 底层机制:主要基于 ICMP 差错报告报文中的 时间超过报文 来探测去往目的主机所经过的整条路由器路径。
  • 实现原理:
    1. 源主机向目的主机发送一系列 IP 数据报,故意将第一个数据报的 $\text{TTL}$ 设为 1。
    2. 第一跳路由器收到后 $\text{TTL}$ 减 1 变为 0,被迫丢弃并回传一个 ICMP 时间超过报文,源主机由此获知第一跳路由器的 IP。
    3. 随后源主机将 $\text{TTL}$ 逐次递增为 2, 3, 4... 发送,依次诱发沿途各级路由器回传超时报文,直到最终数据报安全到达目的主机(通常通过触发目的端口不可达报文来收尾),从而拼接出完整的网络拓扑路径。

IPv6

一、 IPv6 的诞生背景与优势(与 IPv4 对比)

特性维度 IPv4 IPv6
诞生年份 1981年 1998年
地址位数 32 位(32 bit) 128 位(128 bit)
地址总数 $2^{32} \approx 42$ 亿个 $2^{128} \approx \mathbf{3.4 \times 10^{38}}$ 个
人均分配量 全球人均不足 1 个 全球人均可得 $4.3 \times 10^{28}$ 个
根本解决问题 依赖 1994 年诞生的 NAT 技术缓解耗尽危机 彻底解决 IP 地址不够用的问题
应用时代 传统互联网时代 万物互联(IoT)时代(让每个传感器、电器都拥有全球唯一IP)

QQ20260610-111225

二、 IPv6 地址格式与基本记法

  • 记法名称:冒号十六进制记法。
  • 划分规则:总共 128 bit,每 16 bit 为一段(共 8 段),每段转换为 4 位十六进制数,段与段之间用冒号(:)分隔。
  • 二进制对照示例:
    • 二进制:00100001000011011000...(共128位)
    • 标准十六进制表现形式:2001:0db8:85a3:0000:0000:8a2e:0370:7334

三、 核心考点:IPv6 地址的压缩记法

为了简化冗长的 IPv6 地址,有两条严格的压缩改写规则:

  1. 省略前导零:每个 16 位分段内部的前导零可以省略,但如果全为 0 则必须保留一个 0。
  2. 双冒号“::”替代:可使用双冒号 :: 替代连续出现的多个全 0 段。
    • ⚠️ 极重要约束:一个地址块中双冒号“::”只能出现一次,否则在恢复全零时会产生位数歧义。

📝 压缩改写实例闭环演练

  • 未压缩的原始地址:2001:0db8:85a3:0000:0000:8a2e:0370:7334
  • 第一步:去除各分段前导零 $\rightarrow$ 2001:db8:85a3:0:0:8a2e:370:7334
  • 第二步:双冒号替代连续全零段 $\rightarrow$ 2001:db8:85a3::8a2e:370:7334

四、 IPv6 地址分类表(高频选择题考点)

一台主机的完整 IPv6 地址结构表现为两级:IP地址 = 【n 位网络前缀, 128-n 位接口标识符】。主要分类映射关系如下:

地址类型 二进制前缀 CIDR 记法形式 含义与实际用途
未指明地址 全 0(128位) ::/128 表示“无地址”,类似于 IPv4 中的 0.0.0.0。
环回地址 前127位全0,最后1位为1 ::1/128 类似 IPv4 的 127.0.0.1,用于本地软件环回自检测试。
多播地址 11111111(8位) FF00::/8 发送到一组属于特定多播组的主机(类似于群号)。
本地链路单播地址 (Link-Local) 1111111010(10位) FE80::/10 局域网内通信使用,该地址的数据报绝不会被路由器转发。
全球单播地址 除上述四种外的其他所有地址 — 相当于公网 IP,可全球路由转发。

五、 IPv6 目的地地址的三种基本类型

IPv6 协议取消了 IPv4 中的广播概念,全面升级为以下三种目的地址通信模式:

  1. 单播(Unicast):传统的点对点通信。
  2. 多播(Multicast):一点对多点的通信。数据报会发送到一组计算机中的每一台。
    • 典型应用:网络视频会议、多播流媒体。
  3. 任播(Anycast):IPv6 新增的高级类型。目的地是一组计算机,但数据报只交付给其中距离最近的一台计算机。
    • 典型应用:多个地理位置分散的 DNS 服务器共享同一个任播地址,引导用户就近访问。
    • 注:任播地址没有固定的专属前缀,通常由地址管理机构从单播地址块中预先统一分配。

六、 IPv6 地址资源的分配与“即插即用”特性

1. 灵活的资源块级联

IPv6 完美支持无分类编址 CIDR。地址层级通常划分为:[前缀 (n bit) , 子网标识符 (m bit) , 接口标识符 (128-n-m bit)]。

  • 分配链路:一级 ISP $\rightarrow$ 二级 ISP(如北京电信) $\rightarrow$ 某学校局域网。
  • 标准校园网前缀示例:2001:0db8:abcd:0012::/64(前缀占 64 bit,留 64 bit 给接口标识符)。

2. 即插即用(无状态自动配置 - SLAAC)

  • 工作机理:由于 IPv6 局域网的接口标识符位数(通常为 64 bit)非常长,一台新主机接入网络后,只需向本地路由器确认本网络的前缀信息,主机便可利用自身的 MAC 地址自动计算并拼接出全球唯一的接口标识符,从而自行生成合法的 IPv6 地址。
  • 对 DHCP 的影响:基于即插即用特性,IPv6 可以在完全不部署 DHCP 服务器的情况下实现全自动联网。但在一些对网络安全性、审计要求极高的特定场所,依然会配合使用 DHCPv6 进行地址的有状态统一集中管理。

路由协议

QQ20260610-134413

一、 路由协议与路由算法概述

  • 路由器的核心功能:转发 IP 数据报。
  • 路由协议的定义:用于规定路由器之间如何交换必要的网络拓扑或状态信息。
  • 路由算法的本质:
    • 路由器之间交换的信息将作为路由算法的输入。
    • 路由算法通过计算,输出最佳转发路径,从而最终生成路由器的路由表。
    • 其数学本质是求解图的最短路径问题(Shortest Path Problem)。
  • 大纲要求掌握的三大路由协议:RIP 协议、OSPF 协议、BGP 协议。

二、 路由算法的分类

根据能否随网络的通信量或拓扑结构自适应地进行调整变化,路由算法可分为两大类:

  1. 静态路由算法(Static Routing):
    • 机制:由网络管理员手工配置每一条路由信息。
    • 特点:实现简单、开销小,但不具备自适应能力。
    • 适用场景:小型、结构稳定的网络。
  2. 动态路由算法(Dynamic Routing):
    • 机制:路由器根据网络流量负载和拓扑结构的变化,通过路由算法动态调整自身的路由表。
    • 特点:实现复杂、开销大,但具备良好的自适应能力。
    • 适用场景:大型、多变的网络。
    • 核心两大动态路由算法:距离-向量路由算法(对应 RIP 协议)和链路状态路由算法(对应 OSPF 协议)。

三、 距离-向量路由算法(Distance-Vector Routing)

1. 核心思想与算法基础

  • 基于 Bellman-Ford(贝尔曼-福特)算法 的核心思想实现。
  • “距离”的定义:不同的路由协议定义不同。例如,RIP 协议中规定直接相连的两个节点之间距离(跳数)= 1;也可以将“往返时延”作为链路的距离。

2. 数学递推公式

假设 $d_x(\text{net1})$ 表示从当前节点 $x$ 到目的网络 $\text{net1}$ 的最短距离(最小代价),则有:

  • 参数解析:
    • $v$:节点 $x$ 的所有邻居节点(共 $k$ 个邻居,如 $v1, v2, v3…vk$)。
    • $c(x, v)$:节点 $x$ 到其特定邻居 $v$ 之间的直接链路距离。
    • $d_v(\text{net1})$:邻居 $v$ 到目的网络 $\text{net1}$ 的最短距离。

3. 向量传递机制

  • 每个邻居节点都会定期向中间路由器 $X$ 发送一个“距离-向量”(即该邻居到一片区域内每个目的网络的已知最短距离集合)。
    • 例如:邻居 $v1$ 传来的向量为:去往 $\langle\text{net1, net2, net3}\rangle$ 的最短距离 = $\langle4, 6, 3\rangle$。
    • 邻居 $v2$ 传来的向量为:去往 $\langle\text{net1, net2, net3}\rangle$ 的最短距离 = $\langle3, 9, 10\rangle$。
  • 路由器 $X$ 收到所有邻居的向量后,结合自己到邻居的直接开销 $c(x, v)$,利用上述公式计算出自己去往各个网络代价最小的路径,更新并维护自己的路由表。

QQ20260610-134105

  • 对应协议:OSPF 协议的底层核心算法。
  • 核心要求:要求每台路由器节点都了解完整的网络拓扑结构。也就是说,每个节点都必须知道全网共有多少个节点、哪些节点是相连的、各条链路的代价(权值)是多少。
  • 对应数据结构:带权图(Weighted Graph)。
  • 核心算法机制:只要一台路由器通过链路状态洪泛拿到了完整的带权图结构,就可以在本地调用 “迪杰斯特拉(Dijkstra)算法”,以自身为源点,计算出到达全网各个目的网络的最短路径,进而生成最优的转发表项。

路由选择

一、 为什么需要分层次的路由选择?

  • 根本矛盾:全世界的路由器数量、网络数量巨大(以亿计),没有任何一台路由器能拥有并维护全网完整的拓扑图或路由表(否则路由表过大,查表极慢,且占满设备内存)。
  • 解决方案:引入分层次的路由选择机制。将全球网络划分为技术上独立的区域,并在“区域内部”和“区域之间”使用不同级别的路由协议。

二、 自治系统 AS (Autonomous System)

  • 定义:指将全世界的因特网划分为多个在技术和行政上相互独立的管理区域。例如,由咸鱼电信管辖的网络区域可以申请成为一个 自治系统 AS。
  • 技术决策权:AS 的管理单位有权自行决定在本自治系统内部,使用何种内部路由协议(如 RIP 或 OSPF)。
  • 层级关系:自治系统之间是平级关系,不存在上下级的包含关系。

📌 AS 的关键特征(考点拓展)

  1. 全球唯一编号:每个自治系统都拥有一个全球唯一的 AS 编号(ASN),需要向互联网管理机构(如由 ICANN 授权的机构)统一申请。目前全球已接近 8 万个自治系统。
  2. CIDR 地址块组合:一个自治系统通常包含一个或多个 CIDR 地址块。这样的设计非常便于在该自治系统的边界处进行路由聚合,减少通告给外网的路由条目。

QQ20260610-135336

三、 路由协议的两大阵营:IGP 与 EGP

因特网将路由选择协议明确划分为两大类:

1. 内部网关协议 IGP (Interior Gateway Protocol)

  • 作用范围:在一个自治系统(AS)内部进行路由选择。
  • 经典代表:
    • RIP 协议:基于距离-向量路由算法(网络数量越多,距离向量越大)。
    • OSPF 协议:基于链路状态路由算法(要求 AS 内每台路由器建立本系统的完整拓扑图)。
  • 形象比喻:相当于本地交流所用的“地方方言”。不同 AS 的内部方言互不影响。

2. 外部网关协议 EGP (External Gateway Protocol)

  • 作用范围:在不同的自治系统(AS)之间进行路由选择。
  • 当前标准:BGP 协议(如目前广泛使用的 BGP-4)。
  • 形象比喻:相当于不同区域之间沟通所采用的“普通话”。

💡 术语历史注脚:在早期 TCP/IP 文献中,“网关(Gateway)” 便是路由器的代称,该术语沿用至今(如“默认网关”即为本地路由器的内网接口)。

四、 边界路由器与域间/域内路由选择

  • 自治系统边界路由器(Border Router):每个 AS 至少需要有一台边界路由器与其他自治系统相连。由于需要承载和计算全网的跨系统路径,边界路由器通常性能强悍、价格高昂。
  • 边界路由器之间的通信:各边界路由器之间统一使用 BGP 协议 进行跨系统的路由信息交换。
  • 概念区分:
    • 域内路由选择(Intra-domain routing):自治系统内部的路由选择。
    • 域间路由选择(Inter-domain routing):自治系统之间的路由选择。

路由信息协议 RIP(Routing Information Protocol)

一、 RIP 协议的核心规定与表项格式

1. 距离度量方式:跳数 (Hop Count)

  • 定义:RIP 使用跳数(也称距离)来衡量到达目的网络的远近。
  • 计算规则:
    • 路由器到其直连网络的距离规定为 1。
    • 每经过一个路由器进行转发,跳数就 加 1。
  • 最优路径判定:RIP 算法非常单纯,它认为好的路由就是跳数最少的路由(即使该路径带宽极低)。

2. 网络规模限制(核心考点)

  • 最大合法距离:RIP 允许一条合法路径的最大跳数为 15。
  • 不可达判定:当距离等于 16 时,即表示网络不可达。
  • 技术评价:由于存在 15 跳的上限限制,RIP 只适用于小型自治系统(AS),无法应用于大规模的全球级网络。

3. RIP 路由表项格式

每个运行 RIP 的路由器都需要维护自己的路由表,其核心三元组表项格式为:

  • 实质:路由器维护的从自身到其他每个目的网络的距离记录,在本质上就是其距离向量。

QQ20260610-142648

二、 RIP 报文的协议层级与封装结构

1. 协议层级定位

  • ⚠️ 易错点敲黑板:虽然 RIP 是网络层路由协议,但在体系结构上 RIP 属于应用层协议。

2. 下层传输依赖

  • RIP 在传输层使用 UDP 协议 传送数据。
  • 专用端口号:520。

3. 数据逐层封装顺序

RIP 报文在流经网络栈时,包装逻辑如下:

4. RIP 报文的两大基本类型

  • Request(请求)报文:用于路由器刚启动或查询时,请求邻居路由器发送其完整的路由表。
  • Response(响应)报文:又称路由刷新报文。用于主动响应请求,或在特定周期到达时自动向外分发。
    • ⚠️ 拆分限制规则:由于每个 Response 报文最多只能携带 25 个路由表项信息,如果路由器的路由表过大,就必须拆分成多个独立的 Response 报文进行分批传输。

三、 RIP 的信息交换机制 (Who / What / When)

运行 RIP 协议的路由器之间,其数据交互严格遵循以下三要素:

1. Who(和谁交换?)

  • 对等体限制:仅和直接相邻的路由器交换信息。非相邻路由器之间不直接发生 RIP 通信。

2. What(交换什么?)

  • 内容总量:交换的是本路由器目前所知道的全部信息,即自己当前的整张路由表(即自身计算出的完整距离向量)。

3. When(何时交换?)

  • 定期交换:通常按固定的时间间隔(默认通常为 30 秒)自动向邻居发送一次 Response 报文。
  • ⚡ 触发更新(Triggered Update)机制:
    • 引入目的:为了加快 RIP 的网络收敛速度。
    • 运行逻辑:一旦路由器自身发现网络拓扑发生了变化(如某个直连链路中断),它绝不等待 30 秒周期,而是立即向相邻路由器发射通告拓扑变化后的路由信息,从而在最大程度上防止路由环路的恶化。

RIP 工作过程与收敛机制

一、 RIP 路由表更新算法(核心机制)

当路由器收到相邻路由器发来的 RIP 报文时,严格按照以下两步执行更新算法(基于 Bellman-Ford 算法):

步骤一:对邻居发来的报文进行“预处理”

假设收到地址为 $X$ 的相邻路由器发来的 RIP 报文,首先修改该报文中的所有项目:

  1. 将所有项目的“下一跳路由器”地址都改为 $X$。
  2. 将所有项目的“距离(跳数)”字段的值全部加 1。

步骤二:对比原路由表,执行查表更新逻辑

针对修改后的各个报文项目,与本地原路由表逐一对比,按照以下 IF-ELSE 分支 决策:

  • 情况 A(发现新网络):
    • 条件:若本地原路由表中没有目的地网络 $N$。
    • 动作:直接将该项目添加到本地路由表中。
  • 情况 B(老路径更新,强行同步):
    • 条件:若本地原路由表中已有目的地网络 $N$,且原表项中的下一跳路由器地址刚好就是 $X$。
    • 动作:强制用收到的新项目替换原有的老项目(无论新项目的距离是变大还是变小,因为本地是通过 $X$ 去往 $N$ 的,必须以 $X$ 的最新状态为准)。
  • 情况 C(全新路径对比,择优录用):
    • 条件:若本地原路由表中已有目的地网络 $N$,但原表项中的下一跳路由器地址不是 $X$(说明这是一条全新的备选路径)。
    • 动作:对比两者的距离值。
      • 若新项目中的距离 $d$ 小于原路由表中的距离 $\rightarrow$ 执行更新,将下一跳改为 $X$,距离改为 $d$(发现了更近的路由)。
      • 若新项目中的距离 $d$ 大于或等于原路由表中的距离 $\rightarrow$ 什么也不做(忽略该信息)。

QQ20260610-162648

QQ20260612-100456

额外补充:超时老化规则

  • 若 180 秒(RIP 的默认超时时间)内一直没有收到某个相邻路由器的更新路由表,则判定该相邻路由器出现故障。
  • 本地路由器会将该相邻路由器标记为不可达,即将其对应的所有路由表项的距离设置为 16(表示不可达)。

二、 RIP 协议的优缺点对比

优点(好消息传播得快)

  1. 实现简单、算法开销小。
  2. 若网络中出现了一条更短、更优的新路由,该好消息会在非常短的时间内迅速在自治系统内传遍所有路由器,网络收敛过程较快。

缺点(坏消息传播得慢)

  1. 限制了网络规模:它能使用的最大合法距离为 15,当距离达到 16 即代表不可达,因此无法用于大型网络。
  2. 开销随规模递增:路由器之间交换的是完整的路由表,网络规模越大,占用的链路开销和处理内存越大。
  3. 慢收敛现象:当网络出现故障、链路断开时,故障消息需要经过沿途路由器多次反复地信息交换和迭代才能最终收敛。

三、 深度解析:“坏消息传播得慢”与“计数到无穷”现象

结合课件中的 360 时刻拓扑故障演练,我们可以彻底复盘慢收敛(计数到无穷)的底层原因:

QQ20260612-131326

1. 正常收敛状态(0时刻)

  • 各路由器开机时,只知道自己到直接相连网络的距离为 1。
  • 经过若干次周期性(30秒)的邻居交换后,全网路由表项达到平稳,称为收敛。此时,R1 知道去往 Net2 需要通过 R2,距离为 2。

2. 故障发生与“相互欺骗”(360时刻)

  • 场景:去往 Net2 的链路突然损坏,或者路由器 R2 发生故障。
  • 环路产生的本质原因:
    1. 当 R2 损坏后,R1 到 Net2 的直接路径其实已经断开。
    2. 然而,由于 RIP 的周期性异步更新,在故障坏消息还没来得及通告全网前,相邻的 R3 在 30 秒周期到来时向 R1 发送了自己的完整路由表。
    3. R3 的路由表中包含旧信息:〈Net2, 距离=3, 下一跳=R1/R4〉。
    4. R1 收到 R3 的报文后,应用更新算法:下一跳设为 R3,距离加 1 变成 4。由于 R1 原本去往 Net2 的下一跳本是损坏的 R2,现在发现 R3 居然“能到”Net2,于是 R1 误信了 R3 的信息,将路由表更新为:通过 R3 去往 Net2,距离为 4。

3. 计数到无穷(Count to Infinity)的过程

  • 紧接着下一轮更新:R3 收到 R1 发来的新路由表(距离=4),根据“同一下一跳强行同步”或者算法叠加,R3 误以为通过 R1 去往 Net2 的距离变成了 $4+1 = \mathbf{5}$。
  • 随后,R1 再次收到 R3 的更新,又将自己的距离加 1 修正为 6。
  • 结论:R1、R3、R4 之间产生了路由环路,它们在彼此发来的报文中不断将距离“层层加码、互推自增”(从 4 递增到 5、6、7……)。

4. 破环机制

这种死循环会一直持续,直到多轮迭代后,Net2 的开销距离增加到 16 时,各路由器才依据 RIP 规定触发“16 = 不可达”的强行限制,丢弃环路路由。

这完美解释了为什么 RIP 协议在面对网络故障时,“坏消息传播得慢”。而将最大跳数粗暴地限制为 16,正是 RIP 为了防止路由环路无限扩散所采取的简单直接的兜底手段。

最短路径优先协议 OSPF(Open Shortest Path First)

一、 OSPF 协议概述与协议栈位置

1. 协议定位

  • ⚠️ 极高频考点(与 RIP 对比):OSPF 属于网络层协议,它不依赖传输层(TCP 或 UDP),而是直接使用 IP 协议提供的服务。
  • IP 首部关键标识:在封装 OSPF 分组的 IP 数据报首部中,协议(Protocol)字段值 = 89。
  • 数据单元称谓:OSPF 的协议数据单元(PDU)常译为 “OSPF 分组” 或 “OSPF 数据报”。

2. 核心底层数据结构

  • 为了计算最佳路径,每一台 OSPF 路由器都需要在本地建立并维护整个网络的拓扑图。
  • 数学模型:网络拓扑在底层被抽象为 带权有向图。
  • 存储方式:在路由器内存中,通常使用 邻接表(Adjacency List) 结构来存储这一图结构。
  • 核心路由算法:在完整掌握全网拓扑的基础上,路由器调用 迪杰斯特拉(Dijkstra)算法 计算自身到各个目的网络的最短路径,进而生成最优转发表。

image-20260612144236262

二、 OSPF 工作流程与洪泛机制 (What / How / When)

OSPF 通过“链路状态(LS, Link-State)”的同步来让全网路由器达成拓扑共识,其交互逻辑表现为:

1. What:说什么?(链路状态通告)

路由器向外通告的是与自己直接相连的所有链路状态。具体包括:

  • 我是谁 $\rightarrow$ 本路由器的唯一标识(顶点)。
  • 我的邻居都有谁 $\rightarrow$ 与哪些路由器或网络直连。
  • 代价是多少 $\rightarrow$ 到达每个邻居的直接链路开销(带权有向边)。

2. How:怎么说?(洪泛法 Flooding)

  • 传播机制:使用 洪泛法。一传十、十传百,迅速扩散至整个自治系统(AS)内的所有 OSPF 路由器。
  • 转发防环规则:
    1. 洪泛信息不可“回流”转发(即不能将收到的信息再倒手发回给发件接口)。
    2. 如果路由器收到了一条本地已经存在的、重复的链路状态信息,不可重复转发。

3. When:什么时候说?(触发时机)

  • 动态触发:当路由器探查到自己身边的链路或节点发生变化时,再次立即洪泛最新的链路状态信息。
  • 优势判定:由于一旦有变动就立即精准洪泛,因此 OSPF 不存在“坏消息传播得慢” 的慢收敛现象。

4. 链路状态的版本控制:32位序号机制

  • 为了确保全网信息的一致性,每个链路状态都带有一个 32 位的序号。
  • 版本判定规则:序号越大,说明该链路状态越新(相当于信息的版本号)。这能保证旧的或延迟到达的干扰报文被路由器自动忽略。

三、 OSPF 的五种核心分组类型

OSPF 协议栈内部定义了 5 种不同的分组类型,用于维护邻居关系和同步拓扑数据库:

Type 值 英文全称 中文译名 核心功能与应用场景
1 Hello Packet 问候分组 用于发现、建立并周期性维护邻居路由器的可达性(“保持心跳”)。
2 Database Description Packet 数据库描述分组 (DD) 握手阶段使用。向邻居给出自己的链路状态数据库的摘要信息,供对方查漏补缺。
3 Link State Request Packet 链路状态请求分组 (LSR) 在收到 DD 分组后,向邻居请求发送本地缺失或已过期的某些链路状态的详细信息。
4 Link State Update Packet 链路状态更新分组 (LSU) 最为核心的分组。采用洪泛法向全网精准发送、同步具体的链路状态信息。
5 Link State Acknowledgment Packet 链路状态确认分组 (LSAck) 对收到的 LSU 更新分组进行显式的可靠确认,保证洪泛信息不丢失。

四、 OSPF 的其他高级重要特性

QQ20260612-142943

1. 灵活自定义链路“代价(Metric)”

  • OSPF 允许网络管理员针对不同的业务类型(如视频、音频或普通数据),为同一条链路赋予不同的代价值,从而计算出多条针对性路由。

  • 默认度量标准:主要基于带宽计算链路代价。

    因特网默认参考带宽一般为 $100\text{ Mbps}$。

  • ⚠️ 边权不对称规则:在实际应用中,一段链路的两个方向,其权值可能不同。另外,从【直连网络 $\rightarrow$ 路由器】方向的权值通常规定为 0。

2. 支持等价多路径转发(ECMP, Equal-Cost Multi-Path)

  • 负载均衡:如果路由器计算出到达同一个目的网络有多条总代价值完全相等的最短路径,OSPF 可以同时启用这些路径,将通信量平均分配,从而实现高效的负载均衡。

3. 强悍的鉴别功能(安全防范)

  • 安全机制:OSPF 分组支持在首部开启鉴别功能,从而保证只有在彼此信任的可信赖路由器之间才会交换链路状态信息。
  • 防范攻击场景:
    • 恶意外部攻击:防止黑客潜入机房,用黑路由器伪造、洪泛虚假的拓扑信息。
    • 中间人篡改攻击:防止沿途被控制的病毒节点(如被污染的中间路由器)恶意篡改 OSPF 洪泛报文。

4. 完美支持 VLSM 与 CIDR

  • OSPF 彻底摆脱了分类 IP 的束缚,完美支持变长子网划分(VLSM)和无分类域间路由(CIDR),能够非常方便地处理通过二叉树机制灵活划分出来的各种大小不一的子网地址块。

OSPF的多区域划分

一、 OSPF 的“多区域(Area)”划分机制

image-20260612151147285

1. 为什么要划分区域?

  • 根本矛盾:当自治系统(AS)规模变得很大时,全网路由器数量激增,会导致两个致命问题:
    1. 洪泛流量过大:全网洪泛链路状态信息会吃满链路带宽。
    2. 计算开销过载:每个路由器的 LSDB(链路状态数据库)变得极大,运行 Dijkstra 算法计算最短路径时会严重消耗 CPU 资源。
  • 解决方案:将一个自治系统再划分为若干个更小的逻辑区域(Area)。
  • 核心核心优化(考点):划分区域后,洪泛法(Flooding)交换链路状态信息的范围被严格局限在各个区域内部,而不是整个 AS。这样极大地减少了网络通信量,并使每个区域内部路由器的 LSDB 保持在一个较小的规模。

2. 区域的层级结构

在一个 OSPF 自治系统内部,区域划分为两个层级:

  • 主干区域(Backbone Area):
    • 标识符固定为 0(32位区域标识符 Area ID = 0.0.0.0)。
    • 核心作用:负责连通所有其他的非主干区域。系统中有且仅有一个主干区域。
  • 非主干区域:
    • 标识符不为0(如 Area 1 可记为 0.0.0.1,Area 2 为 0.0.0.2 等)。
    • 连接规范:每一个非主干区域都至少有一台区域边界路由器(ABR)与主干区域直接相连。非主干区域之间通常不直接相连,它们的跨区域流量必须通过主干区域进行中转。

二、 OSPF 路由器的四种角色分类

根据路由器在多区域架构中所处的地理位置,OSPF 将路由器精确划分为以下几类(结合拓扑图示例):

路由器类型 英文简称 拓扑对应节点 核心核心职责与流量控制
区域内部路由器 (Internal Router) IR R1, R2, R5, R8, R9 也称非边界路由器。所有接口都属于同一个非主干区域,仅需知道本区域内部的局部网络拓扑。发往其他区域的流量一律盲投给本区域的 ABR 处理。
区域边界路由器 (Area Border Router) ABR R3, R4, R7 跨区域的桥梁。接口同时连接主干区域和非主干区域,负责流向/流出该区域的 IP 分组的路由转发与通告。
主干路由器 (Backbone Router) BR R3, R4, R5, R6, R7 所有接口或至少一个接口属于主干区域(Area 0)的路由器。(注:ABR 同时也是主干路由器)。
自治系统边界路由器 (AS Boundary Router) ASBR R6 处在 OSPF 自治系统的最外外沿,至少有一个接口连接了外部的其他自治系统(或其他路由协议区域),负责本自治系统与其他 AS 之间的域间 IP 分组路由转发。

三、 OSPF 核心术语深度辨析(易混淆点)

课件中对三个高频概念进行了严谨的界定,复习时务必清晰:

  1. LSI(Link State Information,链路状态信息):
    • 性质:是一个广义的、抽象的概念。
    • 含义:泛指所有用来描述链路状态、边开销、邻居关系的各种原始网络信息。
  2. LSA(Link-State Advertisement,链路状态通告):
    • 性质:是 OSPF 协议中显式定义的一种具体的数据结构(封装格式)。
    • 含义:路由器实际产生的、用来在网络中进行洪泛传递的规范化承载单元。
  3. LSDB(Link-State Database,链路状态数据库):
    • 性质:是 LSA 的集合。
    • 含义:全网 LSA 汇聚在路由器内存中构建出的全网拓扑大地图(带权有向图的邻接表存储形式)。

四、 路由表的生成与事件驱动收敛特性

image-20260612151120461

1. 经典转折:从“完整路径”到“只存下一跳”

  • Dijkstra 算法的输出:以 R1 为例,运行迪杰斯特拉算法后,计算出的是到达全网各个网段的完整最优路径。
    • 例如:到 Net4 的最优完整路径是:$\text{cost}=3, \text{ R1}\rightarrow\text{R3}\rightarrow\text{R4}\rightarrow\text{Net4}$。
  • 路由表的实际存储(核心考点):虽然算出了完整路径,但路由表中绝对不会存储完整路径,而只存储“下一跳(Next Hop)”和“当前距离”。
    • 转换结果表项:目的网络: Net4 | 下一跳: R3 | 距离: 3。这种设计极大地精简了路由表的体积,加快了查表转发速度。

2. 完美的事件驱动响应闭环

OSPF 属于事件驱动型协议,当网络平稳时,不发生无谓的信息交换。一旦网络发生风吹草动,将触发以下联动收敛机制:

OSPF 邻居关系建立与数据库(LSDB)同步

一、 三大核心阶段概述

QQ20260612-152346

  1. 阶段一:确定可达性
    • 核心目标:发现并确认双向邻居关系。
    • 依赖分组:Type 1: Hello 分组(问候分组)。
  2. 阶段二:达到数据库的同步
    • 核心目标:互换本地拓扑地图的“简要目录”,找出彼此缺失的条目。
    • 依赖分组:Type 2: DD 分组(数据库描述分组)。
  3. 阶段三:新情况下的同步(查漏补缺)
    • 核心目标:精准索取缺失的数据并进行可靠确认,最终让两端 LSDB 完全一致。
    • 依赖分组:Type 3: LSR、Type 4: LSU、Type 5: LSAck。

二、 五种分组流向与底层机制(深度解剖)

QQ20260612-152905

1. 确定可达性 —— Type 1: Hello 分组(问候分组)

  • 交互逻辑:当某时刻 R2 与 R1 建立物理连接,双方开始对传 Hello 分组。
  • 两个核心定时器(高频选择题考点):
    • Hello 周期(心跳间隔):各台路由器每隔 10 秒向其直接邻居发送一次 Hello 分组,以维持邻居关系的存活。
    • 死亡超时(Dead Timer):如果超过 40 秒没有收到邻居发来的 Hello 问候,路由器就会判定该邻居已经发生损坏或不可达。
  • 状态变更触发:初次建立连接时,Hello 分组会触发两端路由器的本地 LSA 序号发生 +1 修正(信息版本升级),准备向全网洪泛最新的链路状态。

2. 目录初对齐 —— Type 2: DD 分组(Database Description,数据库描述分组)

  • 交互逻辑:两台路由器的邻居关系建立好后,并不盲目发送庞大的完整数据库,而是首先向邻居互传 DD 分组。
  • 核心载荷:DD 分组内包含的是各自 LSDB 的摘要信息(即所有 LSA 的头部信息,不含具体载荷)。
  • 通俗类比:相当于把各自手里的“拓扑地图”变成一份“精简目录索引”丢给对方。R1 和 R2 通过阅读对方的目录,就能立刻核对出哪些 LSA 对方有而自己没有,或者哪些 LSA 对方的版本比自己更新。

3. 精准查漏补缺 —— Type 3 ~ Type 5 分组联动闭环

当两台路由器比对完 DD 分组的“目录索引”后,将触发以下三步高可靠响应闭环:

  • 核心机制:对比 DD 目录后,如果 R1 发现 R2 拥有自己缺失的、或者版本更新的 LSA 项,R1 就会产生并向 R2 单播发射 LSR 分组。
  • 形象写照(源自课件趣味表情包):“好朋友作业借我抄抄”——在分组里显式列出所有自己需要抄录的 LSA 编号。
  • 核心机制:这是 OSPF 协议中最核心、数据量最大的实质性分组。R2 收到 R1 的 LSR 请求后,将对方索要的、包含完整底层信息(邻居是谁、代价多少)的 LSA 完整数据打包嵌入 LSU 分组中,单播回复给 R1。
  • 注:在真实的现网环境中,一旦任何一处链路发生变动,路由器也会主动产生 LSU 向全网进行事件驱动型洪泛通告。
  • 核心机制:R1 成功接收到 R2 发来的 LSU 完整内容后,必须回传一个 LSAck 分组进行显式交代。
  • 载荷内容:LSAck 中携带确认收到的 LSA 头部信息(“已确认收到”)。
  • 技术价值:由于 IP 协议本身是不可靠的,OSPF 作为直接运行在网络层上的协议,通过 LSU ↔ LSAck 机制在网络层实现了数据的 100% 可靠传输,确保同步过程中不会因为丢包而导致拓扑图产生偏差。

边界网关协议 BGP(Border Gateway Protocol)

一、 BGP 协议的定义与四大核心特点

1. 根本定位

BGP 属于 外部网关协议(EGP),专门用于在不同的自治系统(AS)之间高效率地交换路由信息并实现跨系统的路由选择。

2. 四大核心特点(高频选择题考点)

  1. 追求“较好路由”,而非“最佳路由”:因特网规模太大,跨越全球不同运营商,寻找绝对的数学“最短路径”是不现实且没有意义的。
  2. 基于“策略”进行路由选择:AS 之间的选路必须通盘考虑政治、安全、控制或经济等非技术因素(例如:某国流量绝不从竞争对手国家的 AS 中转)。
  3. 采用路径向量(Path-Vector)路由算法:
    • 路由器在向邻居通告 BGP 路由信息时,不仅告诉对方目的地是谁,还会显式告知到达该目的地需要经过的完整自治系统路径(即 ASN 序列)。
    • 技术优势:拿到完整的 AS 路径后,路由器如果发现自己的 AS 编号已经在路径列表里,就能瞬间识别并斩断路由环路。
  4. 协议层级定位:⚠️ 极易错点!BGP 属于应用层协议,其底层传输依赖 TCP 协议 提供可靠连接,专用的 TCP 端口号 = 179。

二、 BGP 邻居关系与会话分类 (eBGP vs iBGP)

1. BGP 邻居(BGP 对等方 / BGP Peers)

运行 BGP 协议的通信双方互称为 BGP 对等方或邻居。两台边界路由器在物理链路连通后,必须首先建立底层 TCP 三次握手连接,随后才能在 TCP 管道之上交换 BGP 报文、确立 BGP 会话(Session)。

QQ20260614-130207

2. 两类 BGP 会话(看图复习,重点区分位置)

  • eBGP 会话(External BGP):处于两个不同 AS 边界且直接相连的路由器之间保持的会话(例如图中的 $R6 \leftrightarrow R4$,$R2 \leftrightarrow R1$)。
  • iBGP 会话(Internal BGP):处于同一个 AS 内部的所有运行 BGP 的路由器之间保持的会话(例如图中的 $R2, R3, R4, R5$ 两两之间)。

3. ⚠️ 核心水平分割规则与全连通(Full Mesh)约束

  • iBGP 核心水平分割规则:从一个 iBGP 邻居学到的 BGP 路由信息,绝对不能再通过 iBGP 传播给局域网内部的其他 iBGP 邻居。引入此规则是为了死死卡住 AS 内部的路由环路扩散。
  • 带来的 Full Mesh 约束:由于不能在内部“倒手传话”,为了让 AS 内部所有路由器都能同步获知外部的路由,规定同一个 AS 内部的所有 BGP 路由器之间必须建立两两全连通(Full Mesh)的 iBGP 会话。若有 $N$ 个内部 BGP 路由器,则在网络内部共需维系 $\frac{N \times (N-1)}{2}$ 条高成本的 iBGP 会话。

三、 BGP 路由信息结构与跨系统传播原理

1. BGP 路由信息组合

一条完整的 BGP 路由条目结构表现为:【CIDR 网络前缀, BGP 属性】。

2. 两大常考核心 BGP 属性

  • AS-PATH(自治系统路径):记录了去往目的网段沿途经过的所有自治系统编号序列。
  • NEXT-HOP(下一跳地址,改写规则是核心大题考点):
    • 在走 eBGP 跨系统发送时:路由器会将“下一跳”改写为自己接口的 IP 地址。
    • 在走 iBGP 内网同步时:⚠️ 默认绝不修改“下一跳”!(谢希仁教材与 RFC 标准)。如图中 $R2$ 从外网学到网段 $X$ 传给内网的 $R4$ 时,下一跳仍保持为外网的 $R1$。这是为了让内网路由器明确知道真正的流量出口是谁。(注:在实际工业界或某些特定真题如2024真题47题中,也可以通过特殊配置强行修改为发送方自己的内网IP)。

QQ20260614-130526

四、 BGP 决策算法:四步高级选路策略 (How)

当路由器通过 BGP 从外部不同的多条路径同时学到了去往同一个外部目的网络 $X$ 的多条 AS-PATH 时,它会启动以下四步优先级过滤算法来做决定:

步一:选择本地偏好值(Local Preference)最高的路线

  • 机制:由本地 AS 的网络管理员手工进行全局调配,相当于给不同出口链路上锁、赋予不同的“初始优先级”。通常会结合各链路的运营带宽成本和安全系数来动态评配。

步二:选择 AS 跳数(AS-PATH 长度)最少的路线

  • ⚠️ 核心敲黑板:AS 跳数最少,不等于路由器跳数最少。它只是说明穿过的国家/运营商区域最少。如果此项依然打平,则向下交由内网算法仲裁。

3. 步三:触发“热土豆路由选择算法(Hot Potato Routing)”

  • 底层理念:“烫手的山芋,要尽快脱手”。
  • 运行逻辑:如果有多条外部路线的 AS 跳数完全相同,那么路由器会查阅自己本地内部的网关协议(IGP,如 RIP 或 OSPF),计算出本机到达各个出口边界路由器所需的内网最小代价(Cost)。
  • 数据包将从本地内网代价最小的那个边界出口被立刻驱逐出去。
    • RIP 协议下,代价看的是内网路由器跳数;OSPF 协议下,代价看的是内部路径总长度。

QQ20260614-133833

步四:选择 BGP 标识符(BGP ID)最小的路由

  • 兜底策略:如果前三步全部打平,为了防止流量产生动荡,执行最终兜底。
  • BGP ID 规范:每台 BGP 路由器都有一个全球唯一的 32 位标识符(通常选取设备所有接口中数值最大的 IP 地址充当)。算法将盲选 BGP ID 最小的那台邻居路由器发来的路由(“听老员工的”)。

五、 BGP 的四种核心报文类型表

BGP 报文直接作为 TCP 的数据载荷进行高可靠传输,其定义的 4 种基础报文功能如下:

报文名称 英文全称 核心功能解析 触发与发送时机
① 打开报文 Open 用于与相邻的另一个 BGP 对等方进行初始化握手,协商参数并正式建立 BGP 会话。 底层 TCP 连接成功建立后,BGP 吐出的第一个报文。
② 更新报文 Update 最为核心的报文。用于向全网通告某条可达路由的详细属性,或声明撤销掉多条已经失效的死路由。 动态触发:一旦发现新的路由线索,或者既有路由属性发生变化时立即发射。
③ 保活报文 Keepalive 纯净的心跳跳动报文。用于向邻居周期性证明自己会话存活,维系 BGP 邻居状态不崩溃。 周期性自动发送(通常为 60秒 一次)。若超出 3 倍周期(180秒)未收到对方的保活,则断开会话并宣告邻居死亡。
④ 通知报文 Notification 异常绝杀报文。当检测到网络或报文首部发生严重差错时,向对方发送错误编码,并立即强行关闭 BGP 会话和底层的 TCP 连接。 处于被动触发状态:检测到 BGP 运行错误时立即发射(如报文首部参数校验出错)。

IP多播

一、 IP 多播的基本概念与核心优势

1. 什么是 IP 多播?

  • 定义:IP 多播(Multicast,曾译为 IP 组播)诞生于1980年代末,是一种典型的“一对多”通信技术。源主机只需发送一个 IP 多播分组,该分组在网络中传输时,由支持多播功能的路由器进行“按需复制”并“多口转发”,最终精准交付给一组特定的接收端主机。
  • 经典应用:网络视频直播、视频会议、IPTV(网络电视)等。

2. ⚡ 深度对比:单播 (Unicast) vs 多播 (Multicast)

假设视频服务器需要向内网的 90 个视频接收端(群成员)同时推送一段流媒体数据:

  • (a) 传统单播模式:
    • 机制:服务器必须串行或并行地向这 90 台主机独立发送 90 次单播分组。
    • 缺陷:主干链路(如主干路由器 R1 之前的链路)会被瞬间塞满 90 个一模一样的数据包拷贝,极度浪费网络带宽,且服务器的 CPU 和网卡 I/O 压力巨大。
  • (b) 升级多播模式:
    • 机制:服务器端仅需发送 1 次多播分组,该包的目的 IP 指向一个公共的“多播组地址”。当数据包到达关键分叉路径上的多播路由器(如 R1)时,路由器发现其下方的三个分支接口内都存在该多播组的成员,于是 R1 在本地按需复制出 3 个副本,分别向三个分支转发。
    • 优势:最大程度节省主干链路带宽,主干电路上永远只有一个数据包在流动,且服务器端实现了解耦与减负。

QQ20260614-144743

💡 考研名师趣味类比:

  • IP 单播 $\approx$ 微信“单独私聊”发送消息(单播地址 = 个人微信号)。
  • IP 多播 $\approx$ 微信“在微信群内”发消息,群成员都能收到(多播地址 = 微信群号)。

二、 IP 多播地址与三大底层特性(高频考点)

1. 多播 IP 地址规范

  • 对应网段:属于 IPv4 地址方案中的 D 类地址。

  • 二进制前缀:最高 4 位固定为 1110。

  • 十进制合法范围:

  • 成员特性:一个多播地址唯一对应一个特定的“多播组”,一个组内可以动态包含零台、一台或多台主机。

2. IP 多播的三大硬性特点(敲黑板划重点)

  1. 尽最大努力交付(Best-Effort):多播 IP 数据报在网络层同样提供的是无连接、不可靠的交付服务,协议本身不提供可靠交付保证(丢包、乱序由上层应用自行处理)。
  2. ❌ 绝不能作为源地址(核心死穴):多播地址只能作为 IP 首部中的“目的 IP 地址”。在网络中传输的任何一个合法数据包,其“源 IP 地址”必须是一个确切的、全球唯一的单播 IP 地址,绝对不能是 D 类多播地址。
  3. 🤫 杜绝触发 ICMP 差错报告:如果多播数据报在传输途中发生任何差错(如 TTL 减为0、找不到路由等),沿途路由器将其丢弃时绝对不产生、不回传 ICMP 差错报告报文。这是为了防止大量多播路由器同时向源端回传差错报文,从而在内网引发毁灭性的“ICMP 广播风暴”。

三、 维系 IP 多播运转的两大核心协议

要实现高效的全球级或局域网级多播,多播路由器必须同时运行并维护以下两种运行在网络层的辅助协议:

1. IGMP 协议(Internet Group Management Protocol,网际组管理协议)

  • 层级与标识:属于 网络层协议。在封装 IGMP 报文的 IP 数据报首部中,协议(Protocol)字段值 = 2。
  • 核心职责:专门用于管理和维护本地局域网(LAN)内部的主机与直接相邻的多播路由器之间的成员映射关系。
  • 通俗功能:管辖主机“如何加群”与“如何退群”。多播路由器通过周期性发射 IGMP 查询报文,获知本局域网内目前还有没有该“QQ群”的成员,从而决定要不要把外网接收到的该群的多播流放行导入到本地局域网内。

2. 多播路由选择协议(Multicast Routing Protocol)

  • 核心职责:专门用于在多播路由器与多播路由器之间进行拓扑和成员信息的交换。
  • 核心功能:管辖“多播数据包在外网大方向上该怎么走”。通过多播路由协议,全网路由器共同配合,建立一棵无环的、以源端为根或以共享点为中心的“多播转发树”,确保外网流量能够精准“多口转发”投递到每个有群成员的接口,同时避开没有群成员的空闲分支,防止流量泛滥。

移动 IP 技术(Mobile IP)

一、 移动 IP 技术要解决的核心问题

  • 传统 IP 的局限性:在传统 TCP/IP 网络中,主机的 IP 地址与其接入的物理子网(网络前缀)强绑定。一旦移动设备在跨区域移动时切换了子网,其 IP 地址就必须动态改变。
  • 带来的网络灾难:如果移动设备的 IP 地址突然改变,上层正在进行的 TCP 长连接(如正在传输大文件、视频通话)会瞬间发生断连并全部强制中断。
  • 移动 IP 的使命:确保移动节点(如手机、笔记本)在跨越不同子网漫游移动时,依然能够保持其原有的永久 IP 地址完全不变。从而让上层网络应用(如 TCP 会话)对物理位置的变化完全透明,实现无缝平滑过渡。

二、 移动 IP 的核心基础概念与物理实体

根据课件,移动 IP 体系内包含以下 6 个不可混淆的核心术语:

核心术语(中英文) 实体角色与具体含义
移动节点 / 移动站 (Mobile Node, MN) 具有永久 IP 地址、在各个不同物理子网之间跨区移动的主机(即你的手机或移动设备)。
永久地址 / 本地地址 (Home Address) 移动节点最初在老家获得的、全球唯一的、长久不变的 IP 地址。无论移动站漫游到天涯海角,上层通信始终只使用这个永久地址。
归属网络 / 本地网络 (Home Network) 移动节点最初所属的、与其永久地址网络前缀完全匹配的原始物理局域网。
归属代理 / 本地代理 (Home Agent, HA) 连接在归属网络上的核心路由器。当移动节点跑到外地时,归属代理在老家充当数据管家,负责代收发往该移动节点的所有流量。
被访网络 / 外地网络 (Foreign Network) 移动节点当前漫游、接入移入的非原始归属的陌生外地子网。
外地代理 (Foreign Agent, FA) 连接在当前被访网络上的路由器。负责接应并维系漫游过来的移动节点,并作为隧道的终点接收数据。
转交地址 (Care-of Address, COA) 移动节点漫游到外地网络时,被外部动态分配的临时 IP 地址。

⚠️ 工业界与考试三大硬性注意点(划重点)

  1. 转交地址的通信局限:转交地址纯粹是供移动站、归属代理及外地代理之间内部通信套娃使用的。外部的普通通信者(如通信者 B)根本不需要知道、也感知不到转交地址的存在。
  2. 转交地址的共享特性:一个临时转交地址可以被同一个外地网络下的多台移动站共同复用。外地代理(FA)在向本地派发时,可以直接根据各个移动站独一无二的 MAC 地址进行精准链路层投递。
  3. 网卡状态判定:移动站处于本地网络时,其通信模式与传统的普通单播通信完全一致。

三、 移动 IP 完整通信全流程(核心大题考点)

当移动站 A(永久 IP 地址为归属网络段)漫游离开老家,进入通信者 B 所在的或外部的外地网络进行通信时,完整历经以下四个阶段:

1. 漫游与登记阶段(MN 到达外地网络)

  • ① 登记暂住:移动站 A 接入外地网络后,首先向当前的外地代理(FA)进行登记,获得当前物理位置关联的临时转交地址(COA)。
  • ② 向老家报备:外地代理(FA)代为向 A 老家的归属代理(HA)进行登记报备,告知 HA:“你家 A 目前在我这,它的临时转交地址是 XX”。
  • ③ 隧道建立:老家的归属代理(HA)接收并批准登记,在 HA 与该转交地址之间成功建立一条通往外地的虚拟隧道(Tunnel)。

image-20260614171241778

2. 接收数据阶段:移动站在外地网络【接收】IP 分组(IP-in-IP 隧道套娃)

这是移动 IP 最具特色的“三角路由(Triangular Routing)”过程:

  • 步骤一(B 正常寄出):通信者 B 想要给移动站 A 发送数据。由于 B 仅知道 A 的永久地址,B 构造原始 IP 分组:
    • 【源 IP = B 的 IP 地址, 目的 IP = A 的永久地址】
  • 步骤二(HA 拦截并套娃):该数据包被常规路由派发到 A 的老家网络,随即被老家的守护管家——归属代理(HA)强行截获。HA 读取后发现 A 在外地,于是 HA 将整个原始 IP 分组作为数据载荷,在外面套上一个全新的外层 IP 首部(称为 IP-in-IP 隧道封装):
    • 【外层源 IP = 归属代理(HA)的 IP, 外层目的 IP = A 当前的临时转交地址(COA)】
  • 步骤三(隧道传输与拆封):这个套娃分组穿过广域网,沿着隧道高效率投递到终点——外地代理(FA)。外地代理将外层首部无情剥离(解封装),暴露出里面完整的原始 IP 分组。
  • 步骤四(落地交付):外地代理(FA)利用数据链路层封装,将原始 IP 分组直接精准投递到移动站 A 的网卡上。A 成功接收。

3. 发送数据阶段:移动站在外地网络【发送】IP 分组

  • 核心规则(绝不绕回老家!):身处外地的移动站 A 如果想要回复或向通信者 B 主动发射数据,其过程极其简单。A 直接在本地构造并输出最纯粹的原始 IP 分组:
    • 【源 IP = A 的永久地址(哪怕在外地,自报家门也必须使用老家永久IP), 目的 IP = B 的 IP 地址】
  • 直接路由:该数据包直接交给当前的外地代理(FA),外地代理将其直接推入普通公共 Internet。数据包不需要绕回老家的归属代理,而是沿着普通的网络最短路径,以最快速度直接单播送达通信者 B。

4. 回归故里阶段(MN 回到归属网络)

  • 登记注销:当移动站 A 漫游结束,物理上重新切回自己的老家(归属网络)时,会立即向自己的归属代理(HA)发起登记注销。
  • 通道恢复:HA 收到注销通知后,当即依法撤销之前的转交地址映射关系,并关闭虚拟传输隧道。至此,移动站 A 的所有通信行为完美且自动地恢复到最传统、最普普通通的经典单播 TCP/IP 通信模式。

传输层

核心概述

一、 传输层核心功能与基本概念

1. 实现“端到端”(进程到进程)的通信

  • 网络层 vs 传输层:
    • 网络层:实现“主机到主机”的通信。信息记录在 IP 数据报的首部,指明源 IP 地址和目的 IP 地址。
    • 传输层:实现“端到端”(进程到进程)的通信。信息记录在 TCP(或 UDP)报文段的首部,指明源端口和目的端口。传输层根据端口号来区分数据来自或去往哪一个特定的进程。

2. 复用与分用

  • 复用(从上到下):在发送数据的时候,同一台主机上的多个进程可以使用同一个传输层协议。
  • 分用(从下到上):在接收数据的时候,传输层可以根据端口号把数据正确交付到对应的目的进程。

3. 差错检测

  • TCP:检测出差错后丢弃数据,并通知发送方重传。
  • UDP:检测出错误后直接丢弃数据,但不通知发送方。

二、 端口与端口号

1. 端口的作用与独立性

  • 通过“端口号”标识本主机的一个特定进程。
  • 相互独立性:
    • 每台主机的端口号是相互独立的。
    • TCP 和 UDP 两种协议的端口号也是相互独立的。

2. 套接字(Socket)

  • 公式:

  • 作用:唯一地标识网络中的一台主机上的一个应用进程($\text{IP地址} + \text{端口号} \rightarrow$ 指向网络中一台主机上的一个特定的进程)。

  • 分类:分为 TCP 套接字 和 UDP 套接字。

  • 进程通信的三要素:当两个进程之间想要通信时,必须指明:

    1. 使用哪种传输层协议(TCP 或 UDP);
    2. 本进程绑定的端口号;
    3. 对方的 IP 地址和端口号。

QQ20260614-172937

3. 端口号的分类

  • 服务器使用的端口号:
    • 熟知端口号(0~1023):通常只能用于被熟知的重要应用程序(系统预留)。
    • 登记端口号(1024~49151)。
  • 客户端使用的端口号:
    • 短暂端口号(49152~65535):进程运行时动态分配,运行结束即释放。

三、 传输层提供的两种传输服务对比(TCP vs UDP)

传输层向应用层提供两种截然不同的端到端传输服务:

1. 有连接 vs 无连接

  • 面向连接的传输(如 TCP):
    • 机制:传输前先“打招呼”(建立连接),确认对方已经准备好接收数据;数据传输结束时,也要告知对方已结束(释放连接)。
    • 阶段:包含 建立连接 $\rightarrow$ 数据传输 $\rightarrow$ 释放连接 三个完整阶段。
  • 无连接的传输(如 UDP):
    • 机制:不打招呼,直接把数据传给对方(直接进入数据传输阶段)。

2. 可靠 vs 不可靠

  • 可靠的传输(如 TCP):
    • 机制:接收方使用“确认机制”(如收到数据后向发送方反馈“①收到、②收到”),让发送方明确知道哪些数据已被正确接收。
    • 优缺点:确保数据正确和完整,但系统开销大、实时性较差。
  • 不可靠的传输(如 UDP):
    • 机制:接收方无论有没有收到数据、数据是否正确,都不给发送方任何反馈。
    • 优缺点:数据可能会出错或丢失,但速度快、开销小。

UDP 协议

一、 UDP 与 TCP 核心特性深度对比

传输层的两大核心协议在报文处理、连接机制和传输可靠性上有着本质的区别:

特性维度 UDP 协议 TCP 协议
工作机制 面向报文:不拆分、不重装。应用层交下来的报文,UDP 仅添加首部后就向下交付。 面向字节流:支持报文的自动拆分与重装,可以传输极长的应用层报文。
首部开销 首部很小,固定为 8 字节 (8B)。 首部较大,占 20~60 字节 (20~60B)。
连接状态 无连接。不需要提前建立连接,直接发送数据。 面向连接。通信双方在传输层必须先通过“三次握手”建立连接。
可靠性 不可靠。不保证交付,不提供拥塞控制(可靠性可由应用层自行实现)。 可靠。通过确认、重传及拥塞控制机制确保数据无差错、不丢失、按序到达。
通信支持 支持一对一、一对多(支持单播、广播和多播/组播)。 仅支持一对一的单播传输。

⚠️ UDP 核心注意点:由于 UDP 不支持报文的自动拆分与重装,因此应用层交付的报文长度绝对不能超过 UDP 协议的上限,否则会在网络层引发复杂的 IP 分片。

QQ20260614-194836

二、 UDP 用户数据报格式

UDP 用户数据报由 首部(Fixed 8 Bytes) 和 数据部分 组成。

1. 首部字段构成(共 8B,分为 4 个字段,每个字段占 2B / 16位):

  1. 16位源端口号(Source Port):发送方进程的端口。如果在某些场景下不需要对方回复,可直接置为全 0。
  2. 16位目的端口号(Destination Port):接收方进程的端口,此项为必填。
  3. 16位UDP长度(Length):指明整个 UDP 用户数据报的长度(包含首部 + 数据部分),单位为字节(Byte)。其理论最大值为 $65535\text{ B}$。
  4. 16位UDP检验和(Checksum):由发送方传输层计算并填入,接收方用来检测传输过程中是否发生比特差错。如果不需要检验,可直接填入全 0。

2. 数据部分(Data):

  • 即应用层交付下来的一个完整的应用层报文。

QQ20260614-195625

三、 UDP 数据封装示例分析

以图片中发送一个 $96\text{ B}$ 的应用层报文为例,其在各层的封装与字段变化如下:

  1. 应用层:生成原始报文,长度为 $96\text{ B}$。
  2. 传输层(UDP):添加固定 $8\text{ B}$ 的 UDP 首部,封装成 UDP 数据报。
    • 此时,首部中的 UDP 长度字段 = 8B (首部) + 96B (数据) = 104。
  3. 网络层(IP):将 104B 的 UDP 数据报作为数据部分,前方加上 $20\sim60\text{ B}$ 的 IPv4 首部,封装成 IP 数据报 向外发送。

四、 UDP 差错检验机制:二进制反码求和(Checksum)

为了提高检测的准确性,UDP 引入了一种特有的检验方法,在计算检验和时引入了“伪首部”。

1. 什么是伪首部(Pseudo Header)?

  • 目的:仅用于计算检验和,既不向下交付给网络层,也不向上递交给应用层。计算前临时添加,计算完后立即去掉。
  • 大小与结构(共 12 字节):
    • 源 IP 地址(4 字节)
    • 目的 IP 地址(4 字节)
    • 固定 0(1 字节)
    • 协议类型(1 字节,UDP 的协议号固定为 17)
    • UDP 长度(2 字节,与 UDP 首部中的长度值完全相同)

2. 差错检验核心算法(二进制反码求和 + 回卷)

  • 基本原理:将需要检验的数据以 16位(2字节)为一组 进行二进制加法。
  • 最高位进位回卷:若最高位(第16位)相加后产生了进位,必须将该进位“回卷”加到最低位。

QQ20260614-200707

3. 发送方与接收方的完整处理步骤

🔹 发送方的传输层逻辑:

  1. 添加伪首部:在 UDP 用户数据报前端临时拼接 12B 的伪首部。
  2. 分组加法:把【伪首部 + UDP首部 + 数据部分】整体以 16位 为一组,进行二进制加法。(注:若数据部分字节数为奇数,需在末尾补一个全0字节,该字节同样不发送)。此时首部中的“检验和”字段先填入全 0。
  3. 求反码:将最终的加法结果逐位取反,得到 16 位的“检验和”,将其填入 UDP 首部的检验和字段。
  4. 去掉伪首部:删除伪首部,将标准的 UDP 数据报交付给网络层。

QQ20260614-201049

🔹 接收方的传输层逻辑:

  1. 接收并补齐:收到网络层递交的 UDP 数据报后,在前端添加完全相同的伪首部。
  2. 全员求和:把【伪首部 + 含有检验和的UDP首部 + 数据部分】同样以 16位 为一组,进行二进制加法(同样支持最高位进位回卷)。
  3. 结果判定:
    • 如果没有发生比特错误:最终的二进制加法结果必然为全 1(即 1111 1111 1111 1111)。接收方顺利接收该报文,并根据目的端口号交付给对应应用层进程。
    • 如果结果不是全 1:说明传输过程中发生差错,直接丢弃该 UDP 数据报。

TCP 协议核心特性与首部格式

一、 TCP 连接生命周期管理(概述)

TCP 是面向连接的协议,其生命周期包含:建立连接(3次握手) $\rightarrow$ 数据传输(全双工) $\rightarrow$ 释放连接(4次挥手)。

QQ20260614-202431

1. 建立连接(3次握手)

  • 角色定义:客户端是主动发起连接的一方;服务器是被动等待连接的一方。
  • 握手过程:通过 3次握手 建立连接,在网络中对应 3个 TCP 报文段。
  • 通信状态:一旦连接建立成功,进程 A 和 B 即可进入全双工通信(双向传输 TCP 报文段)。

2. 释放连接(2 + 2 = 4次挥手)

由于 TCP 是全双工的,断开连接时需要双向分别释放:

  • 挥手 ① & ②:进程 A 没有数据要发送了,发起 2 次挥手断开 A $\rightarrow$ B 的单向连接。
  • 半关闭状态:此时连接进入单工通信状态,进程 A 只能接收数据,只有进程 B 可以继续给进程 A 发送数据。
  • 挥手 ③ & ④:进程 B 也没有数据要发送后,发起后 2 次挥手断开 B $\rightarrow$ A 的单向连接。至此结束通信。

二、 TCP 面向字节流(Byte-Stream Oriented)的特性

QQ20260614-203425

1. 核心概念

  • 传输机制:无论应用层一次交付多少个、多大的报文,在 TCP 看来都是一连串无结构的字节流。
  • 单次连接传多个报文:建立一次 TCP 连接可以双向传输多个应用层报文。
  • 报文的拆分与重组:TCP 会根据对方协商的 MSS(最大段长) 自动拆分和打包数据。

2. 字节流拼接与拆分示例

假设进程 1 要连续发送报文 Y(1600B)和报文 Z(1800B),双方协商的 $\text{MSS} = 1000\text{ B}$:

  • TCP 并不强制要求每个 TCP 段都满载,只要不超过 MSS 即可。
  • 打包结果:
    • TCP 段 1:TCP 首部 + 报文 Y 的前 $1000\text{ B}$
    • TCP 段 2:TCP 首部 + 报文 Y 剩余的 $600\text{ B}$ + 报文 Z 的前 $400\text{ B}$ (不同应用层报文的数据可以在同一个 TCP 段中拼装)
    • TCP 段 3:TCP 首部 + 报文 Z 的中间 $1000\text{ B}$
    • TCP 段 4:TCP 首部 + 报文 Z 剩余的 $400\text{ B}$
  • 接收端处理:这 4 个 TCP 段在网络中可能会乱序到达,但接收方的 TCP 协议栈会自动按序号重新组装排序,最终按序递交给应用层进程 2。

三、 TCP 报文段首部格式详解

TCP 报文段由 TCP 首部 和 TCP 数据部分 组成。其固定首部为 20 字节 (20B),通过选项字段可扩展,最长可达 60 字节 (60B)。

QQ20260615-084153

1. 端口号(各占 2B / 16位)

  • 源端口 和 目的端口:标识发送方和接收方的应用层进程。

2. 序号与确认号(各占 4B / 32位)—— 拆包与重组的核心

  • 序号(seq):用于标记本报文段所发送的数据部分第一个字节在原始字节流中的位置。
    • 注意:起始序号是由发送方随机设置的,不一定从 0 开始。
  • 确认号(ack 或 ack_seq):用于期望收到对方下一个报文段的第一个数据字节的序号。表示在该确认号之前的所有字节都已正确收到。

3. 数据偏移与保留字段

  • 数据偏移(4位):实际上表示 TCP 首部的长度。
    • 计算单位:以 $4\text{ B}$ 为单位。其 4 位二进制取值范围是 $0\sim15$。
    • 最大长度:$15 \times 4\text{ B} = 60\text{ B}$(扣除 20B 固定首部,选项部分最多 40B)。
    • 注意:TCP 首部中不会专门记录 TCP 数据部分的长度,该长度是通过 IP 首部中的总长度信息计算出来的。
  • 保留(6位):目前暂未使用,通常全部置为 0。

4. 核心标志位(6位,每位占 1 bit)

  • ACK:确认有效位。ACK=1 时确认号(ack_seq)才有效;ACK=0 时确认号无效。
    • 注意:在 TCP 全生命周期中,只有连接建立的“握手①”报文段 ACK=0,其余所有 TCP 报文段的 ACK 必须置为 1。
  • SYN:同步位。SYN=1 表示这是一个连接请求或连接接受报文。
    • 注意:只有“握手①”和“握手②”的 SYN=1,其他传输阶段的 SYN 均为 0。
  • FIN:终止位。FIN=1 表示此报文段发送方的数据已发送完毕,要求释放传输连接。
    • 注意:只有挥手阶段的“挥手①”和“挥手③”的 FIN=1。
  • RST:复位位。RST=1 时表示 TCP 连接中出现严重差错(如主机崩溃),必须强制释放连接重新建立。也可用于拒绝非法报文段或黑客恶意攻击。
  • URG:紧急位。URG=1 时紧急指针字段有效,告诉系统此报文段中有紧急数据,应插队优先发送。
  • PSH:推送位。PSH=1 时要求接收方尽快将数据交付给应用层进程,而不要等到缓存填满再向上交付(常用于交互式通信)。

5. 窗口与功能字段

  • 窗口(16位,简记为 rwnd 或 rcvwnd):表示接收方的接收窗口大小。
    • 含义:指明从本报文段首部中的 ack_seq 算起,接收方目前还能接收多少个字节的数据。
    • 作用:是实现流量控制的关键字段。
  • 紧急指针(16位):当 $\text{URG}=1$ 时有效,指出本报文段中紧急数据的具体字节数(紧急数据排在数据部分的最前面)。
  • 检验和(16位):
    • 原理与 UDP 雷同,计算前同样需要临时添加 12B 的伪首部。
    • 区别:只需将伪首部中的协议字段由 17(UDP)改为 6(TCP),并将原来的 UDP 长度字段改为 TCP 长度。

6. 选项(选项长度可变,占 0~40B)

  • 填充:为了确保整个 TCP 首部是 $4\text{ B}$ 的整数倍,若选项长度不足,后面需加全 0 字节填充凑足。
  • MSS(最大段长)协商:在建立连接时(即 握手①、握手② 的选项中),双方会协商接下来的传输中一个 TCP 报文段最多能携带的数据字节数(不包括首部)。通常 MSS 不会设置过大,以避免在 IP 层被分片。

TCP 三次握手与四次挥手

一、 核心控制位(Flags)的绝对规律

在 TCP 全生命周期的报文段中,标志位满足以下“只有……”的确定性排他规律(选择题高频考点):

  • ACK(确认位):只有握手①的 ACK = 0,其余全生命周期的所有 TCP 报文段均为 ACK = 1。
  • SYN(同步位):只有握手①和握手②的 SYN = 1,其余所有 TCP 报文段均为 SYN = 0。
  • FIN(终止位):只有挥手①和挥手③的 FIN = 1,其余所有 TCP 报文段均为 FIN = 0。

二、 TCP 三次握手核心机制

QQ20260615-091440

1. 详细交互过程与序号变化示例

假设客户端初始序号 $\text{seq} = 666$,服务器初始序号 $\text{seq} = 50$:

  • 握手 ①(Client $\rightarrow$ Server):
    • SYN=1, ACK=0, FIN=0,$\text{seq} = 666$。此时确认号 ack 无效。
  • 握手 ②(Server $\rightarrow$ Client):
    • SYN=1, ACK=1, FIN=0,$\text{seq} = 50$,$\text{ack} = 667$(即 $666 + 1$)。
  • 握手 ③(Client $\rightarrow$ Server):
    • SYN=0, ACK=1, FIN=0,$\text{seq} = 667$,$\text{ack} = 51$(即 $50 + 1$)。

2. 🚨 核心考点:携带数据与消耗序号规则

QQ20260615-092120

  1. 握手 ① 和 握手 ②:绝对不能携带数据(只包含 TCP 首部,数据部分为 0B)。但是,它们固定消耗 1 个序号。因此下一阶段的 ack 必须在收到的 seq 基础上 $+1$。
  2. 握手 ③:可以携带数据,也可以不携带数据。
    • 如果不携带数据:不消耗序号。下一个由客户端发出的 TCP 报文段的序号仍然是 $\text{seq} = 667$。
    • 如果携带数据(例如携带了 100B 数据):下一个由客户端发出的 TCP 报文段的序号将变为 $\text{seq} = 667 + 100 = 767$。

3. 握手阶段耗时分析

  • 从客户端发出握手①,到客户端进程可以发送数据,至少需要多远?
    • 答:$1\text{ RTT}$。因为客户端发出握手①,经过 $0.5\text{ RTT}$ 到达服务器,服务器立刻发出握手②,再经过 $0.5\text{ RTT}$ 到达客户端。此时客户端进入 ESTABLISHED 状态,数据可以伴随握手③直接发出。
  • 从客户端发出握手①,到服务器进程可以发送数据,至少需要多远?
    • 答:$1.5\text{ RTT}$。因为必须等待客户端的握手③到达服务器,服务器才能进入 ESTABLISHED 状态并发送数据。

三、 TCP 四次挥手核心机制

QQ20260615-095338

1. 详细交互过程与序号变化示例

假设主动关闭方(客户端)发起挥手时 $\text{seq} = 9999$,服务器收到后发送完 2200B 的单向数据:

  • 挥手 ①(Client $\rightarrow$ Server,主动关闭):
    • SYN=0, ACK=1, FIN=1,$\text{seq} = 9999$,$\text{ack} = 5600$。(消耗 1 个序号)
  • 挥手 ②(Server $\rightarrow$ Client,被动关闭确认):
    • SYN=0, ACK=1, FIN=0,$\text{seq} = 5600$,$\text{ack} = 10000$(即 $9999 + 1$)。(此后服务器可以继续单向传送剩余数据,假设传了 2200B)
  • 挥手 ③(Server $\rightarrow$ Client,被动方发送完毕准备关闭):
    • SYN=0, ACK=1, FIN=1,$\text{seq} = 7800$(即 $5600 + 2200$),$\text{ack} = 10000$。(消耗 1 个序号)
  • 挥手 ④(Client $\rightarrow$ Server,最后确认):
    • SYN=0, ACK=1, FIN=0,$\text{seq} = 10000$,$\text{ack} = 7801$(即 $7800 + 1$)。

2. 🚨 核心考点:挥手数据与序号规则

  • 挥手 ① 和 挥手 ③:即使不携带数据,也必须固定消耗 1 个序号(因为 FIN=1)。
  • 挥手 ②:可以携带数据。
  • 挥手 ④:绝对不可以携带数据。

四、 状态机转换与 TIME-WAIT 机制

1. 三次握手状态流转

QQ20260615-092120

  • 客户端:CLOSED $\rightarrow$ 发出① $\rightarrow$ SYN-SENT $\rightarrow$ 收到②并发出③ $\rightarrow$ ESTABLISHED
  • 服务器:CLOSED $\rightarrow$ 打开监听 $\rightarrow$ LISTEN $\rightarrow$ 收到①并发出② $\rightarrow$ SYN-RCVD $\rightarrow$ 收到③ $\rightarrow$ ESTABLISHED

2. 四次挥手状态流转(重点、难点)

QQ20260615-095534

  • 主动关闭方(Client):ESTABLISHED $\rightarrow$ 发出① $\rightarrow$ FIN-WAIT-1 $\rightarrow$ 收到② $\rightarrow$ FIN-WAIT-2 $\rightarrow$ 收到③并发出④ $\rightarrow$ TIME-WAIT $\rightarrow$ 倒计时 2MSL $\rightarrow$ CLOSED
  • 被动关闭方(Server):ESTABLISHED $\rightarrow$ 收到①并发出② $\rightarrow$ CLOSE-WAIT (关闭等待) $\rightarrow$ 发送完数据并发出③ $\rightarrow$ LAST-ACK $\rightarrow$ 收到④ $\rightarrow$ CLOSED

3. 💡 2MSL 与 TIME-WAIT 深度解析

  • MSL(Maximum Segment Lifetime,最大报文段寿命):由 TCP 协议规定的固定时间长度(如示例中 $1\text{ MSL} = 800\text{ms}$)。
  • 进入 TIME-WAIT 后的重置定时器规则:客户端收到挥手③并发出挥手④后,立即启动 TIME-WAIT 计时器,开始倒计时 $2\text{MSL}$。如果在此期间,客户端重新收到了对方重传的挥手③(意味着客户端发出的挥手④丢失了,服务器在超时后重传了挥手③),客户端会重新发送挥手④并直接“重置”整个 2MSL 计时器。

五、 释放连接阶段耗时分析(大题常考)

  • 从客户端发出挥手①,到客户端进程彻底进入 CLOSED 状态,至少需要多远?
    • 答:$1\text{ RTT} + 2\text{MSL}$。
    • 解析:客户端发出① ($0.5\text{ RTT}$) $\rightarrow$ 服务器收到并回复② ($0.5\text{ RTT}$) $\rightarrow$ 客户端收到②。在这个时间点之后,还要等待服务器传输完数据并发出挥手③,客户端收到③后发出挥手④并进入 TIME-WAIT,最后死等 $2\text{MSL}$。在图示的最短理想情况下(服务器收到①后如果没有数据,紧接着就发③),从发出①到收到③正好经过 $1\text{ RTT}$,随后再经历 $2\text{MSL}$ 的等待。
  • 从客户端发出挥手①,到服务器进程彻底进入 CLOSED 状态,至少需要多远?
    • 答:$1.5\text{ RTT}$。
    • 解析:客户端发出① ($0.5\text{ RTT}$) $\rightarrow$ 服务器收到并回复② ($0.5\text{ RTT}$) $\rightarrow$ 客户端收到②并等待服务器发出③ ($0.5\text{ RTT}$) $\rightarrow$ 客户端发出④并到达服务器 ($0.5\text{ RTT}$)。在最快情况下,服务器收到④后立刻转入 CLOSED。

📝 特殊合并情况(考试简答题重点):

如果服务器进程在收到客户端的挥手①时,已经没有要发送给客户端的数据了,那么服务器可以将挥手②(ACK)和挥手③(FIN)合并为同一个报文段连续发出。此时,服务器的 CLOSE-WAIT 状态以及客户端的 FIN-WAIT-2 状态的持续时间都会极短,甚至接近于 0。

QQ20260615-100223

TCP超时重传与可靠传输

QQ20260615-125845

一、 TCP 多连接与端口复用机制

1. 单端口支持多连接

  • 核心结论:一个服务器端口(如 996)可以同时支持多个不同的 TCP 连接。

  • 连接的唯一标识:操作系统内核通过四元组来唯一确定一个 TCP 连接:

  • 底层实现:如图所示,服务器进程绑定端口 996,但当客户端 1(166.1.1.1:1111)和客户端 2(166.2.2.2:2222)分别发起连接时,操作系统内核会在传输层为它们分别创建独立的 Socket(套接字)对象(对象 1 和对象 2)。

2. 内存缓冲区分配

  • 每个 Socket 对象都拥有自己独立的发送缓冲区和接收缓冲区。
  • 这些缓冲区是在建立连接时由操作系统内核动态分配的,大小不固定,可以根据网络状况和系统预留进行动态扩容。

二、 TCP 传输底层原理:双端缓冲区与滑动窗口

TCP 报文段中的 seq(序号)、ack(确认号)以及 rwnd(接收窗口)是控制双端缓冲区读写的核心指针。

1. 握手阶段的参数初始化(以图 1、图 2 为例)

  • 握手 ①:客户端向服务器发送 seq=599(设置客户端进程初始序号),无有效 ack,携带有客户端的发送意愿。
  • 握手 ②:服务器为该连接分配接收缓冲区(假设大小为 8B),并在报文段中设置 seq=199(服务器初始序号),ack=600(期待收到客户端第 600 字节),并通告 rwnd=8。
    • 关键联动:客户端收到握手 ② 后,根据服务器通告的 rwnd=8,将其发送窗口大小限制为 8。
  • 握手 ③:客户端反馈 seq=600, ack=200,并通过 rwnd=10 告知服务器自己的接收缓冲区剩余大小为 10B。

QQ20260615-105137

QQ20260615-104104

2. 数据动态传输与窗口滑动(以图 4、图 7、图 8 为例)

  • 发送方的发送窗口:由接收方通告的 rwnd 决定。发送窗口内的字节允许直接发送,无需等待确认。
  • 数据打包发送:
    • 客户端的发送缓冲区中存放着待发送数据。若当前发送窗口为 5(图 4 场景),它连续发送了两个报文段:
      • 报文段 1:seq=603,携带 2B 数据(对应缓冲区中编号为 3、4 的字节)。
      • 报文段 2:seq=605,携带 2B 数据(对应缓冲区中编号为 5、6 的字节)。
    • 此时服务器成功接收到 3, 4, 5, 6 字节并存入接收缓冲区,缓冲区中下一个期待接收的空位是 607。

image-20260615125557419

三、 TCP 高效确认机制:累积确认与捎带确认

为了减少网络中纯控制报文占用的带宽,TCP 引入了以下优化确认机制:

1. 累积确认(Cumulative ACK)

  • 机制:接收方收到多个连续的有效报文段后,不需要逐个回复 ACK,而是只返回一个总的 ACK 报文段。
  • 图示解析(图 5):
    • 客户端连续发出 seq=603 (2B) 和 seq=605 (2B)。
    • 服务器一并收到后,只返回了一个确认段:ack=607, rwnd=1。这代表 607 之前的所有数据(3, 4, 5, 6)都已正确接收。

image-20260615125620943

2. 捎带确认(Piggybacking)

  • 机制:当接收方收到数据需要返回确认,且刚好自己也有数据要发送给对方时,将确认信息(ack)放入该数据报文段的首部中一同发送,从而将两个报文合并为一个。
  • 图示解析(图 6、7、8):
    • 服务器收到客户端发送的 seq=608 (2B) 数据。
    • 此时服务器应用层刚好有 5B 数据要发回客户端。
    • 服务器不再单独发送 ACK 段,而是直接发送数据报文段:seq=200, ack=610, rwnd=6, 数据5B。这里的 ack=610 就是捎带确认。
    • 同理,客户端收到后,也在发出的 6B 数据中捎带了 ack=205 的确认信息。

image-20260615125634074

image-20260615125724568

四、 TCP 可靠传输机制:超时重传详解

TCP 每发送一个报文段,都会为其设置一个超时重传计时器(Retransmission Timer)。如果在规定时间内未收到确认,就会触发重传。考试中主要考察以下两种丢失场景:

场景 1:发送方发出的“数据报文段”丢失(图 9)

  1. 丢失:客户端发出 seq=618, 数据2B,但在网络中丢失。
  2. 触发:客户端的超时重传计时器到期,仍未收到来自服务器的 ACK。
  3. 动作:重传并重置计时器。客户端重新发出完全相同的报文段 seq=618, 数据2B。
  4. 完成:服务器这次顺利收到,并返回最新确认号 ack=620。

image-20260615125656800

场景 2:接收方返回的“ACK报文段”丢失(图 10)

  1. 丢失:客户端发出 seq=620, 数据2B 顺利到达服务器。服务器正常回复 ack=622,但该 ACK 段在网络中丢失。
  2. 触发:对客户端而言,它并不知道服务器已经收到。客户端的计时器到期后,判定报文可能丢失。
  3. 重传与去重:
    • 客户端重新发送 seq=620, 数据2B。
    • 服务器再次收到该段数据,对比内核指针后发现是重复数据,于是传输层直接丢弃该重复数据,以确保应用层拿到的字节流不重复。
  4. 必须二次确认:服务器必须再次返回 ACK 段(ack=622),通知客户端“我早就收到了,请不要再发了”。客户端收到后关闭计时器,重传成功结束。

image-20260615125759698

TCP拥塞控制

一、 滑动窗口与流量控制(Flow Control)

QQ20260615-125046

流量控制是端到端的通信控制,主要通过双端的滑动窗口和缓冲区协同实现:

  • 接收方(Receiver):
    • 维持一个接收窗口(rwnd 或 rcvwnd)。
    • 限制条件:接收窗口大小不能大于接收缓冲区的大小。
    • 物理意义:接收窗口像一个“框柱”的范围,框住的是接收方当前还允许接收的序号范围。
  • 发送方(Sender):
    • 维持一个发送窗口。
    • 双重限制条件(🚨 填空/选择题常考):
      1. 发送窗口不能大于发送缓冲区大小。
      2. 发送窗口不能大于接收方通告的接收窗口(rwnd)。
    • 物理意义:发送窗口“框住”的是发送缓冲区中已发送但尚未收到确认的数据,以及可以发送但尚未发送的数据。

二、 TCP 可靠传输机制之重传演进

可靠传输由确认机制与重传机制共同支撑。

1. 确认机制的优化:推迟确认(Delayed ACK)

  • 累积确认规则:若收到 ack_seq = n,说明序号在 $n$ 之前的所有字节均已正确接收。
  • 返回 ACK 的时机(推迟确认):
    • 为了减少网络开销,TCP 允许推迟发送确认,但推迟时间最多不能超过 0.5 秒(TCP 标准规定)。
    • 两个打破推迟、必须立刻返回 ACK 的场景:
      1. 捎带确认:如果自己刚好也有数据要发送给对方,必须立即返回 ACK 段并“捎带”数据。
      2. 满两个 MSS:若连续收到两个长度为 MSS 的报文段,必须立即返回 ACK 段。

image-20260615153355505

2. 🚨 超时重传 vs 快重传(重难点、考试大题高频陷阱)

当传输过程中出现丢包(例如客户端连续发出 4 个报文段,第 2 个丢失)时,TCP 有两种重传应对策略:

🔹 策略 A:传统的超时重传

  • 现象:服务器收到第 1 个包,返回确认。因为第 2 个包丢了,服务器收到第 3、4 个包时属于失序报文段,触发服务器推迟确认延迟。
  • 缺点:客户端的发送窗口很快被填满并卡住。客户端必须死等超时重传计时器到期,才开始连续重发后面的报文段,导致传输效率低下。

🔹 策略 B:快重传(Fast Retransmit)与立即确认机制

  • 配套机制(立即确认):接收方每收到一个 TCP 报文段就立即返回一个 ACK。即使接收到的是一个失序(乱序)的报文段,也要立即返回当前期望的 ACK 段。这种失序触发的即时确认,直接导致了“冗余 ACK”的产生。
  • 快重传触发机制:当发送方连续收到 3 个完全相同的冗余 ACK(即总共收到 4 个相同的 ACK)时,判定该确认号对应的报文段已丢失,立刻重传该报文段,而不需要等待超时计时器到期。

🚨 关于“3个冗余ACK”的真题概念陷阱:

参考图片 QQ20260615-130654.jpg 中的手写批注:

当丢包发生后,服务器发回的第 1 个正常确认(比如 ack=623)不属于冗余 ACK,它只是基准确认。

随后由于客户端继续发送后续报文,服务器又触发返回了 3 次一模一样的 ack=623。这后 3 个才叫“冗余(Duplicate)ACK”。加上第 1 个,发送方一共会连续收到 4 个相同的 ACK。

image-20260615153415039

三、 流量控制 vs 拥塞控制(Congestion Control)

维度 流量控制(Flow Control) 拥塞控制(Congestion Control)
控制范围 局部(端到端) 全局(涉及整个网络中的所有主机、路由器)
核心目的 控制接收端的数据接收量,防止接收端缓冲区溢出。 减小整个网络中路由器的负载,防止网络发生崩溃。
控制窗口 接收窗口(rwnd):由接收方根据自身缓冲区大小通告。 拥塞窗口(cwnd):由发送方根据当前网络拥塞程度估算。

💡 发送窗口的终极决定公式

在实际的网络传输中,发送方的发送窗口上限值由双重因素共同决定,取两者的极小值:

📊 如何通过 ACK 状态判断网络拥塞程度?

  • 顺利收到每一个 ACK $\rightarrow$ 网络网络不拥塞。
  • 收到冗余 ACK 并触发快重传 $\rightarrow$ 网络有点拥塞 $\rightarrow$ 适当缩小拥塞窗口。
  • 引发超时重传(Timer 到期) $\rightarrow$ 网络严重拥塞 $\rightarrow$ 迅速收缩拥塞窗口。

四、 拥塞控制经典算法:慢开始与拥塞避免

QQ20260615-140621

发送方通过调整拥塞窗口 来动态适应网络。这里引入一个分界线阈值:慢开始门限(ssthresh)。

1. 慢开始算法(Slow Start)

  • 适用条件:当 时。
  • 增长规律:初始 (单位为 MSS)。每收到一个 ACK,让 。
  • 宏观表现:在每个传输轮次(RTT时间)里,如果没有丢包,拥塞窗口 会呈现指数规律增长($1 \rightarrow 2 \rightarrow 4 \rightarrow 8 \rightarrow 16 \dots$)。

2. 拥塞避免算法(Congestion Avoidance)

  • 适用条件:当 时。
  • 增长规律:在一个 RTT(传输轮次)内,即使收到多个 ACK,也只能让 。
  • 宏观表现:窗口呈现线性规律增长(“加法增大”),使窗口平缓上升,探测网络极限。

五、 💥 发生严重拥塞(超时重传)后的收缩逻辑(大题必考计算)

QQ20260615-141058

QQ20260615-141415

根据图片 QQ20260615-140621.jpg 和 141415.jpg 的标准图像轨迹,当网络发生严重拥塞并触发超时重传时,算法执行“乘法减小”收缩:

  1. 重置慢开始门限(ssthresh):

    (注意:ssthresh 的值通常不能小于 2)。

  2. 断崖式下跌:

    将拥塞窗口 强行跌回 1。

  3. 重新开始:

    再次进入慢开始算法,从 开始指数增长,直到到达新设定的 分界线后,再次转为拥塞避免算法(线性增长)。

📉 图像实例解析(以图 5 为例):

  • 初始 $ssthresh = 16$, 从 1 开始慢开始指数增长。
  • 在第 4 个轮次时, 刚好到达分界线 16,转为拥塞避免(线性增加:$16 \rightarrow 17 \rightarrow 18 \dots$)。
  • 当 线性增长到 24 时,网络发生超时重传(说明严重拥塞)。
  • 调整策略:新 $ssthresh = 24 / 2 = 12$;同时 跌落回 1。
  • 接下来的轮次: 从 1 指数增长到 12(慢开始),到达 12 后再次改为线性步进增长(拥塞避免)。

TCP 快重传与快恢复算法

一、 快重传与快恢复的触发背景

  • 拥塞程度判定:
    • 引发超时重传 $\rightarrow$ 网络发生严重拥塞。
    • 收到 3 个冗余 ACK(触发快重传) $\rightarrow$ 报文段虽然丢失,但后续报文仍能顺利到达接收方并触发回传确认,说明网络只是“有点拥塞”。
  • 优化思想:既然网络没有发生全面瘫痪,发送窗口就不需要像发生超时那样“断崖式跌回 1”。可以通过快恢复算法使网络吞吐量维持在一个较高水平。

二 快恢复算法(Fast Recovery)核心窗口调整逻辑

QQ20260615-154002

当发送方连续收到 3 个完全相同的冗余 ACK 时,触发快重传,紧接着进入快恢复阶段(以当前主流的 TCP Reno 版本为准):

  1. 慢开始门限($ssthresh$)减半:

    (注:在真题中,若除不尽通常向下取整,且 $ssthresh$ 的值不能小于 2)

  2. 拥塞窗口($cwnd$)同步减半:

    直接将新拥塞窗口设为改变后的门限值:

  3. 直接进入拥塞避免:

    跳过“从 1 开始的慢开始阶段”,直接从减半后的 $cwnd$ 开始执行拥塞避免算法(即按传输轮次 RTT 进行“加法增大”线性增长)。

三、 经典对比:TCP Reno 版本 vs TCP Tahoe 版本

在考试大题中,必须看清题目要求的是老旧的 Tahoe 版本还是现代的 Reno 版本,它们在收到 3 个冗余 ACK 后的表现截然不同:

  • TCP Tahoe 版本(已废弃不用):
    • 一刀切策略:无论是收到 3 个冗余 ACK 还是超时重传,全部一律将 $ssthresh$ 减半,并将 $cwnd$ 强行降回 1,重新执行慢开始。
  • TCP Reno 版本(当前标准):
    • 分情况区别对待:
      • 遇到超时重传:$ssthresh$ 减半,$cwnd$ 降回 1,走慢开始。
      • 遇到3个冗余 ACK:执行快恢复,$ssthresh$ 和 $cwnd$ 同时减半,直接走拥塞避免(宏观图像上表现为一个“V字形半折返”,而不是跌落到底部)。

四、 💥 实例拆解分析(基于 RTT 传输轨迹大题思维)

QQ20260615-154035

结合图片 QQ20260615-154035.jpg 的真实传输轨迹,我们来拆解动态计算过程:

1. 拥塞发生点(第 12 轮次)

  • 状态:此时经过加法增大后,拥塞窗口增大到了 $cwnd = 24$。
  • 事件:由于网络出现轻度丢包,发送方在这个轮次内触发并集齐了 3 个冗余 ACK。
  • 触发动作(快重传 + 快恢复):
    • 立即重传丢失的那个报文段。
    • 更新门限:$ssthresh = 24 / 2 = 12$。
    • 更新窗口:$cwnd = ssthresh = 12$。

2. 快恢复平稳过渡(第 13 轮次)

  • 状态:进入第 13 个 RTT,此时 $ssthresh = 12$,$cwnd = 12$。
  • 传输行为:由于 $cwnd=12$,发送方在这个 RTT 内连续发送 12 个报文段,并顺利连回了 12 个确认 ACK。
  • 算法适用:因为 $cwnd \ge ssthresh$(此处刚好相等),从这一轮次开始,正式切换并执行拥塞避免算法。

3. 进入线性增长(第 14、15 轮次)

  • 第 14 RTT:根据拥塞避免的“加法增大”规律,一个完整的传输轮次(RTT)结束后,窗口只能 $+1$。

  • 第 15 RTT:同样保持线性步进。

📝 总结大题得分技巧:

做计算题时,先通过“超时”还是“3个冗余ACK”判定是断崖下跌($cwnd=1$)\还是*高位减半($cwnd=ssthresh=原cwnd/2$)*。如果是后者,在改变后的第一个 RTT 里窗口大小不变(用于发送并收回减半后的满载数据包),从下一个 RTT 开始,保持每轮次 $+1$ 稳步线性上扬即可。

应用层

DNS(域名系统)

一、 DNS 的核心概念与基本特征

1. 域名与 IP 地址的关系

  • 域名(Domain Name):给互联网上的主机/路由器取的一个层次结构的名字(民间俗称的“网址”),便于人类记忆(如 www.cskaoyan.com)。
  • IP 地址:便于计算机处理的数字化标识(如 111.124.194.228)。
  • DNS 的作用:作为分布式数据库,将便于人类记忆的域名转换成便于计算机处理的 IP 地址。

2. DNS 系统设计:集中式 vs 分布式

  • 集中式系统(Centralized System):所有数据和服务集中在少数服务器上。
    • 若 DNS 采用集中式:会导致全球计算机访问过载、单点故障导致整个网络瘫痪。
  • 分布式系统(Distributed System):数据和服务分散在多台服务器上,协同工作。DNS 采用此架构以实现高可用和去中心化。

3. DNS 的工作特性

  • 工作模式:采用 C/S(客户端/服务器) 工作方式。
    • 注意:DNS 客户端和服务器的角色是相对的。一台 DNS 服务器在向上级查询时,会充当客户端。
  • 协议与端口:属于应用层协议,在传输层通常基于 UDP 协议进行封装,使用固定端口 53。

二、 域名的层次结构与顶级域名(TLD)分类

为了便于级别授权与管理,域名被设计为树状的层次结构。

1. 层次划分(从右往左看)

  • 根域名(.):在顶级域名之上,被记作一个“点”,通常省略不写。由 ICANN 统一管理和划分。
  • 顶级域名(TLD):如 cn(国家)、com(公司)。
  • 二级域名:如 edu.cn(教育网)、cskaoyan.com。
  • 三级域名:如 pku.edu.cn、www.cskaoyan.com。

image-20260615175526609

2. 顶级域名(TLD)三大分类

  1. 国家顶级域名(ccTLD):由各个国家/地区独立管理。如 .cn(中国,由 CNNIC 管理)、.us(美国)、.jp(日本)。
  2. 通用顶级域名(gTLD):表征组织性质。
    • .com:商业/公司类组织(Commercial)。
    • .net:网络服务机构(如 battle.net)。
    • .org:非营利性组织(如 wto.org)。
    • .edu / .gov:美国教育机构/政府专用(历史保留)。
  3. 基础结构域名 / 反向域名:全球只有 1 个,即 .arpa。
    • 作用:用于反向域名解析(即从 IP 地址 $\rightarrow$ 域名)。
    • 示例:查询 8.8.8.8 对应的域名,工具会访问反向域名 8.8.8.8.in-addr.arpa。

三、 四类域名服务器系统

QQ20260615-171944

通过“根 $\rightarrow$ 顶级 $\rightarrow$ 权限”逐层查询,一定可以查到任何有效域名的 IP 地址。

  1. 根域名服务器:记录并维护所有顶级域名服务器的 IP 地址。
    • 🚨 易错概念陷阱:全世界有 13 组根域名服务器(每组绑定一个固定 IP,如 1.1.1.1)。但 “每组”绝非单台主机,而是部署在全球多地的大量镜像节点。它们通过 Anycast(任播) 技术支持就近访问、冗余和容灾。
  2. 顶级域名服务器(TLD 服务器):管理在该顶级域名下注册的所有二级域名(如负责 .com 的服务器)。
  3. 权限域名服务器(授权域名服务器):负责将其管辖区内的主机名转换为 IP 地址(如 qq.com 权限服务器 记录了下属 mail.qq.com 的 IP)。
  4. 本地域名服务器(默认 DNS 入口):
    • 用户侧的默认 DNS 入口。主机入网时通过 DHCP 协议自动获取其 IP。
    • 通常由当地 ISP(电信/联通)、高校或企业自建。

四、 提高查询效率的关键:DNS 缓存机制

  • 本地域名服务器命中:若本地域名服务器中存有对应的 DNS 缓存,则直接返回结果,无需向外发起层层查询。
  • 生存时间(TTL, Time to Live):DNS 缓存记录会设置过期时间,到期后自动失效,以保证域名变更时数据能够更新。
  • 多级缓存:除了本地服务器,用户主机的操作系统级 DNS 解析器(Resolver) 以及 浏览器(如 Chrome) 内部都会维护应用级的 DNS 缓存。

五、 🚨 域名解析核心过程:递归查询 vs 迭代查询(必考大题)

在缓存未命中的情况下,本地域名服务器向外查询主要有两种方式:

1. 递归查询(Recursive Query)

  • 通俗口诀:“你问我问题,我想方设法帮你找到最终答案。”(一问一答,一查到底)
  • 流程轨迹:
    • 主机 $\rightarrow$ 本地域名服务器
    • 本地 $\rightarrow$ 根域名服务器 $\rightarrow$ 顶级 $\rightarrow$ 权限(由被询问方代替查询方继续向前问)
    • 最终结果沿原路返回。
  • 缺点:靠前的服务器(如根服务器)负担极重,极少在公网服务器之间采用。

QQ20260615-172951

2. 迭代查询(Iterative Query)

  • 通俗口诀:“你问我问题,我如果不知道最终答案,就告诉你‘下一个该去问谁’。”(指引方向,自主接力)
  • 标准流程轨迹(以查询 mail.qq.com 为例):
    1. 主机问本地:“mail.qq.com 的 IP 是多少?”
    2. 本地问根,根回复:“我不知道,你去问 .com 顶级服务器(IP: 3.3.3.3)。”
    3. 本地问顶级,顶级回复:“我不知道,你去问 qq.com 权限服务器(IP: 4.4.4.4)。”
    4. 本地问权限,权限给出最终权威解析:“它的 IP 是 7.7.7.7。”
    5. 本地将结果登记到缓存,并回复给主机。

QQ20260615-173102

SSH 是什么

SSH 的全称是 Secure Shell,中文一般翻译成“安全外壳协议”。

它最常见的用途是:让我们在本地电脑上,安全地登录到另一台远程服务器,并在远程服务器上执行命令。

比如:

1
ssh user@example.com

这条命令的意思是:用 user 这个用户名,通过 SSH 登录到 example.com 这台服务器。

SSH 解决的核心问题是:在不可信的网络里,安全地连接另一台机器。

它通常会做几件事:

  • 身份认证:确认你是不是有权限登录这台服务器,比如密码登录、密钥登录。
  • 加密通信:登录后输入的命令、返回的结果都会被加密传输。
  • 远程命令执行:可以像操作本地终端一样操作远程服务器。
  • 端口转发:也就是常说的 SSH 隧道,可以把某个网络连接“套”进 SSH 加密连接里传输。

这篇笔记主要记录 SSH 的 端口转发 / 隧道 用法。

SSH 隧道是什么

SSH 隧道的本质是:借用一条已经建立好的 SSH 连接,把其他 TCP 流量转发过去。

可以先把 SSH 想成一条安全管道:

1
本地电脑  <====== 加密 SSH 连接 ======>  SSH 服务器

正常情况下,这条管道只用来传输终端命令。

但端口转发会让这条管道承担更多事情:把某个端口收到的流量,经由 SSH 连接送到另一端,再转发给真正的目标服务。

常见的 SSH 隧道有三种:

类型 参数 核心方向 记忆方式
本地隧道 -L 本地端口 -> SSH 服务器侧的目标服务 把远程服务拉到本地
远程隧道 -R 远程端口 -> 本地侧的目标服务 把本地服务暴露到远程
动态隧道 -D 本地 SOCKS 代理 -> SSH 服务器 -> 任意目标 让 SSH 临时变成代理

本地隧道:把远程服务拉到本地

一句话理解:在本地开一个端口,访问这个本地端口时,流量会通过 SSH 转发到远程网络里的某个服务。

命令模板:

1
ssh -L 本地监听端口:目标主机:目标端口 用户名@SSH服务器

例子:

1
ssh -L 3307:127.0.0.1:3306 user@example.com

这条命令的含义是:

  • 在本地电脑监听 3307 端口。
  • 通过 SSH 登录到 example.com。
  • 当本地访问 127.0.0.1:3307 时,流量会被送到 SSH 服务器那一侧的 127.0.0.1:3306。

如果远程服务器上有一个 MySQL 只监听自己的 127.0.0.1:3306,外部不能直接访问,那么本地隧道就可以让你在本地这样连:

1
mysql -h 127.0.0.1 -P 3307 -u root -p

注意:这里连的是本地的 3307,但真正到达的是远程服务器那边的 3306。

本地隧道示意图

适用场景

  • 远程数据库不允许公网访问,只允许服务器本机访问。
  • 公司内网服务只能从跳板机访问。
  • 想安全地临时访问远程机器上的管理后台。

容易混淆的点

目标主机 是站在 SSH 服务器那一侧 来看的。

比如:

1
ssh -L 8080:127.0.0.1:80 user@example.com

这里的 127.0.0.1:80 不是你本地电脑的 80 端口,而是 example.com 那台服务器自己看到的 127.0.0.1:80。

远程隧道:把本地服务暴露到远程

一句话理解:在远程服务器上开一个端口,别人访问远程服务器这个端口时,流量会通过 SSH 反向回到本地服务。

命令模板:

1
ssh -R 远程监听端口:目标主机:目标端口 用户名@SSH服务器

例子:

1
ssh -R 9000:127.0.0.1:3000 user@example.com

这条命令的含义是:

  • 通过 SSH 登录到 example.com。
  • 在远程服务器上监听 9000 端口。
  • 当远程服务器访问自己的 127.0.0.1:9000 时,流量会通过 SSH 隧道回到本地电脑的 127.0.0.1:3000。

如果你本地正在跑一个 Web 服务:

1
npm run dev

服务地址是:

1
http://127.0.0.1:3000

那么远程隧道可以让 SSH 服务器通过下面这个地址访问到它:

1
http://127.0.0.1:9000

远程隧道示意图

适用场景

  • 本地电脑没有公网 IP,但想让远程服务器访问本地服务。
  • 在服务器上借用本地电脑的代理端口。
  • 临时把本地开发服务暴露给远程环境调试。

容易混淆的点

远程隧道默认通常只监听远程服务器自己的 127.0.0.1。

也就是说,下面这条命令:

1
ssh -R 9000:127.0.0.1:3000 user@example.com

一般只代表 SSH 服务器本机 可以访问 127.0.0.1:9000,不一定代表互联网上所有人都能访问 example.com:9000。

如果希望远程端口监听公网地址,通常还涉及 SSH 服务端配置,比如 GatewayPorts,这个属于更高风险的公网暴露场景,需要谨慎处理。

动态隧道:让 SSH 变成 SOCKS 代理

一句话理解:在本地开一个 SOCKS 代理端口,应用把请求交给这个代理后,SSH 会把请求转发到不同的目标地址。

命令模板:

1
ssh -D 本地SOCKS端口 用户名@SSH服务器

例子:

1
ssh -D 1080 user@example.com

这条命令的含义是:

  • 在本地电脑监听 1080 端口。
  • 这个端口不是固定转发到某一个服务,而是作为 SOCKS 代理使用。
  • 浏览器或其他程序把请求交给 127.0.0.1:1080 后,SSH 会根据请求里的目标地址,帮你从 SSH 服务器那一侧访问目标网站或服务。

浏览器代理可以这样理解:

1
浏览器 -> 127.0.0.1:1080(SOCKS5) -> SSH 服务器 -> 目标网站

动态隧道示意图

适用场景

  • 想让浏览器临时通过 SSH 服务器访问网络。
  • 目标地址很多,不想为每个服务单独写一个 -L。
  • 需要一个临时 SOCKS5 代理。

容易混淆的点

动态隧道和本地隧道都在本地开端口,但区别很大:

  • -L 是固定转发:一个本地端口对应一个固定目标。
  • -D 是动态代理:目标地址由应用的 SOCKS 请求决定。

所以 -D 后面不需要写 目标主机:目标端口。

三种隧道对比

对比项 本地隧道 -L 远程隧道 -R 动态隧道 -D
监听端口在哪里 本地电脑 SSH 服务器 本地电脑
流量最终去哪里 SSH 服务器那一侧的固定目标 本地电脑那一侧的固定目标 请求指定的动态目标
是否固定目标 固定 固定 不固定
常见用途 访问远程内网服务 让远程机器访问本地服务 临时 SOCKS 代理
记忆方式 拉回来 推过去 代理出去

更口语化地记:

  • 本地隧道 -L:我在本地开门,把远程服务拉到我面前。
  • 远程隧道 -R:我在远程开门,把访问反向带回本地。
  • 动态隧道 -D:我在本地开一个代理入口,后面去哪由请求自己决定。

参考资料

没问题,这个方法叫 SSH 远程端口转发(SSH Remote Port Forwarding)。它是绕过校园网/公司内网限制、借用本地网络最安全且最高效的“基操”。

以下是为你整理的完整流程手册,你可以直接收藏:


🛠️ 学校服务器“借网”出海手册

第一步:在本地电脑开启隧道

在你自己电脑的终端(Windows PowerShell 或 Mac Terminal)中运行,不要在 VS Code 的内置终端里运行。

Bash

1
2
3
4
5
6
7
8
9
10
11
12
# 命令格式:ssh -R [远程端口]:127.0.0.1:[本地代理端口] [用户名]@[服务器IP]
ssh -R 7897:127.0.0.1:7897 wjw@223.2.18.229
wjw19230415
ssh -R 7890:127.0.0.1:7890 zxj@223.2.18.229
cd MLLM_pre
npx @anthropic-ai/claude-code
export ANTHROPIC_MODEL=deepseek-v4-pro[1m]
export ANTHROPIC_DEFAULT_OPUS_MODEL=deepseek-v4-pro[1m]
export ANTHROPIC_DEFAULT_SONNET_MODEL=deepseek-v4-pro[1m]
export ANTHROPIC_DEFAULT_HAIKU_MODEL=deepseek-v4-flash
export CLAUDE_CODE_SUBAGENT_MODEL=deepseek-v4-flash
export CLAUDE_CODE_EFFORT_LEVEL=max
  • -R: 开启远程转发模式。
  • 7890: 这里的两个数字必须对应。前一个是服务器上开启的口子,后一个是你本地梯子的监听端口。

第二步:在服务器终端挂载代理

连接成功进入服务器后,在弹出的黑色窗口中输入以下命令,告诉系统“指路牌”:

1
2
3
export https_proxy=http://127.0.0.1:7897
export http_proxy=http://127.0.0.1:7897
export all_proxy=http://127.0.0.1:7897

注意:这几行命令只对当前这个窗口有效。如果你关掉窗口重新连,需要重新输入。


第三步:验证联网状态

输入测试命令,观察是否返回 HTTP/2 200:

Bash

1
curl -I https://www.google.com

第四步:启动 Copilot CLI

联网成功后,直接运行你安装好的工具:

Bash

1
npx @github/copilot /login

⚠️ 避坑指南(如果失败请检查这里)

  1. 本地梯子设置:

    必须打开梯子软件(Clash/v2ray/Surfboard 等)中的 “允许局域网连接 (Allow LAN)” 开关。即使是在同一台电脑通过隧道回传,软件有时也会拦截非本地请求。

  2. 端口匹配:

    如果你的梯子端口不是 7890(比如是 10809 或 1080),请把上面所有步骤中的 7890 替换成你的实际端口。

  3. VS Code 侧边栏同步:

    如果你想让 VS Code 的 Chat 插件也通,请在 VS Code 的 settings.json 中添加:

    "http.proxy": "http://127.0.0.1:7890"。

    由于你已经开了 SSH 隧道,此时远程服务器会认为 127.0.0.1:7890 就是它的出口。


💡 进阶:如何不用每次都打 export?

你可以把代理命令写入服务器的个人配置文件中:

  1. 输入 vim ~/.bashrc。

  2. 在文件最后添加:

    alias proxyon="export https_proxy=http://127.0.0.1:7890 http_proxy=http://127.0.0.1:7890 all_proxy=http://127.0.0.1:7890"

  3. 保存退出后,下次连上服务器只需输入 proxyon 即可一键开启。

这个流程存好了吗?如果你准备好尝试在服务器上直接安装一个永久“梯子”(不需要本地电脑一直开着),我们可以开始讲解 Tailscale 的方案。

tailscale/tailscale: The easiest, most secure way to use WireGuard and 2FA.

Repository search results

Releases · MetaCubeX/mihomo

SSH隧道的原理和实操 | 本地隧道、远程隧道、动态隧道_哔哩哔哩_bilibili

常用命令

1
2
3
4
5
6
7
8
openclaw gateway restart
openclaw gateway start
openclaw gateway stop

openclaw dashboard
openclaw onboard
openclaw gateway status
openclaw models auth login --provider openai-codex

长期记忆

方式

OpenClaw 记忆是智能体工作空间中的纯 Markdown 文件。这些文件是唯一的事实来源;模型只”记住”写入磁盘的内容。

这里面的记忆又分为两种,分别是用户级的记忆和日志记忆,memory文件夹中存放的是日志记忆,记录每天对话的需要长期存储的内容

新对话长期记忆的读取:1.读取日志记忆中今天和昨天的内容。2.读取用户级记忆

image-20260204150129994

还可以通过配置,设定额外的记忆空间

1
2
3
4
5
6
7
agents: {
defaults: {
memorySearch: {
extraPaths: ["../team-docs", "/srv/shared-notes/overview.md"]
}
}
}

除了对话和用户的记忆,openclaw本身的功能文件也是markdown存储

  1. 本地文档路径:C:\Users\ASUS\AppData\Roaming\npm\node_modules\openclaw\docs
  2. 技能(skills)路径:C:\Users\ASUS\AppData\Roaming\npm\node_modules\openclaw\skills\

image-20260204153018870

何时写入记忆

对于何时写入记忆分为两种:

1.当用户谈及决策、偏好和持久性事实,写入 MEMORY.md。

2.日常笔记和运行上下文写入 memory/YYYY-MM-DD.md。当会话达到上下文限制时,会触发压缩,将当前会话记忆存储于memory/YYYY-MM-DD.md

用户同样可以通过对话进行记忆的存储

image-20260204151029801

记忆的压缩

当会话接近自动压缩时,OpenClaw 会询问agent是否需要型在上下文被压缩之前写入持久记忆。

这由 agents.defaults.compaction.memoryFlush 控制:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
agents: {
defaults: {
compaction: {
reserveTokensFloor: 20000,
memoryFlush: {
enabled: true,
softThresholdTokens: 4000,
systemPrompt: "Session nearing compaction. Store durable memories now.",
prompt: "Write any lasting notes to memory/YYYY-MM-DD.md; reply with NO_REPLY if nothing to store.",
},
},
},
},
}

reserveTokensFloor: 20000代表必须要在还剩20000tokens前进行压缩(最后底线)

softThresholdTokens: 4000代表一个缓冲空间,当会话 token 估计超过 contextWindow - reserveTokensFloor - softThresholdTokens 时触发压缩提醒

记忆的搜索

openclaw分为两种记忆搜索的方式

1.使用向量索引构建小型的rag,需要配置向量模型的api或本地部署向量模型(默认方式)

2.如果没有配置向量模型,openclaw会选择直接遍历查看markdown文件进行记忆搜索

向量搜索

OpenClaw 可以在 MEMORY.md 和 memory/*.md上构建小型向量索引,以便语义查询可以找到相关笔记

流程如下:

  • 默认使用远程嵌入。如果未设置 memorySearch.provider,OpenClaw 自动选择:
    1. 如果配置了 memorySearch.local.modelPath 且文件存在,则使用 local。
    2. 如果可以解析 OpenAI 密钥,则使用 openai。
    3. 如果可以解析 Gemini 密钥,则使用 gemini。
    4. 否则记忆搜索保持禁用状态直到配置完成。
  • 本地模式使用 node-llama-cpp,可能需要运行 pnpm approve-builds。
  • 使用 sqlite-vec(如果可用)在 SQLite 中加速向量搜索。

为什么 OpenAI 批处理快速又便宜:

  • 对于大型回填,OpenAI 通常是我们支持的最快选项,因为我们可以在单个批处理作业中提交许多嵌入请求,让 OpenAI 异步处理它们。
  • OpenAI 为 Batch API 工作负载提供折扣定价,因此大型索引运行通常比同步发送相同请求更便宜。
  • 详情参见 OpenAI Batch API 文档和定价:

批处理模式 (Batch):你把几千个、几万个请求打包成一个文件塞给 OpenAI,并告诉它:“我不急,你 24 小时内给我就行。”

原理:OpenAI 会利用服务器的闲置时段(比如凌晨)来处理这些任务。因为你帮它平摊了服务器压力,所以它给你 50% 甚至更多的折扣。

记忆工具

openclaw提供了两种记忆工具

memory_search 从 MEMORY.md + memory/**/*.md 语义搜索 Markdown 块。它返回片段文本、文件路径、行范围、分数,以及我们是否从本地回退到远程嵌入。不返回完整文件内容。

memory_get 读取特定的记忆 Markdown 文件(工作空间相对路径),可选从起始行开始读取 N 行。

两个工具仅在智能体的 memorySearch.enabled 解析为 true 时启用。

image-20260204155222237

搜索方式

OpenClaw 结合:

  • 向量相似度(语义匹配,措辞可以不同)
  • BM25 关键词相关性(精确令牌如 ID、环境变量、代码符号)

构建索引

索引存储:每个智能体的 SQLite 位于 ~/.openclaw/memory/<agentId>.sqlite

配置向量模型

image-20260204160814962

查看构建结果

image-20260204161159014

image-20260204161150230

会话记忆

  • 在 Gateway 网关主机上:
    • 存储文件:~/.openclaw/agents/<agentId>/sessions/sessions.json(每个智能体)。
  • 对话记录:~/.openclaw/agents/<agentId>/sessions/<SessionId>.jsonl(Telegram 话题会话使用 .../<SessionId>-topic-<threadId>.jsonl)。

image-20260204162005555

image-20260204161956796

参考资料

openclaw/openclaw: Your own personal AI assistant. Any OS. Any Platform. The lobster way. 🦞

Gateway Runbook - OpenClaw

【Clawdbot为什么能记住你说过的话? AI记忆系统拆解】 https://www.bilibili.com/video/BV1fv61B4EQ5/?share_source=copy_web&vd_source=5e54f7845fd2cf2828efb1bae2286590

TCP和UDP协议的核心原理和区别

TCP:全称是 Transmission Control Protocol,中文翻译为传输控制协议。

UDP:全称是 User Datagram Protocol,中文翻译为用户数据报协议。

1. 连接类型和服务对象

这决定了通信双方在“开聊”之前要做什么,以及能和谁聊。

  • TCP(面向连接):
    • 连接类型:必须通过“三次握手”建立逻辑连接。通信结束后,还需“四次挥手”断开。
    • 服务对象:它是端到端的,即点对点通信。一旦连接建立,就是两个特定 IP/端口之间的独占通道。
  • UDP(无连接):
    • 连接类型:完全无连接。想发就发,不需要预先建立任何关系。
    • 服务对象:支持一对一、一对多(广播)、多对一和多对多(组播)。它更像是一个大喇叭,谁都能听,谁都能喊。

2. 首部(Header)

image-20260126145037525

这决定了协议的“管理成本”和传输效率。

  • TCP(臃肿但功能全):
    • 长度:最小 20 字节,如果包含选项字段会更长。
    • 内容:包含序列号、确认号、各种标志位(SYN/ACK/FIN)、窗口大小等。这些复杂的字段都是为了确保“可靠传输”。
  • UDP(简洁精悍):
    • 长度:固定为 8 字节。
    • 内容:只包含四个字段:源端口、目的端口、长度和校验和。这让它的处理速度极快,开销极低。

3. 可靠性与传输方式

image-20260126145256624

这是两者最本质的区别。

  • TCP(可靠的字节流):
    • 可靠性:通过确认应答(ACK)、超时重传、丢包检测来保证数据不丢失、不重复、且按序到达。
    • 传输方式:面向字节流。TCP 不管你应用层发的是什么,它把数据看成一串流。它可能会把你的大报文拆开,也可能把几个小报文合并。
  • UDP(不可靠的报文):
    • 可靠性:尽力而为。不保证送达,也不保证顺序。丢了就丢了,UDP 本身不负责重传。
    • 传输方式:面向报文。你给它一个 500 字节的包,它就原封不动打上头发出去,保留了报文的边界,不会进行拆分或合并。

4. 流量控制和拥塞控制

这决定了协议如何应对网络环境的变化。

  • TCP(智能管家):
    • 流量控制:通过“滑动窗口”机制,根据接收方的处理能力动态调整发送速度,防止把对方“淹没”。
    • 拥塞控制:当网络出现拥堵时,TCP 会通过慢启动、拥塞避免等算法主动降低发送速率。它有很强的自我约束和适应能力。
  • UDP(热血青年):
    • 控制机制:完全没有。网络堵不堵、对方收不收得下,UDP 全然不理。它只会按应用层要求的速度拼命发。这种特性在网络差时可能导致严重的丢包,但也保证了实时性。

5. 应用场景

基于上述特性,它们的应用领域泾渭分明。

协议 适用核心需求 典型应用
TCP 准确性第一,宁慢勿错 浏览器(HTTP/HTTPS)、文件传输(FTP)、邮件(SMTP)、远程登录(SSH)
UDP 速度/实时性第一,允许少量错误 视频会议(Zoom/WeChat)、在线直播、实时竞技游戏、域名解析(DNS)

TCP三次握手和四次挥手

理解 TCP 的“三次握手”和“四次挥手”,本质上是理解两个终端如何在不可靠的网络环境下,建立并优雅地释放一个可靠的双工(双向)通道。


1. 三次握手(建立连接)

image-20260126145536691

目的是确认双方的收发能力都正常,并交换初始序列号(ISN)。

  1. 第一次握手 (SYN):客户端发送一个 SYN 包(序列号为 $x$)给服务端。
    • 潜台词:客户端说:“我想和你建连,这是我的初始序列号。”
  2. 第二次握手 (SYN + ACK):服务端收到后,回传一个 SYN 包(序列号为 $y$)和 ACK 包(确认号为 $x+1$)。
    • 潜台词:服务端说:“收到!我同意建连,这是我的序列号,我也确认收到你的了。”
  3. 第三次握手 (ACK):客户端再回传一个 ACK 包(确认号为 $y+1$)。
    • 潜台词:客户端说:“收到!那我们正式开始传数据吧。”

为什么要三次?

为了防止“已失效的连接请求报文”突然又传到了服务端。如果只有两次握手,服务端一响应就建立连接,那么旧的重复请求会导致服务端浪费资源去维持一个根本不存在的连接。


2. 四次挥手(释放连接)

image-20260126151050859

由于 TCP 是全双工的,断开连接时需要双方都确认“我没数据要发了”。

  1. 第一次挥手 (FIN):客户端发一个 FIN 包给服务端。
    • 状态:客户端进入 FIN_WAIT_1,表示客户端不再发数据了。
  2. 第二次挥手 (ACK):服务端回一个 ACK。
    • 状态:服务端进入 CLOSE_WAIT。此时连接处于半关闭状态,服务端可能还有没发完的数据。
  3. 第三次挥手 (FIN):服务端发完最后的数据,发送 FIN 包给客户端。
    • 状态:服务端进入 LAST_ACK。
  4. 第四次挥手 (ACK):客户端收到后,回一个 ACK,然后进入 TIME_WAIT 状态。
    • 状态:经过 $2MSL$(最长报文段寿命)时间后,客户端正式关闭。

参考资料

【神奇的滑动窗口 | TCP流量控制】 https://www.bilibili.com/video/BV1sQXDYREwP/?share_source=copy_web&vd_source=5e54f7845fd2cf2828efb1bae2286590

【“我为人人,人人为我”的TCP拥塞控制】 https://www.bilibili.com/video/BV1jWS7BeEX5/?share_source=copy_web&vd_source=5e54f7845fd2cf2828efb1bae2286590

0%