sábado, 4 de mayo de 2024

32.- Construyendo un servidor de chat.

En este post vamos a construir un servidor de chat para que los estudiantes se puedan comunicar entre si. Los estudiantes podrán entrar en las diferentes salas de chat en la que estén matriculados. Para ello, aprenderemos como usar un ASGI (Asynchronous Server Gateway Interface) para implementar una comunicación asíncrona. 

Los puntos que veremos son:

  • Añadir canales de chat al proyecto.
  • Crear un WebSocket y un enrutamiento adecuado.
  • Implementar un cliente de WebSocket.
  • Habilitar Channels con REDIS.
  • Hacer que todo funcione de manera asíncrona.

Vamos a ello.


Creando una aplicación de Chat.


Vas a implementar un servidor de chat para proporcionar a los estudiantes una sala de chat para cada curso. Los estudiantes inscritos en un curso podrán acceder a la sala de chat del curso e intercambiar mensajes en tiempo real. Utilizarás Channels para construir esta funcionalidad. Channels es una aplicación de Django que extiende Django para manejar protocolos que requieren conexiones de larga duración, como WebSockets, chatbots o MQTT (un transporte de mensajes de publicación/suscripción liviano comúnmente utilizado en proyectos de Internet de las Cosas (IoT)). Usando Channels, puedes implementar fácilmente funcionalidades en tiempo real o asíncronas en tu proyecto además de tus vistas síncronas HTTP estándar. Comenzarás agregando una nueva aplicación a tu proyecto. La nueva aplicación contendrá la lógica para el servidor del chat.

Puedes encontrar la documentación de Django Channels en https://channels.readthedocs.io/.

Vamos a empezar a implementar el servidor de chat. Ejecuta el siguiente comando desde el directorio del proyecto educa para crear la nueva estructura de archivos de la aplicación:

django-admin startapp chat


Edita el archivo de configuración del proyecto educa (settings.py) para registrar la aplicación.

INSTALLED_APPS = [
    # ...
    'chat',
]

La nueva aplicación "chat" está ya activa en nuestro proyecto.


Implementando la vista de la sala de Chat.


En este apartado vamos a crear una sala de chat para que los estudiantes se comuniquen entre si pero que sea diferente para cada curso. Para ello crearemos una vista para que solo los estudiantes que estén matriculados en ese curso puedan acceder a ella. 

Edita el archivo views.py de la nueva aplicación de chat y añade el siguiente código:

Elearning/educa/chat/views.py

from django.shortcuts import render, get_object_or_404
from django.http import HttpResponseForbidden
from django.contrib.auth.decorators import login_required

@login_required
def course_chat_room(request, course_id):
    try:
        # retrieve course with given id joined by the current user
        course = request.user.courses_joined.get(id=course_id)
    except:
        # user is not a student of the course or course does not exist
        return HttpResponseForbidden()
    return render(request, 'chat/room.html', {'course': course})

Esta es la vista "course_chat_room". En esta vista, se utiliza el decorador @login_required para evitar que cualquier usuario no autenticado acceda a la vista. La vista recibe un parámetro obligatorio course_id que se utiliza para recuperar el curso con el id proporcionado. 

Accedes a los cursos en los que el usuario está inscrito a través de la relación courses_joined y recuperas el curso con el id proporcionado de ese subconjunto de cursos. Si el curso con el id proporcionado no existe o el usuario no está inscrito en él, se devuelve una respuesta HttpResponseForbidden, que se traduce en una respuesta HTTP con estado 403.

Si el curso con el id proporcionado existe y el usuario está inscrito en él, se renderiza la plantilla chat/room.html, pasando el objeto de curso al contexto de la plantilla.

Necesitas agregar un patrón de URL para esta vista. Crea un nuevo archivo dentro del directorio de la aplicación de chat y nómbralo urls.py. Agrega el siguiente código a él:

Elearning/educa/chat/urls.py

from django.urls import path
from . import views

app_name = 'chat'

urlpatterns = [
    path('room/<int:course_id>/', views.course_chat_room, name='course_chat_room'),
]
Este es el parámetro inicial de la URL de la aplicación chat. Hemos definido el patrón course_chat_room, incluyendo el parámetro course_id con el prefijo int, que como ya sabes recibe un número entero.

Incluye el patrón principal de la aplicación chat dentro de los patrones de la aplicación principal. Edita el archivo urls.py del proyecto principal y añade en él la siguiente línea de código:

Elearning/educa/educa/urls.py

urlpatterns = [
# ...
    path('chat/', include('chat.urls', namespace='chat')),
]
También tenemos que crear la plantilla para la vista course_chat_room. Esta plantilla tendrá una zona para visualizar los mensajes que intercambien en el chat y otra para introducir el texto con un botón para enviar el mensaje al chat. 

Crea la siguiente estructura de directorios dentro de la aplicación chat.

templates/
        chat/
                room.html

Edita esta nueva plantilla que hemos creado, room.html, y añade el siguiente código:

Elearning/educa/chat/templates/chat/room.html

% extends "base.html" %}

{% block title %}Chat room for "{{ course.title }}"{% endblock %}

{% block content %}

<div id="chat">
</div>

<div id="chat-input">
    <input id="chat-message-input" type="text">
    <input id="chat-message-submit" type="submit" value="Send">
</div>

{% endblock %}

{% block include_js %}

{% endblock %}

{% block domready %}

{% endblock %}
Esta es la plantilla para la sala de chat del curso. En esta plantilla, extiendes la plantilla base.html de tu proyecto y llenas su bloque de contenido. En la plantilla, defines un elemento HTML <div> con el ID de chat que utilizarás para mostrar los mensajes de chat enviados por el usuario y por otros estudiantes. También defines un segundo elemento <div> con un campo de entrada de texto y un botón de enviar que permitirá al usuario enviar mensajes.

 Agregas los bloques include_js y domready definidos en la plantilla base.html, que implementarás más tarde, para establecer una conexión con un WebSocket y enviar o recibir mensajes. Ejecuta el servidor de desarrollo y abre http://127.0.0.1:8000/chat/room/1/ en tu navegador, reemplazando 1 con el ID de un curso existente en la base de datos. Accede a la sala de chat con un usuario conectado que esté inscrito en el curso. Verás la siguiente pantalla:


sala de chat


Django en tiempo real con Channels


Estás construyendo un servidor de chat para proporcionar a los estudiantes una sala de chat para cada curso. Los estudiantes inscritos en un curso podrán acceder a la sala de chat del curso e intercambiar mensajes. Esta funcionalidad requiere comunicación en tiempo real entre el servidor y el cliente. El cliente debería poder conectarse al chat y enviar o recibir datos en cualquier momento. Hay varias formas en las que podrías implementar esta característica, utilizando AJAX polling o long polling en combinación con el almacenamiento de los mensajes en tu base de datos o Redis. Sin embargo, no hay una forma eficiente de implementar un servidor de chat utilizando una aplicación web síncrona estándar. Vamos a construir un servidor de chat utilizando comunicación asíncrona a través de ASGI.


