Django dWebScoket 实时消息推送

一、WebScoket是什么

       WebSocket 是 HTML5 提供的一种浏览器与服务器间进行全双工通讯的协议。依靠这种协议可以实现客户端和服务器端 ,一次握手,双向实时通信。目前主流的浏览器都支持WebSockets,包括火狐、IE、Chrome、Safari以及Opera等,而且,越来越多的服务器应用框架也开始支持WebSockets。

       简单来说,WebSocket减少了客户端与服务器端建立连接的次数,减轻了服务器资源的开销,只需要完成一次HTTP握手。整个通讯过程是建立在一次连接/状态中,也就避免了HTTP的非状态性,服务端会一直与客户端保持连接,直到双方发起关闭请求,同时由原本的客户端主动询问,转换为服务器有信息的时候推送。所以,它能做实时通信(聊天室、直播间等),其他特点还包括:

  • 建立在 TCP 协议之上,服务器端的实现比较容易
  • 与 HTTP 协议有着良好的兼容性。默认端口也是80和443,并且握手阶段采用 HTTP 协议,因此握手时不容易屏蔽,能通过各种 HTTP 代理服务器
  • 数据格式比较轻量,性能开销小,通信高效
  • 可以发送文本,也可以发送二进制数据
  • 没有同源限制,客户端可以与任意服务器通信
  • 协议标识符是ws(如果加密,则为wss),服务器网址就是 URL

        Django实现websocket大致上有两种方式,一种channels,一种是dwebsocket。channels依赖于redis,twisted等,相比之下使用dwebsocket要更为方便一些。

二、在代码中的使用

url.py

import xadmin
from django.urls import path
from Apps.AuditList import views as auditViews
 
 
urlpatterns = [
    path('xadmin/', xadmin.site.urls),
    path('linkClients', auditViews.linkClients, name='linkClients'),    #webscoket连接
]

views.py

import json
from dwebsocket.decorators import accept_websocket #引入dwbsocket的accept_websocket装饰器

#创建客户端列表,存储所有在线客户端
clients={}

#发送实时消息给客户端, 允许接受ws请求
@accept_websocket
def linkClients(request):
    if request.is_websocket():      # 判断是不是ws请求
        # userid=str(uuid.uuid1())
        while True:
            message=request.websocket.wait()
            if not message:
                break
            else:
                message_str = str(message, encoding = "utf-8")
                message_dict = json.loads(message_str)

                name = message_dict['name']
                msg = message_dict['msg']
                url = message_dict['url'] if 'url' in message_dict else ''

                if 'openClient'== msg:
                    clients[name]=request.websocket     #保存客户端的ws对象
                elif 'ping' == msg:
                    pass
                else:
                    #分发消息给其他客户端
                    for client in clients.keys():
                        if client!=name:
                           clients[client].send(msg.encode('utf-8'))

settings.py添加如下代码

#dwebSocket
# MIDDLEWARE_CLASSES=['dwebsocket.middleware.WebSocketMiddleware']      # 为所有的URL提供websocket,如果只是单独的视图需要可以不选
WEBSOCKET_ACCEPT_ALL=True         # 可以允许每一个单独的视图实用websockets

vue  App.vue

<template>
  <div id="app">
    <router-view/>
  </div>
</template>

