WebSockets: conexión bidireccional en tiempo real entre cliente y servidor
Desarrollo Backend 2025-05-02 18 min de lectura

WebSockets: comunicación en tiempo real en la web

Qué son los WebSockets, cómo funcionan, alternativas como SSE, seguridad, escalabilidad y ejemplos completos con Node.js y el navegador.

Por CodeNaes

WebSockets: comunicación en tiempo real en la web

Los WebSockets son un protocolo estándar (RFC 6455) que permite una conexión bidireccional y persistente entre un cliente (navegador, app móvil, otro servidor) y un servidor. A diferencia de HTTP, donde el cliente siempre inicia la petición y el servidor responde, con WebSockets ambos pueden enviar datos en cualquier momento sobre la misma conexión, con muy poca sobrecarga. Por eso son la base de chats, notificaciones en vivo, dashboards en tiempo real, juegos multijugador y cualquier aplicación que necesite actualizaciones al instante.

En este artículo verás qué problema resuelven, cómo funcionan a nivel de protocolo, en qué se diferencian de otras opciones como polling o Server-Sent Events, ejemplos prácticos con Node.js y el navegador, seguridad, escalabilidad y buenas prácticas.

El límite del HTTP clásico

Con HTTP/1.1 (y en la práctica también con HTTP/2 para muchos casos), la comunicación sigue siendo request–response: el cliente envía una petición, el servidor responde y la conexión se cierra o se reutiliza para otra petición. Si el servidor quiere avisar al cliente de algo nuevo (un mensaje en un chat, un cambio en un dato), no puede “empujar” datos por su cuenta. Las alternativas tradicionales son:

  • Polling: el cliente hace peticiones HTTP periódicas (cada X segundos) para preguntar si hay novedades. Simple, pero ineficiente: muchas peticiones sin datos nuevos y mayor latencia.
  • Long polling: el cliente abre una petición que el servidor mantiene abierta hasta que hay algo que enviar; entonces responde y el cliente vuelve a abrir otra. Reduce peticiones pero sigue siendo request–response y más complejo de gestionar en el servidor.

Los WebSockets resuelven esto de raíz: una sola conexión TCP que se mantiene abierta y por la que cliente y servidor pueden enviar frames en cualquier momento, sin que el cliente tenga que “preguntar” cada vez. Es un canal full-duplex sobre el que puedes enviar texto o binario con un overhead mínimo por mensaje.

WebSockets frente a Server-Sent Events (SSE)

Otra opción para tiempo real es Server-Sent Events (SSE): el servidor envía datos al cliente por una conexión HTTP que permanece abierta; el cliente no envía nada por ese canal (solo es servidor → cliente).

Comparación en tabla

AspectoWebSocketsSSE
DirecciónBidireccional (cliente y servidor envían)Solo servidor → cliente
ProtocoloPropio (tras handshake HTTP con Upgrade)HTTP estándar (Content-Type: text/event-stream)
FormatoTexto o binarioSolo texto (eventos con nombre y datos)
ReconexiónHay que implementarla en el clienteNativa (navegador reconecta; Last-Event-ID para no perder eventos)
SoporteMuy bueno en navegadores y servidoresMuy bueno

Otra forma de verlo

WebSockets

  • Canal en dos direcciones sobre una sola conexión.
  • Tras el handshake HTTP, el tráfico ya no es HTTP.
  • Puedes enviar texto o binario.
  • La reconexión y el “qué hacer si se cae” los implementas tú (o usas una librería como Socket.IO).

SSE

  • Solo el servidor envía por esa conexión; el cliente usa HTTP normal para enviar.
  • Es HTTP de principio a fin (una petición larga con text/event-stream).
  • Solo texto; cada “evento” puede tener tipo y datos.
  • El navegador reconecta solo y usa Last-Event-ID para retomar; muy cómodo para feeds y notificaciones.

