☰
Channels 2.0.1 发布详解:异步 WebSocket 消费者、Origin 校验中间件与 URLRouter 路由修复
2026/10/7 2:41:59 网站建设 项目流程
  • 后端
  • WebSocket
  • 异步编程

【免费下载链接】channels

Developer-friendly asynchrony for Django

项目地址:https://gitcode.com/gh_mirrors/ch/channels
点击查看免费下载

Channels 2.0.1 是 Channels 2.0 系列的首个补丁版本,在保持 2.0 大版本重写成果的基础上,为 WebSocket 场景补充了原生异步通用消费者,将残缺的allowed_hosts_only装饰器替换为完整可用的 Origin 校验中间件,并修复了URLRouter在多个路由场景下无法越过首个 URL 继续匹配的缺陷。本文以官方发布说明为骨架,结合仓库源码与测试用例,逐一拆解这三项变更的用法、原理与升级注意点,帮助读者安全升级并正确使用异步 WebSocket 消费者与 Origin 校验能力。

版本定位与升级须知

Channels 2.0.1 是一个patch 级别的发布(发布说明原文),其定位是:在 2.0.0 的大规模重构(由 Channels 1 的"消息通过 channel layer 传输"改为"应用直接运行在协议服务器内部",并全面转向 asyncio 异步运行时,详见 2.0.0 发布说明)基础上,补充少量小特性并修复一个 URL 解析缺陷。

升级时有一个官方反复强调的注意点:更新 Channels 的同时,务必同步升级其依赖asgiref与daphne。因为这两个包各自也会发布自己的 bugfix 更新,而部分表面上看像 Channels 自身问题的 bug,实际根源在 asgiref 或 daphne 中。仅单独升级 Channels 而保留旧版依赖,可能在运行时遇到难以定位的异常。

本次 2.0.1 的向后不兼容变更为None,即按官方说明,从 2.0.0 升级到 2.0.1 不需要修改应用代码。

新特性一:原生异步 WebSocket 通用消费者

2.0.1 为 WebSocket 场景引入了两个新的异步版本通用消费者:

  • channels.generic.websocket.AsyncWebsocketConsumer
  • channels.generic.websocket.AsyncJsonWebsocketConsumer

它们与 2.0 已有的同步版WebsocketConsumer、JsonWebsocketConsumer在方法签名上一一对应,但所有处理方法均为协程,self.send等操作也全部变为可await的异步调用。相关背景与用法可参见 Consumers 文档。

用法示例

一个最基本的异步 WebSocket 消费者如下(取自 Consumers 文档):

from channels.generic.websocket import AsyncWebsocketConsumer class MyConsumer(AsyncWebsocketConsumer): async def connect(self): # 连接建立时调用 # 接受连接: await self.accept() # 或接受连接并指定服务端选中的子协议 # 客户端声明的子协议列表位于 self.scope['subprotocols'] await self.accept("subprotocol") # 拒绝连接: await self.close() async def receive(self, text_data=None, bytes_data=None): # 每个帧到达时调用,text_data 与 bytes_data 二选一 # 发送文本帧: await self.send(text_data="Hello world!") # 发送二进制帧: await self.send(bytes_data="Hello world!") # 强制关闭连接: await self.close() # 或携带自定义 WebSocket 错误码: await self.close(code=4123) async def disconnect(self, close_code): # 连接关闭时调用 pass

AsyncJsonWebsocketConsumer则自动完成 JSON 的编解码:你只需实现receive_json(self, content)接收已解码的 JSON 对象,用await self.send_json(content)发送数据,框架会负责json.loads/json.dumps。若需自定义编解码逻辑,可覆写encode_json与decode_json类方法——在异步版本中,这两个方法同样是协程(async def),见 channels/generic/websocket.py。

源码级流程拆解

从 channels/generic/websocket.py 的AsyncWebsocketConsumer实现可以看到,异步版本完整复刻了同步版的语义:

  • 连接建立阶段:协议服务器下发websocket.connect消息后,框架调用websocket_connect,其内部尝试调用用户实现的connect();若connect()抛出AcceptConnection则自动accept(),抛出DenyConnection则自动close()(异常定义见 channels/exceptions.py)。
  • 帧收发阶段:websocket_receive根据消息中text与bytes键区分文本帧与二进制帧,分别以text_data/bytes_data关键字传入receive();send()则按传入内容构造{"type": "websocket.send", "text": ...}或{"type": "websocket.send", "bytes": ...}事件,且支持close=True时发送后立即关闭连接。
  • 断开清理阶段:websocket_disconnect在调用用户disconnect()后会执行await aclose_old_connections()(来自 channels/db.py)并抛出StopConsumer以干净地终止 ASGI 应用——这正是 Consumers 文档 中强调的"关闭后必须抛出StopConsumer"约定的内置实现,避免应用因超时被 Daphne 强杀并产生警告。

