A diferencia que AJAX, el protocolo WebSocket proporciona una comunicación bidireccional entre el servidor y el cliente. Podemos intercambiar información asíncrona sin la obligación de hacer más peticiones u obligar a un refresco del navegador; es posible enviar información al servidor en cualquier momento o recibirla. Idóneo para montar un Chat, objetivo de este tutorial. Aunque los usos son ilimitados: editar información en tiempo real, enviar notificaciones, etc. Tu creatividad es la única limitación técnica.
Cuando trabajamos con Django es posible utilizar WebSockets gracias a los Channels, una función nacida en el agosto del 2016 para gestionar este tipo de tareas en paralelo, y lanzaremos un servidor web bautizado como Daphne adapto para esta labor.
Resultado final
Características del chat
- Integración completa con Django y Websockets, por medio de Channels.
- Respuestas asincronas con el objetivo de mejorar el rendimiento.
- Sala grupal donde todos reciben los mensajes nuevos en tiempo real.
- Front-End minimalista con JavaScript para su fácil implementación en otros sistemas.
Tutorial
1) Levantar una base de datos y Redis
Antes de empezar a trabajar con Django necesitaremos 2 piezas fundamentales.
- Base de datos relacional: en el tutorial se incorporará PostgreSQL al ser la más recomendada dentro del ecosistema. Su objetivo es almacenar los datos de los usuarios, como su perfil e historial.
- Base de datos clave-valor: En este caso usaremos Redis. Con ella gestionaremos los mensajes para separar los usuarios en salas y puedan mantener conversaciones privadas.
Para simplificar la tarea de instalación y configuración lanzaremos estos servicios con Docker.
Creamos un documento con el nombre docker-compose.yaml
con el siguiente contenido.
version: '3.1'
services:
db:
image: postgres
restart: always
volumes:
- ./postgres:/var/lib/postgresql/data
environment:
POSTGRES_DB: postgres
POSTGRES_PASSWORD: postgres
ports:
- 5432:5432
redis:
image: redis:alpine
restart: always
ports:
- 6379:6379
Ejecutamos.
docker-compose up -d
2) Crear entorno virtual
Doy por sentado que dispones en el equipo el paquete python-virtualenv
. En caso contrario no dudes de buscarlo para tu sistema.
Después creamos un entorno virtual.
python3 -m venv .venv
Iniciamos el entorno.
source .venv/bin/activate
3) Instalar dependencias
Antes de continuar deberemos instalar las dependencias de Python que nos dejarán acceder a elementos no nativos de Django como un servidor asíncrono o Websockets.
Creamos un archivo llamado requirements.txt
con el contenido.
# Django
django
# Servidor para Django
daphne==2.4.1
# Conector para PostgreSQL
psycopg2-binary
# Channels
channels==2.4.0
# Conector de Redis para Channels
channels_redis
Instalamos con pip.
pip3 install -r requirements.txt
4) Crear proyecto Django
Creamos el esqueleto del proyecto.
django-admin startproject mi_web
cd mi_web
Ahora, dentro del proyecto Django crearemos 2 aplicaciones.
- front: Mostrará el HTML necesario para que los usuarios puedan navegar.
- chat: Gestionará los Websockets y salas.
Creamos los directorios que usaremos.
mkdir -p app/front app/chat
Generaremos las aplicaciones.
django-admin startapp front app/front
django-admin startapp chat app/chat
En mi_web/settings.py
activamos ambas.
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'app.chat',
'app.front',
]
Es el momento de configurar la base de datos. Modificamos la siguiente sección.
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': BASE_DIR / 'db.sqlite3',
}
}
… por la siguiente.
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'NAME': 'postgres',
'USER': 'postgres',
'PASSWORD': 'postgres',
'HOST': '127.0.0.1',
'PORT': '5432',
}
}
Actualizamos la base de datos.
python3 manage.py makemigrations
python3 manage.py migrate
Django esta preparado.
5) Incluir channels
Incluimos channels
al listado de aplicaciones dentro de mi_web/settings.py
. Debe estar antes de las anteriores.
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'channels',
'app.chat',
'app.front',
]
Continuamos configurando settings.py
, de indicamos a channels
donde estará su base de datos para gestionar las salas.
CHANNEL_LAYERS = {
"default": {
"BACKEND": "channels_redis.core.RedisChannelLayer",
"CONFIG": {
"hosts": [("127.0.0.1", 6379)],
},
},
}
6) Enviar/Recibir por Websockets
Es el momento de montar una puerta de entrada para que el cliente de Websocket pueda conectarse y esperar nuevo contenido, para ser alimentado con información asíncrona hasta que se sacie o se desconecte. A este concepto se le denomina Consumidor (Consumer).
Para aplicarlo dentro de Django creamos un archivo llamado consumers.py
en app/chat/
con el siguiente contenido.
# app/chat/consumers.py
import json
from channels.generic.websocket import AsyncWebsocketConsumer
from asgiref.sync import sync_to_async
class ChatConsumer(AsyncWebsocketConsumer):
async def connect(self):
''' Cliente se conecta '''
# Recoge el nombre de la sala
self.room_name = self.scope["url_route"]["kwargs"]["room_name"]
self.room_group_name = "chat_%s" % self.room_name
# Se une a la sala
await self.channel_layer.group_add(self.room_group_name, self.channel_name)
# Informa al cliente del éxito
await self.accept()
async def disconnect(self, close_code):
''' Cliente se desconecta '''
# Leave room group
await self.channel_layer.group_discard(self.room_group_name, self.channel_name)
async def receive(self, text_data):
''' Cliente envía información y nosotros la recibimos '''
text_data_json = json.loads(text_data)
name = text_data_json["name"]
text = text_data_json["text"]
# Enviamos el mensaje a la sala
await self.channel_layer.group_send(
self.room_group_name,
{
"type": "chat_message",
"name": name,
"text": text,
},
)
async def chat_message(self, event):
''' Recibimos información de la sala '''
name = event["name"]
text = event["text"]
# Send message to WebSocket
await self.send(
text_data=json.dumps(
{
"type": "chat_message",
"name": name,
"text": text,
}
)
)
Lo siguiente es definir sus rutas.
Creamos routing.py
en la aplicación de chat.
# app/chat/routing.py
from django.urls import re_path
from . import consumers
websocket_urlpatterns = [
re_path(r'ws/chat/(?P<room_name>\w+)/$', consumers.ChatConsumer),
]
7) Cambiar a un servidor asincrono.
Lamentablemente Django no nos permite trabajar con aplicaciones asincronía con su servidor WSGI. Deberemos dar el salto a ASGI (Asynchronous Server Gateway Interface) su sucesor espiritual, creado con la finalidad de otorgar una interfaz estándar entre servidores web, marcos y aplicaciones Python con capacidad asíncrona.
Creamos en la raíz el archivo asgi.py
con el siguiente contenido.
import os
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "settings")
import django
django.setup()
from channels.auth import AuthMiddlewareStack
from channels.routing import ProtocolTypeRouter, URLRouter
from django.core.asgi import get_asgi_application
from app.chat.routing import websocket_urlpatterns
application = ProtocolTypeRouter(
{
"websocket": AuthMiddlewareStack(URLRouter(websocket_urlpatterns)),
}
)
Al final de mi_web/settings.py
añadimos la configuración para que Django cambie a ASGI.
ASGI_APPLICATION = "asgi.application"
Lanzamos el servidor.
python3 manage.py runserver
Nos encontraremos un mensaje informativo que nos confirmará su conexión.
Starting ASGI/Channels
Opcionalmente puedes borrar en mi_web/settings.py
.
WSGI_APPLICATION = 'mi_web.wsgi.application'
8) Montar Front-End para chat
El Back-End esta hambriendo de contenido y los WebSockets están prácticamente sin estrenar. Vamos a construir un minimalista interfaz web para poder enviar y recibir todos los mensajes que necesitemos sin limitación de usuarios. Usaremos como base el sistema de plantillas que incorpora Django.
Dentro de app/front/
creamos la carpeta templates
y dentro el archivo chat.html
. Quedando con la siguiente ruta: app/front/templates/chat.html
. Dentro incluiremos todo el HTML y JavaScript necesario. El stack tecnológico lo puedes customizar al gusto, en el ejemplo he dejado lo esencial.
- HTML5
- Spectre.css para el aspecto visual.
- JavaScript Vanilla.
<!doctype html>
<html lang="es">
<head>
<meta charset="UTF-8"/>
<title>Chat</title>
<!-- Spectre CSS -->
<link rel="stylesheet" href="https://unpkg.com/spectre.css/dist/spectre.min.css">
</head>
<body>
<div class="container grid-md">
<h1 class="mt-2 text-center">Chat con Django</h1>
<div class="columns">
<aside class="column col-4">
<!-- Configurar alias -->
<div class="form-group">
<label class="form-label">
Nombre
<input id="nombre" class="form-input" type="text" placeholder="Oveja programadora...">
</label>
</div>
<!-- Fin Configurar alias -->
</aside>
<main class="column">
<!-- Mensajes -->
<section id="mensajes"></section>
<!-- Fin Mensajes -->
<!-- Enviar mensajes -->
<section class="form-group">
<input id="texto" class="form-input" type="text" placeholder="Nuevo mensaje...">
<button id="enviar" class="btn btn-block">Enviar</button>
</section>
<!-- Fin Enviar mensajes -->
</main>
</div>
</div>
<script>
/*
* VARIABLES
*/
// Define el nombre de la sala
const SALA_CHAT = 'python';
// Conecta con WebSockets
const CHAT_SOCKET = new WebSocket('ws://localhost:8000/ws/chat/' + SALA_CHAT + '/');
// Captura el campo con el nombre del usuario
const CAMPO_NOMBRE = document.querySelector('#nombre');
// Captura el contenedor que posee todos los mensajes
const MENSAJES = document.querySelector('#mensajes');
// Captura el campo con el nuevo texto
const CAMPO_TEXTO = document.querySelector('#texto');
// Boton para enviar mensaje
const BOTON_ENVIAR = document.querySelector('#enviar');
/*
* FUNCIONES
*/
/**
* Método que añade un nuevo mensaje en el HTML (#mensajes)
*/
function anyadirNuevoMensajeAlHTML(nombre, texto, propio = false) {
// Contenedor
const MI_CONTENEDOR = document.createElement('div');
MI_CONTENEDOR.classList.add(propio ? 'bg-primary' : 'bg-secondary', 'p-2');
// Nombre
const MI_NOMBRE = document.createElement('h2');
MI_NOMBRE.classList.add('text-tiny', 'text-bold', 'mt-2');
MI_NOMBRE.textContent = nombre;
MI_CONTENEDOR.appendChild(MI_NOMBRE);
// Texto
const MI_TEXTO = document.createElement('p');
MI_TEXTO.classList.add('my-2');
MI_TEXTO.textContent = texto;
MI_CONTENEDOR.appendChild(MI_TEXTO);
// Anyade todo a MENSAJES
MENSAJES.appendChild(MI_CONTENEDOR);
}
/**
* Método que envia el mensaje al consumer por medio de WebSockets
*/
function enviarNuevoMensaje() {
// Envia al WebSocket un nuevo mensaje
CHAT_SOCKET.send(JSON.stringify({
name: CAMPO_NOMBRE.value,
text: CAMPO_TEXTO.value
}));
// Limpiamos el campo donde hemos escrito
CAMPO_TEXTO.value = '';
// Le volvemos a dar el foco para escribir otro mensaje
CAMPO_TEXTO.focus();
}
/*
* EVENTOS
*/
// Conectado
CHAT_SOCKET.addEventListener('open', () => {
console.log('Conectado');
});
// Desconectado
CHAT_SOCKET.addEventListener('close', () => {
console.log('Desconectado');
});
// Recibir mensaje
CHAT_SOCKET.addEventListener('message', (event) => {
console.log('Recibido nuevo mensaje');
const MI_NUEVA_DATA = JSON.parse(event.data);
anyadirNuevoMensajeAlHTML(MI_NUEVA_DATA.name, MI_NUEVA_DATA.text, MI_NUEVA_DATA.name === CAMPO_NOMBRE.value);
});
// Enviar mensaje cuando se pulsa en el botón Enviar
BOTON_ENVIAR.addEventListener('click', enviarNuevoMensaje);
// Enviar mensaje cuando se pulsa en el teclado Enter
CAMPO_TEXTO.addEventListener('keyup', (e) => e.keyCode === 13 ? enviarNuevoMensaje() : false);
</script>
</body>
</html>
Dentro de app/front/views.py
indicamos en el vista donde está la plantilla que acabamos de definir.
# app/front/views.py
from django.shortcuts import render
def chat(request):
return render(request, 'chat.html')
Definimos la ruta donde será mostrado la vista, iremos a mi_web/urls.py
.
# mi_web/urls.py
from django.contrib import admin
from django.urls import path
from app.front import views
urlpatterns = [
path('admin/', admin.site.urls),
path('', views.chat),
]
9) Probarlo
Abrimos en 2 navegadores/ventajas/pestañas la siguiente ruta http://127.0.0.1:8000/
.
En cada uno indicamos un nombre diferente y escribimos en la caja de mensaje.
Te felicito, acabas de crear un chat en Django.
Apuntes finales
Si algún paso no ha funcionado como esperabas dispones del código completo en Github unificando todo lo explicado en el tutorial.
Para terminar quiero transmitirte que realizar un chat moderno es laborioso, aún queda muchas características que puedes incluir:
- Capacidad para borrar mensajes individuales.
- Integrar los usuarios.
- Marcas quienes están conectados.
- Enviar archivos.
- Y muchas otras…
Es una excelente base para continuar el trabajo. Te deseo mucha suerte en el camino, estoy seguro que poco a poco llegarás donde te lo propongas.
{{ comments.length }} comentarios