Featured image of post ASGI到底是什么?从一头雾水到豁然开朗的理解之旅
科技 Web开发

ASGI到底是什么?从一头雾水到豁然开朗的理解之旅

跟随一个开发者的心路历程, 揭开ASGI的神秘面纱, 从框架疑惑到理解现代Python异步Web服务器底层协议的全过程

scope的隐身术: 让人抓狂的神秘变量

故事要从我开发一个需要实时推送数据的小功能说起。没啥花头, 就是想服务器一有新消息, 客户端立马能收得到。于是我自然而然地用上了FastAPI, 结果一头扎进文档, 发现满篇都是scope

没错, 就是这个scope。看文档到哪都能看到它的影子, 教程代码里有它, Stack Overflow的解答也是默契地假设你已经会用它了。但最让我抓狂的是: **我在自己的代码里根本没看到过它的踪影!**路由、WebSocket端点, 统统没露面。scope去哪了?难道是传说中的“空气变量”?

更离谱的是, receivesend也差不多, 大家都在谈, 一到自己写代码就消失了。AI助手讲解得头头是道, 但我死活没法把这些虚空中的参数和自己写的Litestar、Advanced Alchemy代码联系起来。

这时候我才幡然醒悟: 原来我一直在学各种框架, 却从没搞懂这些框架的地基是什么。只有“刨地三尺”, 搞懂Python异步Web服务器最底层的原理, 才能明白这些玄学参数到底干嘛用的——不是FastAPI的套路, 也不是Django Channels的玩法, 而是底层协议的真相。

于是, 这就是我的ASGI探险记。如果你也曾经对ASGI、WebSocket一头雾水, 觉得Python异步Web开发跟传统Flask/Django完全不是一个画风, 这篇文章就是为你写的。读完你就能明白ASGI到底是什么, 它为啥存在, 以及它如何优雅统一了HTTP、SSE、WebSocket等各种通信方式。

第一重顿悟: ASGI其实就是个“君子协定”

最让我拍案叫绝的发现是: ASGI不是框架, 也不是库, 而是一个规范——规定了Web服务器如何跟你的Python应用对话。

就像电源插座一样, 国家标准一出, 不管是小米还是海尔, 都能造出插得上的电器。ASGI也是一样, 定义了Web服务器和异步Python应用之间的“对接协议”。

整个协议简单到极致, 只有三个参数:

async def app(scope, receive, send):
    # 应用逻辑

就是这么朴实无华。无论你用FastAPI、Starlette、Django Channels, 还是自己手撸, 从本质上看, ASGI应用就是一个可调用对象, 接收这三个参数:

  • scope: 一个字典, 包含了连接的元信息。可以理解为“场景说明书”, 告诉你这是啥连接, 谁发起的, 要干啥。
  • receive: 一个异步函数, 你调用它能收到客户端发来的消息。就像查邮箱收信。
  • send: 另一个异步函数, 用它给客户端发消息。等于往邮箱里塞信寄出去。

更妙的是, 这套“三件套”通吃所有场景——普通HTTP、流式Server-Sent Events、双向WebSocket, 甚至连应用的启动和关闭都能用这一套。只要scope说明了“这次玩哪种花样”, 你就能对症下药。

不过刚开始让我迷糊的是——“会话”到底指啥?一次请求?一场对话?服务器的整个寿命?

答案其实很灵性: **得看scope的type。**接下来, 咱们一个个举例聊聊。

HTTP: 简单明了的一次性买卖

先从最熟悉的HTTP说起。你可能早就用惯了, 但不一定知道ASGI在背后默默打工。

当有客户端发起HTTP请求时, ASGI会生成一个scope, 大致长这样:

{
    "type": "http",
    "method": "GET",
    "path": "/api/users/123",
    "headers": [...],
    "query_string": b"format=json",
    "client": ("192.168.1.5", 54321),
    "server": ("10.0.0.1", 8000),
}

注意type: "http", 这就是告诉你, 这是一锤子买卖, 请求-响应搞定即走。

流程如下:

  1. 客户端发请求 → 服务器生成scope并调用你的app
  2. 你的应用用receive()收请求体 (可能是分段的)
  3. send()发响应头 (状态、头信息)
  4. 再用send()发响应体 (具体内容)
  5. scope消失, 连接结束

这就像自动贩卖机: 你投币、选饮料、拿饮料, 完事走人。贩卖机根本不记得你是谁。

重点: HTTP在ASGI里是无状态的。每次请求独立, scope活不了一秒, 处理完就销毁。

举个响应例子:

# 先发响应头
await send({
    "type": "http.response.start",
    "status": 200,
    "headers": [[b"content-type", b"application/json"]],
})

# 再发响应体
await send({
    "type": "http.response.body",
    "body": b'{"user": "Shane", "id": 123}',
})

短平快, 简单明了。这是最基础的模式。

Server-Sent Events: 服务器单向直播

