server {
listen 80;
server_name 域名;
charset utf-8;
location / {
# uwsgi配置
include uwsgi_params;
uwsgi_pass unix:/home/CSJ_uwsgi/uwsgi_socket.sock;
}
# 处理websocket请求
location ~ /viewer/chat_room {
# uwsgi配置
include uwsgi_params;
uwsgi_pass unix:/home/CSJ_uwsgi/uwsgi_socket.sock;
# 下面两行是重点
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}
#使用nginx连接时使用
#socket = 127.0.0.1:8000
#直接做web服务器使用
#http = :8000
#项目目录
chdir = /home/CSJ_Live
#项目中wsgi.py文件的目录,相对于项目目录
#wsgi-file = CSJ_Live/wsgi.py
#指定项目的application
module = CSJ_Live.wsgi
#指定启动的工作的进程数
processes = 5
#请求超时事件
#harakiri = 60
#最大请求次数,重启进程,防止内存泄漏
max-request = 5000
#用户组相关配置
#uid = root
#gid = root
#指定工作进程中的线程数
#threads = 2
#启动主进程
master = True
#保存启动之后主进程的pid
pidfile = /home/CSJ_uwsgi/uwsgi.pid
#设置uwsgi后台允许,uwgsi.log保存日志信息
daemonize = /home/CSJ_uwsgi/uwsgi.log
#停止时自动删除进程号文件
vacuum=true
#设置缓冲
post-buffering=65535
#不设置会导致上传大文件失败
buffer-size=65535
#加载项目配置(django + websocket时需要配置的信息)
DJANGO_SETTINGS_MODULE=CSJ_Live.settings
WEBSOCKET_FACTORY_CLASS="dwebsocket.backends.uwsgi.factory.uWsgiWebSocketFactory"
socket = /home/CSJ_uwsgi/uwsgi_socket.sock
#这里给664就可以
chmod-socket = 777
#开启异步
async = 30
ugreen = ''
http-timeout = 300
<script>
var num = '';
for (var i=0;i<15;i++){
num += Math.floor(Math.random()*9+1)
}
if ("WebSocket" in window) {
{#alert("您的浏览器支持 WebSocket!");#}
// 打开一个 web socket
{#ws = new WebSocket('ws://' + window.location.host + '/viewer/chat_room/' + $("#event_uri_key").text());#}
ws = new WebSocket('ws://' + window.location.host + '/viewer/chat_room/' + num);
ws.onopen = function () {
// Web Socket 已连接上,使用 send() 方法发送数据
alert("onopen!");
};
ws.onmessage = function (evt) {
var received_msg = evt.data;
$("#info").text(received_msg);
};
ws.onclose = function () {
// 关闭 websocket
alert("连接已关闭...");
};
function sendmsg() {
var msg = $("#msg").val();
ws.send(msg);
alert('发送成功')
}
}
</script>
#uwsgi聊天室dwebsocket配置文件
WEBSOCKET_FACTORY_CLASS = "dwebsocket.backends.uwsgi.factory.uWsgiWebSocketFactory"
conn_dict = {}
@accept_websocket
def chat_room(request, event_uri_key):
if request.is_websocket():
conn = request.websocket
global conn_dict
conn_dict[event_uri_key] = conn
while True:
msg = conn.wait()
if msg == None:
conn_dict.pop(event_uri_key)
return
else:
for k, v in conn_dict.items():
v.send(str(conn_dict))
nginx -t
nginx -s reload
如果是第一次启动使用
uwsgi --ini /路径/uwsgi.ini #自己的ini配置文件路径
重启和关闭( 设置pidfile = /home/CSJ_uwsgi/uwsgi.pid 会生成pid文件)
uwsgi --reload /路径/uwsgi.pid # pid文件路径
uwsgi --stop /路径/uwsgi.pid # pid文件路径
由于不想使用channels+redis的方式实现,所以导致踩坑,这种方式实现了websocket通信但是uwsgi多进程启动django的原因,我使用一个全局字典存储连接套接字,共享变量不适用于多进程,进程间的变量是互相隔离的,子进程的全局变量是完全复制一份父进程的数据,对子进程的全局变量修改完全影响不到其他进程的全局变量。导致这个字典在多个进程间是相互独立的,无法实现消息的转发。通过修改uwsgi的processes = 1行不通,这样会导致阻塞,在一个长连接没有断开之前,其他请求将会阻塞。
到这里可以尝试使用进程间通信,通不通我就没有去尝试了,或者加redis(不如channels实现…)
server {
listen 80;
server_name 域名;
charset utf-8;
location / {
# gunicorn配置
proxy_pass http://127.0.0.1:8000;
}
# 处理websocket请求
location ~ /viewer/chat_room {
# gunicorn配置
proxy_pass http://127.0.0.1:8000;
proxy_http_version 1.1;
proxy_redirect off;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# 下面两行是重点
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}
在manage.py同级目录下创建gunicorn配置文件
#gunicorn.py
import logging
import logging.handlers
from logging.handlers import WatchedFileHandler
import os
import multiprocessing
bind = '127.0.0.1:8000' # 绑定ip和端口号
backlog = 512 # 监听队列
chdir = '/home/CSJ_Live' # gunicorn要切换到的目的工作目录
worker_class = 'gevent' # 使用gevent模式,还可以使用sync 模式,默认的是sync模式
#workers = multiprocessing.cpu_count() * 2 + 1 # 进程数(多进程数据不互通,默认workers为1,单进程模式)
loglevel = 'info' # 日志级别,这个日志级别指的是错误日志的级别,而访问日志的级别无法设置
errorlog = '../logs/gunicorn.error.log' #错误日志
accesslog = '../logs/gunicorn.access.log' #访问日志
使用worker_class = 'gevent’的方式需要安装gevent模块
pip3 install wheel
pip3 install gevent
启动gunicorn有多种方式,可以写成shell脚本启动,nohup启动等…
这里我使用 nohup启动(项目目录下启动),(默认daemon = Flase)这样启动会占用终端
nohup gunicorn -c gunicorn.py CSJ_Live.wsgi:application
在gunicorn配置文件中加上参数,再使用上述命令会在后台启动
daemon = True # 后台运行
通过ps命令可以查看进程号
ps aux | grep gunicorn
到这里已经可以实现websocket,其实根本问题还是在于全局字典的维护,gunicorn+gevnet启动相比uwsgi启动的好处在于,gevent协程启动(设置workers=1,默认就是1)不会有IO阻塞行为,而uwsgi会导致阻塞。
如果设置workers数量不为1,多进程启动方式存在和uwsgi相同的问题,同样也会导致全局字典不可用,考虑尝试使用进程间通信解决。
由于websocket长时间没有消息互通会导致超时断开,可以引入心跳机制,大致逻辑就是前端定时发送ping,django收到之后立即返回pong,前端定时器重置。这里我使用了三方库websocket-heartbeat-js,封装了心跳,重连等功能,所以无需手动实现心跳机制。
<body>
<input type="text" id="msg">
<button id="send" onclick="sendmsg()">发送</button>
</body>
<script src="/static/websocket-heartbeat-js-master/dist/index.js"></script>
<script src="/static/js/jquery-3.2.1.min.js"></script>
<script>
const options = {
url: 'ws://' + window.location.host + '/viewer/chat_room/' + '房间号', # 房间号根据后端逻辑定义
pingTimeout: 30000,
pongTimeout: 10000,
reconnectTimeout: 2000,
pingMsg: "heartbeat"
};
let websocketHeartbeatJs = new WebsocketHeartbeatJs(options);
websocketHeartbeatJs.onopen = function () {
console.log('连接成功');
};
websocketHeartbeatJs.onmessage = function (e) {
console.log(`onmessage: ${e.data}`);
};
function sendmsg() {
var msg = $("#msg").val();
websocketHeartbeatJs.send(msg)
}
</script>
使用方法不赘述,参照websocket-heartbeat-js
from django.conf.urls import url,re_path
from . import views
urlpatterns = [
url(r"^chat_room/(?P<event_uri_key>[a-zA-Z0-9]{15})",views.chat_room) # 正则匹配房间号
]
#dwebsocket
from dwebsocket.decorators import accept_websocket
from collections import defaultdict
#保存所有接入的用户地址
#用event_uri_key来区分房间,房间号作为键,值为字典,用 微信名称加头像 标识每一个套接字放入字典{房间号1:{微信名称-头像url:套接字,微信名称-头像url2:套接字2}}
allconn = defaultdict(dict)
@accept_websocket
def chat_room(request, event_uri_key):
if request.is_websocket():
# 获取用户相关信息
user = request.COOKIES.get('user')
nickname = json.loads(user)['nickname'] # 用户昵称
headimgurl = json.loads(user)['headimgurl'] # 用户头像
conn = request.websocket # 连接套接字
global allconn
allconn[event_uri_key][nickname + '-' + headimgurl] = conn
room_dict = allconn[event_uri_key]
while True:
message = conn.wait()
if message == None:
del allconn[event_uri_key][nickname + '-' + headimgurl]
return
elif message == b'heartbeat': # 心跳响应
conn.send(b"heartbeat") # pong
else: # 转发到房间内所有连接
for k, v in room_dict.items():
msg = nickname + '-' + headimgurl + '-' + bytes.decode(message)
v.send(str.encode(msg))
个人感觉uwsgi对dwebsocket的支持有点差强人意,uwsgi使用单进程会导致阻塞,多进程之间数据不互通,无法做转发。而使用gunicorn的协程模式则可以比较好的解决消息转发问题。