Django Channels / Daphne 中的 Websocket 超时
Websocket timeout in Django Channels / Daphne
简短问题版本:我在 Daphne 配置、消费者代码或客户端代码中做错了什么?
channels==1.1.8
daphne==1.3.0
Django==1.11.7
详情如下:
我正在尝试使用 Django Channels 和 Daphne 接口服务器保持持久的 Websocket 连接打开。我正在使用大多数默认参数启动 Daphne:daphne -b 0.0.0.0 -p 8000 my_app.asgi:channel_layer
.
我看到连接在浏览器空闲一段时间后关闭,大约 20 秒多一点。与断开连接一起发送的 CloseEvent
的 code
值为 1006
(异常关闭),未设置 reason
,并且 wasClean
设置为 false。 应该是服务器关闭连接而不发送明确的关闭帧。
Daphne CLI 有 --ping-interval
和 --ping-timeout
标志,默认值分别为 20 秒和 30 秒。前者记录为 "The number of seconds a WebSocket must be idle before a keepalive ping is sent,",后者记录为 "The number of seconds before a WebSocket is closed if no response to a keepalive ping,"。我读到这个是因为 Daphne 会等到 WebSocket 空闲 20 秒后发送 ping,如果 30 秒后没有收到响应,则会关闭 Websocket。相反,我看到的是连接在空闲 20 秒后关闭。 (跨越 3 次默认尝试,分别在 20081 毫秒、20026 毫秒和 20032 毫秒后关闭)
如果我将服务器更改为使用 daphne -b 0.0.0.0 -p 8000 --ping-interval 10 --ping-timeout 60 my_app.asgi:channel_layer
启动,连接仍然关闭,大约 20 秒的空闲时间。 (尝试更新 ping 三次后,分别在 19892 毫秒、20011 毫秒、19956 毫秒后关闭)
代码如下:
consumer.py
:
import logging
from channels import Group
from channels.generic.websockets import JsonWebsocketConsumer
from my_app import utilities
logger = logging.getLogger(__name__)
class DemoConsumer(JsonWebsocketConsumer):
"""
Consumer echos the incoming message to all connected Websockets,
and attaches the username to the outgoing message.
"""
channel_session = True
http_user_and_session = True
@classmethod
def decode_json(cls, text):
return utilities.JSONDecoder.loads(text)
@classmethod
def encode_json(cls, content):
return utilities.JSONEncoder.dumps(content)
def connection_groups(self, **kwargs):
return ['demo']
def connect(self, message, **kwargs):
super(DemoConsumer, self).connect(message, **kwargs)
logger.info('Connected to DemoConsumer')
def disconnect(self, message, **kwargs):
super(DemoConsumer, self).disconnect(message, **kwargs)
logger.info('Disconnected from DemoConsumer')
def receive(self, content, **kwargs):
super(DemoConsumer, self).receive(content, **kwargs)
content['user'] = self.message.user.username
# echo back content to all groups
for group in self.connection_groups():
self.group_send(group, content)
routing.py
:
from channels.routing import route
from . import consumers
channel_routing = [
consumers.DemoConsumer.as_route(path=r'^/demo/'),
]
demo.js
:
// Tracks the cursor and sends position via a Websocket
// Listens for updated cursor positions and moves an icon to that location
$(function () {
var socket = new WebSocket('ws://' + window.location.host + '/demo/');
var icon;
var moveTimer = null;
var position = {x: null, y: null};
var openTime = null;
var lastTime = null;
function sendPosition() {
if (socket.readyState === socket.OPEN) {
console.log('Sending ' + position.x + ', ' + position.y);
socket.send(JSON.stringify(position));
lastTime = Date.now();
} else {
console.log('Socket is closed');
}
// sending at-most 20Hz
setTimeout(function () { moveTimer = null; }, 50);
};
socket.onopen = function (e) {
var box = $('#websocket_box');
icon = $('<div class="pointer_icon"></div>').insertAfter(box);
box.on('mousemove', function (me) {
// some browsers will generate these events much closer together
// rather than overwhelm the server, batch them up and send at a reasonable rate
if (moveTimer === null) {
moveTimer = setTimeout(sendPosition, 0);
}
position.x = me.offsetX;
position.y = me.offsetY;
});
openTime = lastTime = Date.now();
};
socket.onclose = function (e) {
console.log("!!! CLOSING !!! " + e.code + " " + e.reason + " --" + e.wasClean);
console.log('Time since open: ' + (Date.now() - openTime) + 'ms');
console.log('Time since last: ' + (Date.now() - lastTime) + 'ms');
icon.remove();
};
socket.onmessage = function (e) {
var msg, box_offset;
console.log(e);
msg = JSON.parse(e.data);
box_offset = $('#websocket_box').offset();
if (msg && Number.isFinite(msg.x) && Number.isFinite(msg.y)) {
console.log((msg.x + box_offset.left) + ', ' + (msg.y + box_offset.top));
icon.offset({
left: msg.x + box_offset.left,
top: msg.y + box_offset.top
}).text(msg.user || '');
}
};
});
asgi.py
:
import os
from channels.asgi import get_channel_layer
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "my_project.settings")
channel_layer = get_channel_layer()
settings.py
:
CHANNEL_LAYERS = {
'default': {
'BACKEND': 'asgi_redis.RedisChannelLayer',
'ROUTING': 'main.routing.channel_routing',
'CONFIG': {
'hosts': [
'redis://redis:6379/2',
],
'symmetric_encryption_keys': [
SECRET_KEY,
],
}
}
}
原来问题出在接口服务器前面的nginx代理上。代理设置为 proxy_read_timeout 20s;
。如果服务器生成了 keepalive ping,则这些不会被计入上游读取超时。将此超时增加到一个更大的值允许 Websocket 保持打开更长时间。我将 proxy_connect_timeout
和 proxy_send_timeout
保留在 20s
。
简短问题版本:我在 Daphne 配置、消费者代码或客户端代码中做错了什么?
channels==1.1.8
daphne==1.3.0
Django==1.11.7
详情如下:
我正在尝试使用 Django Channels 和 Daphne 接口服务器保持持久的 Websocket 连接打开。我正在使用大多数默认参数启动 Daphne:daphne -b 0.0.0.0 -p 8000 my_app.asgi:channel_layer
.
我看到连接在浏览器空闲一段时间后关闭,大约 20 秒多一点。与断开连接一起发送的 CloseEvent
的 code
值为 1006
(异常关闭),未设置 reason
,并且 wasClean
设置为 false。 应该是服务器关闭连接而不发送明确的关闭帧。
Daphne CLI 有 --ping-interval
和 --ping-timeout
标志,默认值分别为 20 秒和 30 秒。前者记录为 "The number of seconds a WebSocket must be idle before a keepalive ping is sent,",后者记录为 "The number of seconds before a WebSocket is closed if no response to a keepalive ping,"。我读到这个是因为 Daphne 会等到 WebSocket 空闲 20 秒后发送 ping,如果 30 秒后没有收到响应,则会关闭 Websocket。相反,我看到的是连接在空闲 20 秒后关闭。 (跨越 3 次默认尝试,分别在 20081 毫秒、20026 毫秒和 20032 毫秒后关闭)
如果我将服务器更改为使用 daphne -b 0.0.0.0 -p 8000 --ping-interval 10 --ping-timeout 60 my_app.asgi:channel_layer
启动,连接仍然关闭,大约 20 秒的空闲时间。 (尝试更新 ping 三次后,分别在 19892 毫秒、20011 毫秒、19956 毫秒后关闭)
代码如下:
consumer.py
:
import logging
from channels import Group
from channels.generic.websockets import JsonWebsocketConsumer
from my_app import utilities
logger = logging.getLogger(__name__)
class DemoConsumer(JsonWebsocketConsumer):
"""
Consumer echos the incoming message to all connected Websockets,
and attaches the username to the outgoing message.
"""
channel_session = True
http_user_and_session = True
@classmethod
def decode_json(cls, text):
return utilities.JSONDecoder.loads(text)
@classmethod
def encode_json(cls, content):
return utilities.JSONEncoder.dumps(content)
def connection_groups(self, **kwargs):
return ['demo']
def connect(self, message, **kwargs):
super(DemoConsumer, self).connect(message, **kwargs)
logger.info('Connected to DemoConsumer')
def disconnect(self, message, **kwargs):
super(DemoConsumer, self).disconnect(message, **kwargs)
logger.info('Disconnected from DemoConsumer')
def receive(self, content, **kwargs):
super(DemoConsumer, self).receive(content, **kwargs)
content['user'] = self.message.user.username
# echo back content to all groups
for group in self.connection_groups():
self.group_send(group, content)
routing.py
:
from channels.routing import route
from . import consumers
channel_routing = [
consumers.DemoConsumer.as_route(path=r'^/demo/'),
]
demo.js
:
// Tracks the cursor and sends position via a Websocket
// Listens for updated cursor positions and moves an icon to that location
$(function () {
var socket = new WebSocket('ws://' + window.location.host + '/demo/');
var icon;
var moveTimer = null;
var position = {x: null, y: null};
var openTime = null;
var lastTime = null;
function sendPosition() {
if (socket.readyState === socket.OPEN) {
console.log('Sending ' + position.x + ', ' + position.y);
socket.send(JSON.stringify(position));
lastTime = Date.now();
} else {
console.log('Socket is closed');
}
// sending at-most 20Hz
setTimeout(function () { moveTimer = null; }, 50);
};
socket.onopen = function (e) {
var box = $('#websocket_box');
icon = $('<div class="pointer_icon"></div>').insertAfter(box);
box.on('mousemove', function (me) {
// some browsers will generate these events much closer together
// rather than overwhelm the server, batch them up and send at a reasonable rate
if (moveTimer === null) {
moveTimer = setTimeout(sendPosition, 0);
}
position.x = me.offsetX;
position.y = me.offsetY;
});
openTime = lastTime = Date.now();
};
socket.onclose = function (e) {
console.log("!!! CLOSING !!! " + e.code + " " + e.reason + " --" + e.wasClean);
console.log('Time since open: ' + (Date.now() - openTime) + 'ms');
console.log('Time since last: ' + (Date.now() - lastTime) + 'ms');
icon.remove();
};
socket.onmessage = function (e) {
var msg, box_offset;
console.log(e);
msg = JSON.parse(e.data);
box_offset = $('#websocket_box').offset();
if (msg && Number.isFinite(msg.x) && Number.isFinite(msg.y)) {
console.log((msg.x + box_offset.left) + ', ' + (msg.y + box_offset.top));
icon.offset({
left: msg.x + box_offset.left,
top: msg.y + box_offset.top
}).text(msg.user || '');
}
};
});
asgi.py
:
import os
from channels.asgi import get_channel_layer
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "my_project.settings")
channel_layer = get_channel_layer()
settings.py
:
CHANNEL_LAYERS = {
'default': {
'BACKEND': 'asgi_redis.RedisChannelLayer',
'ROUTING': 'main.routing.channel_routing',
'CONFIG': {
'hosts': [
'redis://redis:6379/2',
],
'symmetric_encryption_keys': [
SECRET_KEY,
],
}
}
}
原来问题出在接口服务器前面的nginx代理上。代理设置为 proxy_read_timeout 20s;
。如果服务器生成了 keepalive ping,则这些不会被计入上游读取超时。将此超时增加到一个更大的值允许 Websocket 保持打开更长时间。我将 proxy_connect_timeout
和 proxy_send_timeout
保留在 20s
。