Implementación básica en el lado del cliente (JavaScript en navegador)

En esta sección verás cómo crear un cliente que use la API WebSocket del navegador para conectarse, enviar y recibir mensajes. Luego construiremos una versión con interfaz mínima, reconexión y mensajes en JSON.

1. Ejemplo mínimo (lo más corto posible)

Pruébalo con un servidor en ws://localhost:8080 (el de tu punto anterior o cualquiera que tengas).

<!doctype html>
<html lang="es">
<body>
<script>
  const ws = new WebSocket("ws://localhost:8080");

  ws.onopen = () => {
    console.log("Conexión abierta");
    ws.send("Hola servidor desde el navegador!");
  };

  ws.onmessage = (e) => console.log("Servidor:", e.data);
  ws.onclose   = () => console.log("Conexión cerrada");
  ws.onerror   = (err) => console.error("Error WS:", err);
</script>
</body>
</html>

2. Cliente base listo para usar (UI + JSON + reconexión)

Qué incluye:

  • Campos usuario y sala (room).
  • Lista de mensajes.
  • Envío con Enter o botón.
  • Reconexión automática con backoff exponencial.
  • Protocolo de mensajes en JSON ({type, payload}).
  • Indicador de “está escribiendo”.

Guárdalo como cliente.html y ábrelo en el navegador. Asegúrate de que tu servidor acepte los tipos join, message, typing (como el que ya hicimos).

