feat(web基础): 新增零基础Web开发课程

This commit is contained in:
zhiye.sun
2026-08-20 16:14:29 +08:00
parent bf1f7042a9
commit 0c3d6402ea
19 changed files with 2005 additions and 8 deletions
@@ -0,0 +1,307 @@
# 第5-6课:HTTP接口调试与综合实践
## 一、本课定位
本课把客户端、服务器、URL、HTTP、状态码、JSON和RESTful API组合成一个可运行的本地闭环。示例使用Python标准库搭建临时图书接口,只服务于协议观察,不替代下一阶段的FastAPI。
## 二、本课目标
完成本课后,你能够:
1. 启动一个只监听本机的临时HTTP服务;
2. 使用客户端代码发送GET和POST请求;
3. 同时检查状态码、响应头和JSON响应体;
4. 识别JSON格式错误、字段错误和资源不存在;
5. 设计图书管理系统的RESTful API;
6. 说明HTTP知识如何迁移到FastAPI。
## 三、前置知识
- 客户端、服务器和请求响应流程;
- URL、HTTP方法、请求头和请求体;
- 常见状态码;
- JSON转换与RESTful API设计;
- Python类、异常、线程和上下文管理器的基本阅读能力。
线程(Thread)表示同一程序中的一条执行路线。本课只用后台线程让服务器等待请求,主线程同时充当客户端;不展开并发编程细节。
本课示例使用了前面阶段学过的类、异常和上下文管理器,但只要求按调用流程阅读,不重新讲解这些Python语法。
## 四、为什么现在才启动真实HTTP服务
前五课分别拆开观察了通信角色、URL、请求、响应、JSON和接口设计。如果一开始就使用框架,路由、自动校验和自动序列化会让新手看到结果,却不容易分清是谁完成了哪一步。
本课使用标准库建立一个最小闭环:
1. 程序在本机启动HTTP服务器;
2. 客户端向它发送真实HTTP请求;
3. 服务器读取路径、请求头和请求体;
4. 服务器修改内存数据;
5. 服务器返回状态码、响应头和JSON;
6. 客户端读取并输出响应;
7. 全部测试结束后关闭服务器。
它是教学服务器,不是下一阶段要长期使用的项目框架。
## 五、先认识本课的新名词
| 名词 | 本课中的含义 |
|---|---|
| `localhost` | 表示自己的计算机,常对应`127.0.0.1` |
| 监听(Listen) | 服务器等待某个地址和端口上的请求 |
| 随机端口 | 传入端口0,由操作系统选择当前可用端口 |
| 处理器(Handler) | 收到请求后负责读取并生成响应的代码 |
| 后台线程 | 在同一程序中持续运行服务器,让主流程可以发送请求 |
| 内存数据 | 只存在于当前程序运行期间,结束后不会保存 |
| 超时(Timeout) | 等待超过限定时间后停止继续等待 |
`127.0.0.1`只指向本机。绑定该地址可以避免本课没有认证的服务被局域网其他设备访问。
## 六、完整调用链
```text
send_request()
↓ HTTP请求
BookApiHandler
↓ 路径匹配、JSON解析、字段校验
内存图书列表
↓ JSON响应
send_request()读取状态码、响应头和响应体
```
示例绑定`127.0.0.1`,只接受本机连接;端口传入`0`,由操作系统选择当前可用端口,避免与现有服务冲突。数据只保存在内存中,程序结束即消失。
同一个Python程序同时扮演两种角色:
- `ThreadingHTTPServer`和`BookApiHandler`是服务器;
- `send_request()`是客户端;
- 它们通过本机网络连接交换HTTP消息,不是普通函数直接传参。
## 七、服务器如何启动
```python
server = ThreadingHTTPServer(("127.0.0.1", 0), BookApiHandler)
```
这行代码只创建服务器对象,还没有开始持续等待请求。地址元组中:
- `127.0.0.1`限制为本机;
- `0`请求操作系统分配端口;
- `BookApiHandler`指定收到请求后由哪个类处理。
随后创建后台线程并执行`serve_forever()`。名称表示服务器会持续等待,直到程序调用`shutdown()`。
## 八、服务端重点
`BaseHTTPRequestHandler`按请求方法调用`do_GET()`或`do_POST()`。`send_json()`负责:
1. 将Python对象序列化为JSON;
2. 编码成UTF-8字节;
3. 写入状态码和响应头;
4. 按字节数计算`Content-Length`;
5. 写出响应体。
中文字符的字符数量与UTF-8字节数量不同,因此必须对编码后的`response_body`调用`len()`。
### 8.1 GET请求
客户端请求`GET /books`时,标准库调用`do_GET()`:
1. 检查`self.path`是否等于`/books`;
2. 路径匹配时返回200和图书列表;
3. 路径不匹配时返回404和错误JSON。
### 8.2 POST请求
客户端请求`POST /books`时,标准库调用`do_POST()`:
1. 检查路径;
2. 从`Content-Length`得知需要读取多少字节;
3. 读取请求体并按UTF-8解码;
4. 使用`json.loads()`解析JSON;
5. 校验`title`是否为非空字符串;
6. 创建新图书并加入内存列表;
7. 返回201、新图书JSON和`Location`响应头。
JSON语法错误返回400,字段不符合要求返回422,未知路径返回404。三个结果代表不同问题。
## 九、客户端重点
`urllib.request.Request`创建请求,`urlopen()`发送请求。遇到4xx或5xx时,标准库会抛出`HTTPError`,但错误对象仍包含状态码和响应体,需要读取后再判断原因。
接口调试不能只看响应体。至少检查:
- 请求方法与完整URL;
- 请求头和请求体;
- HTTP状态码;
- 响应`Content-Type`;
- JSON响应结构;
- 服务端数据是否按预期变化。
客户端发送JSON前,需要完成与服务器相反的转换:
```text
Python字典 → JSON字符串 → UTF-8字节 → HTTP请求体
```
客户端收到响应后再反向处理:
```text
HTTP响应体字节 → UTF-8字符串 → 显示或解析JSON
```
示例为保持输出直观,只显示JSON字符串。实际业务客户端通常还会使用`json.loads()`转换成对象。
## 十、程序完整执行顺序
1. 创建服务器对象;
2. 创建并启动后台线程;
3. 从服务器对象取得操作系统分配的端口;
4. 第一次GET查询初始列表;
5. POST创建“HTTP接口实践”;
6. 第二次GET验证列表已经变化;
7. GET未知路径验证404;
8. 无论中间是否出错,`finally`都会关闭服务器;
9. 主线程等待后台线程结束;
10. 程序输出“本地服务已关闭”。
这里先新增再查询,是为了验证POST不只是返回201,还真实改变了当前程序的内存状态。
## 十一、运行方法
```powershell
cd D:\Code\Python\05_web基础\5_6_HTTP接口调试与综合实践
python local_book_api_example.py
```
## 十二、预期结果
```text
本地服务已启动:http://127.0.0.1:随机端口号
GET http://127.0.0.1:相同端口/books -> 200
响应体:{"items": [{"id": 1, "title": "Python入门"}]}
POST http://127.0.0.1:相同端口/books -> 201
响应体:{"id": 2, "title": "HTTP接口实践"}
GET http://127.0.0.1:相同端口/books -> 200
响应体中包含两本图书
GET http://127.0.0.1:相同端口/missing -> 404
响应体:{"message": "资源不存在"}
本地服务已关闭。
```
端口号每次不同是正常现象。第二次查询应包含新创建的“HTTP接口实践”。
## 十三、怎样判断测试真正通过
不要只检查程序是否结束,应逐项确认:
1. 四次请求使用同一个随机端口;
2. 第一次GET只有初始图书;
3. POST状态码是201而不是200;
4. POST响应中生成了编号2;
5. 第二次GET包含编号1和编号2;
6. 未知路径返回404;
7. 最后一行确认服务器已关闭;
8. 再运行一次时仍从一条初始数据开始。
最后一项说明数据只在内存中,没有持久保存,也说明示例可重复运行。
## 十四、使用接口调试工具
示例服务会自动结束,不适合手工操作。完成`practice.py`时,可以让服务等待键盘输入后再关闭,然后使用Postman、Apifox或IDE内置HTTP客户端发送请求:
```http
GET http://127.0.0.1:端口/books
```
```http
POST http://127.0.0.1:端口/books
Content-Type: application/json
{"title": "Web实践", "author": "张三"}
```
不要把服务绑定到`0.0.0.0`,本课没有实现认证和生产安全配置。
调试工具中的请求配置通常分为:
1. 选择HTTP方法;
2. 输入完整URL;
3. 配置请求头;
4. 选择JSON请求体并输入数据;
5. 发送请求;
6. 查看状态码、耗时、响应头和响应体。
不要只复制响应内容,还要记录发送了什么请求,否则别人无法复现问题。
## 十五、图书管理API设计草案
| 方法 | 路径 | 成功状态 | 作用 |
|---|---|---:|---|
| GET | `/books` | 200 | 分页查询图书 |
| GET | `/books/{book_id}` | 200 | 查询图书详情 |
| POST | `/books` | 201 | 新增图书 |
| PATCH | `/books/{book_id}` | 200 | 修改部分字段 |
| DELETE | `/books/{book_id}` | 204 | 删除图书 |
常见失败包括400格式错误、404图书不存在、409编号冲突和422字段校验失败。实际项目还要处理认证、权限、日志、数据库事务和并发。
## 十六、与FastAPI的衔接
下一阶段会由FastAPI代替底层标准库代码:
- 路由装饰器代替手工判断路径和方法;
- Pydantic模型代替手工字段校验;
- 框架自动序列化JSON并生成接口文档;
- 异常处理器统一生成错误响应。
不会改变的是HTTP层的设计:方法、路径、参数、状态码、请求结构和响应结构仍需开发者决定。
## 十七、常见错误
### 17.1 服务启动后程序不结束
服务器会持续等待请求。必须调用`shutdown()`和`server_close()`,并等待后台线程结束。
### 17.2 只测试成功情况
至少覆盖未知路径、无效JSON和缺少必填字段,否则无法确认错误边界。
### 17.3 把标准库示例当作生产服务器
本例只用于教学,没有完整认证、限流、日志、安全加固和部署能力。
### 17.4 把服务绑定到`0.0.0.0`
这会让服务监听所有可用网络接口。本课没有认证,不应扩大访问范围。
### 17.5 按字符数计算`Content-Length`
HTTP传输的是字节。包含中文时必须先编码,再计算字节长度。
### 17.6 忘记处理`HTTPError`
标准库遇到4xx和5xx会抛出`HTTPError`。错误对象中仍有响应体,应读取并用于定位问题。
### 17.7 端口不同就认为结果错误
本课故意使用随机可用端口。只要同一次运行中的请求端口一致即可。
## 十八、课堂练习
打开`practice.py`,完成图书管理接口设计、服务端实现和客户端调试。练习不提供答案或代码骨架。
## 十九、本课小结
一个接口是否正确,要从客户端请求、服务器处理和HTTP响应三个方向共同验证。框架会减少底层代码,但不会代替接口语义和业务规则设计。
## 二十、验收标准
- 本地服务能自动启动和关闭;
- GET、POST和404请求输出符合预期;
- JSON中文编码与`Content-Length`正确;
- 能设计完整的图书管理接口;
- 能说明本阶段知识与FastAPI的对应关系;
- 能完整描述GET和POST在服务器中的处理顺序;
- 能从请求方法、URL、状态码、响应头、响应体和数据变化六方面调试接口;
- 完成`practice.py`综合练习。
@@ -0,0 +1,140 @@
"""第5-6课标准示例:启动本地HTTP服务并自动调用图书接口。"""
import json
from http import HTTPStatus
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from threading import Thread
from urllib.error import HTTPError
from urllib.request import Request, urlopen
class BookApiHandler(BaseHTTPRequestHandler):
"""处理本课图书查询和新增请求的最小HTTP处理器。"""
# 本课不连接数据库,使用类属性保存当前程序运行期间的图书。
# 程序退出后这份列表会消失,再次运行时会重新从一条图书开始。
books = [{"id": 1, "title": "Python入门"}]
def send_json(
self,
status: HTTPStatus,
data: dict | list,
extra_headers: dict | None = None,
) -> None:
"""把Python对象转换为UTF-8 JSON响应。"""
# HTTP连接传输的是字节,因此先生成JSON字符串,再编码成UTF-8字节。
response_body = json.dumps(data, ensure_ascii=False).encode("utf-8")
self.send_response(status)
self.send_header("Content-Type", "application/json; charset=utf-8")
# Content-Length必须填写字节数量,不能直接计算中文字符串的字符数。
self.send_header("Content-Length", str(len(response_body)))
if extra_headers is not None:
for header_name, header_value in extra_headers.items():
self.send_header(header_name, header_value)
self.end_headers()
self.wfile.write(response_body)
def do_GET(self) -> None:
"""处理GET /books,并拒绝未知路径。"""
# 标准库收到GET请求后会自动调用本方法,self.path保存请求目标。
if self.path == "/books":
self.send_json(HTTPStatus.OK, {"items": self.books})
return
self.send_json(HTTPStatus.NOT_FOUND, {"message": "资源不存在"})
def do_POST(self) -> None:
"""处理POST /books,校验JSON并创建图书。"""
if self.path != "/books":
self.send_json(HTTPStatus.NOT_FOUND, {"message": "资源不存在"})
return
try:
# Content-Length告诉服务器本次请求体一共有多少字节。
content_length = int(self.headers.get("Content-Length", "0"))
request_body = self.rfile.read(content_length).decode("utf-8")
request_data = json.loads(request_body)
except (UnicodeDecodeError, json.JSONDecodeError):
self.send_json(HTTPStatus.BAD_REQUEST, {"message": "JSON格式错误"})
return
title = request_data.get("title")
if not isinstance(title, str) or not title.strip():
self.send_json(
HTTPStatus.UNPROCESSABLE_CONTENT,
{"message": "书名不能为空"},
)
return
# 本课用列表长度模拟生成编号;真实项目通常由数据库生成主键。
book = {"id": len(self.books) + 1, "title": title.strip()}
self.books.append(book)
self.send_json(
HTTPStatus.CREATED,
book,
{"Location": f"/books/{book['id']}"},
)
def log_message(self, format_text: str, *args: object) -> None:
"""关闭标准库默认访问日志,让教学输出保持清晰。"""
def send_request(
url: str,
method: str = "GET",
data: dict | None = None,
) -> None:
"""作为客户端发送HTTP请求并输出响应。"""
request_body = None
headers = {"Accept": "application/json"}
if data is not None:
request_body = json.dumps(data, ensure_ascii=False).encode("utf-8")
headers["Content-Type"] = "application/json; charset=utf-8"
# Request只负责描述请求;urlopen()执行时才真正连接服务器并发送请求。
request = Request(url, data=request_body, headers=headers, method=method)
try:
with urlopen(request, timeout=3) as response:
response_text = response.read().decode("utf-8")
print(f"{method} {request.full_url} -> {response.status}")
print(f"响应体:{response_text}")
except HTTPError as error:
response_text = error.read().decode("utf-8")
print(f"{method} {request.full_url} -> {error.code}")
print(f"响应体:{response_text}")
def main() -> None:
"""在随机本地端口启动服务,完成调用后安全关闭。"""
# 127.0.0.1限制为本机访问;端口0表示让操作系统选择可用端口。
server = ThreadingHTTPServer(("127.0.0.1", 0), BookApiHandler)
# 服务器需要持续等待请求,因此放入后台线程;主线程继续充当客户端。
server_thread = Thread(target=server.serve_forever, daemon=True)
server_thread.start()
host, port = server.server_address
base_url = f"http://{host}:{port}"
print(f"本地服务已启动:{base_url}")
try:
send_request(f"{base_url}/books")
send_request(
f"{base_url}/books",
method="POST",
data={"title": "HTTP接口实践"},
)
send_request(f"{base_url}/books")
send_request(f"{base_url}/missing")
finally:
# 即使某次请求失败,也必须释放监听端口并等待后台线程退出。
server.shutdown()
server.server_close()
server_thread.join()
print("本地服务已关闭。")
if __name__ == "__main__":
main()
@@ -0,0 +1,56 @@
# 第5-6课练习:HTTP接口调试与综合实践
#
# 本文件只提供题目、预期结果、自查清单和验收标准,不包含参考答案或代码骨架。
# 综合任务:设计并调试图书管理HTTP接口
#
# 第一部分:接口设计
# 1. 设计查询图书列表、查询详情、新增、局部修改和删除接口。
# 2. 列表接口支持keyword和page查询参数。
# 3. 为每个接口写出方法、路径、请求体、成功状态码和失败状态码。
# 4. 不需要连接数据库,可以使用内存列表保存本次运行的数据。
# 第二部分:服务端实现
# 1. 使用http.server实现GET /books和GET /books/{id}。
# 2. 实现POST /books,接收JSON格式的title与author。
# 3. 校验JSON格式和必填字段,返回合理的4xx状态码。
# 4. 创建成功返回201、图书JSON和Location响应头。
# 5. 未知路径返回404;服务器只绑定127.0.0.1和随机端口。
# 第三部分:客户端调试
# 1. 使用urllib.request依次调用列表、详情、新增和未知路径。
# 2. 输出请求方法、完整URL、状态码、Content-Type和响应体。
# 3. 额外发送一次无效JSON或缺少书名的请求,检查错误响应。
# 4. 使用try/finally保证测试完成或失败后都关闭服务器。
# 预期关键结果:
# 1. 初始列表至少包含一条图书;
# 2. 新增接口返回201和新图书编号;
# 3. 再次查询列表能够看到新增图书;
# 4. 查询不存在的编号返回404;
# 5. 缺少必填字段返回400或422;
# 6. 最终输出“本地服务已关闭”。
# 自查清单:
# 1. 服务是否只监听127.0.0.1,而不是暴露到局域网?
# 2. Content-Length是否按UTF-8字节长度计算?
# 3. JSON响应是否声明application/json与UTF-8?
# 4. 是否区分JSON语法错误、字段错误和资源不存在?
# 5. 客户端是否同时检查状态码、响应头和响应体?
# 6. 是否在finally中关闭服务并等待线程结束?
# 7. 是否能画出客户端、HTTP服务、业务数据之间的调用方向?
# 最终验收标准:
# 1. practice.py通过语法检查并能连续运行两次;
# 2. 完成至少两个GET接口和一个POST接口;
# 3. 新增后列表数据发生预期变化;
# 4. 成功与失败响应的状态码和JSON结构合理;
# 5. 不访问公网、不连接生产数据库、不长期占用固定端口;
# 6. 能使用接口调试工具复现同样请求;
# 7. 能解释这些接口迁移到FastAPI后哪些业务设计保持不变。