Cómo usar WebSockets en Node.js para crear aplicaciones en tiempo real

Este tutorial demuestra cómo utilizar WebSockets en Node.js para una comunicación bidireccional e interactiva entre un navegador y un servidor. Esta técnica es esencial para aplicaciones en tiempo real rápidas, como paneles de control, aplicaciones de chat y juegos multijugador.

Table of Contents

La Web se basa en mensajes HTTP de solicitud-respuesta. Su navegador realiza una solicitud de URL y un servidor responde con datos. Esto puede llevar a más solicitudes del navegador y respuestas del servidor para imágenes, CSS, JavaScript, etc., pero el servidor no puede enviar datos arbitrariamente a un navegador.

Las técnicas de sondeo de larga duración de Ajax pueden hacer que las aplicaciones web parezcan actualizarse en tiempo real, pero el proceso es demasiado limitante para aplicaciones de tiempo real verdaderas. Sondear cada segundo sería ineficiente en ciertos momentos y demasiado lento en otros.

Tras una conexión inicial desde un navegador, eventos enviados por el servidor son una respuesta HTTP estándar (transmitida) que puede enviar mensajes desde el servidor en cualquier momento. Sin embargo, el canal es unidireccional y el navegador no puede enviar mensajes de regreso. Para una comunicación bidireccional verdaderamente rápida, se requiere WebSockets.

Resumen de WebSockets

El término WebSocket se refiere a un protocolo de comunicación TCP sobre ws:// o el seguro y encriptado wss://. Es diferente de HTTP, aunque puede funcionar en el puerto 80 o 443 para asegurar que funcione en lugares que bloquean el tráfico no web. La mayoría de los navegadores lanzados desde 2012 son compatibles con el protocolo WebSocket.

En una aplicación web de tiempo real típica, debe tener al menos un servidor web para servir contenido web (HTML, CSS, JavaScript, imágenes, etc.) y un servidor WebSocket para manejar la comunicación bidireccional.

El navegador sigue realizando una solicitud inicial de WebSocket a un servidor, que abre un canal de comunicación. Entonces, ya sea el navegador o el servidor pueden enviar un mensaje en ese canal, lo que genera un evento en el otro dispositivo.

Comunicación con otros navegadores conectados

Después de la solicitud inicial, el navegador puede enviar y recibir mensajes hacia/desde el servidor WebSocket. El servidor WebSocket puede enviar y recibir mensajes hacia/desde cualquiera de sus navegadores cliente conectados.

La comunicación peer-to-peer es no posible. BrowserA no puede enviar mensajes directamente a BrowserB, incluso cuando ambos se ejecutan en el mismo dispositivo y están conectados al mismo servidor WebSocket. BrowserA solo puede enviar un mensaje al servidor y esperar que sea reenviado a otros navegadores según sea necesario.

Soporte de Servidor WebSocket

Node.js aún no tiene soporte nativo para WebSocket, aunque hay rumores de que llegará pronto. Para este artículo, estoy utilizando el módulo de terceros ws, pero hay docenas de otros.

El soporte integrado de WebSocket está disponible en los entornos de ejecución JavaScript Deno y Bun.

Las bibliotecas de WebSocket están disponibles para entornos de ejecución que incluyen PHP, Python y Ruby. Las opciones de SaaS de terceros como Pusher y PubNub también ofrecen servicios de WebSocket alojados.

Inicio rápido de demostración de WebSockets

Las aplicaciones de chat son el ¡Hola, Mundo! de las demostraciones de WebSocket, así que me disculpo por:

  1. Ser poco original. Dicho esto, las aplicaciones de chat son una excelente forma de explicar los conceptos.

  2. No poder proporcionar una solución en línea totalmente alojada. Preferiría no tener que monitorear y moderar una corriente de mensajes anónimos!

Clonar o descargar el repositorio node-wschat de GitHub:

git clone https://github.com/craigbuckler/node-wschat

Instalar las dependencias de Node.js:

cd node-wschat
npm install

Iniciar la aplicación de chat:

npm start