Aplicaciones asíncronas utilizando ASGI


Django suele desplegarse utilizando Web Server Gateway Interface (WSGI), que es la interfaz estándar para que las aplicaciones Python manejen las solicitudes HTTP. Sin embargo, para trabajar con aplicaciones asíncronas, necesitas usar otra interfaz llamada ASGI, que también puede manejar solicitudes WebSocket. ASGI es el estándar emergente de Python para servidores y aplicaciones web asíncronas.

Puedes encontrar una introducción a ASGI en https://asgi.readthedocs.io/en/latest/introduction.html.

Django viene con soporte para ejecutar Python asíncrono a través de ASGI. Es compatible con la escritura de vistas asíncronas desde Django 3.1 y Django 4.1 introduce manejadores asíncronos para vistas basadas en clases. Channels se basa en el soporte ASGI nativo disponible en Django y proporciona funcionalidades adicionales para manejar protocolos que requieren conexiones de larga duración, como WebSockets, protocolos IoT y protocolos de chat.

Los WebSockets proporcionan comunicación full-duplex al establecer una conexión persistente, abierta y bidireccional del Protocolo de Control de Transmisión (TCP) entre servidores y clientes. Utilizaremos WebSockets para implementar tu servidor de chat.

Puedes encontrar más información sobre cómo desplegar Django con ASGI en https://docs.djangoproject.com/en/5.0/howto/deployment/asgi/.

Puedes encontrar más información sobre el soporte de Django para escribir vistas asíncronas en https://docs.djangoproject.com/en/5.0/topics/async/ y el soporte de Django para vistas basadas en clases asíncronas en https://docs.djangoproject.com/en/4.1/topics/class-based-views/#async-classbased-views.

El ciclo de solicitud/respuesta utilizando Channels


Es importante entender las diferencias en el ciclo de solicitud entre un ciclo de solicitud síncrono estándar y una implementación de Channels. El siguiente esquema muestra el ciclo de solicitud de una configuración síncrona de Django:

el ciclo petición respuesta de django


Cuando se envía una solicitud HTTP desde el navegador al servidor web, Django maneja la solicitud y pasa el objeto HttpRequest a la vista correspondiente. La vista procesa la solicitud y devuelve un objeto HttpResponse que se envía de vuelta al navegador como una respuesta HTTP. No hay un mecanismo para mantener una conexión abierta o enviar datos al navegador sin una solicitud HTTP asociada. El siguiente esquema muestra el ciclo de solicitud de un proyecto de Django utilizando Channels con WebSockets:

ciclo petición respuesta de Django channels

Channels reemplaza el ciclo de solicitud/respuesta de Django con mensajes que se envían a través de canales. Las solicitudes HTTP aún se dirigen a funciones de vista utilizando Django, pero se enrutaron a través de canales. Esto permite el manejo de mensajes de WebSockets también, donde tienes productores y consumidores que intercambian mensajes a través de una capa de canal. Channels conserva la arquitectura síncrona de Django, lo que te permite elegir entre escribir código síncrono y asíncrono, o una combinación de ambos.

Instalando Channels y Daphne.


Vamos a añadir Channels a nuestro proyecto y realizar las configuraciones básicas de la aplicación ASGI para que administre las peticiones HTTP. También necesitamos el paquete Daphne que es un servidor ASGI.

Ambos se pueden instalar juntos con el siguiente comando:

python -m pip install -U channels["daphne"]

o también se pueden instalar de forma individual en tu entorno virtual con los siguientes comandos:

pip install channels
pip install dapfne

Edita el archivo settings.py del proyecto y añade "channels" y "daphne" a la lista de programas instalados. Es importante que daphne este el primero en la lista de programas instalados:

INSTALLED_APPS = [
    'daphne'
    # ...
    'channels',
]

Channels espera que definas una única aplicación raíz que será ejecutada para todas las solicitudes. Puedes definir la aplicación raíz agregando el ajuste ASGI_APPLICATION a tu proyecto. Esto es similar al ajuste ROOT_URLCONF que apunta a los patrones de URL base de tu proyecto. Puedes ubicar la aplicación raíz en cualquier lugar de tu proyecto, pero se recomienda colocarla en un archivo a nivel de proyecto. Puedes agregar tu configuración de enrutamiento raíz al archivo asgi.py directamente, donde se definirá la aplicación ASGI.

Edita el archivo asgi.py en el directorio del proyecto educa y agrega el siguiente código resaltado en azul:

Elearning/educa/educa/asgi.py

import os

from django.core.asgi import get_asgi_application
from channels.routing import ProtocolTypeRouter

os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'educa.settings')

django_asgi_app = get_asgi_application()

application = ProtocolTypeRouter({
    'http': django_asgi_app,
})
En el código anterior, defines la aplicación ASGI principal que se ejecutará al servir el proyecto Django a través de ASGI. Utilizas la clase ProtocolTypeRouter proporcionada por Channels como el punto de entrada principal de tu sistema de enrutamiento. ProtocolTypeRouter toma un diccionario que mapea tipos de comunicación como http o websocket a aplicaciones ASGI. Instancias esta clase con la aplicación predeterminada para el protocolo HTTP. Más tarde, agregarás un protocolo para el WebSocket.

Añade la siguiente línea al archivo de configuración settings.py del proyecto:


ASGI_APPLICATION = 'educa.asgi.application'

La configuración ASGI_APPLICATION se utiliza en Channels para localizar la configuración de enrutamiento raíz. Cuando Channels se agrega a la configuración INSTALLED_APPS, toma el control sobre el comando runserver, reemplazando el servidor de desarrollo estándar de Django. Además de manejar el enrutamiento de URL a las vistas de Django para solicitudes síncronas, el servidor de desarrollo de Channels también gestiona las rutas a consumidores de WebSockets. Inicia el servidor de desarrollo usando el siguiente comando:

Activa el servidor de desarrollo usando el siguiente comando:

python manage.py runserver


Verás algo similar a lo siguiente:

Watching for file changes with StatReloader
Performing system checks...

System check identified no issues (0 silenced).
April 09, 2024 - 22:57:18
Django version 5.0.2, using settings 'educa.settings'
Starting ASGI/Daphne version 4.1.0 development server at http://127.0.0.1:8000/
Quit the server with CONTROL-C.
Verifica que la salida contenga la línea "Starting ASGI/Daphne version 4.1.0 development server". Esta línea confirma que estás utilizando el servidor de desarrollo de Daphne, que es capaz de manejar solicitudes síncronas y asíncronas, en lugar del servidor de desarrollo estándar de Django. Las solicitudes HTTP continúan comportándose de la misma manera que antes, pero se enrutan a través de Channels.

