Pular para o conteúdo
desenvolvimento

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.

Douglas M. Pereira5 min de leitura
websocketsocket.ionodejstempo realchatnotificaçõesredis

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.