Abrir http://localhost:3000/ en varios navegadores o pestañas (también puedes definir tu nombre de chat en la cadena de consulta, como http://localhost:3000/?Craig). Escribe algo en una ventana y presiona ENVIAR o pulsa Enter, verás que aparece en todos los navegadores conectados.

Resumen del Código de Node.js

El archivo de entrada index.js de la aplicación Node.js inicia dos servidores:

  1. Una aplicación Express que se ejecuta en http://localhost:3000/ con una plantilla EJS para servir una sola página con HTML, CSS y JavaScript del lado del cliente. El JavaScript del navegador utiliza la API de WebSocket para realizar la conexión inicial y luego enviar y recibir mensajes.

  2. A WebSocket server running at ws://localhost:3001/, which listens for incoming client connections, handles messages, and monitors disconnections. The full code:

    // Servidor WebSocket
    import WebSocket, { WebSocketServer } from 'ws';
    
    const ws = new WebSocketServer({ port: cfg.wsPort });
    
    // conexión del cliente
    ws.on('connection', (socket, req) => {
    
      console.log(`connection from ${ req.socket.remoteAddress }`);
    
      // mensaje recibido
      socket.on('message', (msg, binary) => {
    
        // retransmitir a todos los clientes
        ws.clients.forEach(client => {
          client.readyState === WebSocket.OPEN && client.send(msg, { binary });
        });
    
      });
    
      // cerrado
      socket.on('close', () => {
        console.log(`disconnection from ${ req.socket.remoteAddress }`);
      });
    
    });

El biblioteca ws de Node.js:

  • Genera un evento "connection" cuando un navegador desea conectarse. La función manejadora recibe un objeto socket utilizado para comunicarse con ese dispositivo individual. Debe mantenerse durante toda la duración de la conexión.

  • Genera un evento socket "message" cuando un navegador envía un mensaje. La función manejadora retransmite el mensaje de vuelta a cada navegador conectado (incluyendo el que lo envió).

  • Genera un evento socket "close" cuando el navegador se desconecta, generalmente cuando se cierra la pestaña o se refresca.

Resumen del Código JavaScript del Cliente

El archivo static/main.js de la aplicación ejecuta la función wsInit() y pasa la dirección del servidor WebSocket (dominio de la página más un valor de puerto definido en la plantilla de la página HTML):

wsInit(`ws://${ location.hostname }:${ window.cfg.wsPort }`);

