【玩转全栈】---- Django 基于 Websocket 实现群聊(解决channel连接不了)

学习视频:

14-11 群聊(一)_哔哩哔哩_bilibili

目录

[Websocket 连接不了?](#Websocket 连接不了?)

收发数据

断开连接

完整代码

聊天室的实现

聊天室一

聊天室二

[settings 配置](#settings 配置)

[consumer 配置](#consumer 配置)

多聊天室


Websocket 连接不了?

基于这篇博客:

【全栈开发】---- 一文掌握 Websocket 原理,并用 Django 框架实现_django websocket-CSDN博客

之前这篇博客虽然大致原理都介绍了,但最终的代码并没有实现,这是因为博主当时遇见了一个问题,尽管我按照教程来的,但是 websocket 服务就是连不上,后面也参考了许多博客,也去官网看了,还去 github 上抄项目来对比,都解决不了,后来急得我转 SpringBoot 去了。但偶然间发现了这篇博客:

https://blog.csdn.net/qq_25218219/article/details/131752459Django的websocket

最终问题才得以解决,再次感谢这位博主!!!

++解决办法很简单,基于上面学习视频的配置后,需要在注册组件的 "channels" 前面添加一个组件 "daphne"++

python 复制代码
INSTALLED_APPS = [
    "daphne",
    "channels",
    "django.contrib.admin",
    "django.contrib.auth",
    "django.contrib.contenttypes",
    "django.contrib.sessions",
    "django.contrib.messages",
    "django.contrib.staticfiles",
    "app01.apps.App01Config"
]

然后运行就能连上 asgi 了:

大致原因是 pip install channels 按照命令默认按照的是最新版的 channels ,可能与 Django 版本并不匹配。

收发数据

websocket 模式中,服务端和客户端都能主动收发数据:

在客户端发数据:

javascript 复制代码
function sendMessage(){
        var txt = document.getElementById("txt")
        console.log(txt.value)
        socket.send(txt.value)
    }

在服务端收数据:

javascript 复制代码
    def websocket_receive(self, message):
        # 收数据message
        print("接收消息-->",message["text"])

在服务端发数据:

使用 send() 方法即可

javascript 复制代码
    def websocket_connect(self,message):
        print("发送连接请求")
        self.accept()
        # 发数据
        self.send("来了呀客官")
    def websocket_receive(self, message):
        # 收数据message
        print("接收消息-->",message["text"])
        self.send(message["text"])

在客户端收数据:

这里的服务端发数据在发送 websocket 连接函数和接收消息函数中都可,相对于,在客户端收数据也对应两种方法,一个是 socket.onopen , 创建好连接后自动触发(握手环节,服务端执行self.accept() );还有一个就是 socket.onmessage,用于正常接收数据。

javascript 复制代码
socket.onopen = function(event){
        console.log(event.value)
        let lag = document.createElement("div")
        lag.innerText = "[websocket连接成功]"
        document.getElementById("message").appendChild(lag)
    }

{#收数据#}
socket.onmessage = function (event){
        var data = event.data
        console.log("客户端接收到消息-->",data)
        let lag = document.createElement("div")
        lag.innerText = data
        document.getElementById("message").appendChild(lag)
    }

断开连接

在服务端断开连接一般是经过下面这个函数:

javascript 复制代码
    def websocket_disconnect(self, message):
        # 浏览器关闭也会自动发送断开链接请求
        print("断开连接")
        # 服务端同意断开连接
        raise StopConsumer()

这个函数不仅仅关闭浏览器的请求链接,还会关闭服务端链接,实现完全断连。在类中其他函数中可使用self.close() 来调用此关闭链接函数,实现完全断连;而如果用 raiseStopConsumer() , 则表示仅仅断开服务器连接,也不会执行 websocket_disconnect 函数。

服务器断开连接时,客户端也会触发一个函数:

javascript 复制代码
socket.onclose = function (event){

    }

并且客户端也可以设置按钮,主动断开连接:

javascript 复制代码
function closeOnn(){
        socket.close()
    }

完整代码

index.html:

html 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <style>
        .message{
            height: 300px;
            width: 100%;
            border: 1px solid #dddddd;
        }
    </style>
</head>
<body>
<div class="message" id="message"></div>
<div>
    <input type="text" placeholder="请输入" id="txt">
    <input type="button" value="发送" onclick="sendMessage()">
    <input type="button" value="断开连接" onclick="closeOnn()">
</div>
<script>
    socket = new WebSocket("ws://127.0.0.1:8080/room/123/")
    {#创建好连接后自动触发(握手环节,服务端执行self.accept())#}
    socket.onopen = function(event){
        console.log(event.value)
        let lag = document.createElement("div")
        lag.innerText = "[websocket连接成功]"
        document.getElementById("message").appendChild(lag)
    }
    {#发数据#}
    function sendMessage(){
        var txt = document.getElementById("txt")
        console.log(txt.value)
        socket.send(txt.value)
    }
    {#收数据#}
    socket.onmessage = function (event){
        var data = event.data
        console.log("客户端接收到消息-->",data)
        let lag = document.createElement("div")
        lag.innerText = data
        document.getElementById("message").appendChild(lag)
    }
    {#服务器主动断开连接,触发#}
    socket.onclose = function (event){

    }
    function closeOnn(){
        socket.close()
    }
</script>
</body>
</html>

consumers.py:

python 复制代码
from channels.generic.websocket import WebsocketConsumer
from channels.exceptions import StopConsumer
# socket = new WebSocket("ws://127.0.0.1:8000/room/123/")
class ChatConsumer(WebsocketConsumer):
    print("进入消费者")
    def websocket_connect(self,message):
        print("发送连接请求")
        self.accept()
        # 发数据
        self.send("来了呀客官")
    def websocket_receive(self, message):
        # 收数据message
        print("接收消息-->",message["text"])
        # 服务器主动断开连接
        if message["text"] == "关闭":
            self.close()
            # 如果在这儿加上下面代码,执行StopConsumer异常,那么就不会执行websocket_disconnect
            raise StopConsumer()
            # return
        self.send(message["text"])
    # 调用self.close()方法默认都会调用下面这个函数
    def websocket_disconnect(self, message):
        # 浏览器关闭也会自动发送断开链接请求
        print("断开连接")
        # 服务端同意断开连接
        raise StopConsumer()

聊天室的实现

当然,上面只是介绍 websocket 的一般使用,还并没有实际应用,下面将以聊天室场景进行应用。

聊天室一

前面的操作都是基于 self 来的。服务端仅仅关心自己与对应浏览器的连接通道,而不会联系到其它浏览器。可使用列表存储各个用户,某用户想断开连接或者主动退出浏览器时,再到列表中删除用户:

需要注意的是,用户添加到列表中后,后续的一系列操作需要在列表中循环操作每一个对象,以实现群聊

python 复制代码
from channels.generic.websocket import WebsocketConsumer
from channels.exceptions import StopConsumer

CONN_LIST = []
class ChatConsumer(WebsocketConsumer):
    print("进入消费者")
    def websocket_connect(self,message):
        print("发送连接请求")
        self.accept()
        CONN_LIST.append(self)
    def websocket_receive(self, message):
        res = message["text"]
        # 收数据message
        print("接收消息-->",res)
        for conn in CONN_LIST:
            conn.send(res)
    # 调用self.close()方法默认都会调用下面这个函数
    def websocket_disconnect(self, message):
        # 浏览器关闭也会自动发送断开链接请求
        print("断开连接")
        CONN_LIST.remove(self)
        # 服务端同意断开连接
        raise StopConsumer()

结果:

聊天室二

聊天室一虽然能实现简单的群聊功能,但是使用列表来储存各个用户,其实效率会很低,并且功能也不强大,Django 的 channels 组件中有一个更加厉害的东西叫 channel layers,可以帮助我们更加方便地去实现这种群聊。

参考文章:django channels - 武沛齐 - 博客园

settings 配置

layers 需要在 setting 中进行配置:

python 复制代码
CHANNEL_LAYERS = {
    "default": {
        "BACKEND": "channels.layers.InMemoryChannelLayer",
    }
}

consumer 配置

再修改 Consumer 代码:

python 复制代码
from channels.generic.websocket import WebsocketConsumer
from channels.exceptions import StopConsumer
from asgiref.sync import async_to_sync

class ChatConsumer(WebsocketConsumer):
    print("进入消费者")
    def websocket_connect(self,message):
        print("发送连接请求")
        self.accept()
        # 将这个客户端的连接对象加入到某个地方(内存或redis),channel_name 随机给个名字
        async_to_sync(self.channel_layer.group_add)("111",self.channel_name)
    def websocket_receive(self, message):
        res = message["text"]
        # 收数据message
        print("接收消息-->",res)
        async_to_sync(self.channel_layer.group_send)("111",{"type":"send_to","message":message})
    def send_to(self,event):
        # 群中每一个连接对象都发送
        text = event["message"]["text"]
        self.send(text)
    # 调用self.close()方法默认都会调用下面这个函数
    def websocket_disconnect(self, message):
        # 浏览器关闭也会自动发送断开链接请求
        print("断开连接")
        async_to_sync(self.channel_layer.group_discard)("111",self.channel_name)
        # 服务端同意断开连接
        raise StopConsumer()

部分解释:

需要注意的是,这里的 channel_layer 操作都是异步进行的,需要自己导入 async_to_sync进行异步转同步操作。

python 复制代码
async_to_sync((self.channel_layer.group_add)("111",self.channel_name)

这里的作用是将本连接对象存入 channel_layer 中,并且 group 名为 "111" ,self.channel_name 的作用是连接对象存储时,随机给一个名字。

python 复制代码
async_to_sync(self.channel_layer.group_send)("111",{"type":"send_to","message":message})
python 复制代码
def send_to(self,event):
    # 群中每一个连接对象都发送
    text = event["message"]["text"]
    self.send(text)

这里的作用是为 "111" 群聊中每个连接对象执行 type 对应的方法,并传入 message 给每个连接对象;下面的 send_to 方法就是为每一个连接对象发送 text消息。

python 复制代码
async_to_sync(self.channel_layer.group_discard)("111",self.channel_name)

这里的作用是为群聊中的每一个连接对象关闭连接。

上诉代码已能实现聊天室功能,但还不够高级,因为群聊 id 是固定的。下面介绍在浏览器中打开多个聊天室,各个聊天室之间有不同的 id ,各个聊天室之前互不干扰。

多聊天室

实现思路是通过 httpget 传参将群号传给视图函数,视图函数给index.html 页面,在 index 页面构造 websocket url 并加入群号,在 consumer中获取群号,并替换群号为原先的固定群号。

实现:

视图函数传参:

python 复制代码
def index(request):
    QQ_number = request.GET.get('qq')
    return render(request, 'index.html', {'QQ_number': QQ_number})

index 页面 websocket传参:

javascript 复制代码
socket = new WebSocket("ws://127.0.0.1:8080/room/{{ QQ_number }}/")

routings中正则接收参数:

python 复制代码
websocket_urlpatterns = [
    re_path(r'^room/(?P<group>\w+)/$', consumers.ChatConsumer.as_asgi()),
]

consumer 中接收 group 并修改群号为 group:

python 复制代码
from channels.generic.websocket import WebsocketConsumer
from channels.exceptions import StopConsumer
from asgiref.sync import async_to_sync

class ChatConsumer(WebsocketConsumer):
    print("进入消费者")
    def websocket_connect(self,message):
        print("发送连接请求")
        self.accept()
        group = self.scope['url_route']['kwargs'].get('group')
        # 将这个客户端的连接对象加入到某个地方(内存或redis),channel_name 随机给个名字
        async_to_sync(self.channel_layer.group_add)(group,self.channel_name)
    def websocket_receive(self, message):
        group = self.scope['url_route']['kwargs'].get('group')
        res = message["text"]
        # 收数据message
        print("接收消息-->",res)
        async_to_sync(self.channel_layer.group_send)(group,{"type":"send_to","message":message})
    def send_to(self,event):
        # 群中每一个连接对象都发送
        text = event["message"]["text"]
        self.send(text)
    # 调用self.close()方法默认都会调用下面这个函数
    def websocket_disconnect(self, message):
        group = self.scope['url_route']['kwargs'].get('group')
        # 浏览器关闭也会自动发送断开链接请求
        print("断开连接")
        async_to_sync(self.channel_layer.group_discard)(group,self.channel_name)
        # 服务端同意断开连接
        raise StopConsumer()

结果:

这样即能实现多聊天室,各个聊天室互不打扰。

相关推荐
Python私教1 小时前
DRF:Django REST Framework框架介绍
后端·python·django
我也要当昏君2 小时前
6.3 文件传输协议 (答案见原书 P277)
网络
Greedy Alg2 小时前
Socket编程学习记录
网络·websocket·学习
刘逸潇20053 小时前
FastAPI(二)——请求与响应
网络·python·fastapi
软件技术员3 小时前
使用ACME自动签发SSL 证书
服务器·网络协议·ssl
我也要当昏君3 小时前
6.4 电子邮件 (答案见原书 P284)
网络协议
Mongnewer3 小时前
通过虚拟串口和网络UDP进行数据收发的Delphi7, Lazarus, VB6和VisualFreeBasic实践
网络
我也要当昏君4 小时前
6.5 万维网(答案见原书P294)
网络
嶔某4 小时前
网络:传输层协议UDP和TCP
网络·tcp/ip·udp
文火冰糖的硅基工坊5 小时前
[嵌入式系统-154]:各种工业现场总线比较
网络·自动驾驶·硬件架构