Ahora que Channels está instalado en tu proyecto, puedes construir el servidor de chat para los cursos. Para implementar el servidor de chat para tu proyecto, necesitarás seguir los siguientes pasos:

1. Configurar un consumidor: Los consumidores son piezas individuales de código que pueden manejar WebSockets de una manera muy similar a las vistas HTTP tradicionales. Construirás un consumidor para leer y escribir mensajes en un canal de comunicación.

2. Configurar el enrutamiento: Channels proporciona clases de enrutamiento que te permiten combinar y apilar tus consumidores. Configurarás el enrutamiento de URL para tu consumidor de chat.

3. Implementar un cliente WebSocket: Cuando el estudiante acceda a la sala de chat, te conectarás al WebSocket desde el navegador y enviarás o recibirás mensajes usando JavaScript.

4. Habilitar una capa de canales: Las capas de canales te permiten hablar entre diferentes instancias de una aplicación. Son una parte útil para crear una aplicación distribuida en tiempo real. Configurarás una capa de canales utilizando Redis.

Comencemos escribiendo tu propio consumidor para manejar la conexión a un WebSocket, recibir y enviar mensajes, y desconectar.


Escribiendo un consumidor.


Los consumidores son el equivalente de las vistas de Django para aplicaciones asíncronas. Como se mencionó, manejan WebSockets de una manera muy similar a cómo las vistas tradicionales manejan las solicitudes HTTP. Los consumidores son aplicaciones ASGI que pueden manejar mensajes, notificaciones y otras cosas. A diferencia de las vistas de Django, los consumidores están diseñados para comunicaciones de larga duración. Las URL se mapean a consumidores a través de clases de enrutamiento que te permiten combinar y apilar consumidores.

Implementemos un consumidor básico que pueda aceptar conexiones WebSocket y devolver cada mensaje que reciba del WebSocket de vuelta a él. Esta funcionalidad inicial permitirá al estudiante enviar mensajes al consumidor y recibir de vuelta los mensajes que envíe.

Crea un nuevo archivo dentro del directorio de la aplicación de chat y nómbralo consumers.py. Agrega el siguiente código a él:

Elearning/educa/chat/consumers.py

import json

from channels.generic.websocket import WebsocketConsumer

class ChatConsumer(WebsocketConsumer):
    def connect(self):
    # accept connection
        self.accept()

    def disconnect(self, close_code):
        pass
    
    # receive message from WebSocket
    def receive(self, text_data):
        text_data_json = json.loads(text_data)
        message = text_data_json['message']
        # send message to WebSocket
        self.send(text_data=json.dumps({'message': message}))

Este es el consumidor ChatConsumer. Esta clase hereda de la clase WebsocketConsumer de Channels para implementar un consumidor básico de WebSocket. En este consumidor, implementas los siguientes métodos:

- connect(): Se llama cuando se recibe una nueva conexión. Aceptas cualquier conexión con self.accept(). También puedes rechazar una conexión llamando a self.close().

- disconnect(): Se llama cuando el socket se cierra. Usas pass porque no necesitas implementar ninguna acción cuando un cliente cierra la conexión.

- receive(): Se llama cada vez que se recibe datos. Esperas que se reciba texto como text_data (también podría ser binary_data para datos binarios). Tratas los datos de texto recibidos como JSON. Por lo tanto, usas json.loads() para cargar los datos JSON recibidos en un diccionario de Python. Accedes a la clave del mensaje, que esperas que esté presente en la estructura JSON recibida. Para devolver el mensaje, envías el mensaje de vuelta al WebSocket con self.send(), transformándolo nuevamente en formato JSON a través de json.dumps().

La versión inicial de tu consumidor ChatConsumer acepta cualquier conexión WebSocket y devuelves al cliente WebSocket cada mensaje que recibe. Ten en cuenta que el consumidor aún no transmite mensajes a otros clientes. Construirás esta funcionalidad implementando una capa de canales más tarde.

Enrutamiento

Necesitas definir una URL para enrutar las conexiones al consumidor ChatConsumer que has implementado.

Channels proporciona clases de enrutamiento que te permiten combinar y apilar consumidores para despachar según la naturaleza de la conexión. Puedes pensar en ellas como el sistema de enrutamiento de URL de Django para aplicaciones asíncronas.

Crea un nuevo archivo dentro del directorio de la aplicación de chat y nómbralo routing.py. Agrega el siguiente código:

Elearning/educa/chat/routing.py

from django.urls import re_path
from . import consumers

websocket_urlpatterns = [
    re_path(r'ws/chat/room/(?P<course_id>\d+)/$',
            consumers.ChatConsumer.as_asgi()),
]
En este código, mapeas un patrón de URL con la clase ChatConsumer que definiste en el archivo chat/consumers.py. Utilizas re_path de Django para definir la ruta con expresiones regulares. Utilizas la función re_path en lugar de la función path común debido a las limitaciones del enrutamiento de URL de Channels. La URL incluye un parámetro entero llamado course_id. Este parámetro estará disponible en el ámbito del consumidor y te permitirá identificar la sala de chat del curso a la que se está conectando el usuario. Llamas al método as_asgi() de la clase del consumidor para obtener una aplicación ASGI que instanciará una instancia del consumidor para cada conexión de usuario. Este comportamiento es similar al método as_view() de Django para vistas basadas en clases.

Nota: Es una buena práctica añadir el prefijo /ws/ a las URL de WebSocket para diferenciarlas de las URLs utilizadas para las solicitudes HTTP síncronas estándar. Esto también simplifica la configuración de producción cuando un servidor HTTP enruta las solicitudes en función de la ruta.

Edita el archivo asgi.py global ubicado junto al archivo settings.py para que se vea así:

Elearning/educa/educa/asgi.py

"""
ASGI config for educa project.

It exposes the ASGI callable as a module-level variable named ``application``.

For more information on this file, see
https://docs.djangoproject.com/en/5.0/howto/deployment/asgi/
"""

import os

from django.core.asgi import get_asgi_application
from channels.routing import ProtocolTypeRouter, URLRouter
from channels.auth import AuthMiddlewareStack
import chat.routing

os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'educa.settings')

django_asgi_app = get_asgi_application()

