Django Channels、WS协议及同步与异步详解

  1. 1. 同步与异步
  2. 2. WebSocket
  3. 3. 在JavaScript中使用WebSocket
  4. 4. Django Channels

介绍同步与异步、WebSocket 协议以及 Django Channels 三者的概念及其联系,并结合代码示例进行讲解。

1. 同步与异步

在 Django 中,同步和异步主要涉及到请求处理的方式。这两种方式的主要区别在于它们如何处理多个并发请求:

  • 同步(Synchronous):在同步模式下,Django 会为每个请求创建一个单独的线程或进程。这意味着,如果一个请求正在等待响应(例如,等待数据库查询返回结果),那么整个线程或进程将被阻塞,直到响应返回。这可能会导致资源的浪费,因为在等待期间,线程或进程不能做其他任何事情。
  • 异步(Asynchronous):与同步模式不同,异步模式允许单个线程或进程同时处理多个请求。当一个请求需要等待响应时(例如,等待数据库查询返回结果),线程或进程可以切换到另一个请求,继续执行其他任务,而不是被阻塞。这样可以更有效地利用系统资源,提高并发处理能力。

Django 3.1 版本开始引入了对异步视图和中间件的支持,这意味着你可以编写异步的视图函数,这些函数可以使用 Python 的 asyncawait 关键字进行定义。这使得 Django 可以更好地处理 I/O 密集型任务,如 HTTP 请求、数据库操作和文件读写等。

然而需要注意的是,并非所有的 Django 组件都支持异步操作。例如,Django 的 ORM(对象关系映射)目前仍然是同步的,这意味着你不能在异步视图或中间件中直接使用它。如果你需要在异步代码中执行数据库操作,你需要使用 Django 提供的 sync_to_asyncasync_to_sync 函数来确保数据库操作在同步环境中执行。

总的来说,同步和异步各有优势和适用场景。对于 CPU 密集型任务,同步模式可能更合适;而对于 I/O 密集型任务,异步模式可能会带来更好的性能。在实际开发中,你可能需要根据应用的具体需求和性能要求来选择使用同步还是异步。

(1)同步代码样例

现在我们来看一些同步与异步的样例,首先是同步视图,在下面这个例子中,当请求到达 sync_view 时,Django 将等待视图函数完成后才会处理下一个请求:

1
2
3
4
5
from django.http import HttpResponse
from django.shortcuts import render

def sync_view(request): # 这是一个同步视图
return HttpResponse('Hello, this is a synchronous view!')

下面这个例子在视图中同步地从数据库获取所有的博客对象,然后将它们传递给模板:

1
2
3
4
5
6
from django.shortcuts import render
from .models import Blog

def sync_view(request):
blogs = Blog.objects.all() # 获取所有的博客对象
return render(request, 'blog/index.html', {'blogs': blogs})

(2)异步代码样例

在下面这个例子中,async_view 是一个异步视图。当请求到达这个视图时,Django 可以在等待 asyncio.sleep(1) 完成时处理其他请求:

1
2
3
4
5
6
import asyncio
from django.http import HttpResponse

async def async_view(request): # 这是一个异步视图
await asyncio.sleep(1)
return HttpResponse('Hello, this is an asynchronous view!')

请注意,要使用异步视图,你需要确保你的 Django 项目正在运行在支持异步的 ASGI 服务器上,而不是传统的 WSGI 服务器。此外,你的中间件和任何你在视图中调用的代码也必须支持异步。否则,你可能会遇到问题。如果你的代码库主要是同步的,那么最好坚持使用同步视图。如果你正在编写新的、主要使用 Python 的异步库的代码,那么异步视图可能会很有用。请记住,混合使用同步和异步代码可以很复杂,需要谨慎对待。

再来看下面的例子,我们创建了一个新的 get_blogs 异步函数,它使用 run_in_executor 方法在一个单独的线程中运行数据库查询。这允许 Django 在等待数据库查询完成时处理其他请求:

1
2
3
4
5
6
7
8
9
10
11
12
import asyncio
from django.http import JsonResponse
from .models import Blog

async def async_view(request): # 异步获取所有的博客对象
blogs = await get_blogs()
return JsonResponse({'blogs': list(blogs.values())})

async def get_blogs():
loop = asyncio.get_event_loop()
blogs = await loop.run_in_executor(None, Blog.objects.all)
return blogs

请注意,虽然这个示例展示了如何在 Django 视图中使用异步代码操作数据库,但是 Django 的数据库层目前还不支持原生的异步操作。因此,在实践中,你可能需要使用像 asgiref.sync_to_async 这样的工具来安全地在异步视图中执行同步数据库操作。同时,你也需要确保你的数据库驱动程序和数据库服务器能够处理并发连接。否则,你可能会遇到性能问题或错误。如果你不确定如何正确地使用异步代码,那么最好使用同步视图和同步数据库操作。

2. WebSocket

WebSocket 是一种用于在 Web 和移动应用程序之间进行实时通信的新标准。WebSocket 设计为在 Web 浏览器和 Web 服务器之间实现,但也可以由客户端或服务器应用程序使用。WebSocket 是一种提供单个 TCP 连接上的全双工通信通道的协议,可以实现服务器和客户端之间的实时交互。

WebSocket 与 HTTP 不同,其主要区别如下:

  • 通信方式:HTTP 是单向的,客户端发送请求,服务器发送响应。而 WebSocket 是双向的,在客户端-服务器通信的场景中使用的全双工协议,即客户端和服务器可以同时发送和接收数据。
  • 连接:HTTP 每次请求都需要重新建立连接,而 WebSocket 使用长连接实现数据实时推送。一旦通信链接建立和连接打开后,消息交换将以双向模式进行,客户端-服务器之间的连接会持久存在。
  • 数据传输:HTTP 协议中的数据传输是文本格式的,而 WebSocket 可以传输文本和二进制数据。
  • 性能:由于 HTTP 的每次请求都需要建立连接和断开连接,而 WebSocket 可以在一次连接上进行多次通信,因此 WebSocket 在性能上比 HTTP 有优势。
  • 应用场景:HTTP 主要用于客户端和服务器之间的请求和响应,如浏览器请求网页和服务器返回网页的 HTML 文件。WebSocket 可以实现双向通信,常常用于实时通信场景。
  • 协议头:HTTP 协议头的大小从200字节到2KB不等,常见大小是700-800字节。而 WebSocket 协议头相对较小,这使得其在高频率、小数据量的通信场景下更有优势。
  • 状态:HTTP 是无状态协议,而 WebSocket 是有状态协议。这意味着客户端和服务器之间的连接将保持活动状态,直到被任何一方(客户端或服务器)终止。

两种协议都位于 OSI 模型的第七层,并依赖于第四层的 TCP。尽管它们是不同的,但 RFC 6455 指出,WebSocket 旨在通过 HTTP 端口443和80工作,并支持 HTTP 代理和中介,从而使其与 HTTP 兼容。为了实现兼容性,WebSocket 握手使用 HTTP Upgrade 头从 HTTP 协议切换到 WebSocket 协议。

WebSocket 协议使得 Web 浏览器(或其他客户端应用程序)和 Web 服务器之间可以在不需要客户端请求的情况下发送内容,以及在保持连接打开的同时传递消息。这样,客户端和服务器之间可以进行双向持续的对话。通信通常是通过 TCP 端口号443(或80,如果是非安全连接)进行的,这对于使用防火墙阻止非 Web Internet 连接的环境是有利的。

在 Django 中实现 WebSocket,你可以选择使用 channels 或者 dwebsocket。但是,channels 被更广泛地使用,因为它可以完美地集成到 Django 的生态系统中。