<script>
import bus from "./components/bus"
export default {
  name: 'App',
  data(){
    return {
    }
  },
  mounted() {
    let that = this
    //监听刷新事件
    window.addEventListener('unload',this.saveState);

    //连接webScoket
    this.newWebSocket()
  
  },  
  methods:{
    saveState(){ 
      localStorage.setItem('state',JSON.stringify(this.$store.state))
    },
    //浏览器发送通知消息
    noteMessage(message){
      if (window.Notification && Notification.permission !==  "denied" ) {
        Notification.requestPermission( function (status) {
          var  notice_ =  new  Notification( 'ATS-WEB2.0 新消息' , { body: message });
          var mes = message.split('\n')
          notice_.onclick =  function () { //单击消息提示框,进入url
            window.focus();
            if (mes.length===2){
              window.location.assign(mes[1])
            }
          }
        });
      }   
    },
     newWebSocket(){
      let ws = null;
      let that = this
      //先判断浏览器是否支持websocket, 发送即时消息
      if ("WebSocket" in window) {
        console.log("您的浏览器支持 WebSocket!");
        ws = new WebSocket("wss://xx.xx.xx.xx:xx/linkClients");
      } else {
        this.$message({type: 'info', message: '您的浏览器不支持 WebSocket!'})
      }

      ws.onopen = function () {
        heartCheck.reset().start();   // 成功建立连接后,重置心跳检测

        that.$store.commit("setWebScoket", ws);     //将webScoket对象保存在vuex中
        
        let param = {name:that.$store.state.userInfo.username, msg:'openClient'}
        let param_str = JSON.stringify(param)
        ws.send(param_str);          // Web Socket 已连接上,使用 send() 方法尝试发送数据
      };
      //监听服务端是否有消息发送过来,当有消息时执行方法
      ws.onmessage = function (evt) {
          var received_msg = evt.data; 
          that.noteMessage(received_msg)
      };
      //关闭页面或其他行为导致与服务器断开链接是执行
      ws.onclose = function () {
        that.$store.commit("setWebScoket", null);
        console.log("连接已关闭...",new Date()); 
      };
      

      // 心跳检测, 每隔一段时间检测连接状态,如果处于连接中,就向server端主动发送消息,来重置server端与客户端的最大连接时间,如果已经断开了,发起重连。
      var heartCheck = {
          timeout: 1100000,        // 19分钟
          serverTimeoutObj: null,
          reset: function(){
              clearTimeout(this.timeoutObj);
              clearTimeout(this.serverTimeoutObj);
              return this;
          },
          start: function(){
              var self = this;
              this.serverTimeoutObj = setInterval(function(){
                  if(ws.readyState == 1){
                      console.log("连接状态,发送消息保持连接");
                      ws.send("ping");
                      heartCheck.reset().start();    // 如果获取到消息,说明连接是正常的,重置心跳检测
                  }else{
                      console.log("断开状态,尝试重连");
                      that.newWebSocket();
                  }
              }, this.timeout)
          }
      } 
    } 
    
  }
}

</script>

<style>

</style>


在其他页面中, 添加如下代码,来广播消息 给其他用户

//send message to other client
let param = {name:'my_name', msg:'你有一条新消息,请注意查看!',url:response.data.url}
this.$store.state.webScoket.send(JSON.stringify(param))     //获取vuex中的webScoket,发送信息

参考链接:python websocket Django 实时消息推送_不太灵光的程序员-CSDN博客

三、Nginx配置转发webScoket请求

upstream VueDevelopTest {
    server 127.0.0.1:86; # for a web port socket
}

map $http_upgrade $connection_upgrade {
     default upgrade;
     '' close;
}

server {
    listen 811;
    server_name xx.xx.xx.xx;         #域名
    charset     utf-8;
    ssl on;                              #SSL  使用https
    ssl_certificate /etc/nginx/conf.d/SSL/psh_cert.pem;
    ssl_certificate_key /etc/nginx/conf.d/SSL/psh_key_with_pass.key.unsecure; 
    ssl_session_cache shared:SSL:1m;
    ssl_session_timeout  5m; 
    ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE:ECDH:AES:HIGH:!NULL:!aNULL:!MD5:!ADH:!RC4;
    ssl_prefer_server_ciphers on;        

    access_log      /var/log/nginx/My_Web_Vue_access.log;
    error_log       /var/log/nginx/My_Web_Vue_error.log;

    client_max_body_size 75M;

    location / { 
        uwsgi_pass   VueDevelopTest;
        include     /etc/nginx/uwsgi_params;
    }   
    location /linkClients {
        proxy_pass http://172.24.155.22:8484;    #webScoket的访问地址
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_connect_timeout 4s;                #配置点1
        proxy_read_timeout 1200s;                  #配置点2,如果没效,可以考虑这个时间配置长一点
        proxy_send_timeout 12s;                  #配置点3     
     }   

}                                                                                                                                      

