Chat vía web con WebSockets y Twisted



Chat con WebSockets

WebSocket es una tecnología que permite realizar conexiones y transmitir información cliente / servidor de forma similar a un socket convencional, pero que es soportada por los principales navegadores web. Se trata de una herramienta relativamente reciente, por ende no está disponible en versiones antiguas de dichos programas. Cuenta con una API estandarizada por el World Wide Web Consortium (W3C), a la cual se accede vía JavaScript. Los WebSockets resultan una herramienta de mucho potencial, que abre un mundo nuevo de posibilidades para las aplicaciones web.

Aquí en Recursos Python somos fanáticos de Twisted. Por ende, mientras que la parte del cliente es manejada por el navegador vía JavaScript, estaremos desarrollando el servidor utilizando dicho framework. Ya que Twisted no soporta de forma nativa el protocolo de WebSocket, utilizamos una pequeña librería para añadir esta característica.

Para introducir al lector a los WebSockets de una forma agradable, a lo largo del artículo estaremos desarrollando, paso a paso, un simple chat web. Preferentemente se requieren conocimientos básicos de HTML, JavaScript y Twisted.

Código completo y descarga al final del artículo.

Cliente

Comenzaremos por crear el archivo chat.js, que tendrá todo el código necesario para comunicarse con el servidor y desplegar los mensajes en el navegador. Implementamos dos variables globales; una representa el WebSocket y otra almacena el nombre del usuario en la conversación.

var ws;
var username;

Dejamos por un momento el código JavaScript para crear el archivo HTML principal, wsclient.html.

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
	"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="es" lang="es">
  <head>
    <title>Chat</title>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
    <script src="chat.js"></script>
  </head>
  <body>
  </body>
</html>

En el código anterior simplemente creamos una estructura básica HTML, establecemos un título (línea número 5) e importamos el archivo chat.js (7).

Ahora introducimos la primera parte del chat, el momento de elegir un nombre para la conversación e ingresar (dentro de las etiquetas body).

    <div id="login">
      <label>Nombre: </label> <input autocomplete="off" type="text" id="username" value="Usuario" />
      <button onclick="loadChat();">Ingresar</button>
    </div>

El botón de ingresar llamará a la función loadChat, que será la encargada de guardar el nombre de usuario y establecer la conexión. La definimos de la siguiente forma.

// chat.js
function loadChat()
{
    username = document.getElementById("username").value;
    if (!username)
        return;

    document.getElementById("login").hidden = true;

Hasta aquí obtenemos el nombre introducido por el usuario, chequeamos que no esté vacío, y ocultamos el recuadro. Retomamos esta función más adelante.

Regresamos a nuestro archivo HTML y, debajo de lo último que hemos añadido, colocamos el siguiente recuadro.

    <div id="chat" hidden>
      <div id="messages" style="width: 600px; height: 300px; overflow-y: scroll; background-color: #D4FCFF; border: 1px solid #CACACA; padding: 10px;">
        Aún no te has conectado al chat.
      </div>
      <input type="text" style="width: 530px; height: 30px; padding-left: 5px; margin: 0px;" id="message"
             onkeyup="onKeyUp(event);" />
      <button style="width:81px; height: 36px; padding: 0px; margin: 0px;" onclick="sendMessage();">Enviar</button>
      <br /><br />
      <button onclick="closeChat();">Salir</button>
    </div>

Hasta aquí hemos introducido un recuadro en donde aparecerán los mensajes de la conversación (id="messages"), una entrada de texto para escribir los mensajes (id="message"), un botón para enviarlos (línea número 7) y otro para salir (9). Inicialmente se encuentra oculto, por lo que retomamos la función loadChat que dejamos a medias e introducimos lo siguiente.

    document.getElementById("chat").hidden = false;

    messages = document.getElementById("messages");
    messages.innerHTML = "";  // Remover el contenido anterior.

De esta forma, una vez ingresado el nombre de usuario se mostrará el recuadro de la conversación.

Llega el momento de crear el WebSocket y establecer la conexión (debajo del código anterior). Nuestro servidor Twisted estará escuchando en el puerto número 9998.

    ws = new WebSocket("ws://localhost:9998");

Y creamos tres funciones para manejar los distintos eventos del WebSocket.

    // Al crearse la conexión.
    ws.onopen = function()
    {
        ws.send(username + " ha ingresado al chat.");
    };
    
    // Al cerrarse la conexión.
    ws.onclose = function()
    {
        chat.innerHTML = "Se ha perdido la conexión."
    };
    
    // Al recibir un mensaje.
    ws.onmessage = function(evt) 
    {
        // Añadir el mensaje al recuadro.
        messages.innerHTML += evt.data + "<br />";
        // Desplegar la barra vertical hacia abajo.
        messages.scrollTop = messages.scrollHeight;
    };
}

