websocket在django中的运用

14-2 聊天室实现思路:轮训、长轮训、websocket_哔哩哔哩_bilibili 参考大佬的B站学习笔记

https://www.cnblogs.com/wupeiqi/p/6558766.html 参考博客

https://www.cnblogs.com/wupeiqi/articles/9593858.html 参考博客


http协议: 是短连接,无状态的,一次性的,无法保证实时信息交互

  • 客户端主动连接服务器
  • 客户端向服务端发送消息,服务端接收到返回数据
  • 客户端接收到数据
  • 端口连接

websock协议:创建持久的连接不断开,基于这个连接进行收发数据

  • 实时响应:接收发送消息
  • 实时图表,柱状图,饼图

websocket 原理:

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

握手流程:

1.客户端向服务端发送

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

2.服务端接收:

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

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

注:magic string固定为:258EAFA5-E914-47DA-95CA-C5AB0DC85B11

返回数据给客户端浏览器,验证通过则完成握手

HTTP/1.1 101 Switching Protocols
      Upgrade:websocket
      Connection: Upgrade
      Sec-WebSocket-Accept: 密文

收发数据流程:

数据 b'asdfa;efawe;sdfas;awdfawea;sdfasdfaf;sdfasdfa;'

先获取第二个字节,8位 10001010

再获取第二个字节的后七位 0001010 -> payload len

  • =127 2个字节,8个字节 其他字节(4字节 masking key + 数据)
  • =126 2个字节,8个字节 其他字节(4字节 masking key + 数据)
  • <=125 2个字节 其他字节(4字节 masking key + 数据)
  • 获取masking key,然后对数据进行解密
    • var DECODED = "";

      for (var i = 0; i < ENCODED.length; i++) {

      DECODED[i] = ENCODED[i] ^ MASK[i % 4];

      }

实时交互的解决方案:

  1. 轮训,浏览器每隔一段时间向后台发送一次请求。缺点:有延迟、请求太多网站压力大
  2. 长轮询,客户端向服务端发送请求,保持一定的时间,一旦有数据就立即返回。特点:数据无延迟,常应用于大平台、WebQQ、Web微信
  3. websocket,客户端和服务端创建连接不断开,可以实现双向通信。特点:旧版浏览器不支持

长轮询实现群聊功能

  • 访问url进入聊天室页面,为每个用户创建一个队列
  • 点击发送内容,数据发送到后台,给到每个人的队列中
  • 递归获取消息,去队列中获取数据,展示在页面

前端

<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>
    USER_ID = "{{uid}}";
    function sendMessage(){
        var text=$('#txt').val();
        // 基于ajax将用户文本信息发送到后台
        $.ajax({
            url:'/send/msg/',
            data:{text:text},
            type: 'GET',
            dataType:'JSON',
            success: function (res){
                console.log('请求发送成功',res)
                //超时,没有新数据
                // 有数据,立即展示
                // if (res.status){
                //     var tag =$("<div>");
                //     tag.text(res.data)
                //     $("#message").appendImage(tag);
                // }
            }
        })
    }
    function getMessage(){
        $.ajax({
            url:'/get/msg/',
            data:{uid:USER_ID},
            type: 'GET',
            dataType:'JSON',
            success: function (res){
                //超时,没有新数据
                // 有数据,立即展示
                if (res.status){
                    var tag =$("<div>");
                    tag.text(res.data)
                    $("#message").appendImage(tag);
                    }
                getMessage(); // 递归调用该函数
            }
        })
    }
    $(function (){
        getMessage();
    })
</script>
</body>

后端

# view 视图
import queue
from django.shortcuts import render,HttpResponse
from django.http import request,JsonResponse
USER_QUEUE = {}
def index(request):
    qq_number = request.GET.get('num')
    return render(request,'index.html',{"qq_number":qq_number})

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)
    # print("接收到客户端的请求:"+request.GET)
    return HttpResponse("ok")

def get_msg(request):
    # 去自己的队列中获取数据
    uid = request.GET.get('uid')
    q  = USER_QUEUE[uid]
    result = {'status':True,'data':None}
    try:
        data = q.get(timeout=10)
        result['data']=data
    except queue.Empty as e:
        result['status']=False
    return JsonResponse(result)


# url
path('home/', home),
path('send/msg/', send_msg),
path('get/msg/', get_msg),

django 中配置websocket

pip install channels  # 安装组件

# 注册app
'channels'  

# 配置asgi.application
ASGI_APPLICATION = 'web.asgi.application'

更新asgi文件(在支持http的基础上支持websocket)

# 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', 'web.settings')

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

创建routing文件在setting同级目录

from django.urls import re_path

from app import consumers

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

创建app目录下consumer文件

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

"""
wsgi: 同步
asgi: 异步+asgi+websocket
"""
class ChatConsumer(WebsocketConsumer):
    def websocket_connect(self, message):
        # 客户端向后端发送websocket连接请求时自动触发

        # 容许和客户端创建连接
        self.accept()

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

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

运行结果:

注意: 当启动服务器是 Starting development server 而非ASGI服务时要检查channel版本,较高的版本可能不适配django,经调试发现3.0.1版本适配,重新安装channels即可

pip install channels==3.0.1
Pycharm 的html 注释小技巧!

setting --> Template Languages --> None