如果说HTTP是买瓶饮料就走, Server-Sent Events (SSE) 则像是你在贩卖机旁边蹲着, 机器一上新立马通知你。

SSE本质上还是HTTP, 但不再遵循一次性请求-响应的老路, 而是:

  1. 客户端发起HTTP请求
  2. 服务器回响应头 (记得加Content-Type: text/event-stream)
  3. 连接保持不断开
  4. 服务器随时发送数据
  5. 直到有一方主动断开

scope依旧是type: "http", 但玩法变了。你就像打电话点了“今日特价”, 电话那边不停播报新菜品上架。

例子:

await send({
    "type": "http.response.start",
    "status": 200,
    "headers": [[b"content-type", b"text/event-stream"]],
})

# 持续推送
await send({
    "type": "http.response.body",
    "body": b"data: {\"new_order\": 42}\n\n",
    "more_body": True,
})

await send({
    "type": "http.response.body",
    "body": b"data: {\"new_order\": 43}\n\n",
    "more_body": True,
})

注意more_body: True, 意思就是“别断, 还会有!”

SSE还是HTTP, 只是持久化了连接, 单向推送。客户端啥都不回, 只是一直听。

适合场景:

  • 实时仪表盘
  • 通知推送
  • 股票行情
  • 进度更新
  • 只需单向推送, 不需要客户端反馈

比WebSocket简单, 功能比普通HTTP强。

WebSocket: 双向持久对话

轮到WebSocket出场, ASGI的奥义也就此揭晓。

WebSocket跟HTTP/SSE都不一样, 因为它是双向、持久、状态化的。不是你一句我一句的买卖, 而是持续的对话, 谁都可以主动说话。

如果HTTP像写信, SSE像广播, WebSocket就是打电话

scope结构:

{
    "type": "websocket",
    "path": "/ws/chat/room-42",
    "headers": [...],
    "query_string": b"user=shane",
    "client": ("192.168.1.5", 54322),
    "server": ("10.0.0.1", 8000),
}

type: "websocket", 完全不同的玩法。

消息类型:

客户端到应用:

{"type": "websocket.connect"}       # 客户端请求建立连接
{"type": "websocket.receive", "text": "Hello!"}  # 客户端发消息
{"type": "websocket.disconnect"}    # 客户端断开

应用到客户端:

{"type": "websocket.accept"}        # 应用同意连接
{"type": "websocket.send", "text": "Welcome!"}   # 应用发消息
{"type": "websocket.close"}         # 应用断开

有木有发现, HTTP是你问我答, WebSocket是持续在线, 谁都能随时插话。

scope也能活很久, 可能几秒, 也可能几小时。你可以在一个scope里互发上百条消息, 状态一直保留着。

适合场景:

  • 聊天应用
  • 协同编辑 (比如在线文档)
  • 在线游戏
  • 实时推送
  • 需要真·双向实时通信的场景

但这里还有个经典疑问……

WebSocket握手的迷思: 升级的秘密

初学时我被“WebSocket升级”整蒙了: WebSocket不是HTTP, 怎么开始的?

答案让人大跌眼镜: WebSocket一开始就是个HTTP请求。

啥?不信?往下看。

WebSocket的设计就是要兼容原有Web基础设施 (端口、代理、SSL等), 所以不是重新发明协议, 而是走“先HTTP, 后升级”的套路。

流程:

1. 客户端发特殊HTTP请求:

GET /ws/chat HTTP/1.1
Host: example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: X3JJHMbDL1EzLkh9GBhXDw==
Sec-WebSocket-Version: 13

就是普通GET, 只是加了些特殊头, 表示“我要升级成WebSocket”。

2. 服务器回101响应:

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: <computed response>

101 Switching Protocols就是同意升级, 从此用WebSocket协议交流。

3. 协议切换, TCP连接保持

底层TCP连接没断, 但HTTP已结束, 之后两边说的是WebSocket的“暗号”。

你可以这么理解: 打电话给餐厅 (HTTP), 请求转接到某个包间 (Upgrade), 服务员说“马上接通” (101响应), 之后你就跟包间的人直接聊天了 (WebSocket) 。线路没变, 协议变了。

ASGI在握手流程中扮演什么角色?

最让我一头雾水的是: ASGI是在哪一环介入的?

答案是: ASGI只在协议升级 (握手) 之后才介入。

分层解释:

服务器层 (Uvicorn/Hypercorn等, 自动完成) :

  • 接收HTTP升级请求
  • 校验WebSocket头
  • 回复101响应
  • 切换TCP连接协议

ASGI应用层 (你的代码) :

  • 服务器创建WebSocket scope
  • 发送websocket.connect消息给你的应用
  • 你来决定: 同意还是拒绝连接 (比如做认证)

这样一分层, 豁然开朗:

  • 协议层 (服务器): 判断是不是有效的WebSocket升级请求
  • 业务层 (应用): 判断要不要让这个连接进来 (比如权限校验)