application = ProtocolTypeRouter({
    'http': django_asgi_app,
    'websocket': AuthMiddlewareStack(
        URLRouter(chat.routing.websocket_urlpatterns)
    ),
})
En este código, agregas una nueva ruta para el protocolo websocket. Utilizas URLRouter para mapear las conexiones websocket a los patrones de URL definidos en la lista websocket_urlpatterns del archivo routing.py de la aplicación chat. También utilizas AuthMiddlewareStack. La clase AuthMiddlewareStack proporcionada por Channels admite la autenticación estándar de Django, donde los detalles del usuario se almacenan en la sesión. Más tarde, accederás a la instancia de usuario en el ámbito del consumidor para identificar al usuario que envía un mensaje.

Implementación del cliente WebSocket


Hasta ahora, has creado la vista course_chat_room y su plantilla correspondiente para que los estudiantes accedan a la sala de chat del curso. Has implementado un consumidor WebSocket para el servidor de chat y lo has vinculado con el enrutamiento de URL. Ahora, necesitas construir un cliente WebSocket para establecer una conexión con el WebSocket en la plantilla de la sala de chat del curso y poder enviar/recibir mensajes.

Vas a implementar el cliente WebSocket con JavaScript para abrir y mantener una conexión en el navegador. Interactuarás con el Modelo de Objetos del Documento (DOM) utilizando JavaScript.

Edita la plantilla chat/room.html de la aplicación de chat y modifica los bloques include_js y domready, de la siguiente manera:

Elearning/educa/chat/templates/chat/room.html


{% block include_js %}
    {{ course.id|json_script:"course-id" }}
{% endblock %}

{% block domready %}
    const courseId = JSON.parse(
    document.getElementById('course-id').textContent
    );
    const url = 'ws://' + window.location.host +
    '/ws/chat/room/' + courseId + '/';
    const chatSocket = new WebSocket(url);
{% endblock %}
En el bloque include_js, utilizas el filtro de plantilla json_script para utilizar de forma segura el valor del identificador del curso con JavaScript. El filtro de plantilla json_script proporcionado por Django emite un objeto de Python como JSON, envuelto en una etiqueta <script>, para que puedas usarlo de forma segura con JavaScript. El código {{ course.id|json_script:"course-id" }} se renderiza como <script id="course-id" type="application/json">6</script>. Este valor luego se recupera en el bloque domready analizando el contenido del elemento con id="course-id" utilizando JSON.parse(). Esta es la forma segura de usar objetos de Python en JavaScript. 

Puedes encontrar más información sobre el filtro de plantilla json_script en https://docs.djangoproject.com/en/5.0/ref/templates/builtins/#json-script.

En el bloque domready, defines una URL con el protocolo WebSocket, que se ve como ws:// (o wss:// para WebSockets seguros, al igual que https://). Construyes la URL utilizando la ubicación actual del navegador, que obtienes de window.location.host. El resto de la URL se construye con la ruta para el patrón de URL de la sala de chat que definiste en el archivo routing.py de la aplicación de chat.

Escribes la URL en lugar de construirla con un resolutor porque Channels no proporciona una forma de invertir las URL. Utilizas el identificador del curso actual para generar la URL del curso actual y almacenas la URL en una nueva constante llamada url.

Luego abres una conexión WebSocket a la URL almacenada utilizando new WebSocket(url). Asignas el objeto cliente WebSocket instanciado a la nueva constante chatSocket.

Has creado un consumidor WebSocket, has incluido el enrutamiento para él y has implementado un cliente WebSocket básico. Vamos a probar la versión inicial de tu chat. Inicia el servidor de desarrollo usando el siguiente comando:

python manage.py runserver

Abre la URL http://127.0.0.1:8000/chat/room/2/ en tu navegador, sustituyendo 2 por el id de un curso existente en la base de datos. Echa un vistazo a la salida en la consola. Además de las solicitudes GET HTTP para la página y sus archivos estáticos, deberías ver dos líneas que incluyen WebSocket HANDSHAKING y WebSocket CONNECT, como la siguiente salida:


HTTP GET /chat/room/2/ 200 [0.14, 127.0.0.1:54216]
WebSocket HANDSHAKING /ws/chat/room/2/ [127.0.0.1:41184]
HTTP GET /static/debug_toolbar/js/toolbar.js 304 [0.02, 127.0.0.1:54216]
WebSocket CONNECT /ws/chat/room/2/ [127.0.0.1:41184]
HTTP GET /static/debug_toolbar/js/utils.js 304 [0.04, 127.0.0.1:41186]
El servidor de desarrollo de Channels escucha las conexiones de socket entrantes utilizando un socket TCP estándar. El handshake es el puente desde HTTP hacia WebSockets. En el handshake, se negocian los detalles de la conexión y cualquiera de las partes puede cerrar la conexión antes de ser completada. Recuerda que estás utilizando self.accept() para aceptar cualquier conexión en el método connect() de la clase ChatConsumer, implementado en el archivo consumers.py de la aplicación de chat. La conexión es aceptada, y por lo tanto, ves el mensaje WebSocket CONNECT en la consola.

Si utilizas las herramientas de desarrollo del navegador para rastrear las conexiones de red, también puedes ver información sobre la conexión WebSocket que se ha establecido. Vamos a interactuar con él. Implementaremos los métodos para los eventos más comunes, como recibir un mensaje y cerrar la conexión.

Edita la plantilla chat/room.html de la aplicación chat y modifica el bloque domready, de la siguiente forma:

Elearning/educa/chat/templates/chat/room.html

{% block domready %}
    const courseId = JSON.parse(
    document.getElementById('course-id').textContent
    );
    const url = 'ws://' + window.location.host +
    '/ws/chat/room/' + courseId + '/';
    const chatSocket = new WebSocket(url);

    chatSocket.onmessage = function(event) {
        const data = JSON.parse(event.data);
        const chat = document.getElementById('chat');
        chat.innerHTML += '<div class="message">' +
        data.message + '</div>';
        chat.scrollTop = chat.scrollHeight;
        };
        chatSocket.onclose = function(event) {
        console.error('Chat socket closed unexpectedly');
        };
{% endblock %}
En este código, defines los siguientes eventos para el cliente WebSocket:

- onmessage: Se activa cuando se recibe datos a través del WebSocket. Analizas el mensaje, que esperas en formato JSON, y accedes a su atributo de mensaje. Luego, agregas un nuevo elemento `<div>` con el mensaje recibido al elemento HTML con el ID del chat. Esto agregará nuevos mensajes al registro de chat, manteniendo todos los mensajes anteriores que se hayan agregado al registro. Desplazas el `<div>` del registro de chat hacia abajo para asegurarte de que el nuevo mensaje sea visible. Logras esto desplazándote a la altura total desplazable del registro de chat, que se puede obtener accediendo a su atributo scrollHeight.

- onclose: Se activa cuando se cierra la conexión con el WebSocket. No esperas cerrar la conexión, por lo tanto, escribes el error "Chat socket closed unexpectedly" en el registro de la consola si esto sucede.

