djangoがdwebsocketを使用して踏んだピットを記録します
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";
}
}
Webサーバーとしてのuwsgiの場合、構成は次のとおりです。
#使用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のプロセス= 1を変更しても機能しません。これにより、ブロックが発生します。長い接続が切断される前に、他の要求がブロックされます。
ここでは、プロセス間通信の使用を試すことができます。失敗した場合は、試していません。または、redisを追加しました(チャネルほど良くありません...)
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を起動するには、シェルスクリプトの起動、nohupの起動など、さまざまな方法があります。
ここでは、nohupの起動(プロジェクトディレクトリで起動)を使用します(デフォルトのデーモン= Flase)。この起動はターミナルを占有します。
nohup gunicorn -c gunicorn.py CSJ_Live.wsgi:application
gunicorn構成ファイルにパラメーターを追加してから、上記のコマンドを使用してバックグラウンドで開始します
daemon = True # 后台运行
プロセス番号は、psコマンドで表示できます
ps aux | grep gunicorn
これまで、websocketを実装できます。実際、根本的な問題はグローバル辞書の保守にあります。uwsgiスタートアップと比較したgunicorn + gevnetスタートアップの利点は、geventコルーチンスタートアップ(set worker = 1、デフォルトは1)です。 IOブロッキング動作はありません。また、uwsgiはブロッキングを引き起こします。
ワーカー数が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<event_uri_key>[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のコルーチンモードを使用すると、メッセージ転送の問題をより適切に解決できます。