La función completa:

function loadChat()
{
    username = document.getElementById("username").value;
    if (!username)
        return;

    document.getElementById("login").hidden = true;
    document.getElementById("chat").hidden = false;

    messages = document.getElementById("messages");
    messages.innerHTML = "";

    ws = new WebSocket("ws://localhost:9998");

    ws.onopen = function()
    {
        ws.send(username + " ha ingresado al chat.");
    };

    ws.onclose = function()
    {
        chat.innerHTML = "Se ha perdido la conexión."
    };

    ws.onmessage = function(evt) 
    { 
        messages.innerHTML += evt.data + "<br />";
        messages.scrollTop = messages.scrollHeight;
    };
}

Continuamos implementando la función sendMessage, encargada de enviar un mensaje al servidor a través del WebSocket.

function sendMessage()
{
    // Obtener el mensaje ingresado por el usuario.
    message = document.getElementById("message");
    if (message.value)
    {
        // Enviarlo al servidor.
        ws.send("<strong>" + username + "</strong>: " + message.value);
        // Borrar el contenido de la caja de texto.
        message.value = "";
    }
    // Enviar el foco nuevamente a la caja de texto.
    message.focus();
}

Y para permitir al usuario enviar mensajes presionando la tecla Enter.

function onKeyUp(event)
{
    if (event.keyCode == 13)
        sendMessage();
}

Por último, la opción para salir de la conversación.

function closeChat()
{
    ws.send(username + " se ha desconectado.");
    ws.close();
}

Ahora bien, como comentamos anteriormente, al tratarse de una tecnología relativamente nueva debemos chequear que el navegador soporte WebSockets.

function checkSupport()
{
    if (!("WebSocket" in window))
    {
        document.getElementById("login").innerHTML = "Este navegador no soporta WebSockets.";
    }
}

Esta función debe ser llamada una vez cargada la página, por lo que la colocamos justo antes de cerrar la etiqueta body.

    <script>checkSupport();</script>
  </body>

Con esto finalizamos la parte del cliente.

Servidor

El código para el servidor es bastante pequeño. Utilizamos Twisted y el módulo txWS que le añade soporte para WebSockets.

Creamos el archivo wsserver.py y comenzamos por importar todo lo necesario.

from twisted.internet import protocol, reactor, endpoints
from txws import WebSocketFactory

Luego, creamos el protocolo.

class ClientProtocol(protocol.Protocol):
    
    def __init__(self, factory):
        self.factory = factory
    
    def dataReceived(self, data):
        """Enviar el mensaje a todos los clientes."""
        self.factory.sendMessage(data)

    def connectionLost(self, reason):
        self.factory.clients.remove(self)
    
    def connectionMade(self):
        self.factory.clients.add(self)

Y a continuación:

class ClientFactory(protocol.Factory):
    
    def __init__(self):
        # Lista de los clientes conectados.
        self.clients = set()
    
    def buildProtocol(self, addr):
        return ClientProtocol(self)
    
    def sendMessage(self, message):
        """Enviar mensaje a todos los clientes."""
        for client in self.clients:
            client.transport.write(message)

Por último, iniciamos un servidor TCP en el puerto 9998, pero envolviendo nuestra clase ClientFactory dentro de WebSocketFactory para soportar el protocolo WebSocket.

endpoints.serverFromString(reactor, "tcp:9998").listen(
    WebSocketFactory(ClientFactory()))
reactor.run()

Código completo

Descarga: websocketchat.zip.

