• 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.客户端向服务端发送

    1. GET /chatsocket HTTP/1.1
    2. Host: 127.0.0.1:8002
    3. Connection: Upgrade
    4. Pragma: no-cache
    5. Cache-Control: no-cache
    6. Upgrade: websocket
    7. Origin: http://localhost:63342
    8. Sec-WebSocket-Version: 13
    9. Sec-WebSocket-Key: mnwFxiOlctXFN/DeMt1Amg==
    10. Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits

             2.服务端接收:

    1. 请求和响应的【握手】信息需要遵循规则:
    2. 从请求【握手】信息中提取 Sec-WebSocket-Key
    3. 利用magic_string 和 Sec-WebSocket-Key 进行hmac1加密,再进行base64加密
    4. 将加密结果响应给客户端
    5. 注:magic string固定为:258EAFA5-E914-47DA-95CA-C5AB0DC85B11

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

    1. HTTP/1.1 101 Switching Protocols
    2. Upgrade:websocket
    3. Connection: Upgrade
    4. 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进入聊天室页面,为每个用户创建一个队列
    • 点击发送内容,数据发送到后台,给到每个人的队列中
    • 递归获取消息,去队列中获取数据,展示在页面

    前端

    1. <body>
    2. <div class="message" id="message"></div>
    3. <div>
    4. <input type="text" placeholder="请输入" id="txt">
    5. <input type="button" value="发送" onclick="sendMessage()">
    6. <!-- <input type="button" value="关闭连接" onclick="closeConn()">-->
    7. </div>
    8. <script>
    9. USER_ID = "{{uid}}";
    10. function sendMessage(){
    11. var text=$('#txt').val();
    12. // 基于ajax将用户文本信息发送到后台
    13. $.ajax({
    14. url:'/send/msg/',
    15. data:{text:text},
    16. type: 'GET',
    17. dataType:'JSON',
    18. success: function (res){
    19. console.log('请求发送成功',res)
    20. //超时,没有新数据
    21. // 有数据,立即展示
    22. // if (res.status){
    23. // var tag =$("
      ");
    24. // tag.text(res.data)
    25. // $("#message").appendImage(tag);
    26. // }
    27. }
    28. })
    29. }
    30. function getMessage(){
    31. $.ajax({
    32. url:'/get/msg/',
    33. data:{uid:USER_ID},
    34. type: 'GET',
    35. dataType:'JSON',
    36. success: function (res){
    37. //超时,没有新数据
    38. // 有数据,立即展示
    39. if (res.status){
    40. var tag =$("
      ");
    41. tag.text(res.data)
    42. $("#message").appendImage(tag);
    43. }
    44. getMessage(); // 递归调用该函数
    45. }
    46. })
    47. }
    48. $(function (){
    49. getMessage();
    50. })
    51. </script>
    52. </body>

    后端

    1. # view 视图
    2. import queue
    3. from django.shortcuts import render,HttpResponse
    4. from django.http import request,JsonResponse
    5. USER_QUEUE = {}
    6. def index(request):
    7. qq_number = request.GET.get('num')
    8. return render(request,'index.html',{"qq_number":qq_number})
    9. def home(request):
    10. uid = request.GET.get('uid')
    11. USER_QUEUE[uid]=queue.Queue()
    12. return render(request,'home.html',{'uid':uid})
    13. def send_msg(request):
    14. text =request.GET.get('text')
    15. for uid,q in USER_QUEUE.items():
    16. q.put(text)
    17. # print("接收到客户端的请求:"+request.GET)
    18. return HttpResponse("ok")
    19. def get_msg(request):
    20. # 去自己的队列中获取数据
    21. uid = request.GET.get('uid')
    22. q = USER_QUEUE[uid]
    23. result = {'status':True,'data':None}
    24. try:
    25. data = q.get(timeout=10)
    26. result['data']=data
    27. except queue.Empty as e:
    28. result['status']=False
    29. return JsonResponse(result)
    30. # url
    31. path('home/', home),
    32. path('send/msg/', send_msg),
    33. path('get/msg/', get_msg),

    django 中配置websocket

    1. pip install channels # 安装组件
    2. # 注册app
    3. 'channels'
    4. # 配置asgi.application
    5. ASGI_APPLICATION = 'web.asgi.application'

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

    1. # asgi.py
    2. import os
    3. from django.core.asgi import get_asgi_application
    4. from channels.routing import ProtocolTypeRouter,URLRouter
    5. from . import routing
    6. os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'web.settings')
    7. application = ProtocolTypeRouter({
    8. 'http':get_asgi_application(),
    9. 'websocket':URLRouter(routing.websocket_urlpatterns),
    10. })

    创建routing文件在setting同级目录

    1. from django.urls import re_path
    2. from app import consumers
    3. websocket_urlpatterns = [
    4. re_path(r'ws/(?P\w+)/$',consumers.ChatConsumer.as_asgi()),
    5. ]

    创建app目录下consumer文件

    1. from channels.generic.websocket import WebsocketConsumer
    2. from channels.exceptions import StopConsumer
    3. """
    4. wsgi: 同步
    5. asgi: 异步+asgi+websocket
    6. """
    7. class ChatConsumer(WebsocketConsumer):
    8. def websocket_connect(self, message):
    9. # 客户端向后端发送websocket连接请求时自动触发
    10. # 容许和客户端创建连接
    11. self.accept()
    12. def websocket_receive(self, message):
    13. # 浏览器基于websocket向后端发送数据,自动触发接收消息
    14. print(message)
    15. self.send('不要回复!!!')
    16. def websocket_disconnect(self, message):
    17. # 客户端与服务端断开时自动触发
    18. 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()
    • 收发消息(客户端向服务端发送消息)
      • 客户端
        // 获取输入框的信息进行发送
        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函数的触发

    1. <div class="message" id="message"></div>
    2. <div>
    3. <input type="text" placeholder="请输入" id="txt">
    4. <input type="button" value="发送" onclick="sendMessage()">
    5. <input type="button" value="关闭连接" onclick="closeConn()">
    6. </div>
    7. <script>
    8. // http://www.baidu.com
    9. // ws://www.baidu.com 注释下面 ws://xxx的使用
    10. socket = new WebSocket("ws://127.0.0.1:8000/ws/123/");
    11. // 创建好连接之后自动触发,即当执行self.accept()
    12. socket.onopen = function (event){
    13. let tag= document.createElement('div');
    14. tag.innerText = '[连接成功]';
    15. console.log(tag.textContent)
    16. document.getElementById('message').appendChild(tag);
    17. }
    18. // 当websocket接收到服务端发来的消息时,自动触发这个函数
    19. socket.onmessage = function (event){
    20. let tag= document.createElement('div');
    21. tag.innerText = event.data;
    22. // console.log(tag.textContent);
    23. document.getElementById('message').appendChild(tag);
    24. }
    25. // 断开连接时触发
    26. socket.onclose =function (event){
    27. let tag= document.createElement('div');
    28. tag.innerText = '[断开连接]';
    29. console.log(tag.textContent)
    30. document.getElementById('message').appendChild(tag);
    31. }
    32. // 获取输入框的信息进行发送
    33. function sendMessage(){
    34. let tag =document.getElementById('txt');
    35. socket.send(tag.value);
    36. }
    37. // 关闭连接
    38. function closeConn(){
    39. socket.close(); // 向服务端发送断开连接的请求
    40. }
    41. </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  # 容许断开连接

    运行结果:

  • 相关阅读:
    Linux
    火眼金睛破局ES伪慢查询
    python:talib.BBANDS 画股价-布林线图
    【Verilog 教程】5.2Verilog 模块例化
    react知识点
    嵌入式基础准备 | Linux命令(包括 文件、目录和压缩、系统操作、vi、vim、ctags、cscope)
    红黑树(Red Black Tree)
    笔试强训day35(抄送列表,年会抽奖)
    精品springboot的二手车管理系统vue
    东南亚三国(新/马/菲)2022女装市场趋势解读及爆品指南
  • 原文地址:https://blog.csdn.net/qq_44238024/article/details/136287374