<!doctype html>
<html lang="es">
<head>
<meta charset="utf-8"/>
<title>WS Cliente Básico</title>
<style>
  body { font-family: system-ui, sans-serif; max-width: 720px; margin: 2rem auto; }
  .fila { display: flex; gap: .5rem; margin-bottom: .5rem; }
  input, button { padding: .5rem .7rem; }
  #estado { font-size: .9rem; margin-bottom: .5rem; }
  #mensajes { border: 1px solid #ddd; padding: .75rem; min-height: 240px; border-radius: .5rem; }
  .sistema { color:#666; font-style: italic; }
  .yo { color:#0a7; }
  .otro { color:#06c; }
</style>
</head>
<body>

<h1>Cliente WebSocket</h1>

<div id="estado">🔴 Desconectado</div>

<div class="fila">
  <input id="user"  placeholder="usuario" value="ana">
  <input id="room"  placeholder="sala"    value="general">
  <button id="btn-join">Unirme</button>
</div>

<div id="mensajes" aria-live="polite"></div>

<div class="fila">
  <input id="texto" placeholder="Escribe un mensaje y presiona Enter" style="flex:1">
  <button id="btn-send">Enviar</button>
</div>

<div id="typing" class="sistema"></div>

<script>
let ws;
let conectado = false;
let usuarioActual = "";
let salaActual = "";
let backoff = 500; // reconexión exponencial (ms), tope suave 5s
let typingTimer;
let puedeAvisarTyping = true;

const $estado   = document.getElementById("estado");
const $mensajes = document.getElementById("mensajes");
const $texto    = document.getElementById("texto");
const $typing   = document.getElementById("typing");

function setEstado(ok, detalle="") {
  conectado = ok;
  $estado.textContent = (ok ? "🟢 Conectado" : "🔴 Desconectado") + (detalle ? "  " + detalle : "");
}

function addLinea(html, clase="") {
  const p = document.createElement("p");
  if (clase) p.className = clase;
  p.innerHTML = html;
  $mensajes.appendChild(p);
  $mensajes.scrollTop = $mensajes.scrollHeight;
}

function safeSend(obj) {
  if (ws && ws.readyState === WebSocket.OPEN) {
    ws.send(JSON.stringify(obj));
  } else {
    addLinea("No se puede enviar: socket no abierto", "sistema");
  }
}

function conectar() {
  ws = new WebSocket("ws://localhost:8080");

  ws.onopen = () => {
    setEstado(true, "socket abierto");
    backoff = 500;
    addLinea("Conexión establecida", "sistema");

    // Si ya teníamos usuario/sala, reingresar
    if (usuarioActual && salaActual) {
      safeSend({ type: "join", payload: { user: usuarioActual, room: salaActual } });
    }
  };

  ws.onmessage = (e) => {
    let msg;
    try { msg = JSON.parse(e.data); }
    catch { return addLinea("Servidor (texto): " + e.data, "otro"); }

    switch (msg.type) {
      case "welcome":
        addLinea("Bienvenido: id=" + (msg.payload?.id ?? "?"), "sistema");
        break;
      case "ack":
        // ACK de join/message u otras operaciones
        // addLinea("ACK: " + JSON.stringify(msg.payload), "sistema");
        break;
      case "system":
        addLinea(`[SISTEMA] ${msg.payload?.text ?? ""}`, "sistema");
        break;
      case "message": {
        const { user, text } = msg.payload || {};
        if (user === usuarioActual) addLinea(`: ${text}`, "yo");
        else addLinea(`${user ?? "?"}: ${text}`, "otro");
        break;
      }
      case "typing":
        if (msg.payload?.user && msg.payload.user !== usuarioActual) {
          $typing.textContent = `${msg.payload.user} está escribiendo`;
          clearTimeout(typingTimer);
          typingTimer = setTimeout(() => $typing.textContent = "", 1000);
        }
        break;
      case "error":
        addLinea("ERROR: " + (msg.payload?.code ?? "desconocido"), "sistema");
        break;
      default:
        // Mensajes personalizados de tu servidor
        // addLinea("Evento " + msg.type + ": " + JSON.stringify(msg.payload));
        break;
    }
  };

  ws.onclose = () => {
    setEstado(false, "cerrado");
    addLinea("Conexión cerrada. Reintentando", "sistema");
    setTimeout(conectar, backoff);
    backoff = Math.min(backoff * 2, 5000);
  };

  ws.onerror = (err) => {
    console.error("WS error:", err);
  };
}

conectar();

// UI
document.getElementById("btn-join").onclick = () => {
  const user = document.getElementById("user").value.trim();
  const room = document.getElementById("room").value.trim();
  if (!user || !room) return addLinea("Completá usuario y sala", "sistema");

  usuarioActual = user;
  salaActual = room;
  safeSend({ type: "join", payload: { user, room } });
};

document.getElementById("btn-send").onclick = enviar;

$texto.addEventListener("keydown", (ev) => {
  if (ev.key === "Enter") enviar();
});

$texto.addEventListener("input", () => {
  if (!puedeAvisarTyping) return;
  puedeAvisarTyping = false;
  setTimeout(() => puedeAvisarTyping = true, 500); // throttle 500ms
  safeSend({ type: "typing", payload: {} });
});

function enviar() {
  const t = $texto.value;
  if (!t) return;
  safeSend({ type: "message", payload: { text: t } });
  $texto.value = "";
}
</script>
</body>
</html>

Qué estás cubriendo con este cliente

  • Conexión y reconexión automática (backoff exponencial).
  • Mensajería JSON con convención {type, payload}.
  • join/message/typing (muy típico en chats).
  • UI básica y accesible (estado, mensajes, typing).
  • Manejo de eventos open, message, close, error.

3. Buenas prácticas del lado del cliente

  • Siempre JSON.parse dentro de try/catch. Tu servidor puede enviar texto o JSON.
  • Envoltorio safeSend: evita InvalidStateError si el socket no está OPEN.
  • Throttle/debounce para eventos frecuentes (por ejemplo, typing).
  • Backoff para reconexión y límite (p. ej. 5s).
  • Buffer opcional de mensajes si el socket cae (cola y reintento al reconectar).
  • En producción, usar wss:// (TLS). Cambia la URL según tu dominio/puerto.
  • No envíes pings de protocolo: el navegador no puede enviar frames Ping. Si quieres keep-alive desde el cliente, hazlo a nivel aplicación (por ejemplo, enviar {type:"ping"} cada X segundos; el servidor responde {type:"pong"}).

Resumen

  • Con muy poco código ya tienes un cliente WebSocket funcional.
  • La convención de mensajes (type/payload) te permite crecer ordenado.
  • Sumando reconexión y UI simple, puedes probar casi cualquier backend WS.