django channels实战

概述

直播聊天室的解决方案

  1. 轮询:让浏览器每隔1s向后台发送一次请求,缺点:延迟响应、请求太多服务器压力太大
  2. 长轮询:客户端向服务端发送请求,服务端最多夯住20s,一旦有数据到来就立即返回,数据响应没有延迟
  3. websocket:客户端和服务端创建链接之后默认不断开,那么就可以实现双向通信

轮询实现聊天室

  1. 后台代码
import json

from django.shortcuts import render, HttpResponse
from django.http import JsonResponse

# Create your views here.

DB = []


def home(request):
    return render(request, "home.html")


def send_msg(request):
    text = request.GET.get("text")
    DB.append(text)
    return HttpResponse("OK")


def get_msg(request):
    index = int(request.GET.get("index"))
    context = {
        "data": DB[index:],
        "max_index": len(DB),
    }

    return JsonResponse(context)
urlpatterns = [
    path('admin/', admin.site.urls),
    path('home/', views.home),
    path('send/msg/', views.send_msg),
    path('get/msg/', views.get_msg),
]
  1. 前台代码
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <style>
        .message{
            height: 300px;
            border: 1px solid #dddddd;
            width: 100%;
        }
    </style>
</head>
<body>
    <div class="message"></div>
    <input type="text" placeholder="请输入" id="txt">
    <input type="button" value="发送" onclick="sendMessage();">

    <script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
    <script>

        function sendMessage() {
            var text = $("#txt").val();

            // 基于ajax将用户输入的请求发送到后台
            $.ajax({
                url: "/send/msg/",
                type: "GET",
                data: {
                    text: text,
                },
                success: function (res) {
                    console.log("请求发送成功", res);
                }
            });
        }

        max_index = 0
        // 每2s向后台发送请求,获取数据
        setInterval(function () {
            $.ajax({
                url: "/get/msg/",
                type: "GET",
                data: {
                    index: max_index,
                },
                dataType: "JSON",
                success: function (res) {
                    console.log("获取到数据:", res)

                    var dataArray = res.data
                    max_index = res.max_index

                    $.each(dataArray, function (index, item) {
                        console.log(index, item)

                        var tag = $("<div>")
                        tag.text(item)
                        $(".message").append(tag)

                    })
                }
            })
        }, 2000)

    </script>
</body>
</html>

长轮询实现聊天室

![在这里插入图片描述](https://img-blog.csdnimg.cn/be6a5a5238e9405ea4072bca4c265b5f.pn

  1. 后台代码:
import queue

from django.shortcuts import render, HttpResponse
from django.http import JsonResponse

# Create your views here.

USER_QUEUE = {}


def home(request):
    uid = request.GET.get("uid")
    USER_QUEUE[uid] = queue.Queue()
    return render(request, "home.html", {"uid": uid})


def send_msg(request):
    text = request.GET.get("text")
    for uid, q in USER_QUEUE.items():
        q.put(text)
    return HttpResponse("OK")


def get_msg(request):
    # 去自己队列获取数据
    uid = request.GET.get("uid")
    q = USER_QUEUE.get(uid)

    result = {"status": True, "data": None}

    try:
        data = q.get(timeout=10)
        result["data"] = data
    except queue.Empty:
        result["status"] = False

    return JsonResponse(result)

urlpatterns = [
    path('admin/', admin.site.urls),
    path('home/', views.home),
    path('send/msg/', views.send_msg),
    path('get/msg/', views.get_msg),
]

  1. 前台代码
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <style>
        #message{
            height: 300px;
            border: 1px solid #dddddd;
            width: 100%;
        }
    </style>
