1. Channels 介绍

Django 中的 HTTP 请求是建立在请求和响应的简单概念之上的。浏览器发出请求,Django服务调用相应的视图函数,并返回响应内容给浏览器渲染。但是没有办法做到 服务器主动推送消息给浏览器。

因此,WebSocket 就应运而生了。WebSocket 是一种基于 HTTP 基础上进行全双工通讯的协议。WebSocket允许服务端主动向客户端推送数据。在WebSocket协议中,浏览器和服务器只需要完成一次握手就可以创建持久性的连接,并在浏览器和服务器之间进行双向的数据传输。

Django Channels 实现了 WebSocket 能力。Channels 允许 Django 以非常类似于传统 HTTP 的方式支持WebSockets。Channels 也允许在运行 Django 的服务器上运行后台任务,HTTP 请求表现以前一样,但也支持通过 Channels 进行路由。

Django channels安装:pip install channels

2. channels 单人聊天

配置文件 settings.py:

INSTALLED_APPS = [

...

'app01.apps.App01Config',

'channels'

]

ASGI_APPLICATION = 'djangoProject.asgi.application'

主路由文件 urls.py:

from app01 import views as vw1

urlpatterns = [

path('admin/', admin.site.urls),

path("chatone", vw1.chat)

]

主业务视图文件 app01/views.py:

from django.shortcuts import render

def chat(request):

return render(request, "chatting.html")

主业务html文件 chatting.html:

Title

websocket路由文件 routings.py:

from django.urls import re_path

from app01 import consumers as consm1

websocket_urlpatterns = [

re_path(r'room/', consm1.ChatConsumer.as_asgi())

]

处理websocket业务文件 app01/consumers.py:

from channels.exceptions import StopConsumer

from channels.generic.websocket import WebsocketConsumer

class ChatConsumer(WebsocketConsumer):

def websocket_connect(self, message):

"""

客户端向服务端发送websocket连接的请求时自动触发。

"""

print("1 > 客户端和服务端开始建立连接")

self.accept()

def websocket_receive(self, message):

"""

客户端基于websocket向服务端发送数据时,自动触发接收消息。

"""

print(f"2 > 服务端接收客户端的消息, message is {message}")

recv_data = message["text"]

if recv_data == "exit": # 服务端主动关闭websocket连接时,前端会执行对应的 onclose

self.close()

# raise StopConsumer() # raise主动抛异常后,websocket_disconnect 就不在执行了,多用于`只处理服务端向客户端断开`的场景

return

send_data = f"服务端主动推送消息:{recv_data}"

self.send(text_data=send_data)

def websocket_disconnect(self, message):

"""

客户端与服务端断开websocket连接时自动触发(不管是客户端向服务端断开还是服务端向客户端断开都会执行)

"""

print("3 > 客户端和服务端断开连接")

self.close()

raise StopConsumer()

3. channel_layer 群组聊天

对于大多数情况来说,发送到单人的 channel 并没有用,更多的情况下希望可以以广播的方式将message 一次性发送给多个 channel 或者 consumer,这不仅适用于想在向房间内的每个人发送消息,还适用于发送给连接了多个浏览器/标签/设备的用户。

channel_layer 是一种通信系统。它允许多个消费者实例相互交谈,借助 channel_layer 可以很方便的实现群聊功能,我们无需手动管理 websocket 连接。channel 官方推荐的是配置channel_redis,它是一个使用Redis作为传输的Django维护层。安装:pip install channels_redis。

channel_layer 属于纯粹的异步接口,如果想要改成同步代码调用,需要使用async_to_sync做转换: from asgiref.sync import async_to_sync。

配置文件 settings.py:

ASGI_APPLICATION = 'djangoProject.asgi.application'

# 开发环境使用

CHANNEL_LAYERS = {

"default": {

"BACKEND": "channels.layers.InMemoryChannelLayer"

}

}

# 真实生产环境上使用

CHANNEL_LAYERS = {

"default": {

"BACKEND": "channels_redis.core.RedisChannelLayer",

"CONFIG": {

"hosts": ["redis://127.0.0.1:6379/1", ], # 无密码连接redis

# "hosts": ["redis://:password@127.0.0.1:6379/1", ], # 有密码连接redis

# "symmetric_encryption_keys": [SECRET_KEY]

}

}

}

主路由文件 urls.py:

from django.contrib import admin

from django.urls import path

from app01 import views as vw1

from app02 import views as vw2

urlpatterns = [

path('admin/', admin.site.urls),

path("chatone", vw1.chat),

path("chatgroup", vw2.groupchat) # 群聊

]

主业务视图 app02/views.py:

from django.shortcuts import render

def groupchat(request):

groupid = request.GET.get("groupID") # 获取群组ID

return render(request, "groupchatting.html", {"group_num": groupid})

主业务html文件 chatting.html: js在实例化websocket对象时需要改动。

Title

群组内聊天[ 群ID: {{ group_num }} ]

websocket 路由文件 routings.py:

from django.urls import re_path

from app01 import consumers as consm1

from app02 import consumers as consm2

websocket_urlpatterns = [

re_path(r'room/', consm1.ChatConsumer.as_asgi()),

re_path(r"group/(?P\w+)/$", consm2.GroupChatConsumer.as_asgi()) # 群聊

]

websocket 群聊业务文件 consumers.py: 注意:从路由url中获取参数时,要使用 self.scope["url_route"]["kwargs"].get("groupID"),这里的 groupID 必须和routings中的路由分组名 re_path(r"group/(?P\w+)/$" 保持一致!!!

from asgiref.sync import async_to_sync

from channels.exceptions import StopConsumer

from channels.generic.websocket import WebsocketConsumer

class GroupChatConsumer(WebsocketConsumer):

group_id = None

def websocket_connect(self, message):

# 接受客户端连接

self.accept()

print(">>> 客户端和服务端已成功建立连接 <<<")

# 从url获取群组id,这个groupID必须和routings里的路由分组名保持一致

self.group_id = self.scope["url_route"]["kwargs"].get("groupID")

# 将当前的连接加入到名为self.group_id的组中

async_to_sync(self.channel_layer.group_add)(self.group_id, self.channel_name)

def websocket_receive(self, message):

print(f"current self is {self}, id is {id(self)}, groupID is {self.group_id}")

# 组内所有的客户端,执行type对应的函数,可以在此函数中自定义任意功能

async_to_sync(self.channel_layer.group_send)(self.group_id, {"type": "send_msg", "message": message})

def send_msg(self, event):

input_msg = event["message"].get("text")

send_msg = f"组内主动推送消息:{input_msg}"

self.send(text_data=send_msg)

def websocket_disconnect(self, message):

# self.channel_name从组self.group_id中删除并断开连接

async_to_sync(self.channel_layer.group_discard)(self.group_id, self.channel_name)

print(">>> 客户端和服务端已断开连接 <<<")

raise StopConsumer()

查看原文