// manejo de comunicación WebSocket
function wsInit(wsServer) {

  const ws = new WebSocket(wsServer);

  // conectar al servidor
  ws.addEventListener('open', () => {
    sendMessage('entered the chat room');
  });

El evento open se activa cuando el navegador se conecta al servidor WebSocket. La función manejadora envía un mensaje entró en la sala de chat llamando a sendMessage():

// enviar mensaje
function sendMessage(setMsg) {

  let
    name = dom.name.value.trim(),
    msg =  setMsg || dom.message.value.trim();

  name && msg && ws.send( JSON.stringify({ name, msg }) );

}

La función sendMessage() obtiene el nombre del usuario y el mensaje desde el formulario HTML, aunque el mensaje puede ser sobreescrito por cualquier argumento setMsg pasado. Los valores se convierten en un objeto JSON y se envían al servidor WebSocket utilizando el método ws.send().

El servidor WebSocket recibe el mensaje entrante que activa el manejador "message" (ver arriba) y lo retransmite a todos los navegadores. Esto activa un evento "message" en cada cliente:

// recibir mensaje
ws.addEventListener('message', e => {

  try {

    const
      chat = JSON.parse(e.data),
      name = document.createElement('div'),
      msg  = document.createElement('div');

    name.className = 'name';
    name.textContent = (chat.name || 'unknown');
    dom.chat.appendChild(name);

    msg.className = 'msg';
    msg.textContent = (chat.msg || 'said nothing');
    dom.chat.appendChild(msg).scrollIntoView({ behavior: 'smooth' });

  }
  catch(err) {
    console.log('invalid JSON', err);
  }

});

El manejador recibe los datos JSON transmitidos en la propiedad .data del objeto de evento. La función lo parsea a un objeto JavaScript y actualiza la ventana de chat.

Finalmente, los nuevos mensajes se envían utilizando la función sendMessage() cada vez que se activa el manejador "submit" del formulario:

// envío de formulario
dom.form.addEventListener('submit', e => {
  e.preventDefault();
  sendMessage();
  dom.message.value = '';
  dom.message.focus();
}, false);

Manejo de errores

Un evento "error" se activa cuando la comunicación WebSocket falla. Esto puede manejarse en el servidor:

// manejar error de WebSocket en el servidor
socket.on('error', e => {
  console.log('WebSocket error:', e);
});

y/o en el cliente:

// manejar error de WebSocket en el cliente
ws.addEventListener('error', e => {
  console.log('WebSocket error:', e);
})

Solo el cliente puede restablecer la conexión ejecutando nuevamente el constructor new WebSocket().

Cierre de conexiones

Cualquiera de los dispositivos puede cerrar el WebSocket en cualquier momento utilizando el método .close() de la conexión. Puede proporcionar opcionalmente argumentos de código entero y motivo de cadena (máximo 123 bytes), que se transmiten al otro dispositivo antes de que se desconecte.

WebSocket avanzado

Administrar WebSockets es fácil en Node.js: un dispositivo envía un mensaje utilizando el método .send(), lo que desencadena un evento "message" en el otro. Cómo cada dispositivo crea y responde a esos mensajes puede ser más desafiante. Las siguientes secciones describen los problemas que puede necesitar considerar.

Seguridad de WebSocket

El protocolo WebSocket no maneja la autorización o la autenticación. No puedes garantizar que una solicitud de comunicación entrante provenga de un navegador o de un usuario conectado a tu aplicación web — especialmente cuando los servidores web y de WebSocket podrían estar en dispositivos diferentes. La conexión inicial recibe un encabezado HTTP que contiene cookies y el servidor Origin, pero es posible falsificar estos valores.

La siguiente técnica garantiza que restrinjas las comunicaciones WebSocket a usuarios autorizados:

  1. Antes de realizar la solicitud inicial de WebSocket, el navegador contacta con el servidor web HTTP (tal vez utilizando Ajax).

  2. El servidor verifica las credenciales del usuario y devuelve un nuevo ticket de autorización. El ticket normalmente referenciaría un registro en la base de datos que contiene el ID del usuario, la dirección IP, la hora de la solicitud, la hora de expiración de la sesión y cualquier otro dato requerido.

  3. El navegador pasa el ticket al servidor WebSocket en la mano inicial.

  4. El servidor WebSocket verifica el ticket y comprueba factores como la dirección IP, la hora de expiración, etc. antes de permitir la conexión. Ejecuta el método .close() de WebSocket cuando un ticket es inválido.

  5. El servidor WebSocket puede necesitar verificar el registro de la base de datos de vez en cuando para asegurarse de que la sesión del usuario permanezca válida.

Es importante, validar siempre los datos entrantes:

  • Al igual que HTTP, el servidor WebSocket es susceptible a inyecciones SQL y otros ataques.

  • El cliente nunca debería inyectar valores crudos en el DOM ni evaluar código JavaScript.

Separación vs múltiples instancias de servidor WebSocket

Considere un juego multijugador en línea. El juego tiene muchos universos jugando instancias separadas del juego: universeA, universeB, y universeC. Un jugador se conecta a un solo universo:

  • universeA: unido por player1, player2, y player3
  • universeB: unido por player99

Podría implementar lo siguiente:

  1. A separate WebSocket server for each universe.

    A player action in universeA would never be seen by those in universeB. However, launching and managing separate server instances could be difficult. Would you stop universeC because it has no players, or continue to manage that resource?

  2. Utilizar un solo servidor WebSocket para todos los universos del juego.

    Esto utiliza menos recursos y es más fácil de administrar, pero el servidor WebSocket debe registrar a qué universo se une cada jugador. Cuando player1 realiza una acción, debe ser transmitida a player2 y player3 pero no a player99.

Múltiples servidores WebSocket

El ejemplo de aplicación de chat puede manejar cientos de usuarios concurrentes, pero se colapsará una vez que la popularidad y el uso de memoria superen los umbrales críticos. Eventualmente necesitará escalar horizontalmente agregando más servidores.

Cada servidor WebSocket solo puede administrar a sus propios clientes conectados. Un mensaje enviado desde un navegador a serverX no podría ser transmitido a aquellos conectados a serverY. Puede volverse necesario implementar sistemas de mensajería de publicación-suscripción (pub-sub) en el backend. Por ejemplo:

  1. El WebSocket serverX desea enviar un mensaje a todos los clientes. Publica el mensaje en el sistema pub-sub.

  2. Todos los servidores WebSocket suscritos al sistema pub-sub reciben un evento de nuevo mensaje (incluyendo serverX). Cada uno puede manejar el mensaje y transmitirlo a sus clientes conectados.

Eficiencia de mensajería WebSocket

La comunicación WebSocket es rápida, pero el servidor debe administrar a todos los clientes conectados. Debes considerar la mecánica y la eficiencia de los mensajes, especialmente al construir juegos de acción multijugador:

  • ¿Cómo sincronizas las acciones de un jugador en todos los dispositivos cliente?

  • Si player1 está en una ubicación diferente a player2, ¿es necesario enviar a player2 información sobre acciones que no puede ver?

  • ¿Cómo lidias con la latencia de la red — o el retraso en la comunicación? ¿Tendría alguien con una máquina y conexión rápida una ventaja injusta?

Los juegos rápidos deben hacer concesiones. Piénsalo como jugar el juego en tu dispositivo local pero algunos objetos son influenciados por las actividades de otros. En lugar de enviar la posición exacta de cada objeto en todo momento, los juegos suelen enviar mensajes más simples y menos frecuentes. Por ejemplo:

  • objectX ha aparecido en pointX
  • objectY tiene una nueva dirección y velocidad
  • objectZ ha sido destruido

Cada juego de cliente llena los vacíos. Cuando objectZ explota, no importará si la explosión se ve diferente en cada dispositivo.

Conclusión

Node.js facilita el manejo de WebSockets. No necesariamente hace que las aplicaciones en tiempo real sean más fáciles de diseñar o codificar, pero la tecnología no te frenará!

Las principales desventajas:

  • Los WebSockets requieren su propia instancia de servidor separada. Las solicitudes Ajax Fetch() y eventos enviados por el servidor pueden ser manejados por el servidor web que ya estás ejecutando.

  • Los servidores WebSocket requieren sus propias verificaciones de seguridad y autorización.

  • Las conexiones WebSocket caídas deben restablecerse manualmente.

¡Pero no dejes que eso te desanime!

Preguntas Frecuentes (FAQs) sobre Aplicaciones en Tiempo Real con WebSockets y Eventos Enviados por el Servidor

¿En qué se diferencian los WebSockets de HTTP en términos de rendimiento y funcionalidad?

WebSockets proporcionan un canal de comunicación full-duplex sobre una única conexión TCP, lo que significa que los datos pueden enviarse y recibirse simultáneamente. Esto supone una mejora significativa sobre HTTP, donde cada solicitud requiere una nueva conexión. WebSockets también permiten la transferencia de datos en tiempo real, lo que los hace ideales para aplicaciones que requieren actualizaciones instantáneas, como aplicaciones de chat o actualizaciones en vivo de deportes. Por otro lado, HTTP es sin estado y cada par de solicitud-respuesta es independiente, lo que puede ser más adecuado para aplicaciones donde las actualizaciones en tiempo real no son necesarias.

¿Puedes explicar el ciclo de vida de una conexión WebSocket?

El ciclo de vida de una conexión WebSocket comienza con un apretón de manos (handshake), que actualiza una conexión HTTP a una conexión WebSocket. Una vez que se establece la conexión, los datos pueden enviarse y recibirse entre el cliente y el servidor hasta que cualquiera de las partes decida cerrar la conexión. La conexión puede cerrarse mediante el envío de un marco de cierre por parte del cliente o del servidor, seguido por la otra parte que reconoce el marco de cierre.

¿Cómo puedo implementar WebSockets en una aplicación Android?

Implementar WebSockets en una aplicación Android implica crear un cliente WebSocket que pueda conectarse a un servidor WebSocket. Esto se puede hacer utilizando bibliotecas como OkHttp o Scarlet. Una vez que el cliente está configurado, puedes abrir una conexión con el servidor, enviar y recibir mensajes, y manejar diferentes eventos como la apertura de la conexión, la recepción de mensajes y el cierre de la conexión.

¿Qué son los Server-Sent Events y cómo se comparan con los WebSockets?

Los Server-Sent Events (SSE) son un estándar que permite a un servidor enviar actualizaciones a un cliente a través de HTTP. A diferencia de los WebSockets, los SSE son unidireccionales, lo que significa que solo permiten que los datos se envíen desde el servidor al cliente. Esto los hace menos adecuados para aplicaciones que requieren comunicación bidireccional, pero pueden ser una solución más simple y eficiente para aplicaciones que solo necesitan actualizaciones desde el servidor.

¿Cuáles son algunos casos de uso comunes para WebSockets y Server-Sent Events?

Los WebSockets se utilizan comúnmente en aplicaciones que requieren comunicación en tiempo real y bidireccional, como aplicaciones de chat, juegos multijugador y herramientas colaborativas. Los Server-Sent Events, por otro lado, suelen utilizarse en aplicaciones que necesitan actualizaciones en tiempo real desde el servidor, como actualizaciones de noticias en vivo, actualizaciones de precios de acciones o informes de progreso para tareas de larga duración.

¿Cómo puedo manejar las conexiones WebSocket en una aplicación Spring Boot?

Spring Boot proporciona soporte para la comunicación WebSocket a través del módulo Spring WebSocket. Puedes usar la anotación @EnableWebSocket para habilitar el soporte de WebSocket y luego definir un WebSocketHandler para manejar el ciclo de vida de la conexión y el manejo de mensajes. También puedes usar el SimpMessagingTemplate para enviar mensajes a los clientes conectados.

¿Cuáles son las consideraciones de seguridad al utilizar WebSockets?

Al igual que cualquier otra tecnología web, los WebSockets pueden ser vulnerables a diversas amenazas de seguridad, como el secuestro de WebSocket entre sitios (CSWSH) y los ataques de denegación de servicio (DoS). Para mitigar estos riesgos, siempre debe utilizar conexiones WebSocket seguras (wss://) y validar y sanitizar todos los datos entrantes. También debería considerar el uso de mecanismos de autenticación y autorización para controlar el acceso a su servidor WebSocket.

¿Puedo usar WebSockets con una API REST?

Sí, puede usar WebSockets en conjunto con una API REST. Si bien las API REST son ideales para la comunicación sin estado de solicitud-respuesta, los WebSockets pueden utilizarse para una comunicación bidireccional en tiempo real. Esto puede ser especialmente útil en aplicaciones que requieren actualizaciones instantáneas, como aplicaciones de chat o actualizaciones en vivo de eventos deportivos.

¿Cómo puedo probar un servidor WebSocket?

Existen varias herramientas disponibles para probar servidores WebSocket, como la Prueba de Eco de WebSocket.org o Postman. Estas herramientas le permiten abrir una conexión WebSocket a un servidor, enviar mensajes y recibir respuestas. También puede escribir pruebas automatizadas para su servidor WebSocket utilizando bibliotecas como Jest o Mocha.

¿Cuáles son las limitaciones de los WebSockets y los Eventos Enviados por el Servidor?

Si bien los WebSockets y los Eventos Enviados por el Servidor ofrecen capacidades potentes para la comunicación en tiempo real, también tienen sus limitaciones. Por ejemplo, no todos los navegadores y redes admiten estas tecnologías, y pueden consumir una cantidad significativa de recursos si no se manejan adecuadamente. Además, pueden ser más complejos de implementar y administrar en comparación con la comunicación HTTP tradicional.

Source:
https://www.sitepoint.com/real-time-apps-websockets-server-sent-events/