Has implementado la acción para mostrar el mensaje cuando se recibe un nuevo mensaje. También necesitas implementar la funcionalidad para enviar mensajes al socket.

Edita la plantilla chat/room.html de la aplicación de chat y agrega el siguiente código JavaScript al final del bloque domready:

Elearning/educa/chat/templates/chat/room.html

const input = document.getElementById('chat-message-input');
const submitButton = document.getElementById('chat-message-submit');
submitButton.addEventListener('click', function(event) {
const message = input.value;
if(message) {
// send message in JSON format
chatSocket.send(JSON.stringify({'message': message}));
// clear input
input.innerHTML = '';
input.focus();
}
});
En este código, defines un event listener para el evento de clic del botón de enviar, al cual seleccionas por su ID chat-message-submit. Cuando se hace clic en el botón, realizas las siguientes acciones:

1. Lees el mensaje ingresado por el usuario desde el valor del elemento de entrada de texto con el ID chat-message-input.
2. Verificas si el mensaje tiene algún contenido con if(message).
3. Si el usuario ha ingresado un mensaje, formas un contenido JSON como {'message': 'cadena ingresada por el usuario'} usando JSON.stringify().
4. Envías el contenido JSON a través del WebSocket, llamando al método send() del cliente chatSocket.
5. Borras el contenido del campo de entrada de texto estableciendo su valor como una cadena vacía con input.innerHTML = ''.
6. Devuelves el foco al campo de entrada de texto con input.focus() para que el usuario pueda escribir un nuevo mensaje de inmediato.

Ahora el usuario puede enviar mensajes utilizando el campo de entrada de texto y haciendo clic en el botón de enviar. Para mejorar la experiencia del usuario, darás foco al campo de entrada de texto tan pronto como se cargue la página para que el usuario pueda escribir directamente en él. También capturarás eventos de pulsación de teclas para identificar la tecla Enter y disparar el evento de clic en el botón de enviar. El usuario podrá hacer clic en el botón o presionar la tecla Enter para enviar un mensaje.

Edita la plantilla chat/room.html de la aplicación de chat y agrega el siguiente código JavaScript al final del bloque domready otra vez:

Elearning/educa/chat/templates/chat/room.html

input.addEventListener('keypress', function(event) {
if (event.key === 'Enter') {
// cancel the default action, if needed
event.preventDefault();
// trigger click event on button
submitButton.click();
}
});
input.focus();
En este código, también defines una función para el evento de presionar teclas del elemento de entrada. Para cualquier tecla que el usuario presione, verificas si su tecla es Enter. Previenes el comportamiento predeterminado para esta tecla con event.preventDefault(). Si se presiona la tecla Enter, activas el evento de clic en el botón de envío para enviar el mensaje al WebSocket.

Fuera del controlador de eventos, en el código JavaScript principal para el bloque domready, das foco al elemento de entrada con input.focus(). De esta manera, cuando el DOM se cargue, el foco se establecerá en el elemento de entrada para que el usuario pueda escribir un mensaje.

El bloque domready del archivo de plantilla chat/room.html debería ser algo como esto:

Elearning/educa/chat/templates/chat/room.html

{% block domready %}
const courseId = JSON.parse(
document.getElementById('course-id').textContent
);
const url = 'ws://' + window.location.host +
'/ws/chat/room/' + courseId + '/';
const chatSocket = new WebSocket(url);
chatSocket.onmessage = function(event) {
const data = JSON.parse(event.data);
const chat = document.getElementById('chat');
chat.innerHTML += '<div class="message">' +
data.message + '</div>';
chat.scrollTop = chat.scrollHeight;
};

chatSocket.onclose = function(event) {
console.error('Chat socket closed unexpectedly');
};

const input = document.getElementById('chat-message-input');
const submitButton = document.getElementById('chat-message-submit');
submitButton.addEventListener('click', function(event) {
const message = input.value;
if(message) {
// send message in JSON format
chatSocket.send(JSON.stringify({'message': message}));
// clear input
input.value = '';
input.focus();
}
});

input.addEventListener('keypress', function(event) {
if (event.key === 'Enter') {
// cancel the default action, if needed
event.preventDefault();
// trigger click event on button
submitButton.click();
}
});
input.focus();
{% endblock %}
Abre la URL http://127.0.0.1:8000/chat/room/2/ en tu navegador, reemplazando "2" con el ID de un curso existente en la base de datos. Con un usuario conectado que esté inscrito en el curso, escribe algún texto en el campo de entrada y haz clic en el botón ENVIAR o presiona la tecla Enter.

Si prefieres que cuando se envie el mensaje se borre el texto que hemos introducido dentro del cuadro de texto y se vuelva a situar de nuevo el mensaje en el foco, modifica el código de la siguiente manera:

Elearning/educa/chat/templates/chat/room.html

{% block content %}

<div id="chat">
</div>

<div id="chat-input">
    <input id="chat-message-input" type="text">
    <input id="chat-message-submit" type="submit" value="Send">
</div>

{% endblock %}

{% block include_js %}
    {{ course.id|json_script:"course-id" }}
{% endblock %}

{% block domready %}

const courseId = JSON.parse(
    document.getElementById('course-id').textContent
);

const url = 'ws://' + window.location.host +
    '/ws/chat/room/' + courseId + '/';

const chatSocket = new WebSocket(url);

chatSocket.onmessage = function(event) {
    const data = JSON.parse(event.data);
    const chat = document.getElementById('chat');
    chat.innerHTML += '<div class="message">' +
        data.message + '</div>';
    chat.scrollTop = chat.scrollHeight;
};

chatSocket.onclose = function(event) {
    console.error('Chat socket closed unexpectedly');
};

const input = document.getElementById('chat-message-input');
const submitButton = document.getElementById('chat-message-submit');

submitButton.addEventListener('click', function(event) {
    sendMessage();
});

input.addEventListener('keypress', function(event) {
    if (event.key === 'Enter') {
        event.preventDefault();
        sendMessage();
    }
});

input.focus();

function sendMessage() {
    const message = input.value.trim(); // Trim para eliminar espacios en blanco al inicio 
    // y al final
    if (message) {
        chatSocket.send(JSON.stringify({'message': message}));
        input.value = ''; // Limpiar el campo de entrada
        input.focus(); // Volver a enfocar el campo de entrada
    }
}

{% endblock %}
Verás como tus mensajes aparecen en el chat:


The chat room page, including messages sent through the WebSocket


