django原生不支持websocket因其同步架构与长连接本质冲突,必须通过channels引入异步运行时和消息分发层;需配置asgi、路由、consumer,并注意握手细节与生产环境redis通道层。

用 Django Channels 实现 WebSocket 连接
纯 HTTP 无法主动推送,必须换通信协议。Django Channels 是官方推荐的异步扩展,它把 Django 从 WSGI 切到 ASGI,支持 WebSocket、HTTP2 和后台任务。不装 Channels,后面所有“实时”都是伪实时(比如轮询),延迟高且浪费连接。
安装后要改 settings.py:把 ASGI_APPLICATION 指向 myproject.asgi.application,并把 channels 加进 INSTALLED_APPS;还要配置 CHANNEL_LAYERS,开发用 channels.layers.InMemoryChannelLayer 就够,生产必须换 Redis。
- 漏配
ASGI_APPLICATION→ 启动报Application instance has no attribute 'scope' - 忘记在
asgi.py中导入ProtocolTypeRouter和URLRouter→ WebSocket 路由不生效 - 开发时用内存层没问题,但多 worker 时进度会丢失——因为每个进程有独立内存层
定义 Consumer 处理进度广播
Consumer 是 Channels 的核心逻辑单元,类似 Django 的 View,但面向长连接。要用 AsyncWebsocketConsumer,不能用同步版,否则并发一高就卡死。
关键点在于:任务启动时生成唯一 task_id,前端按 task_id 订阅对应 group;后端用 channel_layer.group_send() 把进度发到该 group;Consumer 收到后原样转发给客户端。
class ProgressConsumer(AsyncWebsocketConsumer):
async def connect(self):
self.task_id = self.scope['url_route']['kwargs']['task_id']
self.group_name = f'progress_{self.task_id}'
await self.channel_layer.group_add(self.group_name, self.channel_name)
await self.accept()
<pre class="brush:python;toolbar:false;">async def receive(self, text_data):
# 可选:接收前端 ping 或取消指令
pass
async def send_progress(self, event):
await self.send(text_data=json.dumps({
'progress': event['progress'],
'status': event['status']
}))
-
send_progress是自定义事件名,必须和group_send()的type参数一致 - 别在
connect里做耗时操作(如查数据库),会阻塞握手 - 前端断连后,
disconnect方法里记得调group_discard,否则 group 积累僵尸连接
后端任务中调用 channel_layer 发送进度
Django 的普通视图或 Celery 任务里,不能直接 await,得用 async_to_sync 包一层。这是最常踩的坑:忘了包、或者在非异步上下文里直接 await channel_layer.group_send,结果报 RuntimeError: This method is asynchronous。
示例场景:用户上传文件后触发一个耗时处理,每 10% 更新一次进度:
from asgiref.sync import async_to_sync
from channels.layers import get_channel_layer
<p>def process_file(task_id, file_path):
channel_layer = get_channel_layer()
for i in range(0, 101, 10):</p><h1>……实际处理逻辑……</h1><pre class="brush:python;toolbar:false;"> async_to_sync(channel_layer.group_send)(
f'progress_{task_id}',
{
'type': 'send_progress',
'progress': i,
'status': 'running' if i
- Celery 任务里同样适用这个写法,但注意 Celery worker 必须用
--pool=threads或gevent,默认 prefork 模式不支持 asyncio -
group_send不保证送达顺序,如果进度更新太密集(比如每毫秒一次),前端可能收乱,建议加节流或合并发送 - task_id 必须全局唯一,推荐用
uuid.uuid4().hex,别用自增 ID,避免被猜出其他任务进度
前端用原生 WebSocket 监听,别依赖轮询
WebSocket 连接地址是 ws://localhost:8000/ws/progress/{task_id}/,路径要和 routing.py 里定义的 URL 一致。重点不是连上,而是连上后能正确响应服务端发来的消息,并处理断连重试。
别用 setInterval 轮询 /api/progress?id=xxx,那不算“实时”,只是“假装实时”。真实场景下,用户等 5 秒没反应就会关页面。
const ws = new WebSocket(`ws://localhost:8000/ws/progress/${taskId}/`);
ws.onmessage = (e) => {
const data = JSON.parse(e.data);
updateProgressBar(data.progress);
if (data.status === 'done') ws.close();
};
ws.onclose = () => console.log('连接断开,可自动重连');
- 浏览器不支持自动重连,要自己实现,比如用指数退避(第一次 1s 后重试,失败则 2s、4s…)
- 别在
onopen里立刻发消息,确保服务端 consumer 已完成group_add,否则第一条进度可能丢失 - 如果用 Nginx 反向代理,必须显式开启 WebSocket 支持:
proxy_http_version 1.1和proxy_set_header Upgrade $http_upgrade
真正难的不是写通这条链路,而是当 task_id 海量、group 数量暴涨、channel_layer 面临 Redis 压力时,如何设计分片或降级策略——这一步多数人还没走到,但架构上得提前留余地。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!











