Friday, July 1, 2016

Django and Websockets. Setup for production

Django Channels is promising but not ready yet. This technology hasn't been included in Django 1.10 (see Django 1.10 release notes) as was promised earlier. Another reason is that daphne is not a reliable replacement at the moment for a bullet proof uwsgi or gunicorn.
A typical way to approach websockets is to use socket.io running inside a NodeJS. But having uwsgi this is not necessary as uwsgi offers websockets. It rather a low-level solution, in order to omit boilerplate it is advisory to use django-websocket-redis. Its setup can be a tricky thing especially if you targeting scalable solution for production. I've been using this approach in 2 projects in production and gathered minimal django application with websockets on board with production ready configuration. See details below. Full source code is available.

Start nginx & redis & uwsgi. In order to do this use docker, to orchestrate containers use docker-compose, see docker-compose.yml below
web:
    image: python:3.5.2
    volumes:
        - ./:/web
        - /media
        - /static
    links:
        - redis:redis
    expose:
        - "9000"
    environment:
        - DJANGO_SETTINGS_MODULE=DjangoWebsocketRedisTest.settings
    command: /bin/bash -c "cd /web && source runit.sh"

redis:
    image: redis:3.2
    expose:
        - "6379"

nginx:
    image: nginx:1.7.11
    volumes_from:
        - web
    links:
        - web:web
    expose:
        - "9000"
    ports:
        - "9000:9000"
    volumes:
        - ./nginx.conf:/etc/nginx/conf.d/web.conf
Standard images of python, nginx and redis are used. Volumes from python container are referenced from nginx to server media and static files and to access unix domain socket of uwsgi.
runit.sh starts 2 processes: uwsgi for django and uwsgi with gevent for websockets (you'd better use supervisor here or similar)
...
uwsgi --http-socket uwsgi_ws.sock --chmod-socket --gevent 1000 --http-websockets --workers=2 --master --module DjangoWebsocketRedisTest.wsgi_ws &
exec uwsgi --touch-reload reload --http-socket uwsgi.sock --chmod-socket --master --module "django.core.wsgi:get_wsgi_application()" --processes 2
Excerpt from nginx.conf:
...
    location / {
        proxy_pass       http://unix:/web/uwsgi.sock;
        proxy_set_header Host      $http_host;
        proxy_set_header X-Real-IP $remote_addr;
    }

    location /ws/ {
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_pass http://unix:/web/uwsgi_ws.sock;
    }
...
/ws/ part will be handled by wsgi_ws and everything else as usual by django.

In order to test our setup go to http://localhost:9000/ - this is a test client subscribed to websocket http://localhost:9000/ws/foobar?subscribe-broadcast&publish-broadcast&echo. It uses special WS4Redis object provided by django-websocket-redis app.
jQuery(document).ready(function($) {
    var ws4redis = WS4Redis({
        uri: '{{ WEBSOCKET_URI }}foobar?subscribe-broadcast&publish-broadcast&echo',
        receive_message: receiveMessage,
        heartbeat_msg: {{ WS4REDIS_HEARTBEAT }}
    });

    // attach this function to an event handler on your site
    function sendMessage() {
        ws4redis.send_message('A message');
    }

    // receive a message though the websocket from the server
    function receiveMessage(msg) {
        $( "#messages" ).append( "" + msg + "

" );
    }
});
To send messages go to http://localhost:9000/send. This view publishes message to websocket which is immediately displayed on http://localhost:9000/
...
def send(request):
    redis_publisher = RedisPublisher(facility='foobar', broadcast=True)
    messageString = datetime.datetime.now().__str__()
    redis_publisher.publish_message(RedisMessage(messageString))
    return HttpResponse(messageString)
...