另外注意:本版本中groups类属性默认初始化为空列表(见 channels/generic/websocket.py),而"连接时自动加入 group、断开时自动退出"的内置组管理是在后续 2.1.0 版本中引入的(见 2.1.0 发布说明),不属于 2.0.1 的能力范围。

何时选择异步消费者

官方在 Consumers 文档 中给出的选型建议是:

  • 默认使用同步版SyncConsumer/WebsocketConsumer:它们运行在线程池中,可安全调用 Django ORM 等同步代码,不会阻塞整个服务器事件循环;
  • 仅在确定收益时使用异步版:即你要处理的是可并行化的长耗时任务,且只调用异步原生库(例如用 HTTPX 并行拉取 20 个页面)。若在AsyncConsumer中调用慢速同步函数,会阻塞整个事件循环;
  • 若确实需要从异步消费者中调用同步函数,可使用asgiref.sync.sync_to_async;调用 ORM 则应使用database_sync_to_async适配器或 ORM 的异步方法(aget等)。

新特性二:OriginValidator 与 AllowedHostsOriginValidator 中间件

2.0.1 移除了在 2.0 中"意外混入但实际不可用"的allowed_hosts_only装饰器,取而代之的是一组全新的ASGI 中间件:

  • channels.security.websocket.OriginValidator
  • channels.security.websocket.AllowedHostsOriginValidator

相关背景与用法详见 Security 文档。

为什么要校验 Origin

WebSocket 握手本身是一次 HTTP 请求,会携带用户站点的 Cookie 与会话。这意味着任意第三方网站都可以向你的域名发起 WebSocket 连接,并在连接中携带受害者浏览器里的 Cookie——存在跨站请求伪造(CSRF)风险。若你的 WebSocket 会下发私密数据,就必须限制允许发起连接的站点。这就是 Origin 校验的用途:检查握手请求的Origin头是否在白名单内。

用法示例

from channels.security.websocket import OriginValidator application = ProtocolTypeRouter({ "websocket": OriginValidator( AuthMiddlewareStack( URLRouter([ ... ]) ), [".goodsite.com", "http://.goodsite.com:80", "http://other.site.com"], ), })

白名单元素支持两种形态:

  • 仅域名,例如.allowed-domain.com(点前缀表示该域名及其所有子域);
  • 完整 Origin,格式为scheme://domain[:port],例如http://allowed-domain.com:80。端口可省略但官方建议显式给出,因为中间件会按 Origin 规范比对协议与端口。

如果想放行任意来源,可直接使用"*"。

AllowedHostsOriginValidator:直接复用 ALLOWED_HOSTS

绝大多数情况下,允许的域名集合与 Django 的ALLOWED_HOSTS设置一致(后者本身对Host头做类似的安全校验)。此时不必重复声明列表:

from channels.security.websocket import AllowedHostsOriginValidator application = ProtocolTypeRouter({ "websocket": AllowedHostsOriginValidator( AuthMiddlewareStack( URLRouter([ ... ]) ), ), })

从 channels/security/websocket.py 的实现可以看到,AllowedHostsOriginValidator是一个工厂函数:它读取settings.ALLOWED_HOSTS构造OriginValidator,并且在DEBUG模式且ALLOWED_HOSTS为空时自动放行localhost、127.0.0.1、[::1],这与 Django 自身的 Host 校验行为保持一致。

匹配逻辑源码剖析

OriginValidator是标准 ASGI 中间件(async def __call__(self, scope, receive, send)),整体流程(见 channels/security/websocket.py):

  1. 校验scope["type"] == "websocket",非 WebSocket 连接直接抛ValueError;
  2. 从scope["headers"]中查找b"origin"头并urlparse解析;
  3. 若校验通过,把控制权交给被包裹的应用;否则交给WebsocketDenier(一个直接close()拒绝连接的内部消费者,见同文件 channels/security/websocket.py)。

核心匹配逻辑match_allowed_origin(channels/security/websocket.py)值得注意的几点:

  • 点前缀通配:域名以.开头时,通过 Django 的is_same_domain匹配该域名及其所有子域。注意*.example.com这种写法不再支持,必须使用.example.com;
  • 协议与端口比对:get_origin_port会为http/ws补默认端口 80、为https/wss补默认端口 443(channels/security/websocket.py),因此http://allowed-domain.com与http://allowed-domain.com:80视为等价;
  • 空 Origin 或非法 Origin 一律拒绝(除非白名单含"*")。

