杰瑞科技汇

Python Channels 中文如何快速上手?

什么是 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 可以处理多种协议,当消息进入通道时,它会附带一个协议类型标识,如 httpwebsocketwebsocket.send 等,Router 根据这个标识来决定将消息交给哪个 Consumer 的哪个方法(http 请求交给 async def http_request(self, scope, receive, send),WebSocket 连接交给 async def websocket_connect(self, scope, receive, send))。

工作流程简述

  1. 连接建立:一个客户端(浏览器)通过 WebSocket 连接到你的 Channels 应用。
  2. ASGI 服务器接收:异步服务器(如 Daphne)接收到连接请求。
  3. 创建通道:ASGI 服务器为这个连接创建一个唯一的通道,并将连接信息(scope)、接收消息的函数(receive)和发送消息的函数(send)一起放入通道。
  4. 路由分发:Router 监听到通道中有消息,检查其协议类型是 websocket
  5. 消费者处理:Router 将这个 WebSocket 连接请求分发给你预先定义好的 WebSocketConsumer
  6. 双向通信
    • 服务器 -> 客户端:你的 Consumer 可以随时调用 send() 方法,通过通道将消息异步地推送给客户端。
    • 客户端 -> 服务器:客户端发送消息到服务器,receive() 函数会从通道中获取消息,并再次由 Router 分发给 Consumer 的 websocket_receive 方法进行处理。

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>

运行

  1. 确保 Redis 服务正在运行。
  2. 启动 ASGI 服务器:daphne myproject.asgi:application
  3. 在浏览器中访问 /chat/some_room_name/,打开两个窗口,你就可以实时聊天了!

Channels 是 Django 生态中构建实时应用的事实标准。

  • 如果你需要构建:聊天应用、实时通知、在线协作工具、多玩家游戏、股票行情推送等任何需要服务器主动向客户端推送数据的场景,Channels 是你的不二之选
  • 如果你只需要构建:传统的博客、电商网站、REST API,那么传统的 WSGI 模式(Gunicorn + uWSGI)已经足够高效,引入 Channels 可能会增加不必要的复杂性。

对于希望拥抱异步编程、提升应用性能和扩展性的 Django 学习 Channels 是一项非常有价值的技能。

分享:
扫描分享到社交APP
上一篇
下一篇