¡Genial! El mensaje ha sido enviado a través del WebSocket y el consumidor ChatConsumer ha recibido el mensaje y lo ha enviado de vuelta a través del WebSocket. El cliente chatSocket ha recibido un evento de mensaje y la función onmessage ha sido activada, añadiendo el mensaje al registro de chat. Has implementado la funcionalidad con un consumidor WebSocket y un cliente WebSocket para establecer comunicación cliente/servidor y enviar o recibir eventos. Sin embargo, el servidor de chat no es capaz de transmitir mensajes a otros clientes. Si abres una segunda pestaña del navegador e introduces un mensaje, el mensaje no aparecerá en la primera pestaña. Para establecer comunicación entre consumidores, debes habilitar una capa de canal.

Habilitar una capa de canal


Las capas de canal te permiten comunicarte entre diferentes instancias de una aplicación. Una capa de canal es el mecanismo de transporte que permite que múltiples instancias de consumidores se comuniquen entre sí y con otras partes de Django.

En tu servidor de chat, planeas tener múltiples instancias del consumidor ChatConsumer para la misma sala de chat del curso. Cada estudiante que se una a la sala de chat instanciará el cliente WebSocket en su navegador, y eso abrirá una conexión con una instancia del consumidor WebSocket. Necesitas una capa de canal común para distribuir mensajes entre consumidores.

Canales y grupos


Las capas de canal proporcionan dos abstracciones para gestionar las comunicaciones: canales y grupos:

- Canal: Puedes pensar en un canal como un buzón donde se pueden enviar mensajes o como una cola de tareas. Cada canal tiene un nombre. Los mensajes se envían a un canal por cualquiera que conozca el nombre del canal y luego se entregan a los consumidores que están escuchando en ese canal.

- Grupo: Se pueden agrupar múltiples canales en un grupo. Cada grupo tiene un nombre. Un canal puede ser añadido o eliminado de un grupo por cualquiera que conozca el nombre del grupo. Utilizando el nombre del grupo, también puedes enviar un mensaje a todos los canales en el grupo.

Trabajarás con grupos de canales para implementar el servidor de chat. Al crear un grupo de canales para cada sala de chat del curso, las instancias de ChatConsumer podrán comunicarse entre sí.

Configuración de una capa de canal con Redis


Redis es la opción preferida para una capa de canal, aunque Channels tiene soporte para otros tipos de capas de canal. Redis funciona como el almacén de comunicación para la capa de canal. Recuerda que ya hemos utilizado Redis anteriormente.

Para usar Redis como capa de canal, debes instalar el paquete channels-redis. Instala channels-redis en tu entorno virtual con el siguiente comando:

pip install channels-redis

Edita el archivo settings.py del proyecto educa y añade el siguiente código:

Elearning/educa/chat/templates/chat/room.html

CHANNEL_LAYERS = {
    'default': {
        'BACKEND': 'channels_redis.core.RedisChannelLayer',
        'CONFIG': {
            'hosts': [('127.0.0.1', 6379)],
        },
    },
}

El ajuste CHANNEL_LAYERS define la configuración para las capas de canal disponibles en el proyecto. Defines una capa de canal predeterminada utilizando el backend RedisChannelLayer proporcionado por channels-redis y especificas el host 127.0.0.1 y el puerto 6379, en el que se está ejecutando Redis.

Intentemos la capa de canal. Inicializa el contenedor Docker de Redis usando el siguiente comando:

sudo docker run -it --rm --name redis -p 6379:6379 redis

Si deseas ejecutar el comando en segundo plano (en modo desvinculado), puedes utilizar la opción -d.

Abre el shell de Django usando el siguiente comando:

python manage.py shell

Para verificar que la capa de canal puede comunicarse con Redis, escribe el siguiente código para enviar un mensaje a un canal de prueba llamado test_channel y ver si lo recibe:

>>> import channels.layers
>>> from asgiref.sync import async_to_sync
>>> channel_layer = channels.layers.get_channel_layer()
>>> async_to_sync(channel_layer.send)('test_channel', {'message': 'hola'})
>>> async_to_sync(channel_layer.receive)('test_channel')
{'message': 'hola'}
Deberías ver la siguiente salida:

{'message': 'hola'}

En el código anterior, enviamos un mensaje a un canal de prueba a través de la capa de canal, y luego lo recuperamos de la capa de canal. La capa de canal está comunicándose con éxito con Redis.

Actualizando el consumidor para transmitir mensajes


Vamos a editar el consumidor ChatConsumer para utilizar la capa de canal. Utilizarás un grupo de canales para cada sala de chat del curso. Por lo tanto, utilizarás el ID del curso para construir el nombre del grupo. Las instancias de ChatConsumer conocerán el nombre del grupo y podrán comunicarse entre sí.

Edita el archivo consumers.py de la aplicación de chat, importa la función async_to_sync() y modifica el método connect() de la clase ChatConsumer de la siguiente manera:

Elearning/educa/chat/consumers.py

import json

from channels.generic.websocket import WebsocketConsumer
from asgiref.sync import async_to_sync


class ChatConsumer(WebsocketConsumer):
    def connect(self):
        self.id = self.scope['url_route']['kwargs']['course_id']
        self.room_group_name = f'chat_{self.id}'
        # join room group
        async_to_sync(self.channel_layer.group_add)(
            self.room_group_name, self.channel_name)
        # acepta la conexión
        self.accept()
En este código, importas la función auxiliar async_to_sync() para envolver las llamadas a métodos asincrónicos de la capa de canal. ChatConsumer es un consumidor sincrónico WebsocketConsumer, pero necesita llamar a métodos asincrónicos de la capa de canal.

En el nuevo método connect(), realizas las siguientes tareas:

1. Recuperas el ID del curso del ámbito para saber el curso al que está asociada la sala de chat. Accedes a self.scope['url_route']['kwargs']['course_id'] para recuperar el parámetro course_id de la URL. Cada consumidor tiene un ámbito con información sobre su conexión, argumentos pasados por la URL y el usuario autenticado, si lo hay.

2. Construyes el nombre del grupo con el ID del curso al que corresponde el grupo. Recuerda que tendrás un grupo de canales para cada sala de chat del curso. Almacenas el nombre del grupo en el atributo room_group_name del consumidor.

3. Te unes al grupo agregando el canal actual al grupo. Obtienes el nombre del canal del atributo channel_name del consumidor. Utilizas el método group_add de la capa de canal para agregar el canal al grupo. Utilizas el envoltorio async_to_sync() para utilizar el método asincrónico de la capa de canal.

4. Mantienes la llamada self.accept() para aceptar la conexión WebSocket.

Cuando el consumidor ChatConsumer recibe una nueva conexión WebSocket, añade el canal al grupo asociado con el curso en su ámbito. El consumidor ahora puede recibir cualquier mensaje enviado al grupo.

En el mismo archivo consumers.py, modifica el método disconnect() de la clase ChatConsumer de la siguiente manera:

Elearning/educa/chat/consumers.py

