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>Titletitle>
    <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 = $("
") tag.text(item) $(".message").append(tag) }) } }) }, 2000) script> body> html>

长轮询实现聊天室

django channels实战_第1张图片

  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>Titletitle>
    <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 = $("
") 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实战_第2张图片

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\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>Titletitle>
    <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://:[email protected]: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})

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

你可能感兴趣的:(django,python,后端)