Cómo utilizar WebSockets en Django 1.10

Moisés Alexander Gama Espinosa
4 min readMay 3, 2017

--

En 2016, la comunidad de desarrolladores de Django recibió una noticia que potenciará sus plataformas web: A partir de Django 1.1o, el framework soportara de manera oficial, el uso de WebSockets, mediante el paquete Django Channels.

Esta nueva caracteristicas permitirá el desarrollo de aplicaciones de tiempo real en Django, es decir, aquellas donde se producen eventos frecuentes en tanto en el servidor, como en el cliente, y que deben notificar estos entre si.

Empezando

Primero, debes ya tener un proyecto de Django, y si no es así, te recomiendo la siguiente guía de DigitalOcean. Este tutorial fue creado bajo para el siguiente conjunto de tecnologías:

  • Python 2.7+ o 3.4+
  • Virtualenv
  • Pip
  • Django 1.8+
  • Postgrsql

Por supuesto, algunos elementos, como el último, son opcionales, y pueden ser sustituidos. Además, los comandos de instalación seran para ubuntu, sin que sean exclusivos los paquetes de esta distribución.

Lo primero, es instalar Redis Server, que es un servidor que útiliza colas de mensajes en entrada y salida:

sudo add-apt-repository ppa:chris-lea/redis-server
sudo apt-get update
sudo apt-get -y -q install redis-server

redis-cli ping # Debe imprimir PONG

Una vez se termine de instalar, agrega los siguientes paquetes a tu requirements.txt

asgi-redis==1.0.0
daphne==0.12.1
celery==4.0.2
channels==1.0.2
redis==2.10.5

E instalalos mediantes Pip; Recuerda hacerlo dentro de tu virtualenv

$ source /home/web/venv/bin/activate
$ pip install
-r requirements.txt
$deactivate

Configuración

En tu archivo de configuración del proyecto, settings.py, incluye las siguientes lineas

INSTALLED_APPS = [
#…
‘channels’,
‘jobs’,
]
WSGI_APPLICATION = 'yourapp.wsgi.application'CHANNEL_LAYERS = {
“default”: {
“BACKEND”: “asgi_redis.RedisChannelLayer”,
“CONFIG”: {
“hosts”: [os.environ.get(‘REDIS_URL’, ‘redis://localhost:6379’)],
},
“ROUTING”: “yourapp.routing.channel_routing”,
},
}
BROKER_URL = ‘redis://localhost:6379/0’ # Al Redis ServerCELERY_ACCEPT_CONTENT = [‘json’]
CELERY_TASK_SERIALIZER = ‘json’
CELERY_RESULT_SERIALIZER = ‘json’

En la carpeta principal del proyecto, debes crear un archivo que se llame routing.py; En el se definen los principales eventos de los WebSockets: La conexión inicial y cuando se recibe un mensaje

from channels import route
from jobs import consumers
channel_routing = [
# Las funciones se definen en consumers.py
route(“websocket.connect”, consumers.ws_connect),
route(“websocket.receive”, consumers.ws_receive),
]

También es necesario crear el archivo asgi.py en el mismo directorio:

import osfrom channels.asgi import get_channel_layer
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "example.settings")
channel_layer = get_channel_layer()

Y el archivo wsgi.py; Este es muy importante, pues es el enlace entre Redis Server y nuestra aplicación

import os from django.core.wsgi import get_wsgi_application os.environ.setdefault("DJANGO_SETTINGS_MODULE", "example.settings") application = get_wsgi_application()

Recibiendo mensajes

En nuestro proyecto, debemos crear una nueva app que se encarge de recibir y contestar los mensajes recibidos mediante WebSockets. A esta app la llamaremos jobs

$ python manage.py startapp jobs

Y creamos dentro de jobs el archivo consumers.py; Este es un equivalente a views.py, pero para WebSockets. En el recibimos los mensajes, y podemos realizar cualquier lógica necesaria, incluido el uso modelos, para contestar los mensajes.

import json
import logging
from channels import Channel
from channels.sessions import channel_session
from .models import Job
from .tasks import long_process
from example.celery import app
log = logging.getLogger(__name__)@channel_session
def ws_connect(message):
message.reply_channel.send({
“text”: json.dumps({
“action”: “reply_channel”,
“reply_channel”: message.reply_channel.name,
})
})
@channel_session
def ws_receive(message):
try:
data = json.loads(message[‘text’])
except ValueError:
log.debug(“ws message isn’t json text=%s”, message[‘text’])
return
if data:
reply_channel = message.reply_channel.name
if data[‘action’] == “long_process”:
long_process(data, reply_channel)
else:
Channel(reply_channel).send({
"text": json.dumps ({
"action": "completed",
"content": "short_process"
})

Para respuestas que tarden mucho tiempo en responderse, lo mejor es crear una tarea asíncrona, que cuando termine envie los resultados. Celery es especialmente útil para esto, por lo que debemos crear, dentro de jobs el archivo tasks.py

from __future__ import absolute_import
import time
import json
import logging
from example.celery import app
from .models import Job
from channels import Channel
log = logging.getLogger(__name__)@app.task
def long_process(job_id, reply_channel):
# sleep para simular un proceso largo
time.sleep(5)
# responder al cliente
if reply_channel is not None:
Channel(reply_channel).send({
"text": json.dumps ({
"action": "completed",
"content": "long_process"
})
})

Para correr el servidor en modo prueba se puede utilizar manage.py runserver, sin embargo, no es aconsejable ocupar esto en ambiente de producción; Para iniciar el servidor en producción se ejecutan los siguientes comandos, que pueden incluirse en supervisord

$ python manage.py runserver
$ celery worker -A example -l info

Cliente

El lado de Front End permanece casi igual. Lo único nuevo es el siguiente bloque de código, que es el encargado de iniciar y mantener la conexión con el servidor. Además procesa los mensajes recibidos.

$(function() {
// When we're using HTTPS, use WSS too.
var ws_scheme = window.location.protocol == "https:" ? "wss" : "ws";
var ws_path = ws_scheme + '://' + window.location.host + '/dashboard/';
console.log("Connecting to " + ws_path)
var socket = new ReconnectingWebSocket(ws_path);
socket.onmessage = function(message) {
console.log("Got message: " + message.data);
var data = JSON.parse(message.data);
// if action is started, add new item to table
if (data.action == "started") {
task_status.append("Mensaje recibido");
}
// if action is completed, just update the status
else if (data.action == "completed"){
var item = $('#item-status-' + data.job_id + ' span');
item.attr("class", "label label-success");
item.text(data.job_status);
}
};
$("#taskform").on("submit", function(event) {
var message = {
action: "long_process",
job_name: $('#task_name').val()
};
socket.send(JSON.stringify(message));
$("#task_name").val('').focus();
return false;
});
});

Conclusión

La inclusión de WebSockets a Django permitira una nueva era en el desarrollo web, pues permitiran hacer aplicaciones mucho más interactivas y con datos en tiempo real.

A pesar de todavía es dificil instalar los paquetes necesarios para hacer uso de WebSockets, en las siguientes versiones de Django será una caracteristica embebida.

--

--