Cuándo usar SSE: cuando solo necesitas que el servidor empuje datos al cliente (notificaciones, feeds, actualizaciones de estado) y el cliente puede seguir usando HTTP normal para enviar acciones. Es más simple y se integra bien con HTTP.

Cuándo usar WebSockets: cuando necesitas que ambos envíen datos en tiempo real (chat, colaboración, juegos, control remoto). También cuando quieres enviar binario o minimizar latencia y número de conexiones.

Cómo funciona el protocolo WebSocket

Handshake inicial (HTTP)

La conexión WebSocket empieza siempre con una petición HTTP que pide “actualizar” la conexión al protocolo WebSocket (Upgrade). El cliente envía algo como:

GET /ws HTTP/1.1
Host: ejemplo.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
Origin: https://ejemplo.com

El servidor, si acepta, responde con 101 Switching Protocols y calcula Sec-WebSocket-Accept a partir de Sec-WebSocket-Key (según la RFC). A partir de ese momento, ya no es HTTP: la conexión pasa a usar el protocolo WebSocket (frames binarios).

Estructura de los frames

Después del handshake, los datos viajan en frames. Cada frame incluye, entre otras cosas:

  • Opcode: tipo de frame (1 = texto, 2 = binario, 8 = close, 9 = ping, 10 = pong).
  • Payload: los datos del mensaje.
  • Máscara: en frames enviados desde el cliente al servidor el payload va enmascarado (obligatorio en la RFC); el servidor responde sin máscara.

No necesitas implementar esto a mano: las APIs y librerías (navegador, ws, etc.) lo gestionan. Lo importante es saber que existe un handshake HTTP y luego un canal simétrico de frames.

Ciclo de vida de la conexión

  1. Cliente envía petición HTTP con Upgrade: websocket.
  2. Servidor responde 101 y se “sube” el protocolo.
  3. Conexión abierta: ambos pueden enviar y recibir.
  4. Cierre: uno de los dos (o ambos) envía un frame de cierre; luego se cierra el TCP. El que inicia el cierre puede enviar un código y un motivo (opcional).

En producción se usa wss:// (WebSocket sobre TLS), igual que https:// para HTTP, para cifrar y evitar que proxies intermedios alteren los frames.

Casos de uso típicos

  • Chats y mensajería: mensajes entrantes sin recargar ni hacer polling; envío y recepción por el mismo canal.
  • Notificaciones en vivo: alertas, avisos, “alguien ha comentado” o “nueva tarea asignada”.
  • Dashboards y métricas: gráficas que se actualizan con datos del servidor al instante (monitorización, analytics).
  • Colaboración: edición simultánea de documentos, cursores compartidos, presencia (quién está conectado).
  • Juegos y apps interactivas: estado del juego, movimientos de otros jugadores, votaciones en vivo.
  • Trading y finanzas: cotizaciones y precios en tiempo real.
  • Control remoto y IoT: envío de comandos y recepción de estado de dispositivos en tiempo real.

En todos ellos, el servidor puede enviar datos en el momento en que ocurren y el cliente puede responder o enviar acciones por la misma conexión, con baja latencia.

Ejemplo: servidor con Node.js (ws) y broadcast

En Node.js una opción muy usada es la librería ws. Un servidor mínimo que reenvía cada mensaje a todos los clientes (broadcast) podría ser:

const { WebSocketServer } = require('ws');

const wss = new WebSocketServer({ port: 8080 });

wss.on('connection', (ws, req) => {
  console.log('Cliente conectado', req.socket.remoteAddress);

  ws.on('message', (data) => {
    const text = data.toString();
    console.log('Mensaje recibido:', text);
    wss.clients.forEach((client) => {
      if (client !== ws && client.readyState === 1) {
        client.send(text);
      }
    });
  });

  ws.on('close', () => {
    console.log('Cliente desconectado');
  });
});

readyState === 1 significa “conexión abierta” (OPEN). Así evitas enviar a conexiones que ya se están cerrando.

Ejemplo: salas y protocolo de mensajes

