Socket.IO 4.8
Visão Geral
Esta skill contém as regras do Socket.IO 4.8 que são fáceis de errar: um socket.id efêmero usado como chave de usuário, listeners registrados duas vezes, confirmações cujo formato do callback muda com timeout(), CORS que não protege WebSocket, sessões fixas esquecidas atrás de um balanceador de carga e APIs v2 ou v3 que não funcionam mais. Cada afirmação traz a URL da página de onde vem. Os arquivos de referência contêm os detalhes por domínio.
Versão Coberta
Socket.IO 4.8 (servidor socket.io, cliente socket.io-client), docs v4. Clientes JS 3.x e 4.x conversam com servidores 3.x e 4.x. https://socket.io/docs/v4/client-installation/#version-compatibility
Início Rápido
- Anexe ao servidor HTTP que você cria e chame
listennele, nunca noappdo Express:app.listen()cria outro servidor HTTP ao qual o Socket.IO não está anexado. 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()na mesma origem,io("https://server-domain.com")em origens diferentes (CORS deve estar habilitado no servidor). Importe comimport { 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
- Os tipos acompanham o pacote
socket.io-client. Não instale@types/socket.io-client, isso conflita com as tipagens incluídas. https://socket.io/docs/v4/client-installation/#from-npm
Regras e Armadilhas
Conexão e Identidade
- Socket.IO não é uma implementação de WebSocket: um cliente WebSocket puro não consegue alcançar um servidor Socket.IO, e o cliente Socket.IO não consegue alcançar um servidor WebSocket puro. https://socket.io/docs/v4/#what-socketio-is-not
- A menos que a recuperação de estado de conexão esteja habilitada, o
socket.idmuda em cada reconexão e atualização de página, difere por aba e não possui fila de mensagens. Identifique usuários com seu próprio ID de sessão enviado em um cookie ou noauth, e alcance cada aba de um usuário através de uma sala nomeada com o ID do usuário. 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
- Autentique em um middleware de conexão com
socket.handshake.auth. Chamenext()em todos os caminhos de código, caso contrário a conexão fica pendente até ser fechada após um tempo limite.next(err)chega ao cliente comoconnect_error, comerr.datapara detalhes. 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
- Uma conexão negada por um middleware não é repetida. Em
connect_error,socket.active === falsesignifica que a conexão foi negada pelo servidor e o cliente deve chamarsocket.connect()por conta própria para reconectar. https://socket.io/docs/v4/client-socket-instance/#connect_error - Registre listeners do cliente no nível superior, nunca dentro do manipulador
connect:connecté acionado novamente em cada reconexão e adiciona um listener duplicado a cada vez. https://socket.io/docs/v4/client-socket-instance/#connect - No servidor, registre
socket.on(...)de forma síncrona no manipuladorconnection, antes de qualquerawait, caso contrário pacotes iniciais não encontrarão manipulador. https://socket.io/docs/v4/troubleshooting-connection-issues/#delayed-event-handler-registration - Middlewares do Express vão para
io.engine.use(...), não paraio.use(...). https://socket.io/docs/v4/middlewares/#compatibility-with-express-middleware
Eventos e Confirmações (Acknowledgements)
- Passe objetos como eles são. Nunca use
JSON.stringify()em um payload.Datechega como string,MapeSetdevem ser convertidos manualmente. https://socket.io/docs/v4/emitting-events/#basic-emit - O callback de confirmação é o último argumento. Sem
timeout()ele recebe(response), comtimeout(ms)ele recebe(err, response). https://socket.io/docs/v4/emitting-events/#with-timeout - Prefira
await socket.timeout(ms).emitWithAck(...)dentro de um blocotry: ele rejeita em caso de timeout. O lado receptor chama seu callback com 1 argumento. https://socket.io/docs/v4/client-api/#socketemitwithackeventname-args - Um broadcast com confirmação exige
timeout()e retorna 1 resposta 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
- Listeners não possuem tratamento de erro embutido nem validação. Capture erros em cada manipulador, valide payloads com uma biblioteca de schema e verifique
typeof callback === "function"antes de usá-lo. https://socket.io/docs/v4/listening-to-events/#validation - Nunca use
connect,connect_error,disconnect,disconnecting,newListenerouremoveListenercomo evento de aplicação: emitir um nome reservado comosocket.emit("disconnecting")lança um erro. https://socket.io/docs/v4/emit-cheatsheet/
Broadcast e Salas (Rooms)
socket.to(room)esocket.broadcastexcluem o remetente,io.to(room)não.socket.to(socket.id).emit()não envia nada: useio.to(socket.id).emit(). https://socket.io/docs/v4/server-api/#sockettoroom- Chamadas
to()encadeadas são uma união, cada soquete recebe o evento uma vez. https://socket.io/docs/v4/rooms/#joining-and-leaving - Cada modificador de broadcast retorna um novo operador imutável: mantenha-o em uma variável em vez de chamar
io.to()e depoisio.emit(). https://socket.io/docs/v4/migrating-from-3-x-to-4-0/#ioto-is-now-immutable - Nomes de salas não são convertidos:
socket.join(42)não corresponde aio.to("42"). https://socket.io/docs/v4/migrating-from-2-x-to-3-0/#room-names-are-not-coerced-to-string-anymore - Salas são abandonadas automaticamente na desconexão. Leia-as em
disconnecting, não emdisconnectondesocket.roomsestá vazio. https://socket.io/docs/v4/rooms/#disconnection - Leia o adaptador com
io.of("/").adapter: a documentação avisa queio.adapter()não funcionará para isso, é o setter. https://socket.io/docs/v4/adapter/#api
Entrega (Delivery)
- A garantia padrão é no máximo uma vez e o servidor não armazena mensagem: um cliente desconectado perde broadcasts permanentemente. Replay a partir de um banco de dados com um offset enviado no
auth, ou habilite a recuperação de estado de conexão. https://socket.io/docs/v4/delivery-guarantees#at-most-once - O cliente armazena em buffer os emits enquanto está offline e os libera na reconexão. Verifique
socket.connectedou usesocket.volatile.emit(...)para dados que podem ser descartados. https://socket.io/docs/v4/client-offline-behavior/#buffered-events - Garantia de pelo menos uma vez do cliente para o servidor:
io({ retries: 3, ackTimeout: 10000 }). Para exatamente uma vez, armazene um ID gerado pelo cliente sob uma restrição única. Confirme em caso de sucesso e em caso de duplicata, mas não confirme em caso de qualquer outro erro, para que o cliente esgote o tempo limite e tente novamente. https://socket.io/docs/v4/tutorial/step-8 - A recuperação de estado de conexão é opt-in, precisa de um
maxDisconnectionDurationlimitado e pode falhar: verifiquesocket.recovered. O adaptador Redis (Pub/Sub) não suporta isso, o adaptador Redis Streams suporta. 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 e Implantação (Deployment)
- CORS está desligado por padrão. A opção é
cors.origin, nuncaorigins.origin: "*"comcredentials: truefalha no navegador: liste origens explícitas. https://socket.io/docs/v4/server-options/#cors - CORS cobre apenas navegadores em HTTP long-polling, não WebSocket. Restrinja quem conecta com
allowRequest. https://socket.io/docs/v4/handling-cors/ - Um proxy deve encaminhar os cabeçalhos
UpgradeeConnection, e seu timeout de leitura deve excederpingInterval + pingTimeout(45 segundos por padrão), ou os clientes verãotransport close. https://socket.io/docs/v4/reverse-proxy/#nginx - Com vários servidores e HTTP long-polling habilitado, sessões fixas ainda são necessárias independentemente do adaptador (as alternativas são desabilitar long-polling ou balanceamento de carga no lado do cliente). https://socket.io/docs/v4/using-multiple-nodes/ e https://socket.io/docs/v4/adapter/
- Sem sessões fixas, os clientes recebem respostas HTTP 400 com
{"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(padrão/socket.io/) deve corresponder em ambos os lados.io("/my-path/")seleciona um namespace, não um caminho. https://socket.io/docs/v4/troubleshooting-connection-issues/#the-request-path-does-not-match-on-both-sides- Feche com
io.close(), que também fecha o servidor HTTP. https://socket.io/docs/v4/server-api/#serverclosecallback
Arquivos de Referência
| Arquivo | Leia para |
|---|---|
| references/server-setup.md | Instalação, inicialização com Express, Koa, Fastify, Hono, HTTPS, HTTP/2, Bun, uWebSockets.js, opções e padrões do servidor, empacotamento, layout do manipulador |
| references/middleware-and-auth.md | Middlewares de conexão, soquete e engine, credenciais, identidade do usuário, hooks de cabeçalho, allowRequest, certificados do cliente |
| references/cors-and-proxies.md | Opções e erros de CORS, cookies, nginx, Apache, Caddy, http-proxy, balanceadores de carga em nuvem, compartilhamento de porta com ws |
| references/server-api.md | Instâncias Server e Socket, namespaces em io, métodos utilitários, serverSideEmit, razões de desconexão, eventos e códigos de erro da engine, fechamento |
| references/client.md | Pacotes, inicialização, eventos do Socket, connect_error e disconnect, Manager e multiplexação, todas as opções do cliente, bundlers |
| references/events-rooms-namespaces.md | Serialização, confirmações, listeners catch-all, validação, tratamento de erros, broadcasting, salas, namespaces dinâmicos |
| references/delivery-and-recovery.md | Garantias de entrega, tentativas, exatamente uma vez, replay com offset, recuperação de estado de conexão |
| references/multiple-nodes.md | Sessões fixas, configurações de proxy para vários nós, alternativas, cluster Node.js, PM2 |
| references/adapters.md | Adaptadores Redis, Redis fragmentado, Redis Streams, Postgres, MongoDB, Cluster, AWS SQS, Azure, GCP, matriz de funcionalidades, emitters |
| references/performance-and-operations.md | Complementos nativos, wsEngine, memória, parsers personalizados, limites do SO, teste de carga, Admin UI, logs de depuração |
| references/typescript.md | Interfaces de evento no servidor, cliente e namespaces, SocketData, testes tipados |
| references/testing-and-troubleshooting.md | Testes de integração do servidor, testes do cliente com um soquete falso, falhas de conexão, desconexões, long-polling travado |
| references/migrations-and-protocol.md | Mudanças de v2 para v3 e v3 para v4, atualização contínua com allowEIO3, protocolos wire Engine.IO e Socket.IO |
Padrões Antigos
| Uso Antigo | Substituição | Fonte |
|---|---|---|
Opção origins ou 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 na opção query do Socket | Opção auth, lida como socket.handshake.auth | https://socket.io/docs/v4/migrating-from-2-x-to-3-0/ |
Evento error do cliente para conexão recusada | connect_error, com err.message e err.data | https://socket.io/docs/v4/migrating-from-2-x-to-3-0/ |
socket.on("reconnect_attempt") no Socket | socket.io.on("reconnect_attempt") no Manager | https://socket.io/docs/v4/client-socket-instance/#events |
Evento pong do cliente para latência | socket.volatile.emit("ping", cb) em um 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, um Map | https://socket.io/docs/v4/migrating-from-2-x-to-3-0/ |
Object.keys(socket.rooms) | socket.rooms é um Set | https://socket.io/docs/v4/migrating-from-2-x-to-3-0/ |
socket.join(room, cb) | socket.join(room), aguardado com um adaptador assíncrono | https://socket.io/docs/v4/migrating-from-2-x-to-3-0/ |
Namespace.clients(cb) (v2), depois allSockets() (v3, a ser removido na próxima versão principal) | fetchSockets() | https://socket.io/docs/v4/server-api/#namespaceallsockets |
socket.binary(false) | Um parser personalizado | https://socket.io/docs/v4/migrating-from-2-x-to-3-0/ |
socket.emit("a").emit("b") | Chamadas separadas, emit() retorna true | https://socket.io/docs/v4/migrating-from-2-x-to-3-0/ |
Importação padrão de socket.io ou socket.io-client | import { Server } from "socket.io" e import { io } from "socket.io-client" | https://socket.io/docs/v4/migrating-from-2-x-to-3-0/#import-path |
Adicionar um namespace sem of() e sem o prefixo / | io.of("/my-namespace") | https://socket.io/docs/v4/migrating-from-2-x-to-3-0/#namespace-initialization |