nginx+(uwsgi/gunicorn)+django+dwebsocket部署

记录一次django使用dwebsocket踩过的坑

  • nginx+uwsgi+django+dwebsocket(存在弊端)
    • 使用nginx做反向代理,配置如下:
    • uwsgi做web服务器,配置如下:
    • 前端html测试用例
    • settings.py添加如下配置:
    • django测试用例
    • nginx重启
    • uwsgi启动
    • 总结:
  • nginx+gunicorn+django+dwebsocket(使用gunicorn协程模式启动成功)
    • nginx配置如下:
    • gunicorn配置如下:
    • 前端html测试用例
    • urls.py配置
    • django测试用例
    • 总结:

nginx+uwsgi+django+dwebsocket(存在弊端)

使用nginx做反向代理,配置如下:

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";
        }
}

uwsgi做web服务器,配置如下:

#使用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

前端html测试用例

<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>

settings.py添加如下配置:

#uwsgi聊天室dwebsocket配置文件
WEBSOCKET_FACTORY_CLASS = "dwebsocket.backends.uwsgi.factory.uWsgiWebSocketFactory"

django测试用例

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重启

nginx -t
nginx -s reload

uwsgi启动

如果是第一次启动使用

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实现…)

nginx+gunicorn+django+dwebsocket(使用gunicorn协程模式启动成功)

nginx配置如下:

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";

        }
}

gunicorn配置如下:

在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相同的问题,同样也会导致全局字典不可用,考虑尝试使用进程间通信解决。

前端html测试用例

由于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

urls.py配置

from django.conf.urls import url,re_path
from . import views
urlpatterns = [
    url(r"^chat_room/(?P[a-zA-Z0-9]{15})",views.chat_room) # 正则匹配房间号
]

django测试用例

#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的协程模式则可以比较好的解决消息转发问题。

你可能感兴趣的:(部署相关问题)