Para algo más parecido a un chat real suele usarse un “protocolo” de mensajes (por ejemplo JSON con type y payload) y agrupar clientes en salas:

const { WebSocketServer } = require('ws');

const wss = new WebSocketServer({ port: 8080 });
const rooms = new Map(); // roomId -> Set<ws>

function joinRoom(ws, roomId) {
  if (!rooms.has(roomId)) rooms.set(roomId, new Set());
  rooms.get(roomId).add(ws);
  ws.roomId = roomId;
}

function leaveRoom(ws) {
  if (ws.roomId && rooms.has(ws.roomId)) {
    rooms.get(ws.roomId).delete(ws);
    if (rooms.get(ws.roomId).size === 0) rooms.delete(ws.roomId);
  }
}

wss.on('connection', (ws) => {
  ws.on('message', (raw) => {
    try {
      const msg = JSON.parse(raw.toString());
      if (msg.type === 'join' && msg.roomId) {
        leaveRoom(ws);
        joinRoom(ws, msg.roomId);
        ws.send(JSON.stringify({ type: 'joined', roomId: msg.roomId }));
      } else if (msg.type === 'chat' && ws.roomId) {
        const room = rooms.get(ws.roomId);
        if (room) {
          room.forEach((client) => {
            if (client.readyState === 1) {
              client.send(JSON.stringify({ type: 'message', text: msg.text }));
            }
          });
        }
      }
    } catch (e) {
      ws.send(JSON.stringify({ type: 'error', message: 'Invalid JSON' }));
    }
  });

  ws.on('close', () => leaveRoom(ws));
});

El cliente enviaría por ejemplo { "type": "join", "roomId": "sala1" } y luego { "type": "chat", "text": "Hola" }. Así puedes tener múltiples salas y mensajes solo dentro de cada una.

Ejemplo: cliente en el navegador con reconexión

La API del navegador es la WebSocket API. Aquí un ejemplo que usa el protocolo JSON anterior y reconexión con backoff exponencial:

const WS_URL = 'wss://ejemplo.com/ws';

function connect() {
  const ws = new WebSocket(WS_URL);

  ws.onopen = () => {
    console.log('Conectado');
    attempt = 0;
    ws.send(JSON.stringify({ type: 'join', roomId: 'sala1' }));
  };

  ws.onmessage = (event) => {
    const msg = JSON.parse(event.data);
    if (msg.type === 'message') {
      console.log('Mensaje:', msg.text);
    }
  };

  ws.onclose = () => {
    console.log('Conexión cerrada, reconectando...');
    scheduleReconnect();
  };

  ws.onerror = (err) => {
    console.error('Error:', err);
  };

  return ws;
}

let attempt = 0;
function scheduleReconnect() {
  const delay = Math.min(1000 * Math.pow(2, attempt), 30000);
  attempt++;
  setTimeout(() => connect(), delay);
}

let ws = connect();

Así, si la conexión se cae (red, reinicio del servidor), el cliente vuelve a intentar con esperas crecientes (1 s, 2 s, 4 s, … hasta 30 s) para no saturar el servidor.

Autenticación y origen

El handshake es una petición HTTP, así que puedes:

  • Validar el origen (Origin / Sec-WebSocket-Origin) para aceptar solo peticiones desde tus dominios y evitar que otros sitios abran conexiones a tu servidor en nombre del usuario.
  • Autenticar antes de considerar la conexión “válida”:
    • Token en query: wss://api.ejemplo.com/ws?token=JWT... (el servidor lee la query en la URL del handshake y valida el JWT).
    • Cookie de sesión: si tu sitio ya usa cookies HTTP-only, el handshake las envía; el servidor puede comprobar la sesión.
    • Primer mensaje: el cliente se conecta y el primer frame es un mensaje con credenciales o token; el servidor lo valida y, si falla, cierra la conexión.

En todos los casos, no confíes en datos que envíe el cliente sin validar en el servidor (por ejemplo, el roomId de “join” debe estar autorizado para ese usuario).

