我们在普通 Django 项目里待久了,会慢慢习惯一个套路:浏览器发请求,视图函数算好结果,再返回一个 HTML 或者 JSON。这正是 HTTP 协议的办事风格。可是,如果有一天你要做一个聊天室、一个实时订单通知,或者一块多人一起画画的画板,这套流程就绕了远路。与其一个劲儿地刷新页面,不如让服务器把新的变化直接“塞”给浏览器。今天要聊的 Channels,就是干这件事的。
一、先想清楚,什么时候才需要 WebSocket
很多朋友一听到 WebSocket 就觉得高端,其实它并不神秘。它就是在 TCP 连接上建立的一个长期存在的双向通道。HTTP 是来回问答,WebSocket 是随时说话。你打开一个网页,通过 WebSocket 和服务端保持联系,服务端一旦有数据,立刻就能发过来。聊天室、在线客服、多人在线游戏、协同编辑文档、股票行情推送、外卖骑手位置更新……这些场景的共同点是“变化来得没规律,而且用户希望立刻看到”。如果你只是做一个普通博客,文章发布后过几分钟刷新才看到新内容,那完全没必要上 WebSocket。实时是一种能力,不是所有系统都必须具备的装饰品。用对了地方,用户体验会很舒服;用不对地方,只会给自己增加一堆复杂度。
二、Channels 到底改变了什么
Django 的常规运行模式走的是 WSGI,它的生命周期是请求进来、响应出去,然后断开。WebSocket 需要的是长连接,所以在最底层上就不合适。Channels 的出现,把 Django 的接口从 WSGI 升级到了 ASGI。ASGI 的全称是 Asynchronous Server Gateway Interface,中文一般叫异步服务器网关接口。这个接口允许 Django 应用同时处理 HTTP、WebSocket、后台任务等等。你可以在同一个项目里,一部分接口保持原来的 HTTP,另一部分路径走 WebSocket。Channels 还能帮你处理连接的生命周期、群组关系、消息广播,让你的实时功能以模块化的方式长在 Django 里。
2.1 从 HTTP 到 ASGI
你可以把 WSGI 想象成一条单行道,车来一辆走一辆,没办法长时间停留。ASGI 则像一片空场地,很多车可以停在那里,车里的乘客随时下车聊天。在 ASGI 世界里,WebSocket 连接一旦建立,就会一直留在那里,直到某一方主动断开。Channels 的架构中有几个关键概念:Application、Scope、Consumer、Channel Layer。Scope 保存的是连接相关信息,比如请求路径、用户身份、请求头。Consumer 是处理连接和消息的类,它就像 Django 的视图,只不过面向的是长连接。
2.2 什么是 Channel Layer 和群组
Channel Layer 像是一个消息中转站。每个 WebSocket 连接都会有一个名字,叫 channel_name。你可以把消息精确地发送给某一个 channel,也可以把很多 channel 拉进一个群组,然后给整个群组发广播。举个例子,你建了一个微信群,每个成员是一个 channel,这个群就是 group。你在群里发一条消息,微信服务器负责把这条消息发给群里的每个人。Channels 里的 group_add、group_send、group_discard 就是干这个用的。群组的名字可以由你自己定,比如 chat_room1,只要路径里有房间名,就能动态创建不同的群。
三、动手搭建一个最小的实时聊天室
纸上谈兵没有用,我们现在开始写代码。假设你已经创建了一个 Django 项目,并安装好了 Django 和 Channels 以及 Daphne。下面所有的主要内容都会放在一个叫 chat 的应用里。
3.1 settings.py 里要做什么
channels 要能够被 Django 识别,第一步就是在 INSTALLED_APPS 里把它加进去。同时我们要指定 ASGI_APPLICATION,告诉 Django 以后入口是哪个文件。
# 技术栈:Python + Django Channels
# 文件名:mysite/settings.py(只展示关键部分)
INSTALLED_APPS = [
'daphne', # 负责处理 ASGI 请求的服务器,放在最前面
'channels', # Channels 核心
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'chat', # 我们自己创建的聊天应用
]
# 告诉 Django 使用哪一个 ASGI 入口文件
ASGI_APPLICATION = 'mysite.asgi.application'
# 配置 Channel Layer,本地开发先用内存版
# 生产环境一般换成 Redis,后面注意事项里会解释
CHANNEL_LAYERS = {
'default': {
'BACKEND': 'channels.layers.InMemoryChannelLayer',
},
}
这里用了 InMemoryChannelLayer,也就是把群组和消息都存在进程内存里。这种方式适合本地开发和测试,要是启动多个 worker,消息就会不互通,所以生产环境一定要换 Redis。
3.2 ASGI 入口负责分配协议
接着需要写 asgi.py,它是整个项目的入口文件。当 Daphne 收到一个请求时,会先看协议类型,如果是 HTTP,交给 Django 原来的流程;如果是 WebSocket,交给 Channels 的路由和消费者。
# 技术栈:Python + Django Channels
# 文件名:mysite/asgi.py
import os
from django.core.asgi import get_asgi_application
# 先设置 Django 的配置模块
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'mysite.settings')
# 初始化 Django 自带的 ASGI 应用,HTTP 请求走它
django_asgi_app = get_asgi_application()
from channels.routing import ProtocolTypeRouter, URLRouter
from channels.auth import AuthMiddlewareStack
import chat.routing
# 根路由:根据协议类型分发到不同处理方式
application = ProtocolTypeRouter({
# HTTP 请求继续使用 Django 原有的视图体系
'http': django_asgi_app,
# WebSocket 请求走自定路由,并使用认证中间件
'websocket': AuthMiddlewareStack(
URLRouter(
chat.routing.websocket_urlpatterns
)
),
})
3.3 WebSocket 路由写在哪里
在 chat 应用里,我们再建一个 routing.py,专门放 WebSocket 路由。这里的路径格式和 Django 的 urlpatterns 很像,但用的是 re_path,因为我们要从路径里取值。
# 技术栈:Python + Django Channels
# 文件名:chat/routing.py
from django.urls import re_path
from . import consumers
# WebSocket 路径规则
websocket_urlpatterns = [
# 示意:ws://你的地址/ws/chat/房间名/
re_path(r'ws/chat/(?P<room_name>\w+)/$', consumers.ChatConsumer.as_asgi()),
]
3.4 消费者 Consumer 核心逻辑(认证 + 群组)
现在到了最关键的部分:消费者。消费者可以理解为处理 WebSocket 事件的视图。我们在里面做了三件事:判断用户是否登录;把连接加入对应群组;把收到的消息广播出去。
# 技术栈:Python + Django Channels
# 文件名:chat/consumers.py
import json
from channels.generic.websocket import AsyncWebsocketConsumer
class ChatConsumer(AsyncWebsocketConsumer):
# 建立连接前会先执行 connect
async def connect(self):
# 从路径中取出房间名
self.room_name = self.scope['url_route']['kwargs']['room_name']
# 检查用户是否已经登录
user = self.scope.get('user')
if not user.is_authenticated:
# 没登录就直接关闭连接,不给进入群组
await self.close()
return
# 给当前房间创建一个群组名
self.group_name = f'chat_{self.room_name}'
# 把这个连接加入群组
await self.channel_layer.group_add(
self.group_name,
self.channel_name
)
# 正式接受 WebSocket 连接
await self.accept()
# 给群里的人广播一条进入消息
await self.channel_layer.group_send(
self.group_name,
{
'type': 'chat.message',
'message': f'{user.username} 进入了聊天室',
}
)
# 断开连接时触发
async def disconnect(self, close_code):
# 如果之前已经加入群组,就主动退群
if hasattr(self, 'group_name'):
await self.channel_layer.group_discard(
self.group_name,
self.channel_name
)
# 收到客户端消息时触发
async def receive(self, text_data=None, bytes_data=None):
# 聊天消息统一用 JSON 字符串传递
data = json.loads(text_data)
message = data.get('message', '')
# 取用户名,防止意外丢失 user
user = self.scope.get('user')
username = user.username if user.is_authenticated else '匿名'
# 把消息广播给群组里的所有人,包括发送者自己
await self.channel_layer.group_send(
self.group_name,
{
'type': 'chat.message',
'message': f'{username}: {message}',
}
)
# 每个群组成员都会收到这个事件回调
async def chat_message(self, event):
# 最终通过 WebSocket 发回给客户端
await self.send(text_data=json.dumps({
'message': event['message'],
}))
这段代码有几个地方要留意。connect 方法在连接建立前调用,我们用它做认证检查。scope['user'] 是 Channels 中间件塞进来的,如果没有 AuthMiddlewareStack,这个 user 是不会存在的。group_add 把当前连接加入一个群组,之后 group_send 就能找到它。receive 负责接收客户端发来的文本消息,解析成 JSON 后再次广播。我们定义了一个 chat_message 方法,这个方法的名字和 group_send 事件里的 type 字段有关。你写 type 为 'chat.message',Channels 就会自动找对应的方法 chat_message,把点号转成下划线。这一点不了解的话,会经常栽跟头。
四、从普通视图也能发广播
聊天的过程中,有时候系统要自动推送一些消息,比如管理员发公告,或者业务逻辑触发通知。我们可以不用手动连接 WebSocket,只要在视图里拿到 channel_layer,再调用 group_send 就可以。
# 技术栈:Python + Django Channels
# 文件名:chat/views.py
import json
from django.http import JsonResponse
from asgiref.sync import async_to_sync
from channels.layers import get_channel_layer
def push_broadcast(request):
# 获取默认的 Channel Layer
channel_layer = get_channel_layer()
# 给整个群组发送一条消息
async_to_sync(channel_layer.group_send)(
'chat_general', # 群组名称,要与消费者里加入的保持一致
{
'type': 'chat.message',
'message': '这是一条系统广播消息',
}
)
return JsonResponse({'ok': True})
注意,Django 视图是同步代码,而 channel_layer 的 group_send 是异步方法,所以使用 async_to_sync 转一下。代码里的 'chat_general' 就是你要广播到的群组名字,必须和 WebSocket 消费者里加入的群组保持一致,否则消息发不进去。
五、用测试模拟客户端验证效果
每次启动浏览器手动测 WebSocket 太累,而且容易忽略边界情况。Channels 自带了测试工具 WebsocketCommunicator,可以帮我们模拟一个 WebSocket 客户端。下面的测试用例覆盖了两种情况:匿名用户会被拒之门外;登录用户可以连接、发消息、收到广播。
# 技术栈:Python + Django Channels
# 文件名:chat/tests.py
import json
from django.test import TestCase
from django.contrib.auth.models import User
from channels.testing import WebsocketCommunicator
from asgiref.sync import async_to_sync
from mysite.asgi import application
class ChatConsumerTests(TestCase):
def setUp(self):
# 准备一个普通用户,用来模拟登录状态
self.user = User.objects.create_user('tester', password='pass123')
def test_anonymous_cannot_join(self):
# 不带任何用户信息,连接应该被拒绝
communicator = WebsocketCommunicator(application, '/ws/chat/room1/')
connected, _ = async_to_sync(communicator.connect)()
self.assertFalse(connected)
async_to_sync(communicator.disconnect)()
def test_logged_in_user_can_chat(self):
# 模拟一个已经登录好的用户
communicator = WebsocketCommunicator(application, '/ws/chat/room1/')
communicator.scope['user'] = self.user
connected, _ = async_to_sync(communicator.connect)()
self.assertTrue(connected)
# 先接收一条“进入聊天室”的广播
entry = async_to_sync(communicator.receive_from)(timeout=2)
self.assertIn('进入了聊天室', json.loads(entry)['message'])
# 再发送一条聊天消息
payload = json.dumps({'message': '大家晚上好'})
async_to_sync(communicator.send_to)(text_data=payload)
# 验证服务器广播回来的内容
response = async_to_sync(communicator.receive_from)(timeout=2)
data = json.loads(response)
self.assertIn('tester', data['message'])
self.assertIn('大家晚上好', data['message'])
async_to_sync(communicator.disconnect)()
这里有一个小细节,连接成功之后,服务器会先广播一条“谁进入了聊天室”的消息,所以我们在测试里要先把它接收掉,再去验证后续的聊天内容,否则收到的时间顺序会乱。把 scope['user'] 手动设置进去,是为了模拟认证成功后的状态。真实情况下,这个 user 是由 AuthMiddlewareStack 根据 Cookie 里的 sessionid 自动解析出来的。
六、做实时功能时的技术优缺点
任何技术都有取舍,Channels 也不例外。
6.1 优点
第一,和 Django 生态衔接得非常好。你可以直接在你的消费者里用 Django 的 ORM、模型和权限系统,不需要另起炉灶。第二,群组广播的抽象让多人通信变得很简单,你不用自己维护一堆 socket 连接。第三,支持同步和异步两种消费者。任务简单可以用同步,需要高并发可以用异步。第四,Channels 背后有活跃的社区和文档,踩坑时有地方问。
6.2 缺点
缺点也不能忽视。首先,部署比普通 WSGI 项目麻烦,你需要运行 Daphne,可能需要 Redis,还要处理进程间的 channel layer 同步。其次,调试起来不如 HTTP 那么直观,Channels 的报错信息有时候藏得比较深。再有,本身的学习曲线比较陡,如果之前完全没有接触过 ASGI 和异步编程,需要一段时间适应。最后,如果你要构建非常复杂的实时平台,比如大型多人游戏服务器,Channels 提供的抽象不一定能覆盖全部需求,你可能需要寻找更底层的方案。
七、实战中躲不开的注意事项
第一,生产环境不要用 InMemoryChannelLayer。开发时为了省事儿可以用,但你一旦部署到多进程或者多台机器上,不同进程之间的 channel layer 不互通,广播就失效了。换成 Redis 的成本并不高,改动也只在 CHANNEL_LAYERS 配置里。第二,认证要提前设计好。WebSocket 协议在浏览器里没法手动添加自定义请求头,但你可以把认证 Token 放在 query string 或者 Cookie 里。不要默认 scope['user'] 一定存在,要写个判断和兜底。第三,消费者里不要做耗时的同步操作。比如查询大量数据库记录、调用外部 HTTP 接口,都会阻塞事件循环,让其他连接卡住。能用异步函数就用异步,实在需要同步操作时,用 asgiref 的 sync_to_async 包一层。第四,注意处理心跳。WebSocket 连接如果长时间没有数据,中间层可能会把它断掉。通常做法是定时发送 ping/pong 帧,Channels 的底层协议支持这些,但你也要在客户端做相应的重连机制。第五,消息格式尽量统一。建议全部使用 JSON 字符串,并且定义好 message 和 type 等字段,方便前后端解析。第六,跨域问题。生产环境的域名如果和 WebSocket 服务不同,需要在前面做好跨域配置。Channels 本身没有内置浏览器跨域限制,但你的部署层可能会拦截。
八、最后总结
这次我们把 Channels 的核心流程完整走了一遍。从 WebSocket 的应用场景,到 ASGI 和 Channel Layer 的概念,再到一个带认证的聊天室 consumer,最后用测试代码验证了效果。我们还聊了它有哪些优缺点,以及真正上生产时要注意的地方。Channels 真正厉害的地方,是让 Django 开发者不用换掉熟悉的语言和框架,也能写出实时功能。当然,它并不是免费的午餐,你需要学习异步、了解 ASGI、接受更复杂的部署。但当你做出一个消息秒级的聊天室时,那种成就感是普通 HTTP 接口给不了的。希望这篇文章能帮你少走一些弯路。
评论
围绕“Django Channels实战:构建实时WebSocket应用并处理群组广播与认证”参与讨论