聊起 FastAPI 的实时通信能力,很多人第一反应就是 WebSocket。这东西说白了就是让浏览器和服务器之间建立一条双向的、持续在线的通道,不用像 HTTP 那样一问一答地折腾。FastAPI 官方文档专门开了一个 Reference 页面来讲这个 WebSocket 类,内容看着不多,但底层逻辑其实相当扎实。这篇文章就带你把这个类从里到外扒一遍,顺便理清背后那些容易让人犯迷糊的核心概念。
WebSocket 类从哪儿来
先说一个容易被忽略的小知识点,FastAPI 里的 WebSocket 类其实并不是 FastAPI 自己发明的,它是直接从 Starlette 借过来的。你可以这样导入
python
from fastapi import WebSocket
也可以这样
python
from starlette.websockets import WebSocket
两种写法效果完全一样,FastAPI 只是帮你把常用的类暴露在自己的命名空间里,省得你还要记住底层框架的路径。这种设计思路其实贯穿了整个 FastAPI,它本质上是 Starlette 的一层增强封装,加上了依赖注入、数据校验、自动文档这些锦上添花的功能。
这个类的构造函数长这样
python
WebSocket(scope, receive, send)
三个参数都是 ASGI 协议里的标准概念。scope 是一个字典,装着这次连接的所有元信息,比如路径、请求头、客户端地址等等。receive 和 send 是两个可等待的函数,分别用来接收和发送底层的 ASGI 消息。普通开发者基本不用直接摆弄这三个参数,FastAPI 会在路由层帮你实例化好,你只管在函数签名里声明一个 WebSocket 类型的参数就行。
核心属性一览
WebSocket 对象继承自 HTTPConnection,所以很多属性跟普通 HTTP 请求长得很像,这也是它设计上的巧妙之处,让你不用重新学一套 API。
| 属性名 | 作用说明 |
|---|---|
scope |
底层 ASGI 作用域字典,包含连接的原始元数据 |
app |
当前的 FastAPI 应用实例 |
url |
连接的完整 URL,可以取 .path .port .scheme 等子字段 |
base_url |
应用的基础 URL |
headers |
请求头,大小写不敏感的多值字典 |
query_params |
查询参数,例如 websocket.query_params['search'] |
path_params |
路径参数,例如 websocket.path_params['username'] |
cookies |
客户端携带的 Cookie |
client |
客户端的地址信息,含 host 和 port |
state |
一个可以自由挂载数据的容器对象,常用来在依赖之间传值 |
client_state |
客户端当前所处的连接状态 |
application_state |
服务端当前所处的连接状态 |
这里特别提一句 client_state 和 application_state,它们的值都来自一个叫 WebSocketState 的枚举,稍后会详细展开。理解这两个状态字段,对排查连接异常关闭的问题特别有帮助。
连接生命周期,从握手到告别
WebSocket 连接跟打电话有点像,得先拨号接通,聊完了再挂断,中间才是真正传数据的环节。FastAPI 把这个流程拆成了几个清晰的方法。
接受连接,accept
客户端发起连接请求之后,服务端必须主动调用 await websocket.accept() 才算真正建立起通道。这一步很关键,如果你在还没 accept 之前就调用了 close(),Starlette 会自动帮你返回一个 HTTP 403 拒绝响应,这也是很多人踩坑的地方,连接莫名其妙关闭,往往就是因为漏了这一步或者顺序搞反了。
accept 方法还支持传入 subprotocol 和 headers 参数,可以用来做协议协商或者携带自定义响应头,这在做鉴权场景时会很有用,比如通过子协议字段传递 Token。
收发数据,三种口味
数据的收发方法设计得很直白,按数据类型分成了三类,文本、字节、JSON。
发送方向
python
await websocket.send_text(data)
await websocket.send_bytes(data)
await websocket.send_json(data)
接收方向
python
await websocket.receive_text()
await websocket.receive_bytes()
await websocket.receive_json()
JSON 数据默认走文本帧传输,如果你想让它走二进制帧,可以加个参数
python
await websocket.send_json(data, mode="binary")
await websocket.receive_json(mode="binary")
这个细节文档里专门提了一嘴,实际开发中如果对方用的是纯二进制协议对接,这个参数就派上用场了。
异步迭代,省掉手动循环
如果你嫌手写 while True 循环调用 receive_text 麻烦,Starlette 还贴心地提供了异步迭代器版本
python
async for message in websocket.iter_text():
await websocket.send_text(f"Message text was: {message}")
对应的还有 iter_bytes() 和 iter_json(),用法一致。这几个迭代器背后其实是在循环调用对应的 receive_* 方法,一旦捕获到 WebSocketDisconnect 异常,迭代器会自动退出,不用你手动写异常捕获逻辑,代码干净不少。
关闭连接
聊完了总得说再见
python
await websocket.close(code=1000, reason=None)
code 参数遵循 WebSocket 协议规定的状态码,1000 表示正常关闭。你也可以传别的状态码来表达不同的关闭原因,比如认证失败、协议错误等等。
连接状态机,WebSocketState 枚举
前面提到的 client_state 和 application_state 都是 WebSocketState 枚举类型的实例,一共四个取值
- CONNECTING 连接刚建立,还没经过 accept 握手
- CONNECTED accept 之后进入的正常通信状态
- DISCONNECTED 连接已经关闭,不管是主动关闭还是异常断开
- RESPONSE 在握手阶段直接返回了拒绝响应,没走到正常连接
这套状态机的意义在于,框架内部会用它来判断当前该不该允许调用 send 或 receive,防止你在连接已经断开的情况下还傻乎乎地往里塞数据,造成难以排查的异常。
异常处理,WebSocketDisconnect
当客户端主动断开或者网络异常时,receive_text 之类的方法会抛出 WebSocketDisconnect 异常,这也是 FastAPI 官方推荐的断线检测方式
python
from fastapi import WebSocket, WebSocketDisconnect
@app.websocket("/ws")
async def websocket_endpoint(websocket: WebSocket):
await websocket.accept()
try:
while True:
data = await websocket.receive_text()
await websocket.send_text(f"Message text was: {data}")
except WebSocketDisconnect:
print("客户端已断开连接")
这个异常对象带有两个属性,code 和 reason,分别对应关闭时的状态码和文字说明,方便你记日志或者做后续的清理逻辑,比如把这个用户从在线列表里移除。
进阶玩法,握手阶段就拒绝连接
有些场景你压根不想让某个客户端建立连接,比如鉴权失败。这时候 Starlette 提供了一个专门的方法叫 send_denial_response
python
await websocket.send_denial_response(response)
这个方法会在握手阶段就把连接拒掉,并附带一个自定义的响应体,而不是简单粗暴地返回 403。不过要注意,它依赖 ASGI 服务器支持 WebSocket Denial Response 这个扩展,如果服务器不支持,会直接抛出 RuntimeError。在 Starlette 里,你也可以用抛出 HTTPException 的方式来达到同样效果,代码会更简洁一些。
一个完整的实战例子
把上面这些概念串起来,一个带鉴权、带异常处理的聊天室后端大概长这样
python
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
app = FastAPI()
connected_clients: list[WebSocket] = []
@app.websocket("/ws/{username}")
async def websocket_endpoint(websocket: WebSocket, username: str):
await websocket.accept()
connected_clients.append(websocket)
try:
while True:
data = await websocket.receive_json()
for client in connected_clients:
await client.send_json({"from": username, "message": data})
except WebSocketDisconnect:
connected_clients.remove(websocket)
print(f"{username} 已离线")
这段代码里,username 是路径参数,直接从 URL 里取出来,跟普通 HTTP 路由的写法一模一样,这也印证了前面说的,FastAPI 把 WebSocket 的开发体验做得跟普通接口尽量保持一致,学习成本没那么高。
总结一下
FastAPI 的 WebSocket 类骨子里就是 Starlette 那套实现,核心围绕着三件事展开,连接的建立和关闭、数据的收发格式、以及贯穿始终的状态管理。搞懂了 accept receive_* send_* close 这几个方法的调用时机,再配合 WebSocketDisconnect 做好异常兜底,基本上就能应付大多数实时通信场景了。至于更进阶的鉴权拒绝连接、二进制数据传输这些,属于锦上添花,等业务需要的时候再深入研究也不迟。
参考资料
FastAPI Reference WebSockets fastapi.tiangolo.com/reference/w...
FastAPI Advanced User Guide WebSockets fastapi.tiangolo.com/advanced/we...
Starlette Websockets Documentation www.starlette.io/websockets/
Stack Overflow Why does my FastAPI websocket connection close immediately after authentication stackoverflow.com/questions/7...