1424 字
4 分钟
PythonWebpython web
ASGI 协议与请求生命周期:从网络包到路由函数的完整旅程

追一条 HTTP 请求在 Python Web 栈里的完整旅行:socket → HTTP 解析 → ASGI 三要素 (scope/receive/send) → Starlette 路由匹配 → 装饰器 handler → 响应返回。FastAPI 那条 `@app.post` 装饰器到底做了什么,这篇文章讲清楚。

你的面经里有一个问题直接熄火了:"FastAPI 的 @app.post 为什么能处理请求?如果让你自己实现一个类似的框架,怎么设计?"

这个问题不是在考 FastAPI 语法,而是在考你是否理解 Python Web 框架的底层机制。这一篇把整条链路拆开。

Java Web 对比:Java 的 Servlet 容器(Tomcat)启动时会加载 web.xml 或扫描 @WebServlet 注解,建立 URL→Servlet 映射表。请求来了,Tomcat 把 HttpServletRequestHttpServletResponse 传给对应的 Servlet。Python 这边,Uvicorn 扮演 Tomcat 的角色,ASGI 协议扮演 Servlet 规范的角色,Starlette/FastAPI 的 Router 扮演 web.xml 路由表。整条链路是对称的。

1. 请求进门前:Uvicorn 做了什么#

真正的请求入口不是 @app.get("/") —— 在这之前,还有一个 ASGI 服务器在接客。

uvicorn app.main:app --host 0.0.0.0 --port 8000

Uvicorn 启动后做了三件事:

1. 在 0.0.0.0:8000 上监听 TCP socket
2. 用 uvloop(libuv 的 Python 绑定)跑事件循环
3. 从 app.main 模块导入 app 对象(`app = FastAPI()`)

当一个 HTTP 请求到达时:

网络包 → socket 接收 → HTTP/1.1 协议解析(httptools)
→ 把原始报文变成三个 Python 对象:scope, receive, send
→ 传给 app(scope, receive, send)

这三个对象就是 ASGI 协议的核心

2. ASGI 三要素:scope, receive, send#

ASGI 不是框架,不是服务器,而是一份接口规范。它规定了一个 Python 应用必须是一个可调用对象,签名为:

Python3 代码示例
2 lines

三个参数的含义:

参数类型内容类比 Java
scopedict请求的元信息:method、path、headers、query_string、server 等HttpServletRequest 的所有 getter
receiveasync callable服务器提供的异步函数。应用调用 await receive() 来读取客户端发来的请求体。每次调用返回一个数据块:{"type": "http.request", "body": b"...", "more_body": bool}more_body=True 表示后面还有数据,应用需要继续调 receive() 直到 more_body=Falserequest.getInputStream()
sendasync callable服务器提供的异步函数。应用调用 await send({...}) 把响应发回客户端。先发 http.response.start(状态码+响应头),再发 http.response.body(响应体),可以分多次发送HttpServletResponse 的所有 setter

关键理解:receivesend 都是服务器(Uvicorn)提供的,不是应用自己实现的。

Uvicorn 在调用你的 app(scope, receive, send) 之前,已经把这两个函数创建好了:

Uvicorn 内部(简化):
1. 从 TCP socket 读到 HTTP 请求行和请求头 → 填好 scope dict
2. 创建 receive 函数 → 这个函数内部从 socket 的缓冲区读 body 数据
3. 创建 send 函数 → 这个函数内部往 socket 写响应数据
4. 调用 app(scope, receive, send)

所以 receive 的"主语"是应用调用它来收数据,但"实现方"是服务器——是 Uvicorn 定义了 receive 的逻辑(从 socket 读),然后把函数引用传给你的应用。这就是为什么 ASGI 规范里说 receive 是一个 "callable provided by the server":

Python3 点击展开代码
12 lines 展开代码

为什么设计成应用主动调用 receive(),而不是服务器直接把完整 body 放在 scope 里?

三个原因:

  1. 大请求体:如果客户端上传一个 500MB 的文件,把整个 body 放进 scope dict 意味着服务器要先把 500MB 全部读到内存才能调用应用。用 receive() 分块读取,应用可以边收边处理(比如边读边写磁盘)。

  2. 流式上传:客户端可能还在发送数据,服务器不可能"预知"完整的 body。receive() 是异步的——数据到了就返回,没到就等待——应用不需要阻塞等整个 body 收完。

  3. WebSocket:WebSocket 连接是持久的、双向的。连接建立后,客户端随时可能发消息过来。receive() 就是"等下一个消息"的接口——每次调用阻塞直到有消息到达。如果用 scope 传一次性数据,WebSocket 根本没法表达。

Java 对比:Java Servlet 的 HttpServletRequest.getInputStream() 返回一个 ServletInputStream,调用者 .read() 来分块读取——本质上也是应用主动读取。区别是 Servlet 的 read() 是阻塞的(同步),ASGI 的 receive()await 的(异步)。

3. 一个最简 ASGI 应用(不用任何框架)#

你可以不用 FastAPI,用 20 行代码写一个完整的 Web 应用:

Python3 点击展开代码
31 lines 展开代码