仓库的 tests/security/test_websocket.py 提供了完整的正反用例佐证上述行为:例如["allowed-domain.com"]拒绝来自http://bad-domain.com的连接;[".allowed-domain.com"]接受http://www.allowed-domain.com;["*"]时无头连接也放行;空白名单与空 Origin 头、非法 Origin 头均被拒绝。

提示:域名字段格式在后续 2.1.0 中进一步规范化(*.example.com彻底弃用,统一用.example.com),见 2.1.0 发布说明。本仓库源码与测试均已按.domain格式实现与验证。

修复:URLRouter 无法越过首个 URL 继续匹配的问题

2.0.1 修复了URLRouter的一个解析缺陷:在某些情况下,路由无法匹配到列表中第一个 URL 之后的条目。修复的同时,官方新增了一整套 URL 解析测试套件,防止回归。

源码中的修复痕迹

从 channels/routing.py 的URLRouter实现可以看到其完整解析机制:

  • 路由以 Django 的path()/re_path()对象为输入,逐个与scope中的路径匹配;
  • 最外层路由器会处理scope["root_path"](如挂在反向代理子路径下时),并剥掉路径开头的/,与 Django URL 处理保持一致;
  • 匹配成功后,把剩余未匹配部分写入scope["path_remaining"],把捕获的位置参数与关键字参数合并进scope["url_route"],再调用目标应用——这正是嵌套路由(及后续 2.1 的URLRouter嵌套复用)得以实现的基础;
  • 外层路由器未匹配时抛ValueError;而path_remaining已存在(即处于内层)时抛Resolver404,允许解析回退到外层继续尝试。

需要留意的是,在 2.0.1 版本中,URLRouter之间的嵌套(即"外层剥掉匹配部分、内层只匹配剩余部分")是下一个版本 2.1.0 引入的行为(见 2.1.0 发布说明),2.0.1 的修复聚焦于单个URLRouter内部多路由场景下"首路由之外的路由无法命中"这一缺陷。

测试套件佐证

仓库的 tests/test_routing.py 覆盖了丰富的解析场景,可作为该 bug 已修复并防回归的证据:

  • test_url_router:验证path("")、path("foo/")、re_path(r"bar")等基础匹配,位置参数(url_route["args"])、关键字参数(url_route["kwargs"])、路由默认参数(kwargs={"default": 42})以及root_path处理;
  • test_url_router_path:验证path()风格的路由与类型转换(<int:year>得到整数 2012);
  • test_path_remaining:验证内层路由器无匹配时,解析会继续回退到外层路由器并命中后续路由——这正是"越过首个 URL 继续匹配"语义的回归保障;
  • test_invalid_routes:确认在URLRouter中使用 Django 的include()会抛出ImproperlyConfigured(include() is not supported in URLRouter.),引导用户改用嵌套URLRouter实例。

升级建议与小结

综合来看,2.0.1 是一次"小而稳"的补丁发布,升级动作可归纳为:

  1. 同步升级依赖:将channels、asgiref、daphne三者的版本一并更新,避免依赖不匹配导致的隐性 bug;
  2. 迁移 Origin 校验代码:若你曾在 2.0 中尝试使用allowed_hosts_only装饰器,请改用以OriginValidator/AllowedHostsOriginValidator包裹 WebSocket 应用的中间件写法(示例见 Security 文档),并确认白名单使用.domain点前缀格式;
  3. 验证 WebSocket 路由:升级后运行仓库自带的 路由测试 思路,重点回归"多个路由并存、首个路由不命中时后续路由能否正确接管"的场景;
  4. 新特性按需采用:AsyncWebsocketConsumer与AsyncJsonWebsocketConsumer仅在确定受益于原生异步、且全部依赖为异步原生库时使用;否则继续使用同步版消费者。

至此,读者应能完整理解 Channels 2.0.1 的三项核心变更——异步 WebSocket 消费者、Origin 校验中间件、URLRouter 解析修复——的用法、源码原理与升级路径,并可直接对照 Consumers 文档、Security 文档 与 Routing 文档 继续深入实践。

  • 后端
  • WebSocket
  • 异步编程

【免费下载链接】channels

Developer-friendly asynchrony for Django

项目地址:https://gitcode.com/gh_mirrors/ch/channels
点击查看免费下载
上一篇:cytoscape.js 集合操作详解:eles.union() 合并元素集合的用法与底层实现
下一篇:深入解析 Erlang/OTP 虚拟机 Thread Progress 机制:无锁并发下的线程进度追踪与内存屏障

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询