一、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的解决方法有额外的好处,如:可以发现连接是否被意外关闭等。