class ChatConsumer(WebsocketConsumer):
    #...
    def disconnect(self, close_code):
        # leave room group
        async_to_sync(self.channel_layer.group_discard)(
            self.room_group_name,
            self.channel_name)
    #...
Cuando la conexión se cierra, llamas al método group_discard() de la capa de canal para abandonar el grupo. Utilizas el envoltorio async_to_sync() para utilizar el método asincrónico de la capa de canal.

En el mismo archivo consumers.py, modifica el método receive() de la clase ChatConsumer de la siguiente manera:

Elearning/educa/chat/consumers.py

class ChatConsumer(WebsocketConsumer):
    # ...
    # receive message from WebSocket
    def receive(self, text_data):
        text_data_json = json.loads(text_data)
        message = text_data_json['message']
        # send message to WebSocket
        # self.send(text_data=json.dumps({'message': message}))
        # send message to room group
        async_to_sync(self.channel_layer.group_send)(
            self.room_group_name,
            {
                'type': 'chat_message',
                'message': message,
            }
        )

Cuando recibes un mensaje de la conexión WebSocket, en lugar de enviar el mensaje al canal asociado, lo envías al grupo. Lo haces llamando al método group_send() de la capa de canal. Utilizas el envoltorio async_to_sync() para utilizar el método asincrónico de la capa de canal. Pasas la siguiente información en el evento enviado al grupo:

- type: El tipo de evento. Esta es una clave especial que corresponde al nombre del método que debe ser invocado en los consumidores que reciben el evento. Puedes implementar un método en el consumidor con el mismo nombre que el tipo de mensaje para que se ejecute cada vez que se recibe un mensaje con ese tipo específico.

- message: El mensaje real que estás enviando.

En el mismo archivo consumers.py, añade un nuevo método chat_message() en la clase ChatConsumer de la siguiente manera:

Elearning/educa/chat/consumers.py

class ChatConsumer(WebsocketConsumer):
    # ...
    # receive message from room group
    def chat_message(self, event):
        # send message to WebSocket
        self.send(text_data=json.dumps(event))
Hemos nombrado este método chat_message() para que coincida con la clave type que se envía al grupo de canales cuando se recibe un mensaje del WebSocket. Cuando se envía un mensaje con el tipo chat_message al grupo, todos los consumidores suscritos al grupo recibirán el mensaje y ejecutarán el método chat_message(). En el método chat_message(), envías el mensaje del evento recibido al WebSocket.

El archivo consumers.py completo debería verse así:

Elearning/educa/chat/consumers.py

import json

from channels.generic.websocket import WebsocketConsumer
from asgiref.sync import async_to_sync


class ChatConsumer(WebsocketConsumer):
    def connect(self):
        self.id = self.scope['url_route']['kwargs']['course_id']
        self.room_group_name = f'chat_{self.id}'
        # join room group
        async_to_sync(self.channel_layer.group_add)(
            self.room_group_name, self.channel_name)
        # acepta la conexión
        self.accept()

    def disconnect(self, close_code):
        # leave room group
        async_to_sync(self.channel_layer.group_discard)(
            self.room_group_name,
            self.channel_name)

    # receive message from WebSocket
    def receive(self, text_data):
        text_data_json = json.loads(text_data)
        message = text_data_json['message']
        # send message to WebSocket
        # self.send(text_data=json.dumps({'message': message}))
        # send message to room group
        async_to_sync(self.channel_layer.group_send)(
            self.room_group_name,
            {
                'type': 'chat_message',
                'message': message,
            }
        )
    # receive message from room group
    def chat_message(self, event):
        # send message to WebSocket
        self.send(text_data=json.dumps(event))

Has implementado una capa de canal en ChatConsumer, lo que permite a los consumidores transmitir mensajes y comunicarse entre sí.

Ejecuta el servidor de desarrollo con el siguiente comando:

python manage.py runserver

Abre la URL http://127.0.0.1:8000/chat/room/2/ en tu navegador, sustituyendo 2 por el ID de un curso existente en la base de datos. Escribe un mensaje y envíalo. Luego, abre una segunda ventana del navegador y accede a la misma URL. Envía un mensaje desde cada ventana del navegador.



The chat room page with messages sent from different browser windows


Verás que el primer mensaje solo se muestra en la primera ventana del navegador. Cuando abres una segunda ventana del navegador, los mensajes enviados en cualquiera de las ventanas del navegador se muestran en ambas. Cuando abres una nueva ventana del navegador y accedes a la URL de la sala de chat, se establece una nueva conexión WebSocket entre el cliente WebSocket en JavaScript en el navegador y el consumidor WebSocket en el servidor. Cada canal se añade al grupo asociado con el ID del curso y se pasa a través de la URL al consumidor. Los mensajes se envían al grupo y son recibidos por todos los consumidores.

Añadiendo contexto a los mensajes


Ahora que los mensajes pueden intercambiarse entre todos los usuarios en una sala de chat, probablemente quieras mostrar quién envió cada mensaje y cuándo se envió. Agreguemos algo de contexto a los mensajes.

Edita el archivo consumers.py de la aplicación de chat e implementa los siguientes cambios:

Elearning/educa/chat/consumers.py

import json

from channels.generic.websocket import WebsocketConsumer
from asgiref.sync import async_to_sync
from django.utils import timezone

class ChatConsumer(WebsocketConsumer):
    def connect(self):
        self.user = self.scope['user']
        self.id = self.scope['url_route']['kwargs']['course_id']
        self.room_group_name = f'chat_{self.id}'
        # join room group
        async_to_sync(self.channel_layer.group_add)(
            self.room_group_name, self.channel_name)
        # acepta la conexión
        self.accept()

    def disconnect(self, close_code):
        # leave room group
        async_to_sync(self.channel_layer.group_discard)(
            self.room_group_name,
            self.channel_name)

    # receive message from WebSocket
    def receive(self, text_data):
        text_data_json = json.loads(text_data)
        message = text_data_json['message']
        now = timezone.now()
        # send message to WebSocket
        # self.send(text_data=json.dumps({'message': message}))
        # send message to room group
        async_to_sync(self.channel_layer.group_send)(
            self.room_group_name,
            {
                'type': 'chat_message',
                'message': message,
                'user': self.user.username,
                'datetime': now.isoformat(),
            }
        )
    # receive message from room group
    def chat_message(self, event):
        # send message to WebSocket
        self.send(text_data=json.dumps(event))

Ahora importas el módulo timezone proporcionado por Django. En el método connect() del consumidor, recuperas al usuario actual del ámbito con self.scope['user'] y lo almacenas en un nuevo atributo user del consumidor. Cuando el consumidor recibe un mensaje a través del WebSocket, obtiene la hora actual utilizando timezone.now() y pasa el usuario actual y la fecha y hora en formato ISO 8601 junto con el mensaje en el evento enviado al grupo de canales.

