【玩转全栈】---- 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()

结果:

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

相关推荐
boring_student几秒前
CUL-CHMLFRP启动器 windows图形化客户端
前端·人工智能·python·5g·django·自动驾驶·restful
森焱森14 分钟前
星型组网和路由器组网的区别
网络·架构·智能路由器
c无序34 分钟前
【Linux网络-NAT、代理服务、内网穿透】
linux·网络·智能路由器
π大星星️1 小时前
VLAN综合实验报告
网络
冬冬小圆帽1 小时前
网络安全威胁与防护措施(下)
网络·web安全·php
柃歌1 小时前
【USTC 计算机网络】第二章:应用层 - P2P、CDN
网络·网络协议·计算机网络·p2p
-指短琴长-1 小时前
网络编程套接字【端口号/TCP&UDP/网络字节序/socket编程接口/UDP&&TCP网络实验】
网络·tcp/ip·udp
do_you_like_van_游戏1 小时前
新版frp-0.61.0 实现泛解析域名穿透 以及 https启用
网络协议·http·https
云祺vinchin1 小时前
Q&A:备份产品的存储架构采用集中式和分布式的优劣?
大数据·运维·网络·分布式·架构
潘多编程3 小时前
Spring Boot 3 新特性实战:从理论到实践
java·开发语言·网络