WebSocket com Node.js na VPS

WebSocket permite comunicação bidirecional em tempo real entre servidor e cliente, essencial para chats, dashboards ao vivo, notificações push e jogos multiplayer. Na VPS, o Nginx precisa de configuração específica para fazer proxy de conexões WebSocket, e aplicações com múltiplos workers exigem um adapter Redis para sincronizar eventos entre processos.

Socket.IO: servidor WebSocket com fallback

Socket.IO é a biblioteca mais popular, inclui fallback para long-polling quando WebSocket não está disponível e suporte nativo a salas:

typescript
// src/index.ts
import express from 'express'
import { createServer } from 'http'
import { Server } from 'socket.io'

const app = express()
const httpServer = createServer(app)
const io = new Server(httpServer, {
  cors: {
    origin: process.env.ALLOWED_ORIGINS?.split(',') || [],
    methods: ['GET', 'POST'],
  },
  // Configurações para produção
  pingTimeout: 60000,
  pingInterval: 25000,
  transports: ['websocket', 'polling'],
})

io.on('connection', (socket) => {
  console.log('Cliente conectado:', socket.id)

  socket.on('join-room', (roomId: string) => {
    socket.join(roomId)
    socket.to(roomId).emit('user-joined', { userId: socket.id })
  })

  socket.on('message', (data: { room: string; text: string }) => {
    io.to(data.room).emit('message', {
      from: socket.id,
      text: data.text,
      timestamp: new Date().toISOString(),
    })
  })

  socket.on('disconnect', (reason) => {
    console.log('Cliente desconectado:', socket.id, reason)
  })
})

httpServer.listen(3000)

Nginx: proxy para WebSocket

O Nginx precisa dos headers Upgrade e Connection para fazer proxy de WebSocket. Sem eles, a conexão cai silenciosamente para HTTP:

nginx
# nginx.conf para WebSocket
server {
    listen 443 ssl;
    http2 on;
    server_name ws.seudominio.com.br;

    ssl_certificate /etc/letsencrypt/live/ws.seudominio.com.br/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/ws.seudominio.com.br/privkey.pem;

    location / {
        proxy_pass http://app:3000;
        proxy_http_version 1.1;

        # Headers obrigatórios para WebSocket
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";

        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;

        # Timeouts para conexões de longa duração
        proxy_read_timeout 3600s;   # 1 hora sem mensagem
        proxy_send_timeout 3600s;
        proxy_connect_timeout 10s;
    }
}

Redis Adapter: WebSocket com múltiplos workers

Com PM2 cluster ou múltiplos containers, cada worker tem sua própria instância Socket.IO. Um evento emitido no worker 1 não chega aos clientes do worker 2. Redis adapter sincroniza eventos entre workers:

typescript
// src/socket.ts
import { Server } from 'socket.io'
import { createAdapter } from '@socket.io/redis-adapter'
import { createClient } from 'redis'

export async function setupSocketIO(httpServer: any) {
  const io = new Server(httpServer)

  // Criar dois clientes Redis: pub e sub
  const pubClient = createClient({ url: process.env.REDIS_URL })
  const subClient = pubClient.duplicate()

  await Promise.all([pubClient.connect(), subClient.connect()])

  // Configurar adapter Redis
  io.adapter(createAdapter(pubClient, subClient))

  io.on('connection', (socket) => {
    // Agora eventos são sincronizados entre todos os workers
    socket.on('message', (data) => {
      // io.to(room).emit() propaga para clientes em TODOS os workers
      io.to(data.room).emit('message', data)
    })
  })

  return io
}

Autenticação em WebSocket

WebSocket não tem headers HTTP após o handshake inicial, use token JWT passado na conexão:

typescript
// Middleware de autenticação Socket.IO
import jwt from 'jsonwebtoken'

io.use((socket, next) => {
  const token = socket.handshake.auth.token
               || socket.handshake.query.token as string

  if (!token) {
    return next(new Error('Token não fornecido'))
  }

  try {
    const payload = jwt.verify(token, process.env.JWT_SECRET!) as JwtPayload
    socket.data.userId = payload.sub
    next()
  } catch (err) {
    next(new Error('Token inválido ou expirado'))
  }
})

// No cliente JavaScript:
const socket = io('wss://ws.seudominio.com.br', {
  auth: { token: localStorage.getItem('access_token') },
})

ws: biblioteca WebSocket nativa (sem Socket.IO)

Para casos simples sem necessidade de salas ou fallback, a biblioteca ws é mais leve que Socket.IO:

typescript
// Servidor WebSocket com ws (mais leve que Socket.IO)
import { WebSocketServer, WebSocket } from 'ws'
import { createServer } from 'http'

const server = createServer()
const wss = new WebSocketServer({ server })

const clients = new Set<WebSocket>()

wss.on('connection', (ws, req) => {
  clients.add(ws)

  ws.on('message', (data) => {
    // Broadcast para todos os clientes conectados
    for (const client of clients) {
      if (client !== ws && client.readyState === WebSocket.OPEN) {
        client.send(data.toString())
      }
    }
  })

  ws.on('close', () => {
    clients.delete(ws)
  })

  // Ping/pong para manter conexão viva
  const pingInterval = setInterval(() => {
    if (ws.readyState === WebSocket.OPEN) ws.ping()
  }, 30000)

  ws.on('close', () => clearInterval(pingInterval))
})

server.listen(3001)

Não quer configurar manualmente?

Implante o VPS em menos de 5 minutos com a Runstack. Infraestrutura da OPEN Datacenter, com servidores em São Paulo.

Perguntas frequentes

ws: biblioteca servidor Node.js para WebSocket puro, sem extras, muito leve. Socket.IO: usa ws internamente, adiciona: rooms, namespaces, fallback para long-polling, reconexão automática, broadcast. WebSocket nativo do browser: API padrão para o cliente. Para chats e dashboards complexos: Socket.IO. Para streaming simples de dados: ws é mais leve.

Sim, e é obrigatório em produção. O protocolo é wss:// (WebSocket Secure) sobre TLS. Com Nginx como proxy SSL, o cliente conecta em wss://dominio.com.br e o Nginx termina o TLS, fazendo proxy para ws://container:3000 internamente. Nunca use ws:// em produção, dados em texto plano são interceptáveis.

Vertical (mais recursos na mesma VPS): simples, sem adapter Redis. Horizontal (múltiplos containers): necessita Redis adapter para sincronização. Para alta escala: use um balanceador com sticky sessions (Nginx ip_hash) para que um cliente sempre chegue no mesmo worker. Para máxima escala: arquitetura separada com servidor WebSocket dedicado.

Em Node.js, cada conexão WebSocket usa ~5-10 KB de RAM. Uma VPS de 2 GB pode suportar 50.000-100.000 conexões simultâneas (limitado pela RAM e pelo file descriptor limit do sistema). Para aumentar o limite de file descriptors: ulimit -n 100000 ou configure /etc/security/limits.conf.

io.engine.clientsCount mostra o total de clientes conectados. Para detalhes: io.sockets.sockets.size. Exponha via endpoint de métricas: app.get("/metrics", (req, res) => res.json({ ws_clients: io.engine.clientsCount })). Combine com Prometheus e Grafana para histórico e alertas.