wsclient.html

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
	"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="es" lang="es">
  <head>
    <title>Chat</title>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
    <script src="chat.js"></script>
  </head>
  <body>
    <div id="login">
      <label>Nombre: </label> <input autocomplete="off" type="text" id="username" value="Usuario" />
      <button onclick="loadChat();">Ingresar</button>
    </div>
    <div id="chat" hidden>
      <div id="messages" style="width: 600px; height: 300px; overflow-y: scroll; background-color: #D4FCFF; border: 1px solid #CACACA; padding: 10px;">
        Aún no te has conectado al chat.
      </div>
      <input type="text" style="width: 530px; height: 30px; padding-left: 5px; margin: 0px;" id="message"
             onkeyup="onKeyUp(event);" />
      <button style="width:81px; height: 36px; padding: 0px; margin: 0px;" onclick="sendMessage();">Enviar</button>
      <br /><br />
      <button onclick="closeChat();">Salir</button>
    </div>
    <script>checkSupport();</script>
  </body>
</html>

chat.js

var ws;
var username;

function onKeyUp(event)
{
    if (event.keyCode == 13)
        sendMessage();
}

function sendMessage()
{
    message = document.getElementById("message");
    if (message.value)
    {
        ws.send("<strong>" + username + "</strong>: " + message.value);
        message.value = "";
    }
    message.focus();
}

function checkSupport()
{
    if (!("WebSocket" in window))
    {
        document.getElementById("login").innerHTML = "Este navegador no soporta WebSockets.";
    }
}

function loadChat()
{
    username = document.getElementById("username").value;
    if (!username)
        return;

    document.getElementById("login").hidden = true;
    document.getElementById("chat").hidden = false;

    messages = document.getElementById("messages");
    messages.innerHTML = "";

    ws = new WebSocket("ws://localhost:9998");

    ws.onopen = function()
    {
        ws.send(username + " ha ingresado al chat.");
    };

    ws.onclose = function()
    {
        chat.innerHTML = "Se ha perdido la conexión."
    };

    ws.onmessage = function(evt) 
    { 
        messages.innerHTML += evt.data + "<br />";
        messages.scrollTop = messages.scrollHeight;
    };
}

function closeChat()
{
    ws.send(username + " se ha desconectado.");
    ws.close();
}

wsserver.py

#!/usr/bin/env python
# -*- coding: utf-8 -*-

from twisted.internet import protocol, reactor, endpoints
from txws import WebSocketFactory


class ClientProtocol(protocol.Protocol):
    
    def __init__(self, factory):
        self.factory = factory
    
    def dataReceived(self, data):
        self.factory.sendMessage(data)

    def connectionLost(self, reason):
        self.factory.clients.remove(self)
    
    def connectionMade(self):
        self.factory.clients.add(self)


class ClientFactory(protocol.Factory):
    
    def __init__(self):
        self.clients = set()
    
    def buildProtocol(self, addr):
        return ClientProtocol(self)
    
    def sendMessage(self, message):
        for client in self.clients:
            client.transport.write(message)


endpoints.serverFromString(reactor, "tcp:9998").listen(
    WebSocketFactory(ClientFactory()))
reactor.run()



6 comentarios.

  1. Yo de nuevo: hay alguna forma de imprimir los nombres de los usuarios (clientes) desde el servidor (wsserver.py) para evitar que estos se repitan.

    Gracias.

    • Recursos Python says:

      Hola. Sí, es posible, el problema con el ejemplo de este artículo es que el servidor no conoce más que los mensajes que envían los clientes (el usuario forma parte del mismo mensaje). Para evitar la repetición de nombres, el servidor debería tener una lista con cada uno de ellos. Para eso, el cliente tendría que enviar su nombre al servidor inmediatamente luego de establecer la conexión; el servidor chequearía la lista y tomaría la acción correspondiente.

      Si tienes problemas para implementarlo te invito a que pases por el foro y lo veamos con mayor detalle.

      Saludos.

  2. Andres Niño says:

    Excelente post, pero tengo una inquietud; que requisitos se deben cumplir para que el chat funcione cunado los usuarios estblecen la conexion desde diferentes redes, o mejor conocida como conexión fuera de LAN.

    Gracias

    • Recursos Python says:

      Hola Andres, me alegro que te haya servido. No se requiere nada adicional, simplemente que el puerto en el que escucha el servidor Twisted (9998 en el ejemplo) esté abierto para conexiones TCP. Una vez hecho esto, tendrás que reemplazar “localhost” por tu IP pública en la URI del WebSocket (ws://localhost:9998).

      Un saludo.

Deja un comentario