Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Websocket timeout in Django Channels / Daphne

Short question version: what am I doing wrong in my Daphne config, or my Consumer code, or my client code?

channels==1.1.8
daphne==1.3.0
Django==1.11.7

Details below:


I am trying to keep a persistent Websocket connection open using Django Channels and the Daphne interface server. I am launching Daphne with mostly default arguments: daphne -b 0.0.0.0 -p 8000 my_app.asgi:channel_layer.

I am seeing the connections closing after some idle time in the browser, shortly over 20 seconds. The CloseEvent sent with the disconnect has a code value of 1006 (Abnormal Closure), no reason set, and wasClean set to false. This should be the server closing the connection without sending an explicit close frame.

The Daphne CLI has --ping-interval and --ping-timeout flags with default values of 20 and 30 seconds, respectively. This is documented as "The number of seconds a WebSocket must be idle before a keepalive ping is sent," for the former, and "The number of seconds before a WebSocket is closed if no response to a keepalive ping," for the latter. I read this as Daphne will wait until a WebSocket has been idle for 20 seconds to send a ping, and will close the Websocket if no response is received 30 seconds later. What I am seeing instead is connections getting closed after being 20 seconds idle. (Across three attempts with defaults, closed after 20081ms, 20026ms, and 20032ms)

If I change the server to launch with daphne -b 0.0.0.0 -p 8000 --ping-interval 10 --ping-timeout 60 my_app.asgi:channel_layer, the connections still close, around 20 seconds idle time. (After three attempts with updated pings, closed after 19892ms, 20011ms, 19956ms)

Code below:


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,
            ],
        }
    }
}
like image 642
wmorrell Avatar asked Nov 27 '17 21:11

wmorrell


People also ask

Does WebSocket have a timeout?

The websocket-idle-timeout command sets the maximum idle time for client connections with the handler. This timer monitors the idle time in the data transfer process. If the specified idle time is exceeded, the connection is torn down.

What is Daphne Django?

Daphne is a pure-Python ASGI server for UNIX, maintained by members of the Django project. It acts as the reference server for ASGI.

What is default timeout WebSocket?

A WebSocket times out if no read or write activity occurs and no Ping messages are received within the configured timeout period. The container enforces a 30-second timeout period as the default.

Is Django good for WebSockets?

Django Channels facilitates support of WebSockets in Django in a manner similar to traditional HTTP views. It wraps Django's native asynchronous view support, allowing Django projects to handle not only HTTP, but also protocols that require long-running connections, such as WebSockets, MQTT, chatbots, etc.

How do I connect to a WebSocket socket in Django?

Django Channels has everything you need to establish WebSocket connections in Django. First, create a project in Django and then an application by any name of your choice and create a virtual environment (Basic level Django stuff) Go to your settings.py file and add “channels” to your list of “installed applications”

Can I replace Daphne with another WebSocket termination server?

In this case you can replace daphne with any other Websocket termination server: The “new” item in the building blocks above is therefore daphne: Daphne is a HTTP, HTTP2 and WebSocket protocol server for ASGI and ASGI-HTTP, developed to power Django Channels.

What is Daphne in Django?

The “new” item in the building blocks above is therefore daphne: Daphne is a HTTP, HTTP2 and WebSocket protocol server for ASGI and ASGI-HTTP, developed to power Django Channels. It supports automatic negotiation of protocols; there’s no need for URL prefixing to determine WebSocket endpoints versus HTTP endpoints.

What is the difference between Django channels and HTTP?

Django still handles traditional HTTP, whilst Channels give you the choice to handle other connections in either a synchronous or asynchronous style. To get started understanding Channels, read our Introduction , which will walk through how things work.


Video Answer


1 Answers

The underlying problem turned out to be the nginx proxy in front of the interface server. The proxy was set to proxy_read_timeout 20s;. If there were keepalive pings generated from the server, these were not getting counted toward the upstream read timeout. Increasing this timeout to a larger value allows the Websocket to stay open longer. I kept proxy_connect_timeout and proxy_send_timeout at 20s.

like image 107
wmorrell Avatar answered Oct 19 '22 20:10

wmorrell