Introducción
En aplicaciones Node.js en tiempo real, una conexión WebSocket no es solo una "tubería abierta". Es algo vivo que se abre, envía datos, puede morir silenciosamente y puede fallar de formas inesperadas. Si tu código no gestiona explícitamente cada fase de ese ciclo de vida, tu servidor presentará fugas de memoria, acumulará conexiones zombie y eventualmente se bloqueará. Esta lección cubre el ciclo de vida completo: el evento open, el evento close, el evento error y los heartbeats ping utilizados para detectar pares inactivos.
El ciclo de vida de la conexión
Cada conexión WebSocket en Node.js pasa por cuatro estados observables. La librería (aquí usamos el popular paquete `ws`) emite eventos que puedes escuchar: `connection`, `message`, `close` y `error`. Un patrón común es envolver todos los handlers en una sola función para que cada nuevo cliente reciba la misma gestión del ciclo de vida.
El `code` del cierre sigue la especificación de WebSocket. Valores comunes: `1000` significa un cierre normal iniciado por cualquiera de las dos partes, `1001` significa que el cliente está navegando hacia otra página, `1006` es el famoso "cierre anormal" que a menudo indica que la red falló de forma inesperada.
Heartbeats: Detectando clientes inactivos
TCP no te aviza cuando un cliente ha desaparecido silenciosamente — la conexión simplemente se queda ahí, medio abierta, hasta que algo más lo nota. El protocolo WebSocket resuelve esto con frames de control llamados ping (opcode 0x9) y pong (opcode 0xA). Cualquiera de las dos partes puede enviar un ping; el par debe responder con un pong que contenga el mismo payload. Si no llega ningún pong dentro de una ventana de timeout, la conexión está muerta y debe terminarse.
La librería `ws` expone `ws.ping()`, `ws.pong()` y una opción `heartbeat` incorporada que hace la mayor parte del trabajo. El patrón siguiente es la implementación canónica.
Cómo funciona esto en la práctica: al iniciar el servidor, `isAlive` se establece en `true` para cada nuevo cliente. Cada 30 segundos, el servidor itera sobre todos los clientes. Si un cliente ya fue marcado como muerto en la ronda anterior, se llama a `terminate()`, que cierra forzosamente el socket TCP subyacente. En caso contrario, el flag se reinicia y se envía un frame ping. Un cliente sano responde automáticamente con un pong, lo que dispara el handler `pong` y vuelve a establecer `isAlive` en `true`. Un cliente muerto nunca responde con pong, así que en la siguiente ronda se termina.
Manejo de errores con elegancia
El evento `error` se dispara por violaciones de protocolo, frames inválidos y fallos del socket a bajo nivel. Sin un listener, un evento de error no manejado bloqueará tu proceso de Node.js. Siempre adjunta uno y combínalo con el evento `close`, porque en la mayoría de los casos un error va seguido inmediatamente de un cierre. Una regla útil: registra el error para diagnóstico, pero no intentes llamar a `send()` desde dentro del handler de error — el socket puede que ya sea inutilizable.
La propiedad `code` del error a menudo contiene valores a nivel de sistema como `ECONNRESET` o `EPIPE`. Son pistas de depuración, no lógica de aplicación con la que reaccionar.
Resumen
Gestionar el ciclo de vida de un WebSocket es una disciplina de tres hábitos. Siempre escucha los eventos `close` y `error` para que los zombies y los fallos se limpien. Implementa un bucle de heartbeat ping/pong con un intervalo razonable (30 segundos es el estándar) para detectar desconexiones silenciosas. Y nunca dejes que un evento `error` no manejado alcance el comportamiento de bloqueo por defecto de Node.js. Juntos, estos patrones mantienen los servidores en tiempo real de larga duración estables incluso bajo condiciones de red inestables y miles de clientes concurrentes.
Punto de control de la lección
Repasa lo aprendido y recibe comentarios sobre tu trabajo.