什么是 Python Channels?
Channels 是一个为 Django 项目添加异步处理能力的扩展库。
传统的 Django 是一个同步框架,当它收到一个 HTTP 请求时,它会启动一个工作线程来处理这个请求,直到处理完成并返回响应,这个线程才会被释放去处理下一个请求,如果处理过程中有耗时操作(如复杂的计算、等待数据库查询、调用外部 API 等),这个线程就会被一直占用,导致整个应用无法响应其他请求,性能瓶颈非常明显。
Channels 的出现改变了这一点,它将 Django 从一个纯 Web 服务器框架转变为一个全功能的异步应用框架,允许你处理不仅仅是 HTTP 请求,还有 WebSocket、HTTP2 服务器推送、MQTT 协议等。
Channels 的核心思想是:将 HTTP/WebSocket 连接的“接收”和“处理”两个过程分离开。
Channels 的核心概念和工作原理
要理解 Channels,你需要了解以下几个核心组件,它们共同构成了 Channels 的工作流:
ASGI (Asynchronous Server Gateway Interface)
- 类比:如果你把 WSGI 看作是 Django 的“同步插座”,ASGI Django 的“异步插座”。
- 作用:它是一个标准,定义了如何将一个异步 Python Web 应用(如 Channels)与一个异步服务器(如 Daphne, Uvicorn)连接起来,Channels 完全兼容 ASGI 标准。
Channel (通道)
- 类比:你可以把它想象成一个消息队列或管道。
- 作用:它是 Channels 中最基本的数据结构,用于在应用程序的不同部分之间传递消息,当一个连接(如 WebSocket)建立时,Channels 会为它创建一个独特的通道,所有发送给这个连接的消息都会先进入这个通道。
Consumer (消费者)
- 类比:一个工人,他站在通道的另一头,负责从通道中取出消息并进行处理。
- 作用:Consumer 是 Channels 中处理消息的核心逻辑,它是一个异步 Python 类,包含
async def方法来处理不同类型的消息,你可以把它看作是 Django View 的异步版本。
Router (路由器)
- 类比:一个调度中心或前台接待。
- 作用:Router 负责检查进入通道的消息,并根据消息的类型(HTTP 请求、WebSocket 连接、消息等)将其路由到正确的 Consumer,它就像 Django 的
urls.py,只不过路由的是消息而不是 URL。
Protocol Type (协议类型)
- 类比:告诉调度中心“这是一个快递件,还是一个普通信件”。
- 作用:Channels 可以处理多种协议,当消息进入通道时,它会附带一个协议类型标识,如
http、websocket、websocket.send等,Router 根据这个标识来决定将消息交给哪个 Consumer 的哪个方法(http请求交给async def http_request(self, scope, receive, send),WebSocket 连接交给async def websocket_connect(self, scope, receive, send))。
工作流程简述
- 连接建立:一个客户端(浏览器)通过 WebSocket 连接到你的 Channels 应用。
- ASGI 服务器接收:异步服务器(如 Daphne)接收到连接请求。
- 创建通道:ASGI 服务器为这个连接创建一个唯一的通道,并将连接信息(
scope)、接收消息的函数(receive)和发送消息的函数(send)一起放入通道。 - 路由分发:Router 监听到通道中有消息,检查其协议类型是
websocket。 - 消费者处理:Router 将这个 WebSocket 连接请求分发给你预先定义好的
WebSocketConsumer。 - 双向通信:
- 服务器 -> 客户端:你的 Consumer 可以随时调用
send()方法,通过通道将消息异步地推送给客户端。 - 客户端 -> 服务器:客户端发送消息到服务器,
receive()函数会从通道中获取消息,并再次由 Router 分发给 Consumer 的websocket_receive方法进行处理。
- 服务器 -> 客户端:你的 Consumer 可以随时调用
Channels vs. 传统 Django (WSGI)
| 特性 | 传统 Django (WSGI) | Django with Channels (ASGI) |
|---|---|---|
| 处理模式 | 同步 | 异步 |
| I/O 操作 | 阻塞式 | 非阻塞式 |
| 适用场景 | 传统网站、REST API | 实时应用、聊天室、通知系统、在线协作工具 |
| 协议支持 | 主要 HTTP | HTTP, WebSocket, HTTP2, MQTT 等 |
| 并发模型 | 基于线程 | 基于事件循环,单线程可处理大量并发连接 |
| 典型服务器 | Gunicorn, uWSGI | Daphne, Uvicorn |
一个简单的 WebSocket 聊天室示例
这个例子能让你直观地感受到 Channels 的强大。
安装 Channels
pip install channels channels_redis
channels_redis 是一个基于 Redis 的通道层,用于在多个服务器进程间传递消息。
项目配置 (myproject/settings.py)
# 添加 INSTALLED_APPS
INSTALLED_APPS = [
# ...
'channels',
'mychat', # 你的聊天应用
]
# 设置 ASGI 应用
ASGI_APPLICATION = 'myproject.asgi.application'
# 配置通道层
CHANNEL_LAYERS = {
'default': {
'BACKEND': 'channels_redis.core.RedisChannelLayer',
'CONFIG': {
"hosts": [('127.0.0.1', 6379)],
},
},
}
创建 ASGI 配置 (myproject/asgi.py)
import os
from django.core.asgi import get_asgi_application
from channels.routing import ProtocolTypeRouter, URLRouter
from channels.auth import AuthMiddlewareStack
import mychat.routing
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'myproject.settings')
application = ProtocolTypeRouter({
"http": get_asgi_application(),
"websocket": AuthMiddlewareStack(
URLRouter(
mychat.routing.websocket_urlpatterns
)
),
})
这里,我们告诉 ASGI 服务器:所有 HTTP 请求交给 Django 处理,所有 WebSocket 请求交给我们的自定义路由处理。
创建 Consumer (mychat/consumers.py)
import json
from channels.generic.websocket import AsyncWebsocketConsumer
class ChatConsumer(AsyncWebsocketConsumer):
async def connect(self):
# 获取聊天室名称
self.room_name = self.scope['url_route']['kwargs']['room_name']
self.room_group_name = 'chat_%s' % self.room_name
# 将通道加入聊天室组
await self.channel_layer.group_add(
self.room_group_name,
self.channel_name
)
# 接受 WebSocket 连接
await self.accept()
async def disconnect(self, close_code):
# 离开聊天室组
await self.channel_layer.group_discard(
self.room_group_name,
self.channel_name
)
# 从 WebSocket 接收消息
async def receive(self, text_data):
text_data_json = json.loads(text_data)
message = text_data_json['message']
# 将消息发送到聊天室组
await self.channel_layer.group_send(
self.room_group_name,
{
'type': 'chat_message', # 指定下面要调用的方法
'message': message
}
)
# 从聊天室组接收消息的方法
async def chat_message(self, event):
message = event['message']
# 将消息发送到 WebSocket
await self.send(text_data=json.dumps({
'message': message
}))
创建路由 (mychat/routing.py)
from django.urls import re_path
from . import consumers
websocket_urlpatterns = [
re_path(r'ws/chat/(?P<room_name>\w+)/$', consumers.ChatConsumer.as_asgi()),
]
创建模板 (mychat/templates/mychat/room.html)
<!DOCTYPE html>
<html>
<head>Chat Room</title>
</head>
<body>
<textarea id="chat-log" cols="100" rows="20"></textarea><br>
<input id="chat-message-input" type="text" size="100"><br>
<input id="chat-message-submit" type="button" value="Send">
{{ room_name|json_script:"room-name" }}
</body>
<script>
const roomName = JSON.parse(document.getElementById('room-name').textContent);
const chatSocket = new WebSocket(
'ws://'
+ window.location.host
+ '/ws/chat/'
+ roomName
+ '/'
);
chatSocket.onmessage = function(e) {
const data = JSON.parse(e.data);
document.querySelector('#chat-log').value += (data.message + '\n');
};
chatSocket.onclose = function(e) {
console.error('Socket closed unexpectedly. Attempting to reconnect...');
};
document.querySelector('#chat-message-input').focus();
document.querySelector('#chat-message-input').onkeyup = function(e) {
if (e.key === 'Enter') { // enter, return
document.querySelector('#chat-message-submit').click();
}
};
document.querySelector('#chat-message-submit').onclick = function(e) {
const messageInputDom = document.querySelector('#chat-message-input');
const message = messageInputDom.value;
chatSocket.send(JSON.stringify({
'message': message
}));
messageInputDom.value = '';
};
</script>
</html>
运行
- 确保 Redis 服务正在运行。
- 启动 ASGI 服务器:
daphne myproject.asgi:application - 在浏览器中访问
/chat/some_room_name/,打开两个窗口,你就可以实时聊天了!
Channels 是 Django 生态中构建实时应用的事实标准。
- 如果你需要构建:聊天应用、实时通知、在线协作工具、多玩家游戏、股票行情推送等任何需要服务器主动向客户端推送数据的场景,Channels 是你的不二之选。
- 如果你只需要构建:传统的博客、电商网站、REST API,那么传统的 WSGI 模式(Gunicorn + uWSGI)已经足够高效,引入 Channels 可能会增加不必要的复杂性。
对于希望拥抱异步编程、提升应用性能和扩展性的 Django 学习 Channels 是一项非常有价值的技能。
