Saltar al contenido

Una conexión WebSocket no es una arquitectura en tiempo real

El transporte mueve mensajes. Tu protocolo decide si el producto conserva un estado coherente.

Diseño de sistemas4 min de lectura

Ingeniero de software. Desarrollo extensiones de Chrome, aplicaciones web y herramientas de IA aplicada.

Contenido

Conectar dos navegadores mediante WebSockets puede tomar unas pocas líneas de código.

socket.emit("move", payload);
socket.on("move", renderMove);

Eso demuestra que los mensajes pueden viajar. No demuestra que ambos navegadores coincidan en el estado del producto.

La arquitectura empieza después de abrir la conexión.

Empieza por la autoridad

Para cada acción compartida, decide qué proceso puede convertir una petición en un cambio aceptado.

En una partida de ajedrez de dos jugadores, el navegador puede comprobar si mover de e2 a e4 parece legal. Sirve para dar una respuesta rápida, pero no basta para el estado compartido: un navegador puede estar desactualizado o modificado.

El servidor debería ser quien acepte la transición:

request → membership → permission → current state → domain validation → save → broadcast

La petición pasa por comprobaciones de pertenencia, permisos, estado actual y reglas del dominio. Después se guarda y se difunde.

Evitar trampas es el beneficio obvio. También impide que dos clientes honestos diverjan al actuar sobre instantáneas diferentes.

Nombra los eventos por lo que ocurrió

Es más fácil mantener un evento cuando su nombre describe un hecho concreto.

Nombres poco precisos:

update
data
message
sync

Nombres útiles:

room-updated
move
commentary
invalid-move

Quien recibe el evento puede distinguir si está manejando una instantánea, una acción del dominio aceptada, un efecto secundario o un rechazo.

Evita un evento enorme cuyo contenido cambie de estructura según un campo type, salvo que hayas definido un formato envolvente y un esquema claros. Separar eventos facilita entender los registros, las pruebas y el control de acceso.

Define los datos como contratos de API

Un evento de socket merece el mismo cuidado que un endpoint HTTP.

Documenta:

  • la dirección;
  • los campos obligatorios;
  • los tipos y sus restricciones;
  • quién puede enviarlo;
  • quién lo recibe;
  • qué cambia en el estado;
  • la respuesta de éxito;
  • el comportamiento al rechazarlo;
  • su orden respecto a otros eventos.

Por ejemplo:

socket.emit("move", {
  roomId: "abc123",
  move: {
    from: "e2",
    to: "e4",
    promotion: "q",
  },
});

El contrato incluye algo más que la estructura JSON. Establece que el servidor comprueba que el socket ocupa una plaza, que es el turno de su color y que la jugada es legal antes de difundir nada.

Separa las instantáneas del flujo de eventos

Los clientes que entran tarde o se reconectan necesitan un estado de partida. Un flujo de eventos solo sirve si quien lo recibe sabe desde dónde empezar.

Usa ambos:

  • una instantánea del estado actual;
  • eventos del dominio para los cambios posteriores.

En Real-Time Chess, room-updated lleva el FEN de la sala, los jugadores, el estado, el historial y los comentarios después de crear o entrar en una sala. move y commentary gestionan los cambios en directo.

El siguiente paso de madurez es un número de versión:

{
  roomId: "abc123",
  version: 42,
  fen: "...",
}

Cada evento puede indicar qué versión produce. Un cliente que ve 40 → 42 sabe que perdió algo y debe pedir una instantánea nueva.

El orden es una decisión de producto

WebSockets conserva el orden de los mensajes en una conexión, pero tu aplicación sigue creando ramas asíncronas.

Supón que una jugada activa un comentario generado por un modelo. Si el servidor espera al modelo antes de enviar la jugada, una función secundaria controla la latencia de la partida.

Un contrato mejor sería:

save move
emit move
generate commentary
save commentary
emit commentary

Primero se guarda y se emite la jugada. Después se genera, se guarda y se emite el comentario.

El tablero responde con rapidez y el panel de comentarios puede mostrar su propio estado pendiente.

Conviene dejar el orden por escrito porque determina qué ve el usuario cuando una parte tarda o falla.

Las salas sirven para enrutar, no para autorizar

Las salas de Socket.IO permiten difundir un evento a un grupo de sockets conectados. Responden quién debe recibirlo.

No responden automáticamente:

  • si este socket puede entrar;
  • si puede ejecutar este comando;
  • si sigue ocupando su plaza;
  • si los datos que envía pueden modificar la sala.

La autorización corresponde al punto donde se recibe el comando. Pertenecer a una sala solo delimita la entrega.

Reconectarse exige identidad

Un identificador de socket representa una conexión de transporte. No identifica a una persona ni a un cliente de forma duradera.

Si la identidad es socket.id, actualizar la página crea un jugador nuevo. Un protocolo de producción necesita algo estable:

player ID + signed room seat token + current socket ID

Se combinan un identificador de jugador, un token firmado de su plaza en la sala y el identificador del socket actual.

Al reconectarse, el servidor valida el token, sustituye la asociación con el socket anterior y envía la instantánea actual con su versión.

Sin esa capa, «admitir reconexión» suele significar que el navegador volvió a conectarse, sin que el usuario recuperara su sesión.

Prueba la conversación entre los clientes y el servidor

Las pruebas unitarias de los manejadores sirven, pero muchos errores de tiempo real aparecen entre eventos.

Una comprobación de extremo a extremo debería conectar varios clientes y verificar:

  • quién recibe cada evento;
  • el orden de los eventos;
  • los comandos aceptados y rechazados;
  • la estructura de la instantánea;
  • el comportamiento después de una desconexión;
  • las rutas de tiempo límite y de respuesta alternativa.

La prueba es una conversación entre los clientes y el servidor. Trátala como tal.

La lista que uso ahora

Antes de dar por terminada una función con WebSockets, necesito respuestas claras a estas preguntas:

  1. ¿Quién decide el estado compartido válido?
  2. ¿Cuáles son los eventos de comando y de resultado?
  3. ¿Qué garantiza el contenido de cada evento?
  4. ¿Qué instantánea recibe un cliente nuevo?
  5. ¿Cómo se detectan los eventos perdidos o duplicados?
  6. ¿Qué se ejecuta en el flujo principal?
  7. ¿Qué ocurre cuando falla el trabajo secundario?
  8. ¿Cómo sobrevive la identidad a una reconexión?
  9. ¿Cómo comparten el estado varias instancias del servidor?
  10. ¿Qué flujo con varios clientes demuestra que se cumple el contrato?

La arquitectura está en las decisiones que mantienen coherente el producto mientras los mensajes viajan.

Un pequeño voto de confianza

¿Valió la pena leer esta nota?