Librerías: ws frente a Socket.IO

  • ws: implementación mínima y estándar del protocolo WebSocket. Ligera, sin salas ni reconexión automática; tú implementas lo que necesites. Ideal cuando quieres control total y no necesitas fallbacks.

  • Socket.IO: librería que ofrece WebSocket cuando está disponible y fallback a long polling en entornos que bloquean WebSocket (algunos proxies, redes restrictivas). Incluye salas, namespaces, reconexión automática, acuse de recibo (ack) y eventos por nombre. El protocolo no es WebSocket puro, así que cliente y servidor deben ser Socket.IO. Muy práctica para aplicaciones que quieren “tiempo real” sin preocuparse del transporte.

Elegir ws cuando busques simplicidad, estándar y menor dependencia; Socket.IO cuando necesites compatibilidad máxima, salas y reconexión out-of-the-box.

Escalabilidad: varios servidores

Con un solo proceso, todos los clientes están en la misma memoria y puedes hacer broadcast como en los ejemplos. Con varios servidores (varios procesos o máquinas), cada uno tiene su propio conjunto de conexiones. Para que un mensaje llegue a todos los usuarios de una sala (o a un usuario concreto que puede estar en cualquier nodo), necesitas un bus de mensajes entre nodos:

  1. Cada servidor WebSocket se suscribe a canales (por ejemplo por sala o por usuario) en un sistema de mensajería (Redis pub/sub, RabbitMQ, etc.).
  2. Cuando un servidor recibe un mensaje que debe reenviar a la sala, además de enviarlo a los clientes locales de esa sala, publica en Redis (o similar) un mensaje del tipo “enviar a sala X”.
  3. Todos los servidores (incluido el que publicó) reciben ese mensaje y lo reenvían a sus clientes que estén en la sala X.

Así, las conexiones pueden repartirse entre nodos (por balanceador con sticky session o por asignación explícita) y el “broadcast” se hace vía pub/sub. También hay que decidir cómo asignar cliente a servidor (sticky session por cookie o por token) para que una misma sesión no salte de nodo sin control.

Buenas prácticas resumidas

  • Usar wss:// en producción para cifrar y evitar que proxies alteren o inspeccionen el tráfico.
  • Reconexión en el cliente con backoff (exponencial o lineal) y un tope máximo de espera.
  • Ping/pong: usar los frames de control o mensajes de aplicación (“heartbeat”) para detectar conexiones muertas y cerrarlas; muchas librerías (incluida ws) pueden hacer ping automático.
  • Límites: limitar tamaño máximo de mensaje y número de conexiones por IP o por usuario para evitar abusos y DoS.
  • Formato de datos: definir un pequeño protocolo (JSON con type y payload) para distinguir tipos de mensaje y mantener el código ordenado y extensible.
  • Cierre ordenado: enviar frame de cierre antes de cerrar la conexión cuando sea el servidor quien cierra (código y motivo opcionales).
  • Validar origen y autenticación en el handshake o en el primer mensaje; no confiar en datos del cliente sin validar.

Resumen

Los WebSockets proporcionan un canal bidireccional y persistente sobre una sola conexión TCP, ideal para aplicaciones que requieren actualizaciones en tiempo real en ambos sentidos. El handshake es HTTP (Upgrade); después, el tráfico va en frames WebSocket. Frente a polling o long polling reducen latencia y carga; frente a SSE, añaden la posibilidad de que el cliente envíe datos en tiempo real.

Con librerías como ws o Socket.IO y la API nativa del navegador puedes implementar chats, notificaciones, dashboards y colaboración con poco código. Para producción: wss://, autenticación y validación de origen, reconexión con backoff, heartbeat o ping/pong, límites de tamaño y conexiones, y si escalas a varios nodos, un bus de mensajes (por ejemplo Redis) para repartir los eventos entre servidores.

Etiquetas:

websocketstiempo realnodejavascriptapicomunicaciónsocket.iowss