难度:★★☆(需要会 Python 函数和 if/else)
教材:查资料/server.py(重点第 908-1451 行)
预计时间:讲解 50 分钟 + 练习 30 分钟
学完你能:说清"浏览器打开网页时到底发生了什么",并且能给项目加一个自己的接口
前置:能读函数、字典、if 和异常。这里只需要认识“类把若干方法放在一起”:Handler 是标准库在收到请求时使用的处理器,self 代表这次处理器对象;不要求你先实现自己的类框架。
在下载包 assets/主线实验/ 运行 py -3 server.py,访问 http://127.0.0.1:8030/api/about。预期 200,正文含 name 和 version;访问 /missing 预期 404 和 {"error": "not found"}。
在 F12 Network 中核对方法、状态、类型和响应正文。这四项共同构成验证,网页“显示了一些字”还不足以证明接口契约正确。
在单独练习文件中运行:
text = "学习"
data = text.encode("utf-8")
print(len(text))
print(len(data))
预期是 2 和 6。Content-Length 说明正文字节数,不能直接填字符串字符数。json.dumps、encode、写响应是三个有先后依赖的动作。
回首页添加卡片,观察 POST /api/cards 返回 201。请求中的 JSON 由服务器读取、解析、校验后才存储;标题为空是 400,路径不存在是 404。GET 与 POST 的区别不仅是路径相同还是不同,也包括方法以及请求体的语义。
原课的片段省略了部分 import 和函数实现,完整可运行版本请使用主线实验。只把 self._json({...}) 复制到一个空文件,当然不能运行,因为它是说明调用位置的摘录。
在主线实验 do_GET 中增加 /api/version,返回 {"version": "2.0"}。重启服务:新路径 200、未知路径仍 404、原有创建卡片仍成功。记录三次请求,而不是只看一次成功。
排错:改完没生效先重启;返回 HTML 时检查是否访问了 /;400 时看 JSON 格式和 title;连接失败时先看监听端口。路由匹配出错与网络没连上是不同阶段。
并发只作边界认识:ThreadingHTTPServer 可以用线程分别处理请求,但共享数据、数据库写锁、CPU 工作和资源上限仍可能造成等待。“用了线程”不等于所有工作互不阻塞。
「查资料」没有用 Flask、没有用 FastAPI,而是用 Python 标准库 http.server
自己撸了一个完整服务器。听起来很硬核,但拆开看逻辑非常朴素。
学会它,你会获得两个好处:
启动服务.bathttp://127.0.0.1:8000//、/static/js/web.js、/api/cards……随便点开一条,注意看:
这些请求,就是接下来要讲的代码在处理。 现在读代码,每一段都能对上号。
整个网络层就是 server.py 里的一个类:Handler(第 908 行开始)。
它其实是三类方法的组合:
Handler(第 908-1350 行)
│
├── ① 工具箱(第 912-960 行)—— 把"怎么回复"的动作封装成小函数
│ _json() 回复 JSON 数据(接口用)
│ _text() 回复纯文本
│ _body_json() 读取请求带来的 JSON(POST 用)
│ _send_file() 把文件发回去(网页、css、js 用)
│
├── ② 路由入口(第 962-1049 行)—— 决定"哪个 URL 交给谁处理"
│ do_GET() 处理浏览器"要数据/要页面"
│ do_POST() 处理"提交/新建/操作"
│ do_DELETE() 处理"删除"
│
└── ③ 业务方法(第 1051-1350 行)—— 具体每个接口的逻辑
_cards_list() / _card_create() / _search() / _graph() / _export() ...
一句话总结:**浏览器发请求 → ② 路由找到负责人 → ③ 业务方法干活
(会用①工具箱回复,还会用数据层查库)→ 返回。**
_json() —— 所有接口的统一出口(第 916-923 行)def _json(self, obj, status=200):
data = json.dumps(obj, ensure_ascii=False).encode("utf-8")
self.send_response(status)
self.send_header("Content-Type", "application/json; charset=utf-8")
self.send_header("Content-Length", str(len(data)))
self.send_header("Cache-Control", "no-store")
self.end_headers()
self.wfile.write(data)
小白翻译,逐行看:
| 代码 | 人话 |
|---|---|
json.dumps(obj, ensure_ascii=False) | 把 Python 字典变成 JSON 字符串;ensure_ascii=False 是为了中文不变成 \uXXXX |
.encode("utf-8") | 字符串变成字节流(网络只能传字节) |
send_response(status) | 先告诉浏览器:"这次结果的状态码是 200(成功)" |
Content-Type: application/json | 告诉浏览器:"内容是 JSON 格式,用 UTF-8 解码" |
Content-Length | "内容一共 N 个字节"——浏览器靠它知道什么时候读完 |
end_headers() | 响应头写完了,接下来写正文 |
self.wfile.write(data) | 把正文(数据)真正发出去 |
重点理解:HTTP 响应 = 状态行 + 响应头 + 空行 + 正文。
这段代码就是在按格式拼一个响应。所有接口都靠它回复,所以只写一次。
do_GET() —— 极简版"路由表"(第 963-1009 行)def do_GET(self):
parsed = urllib.parse.urlparse(self.path)
path = parsed.path.rstrip("/") or "/"
qs = urllib.parse.parse_qs(parsed.query)
if path == "/api/health":
return self._json({...})
if path == "/api/cards":
return self._cards_list(qs)
if path.startswith("/api/cards/"):
cid = urllib.parse.unquote(path[len("/api/cards/"):])
return self._card_get(cid)
...
小白翻译:
self.path 是浏览器请求的原始路径,比如 /api/cards?q=诗经urlparse 把它拆成:路径 /api/cards + 查询参数 q=诗经parse_qs 把 q=诗经 变成字典 {"q": ["诗经"]}关键设计点:
path.startswith("/api/cards/") 表示 /api/cards/abc123这种"带编号"的 URL 走这里,abc123 就是卡片 id
(unquote 是解码中文/特殊字符)
/api/cards/recent 写在 /api/cards/ 前面!如果反过来,recent 会被当成卡片 id 吃掉——路由顺序是有讲究的
/api/ 开头的路径,都当作"要网页文件"处理if os.path.commonpath([path, WEB_DIR]) != WEB_DIR:
return self._json({"error": "forbidden"}, 403)
这段什么意思?浏览器如果请求 /../server.py(想偷看源码),
拼出来的路径会跑到 web/ 文件夹外面。这行代码就是在检查:
"你要的文件必须老老实实待在 web 目录里,不然 403 拒绝。"
练习里会让你亲手试一次攻击,看看 403 长什么样。
do_POST() —— 表驱动的小聪明(第 1011-1039 行)POST 是"提交数据",所以要先把请求正文读出来(_body_json,第 933 行):
从 Content-Length 知道有多长 → 读那么多字节 → 解析 JSON。
有意思的是这段(第 1029 行):
for suffix, act in (("/link", "link"), ("/links", "unlink"), ("/move", "move"), ("/dup", "dup"), ("/set-tags", "set-tags")):
if path.endswith(suffix):
...break
这叫表驱动:把"后缀 → 动作名"写成一张表,循环匹配。
比写 5 个 if 更整齐,加新动作时只改表。是一个值得学的小技巧。
server = ThreadingHTTPServer((HOST, PORT), Handler)
server.serve_forever()
Threading 表示用线程分别处理请求,但数据库写锁、共享状态和资源限制仍会造成等待serve_forever() 是死循环:监听端口 → 有请求就交给 Handler → 继续等冷知识:Handler 类里必须叫 do_GET / do_POST / do_DELETE,
这是 BaseHTTPRequestHandler 定的规矩——请求方法是什么,就找 do_什么。
名字写错,服务器就"听不懂"(返回 501)。
官方文档里最简单的例子是这样的:
class Handler(BaseHTTPRequestHandler):
def do_GET(self):
self.send_response(200)
self.end_headers()
self.wfile.write(b"Hello")
教材项目 = 这个例子的完整版:
多了路由(if 链)、工具箱(_json/_send_file)、业务层(查数据库)、安全校验。
你不是在"用"http.server,你是在它上面盖了一栋楼。
from flask import Flask, jsonify
app = Flask(__name__)
@app.get("/api/cards")
def cards_list():
return jsonify(load_cards())
@app.get("/api/cards") 把"路径 → 函数"登记到一张路由表jsonify() 与 _json() 都负责 JSON 响应;实际并发方式还取决于 WSGI/ASGI 服务器与部署配置@app.get("/api/cards/{cid}")
def card_get(cid: str):
return card_by_id(cid)
{cid} 自动把值传给参数——你再也不用 path.startswith + 字符串裁剪了cid: str 让它自动校验参数,还自动生成接口文档unquote(path[len("/api/cards/"):]) 就是在手工做 FastAPI 自动做的事| 你的 server.py | Flask | FastAPI | |
|---|---|---|---|
| 路由方式 | if 链 | 装饰器 + 路由表 | 装饰器 + 类型注解 |
| JSON 序列化 | 自己写 _json() | jsonify() | 自动 |
| 参数解析 | 手工解析/裁剪 | 半自动 | 全自动 |
| 并发 | 线程式服务器 | 取决于 WSGI 服务器配置 | 取决于 ASGI 服务器及应用中的阻塞操作 |
| 学习价值 | 彻底看懂原理 | 上手快 | 现代标准 |
面试时你可以说:"我用标准库手写过 HTTP 服务,所以理解框架在路由、
序列化、并发上分别帮我封装了什么。"——这句话含金量很高。
练习 1(必做):加一个自己的接口
在 do_GET() 里加一条:访问 /api/about 时,返回"项目名 + 你的名字"的 JSON。
提示:照抄 /api/health 那三行,改路径和内容,改完重启服务再访问。
练习 2(必做):给接口加字段
在 /api/config 的返回值里加一个 "version": "1.0",刷新网页看看前端能不能收到变化
(可选进阶:在 web/js/ 里找到用它的地方)。
练习 3(安全实验):
在本地副本中阅读路径校验:先把候选路径规范化,再检查它是否仍位于允许目录。浏览器常会先规范化 URL 中的 ..,因此在地址栏输入 /../server.py 不能保证发出的就是原始路径,也不能保证得到 403。记录 Network 中实际发出的路径,并用临时目录的路径解析用例验证目录内允许、目录外拒绝;无需删除正在运行服务的校验。
练习 4(选做):
用 F12 网络面板,找一个 404 的请求(比如随便访问一个不存在的路径),
观察它的状态码和响应内容,说说看你的代码里哪一行决定了它返回 404。
_json() 就是按这个格式拼装Content-Type 告诉浏览器"用什么格式解读",Content-Length 告诉它"读多长"ThreadingHTTPServer 让服务能同时接客;do_GET 等名字是标准库的固定约定| 术语 | 人话解释 |
|---|---|
| HTTP | 浏览器和服务器聊天的“语言协议” |
| GET / POST / DELETE | 要数据 / 交数据 / 删数据 三种基本动作 |
| 路由(routing) | 根据 URL 路径决定由哪段代码处理 |
| 状态码 | 200 成功、404 找不到、403 没权限、500 服务器出错 |
| Content-Type | 正文的格式说明(JSON、HTML、图片……) |
| 端口(port) | 一台电脑上不同服务的“门牌号”,8000 是这个应用的门牌 |
| 线程 | 操作系统里的“打工人”,多线程 = 多雇几个人同时干活 |
| 路径穿越 | 用 ../ 跳出允许目录去偷文件的黑客手法 |
第 03 课:单片应用也要分模块:MVC。
换一本教材——「关系图」,打开就是 1680 行的一个 HTML 文件。
但它内部用注释切成了清清楚楚的五段,还有一个经典的 MVC 架构。
我们会看看"一个文件如何写出多文件项目的秩序感",并对照 ECharts 官方示例。