Revista Tecnología

¿Cómo crear un chat utilizando WebSockets en C++? Y no morir en el intento

Publicado el 21 noviembre 2016 por Gaspar Fernández Moreno @gaspar_fm

¿Cómo crear chat utilizando WebSockets C++? morir intento¿Cómo crear un chat utilizando WebSockets en C++? Y no morir en el intento
Los WebSockets proporcionan un canal bidireccional entre el servidor y el navegador y nos permiten crear aplicaciones aún más dinámicas y rápidas. Hace unas semanas vimos cómo funcionan los WebSockets por dentro. En este post vamos a ver una implementación de los mismos en C++, en realidad la parte de navegador como habréis imaginado será Javascript, HTML y CSS, como siempre; será la parte de servidor la que programemos en C++.

Para no hacer este desarrollo desde cero, he utilizado Glove, una biblioteca con la que podemos crear rápidamente un servidor web en C++ con muchas posibilidades. Aunque para analizar un poco las tripas de los WebSockets, vamos a analizar parte del código utilizado dentro de Glove, vamos esto no tendremos que hacerlo para nuestra aplicación, pero nos viene bien para entender cómo funciona. En principio, tenemos tres clases:

  1. WebSocketHandler: encargada de enviar y recibir frames al cliente.
  2. WebSocketFrame: encargada de leer un stream de datos proveniente de un frame y obtener la información del mismo (usado cuando recibamos información). Así como crear el stream de datos de un frame a partir de la información que vamos a enviar (usado para enviar).
  3. WebSocketData: La información por WebSockets puede estar fragmentada, por lo que un dato serán muchos frames.

Para ello, el código utilizado es el siguiente:

WebSocketFrame

Podremos generar un WebSocketFrame desde un string con los datos en bruto o a partir de la información necesaria como el opcode, datos, fin (si es el último frame de una serie y si está enmascarado). En definitiva. Podemos utilizar los constructores:

  • GloveWebSocketFrame(std::string& data);
  • GloveWebSocketFrame(unsigned char opcode, std::string& data, bool fin=true, bool masked=true);

Y tras ello, podemos consultar información de dicho frame:

  • std::string& data() : Para obtener la información de dicho frame (texto o binaria)
  • bool fin() : Para saber si es un último frame de serie
  • bool masked() : Para saber si viene enmascarado.
  • bool error() : ¿Ha habido algún fallo leyendo o generando el frame?
  • bool iscontrol() : ¿Es un frame de control?
  • bool isdata() : ¿Es un frame de datos? Será de datos si no es de control, pero tenemos otra función para hacer el sistema más humano
  • std::string raw() : Devuelve los datos que serán enviados o que han sido recibidos por red
  • unsigned short opcode() const : Devuelve el opcode de dicho frame.

Asimismo los opcodes pueden ser:

  • TYPE_CONT: El frame es continuación de una secuencia
  • TYPE_TEXT: Es un frame de texto
  • TYPE_BIN: Es un frame binario
  • TYPE_CLOSE: Petición o respuesta de cierre de sesión
  • TYPE_PING: Ping, para verificar respuesta de la otra parte.
  • TYPE_PONG: Respuesta de Ping
  • TYPE_ERROR: Este tipo no está en la especificación, pero lo usamos en la biblioteca para indicar que ha habido un problema con el frame.

Más o menos esto nos servirá para hacernos una idea de cómo funciona esto. Aunque vamos a ver qué métodos tenemos para que nuestro programa utilice WebSockets a modo de servidor.

WebSocketData

Una clase mucho más pequeña, en la que juntamos los datos de varios Frames. El objetivo es que los Frames se vayan destruyendo a medida que se reciban y sus datos se añadan a una instancia de esta clase. Con el futuro se podrá optimizar en memoria ya que los tamaños que pueden alcanzar los frames pueden ser desorbitados (vamos, el sistema está preparado para los años venideros). Para utilizar esta clase tenemos los métodos:

  • GloveWebSocketData(): Constructor que inicializa atributos de la clase.
  • void update(GloveWebSocketFrame& frame): Añade un nuevo frame
  • unsigned char type(): Indica el tipo de datos que se han recibido (coinciden con el opcode del primer frame).
  • std::string& data(): Devuelve los datos que se han recibido.
  • std::string::size_type length(): Tamaño de los datos.
  • bool empty(): ¿Hay datos ahora mismo?

WebSocketHandler