用 Uvicorn 跑起来:

uvicorn raw_asgi:app

这就是 FastAPI 的底层。FastAPI 只是在这个基础上加了路由表、Pydantic 校验、依赖注入、自动文档。

4. 路由是怎么注册的:@app.get("/") 的真相#

app = FastAPI() 创建了一个应用对象。它的核心是一个路由表——本质上是一个 Python 字典:

Python3 点击展开代码
8 lines 展开代码

当你写:

Python3 点击展开代码
3 lines 展开代码

装饰器 @app.get("/items/{item_id}") 实际做的事:

  1. 创建一个 APIRoute 对象,包含:路径 /items/{item_id}、HTTP 方法 GET、handler 函数 read_item
  2. 把这个 route 对象注册到 app.router.routes 列表里
  3. 同时解析 item_id: int 类型注解 → 存为路径参数的 Pydantic 校验器

Java Web 对比:这一步等价于 Spring 扫描 @GetMapping("/items/{itemId}") 注解 → 往 RequestMappingHandlerMapping 注册一个映射。FastAPI 的路由表存在 app.router.routes(内存 list),Spring 存在 HandlerMapping(内存 map)。数据结构不同,职责一样。

5. 请求来了怎么匹配:Starlette Router#

当 scope 被传入 FastAPI.__call__(scope, receive, send) 后:

FastAPI.__call__(scope, receive, send)
└→ Starlette Router.__call__(scope, receive, send)
├─ 从 scope 取出 method 和 path
├─ 遍历 self.routes,找匹配的 APIRoute
│ (路径参数在这里被正则匹配并提取)
├─ 找到了 → route.handle(scope, receive, send)
│ → Pydantic 校验路径参数、查询参数、请求体
│ → 解析依赖注入链
│ → 调用你的 handler 函数
│ → 序列化返回值 → send 出去
└─ 没找到 → 404

Starlette 的 Router 是路由匹配的核心。它内部把每个路径模式(如 /items/{item_id})编译成正则表达式,请求来了就逐个匹配。路径声明顺序就是匹配优先级——所以 /users/me 必须写在 /users/{user_id} 前面。

6. 如果让你自己实现一个框架,三步搞定#

面试官问"自己怎么做一个 FastAPI",其实是在问三个核心能力:

第一步:装饰器注册路由表

Python3 点击展开代码
23 lines 展开代码

第二步:ASGI 兼容——让 MiniWeb() 实例能作为 app 传给 uvicorn mini_web:app。只要实现了 __call__(self, scope, receive, send),Uvicorn 就能跑。

第三步:路径参数提取——把 /items/{item_id} 编译成正则 /items/(?P<item_id>[^/]+),匹配时提取 item_id 的值,根据函数签名里的类型注解做转换。

这就是一个最小 Web 框架的骨架。

7. 完整请求生命周期图#

┌─────────────────────────────────────────────────┐
│ 网络层 │
├─────────────────────────────────────────────────┤
│ 1. TCP 包到达 socket │
│ 2. Uvicorn (uvloop) 接收 │
│ 3. httptools 解析 HTTP → scope dict │
├─────────────────────────────────────────────────┤
│ ASGI 协议层 │
├─────────────────────────────────────────────────┤
│ 4. scope = { │
│ "type": "http", │
│ "method": "GET", │
│ "path": "/items/42", │
│ "headers": [(b"authorization", b"...")], │
│ "query_string": b"q=test", │
│ } │
│ 5. receive() → {"type": "http.request", │
│ "body": b'{"name":"Foo"}'} │
├─────────────────────────────────────────────────┤
│ Starlette / FastAPI 层 │
├─────────────────────────────────────────────────┤
│ 6. Router 匹配: GET /items/42 → read_item() │
│ 7. 路径参数提取: item_id = 42 (Pydantic 校验) │
│ 8. 依赖注入链: Depends(get_db) → db session │
│ 9. 中间件洋葱层: CORS → Auth → 计时 → handler │
│ 10. handler 执行 → 返回 {"item_id": 42, ...} │
│ 11. response_model 过滤 │
│ 12. jsonable_encoder 序列化 │
├─────────────────────────────────────────────────┤
│ ASGI 协议层 │
├─────────────────────────────────────────────────┤
│ 13. send({"type": "http.response.start", 200}) │
│ 14. send({"type": "http.response.body", body}) │
├─────────────────────────────────────────────────┤
│ 网络层 │
├─────────────────────────────────────────────────┤
│ 15. Uvicorn 把响应写回 socket │
│ 16. TCP 包 → 客户端 │
└─────────────────────────────────────────────────┘

这张图值得记。面试时被问"请求怎么进来的",从第 1 步讲到第 16 步,没人会觉得你不懂 Web。

专题阅读

PythonWeb

这篇文章属于同一条阅读链。你可以直接在这里切换,不用再回到列表页重新找。

当前进度13 / 16

留言区

留言

欢迎纠错、补充、交流。昵称和评论内容必填;如果你愿意,也可以留下联系方式,仅站主可见。

0

正在加载评论...

0 / 2000

阅读导航

文章目录

当前阅读位置将在这里显示

0 节