3. 在JavaScript中使用WebSocket

(1)创建 WebSocket 对象

1
let ws = new WebSocket('ws://localhost:8888');  // 如果是安全连接则地址为wss://...

(2)连接成功时的回调函数

当 WebSocket 连接成功时,onopen 事件会被触发。你可以在这个函数中发送消息到服务器:

1
2
3
4
ws.onopen = function(params) {
console.log('客户端连接成功');
ws.send('hello'); // 向服务器发送消息
};

(3)从服务器接收信息时的回调函数

当从服务器接收到信息时,onmessage 事件会被触发。你可以在这个函数中处理接收到的数据:

1
2
3
ws.onmessage = function(e) {
console.log('收到服务器响应', e.data);
};

(4)连接关闭时的回调函数

当连接关闭后,onclose 事件会被触发。你可以在这个函数中处理连接关闭后的逻辑:

1
2
3
ws.onclose = function(e) {
console.log("关闭客户端连接");
};

(5)连接失败时的回调函数

1
2
3
ws.onerror = function(e) {
console.log("连接失败");
};

(6)监听窗口关闭事件

当窗口关闭时,主动去关闭 WebSocket 连接,防止连接还没断开就关闭窗口,这样服务端会抛异常:

1
2
3
window.onbeforeunload = function() {
ws.close();
}

4. Django Channels

Django Channels 是一个开源框架,它扩展了 Django 的功能,使得 Django 不仅可以处理 HTTP,还可以处理需要长时间连接的协议,如 WebSocket、MQTT(消息队列遥测传输)、聊天协议、广播等实时应用。

Channels 允许 Django 项目支持“长连接”,它用 ASGI 替换了 Django 的默认 WSGI。ASGI(Asynchronous Server Gateway Interface)为异步 Python Web 服务器和应用程序提供了一个接口,同时支持 WSGI 提供的所有功能。

Channels 保留了 Django 的同步行为,并添加了一层异步协议,允许用户编写完全同步、异步或两者混合的视图(Views)。Django Channels 提供了一种通信系统,叫做 Channel Layer,它可以让多个 Consumer 实例之间互相通信,以及与外部 Django 程序实现互通。

  • Channel Layer 主要包括两种抽象概念:Channel 和 Group。Channel 是一个发送消息的通道,每个 Channel 都有一个名称,拥有这个名称的人都可以往 Channel 里面发送消息。Group 是多个 Channel 的集合,每个 Group 都有一个名称,拥有这个名称的人都可以往这个 Group 里添加/删除 Channel,也可以往 Group 里发送消息。Group 内的所有 Channel 都可以收到,但是不能给 Group 内的具体某个 Channel 发送消息。使用 Django Channels 可以实现一些实时通讯的功能,如在线聊天室、游戏、通知等。
  • Consumer 是 Channels 的基本单位,相当于 Django 的视图,它是一个事件驱动的类,可以处理不同类型的事件,如连接、断开、接收消息等,支持同步和异步应用程序。

现在我们来看一下 Django Channels 的样例,首先需要安装 Channels 和 Channels Redis:

1
pip install channels channels_redis

然后需要在你的项目设置中(settings.py 文件)配置 CHANNEL_LAYERS 需要添加 channels 到你的 INSTALLED_APPS 列表,并设置 ASGI_APPLICATIONCHANNEL_LAYERS。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
INSTALLED_APPS = [ 
'channels', # 添加此行
'game.apps.GameConfig',
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
]

ASGI_APPLICATION = 'djangoapp.asgi.application' # 'djangoapp'为你的项目名

CHANNEL_LAYERS = {
"default": {
"BACKEND": "channels_redis.core.RedisChannelLayer",
"CONFIG": {
"hosts": [("127.0.0.1", 6379)],
},
},
}

在这个例子中,我们假设你正在本地运行一个 Redis 服务器,它监听在6379端口。如果你的 Redis 服务器在其他地方或者使用了不同的端口,你需要更新 hosts 设置。

