- 后端
- WebSocket
- 异步编程
【免费下载链接】channels
Developer-friendly asynchrony for Django
Channels 2.1.4 是 2.1 系列的一个 bugfix 版本,重点解决了 Django 中间件重复实例化带来的 HTTP 服务性能瓶颈,同时修复了测试框架静态文件服务、Origin 头校验报错信息、runserver 日志体系等多个问题,并首次允许通用 Consumer 通过channel_layer_alias属性使用非默认的 Channel Layer。本文以官方发布说明为主体,结合当前仓库源码逐条剖析每个变更的实现原理与使用影响,帮助读者理解 Channels 2.x 内部机制,并掌握升级到 2.1.4 前后的行为差异。
版本概况与升级兼容性
2.1.4 属于 Channels 2.1 系列的维护性(bugfix)发布,不包含新功能特性,只针对 2.1 系列已有代码进行缺陷修复与小型改进。版本发布说明中明确列出了"向后不兼容变更(Backwards Incompatible Changes):None",即从 2.1.3 或更早的 2.1.x 版本升级到 2.1.4 不需要修改应用代码,配置、路由、Consumer 的既有写法均保持兼容,可以直接原地升级。
对于仍停留在 2.0.x 的工程,升级到 2.1.4 前仍需遵循 2.1 系列的迁移要求(如ROUTING配置项在 Channels 2 中已被移除),这一点在 channels/layers.py 中仍有明确体现:ChannelLayerManager遇到旧格式配置中的ROUTING键会直接抛出InvalidChannelLayerError。
Django 中间件改为缓存复用,HTTP 服务性能显著提升
本次发布最重要的一条是:
Django middleware 现在被缓存而不是在每个请求时重新实例化,带来了显著的性能提升。此前某些中间件加载需要数秒,导致 Channels 在处理 HTTP 请求时几乎不可用。
问题根源:中间件按请求实例化
在早期实现中,Channels 会在每个 ASGI 请求(scope)到来时重新创建 Django 中间件实例。中间件的__init__中如果包含耗时操作(例如加载模板引擎、初始化数据库连接池、解析大型配置文件),那么每个 HTTP 请求都要重复付出这些初始化成本,部分中间件初始化耗时甚至高达数秒,直接导致 Channels 的 HTTP 服务在负载下"卡死"、近乎不可用。
修复后的行为
修复后中间件实例被缓存并跨请求复用。这与 Django WSGI 体系中中间件在进程启动时一次性实例化、请求期间复用的模型一致。对使用者而言,这意味着:
- 中间件类实例在整个进程生命周期内共享,
__init__中的初始化逻辑只执行一次; - 不应在中间件实例属性中存放与单个请求绑定的状态,否则会在请求间串扰。
这恰好与 channels/middleware.py 中BaseMiddleware的类注释相呼应——它明确警告子类"并非自安全(not self-safe),不要在实例上保存状态,因为同一个实例会服务于多个应用实例,请改用 scope 传递状态":
class BaseMiddleware: """ Base class for implementing ASGI middleware. Note that subclasses of this are not self-safe; don't store state on the instance, as it serves multiple application instances. Instead, use scope. """可见本次修复正是让实际实现与这一设计约束重新对齐:中间件实例一旦被缓存复用,实例级状态存储就必然成为 bug 温床。推荐做法是把请求级数据写入scope字典——BaseMiddleware.__call__也特意通过scope = dict(scope)复制一份 scope,防止修改向上游泄漏。
Channels 官方自带的多层中间件栈正是基于BaseMiddleware构建的,例如 channels/auth.py 提供的AuthMiddlewareStack:
def AuthMiddlewareStack(inner): return CookieMiddleware(SessionMiddleware(AuthMiddleware(inner)))这些中间件均通过向 scope 注入session、user等键工作,天然符合"状态放 scope"的缓存安全模式。
ChannelServerLiveTestCase 恢复静态文件服务
2.1.4 修复了ChannelServerLiveTestCase不再提供静态文件服务的问题。
在 channels/testing/live.py 中可以看到,ChannelsLiveServerTestCase(继承 Django 的TransactionTestCase)会在独立的 Daphne 进程中启动真实服务器,供 Selenium 等外部测试框架连接:
class ChannelsLiveServerTestCase(TransactionTestCase): host = "localhost" ProtocolServerProcess = DaphneProcess static_wrapper = ASGIStaticFilesHandler serve_static = True关键字段解释:
serve_static = True:默认开启静态文件服务;static_wrapper = ASGIStaticFilesHandler:静态文件通过 Django 的ASGIStaticFilesHandler包装;host = "localhost":测试服务器监听主机,可通过live_server_url(http://host:port)与live_server_ws_url(ws://host:port)属性获取地址。
在_pre_setup中,测试用例通过make_application把静态文件包装器套在默认 ASGI 应用外层(见 channels/testing/live.py):
def make_application(*, static_wrapper): # Module-level function for pickle-ability application = get_default_application() if static_wrapper is not None: application = static_wrapper(application) return application2.1.4 修复的就是该流程中serve_static配置被忽略、静态文件包装器未生效的问题。修复后,基于该测试类做端到端测试时,页面引用的 CSS/JS 静态资源可以正常从测试服务器加载,不再出现 404。需要注意:该测试类要求使用磁盘型数据库,若检测到内存数据库会抛出ImproperlyConfigured(见 channels/testing/live.py),并且_post_teardown会负责终止 Daphne 进程、还原ALLOWED_HOSTS修改。
Origin 头校验失败的错误信息改进
2.1.4 改进了由于非法 Origin 头导致的错误提示。要理解这一改进,需要先了解 Channels 的 Origin 校验机制——它由 channels/security/websocket.py 中的OriginValidator类实现。
校验流程
OriginValidator是 ASGI 中间件形态的 WebSocket 应用包装器,其核心流程如下(对应__call__实现):
- 校验 scope 类型必须是
websocket,否则抛出ValueError(见 channels/security/websocket.py); - 从 scope 的
headers中提取origin头并解码(latin1),用urlparse解析(见 channels/security/websocket.py); - 调用
valid_origin()判断是否允许:若 Origin 缺失且允许列表中无"*",直接拒绝;否则交给validate_origin()与允许列表匹配(见 channels/security/websocket.py); - 校验通过则放行到内层应用,否则交给
WebsocketDenier拒绝连接——WebsocketDenier是一个继承自AsyncWebsocketConsumer的简化应用,connect时直接close()(见 channels/security/websocket.py)。
允许列表匹配规则
match_allowed_origin(见 channels/security/websocket.py)支持以下模式:
"*":匹配一切;- 精确域名,如
"example.com"(无 scheme 时按域名比较主机名); - 带 scheme 的完整 Origin,如
"https://example.com",此时 scheme、端口、域名必须全部匹配; - 以点开头的域名,如
".example.com",匹配该域名及其全部子域(借助 Django 的is_same_domain)。
端口比较存在默认值逻辑:http/ws默认端口 80,https/wss默认端口 443(见 channels/security/websocket.py),因此显式写 443 端口与不写端口在https下视为等价。
便捷工厂与 DEBUG 行为
AllowedHostsOriginValidator(见 channels/security/websocket.py)是常用便捷封装:直接读取 Django 的ALLOWED_HOSTS设置作为允许列表;若DEBUG=True且ALLOWED_HOSTS为空,则自动回退为["localhost", "127.0.0.1", "[::1]"],便于本地开发调试。
与测试的对应关系
仓库测试 tests/security/test_websocket.py 覆盖了允许/拒绝、通配子域、空 Origin、非法 Origin 等场景。其中空 Origin 头与非法 Origin 头的用例(如(b"origin", b"")和(b"origin", b"something-invalid"))正是针对 2.1.4 这类边界场景的回归验证:此前这类输入可能产生晦涩难懂的异常或行为异常,2.1.4 后校验逻辑与错误路径更清晰可控。
2.1.4 中"改进的错误信息"主要落在校验被拒绝、校验过程异常时向开发者反馈的信息质量上,配合上述流程,开发者可以更快定位"被拒连接到底是 Origin 缺失、域名不匹配还是格式非法"。
runserver 日志接入 Django 日志框架
2.1.4 将runserver的日志输出切换到 Django 的标准日志框架(logging),以对齐现代 Django 的日志行为。
在此之前,runserver启动信息往往直接打印到 stdout/stderr,无法与 Django 项目的LOGGING配置联动,难以统一日志格式、级别过滤与收集。修复后,运行输出通过 Django logging 框架发布,开发者可以在LOGGING配置中按 logger 名称(例如django.channels相关 logger)自定义处理方式。
这一点与仓库中 Channels worker 命令的日志实践一致:channels/management/commands/runworker.py 中即使用logger = logging.getLogger("django.channels.worker")记录 worker 启动与运行信息,并通过--verbosity控制输出详略;runserver采用同样的 logging 通道后,两类常驻进程的日志可以统一纳入项目日志体系。若项目在升级后发现 runserver 日志样式变化或丢失,应检查LOGGING配置中是否存在覆盖django.channels系列 logger 的规则。
通用 Consumer 支持非默认 Channel Layer:channel_layer_alias
2.1.4 允许通用 Consumer(WebsocketConsumer、AsyncWebsocketConsumer、AsyncHttpConsumer等)使用非默认的 Channel Layer。此前通用 Consumer 固定绑定默认 layer,需要同时配置多个后端(例如生产用 Redis、部分连接走内存)时只能通过自定义 Consumer 实现;2.1.4 起只需在 Consumer 类上设置channel_layer_alias属性。
底层实现
该能力的根基在 channels/consumer.py:AsyncConsumer是全部通用 Consumer 的公共基类,定义了类属性:
class AsyncConsumer: _sync = False channel_layer_alias = DEFAULT_CHANNEL_LAYER async def __call__(self, scope, receive, send): self.scope = scope # Initialize channel layer self.channel_layer = get_channel_layer(self.channel_layer_alias) ...Consumer 每次被调用时,通过get_channel_layer(self.channel_layer_alias)按别名获取对应的 Channel Layer 实例。而get_channel_layer(见 channels/layers.py)从全局channel_layers管理器(ChannelLayerManager)中按别名取层;ChannelLayerManager依据 Django 设置的CHANNEL_LAYERS字典按需惰性实例化后端,并在CHANNEL_LAYERS设置变化(setting_changed信号)时清空缓存重新加载(见 channels/layers.py)。
配置与使用示例
首先在settings.py中配置多个命名后端(default与自定义别名):
CHANNEL_LAYERS = { "default": { "BACKEND": "channels_redis.core.RedisChannelLayer", "CONFIG": {"hosts": [("127.0.0.1", 6379)]}, }, "memory": { "BACKEND": "channels.layers.InMemoryChannelLayer", "CONFIG": {"capacity": 100, "expiry": 60}, }, }然后在 Consumer 类中声明别名:
from channels.generic.websocket import AsyncWebsocketConsumer class MemoryChatConsumer(AsyncWebsocketConsumer): channel_layer_alias = "memory" async def connect(self): await self.channel_layer.group_add("chat", self.channel_name) await self.accept()此时self.channel_layer指向CHANNEL_LAYERS["memory"]对应的后端。需要说明:
channel_layer_alias是类属性,默认值为DEFAULT_CHANNEL_LAYER(即"default"),不设置时行为与旧版本完全一致;- 若指定的别名未在
CHANNEL_LAYERS中配置,get_channel_layer返回None,Consumer 将以无 channel layer 模式运行(不再创建专属接收通道),这是从源码结构可以直接推断的边界行为(见 channels/consumer.py); - 仓库测试 tests/test_generic_websocket.py 中有多处以
channel_layer_alias = "testlayer"验证该特性,说明别名机制在通用 WebSocket Consumer 场景下已被测试覆盖。
提前访问 scope['user'] 的错误信息改进
2.1.4 改进了在"用户对象尚未就绪"时访问scope['user']的错误提示。
背景:异步环境下的懒加载
Django 用户加载依赖数据库与会话,必须在异步环境中执行。AuthMiddleware在populate_scope阶段先把scope["user"]填为一个懒加载代理(LazyObject),真正的用户实例要等resolve_scope阶段通过get_user(scope)异步取回后才填充(见 channels/auth.py)。
此前,如果在 ASGI 应用的构造阶段(而非请求处理阶段)就访问scope['user'],由于代理尚未解析,只会抛出一个泛化的初始化错误,让人难以判断问题原因——尤其是"应用构造函数里拿不到 user"这一 Channels 独有的约束(构造阶段是同步上下文,无法执行异步数据库加载)。
2.1.4 的改进
修复在 channels/auth.py 中新增了UserLazyObject:
class UserLazyObject(LazyObject): """ Throw a more useful error message when scope['user'] is accessed before it's resolved """ def _setup(self): raise ValueError("Accessing scope user before it is ready.")现在一旦在解析完成前访问scope['user'],会立即得到明确的ValueError: Accessing scope user before it is ready.,直接点明"用户尚未就绪"。配套地,AuthMiddleware.populate_scope在无 session 时也会给出清晰指引:"AuthMiddleware无法在 scope 中找到 session,SessionMiddleware必须位于其上层"(见 channels/auth.py),提示开发者按CookieMiddleware → SessionMiddleware → AuthMiddleware的正确顺序(即AuthMiddlewareStack)组织中间件。
对应用开发者的实践意义:用户对象只能在 Consumer 的异步方法(如connect、receive)内部访问,不能在 ASGI 应用/Consumer 的__init__构造函数中访问scope['user'];如需在连接时判断登录态,应放在connect等异步处理器中。
升级建议与版本总结
- 性能:如果此前因中间件初始化开销导致 Channels HTTP 服务响应缓慢,2.1.4 的中间件缓存机制可以直接缓解;但请确保自定义 ASGI 中间件不在实例上保存请求级状态(遵循 channels/middleware.py 的约束,状态放 scope)。
- 日志:升级后检查项目
LOGGING配置,确认django.channels相关 logger 的处理器、格式与预期一致。 - 多后端:需要为部分 Consumer 单独指定 channel layer 时,使用
channel_layer_alias类属性并配套配置CHANNEL_LAYERS中的命名后端。 - 测试:依赖静态资源的 Live Server 测试(Selenium 等)在 2.1.4 可正常加载静态文件;注意此类测试需使用磁盘数据库。
- 兼容性:本次发布无向后不兼容变更,可安全升级;若需继续深入了解 Channel Layer 配置语法与容量/过期参数,可参阅 channels/layers.py 的
BaseChannelLayer基类实现。
Channels 2.1.4 虽然只是一个小版本维护发布,却通过中间件缓存、日志体系对齐、错误信息优化与channel_layer_alias别名支持,在性能、可观测性与多后端灵活性三个方向上为 2.1 系列补上了关键短板,值得所有使用 Channels 2.1 的 Django 异步应用跟进升级。
- 后端
- WebSocket
- 异步编程
【免费下载链接】channels
Developer-friendly asynchrony for Django
相关推荐
LND v0.20.1 发布说明深度解读:关键修复、Postgres 并发控制与 BOLT 7 时间戳强制
LND v0.20.1 发布说明深度解读:关键修复、Postgres 并发控制与 BOLT 7 时间戳强制 本文是对 LND(Lightning Network
区块链Node.js 0.12.3 (Stable) 发布说明深度解析:V8 与 libuv 升级、九项关键修复及发布物校验指南
Node.js 0.12.3 Stable 发布说明深度解析:V8 与 libuv 升级、九项关键修复及发布物校验指南 本篇技术指南以 apps/site/pa
前端文档Robot Framework 4.0.1 版本发布说明与关键修复深度解析
Robot Framework 4.0.1 版本发布说明与关键修复深度解析 Robot Framework 4.0.1 是 4.0.x 系列的首个缺陷修复版本,
测试RPA接口测试
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考