</head>
<body>
    <div id="message"></div>
    <input type="text" placeholder="请输入" id="txt">
    <input type="button" value="发送" onclick="sendMessage();">

    <script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
    <script>

        USER_UID = "{{ uid }}"

        function sendMessage() {
            var text = $("#txt").val();

            // 基于ajax将用户输入的请求发送到后台
            $.ajax({
                url: "/send/msg/",
                type: "GET",
                data: {
                    text: text,
                },
                success: function (res) {
                    console.log("请求发送成功", res);
                }
            });
        }

        function getMessage() {
            $.ajax({
                url: "/get/msg/",
                data: {
                    uid: USER_UID,
                },
                type: "GET",
                dataType: "JSON",  // 后台传递过来的数据类型必须是JSON
                success: function (res) {
                    // 超时,没有获取到新数据
                    // 有新数据,展示新数据
                    if (res.status) {
                        var tag = $("<div>")
                        tag.text(res.data)
                        $("#message").append(tag)
                    }
                    // js中的这种模式底层实现不是通过递归,所以这样一直调用没有问题
                    getMessage()
                }
            })
        }

        // 当页面框架加载完成之后执行
        $(function () {
            getMessage()
        })

    </script>
</body>
</html>

问题:服务端持有这个链接,压力是否会很大?
我们目前采用的长轮询案例:100线程同时只能有100个用户请求,其他用户就需要等待。
如果是基于IO多路复用+异步,那么就既能节省资源,又可以提高并发量

初始 websocket

websocket: web版的socket
原来web中:

  • http协议(无状态、短链接):客户端主动链接服务端,客户端向服务端发送消息,服务端给客户端响应数据,客户端接收数据,断开链接。
  • https基于http+ssl/tls(对数据进行加密)
  • websocket协议:创建持久链接不断开,基于这个链接进行收发数据,当服务端向客户端推送消息的时候,就需要使用websocket协议了。
  • websocket应用场景:聊天室、数据大屏

websocket握手

http协议:

  • 链接
  • 数据传输
  • 断开链接
    websocket协议:
  • 链接,客户端发起
  • 握手,客户端发送一个消息,后端在接收到消息后在做一些特殊处理并返回,服务端支持websocket协议
  • 收发数据(加密)
  • 断开链接

请求和响应的【握手】信息需要遵循规则:

  • 从请求【握手】信息中提取 Sec-WebSocket-Key
  • 利用magic_string 和 Sec-WebSocket-Key 进行hmac1加密,再进行base64加密
  • 将加密结果响应给客户端

注:magic string为:258EAFA5-E914-47DA-95CA-C5AB0DC85B11
请求握手信息为:

GET /chatsocket HTTP/1.1
Host: 127.0.0.1:8002
Connection: Upgrade
Pragma: no-cache
Cache-Control: no-cache
Upgrade: websocket
Origin: http://localhost:63342
Sec-WebSocket-Version: 13
Sec-WebSocket-Key: mnwFxiOlctXFN/DeMt1Amg==
Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits
...
...

提取Sec-WebSocket-Key值并加密:

magic_string = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'
value = headers['Sec-WebSocket-Key'] + magic_string
ac = base64.b64encode(hashlib.sha1(value.encode('utf-8')).digest())

参考文档
服务端响应头:

"HTTP/1.1 101 Switching Protocols
"Upgrade:websocket
"Connection: Upgrade
"Sec-WebSocket-Accept: ac

websocket数据解密

解密过程:
在这里插入图片描述

django中配置channels

django默认不支持websocket,需要安装组件
pip install channels

配置:
注册channels应用:

INSTALLED_APPS = [
	...
    'channels',
]

settings.py中增加:

ASGI_APPLICATION = "ws_demo.asgi.application"

修改asgi.py文件:

import os

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

from . import routing

os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'ws_demo.settings')

# application = get_asgi_application()
application = ProtocolTypeRouter({
    "http":  get_asgi_application(),
    "websocket": URLRouter(routing.websocket_urlpatterns),
})

在settings同级目录创建routing.py文件

from django.urls import re_path

from app01 import consumers

websocket_urlpatterns = [
    re_path(r"ws/(?P<group>\w+)/$", consumers.ChatConsumer.as_asgi()),
]

在app01目录下创建consumers.py文件,编写处理websocket的业务逻辑