接下来你需要在 Django App 的目录下创建一个路由文件 routing.py,作用相当于 HTTP 的 urls,并在其中定义你的 WebSocket 路由。我们先创建出来:

1
2
3
4
from django.urls import path

websocket_urlpatterns = [
]

此外,你还需要运行一个兼容的 ASGI 服务器,如 Daphne 或 Uvicorn。我们安装 Daphne:

1
pip install daphne

输入 daphne 命令查看是否可用,如果不可用说明应该是没有配置环境变量,按如下方式修改环境变量(需要重启系统):

1
2
3
sudo vim /etc/environment
在 PATH='xxx' 后面添加 ':/home/<用户名>/.local/bin'
即: 'xxx:/home/<用户名>/.local/bin'

为了在 Django 项目中使用 Daphne,你需要确保你的项目已经配置为使用 ASGI 而不是 WSGI。这通常意味着你需要在你的项目中创建一个 asgi.py 文件,并在你的设置文件中设置 ASGI_APPLICATION 变量(之前已经设置好了)。

现在我们配置 djangoapp/djangoapp/asgi.py 文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
import os

from channels.auth import AuthMiddlewareStack
from channels.routing import ProtocolTypeRouter, URLRouter
from django.core.asgi import get_asgi_application
from game.routing import websocket_urlpatterns

os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'djangoapp.settings') # 'djangoapp'为你的项目名

application = ProtocolTypeRouter({
"http": get_asgi_application(),
"websocket": AuthMiddlewareStack(URLRouter(websocket_urlpatterns))
})

现在我们在 Django App 目录下创建 consumers.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
from channels.generic.websocket import AsyncWebsocketConsumer
import json

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
)

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
}))

在这个例子中,我们创建了一个 ChatConsumer 类,它是一个异步的 WebSocket Consumer,这个类继承自 AsyncWebsocketConsumer,这是 Django Channels 提供的一个基础类。

其中的主要函数说明如下:

  • async def connect(self):当一个 WebSocket 连接打开时,这个方法会被调用。在这个方法中,我们从 URL 路由中获取房间名,并将其保存在 self.room_name 中。然后,我们创建一个房间组名,并将其保存在 self.room_group_name 中。然后,我们将当前的 Channel 添加到房间组中。最后,我们接受 WebSocket 连接。
  • async def disconnect(self, close_code):当一个 WebSocket 连接关闭时,这个方法会被调用。在这个方法中,我们将当前的 Channel 从房间组中移除。
  • async def receive(self, text_data):当从 WebSocket 接收到消息时,这个方法会被调用。在这个方法中,我们首先将接收到的文本数据解析为 JSON,然后我们从 JSON 数据中获取消息,并将其发送到房间组中。
  • async def chat_message(self, event):这是一个自定义的事件处理器方法,当从房间组接收到类型为 chat_message 的事件时,这个方法会被调用。在这个方法中,我们首先从事件中获取消息,然后我们将消息发送回 WebSocket。

总的来说,这段代码的功能为:当用户连接到 WebSocket 时,他们会被添加到一个名为 chat_{room_name} 的组中。当他们发送消息时,这个消息会被广播到他们所在的组中的所有其他用户。当他们接收到组中的消息时,这个消息会被发送回他们的 WebSocket,即实现了一个简单的聊天室功能。

最后我们定义一下 Consumer 的路由,以下代码将 URL 路径 ws/chat/{room_name}/ 映射到我们的 ChatConsumer。这意味着当用户连接到这个路径的 WebSocket 时,他们会被连接到聊天室:

1
2
3
4
5
6
from django.urls import re_path
from .consumers import ChatConsumer

websocket_urlpatterns = [
re_path(r'ws/chat/(?P<room_name>\w+)/$', ChatConsumer.as_asgi()),
]