Socket.IO 4.8
Resumen
Este skill contiene las reglas de Socket.IO 4.8 que son fáciles de cometer: un socket.id efímero usado como clave de usuario, listeners registrados dos veces, acknowledgements cuya forma del callback cambia con timeout(), CORS que no protege WebSocket, sesiones pegajosas olvidadas detrás de un balanceador de carga y APIs de v2 o v3 que ya no funcionan. Cada afirmación lleva la URL de la página de donde proviene. Los archivos de referencia contienen el detalle por dominio.
Versión cubierta
Socket.IO 4.8 (servidor socket.io, cliente socket.io-client), documentación v4. Clientes JS 3.x y 4.x se comunican con servidores 3.x y 4.x. https://socket.io/docs/v4/client-installation/#version-compatibility
Inicio rápido
- Enlaza al servidor HTTP que crees y llama a
listenen él, nunca en la aplicación de Express:app.listen()crea otro servidor HTTP al que Socket.IO no está enlazado. https://socket.io/docs/v4/server-initialization/#with-express
import express from "express";
import { createServer } from "node:http";
import { Server } from "socket.io";
const app = express();
const httpServer = createServer(app);
const io = new Server(httpServer, { cors: { origin: ["https://example.com"] } });
io.on("connection", (socket) => {
socket.on("hello", (arg, callback) => callback("got it"));
});
httpServer.listen(3000);
https://socket.io/docs/v4/server-initialization/#with-express
- Cliente:
io()en el mismo origen,io("https://server-domain.com")en orígenes cruzados (CORS debe estar habilitado en el servidor). Importa conimport { io } from "socket.io-client". https://socket.io/docs/v4/client-initialization/#from-a-different-domain
import { io } from "socket.io-client";
const socket = io("https://server-domain.com", { auth: { token: "abc" } });
const answer = await socket.timeout(5000).emitWithAck("hello", "world");
https://socket.io/docs/v4/client-api/#socketemitwithackeventname-args
- Los tipos vienen incluidos con
socket.io-client. No instales@types/socket.io-client, ya que entra en conflicto con los tipados integrados. https://socket.io/docs/v4/client-installation/#from-npm
Reglas y dificultades
Conexión e identidad
- Socket.IO no es una implementación de WebSocket: un cliente WebSocket simple no puede alcanzar un servidor Socket.IO, y el cliente Socket.IO no puede alcanzar un servidor WebSocket simple. https://socket.io/docs/v4/#what-socketio-is-not
- A menos que la recuperación del estado de conexión esté habilitada,
socket.idcambia en cada reconexión y recarga de página, difiere por pestaña y no tiene cola de mensajes. Identifica a los usuarios con tu propio ID de sesión enviado en una cookie o enauth, y alcanza cada pestaña de un usuario a través de una sala nombrada con el ID del usuario. https://socket.io/docs/v4/server-socket-instance/#socketid
io.on("connection", async (socket) => {
const userId = await computeUserIdFromHeaders(socket.handshake.headers);
socket.join(userId);
io.to(userId).emit("hi");
});
https://socket.io/docs/v4/rooms/#sample-use-cases
- Autentica en un middleware de conexión con
socket.handshake.auth. Llama anext()en cada camino de código, de lo contrario la conexión queda colgada hasta que se cierra después de un tiempo de espera.next(err)llega al cliente comoconnect_error, conerr.datapara más detalles. https://socket.io/docs/v4/middlewares/
io.use((socket, next) => {
if (isValid(socket.handshake.auth.token)) return next();
const err = new Error("not authorized");
err.data = { content: "Please retry later" };
next(err);
});
https://socket.io/docs/v4/middlewares/#handling-middleware-error
- Una conexión denegada por un middleware no se reintenta. En
connect_error,socket.active === falsesignifica que la conexión fue denegada por el servidor y el cliente debe llamar asocket.connect()por sí mismo para reconectar. https://socket.io/docs/v4/client-socket-instance/#connect_error - Registra los listeners del cliente en el nivel superior, nunca dentro del manejador
connect:connectse dispara de nuevo en cada reconexión y añade un listener duplicado cada vez. https://socket.io/docs/v4/client-socket-instance/#connect - En el servidor, registra
socket.on(...)de forma síncrona en el manejadorconnection, antes de cualquierawait, o los paquetes tempranos no llegan a ningún manejador. https://socket.io/docs/v4/troubleshooting-connection-issues/#delayed-event-handler-registration - Los middlewares de Express van a
io.engine.use(...), no aio.use(...). https://socket.io/docs/v4/middlewares/#compatibility-with-express-middleware
Eventos y acknowledgements
- Pasa objetos tal cual. Nunca uses
JSON.stringify()en una carga útil.Datellega como string,MapySetdeben convertirse manualmente. https://socket.io/docs/v4/emitting-events/#basic-emit - El callback de ack es el último argumento. Sin
timeout()recibe(response), contimeout(ms)recibe(err, response). https://socket.io/docs/v4/emitting-events/#with-timeout - Prefiere
await socket.timeout(ms).emitWithAck(...)en un bloquetry: se rechaza al exceder el tiempo de espera. El lado receptor llama a su callback con 1 argumento. https://socket.io/docs/v4/client-api/#socketemitwithackeventname-args - Una broadcast con acknowledgement requiere
timeout()y devuelve 1 respuesta por cliente. https://socket.io/docs/v4/server-api/#serveremiteventname-args
const responses = await io.timeout(10000).emitWithAck("some-event");
https://socket.io/docs/v4/server-api/#serveremitwithackeventname-args
- Los listeners no tienen manejo de errores ni validación incorporados. Captura errores en cada manejador, valida las cargas útiles con una biblioteca de esquemas y verifica
typeof callback === "function"antes de usarlo. https://socket.io/docs/v4/listening-to-events/#validation - Nunca uses
connect,connect_error,disconnect,disconnecting,newListeneroremoveListenercomo un evento de aplicación: emitir un nombre reservado comosocket.emit("disconnecting")lanza un error. https://socket.io/docs/v4/emit-cheatsheet/
Broadcasting y salas
socket.to(room)ysocket.broadcastexcluyen al emisor,io.to(room)no.socket.to(socket.id).emit()no envía nada: usaio.to(socket.id).emit(). https://socket.io/docs/v4/server-api/#sockettoroom- Las llamadas encadenadas
to()son una unión, cada socket recibe el evento una vez. https://socket.io/docs/v4/rooms/#joining-and-leaving - Cada modificador de broadcast devuelve un nuevo operador inmutable: guárdalo en una variable en lugar de llamar a
io.to()y luegoio.emit(). https://socket.io/docs/v4/migrating-from-3-x-to-4-0/#ioto-is-now-immutable - Los nombres de las salas no se convierten:
socket.join(42)no coincide conio.to("42"). https://socket.io/docs/v4/migrating-from-2-x-to-3-0/#room-names-are-not-coerced-to-string-anymore - Las salas se abandonan automáticamente al desconectar. Léelas en
disconnecting, no endisconnectdondesocket.roomsestá vacío. https://socket.io/docs/v4/rooms/#disconnection - Lee el adaptador con
io.of("/").adapter: la documentación advierte queio.adapter()no funcionará para eso, es el setter. https://socket.io/docs/v4/adapter/#api
Entrega
- La garantía predeterminada es como máximo una vez y el servidor no almacena ningún mensaje: un cliente desconectado pierde las broadcasts para siempre. Replica desde una base de datos con un offset enviado en
auth, o habilita la recuperación del estado de conexión. https://socket.io/docs/v4/delivery-guarantees#at-most-once - El cliente almacena en búfer los emits mientras está fuera de línea y los vacía al reconectar. Verifica
socket.connectedo usasocket.volatile.emit(...)para datos que pueden descartarse. https://socket.io/docs/v4/client-offline-behavior/#buffered-events - Cliente a servidor al menos una vez:
io({ retries: 3, ackTimeout: 10000 }). Para exactamente una vez, almacena un ID generado por el cliente bajo una restricción única. Confirma en éxito y en duplicado, pero no confirmes en ningún otro error, para que el cliente agote el tiempo de espera y reintente. https://socket.io/docs/v4/tutorial/step-8 - La recuperación del estado de conexión es opcional, necesita un
maxDisconnectionDurationlimitado y puede fallar: bifurca ensocket.recovered. El adaptador Redis (Pub/Sub) no lo soporta, el adaptador Redis Streams sí. https://socket.io/docs/v4/connection-state-recovery
const io = new Server(httpServer, {
connectionStateRecovery: { maxDisconnectionDuration: 2 * 60 * 1000, skipMiddlewares: false },
});
https://socket.io/docs/v4/connection-state-recovery#usage
CORS y despliegue
- CORS está desactivado por defecto. La opción es
cors.origin, nuncaorigins.origin: "*"concredentials: truefalla en el navegador: lista orígenes explícitos. https://socket.io/docs/v4/server-options/#cors - CORS cubre solo navegadores en HTTP long-polling, no WebSocket. Restringe quién se conecta con
allowRequest. https://socket.io/docs/v4/handling-cors/ - Un proxy debe reenviar los encabezados
UpgradeyConnection, y su tiempo de espera de lectura debe excederpingInterval + pingTimeout(45 segundos por defecto), o los clientes ventransport close. https://socket.io/docs/v4/reverse-proxy/#nginx - Con varios servidores y HTTP long-polling habilitado, las sesiones pegajosas siguen siendo necesarias independientemente del adaptador (las alternativas son deshabilitar long-polling o el balanceo de carga del lado del cliente). https://socket.io/docs/v4/using-multiple-nodes/ y https://socket.io/docs/v4/adapter/
- Sin sesiones pegajosas, los clientes obtienen respuestas HTTP 400 con
{"code":1,"message":"Session ID unknown"}. https://socket.io/docs/v4/troubleshooting-connection-issues/#you-didnt-enable-sticky-sessions-in-a-multi-server-setup path(por defecto/socket.io/) debe coincidir en ambos lados.io("/my-path/")selecciona un namespace, no una ruta. https://socket.io/docs/v4/troubleshooting-connection-issues/#the-request-path-does-not-match-on-both-sides- Cierra con
io.close(), que también cierra el servidor HTTP. https://socket.io/docs/v4/server-api/#serverclosecallback
Archivos de referencia
| Archivo | Léelo para |
|---|---|
| references/server-setup.md | Instalación, inicialización con Express, Koa, Fastify, Hono, HTTPS, HTTP/2, Bun, uWebSockets.js, opciones y valores predeterminados del servidor, empaquetado, diseño de manejadores |
| references/middleware-and-auth.md | Middlewares de conexión, socket y engine, credenciales, identidad de usuario, hooks de encabezados, allowRequest, certificados de cliente |
| references/cors-and-proxies.md | Opciones y errores de CORS, cookies, nginx, Apache, Caddy, http-proxy, balanceadores de carga en la nube, compartir un puerto con ws |
| references/server-api.md | Instancias de Server y Socket, namespaces en io, métodos de utilidad, serverSideEmit, razones de desconexión, eventos de engine y códigos de error, cierre |
| references/client.md | Bundles, inicialización, eventos de Socket, connect_error y disconnect, Manager y multiplexación, cada opción del cliente, bundlers |
| references/events-rooms-namespaces.md | Serialización, acknowledgements, listeners comodín, validación, manejo de errores, broadcasting, salas, namespaces dinámicos |
| references/delivery-and-recovery.md | Garantías de entrega, reintentos, exactamente una vez, repetición con offset, recuperación del estado de conexión |
| references/multiple-nodes.md | Sesiones pegajosas, configuraciones de proxy para varios nodos, alternativas, clúster de Node.js, PM2 |
| references/adapters.md | Adaptadores Redis, Redis sharded, Redis Streams, Postgres, MongoDB, Cluster, AWS SQS, Azure, GCP, matriz de características, emisores |
| references/performance-and-operations.md | Complementos nativos, wsEngine, memoria, analizadores personalizados, límites del SO, pruebas de carga, UI de administración, registros de depuración |
| references/typescript.md | Interfaces de eventos en servidor, cliente y namespaces, SocketData, pruebas tipadas |
| references/testing-and-troubleshooting.md | Pruebas de integración del servidor, pruebas del cliente con un socket falso, fallos de conexión, desconexiones, long-polling atascado |
| references/migrations-and-protocol.md | Cambios de v2 a v3 y v3 a v4, actualización gradual con allowEIO3, protocolos de Engine.IO y Socket.IO |
Patrones antiguos
| Uso antiguo | Reemplazo | Fuente |
|---|---|---|
Opción origins o handlePreflightRequest | cors: { origin: [...] } | https://socket.io/docs/v4/migrating-from-2-x-to-3-0/#cors-handling |
io.set("authorization", fn) | io.use((socket, next) => ...) | https://socket.io/docs/v4/migrating-from-2-x-to-3-0/#ioset-is-removed |
Token en la opción query del Socket | Opción auth, leída como socket.handshake.auth | https://socket.io/docs/v4/migrating-from-2-x-to-3-0/ |
Evento error del cliente para una conexión rechazada | connect_error, con err.message y err.data | https://socket.io/docs/v4/migrating-from-2-x-to-3-0/ |
socket.on("reconnect_attempt") en el Socket | socket.io.on("reconnect_attempt") en el Manager | https://socket.io/docs/v4/client-socket-instance/#events |
Evento pong del cliente para latencia | socket.volatile.emit("ping", cb) en un intervalo | https://socket.io/docs/v4/migrating-from-2-x-to-3-0/#no-more-pong-event-for-retrieving-latency |
Objeto Namespace.connected | Namespace.sockets, un Map | https://socket.io/docs/v4/migrating-from-2-x-to-3-0/ |
Object.keys(socket.rooms) | socket.rooms es un Set | https://socket.io/docs/v4/migrating-from-2-x-to-3-0/ |
socket.join(room, cb) | socket.join(room), esperado con un adaptador asíncrono | https://socket.io/docs/v4/migrating-from-2-x-to-3-0/ |
Namespace.clients(cb) (v2), luego allSockets() (v3, a eliminar en la próxima versión principal) | fetchSockets() | https://socket.io/docs/v4/server-api/#namespaceallsockets |
socket.binary(false) | Un parser personalizado | https://socket.io/docs/v4/migrating-from-2-x-to-3-0/ |
socket.emit("a").emit("b") | Llamadas separadas, emit() devuelve true | https://socket.io/docs/v4/migrating-from-2-x-to-3-0/ |
Importación por defecto de socket.io o socket.io-client | import { Server } from "socket.io" o import { io } from "socket.io-client" | https://socket.io/docs/v4/migrating-from-3-x-to-4-0/ |
Agregar eventos después de new Server(server) | No: new Server() inicia la escucha en el servidor, se deben pasar las opciones con cors, connectionStateRecovery, etc. en el constructor | https://socket.io/docs/v4/migrating-from-3-x-to-4-0/ |