from channels.generic.websocket import WebsocketConsumer
from channels.exceptions import StopConsumer


class ChatConsumer(WebsocketConsumer):
    def websocket_connect(self, message):
        # 有客户端向后端发送websocket链接的请求时, 自动触发
        # 服务端允许和客户端创建链接
        self.accept()
        # 服务端不允许和客户端创建链接
        # raise StopConsumer()

    def websocket_receive(self, message):
        # 浏览器基于websocket向后端发送数据的时候, 自动触发接收消息
        print(message)
        self.send("不要回复,不要回复")

        # 服务端主动断开链接
        # self.close()

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

在django中需要了解的:

  • wsgi: Web服务器网关接口,是Python为了解决Web服务器与客户端之间的通信基于CGI标准而设计的。实现WSGI协议的服务器有uWSGI、uvicorn、gunicorn。
  • asgi: wsgi+异步+websocket

http:

  • urls.py
  • views.py

websocket:

  • routing.py
  • consumers.py

注意一点: 如果在channels4.0开始,注册组件使用daphne,一定放在开头
`pip install daphne’

INSTALLED_APPS = [
    "daphne",  #注册daphne组件,在channels4.0开始,注册组件使用daphne,一定放在开头
    ...
]

websocket收发数据

  • 访问地址,看到聊天室页面,http请求
  • 让客户端主动向服务端发起websocket链接,服务端接收到链接后通过(握手)

demo版本:

  • 客户端
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <style>
        .message {
            height: 300px;
            border: 1px solid #dddddd;
            width: 100%;
        }
    </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="closeConn()">
</div>

<script>
    socket = new WebSocket("ws://127.0.0.1:8000/room/123/");
    // 创建好链接之后,自动触发(服务端执行self.accept())
    socket.onopen = function (event) {
        let tag = document.createElement("div")
        tag.innerText = "[链接成功]"
        document.getElementById("message").appendChild(tag)
    }
    // 当websocket接收到服务端发来的消息时,自动会触发这个函数
    socket.onmessage = function (event) {
        let tag = document.createElement("div")
        tag.innerText = event.data
        document.getElementById("message").appendChild(tag)
    }
    // 服务端主动断开链接时,会被触发
    socket.onclose = function (event) {
        let tag = document.createElement("div")
        tag.innerText = "[断开链接]"
        document.getElementById("message").appendChild(tag)
    }
    // 发送数据
    function sendMessage() {
        let tag = document.getElementById("txt")
        socket.send(tag.value)
    }
    function closeConn() {
        socket.close()  // 向服务端发送断开链接的请求
    }
</script>
</body>
</html>

  • 服务端
from channels.generic.websocket import WebsocketConsumer
from channels.exceptions import StopConsumer


class ChatConsumer(WebsocketConsumer):
    def websocket_connect(self, message):
        # 有客户端向后端发送websocket链接的请求时, 自动触发
        print("有人来链接了...")

        # 服务端允许和客户端创建链接(握手)
        self.accept()

        # 给客户端发送消息
        # self.send("来了呀,客官")

        # 服务端不允许和客户端创建链接
        # raise StopConsumer()

    def websocket_receive(self, message):
        # 浏览器基于websocket向后端发送数据的时候, 自动触发接收消息
        print("接收到消息", message["text"])  # {'type': 'websocket.receive', 'text': '你好啊'}

        if message["text"] == "关闭":
            # 服务端主动断开链接, 给客户端发送一条断开链接的消息
            self.close()
            # 如果服务端断开链接时,执行StopConsumer()异常,那么websocket_disconnect将不会再执行
            raise StopConsumer()
            # return

        res = "{}SB".format(message["text"])
        self.send(res)

        # 服务端主动断开链接
        # self.close()

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

小结: 上面基于django实现的websocket请求,只能对某个人进行处理。

群聊(1)

  1. 前端代码:跟上面一样
  2. 后端代码:
from channels.generic.websocket import WebsocketConsumer
from channels.exceptions import StopConsumer


CONN_OBJ_LIST = []


class ChatConsumer(WebsocketConsumer):
    def websocket_connect(self, message):
        # 有客户端向后端发送websocket链接的请求时, 自动触发
        print("有人来链接了...")

        # 服务端允许和客户端创建链接(握手)
        self.accept()
        CONN_OBJ_LIST.append(self)

        # 给客户端发送消息
        # self.send("来了呀,客官")

        # 服务端不允许和客户端创建链接
        # raise StopConsumer()

    def websocket_receive(self, message):
        # 浏览器基于websocket向后端发送数据的时候, 自动触发接收消息
        print("接收到消息", message["text"])  # {'type': 'websocket.receive', 'text': '你好啊'}
        res = "{}SB".format(message["text"])
        for conn in CONN_OBJ_LIST:
            conn.send(res)

        # 服务端主动断开链接
        # self.close()

    def websocket_disconnect(self, message):
        # 客户端与服务端断开链接时,自动触发
        print("断开链接")
        CONN_OBJ_LIST.remove(self)
        raise StopConsumer()

群聊(2)

基于channels中提供的channel layers来实现

  • settings中进行配置
# channel layers
CHANNEL_LAYERS = {
    "default": {
        "BACKEND": "channels.layers.InMemoryChannelLayer",
    }
}

基于redis的channle layer
pip install channels-redis

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


CHANNEL_LAYERS = {
    'default': {
    'BACKEND': 'channels_redis.core.RedisChannelLayer',
    'CONFIG': {"hosts": ["redis://10.211.55.25:6379/1"],},
    },
}
 

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

CHANNEL_LAYERS = {
    "default": {
        "BACKEND": "channels_redis.core.RedisChannelLayer",
        "CONFIG": {
            "hosts": ["redis://:password@10.211.55.25:6379/0"],
            # "symmetric_encryption_keys": [SECRET_KEY],
        },
    },
}
  • consumers中需要做一些修改
from channels.generic.websocket import WebsocketConsumer
from channels.exceptions import StopConsumer
from asgiref.sync import async_to_sync


class ChatConsumer(WebsocketConsumer):
    def websocket_connect(self, message):
        # 接受客户端的链接
        self.accept()
        # 获取群号, 路由匹配当中的
        group = self.scope["url_route"]["kwargs"].get("group")
        # 将这个客户端的链接对象加入到某个地方(内存,redis)
        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")
        # 通知组内所有客户端,执行xx_oo方法,在此方法中自己可以去定义任意的功能
        async_to_sync(self.channel_layer.group_send)(group, {"type": "xx.oo", "message": message})

    def xx_oo(self, event):
        text = event["message"]["text"]
        self.send(text)

    def websocket_disconnect(self, message):
        group = self.scope["url_route"]["kwargs"].get("group")
        # 将当前客户端从组内移除
        async_to_sync(self.channel_layer.group_discard)(group, self.channel_name)
        raise StopConsumer()

同时,前端发送websocket请求的时候,需要把群号传递过来:
socket = new WebSocket("ws://127.0.0.1:8000/room/{{ qq_group_num }}/");
这个群号可以是后台给返回过来的:

def index(request):
    qq_group_num = request.GET.get("num")
    return render(request, "index.html", {"qq_group_num": qq_group_num})

也就是说前端第一次请求页面的时候携带上群号即可。

脚本主动给客户端推送消息

  1. 新建文件 app01/utils/push_message.py 文件
import channels.layers

from asgiref.sync import async_to_sync


def push_message_to_manager_user(num, data):
    channel_layer = channels.layers.get_channel_layer()
    async_to_sync(channel_layer.group_send)(
        num,
        {
            "type": "xx.oo",
            "message": {"text": data},
        },
    )

  1. 进入shell环境,直接给浏览器推送消息
python manage.py shell

from app01.utils.push_message import push_message_to_manager_user
push_message_to_manager_user("123", "大富科技拉开发风巨大卡水电费了")

注意:123是我们创建的群号

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

专职

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值