Será la clase con la que interactuaremos directamente en las funciones de envío, recepción y control (inicio/fin de sesión, mantenimiento de conexión, etc) de datos de tipo WebSockets; además, en el ejemplo del chat veremos cómo cada uno de los usuarios conectados tendrá un WebSocketHandler asociado, con el que podremos enviarle mensajes en cualquier momento. Dicha clase tendrá los siguientes métodos:

  • GloveWebSocketHandler(Glove::Client& client, uint64_t fragmentation): Inicializa el objeto. fragmentation indicará un recorte automático de los mensajes estableciendo un tamaño máximo de los frames que envía el sistema (podemos optimizar el sistema tocando este valor). Este objeto lo suele crear automáticamente GloveHttpServer, así que a nosotros se nos entregará siempre una referencia de un objeto de esta clase.
  • void ping(std::string data ="", std::function callback=nullptr): Envía un ping al cliente que tengamos conectado. En el ping podemos enviar también algo de información que teóricamente tiene que venir devuelta (así que tampoco enviemos el Quijote). Además, desde esta función podemos establecer un callback que se llamará cuando recibamos el pong.
  • bool pong(GloveWebSocketFrame& frame): Envía un pong al cliente. Se suele llamar automáticamente por Glove.
  • bool pong(): Recibe un pong del cliente. Cuando se recibe un pong() se puede llamar adicionalmente a un callback que establecemos cuando enviamos el ping.
  • void close(GloveWebSocketFrame& frame): Responde una petición de cierre de sesión
  • void close(uint16_t closeCode, std::string closeMessage): Envía una petición de cierre de sesión. con el código y mensaje que indiquen el por qué.
  • unsigned clientId(): Devuelve el Id del cliente.
  • unsigned char type(): Devuelve el tipo por defecto de frame que se envía (pueden ser frames de tipo texto o binarios, no tendría mucho sentido enviar por defecto pings o close, o incluso pong, porque muchos clientes rechazan la conexión cuando reciben un pong sin venir a cuento.
  • double latency(): Tiempo desde que se envió un ping hasta que se recibió el pong.
  • unsigned char type (unsigned char type): Establece el tipo de frame por defecto.
  • uint64_t fragmentation(): Devuelve el número de bytes máximo de cada frame, es decir fragmentaremos los mensajes en frames de este tamaño.
  • uint64_t fragmentation(uint64_t val): Establecemos el valor de la fragmentación.
  • int send(std::string data, unsigned char type=0): Enviamos datos. Si type es 0, se enviarán del tipo por defecto.

Ya estamos listos para jugar con los WebSockets utilizando Glove en C++.

Un ejemplo práctico: echo

¿Cómo crear chat utilizando WebSockets C++? morir intento
¿Cómo crear un chat utilizando WebSockets en C++? Y no morir en el intento

Pero vamos al lío, a crear algo que podamos utilizar después. Los ejemplos los podemos encontrar en GitHub, y tal vez allí se vean actualizados. Lo primero que vamos a ver será el código C++ de nuestro servidor. Éste se encargará de aceptar las peticiones http:// y ws:// y procesarlas. Primero implementaremos un servidor echo, es decir, todo lo que le mandemos, vamos a reenviarlo. Es algo sencillo, pero nos vale como prueba de concepto (menos de 50 líneas):

Como vemos, lo que hacemos es crear el servidor con el constructor de GloveHttpServer, ya que en sí es un servidor HTTP en el puerto 8080 y más tarde añadimos rutas:

  • /echo/ : Será la ruta a la que debemos conectar nuestro WebSocket. Desde aquí se inicia el handshake y se establece la comunicación. Si conectamos desde http a esta dirección se enviará una web estática.
  • /websocket_test.js : Es el archivo Javascript que se envía el cliente. Ya que tenemos un servidor iniciado, éste se encargará de este tipo de cosas también.
  • /websocket_test.css : Es el archivo CSS asociado y que también se envía al cliente.
  • / : Es el directorio principal. Se envía un HTML con un entorno para que el cliente envíe mensajes.

Y, como vemos se utilizarán los métodos addRoute (para rutas HTTP) y addWebSocket (para rutas WS) de la siguiente forma:

donde,

  • ruta : Indica la dirección del recurso que escuchamos. Es decir, cuando accedamos a dicha ruta, llamaremos a una función que la procese.
  • callback : Será la función que se llamará para generar la salida deseada (enviar una web, añadir un dato, o lo que sea. Los callbacks tienen la forma:
    void (GloveHttpRequest& request, GloveHttpResponse& response)
    donde GloveHttpRequest nos proporciona información sobre la petición y en GloveHttpResponse enviaremos la información sobre la respuesta.

En realidad addRoute puede admitir más argumentos como el virtualhost al que estamos accediendo, número máximo y mínimo de argumentos (por si en la ruta hay variables o los métodos HTTP admitidos. Poco a poco voy documentando.

En este caso, tenemos más callbacks que coincidirán con los diferentes eventos que se pueden producir:

  • callback : Es la función que se llamará cuando se acceda por HTTP. Con la misma forma que el callback de addRoute.
  • acceptCallback : Será la función llamada cuando un cliente acabe de establecer una conexión, algo así como un login en el sistema. Tendrá la forma void (GloveHttpRequest& request, GloveWebSocketHandler& ws). Si este callback es nullptr, no haremos nada cuando entre un cliente,
  • receiveCallback : Será la función llamada cuando se recibe un mensaje desde el cliente. Tendrá la forma void (GloveWebSocketData& data, GloveWebSocketHandler& ws). Si este callback es nullptr no haremos nada cuando se reciba un mensaje.
  • maintenanceCallback : Se llamará periódicamente para realizar tareas de mantenimiento sobre un cliente en cuestión. Tendrá la forma void (GloveWebSocketHandler& ws). Tenemos que tener cuidado y no alargar mucho esta función porque si tenemos muchos clientes, puede llegar a tener muchas llamadas simultáneas. Si este callback es nullptr no haremos nada periódicamente.
  • closeCallback : Se llamará cuando un cliente pida cerrar la conexión. Tendrá la misma forma que un callback de mantenimiento. Si este callback es nullptr no haremos nada cuando un cliente cierre la conexión, Glove se encarga de cerrar bien la conexión, pero nada más.

Si miramos la función en el archivo hpp podremos ver que acepta más argumentos, tal y como hace addRoute(), pero con esto, tenemos para montar nuestro servidor de echo con WebSockets. Aunque paso a escribir los archivos CSS, JS y HTML necesarios para ejecutar este ejemplo.

websockets.html:

websocket_test.js:

websocket_test.css:

Aquí tenemos todo lo necesario para ejecutar el ejemplo y jugar con él. Para compilar el ejemplo podremos hacerlo con


Si queremos soporte SSL (tendremos que instalar libssl-dev o un paquete similar en nuestra distribución). En el siguiente ejemplo cuento cómo activar SSL y por tanto deberemos acceder con wss:// (con esto, estaremos utilizando un protocolo seguro). También necesitaremos zlib si queremos habilitar la compresión. O:


Si no queremos dicho soporte. O incluso podemos indicar -DENABLE_COMPRESSION=0 si no queremos habilitar compresión de la salida, quitando a la vez -lz.

Otro ejemplo: nuestro propio chat en C++

Pero la gracia de esto está en que podemos montar un servidor de chat con WebSockets en unas 200 líneas de C++ con cierto control de usuarios online.

Primero crearemos una clase SimpleChat que sea capaz de manejar los eventos que puedan ocurrir como la entrada y salida de usuarios, establecimiento de su nickname o alias y el envío y recepción de mensajes. Esta clase tendrá los siguientes métodos:

  • void login(GloveHttpRequest& request, GloveWebSocketHandler& ws) : llamada cuando entra un usuario nuevo.
  • void message(GloveWebSocketData& data, GloveWebSocketHandler& ws) : llamada cuando se recibe un mensaje. Puede haber mensajes de comando, precedidos de /, por ahora sólo vale el comando /name que cambia el nombre del usuario.
  • void setName(GloveWebSocketHandler& ws, std::string name) : comprueba que el nombre del usuario no esté siendo utilizado y asocia un nombre con un identificador interno de usuario (ws.clientId()). Con esto, siempre que un usuario envíe un mensaje, el mensaje aparecerá junto con el nombre que el usuario haya elegido.
  • bool nameIsUsed(std::string& name) : Verifica que el nombre especificado está siendo usado.
  • std::string userName(GloveWebSocketHandler& ws) : Devuelve el nombre de usuario asociado con un Id de cliente.
  • bool quit(GloveWebSocketHandler& ws) : Un usuario ha abandonado el chat. Normalmente los navegadores cierran bien la conexión cuando el cliente se marcha. Si no, cuando ocurre un timeout en el socket también se cierra automáticamente la conexión.

Paso a poner el código fuente:

Como curiosidad, indicar que la función getChatJs() que devuelve el archivo Javascript de nuestro chat es dinámica. Es decir, aunque tenemos un archivo wschat.js que contiene la información. Dentro de éste hay una palabra clave %CHATURL% que indica la dirección donde el chat está instalado y será dinámica dependiendo de si utilizamos ws:// o wss:// ; esto nos puede dar mucho juego, aunque será más difícil proporcionar caché a nuestros usuarios.

Aquí tenemos el resto de archivos.

wschat.html:

wschat.js (cuidado con %CHATURL% que es nuestra palabra clave):

wschat.css:

Para compilar:

Proxy con Apache

Podéis poner este programa detrás de Apache, por ejemplo utilizando el módulo proxy_wstunnel y utilizando:

o

en la configuración del VirtualHost.

¿Cómo usas los WebSockets?

Y tú, ¿cómo usas los WebSockets en tus aplicaciones?
Foto original: Marcus Dall Col


Volver a la Portada de Logo Paperblog