Edita la plantilla chat/room.html de la aplicación de chat y agrega la siguiente línea resaltada en azul al bloque include_js:

Elearning/educa/chat/templates/chat/room.html

{% block include_js %}
    {{ course.id|json_script:"course-id" }}
    {{ request.user.username|json_script:"request-user" }}
{% endblock %}
Usando la plantilla json_script, imprimes de forma segura el nombre de usuario del usuario de la solicitud para usarlo con JavaScript.

En el bloque domready de la plantilla chat/room.html, agrega las siguientes líneas resaltadas en azul:

Elearning/educa/chat/templates/chat/room.html

{% block domready %}

const courseId = JSON.parse(
    document.getElementById('course-id').textContent
);
const requestUser = JSON.parse(
document.getElementById('request-user').textContent
);
#...
{% endblock %}
En el nuevo código, analizas de forma segura los datos del elemento con el ID request-user y los almacenas en la constante requestUser.

Luego, en el bloque domready, encuentra las siguientes líneas:

Elearning/educa/chat/templates/chat/room.html

const data = JSON.parse(e.data);
const chat = document.getElementById('chat');
chat.innerHTML += '<div class="message">' +
data.message + '</div>';
chat.scrollTop = chat.scrollHeight;
Modifica esas líneas con el siguiente código:

Elearning/educa/chat/templates/chat/room.html

const data = JSON.parse(event.data);
const chat = document.getElementById('chat');

const dateOptions = {hour: 'numeric', minute: 'numeric', hour12: true};
const datetime = new Date(data.datetime).toLocaleString('es', dateOptions);
const isMe = data.user === requestUser;
const source = isMe ? 'me' : 'other';
const name = isMe ? 'Me' : data.user;

chat.innerHTML += '<div class="message ' + source + '">' + '<strong>' + name + '</strong> '+ '<span class="date">' +
        datetime + '</span><br>' + data.message + '</div>';
chat.scrollTop = chat.scrollHeight;
};
En este código, implementas los siguientes cambios:

1. Conviertes la fecha y hora recibida en el mensaje a un objeto Date de JavaScript y la formateas con una localización específica.
2. Comparas el nombre de usuario recibido en el mensaje con dos constantes diferentes como ayudantes para identificar al usuario.
3. La constante source obtiene el valor "me" si el usuario que envía el mensaje es el usuario actual, o "other" en caso contrario.
4. La constante name obtiene el valor "Me" si el usuario que envía el mensaje es el usuario actual o el nombre del usuario que envía el mensaje en caso contrario. Lo utilizas para mostrar el nombre del usuario que envía el mensaje.
5. Utilizas el valor source como una clase del elemento principal <div> del mensaje para diferenciar los mensajes enviados por el usuario actual de los mensajes enviados por otros. Se aplican diferentes estilos CSS en función del atributo de clase. Estos estilos CSS se declaran en el archivo estático css/base.css.
6. Utilizas el nombre de usuario y la fecha y hora en el mensaje que se agrega al registro de chat.

Abre la URL http://127.0.0.1:8000/chat/room/2/ en tu navegador, sustituyendo 2 por el ID de un curso existente en la base de datos. Con un usuario conectado que esté inscrito en el curso, escribe un mensaje y envíalo.

Luego, abre una segunda ventana del navegador en modo incógnito para evitar el uso de la misma sesión. Inicia sesión con un usuario diferente, también inscrito en el mismo curso, y envía un mensaje.

Podrás intercambiar mensajes utilizando dos usuarios diferentes y ver el usuario y la hora, con una distinción clara entre los mensajes enviados por el usuario y los mensajes enviados por otros. La conversación entre dos usuarios debería lucir similar a la siguiente:

The chat room page with messages from two different user sessions


Modificando el consumidor para que sea completamente asíncrono.


El ChatConsumer que has implementado hereda de la clase base WebsocketConsumer, la cual es síncrona. Los consumidores síncronos son convenientes para acceder a los modelos de Django y llamar a funciones regulares de entrada/salida sincrónicas. Sin embargo, los consumidores asíncronos tienen un mejor rendimiento, ya que no requieren hilos adicionales al manejar las solicitudes. Dado que estás utilizando las funciones asíncronas del canal de capa, puedes reescribir fácilmente la clase ChatConsumer para que sea asíncrona.

Edita el archivo consumers.py de la aplicación de chat e implementa los siguientes cambios:"

Elearning/educa/chat/consumers.py

import json
from channels.generic.websocket import AsyncWebsocketConsumer
#from channels.generic.websocket import WebsocketConsumer
from asgiref.sync import async_to_sync
from django.utils import timezone

class ChatConsumer(AsyncWebsocketConsumer):
    async def connect(self):
        self.user = self.scope['user']
        self.id = self.scope['url_route']['kwargs']['course_id']
        self.room_group_name = f'chat_{self.id}'
        # join room group
        await self.channel_layer.group_add(
            self.room_group_name, 
            self.channel_name)
        # acepta la conexión
        await self.accept()

    async def disconnect(self, close_code):
        # leave room group
        await self.channel_layer.group_discard(
            self.room_group_name,
            self.channel_name)

    # receive message from WebSocket
    async def receive(self, text_data):
        text_data_json = json.loads(text_data)
        message = text_data_json['message']
        now = timezone.now()
        # send message to WebSocket
        # self.send(text_data=json.dumps({'message': message}))
        # send message to room group
        await self.channel_layer.group_send(
            self.room_group_name,
            {
                'type': 'chat_message',
                'message': message,
                'user': self.user.username,
                'datetime': now.isoformat(),
            }
        )
    # receive message from room group
    async def chat_message(self, event):
        # send message to WebSocket
        await self.send(text_data=json.dumps(event))

Integrando la aplicación del chat con las vistas existentes.


EL servidor del chart ya esta totalmente implementado, y los estudiantes pueden matricularse en los cursos y comunicarse con otros compañeros. Añadamos un enlace para que los estudiantes puedan unirse al chat de cada curso.

Edita la plantilla de la aplicación estudiantes "students/course/detail.htnl" y añade la siguiente etiqueta <h3> al final de la etiqueta <div class="contents">:

  <div class="contents">
    ...
    <h3>
        <a href="{% url 'chat:course_chat_room' object.id %}">
        Course chat room
        </a>
    </h3>
</div>

Abre el navegador y accede a cualquier curso en el que este inscrito el estudiante para ver el contenido del curso. La barra lateral ahora contendrá un enlace a la sala de chat del curso que apunta a la vista de la sala chat del curso. Si haces clic en el entrarás a la sala del chat. 

The course detail page, including a link to the course chat room



Puedes encontrar el código de este post en el siguiente enlace de Github.

No hay comentarios:

Publicar un comentario