djangochannelsrestframework 消息协议详解:action 与 request_id 如何驱动 WebSocket 双向通信
【免费下载链接】djangochannelsrestframeworkA Rest-framework for websockets using Django channels-v4项目地址: https://gitcode.com/gh_mirrors/dj/djangochannelsrestframework
djangochannelsrestframework(简称 DCRF)是一个基于 Django Channels v4 的 WebSocket REST 框架,它把 DRF(Django REST Framework)中熟悉的接口风格搬到了 WebSocket 之上。本文将从消息协议入手,用最通俗的方式拆解djangochannelsrestframework 消息协议中两个最核心的字段:action(动作)与request_id(请求标识),带你看懂它们如何像"地址"和"快递单号"一样,驱动起 WebSocket 的双向通信。无论你是刚接触 WebSocket 的新手,还是想深入源码的进阶用户,这篇文章都能帮你快速建立完整的协议认知。
什么是 djangochannelsrestframework 消息协议?🧩
在传统 HTTP 世界里,一次请求对应一次响应,结构天然清晰。但 WebSocket 是一条长连接,客户端和服务端可以在任意时刻互相发消息,那么问题来了:
- 服务端怎么知道客户端想"干什么"?
- 多条消息并发时,客户端怎么知道哪条响应对应哪条请求?
djangochannelsrestframework 消息协议就是为此设计的答案。它规定了统一的消息外壳:客户端发出去的每条消息都包含action和request_id,服务端返回的每条响应也都原样带上这两个字段。核心实现就在项目源码的 djangochannelsrestframework/consumers.py 中——AsyncAPIConsumer类是整个协议的中枢。
一条最典型的客户端消息长这样:
{ "action": "list", "request_id": 150060530 }对应服务端的响应:
{ "action": "list", "errors": [], "response_status": 200, "request_id": 150060530, "data": [{"id": 1, "username": "test1"}, {"id": 2, "username": "test2"}] }是不是感觉和 HTTP 的"URL + 状态码 + 响应体"很像?没错,action 就是"URL",response_status 就是"状态码",request_id 则是让这一切可以异步对号入座的"快递单号"。
action 字段:WebSocket 消息的路由入口 🚪
action 的作用只有一个:告诉服务端你想调用哪个功能。它相当于 RESTful API 里的端点(endpoint),但通过 WebSocket 传输。
消息是如何被路由的?
当你通过 WebSocket 发来一条 JSON 消息时,服务端的处理流水线是这样的(见 consumers.py 的receive_json方法):
- 先从消息中取出
request_id; - 再通过
get_action_name方法取出action字段; - 交给
handle_action进行权限校验和分发; - 找到对应的方法执行,并把结果通过
reply方法回传。
如果消息里没有 action 字段,服务端会抛出ActionMissingException(定义在 djangochannelsrestframework/exceptions.py),并返回 405 状态码的错误响应——这就像访问了一个不存在的 URL 一样。
内置 action 一览:开箱即用的 CRUD
DCRF 在 djangochannelsrestframework/mixins.py 中内置了一整套类 DRF 的 action,几乎覆盖了日常 80% 的需求:
| action | 对应功能 | 类似 HTTP 方法 | 典型响应码 |
|---|---|---|---|
create | 新建数据 | POST | 201 |
list | 列表查询 | GET | 200 |
retrieve | 单条详情 | GET | 200 |
update | 整体更新 | PUT | 200 |
patch | 局部更新 | PATCH | 200 |
delete | 删除数据 | DELETE | 204 |
比如前端要"创建用户",只需发送:
{ "action": "create", "request_id": new Date().getTime(), "data": {"username": "test", "password": "123456"} }服务端CreateModelMixin.create方法会完成序列化校验并落库,然后返回 201 和新数据的完整序列化结果。整个过程对前端来说就是"发一条消息、收一条消息",非常直观。
自定义 action:用 @action 装饰器扩展协议
内置 action 不够用?你完全可以自定义。在 djangochannelsrestframework/decorators.py 中,@action()装饰器会把任意方法"登记"进消费者的 action 路由表(available_actions),这一步由APIConsumerMetaclass元类在类创建时自动完成。
from djangochannelsrestframework.decorators import action class MyConsumer(AsyncAPIConsumer): @action() async def delete_user(self, request_id, user_pk, **kwargs): ...之后前端发送{"action": "delete_user", "request_id": 42, "user_pk": 82}即可触发。这个装饰器还支持两个高级选项:
@action(atomic=True):用于同步方法,让操作包在数据库事务中;@action(detached=True):用于异步方法,让耗时操作(如请求外部 API)脱离主循环运行,期间连接仍能处理其他消息。
request_id 字段:让异步应答"对号入座" 📬
如果说 action 是"地址",那request_id 就是"快递单号"。它是 djangochannelsrestframework 消息协议中最容易被新手忽略、却最关键的字段。
为什么需要 request_id?
WebSocket 是异步的:客户端可以连续发送多条消息,服务端处理耗时各不相同,返回顺序完全无法保证。如果没有 request_id,客户端收到响应时根本不知道这是哪条请求的结果。而 DCRF 的约定是:服务端返回的每条响应,都会原样携带触发它的 request_id。
前端收到响应后,只需比对 request_id 就能把异步响应准确关联到对应请求上:
ws.send(JSON.stringify({ action: "list", request_id: 1 })); ws.send(JSON.stringify({ action: "create", request_id: 2, data: {...} })); // 响应即使乱序到达,也能靠 request_id 区分归属在 consumers.py 的reply方法中可以看到,响应的固定结构就是errors / data / action / response_status / request_id五个字段,其中 request_id 的设计初衷,文档注释里写得很明白:"include the request_id if possible as this helps clients link messages they have sent to responses"(尽量带上 request_id,它帮助客户端把收到的消息与发出的消息对应起来)。
观察者模式下 request_id 的高级玩法
request_id 的价值在观察者(Observer)模式下体现得淋漓尽致。DCRF 的 Observer 机制允许客户端订阅某个数据源的变化,一旦数据变化,服务端会主动推送消息——这就是真正的"服务端 → 客户端"推送。
在 djangochannelsrestframework/observer/base_observer.py 的subscribe/unsubscribe方法中,request_id 被用来跟踪订阅关系:当你用某个 request_id 订阅后,服务端会把 request_id 记录在_observer_group_to_request_id映射表中;当订阅的数据发生变化时,服务端就能精确地把更新推送给"发起了那次订阅请求"的连接,甚至能把同一个数据源的多个订阅请求合并成一次推送,大大节省带宽。
一次完整的双向通信:请求与响应拆解 🔄
把前面的知识串起来,一次完整的 djangochannelsrestframework 双向通信是这样的:
- 连接建立:客户端通过
ws://localhost:8000/ws/建立 WebSocket 连接,期间会执行websocket_connect中的权限校验; - 发起请求:客户端发送
{"action": "retrieve", "request_id": 42, "pk": 1},请求单条数据; - 权限与路由:服务端在
handle_action中先做权限检查,再根据 action 找到retrieve方法执行; - 返回响应:执行结果通过
reply打包成统一协议格式返回,携带相同的 request_id; - 持续通信:连接保持打开,双方可继续按同样协议收发消息,直到某一方关闭连接。
值得一提的还有 djangochannelsrestframework/consumers.py 中的DjangoViewAsConsumer和配套的view_as_consumer工具——它能把一个普通 Django View 直接包装成 WebSocket 消费者,让 action 映射到 HTTP 方法(如create→PUT、list→GET),实现"老代码零改造上 WebSocket"的平滑迁移。
异常与错误处理的协议约定 ⚠️
理解消息协议,错误处理同样重要。DCRF 的异常处理遵循统一约定(见handle_exception方法):
- 权限不足:抛出
PermissionDenied,返回 403; - action 不存在:抛出
MethodNotAllowed,返回 405; - 资源找不到:抛出
Http404,返回 404; - 校验失败:序列化器的
is_valid(raise_exception=True)抛出 DRF 的APIException,错误的详细信息会以列表形式放入errors字段返回。
也就是说,无论成功还是失败,响应永远保持同一种协议结构,前端只需解析response_status和errors两个字段,就能统一处理所有场景,无需为异常单独设计协议分支。
常见问题与最佳实践 💡
最后,给初学者的几条实用建议:
- request_id 必须唯一:建议用时间戳、UUID 或自增序号,别用固定值,否则多请求并发时无法区分响应;
- action 命名保持语义化:遵循 REST 风格(create/list/retrieve/update/delete),自定义 action 也尽量用"动词+名词";
- 善用订阅式推送:需要实时刷新数据的场景(如聊天、通知、在线列表),优先使用 Observer 的订阅机制,而不是频繁轮询;
- 权限校验别遗漏:每个 action 都会经过
check_permissions,自定义 action 记得配置好对应的权限类; - 关注 detached 任务:使用
detached=True的长任务会在连接关闭时被自动取消,注意在任务中妥善处理取消异常。
总结 📝
djangochannelsrestframework 消息协议的设计精髓,就是用 action 定义"做什么",用 request_id 关联"哪次请求"。理解了这两个字段,你就掌握了 WebSocket 双向通信的"钥匙":既能实现类 REST 的请求-响应模式,也能通过订阅机制实现服务端主动推送。配合@action装饰器、内置 CRUD mixins 和 Observer 观察者,你可以用极少的代码搭建出功能完整的实时 API——这正是 djangochannelsrestframework 作为"WebSocket 版 DRF"的核心价值。
【免费下载链接】djangochannelsrestframeworkA Rest-framework for websockets using Django channels-v4项目地址: https://gitcode.com/gh_mirrors/dj/djangochannelsrestframework
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考