websocket 收发消息流程

  • 访问地址看到聊天室的页面

  • 客户端主动向服务端发送websocket连接,服务端接收到通过(完成握手)

  • 客户端,执行websocket连接动作

    复制代码
    // http://www.baidu.com
    // ws://www.baidu.com 注释下面 ws://xxx的使用
    socket = new WebSocket("ws://127.0.0.1:8000/ws/123/");
  • 服务端

    复制代码
    def websocket_connect(self, message):
        # 客户端向后端发送websocket连接请求时自动触发
        # 容许和客户端创建连接(握手)
        print("有人来连接了")
        self.accept()
  • 收发消息(客户端向服务端发送消息)

    • 客户端

      复制代码
      <input type="button" value="发送" οnclick="sendMessage()">
      复制代码
      // 获取输入框的信息进行发送
      function sendMessage(){
          let tag =document.getElementById('txt');
          socket.send(tag.value);
      }
    • 服务端

      复制代码
      def websocket_receive(self, message):
          # 浏览器基于websocket向后端发送数据,自动触发接收消息
          txt  = message['text']
          print('收到消息-->', txt)
          self.send(txt+'哈哈') # 在客户端发送消息的基础上加上字段 "哈哈"
    • 收发消息(服务端主动发给客户端)
      *

      复制代码
      服务端
      复制代码
      def websocket_connect(self, message):
          # 客户端向后端发送websocket连接请求时自动触发
          # 容许和客户端创建连接(握手)
          print("有人来连接了")
          self.accept()
          # 服务端向客户端发送消息
          self.send('来了呀年轻人!')
      复制代码
      客户端
      复制代码
      // 当websocket接收到服务端发来的消息时,自动触发这个函数
      socket.onmessage = function (event){
          var tag= document.createElement('div');
          tag.innerText = event.data;
          console.log(tag.textContent);
      }

运行结果:

前端页面的实现及DOM函数的触发

<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>
    // http://www.baidu.com
    // ws://www.baidu.com 注释下面 ws://xxx的使用
    socket = new WebSocket("ws://127.0.0.1:8000/ws/123/");

    // 创建好连接之后自动触发,即当执行self.accept()
    socket.onopen = function (event){
        let tag= document.createElement('div');
        tag.innerText = '[连接成功]';
        console.log(tag.textContent)
        document.getElementById('message').appendChild(tag);
    }

    // 当websocket接收到服务端发来的消息时,自动触发这个函数
    socket.onmessage = function (event){
        let tag= document.createElement('div');
        tag.innerText = event.data;
        // console.log(tag.textContent);
        document.getElementById('message').appendChild(tag);
    }

    // 断开连接时触发
    socket.onclose =function (event){
        let tag= document.createElement('div');
        tag.innerText = '[断开连接]';
        console.log(tag.textContent)
        document.getElementById('message').appendChild(tag);
    }

    // 获取输入框的信息进行发送
    function sendMessage(){
        let tag =document.getElementById('txt');
        socket.send(tag.value);
    }
    // 关闭连接
    function closeConn(){
        socket.close(); // 向服务端发送断开连接的请求
    }
</script>

群聊功能的实践:

方法1: 方法笨重,可行性差,不便于接口

复制代码
CONN_LIST = []
复制代码
def websocket_connect(self, message):
    # 客户端向后端发送websocket连接请求时自动触发
    self.accept()
    CONN_LIST.append(self)
复制代码
def websocket_receive(self, message):
    txt  = message['text']
    for conn in CONN_LIST:
        conn.send(txt)
复制代码
def websocket_disconnect(self, message):
    CONN_LIST.remove(self)
    raise StopConsumer  # 容许断开连接

Channel Layers实现群聊

方法2:基于channel中提供channel layers来实现-主要流程

  • 配置setting channel layers

    复制代码
    CHANNEL_LAYERS = {
        "default": {
            "BACKEND": "channels.layers.InMemoryChannelLayer",
        }
    }
  • 更新view

    复制代码
    qq_number = request.GET.get('num')
  • 更新收发后端consumer.py

    复制代码
    def websocket_connect(self, message):
        # 客户端向后端发送websocket连接请求时自动触发
        self.accept()
        # 获取群号即路由匹配的数值
        group = self.scope['url_route']['kwargs'].get('group') # 固定用法
        # 将客户端的连接对象加入到某个地方 redis or 内存
        async_to_sync(self.channel_layer.group_add)(group,self.channel_name)
        # 服务端向客户端发送消息
        self.send('来了呀年轻人!')
    复制代码
    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  # 容许断开连接

运行结果:

相关推荐
速盾cdn2 小时前
速盾:CDN是否支持屏蔽IP?
网络·网络协议·tcp/ip
yaoxin5211232 小时前
第二十七章 TCP 客户端 服务器通信 - 连接管理
服务器·网络·tcp/ip
内核程序员kevin2 小时前
TCP Listen 队列详解与优化指南
linux·网络·tcp/ip
PersistJiao3 小时前
Spark 分布式计算中网络传输和序列化的关系(一)
大数据·网络·spark
黑客Ash6 小时前
【D01】网络安全概论
网络·安全·web安全·php
->yjy6 小时前
计算机网络(第一章)
网络·计算机网络·php
摘星星ʕ•̫͡•ʔ7 小时前
计算机网络 第三章:数据链路层(关于争用期的超详细内容)
网络·计算机网络
.Ayang8 小时前
SSRF漏洞利用
网络·安全·web安全·网络安全·系统安全·网络攻击模型·安全架构
好想打kuo碎8 小时前
1、HCIP之RSTP协议与STP相关安全配置
网络·安全