握手早就由服务器搞定了, 你收到websocket.connect时, 客户端已经连接上, 等你点头。你发websocket.accept只是“业务同意”, 不是协议握手。

代码示意:

# 收到握手后服务器转发的消息
message = await receive()
# message = {"type": "websocket.connect"}

# 业务判断, 比如认证
if user_is_authenticated:
    await send({"type": "websocket.accept"})
    # 开始双向消息循环
else:
    await send({"type": "websocket.close", "code": 1008})
    # 拒绝连接

理解了这点, FastAPI、Starlette各种WebSocket玩法就都清楚了——协议交给服务器, 业务你说了算。

你和客户端都能随时断开连接, 谁也不是大爷, 谁都能说“我不玩了”。

Lifespan: 横空出世的第四种scope

就在我以为ASGI只有HTTP/SSE/WebSocket三种模式时, 突然冒出来个lifespan, 彻底打乱了我的世界观。

原以为lifespan是“一次连接的生存期”, 比如WebSocket连着多久。事实证明, 我错得离谱

lifespan根本跟一次连接没关系, 而是整个应用的生命周期——服务器进程从启动到关闭。

你可以这么总结:

  • HTTP scope: 活一次请求 (百毫秒)
  • SSE scope: 活一次流式会话 (几分钟到几小时)
  • WebSocket scope: 活一次双向会话 (几秒到几小时)
  • Lifespan scope: 活整个应用 (几天到几个月)

服务器启动时, 先创建lifespan scope, 给你发startup消息;关机时再发shutdown消息。

scope结构:

{
    "type": "lifespan",
}

消息类型:

{"type": "lifespan.startup"}
{"type": "lifespan.shutdown"}

啥用?适合那些只需要在应用启动或关闭时做一次的事:

  • 数据库连接池初始化
  • 机器学习模型加载到内存
  • 启动后台任务调度器
  • 缓存预热
  • 监控/指标采集器初始化
  • 优雅关闭各种资源

打个比方: HTTP/SSE/WebSocket是你招待每个顾客, lifespan是你每天开店 (准备食材、开火) 和打烊 (关门、清扫) 。

你肯定不想每次请求都加载2G模型吧?只需要在lifespan.startup一次性加载, 所有请求复用。同理, 关机时优雅关闭连接池。

FastAPI里的@app.on_event("startup")@app.on_event("shutdown"), 其实就是在帮你处理这些lifespan消息。

lifespan和其他三种scope是正交的, 不是通信场景, 而是应用本身的生命周期管理。

ASGI的终极思维模型

经历了这些疑惑和顿悟, 我终于形成了这样一个清晰的思维框架:

ASGI是个统一的接口, 用来描述“通信会话”, 会话的类型有:

scope类型存活时间通信方向典型场景
HTTP毫秒级请求→响应API调用、页面加载、表单提交
SSE分钟到小时服务器→客户端实时推送、仪表盘、通知
WebSocket秒到小时双向聊天、协作、游戏、实时控制
Lifespan应用生命周期N/A启动/关闭、资源管理

这四种都用同一个接口: async def app(scope, receive, send)

scope字典告诉你遇上了啥会话, receive/send让你与之交互。合同不变, 玩法多变。

这样一来, 中间件也能通吃所有类型。比如认证中间件, 可以根据scope类型分别处理HTTP、WebSocket、SSE。日志中间件也是一样, 写一次通用所有。

这就是为啥你一旦理解ASGI, FastAPI用起来就很顺畅。所有路由、WebSocket端点、启动事件, 本质上都是ASGI应用, 遵守同一个简单协议。

隐身的scope其实一直都在, 只是框架帮你藏起来了。等你需要深入理解WebSocket、优雅管理资源、写中间件的时候, 就会发现ASGI这层基础有多香。

从迷茫到通透

刚开始时, scope像个幽灵, 文档总假设你早就会。现在我终于明白: 它不过是个用来描述通信模式的字典。

美妙的是, 这种理解一通百通。读FastAPI的WebSocket例子, 能明白底层怎么回事;Starlette的启动事件, 其实就是lifespan消息的处理;排查连接断了, 也能思考是HTTP scope正常结束, 还是WebSocket意外关闭。

一旦下潜到“协议层”, 理解ASGI底层, 而非只会用框架, 迷雾就散了, 自信心up!

如果你正在开发实时功能、异步API, 或者单纯想吃透现代Python Web后端, 希望这段旅程能帮你像我一样拨云见日。下次看到文档里的scope, 别慌, 你已经明白它的底细。

说不定哪天你也会撸出极简ASGI应用、写出专属中间件, 或者终于搞懂框架背后的魔法。

ASGI的兔子洞随时欢迎你深入探索——但至少现在, 你已经站在了入口。


(文章原创, 部分内容用AI润色。)

© 2022 - 2026 张欣耕

保留所有权利