map指令的作用
        主要是根据客户端请求中$http_upgrade 的值,来构造改变$connection_upgrade的值,即根据变量$http_upgrade的值创建新的变量$connection_upgrade,
创建的规则就是{}里面的东西。其中的规则没有做匹配,因此使用默认的,即 $connection_upgrade 的值会一直是 upgrade。然后如果 $http_upgrade为空字符串的话,
那值会是 close。

proxy_http_version 1.1;,为什么使用http1.1协议?(https://blog.csdn.net/moxiaomomo/article/details/74734565)

        设置代理使用的HTTP协议版本。默认使用的版本是1.0,而1.1版本则推荐在使用keepalive连接时一起使用。

        因为http1.0不支持keepalive特性,当没有使用http1.1的时候,后端服务会返回101错误,然后断开连接。

HTTP的Upgrade协议头

        HTTP的Upgrade协议头机制用于将连接从HTTP连接升级到WebSocket连接,Upgrade机制使用了Upgrade协议头和Connection协议头;为了让Nginx可以将来自客户端的Upgrade请求发送到后端服务器,Upgrade和Connection的头信息必须被显式的设置。

参考链接:Nginx反向代理WebSocket(WSS)_weixin_34018169的博客-CSDN博客

四、gunicorn配置

        感觉uwsgi对dwebsocket的支持有点差强人意,uwsgi使用单进程会导致阻塞,多进程之间数据不互通,无法做转发。而使用gunicorn的协程模式则可以比较好的解决消息转发问题。

在manage.py 同级目录下创建python文件

#gunicorn.py
import logging
import logging.handlers
from logging.handlers import WatchedFileHandler
import os
import multiprocessing

bind = '172.24.155.22:8484'             # 绑定ip和端口号
backlog = 512  # 监听队列
chdir = '/data/django/My_Vue_Test'          # gunicorn要切换到的目的工作目录
worker_class = 'gevent'          # 使用gevent模式,还可以使用sync 模式,默认的是sync模式
#workers = multiprocessing.cpu_count() * 2 + 1       # 进程数(多进程数据不互通,默认workers为1,单进程模式)
loglevel = 'info'       # 日志级别,这个日志级别指的是错误日志的级别,而访问日志的级别无法设置
errorlog = '/data/django/gunicorn_logs/gunicorn.error.log'       #错误日志
accesslog = '/data/django/gunicorn_logs/gunicorn.access.log'     #访问日志

daemon = True        #默认daemon = Flase 这样启动会占用终端

使用worker_class = 'gevent’的方式需要安装gevent模块

pip3 install wheel
pip3 install gevent

启动gunicorn有多种方式,这里我使用 nohup启动(项目目录下启动),(默认daemon = Flase)这样启动会占用终端

nohup gunicorn -c gunicorn.py MY_WEB_VUE.wsgi:application

查看打开的gunicorn进程

ps aux | grep gunicorn 

测试webScoket链接是否好用

# Create your tests here.
 
from websocket import create_connection
 
# 建立一个websocket连接
ws = create_connection("ws://172.24.155.22:8484/linkClients")
# #对websocket客户端发送一个请求
print(ws)

参考链接:nginx+(uwsgi/gunicorn)+django+dwebsocket部署_一剑仙人跪的博客-CSDN博客_django gunicorn websocket

五、期间遇到的问题:

1.uwsgi配置webscoket不成功,最后使用了gunicorn

2.保持webscoket长连接

WebSocket 仍然受到 Nginx 缺省为60秒的 proxy_read_timeout 配置影响。这意味着,如果你有一个程序使用了 WebSocket,但又可能超过60秒不发送任何数据的话,那么需要增大超时时间(配置proxy_read_timeout),要么实现一个Ping、Pong的心跳消息以保持客户端和服务端的联系。使用Ping、Pong的解决方法有额外的好处,如:可以发现连接是否被意外关闭等。

  • 0
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值