本文在 django 中实现 websocket 协议。

在 django 中使用 websocket

  纯净的 django 是不支持 websocket 的,要想实现 websocket 协议,我们需要更改一些配置。

Step1: 安装第三方包

  第三方包channels提供了在 django 中实现 websocket 通信协议的方式。我们安装这个包:

然后,我们前往settings.py下注册这个 app:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',

- 'django.contrib.staticfiles'

* 'django.contrib.staticfiles',


# MyApp

- 'channels'
]

一定要注意这里的逗号,千万不要遗漏,否则 django 会将没有被逗号分隔开的两个应用视作一个应用。

Step2: 配置 asgi

  前往项目的settings.py下配置 asgi:

1
2
3
4
WSGI_APPLICATION = '{ProjectName}.wsgi.application'

- ASGI_APPLICATION = '{ProjectName}.asgi.application'

然后前往asgi.py下更改默认配置,删除全部内容改写为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
"""
ASGI config for Project0728 project.

It exposes the ASGI callable as a module-level variable named `application`.

For more information on this file, see
https://docs.djangoproject.com/en/4.0/howto/deployment/asgi/
"""

import os
from django.core.asgi import get_asgi_application
from channels.routing import ProtocolTypeRouter, URLRouter
from . import routings

os.environ.setdefault('DJANGO_SETTINGS_MODULE', '{ProjectName}.settings')

application = ProtocolTypeRouter({
"http": get_asgi_application(), # http 路由列表
"websocket": URLRouter(routings.websocket_urlpatterns), # websocket 路由列表
})

Step3: 配置路由和视图类

  在settings.py同级目录下新建routings.py,其功能相当于urls.py,专门负责 websocket 协议的路由:

1
2
3
4
5
6
7
8
9
"""
routings.py
"""
from django.urls import path
from {应用名称} import consumers

websocket_urlpatterns = [
path('{URL}', consumers.{视图类}.as_asgi()),
]

  然后,前往需要使用到 websocket 的应用下新建consumers.py,其功能相当于views.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from channels.generic.websocket import WebsocketConsumer
from channels.exceptions import StopConsumer

class {类名}(WebsocketConsumer):

def websocket_connect(self, message):
# 客户端发来链接请求后自动执行
# 允许创建链接
self.accept()

def websocket_receive(self, message):
# 客户端发来数据后自动执行
pass

def websocket_disconnect(self, message):
# 客户端断开wb链接时自动触发
# print("断开连接")
raise StopConsumer()

以上是默认的结构,当然,你可以在routings.py中配置多个路由,然后在consumers.py中声明多个类。

运行程序并检查

  此时我们运行程序,发现控制台输出变成了:

1
2
3
4
August 02, 2022 - 16:12:48
Django version 4.0.6, using settings 'Project0728.settings'
Starting ASGI/Channels version 3.0.5 development server at http://127.0.0.1:8000/
Quit the server with CTRL-BREAK.

说明此时 django 项目同时支持 http 协议和 websocket 协议了。

前后端数据交互

后端

  后端对于 websocket 的操作相对较少,大多是对于数据的处理。主要用到的是self.send()self.close()

  self.send(),向建立连接的客户端发送数据,参数即为准备发送的数据。

  self.close(),服务端主动与客户端关闭连接。该函数被执行后,需要追加return停止函数继续运行。客户端接收到断开连接的请求后,客户端断开连接,同时,由于断开连接,服务端会自动执行websocket_disconnect()函数,然后抛出异常StopConsumer(),终止 websocket 连接。

前端

创建 websocket 连接

  前端可以主动与目标 url 创建 websocket 连接。首先需要新建一个WebSocket对象,初始化参数是目标 url。

1
ws = new WebSocket("ws://127.0.0.1:8000/room/123/");

该请求匹配到后端的路由:

1
path("room/<int>/",consumers.ChatRoom.as_asgi())

于是执行

1
2
3
4
5
6
"""
consumers.py
"""
class ChatRoom(WebsocketConsumer):
def websocket_connect(self, message):
self.accept() # 执行这句语句,即连接创建成功

  此时,前端可以使用ws.send()发送数据了。

回调函数

  当发生连接请求传递数据断开连接请求时,后端都有对应的函数会自动执行,前端也如此。这被称为回调函数。回调函数在满足条件后自动触发,JavaScript 中可以为 websocket 绑定三种回调函数。

  如果ws是 websocket 的一个实例化对象,那么可以使用ws.onopenws.onmessagews.onclose绑定函数。例如:

1
2
3
4
5
6
ws.onmessage = function(event){
let tag = document.createElement("div");
tag.innerText = event.data;
document.getElementById("msg").appendChild(tag);
console.log("已展示最新数据");
}

上述函数在后端主动发送数据后被自动执行,结果是向 id 为msg的块级内容写入新的元素。其中,event封装了后端发送的所有数据,利用event.data可以提取出数据。

  更多地,onopen发生在建立连接后,onclose发生在收到后端关闭连接请求后。

多客户端的管理:聊天室为例

更改配置文件

  使用 channel_layers,可以对同时连接的多个客户端进行管理。相关的数据需要被写入内存当中,因此需要在配置文件中添加:

1
2
3
4
5
6
7
8
9
"""
settings.py
"""

CHANNEL_LAYERS = {
"default": {
"BACKEND": "channels.layers.InMemoryChannelLayer",
}
}

将新连接的客户端写入内存中

  我们需要将新建立的连接写入内存。对实例对象调用channel_layer.group_add()方法。例如:

1
2
3
4
from asgiref.sync import async_to_sync

def websocket_connect(self, message): # 将连接加入组中,self.channel_name 表示在该组中该连接的名称。
async_to_sync(self.channel_layer.group_add)({加入的组名}, self.channel_name)

其中,由于上述方法只支持异步,然而我们并没有编写异步代码,因此需要使用同步的方式完成该操作asnyc_to_sync是将方法由异步转变成为同步。

  获得新消息时,对同一组内所有连接群发消息,可以使用group_send()完成。例如:

1
2
def websocket_message(self, message): # 对组名中所有连接对象调用方法名所对应的方法,字典作为参数传入
async_to_sync(self.channel_layer.group_send)({组名},{"type": {方法名},"message": message})

如果方法定义为:

1
2
def {方法名}(self, event):
self.send(event['message']['text'])

也就意味着对群组内所有连接都发送message['text']

  断开连接时,需要从组内同时剔除该连接。可以使用:

1
2
def websocket_disconnect(self, message):
async_to_sync(self.channel_layer.group_discard)({组名}, self.channel_name)

这样,当连接终止时,自动触发websocket_disconnect,该连接就从 group 中被剔除了。

- -