WebSocket em Node.js: Aplicações em Tempo Real com Socket.IO e WS
Como implementar comunicação em tempo real com WebSocket em Node.js: chat, notificações push, dashboards ao vivo e escalabilidade com Redis adapter.
Quando usar WebSocket ao invés de HTTP
HTTP é request-response: o cliente pede, o servidor responde. Para dados que mudam constantemente — preço de ações, posição de entregadores no mapa, mensagens de chat, notificações em tempo real — polling HTTP é ineficiente: muitas requisições para poucos eventos, ou latência alta quando o intervalo é longo.
WebSocket mantém uma conexão persistente bidirecional: servidor pode empurrar dados ao cliente a qualquer momento, sem que o cliente precise pedir.
Socket.IO vs. ws nativo
ws: biblioteca WebSocket nativa, zero overhead, protocolo puro.
Socket.IO: abstração sobre WebSocket com fallback (Long Polling), reconexão automática, rooms, namespaces e broadcast.
Para a maioria dos casos de uso de produto, Socket.IO economiza semanas de implementação de features que você precisaria construir do zero no ws.
Implementação com Socket.IO
Servidor
import express from 'express'
import { createServer } from 'http'
import { Server, Socket } from 'socket.io'
import { createAdapter } from '@socket.io/redis-adapter'
import { createClient } from 'redis'
const app = express()
const httpServer = createServer(app)
const io = new Server(httpServer, {
cors: {
origin: process.env.ALLOWED_ORIGIN || 'http://localhost:3000',
methods: ['GET', 'POST'],
credentials: true,
},
// Configurações de reconexão
pingTimeout: 60000,
pingInterval: 25000,
})
// Redis adapter para múltiplas instâncias (horizontal scaling)
const pubClient = createClient({ url: process.env.REDIS_URL })
const subClient = pubClient.duplicate()
await Promise.all([pubClient.connect(), subClient.connect()])
io.adapter(createAdapter(pubClient, subClient))
// Middleware de autenticação
io.use(async (socket, next) => {
const token = socket.handshake.auth.token
if (!token) {
return next(new Error('Token de autenticação requerido'))
}
try {
const payload = verifyJWT(token)
socket.data.userId = payload.sub
socket.data.userRole = payload.role
next()
} catch {
next(new Error('Token inválido'))
}
})
// Conexão
io.on('connection', (socket: Socket) => {
const userId = socket.data.userId
console.log(`Usuário ${userId} conectou — socket ${socket.id}`)
// Entrar na room pessoal (para notificações direcionadas)
socket.join(`user:${userId}`)
// Handler: entrar em uma sala de chat
socket.on('join-room', async (roomId: string) => {
// Verificar se o usuário tem permissão para entrar nessa sala
const hasAccess = await checkRoomAccess(userId, roomId)
if (!hasAccess) {
socket.emit('error', { message: 'Acesso negado a esta sala' })
return
}
socket.join(`room:${roomId}`)
socket.to(`room:${roomId}`).emit('user-joined', { userId, roomId })
})
// Handler: enviar mensagem
socket.on('send-message', async (data: { roomId: string; content: string }) => {
const { roomId, content } = data
// Validar dados
if (!roomId || !content?.trim()) return
if (content.length > 2000) {
socket.emit('error', { message: 'Mensagem muito longa' })
return
}
// Salvar no banco
const message = await db.messages.create({
data: {
roomId,
userId,
content: content.trim(),
}
})
// Broadcast para todos na sala (incluindo o remetente)
io.to(`room:${roomId}`).emit('new-message', {
id: message.id,
roomId,
userId,
content: message.content,
createdAt: message.createdAt,
})
})
// Handler: digitando...
socket.on('typing', ({ roomId }: { roomId: string }) => {
socket.to(`room:${roomId}`).emit('user-typing', { userId, roomId })
})
socket.on('disconnect', (reason) => {
console.log(`Usuário ${userId} desconectou — ${reason}`)
})
})
httpServer.listen(3000)
Notificações push direcionadas
// Enviar notificação para um usuário específico (de qualquer serviço)
async function notifyUser(userId: string, notification: Notification) {
// Funciona mesmo com múltiplas instâncias graças ao Redis adapter
io.to(`user:${userId}`).emit('notification', notification)
// Salvar no banco para histórico e usuários offline
await db.notifications.create({
data: {
userId,
type: notification.type,
title: notification.title,
body: notification.body,
readAt: null,
}
})
}
// Exemplos de uso
await notifyUser('user-123', {
type: 'order-shipped',
title: 'Pedido enviado!',
body: 'Seu pedido #4521 foi postado. Código de rastreio: BR123456789BR',
})
await notifyUser('user-456', {
type: 'payment-received',
title: 'Pagamento confirmado',
body: 'Recebemos o pagamento de R$ 250,00',
})
Cliente (React/TypeScript)
import { useEffect, useRef, useState } from 'react'
import { io, Socket } from 'socket.io-client'
export function useSocket(token: string) {
const socketRef = useRef<Socket | null>(null)
const [connected, setConnected] = useState(false)
useEffect(() => {
if (!token) return
socketRef.current = io(process.env.NEXT_PUBLIC_WS_URL!, {
auth: { token },
reconnection: true,
reconnectionAttempts: 5,
reconnectionDelay: 1000,
})
socketRef.current.on('connect', () => setConnected(true))
socketRef.current.on('disconnect', () => setConnected(false))
socketRef.current.on('connect_error', (err) => {
console.error('WebSocket connection error:', err.message)
})
return () => {
socketRef.current?.disconnect()
}
}, [token])
return { socket: socketRef.current, connected }
}
// Componente de chat
function ChatRoom({ roomId }: { roomId: string }) {
const { socket } = useSocket(useAuthToken())
const [messages, setMessages] = useState<Message[]>([])
const [isTyping, setIsTyping] = useState<string[]>([])
useEffect(() => {
if (!socket) return
socket.emit('join-room', roomId)
socket.on('new-message', (message: Message) => {
setMessages(prev => [...prev, message])
})
socket.on('user-typing', ({ userId }: { userId: string }) => {
setIsTyping(prev => [...new Set([...prev, userId])])
setTimeout(() => {
setIsTyping(prev => prev.filter(id => id !== userId))
}, 3000)
})
return () => {
socket.off('new-message')
socket.off('user-typing')
}
}, [socket, roomId])
const sendMessage = (content: string) => {
socket?.emit('send-message', { roomId, content })
}
// ...render
}
Escalando para múltiplas instâncias
Com o Redis adapter, mensagens são distribuídas entre todas as instâncias do servidor:
Cliente A → Instância 1 → Redis pub/sub → Instância 2 → Cliente B
// Monitoramento de conexões ativas
io.of('/').adapter.on('join-room', (room, id) => {
metrics.gauge('websocket.room.connections',
await io.in(room).allSockets().then(s => s.size),
{ room }
)
})
// Total de conexões ativas
setInterval(async () => {
const sockets = await io.fetchSockets()
metrics.gauge('websocket.connections.total', sockets.length)
}, 30000)
Performance: quantas conexões WebSocket um servidor suporta?
| Configuração | Conexões simultâneas | |---|---| | Node.js single instance 2GB RAM | ~50.000–100.000 | | Cluster de 4 nodes atrás de Nginx | ~300.000–400.000 | | Horizontal scaling com Redis adapter | Virtualmente ilimitado (add nodes) |
O gargalo principal não é CPU mas memória — cada socket usa ~10–15KB de memória.
