WebSockets com ws e Socket.io

1. Introdução aos WebSockets no Ecossistema Node.js

WebSockets representam uma evolução significativa na comunicação web, permitindo conexões bidirecionais persistentes entre cliente e servidor. Diferentemente do HTTP/REST, onde o cliente sempre inicia a requisição e o servidor responde, os WebSockets estabelecem um canal aberto contínuo onde ambas as partes podem enviar dados a qualquer momento.

Casos de uso típicos incluem:
- Chats em tempo real: mensagens instantâneas sem polling
- Notificações ao vivo: alertas de sistemas, atualizações de status
- Jogos multiplayer: sincronização de estado entre jogadores
- Dashboards dinâmicos: atualização de gráficos e métricas

Duas bibliotecas dominam o ecossistema Node.js: ws (leve e minimalista) e Socket.io (rica em recursos como fallback, salas e reconexão automática).

2. Configuração do Ambiente e Primeiros Passos

Servidor com ws

Primeiro, instale o pacote:

npm install ws

Crie um servidor WebSocket mínimo:

const WebSocket = require('ws');

const wss = new WebSocket.Server({ port: 8080 });

wss.on('connection', (ws) => {
  console.log('Cliente conectado');

  ws.send('Bem-vindo ao servidor WebSocket!');

  ws.on('message', (message) => {
    console.log(`Recebido: ${message}`);
  });

  ws.on('close', () => {
    console.log('Cliente desconectado');
  });
});

console.log('Servidor WebSocket rodando em ws://localhost:8080');

Cliente no navegador

const socket = new WebSocket('ws://localhost:8080');

socket.onopen = () => {
  console.log('Conectado ao servidor');
  socket.send('Olá, servidor!');
};

socket.onmessage = (event) => {
  console.log(`Mensagem do servidor: ${event.data}`);
};

socket.onclose = () => {
  console.log('Conexão encerrada');
};

3. Comunicação Bidirecional com a Biblioteca ws

Envio e recebimento de mensagens JSON

const WebSocket = require('ws');
const wss = new WebSocket.Server({ port: 8080 });

wss.on('connection', (ws) => {
  ws.on('message', (data) => {
    try {
      const parsed = JSON.parse(data);
      console.log('Mensagem JSON recebida:', parsed);

      // Resposta em JSON
      ws.send(JSON.stringify({ status: 'ok', echo: parsed }));
    } catch (e) {
      ws.send(JSON.stringify({ error: 'Formato inválido' }));
    }
  });
});

Chat simples com broadcast

const WebSocket = require('ws');
const wss = new WebSocket.Server({ port: 8080 });

const clients = new Set();

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

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

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

4. Introdução ao Socket.io no Backend (Node.js)

Instalação e configuração básica

npm install socket.io express
const express = require('express');
const http = require('http');
const { Server } = require('socket.io');

const app = express();
const server = http.createServer(app);
const io = new Server(server, {
  cors: {
    origin: 'http://localhost:3000',
    methods: ['GET', 'POST']
  }
});

io.on('connection', (socket) => {
  console.log(`Usuário conectado: ${socket.id}`);

  socket.on('chat message', (msg) => {
    io.emit('chat message', { user: socket.id, message: msg });
  });

  socket.on('disconnect', () => {
    console.log(`Usuário desconectado: ${socket.id}`);
  });
});

server.listen(3001, () => {
  console.log('Servidor Socket.io rodando na porta 3001');
});

5. Socket.io no Frontend (React)

Instalação do cliente

npm install socket.io-client

Hook customizado useSocket

import { useEffect, useState } from 'react';
import { io } from 'socket.io-client';

const useSocket = (url = 'http://localhost:3001') => {
  const [socket, setSocket] = useState(null);
  const [connected, setConnected] = useState(false);

  useEffect(() => {
    const newSocket = io(url);

    newSocket.on('connect', () => setConnected(true));
    newSocket.on('disconnect', () => setConnected(false));

    setSocket(newSocket);

    return () => newSocket.close();
  }, [url]);

  return { socket, connected };
};

export default useSocket;

Componente de chat em tempo real

import React, { useState, useEffect } from 'react';
import useSocket from './useSocket';

const ChatRoom = () => {
  const { socket, connected } = useSocket();
  const [messages, setMessages] = useState([]);
  const [input, setInput] = useState('');

  useEffect(() => {
    if (!socket) return;

    socket.on('chat message', (data) => {
      setMessages((prev) => [...prev, data]);
    });

    return () => socket.off('chat message');
  }, [socket]);

  const sendMessage = (e) => {
    e.preventDefault();
    if (input.trim()) {
      socket.emit('chat message', input);
      setInput('');
    }
  };

  return (
    <div>
      <h2>Chat em Tempo Real</h2>
      <p>Status: {connected ? 'Conectado' : 'Desconectado'}</p>

      <div style={{ border: '1px solid #ccc', height: 300, overflow: 'auto', padding: 10 }}>
        {messages.map((msg, i) => (
          <div key={i}>
            <strong>{msg.user}:</strong> {msg.message}
          </div>
        ))}
      </div>

      <form onSubmit={sendMessage}>
        <input
          value={input}
          onChange={(e) => setInput(e.target.value)}
          placeholder="Digite sua mensagem..."
        />
        <button type="submit">Enviar</button>
      </form>
    </div>
  );
};

export default ChatRoom;

6. Recursos Avançados do Socket.io

Salas (rooms) para comunicação em grupo

// Servidor
io.on('connection', (socket) => {
  socket.join('sala-geral');

  socket.on('join-room', (room) => {
    socket.join(room);
    io.to(room).emit('message', `${socket.id} entrou na sala ${room}`);
  });

  socket.on('room-message', ({ room, message }) => {
    io.to(room).emit('message', { user: socket.id, message });
  });
});

Broadcast e emissão para todos exceto o emissor

// Envia para todos, exceto o remetente
socket.broadcast.emit('user-typing', { user: socket.id });

// Envia para uma sala específica, exceto o remetente
socket.to('sala-geral').emit('notification', 'Novo usuário entrou');

Reconexão automática (configuração do cliente)

const socket = io('http://localhost:3001', {
  reconnection: true,
  reconnectionAttempts: 10,
  reconnectionDelay: 1000,
  reconnectionDelayMax: 5000,
  timeout: 20000
});

7. Comparação e Boas Práticas

Quando usar ws vs Socket.io

Critério ws Socket.io
Performance Excelente (baixo overhead) Bom (ligeiramente maior)
Simplicidade Mínimo, cru Rico em recursos prontos
Fallback Nenhum (apenas WebSocket) Long-polling, FlashSocket
Reconexão Manual Automática
Salas/Namespaces Manual Nativo
Tamanho ~5KB ~50KB (cliente)

Segurança

// Validação de origem no Socket.io
const io = new Server(server, {
  cors: {
    origin: 'https://meudominio.com',
    credentials: true
  }
});

// Rate limiting básico
const rateLimit = new Map();

io.use((socket, next) => {
  const ip = socket.handshake.address;
  const now = Date.now();

  if (!rateLimit.has(ip)) {
    rateLimit.set(ip, []);
  }

  const timestamps = rateLimit.get(ip).filter(t => now - t < 1000);

  if (timestamps.length >= 5) {
    return next(new Error('Muitas conexões'));
  }

  timestamps.push(now);
  rateLimit.set(ip, timestamps);
  next();
});

Escalabilidade com Redis adapter

npm install @socket.io/redis-adapter redis
const { createClient } = require('redis');
const { Server } = require('socket.io');
const { createAdapter } = require('@socket.io/redis-adapter');

const pubClient = createClient({ url: 'redis://localhost:6379' });
const subClient = pubClient.duplicate();

const io = new Server(server);
io.adapter(createAdapter(pubClient, subClient));

8. Integração com Temas Vizinhos da Série

WebSockets com autenticação JWT

const jwt = require('jsonwebtoken');

io.use((socket, next) => {
  const token = socket.handshake.auth.token;

  try {
    const decoded = jwt.verify(token, process.env.JWT_SECRET);
    socket.userId = decoded.id;
    next();
  } catch (err) {
    next(new Error('Autenticação falhou'));
  }
});

io.on('connection', (socket) => {
  console.log(`Usuário ${socket.userId} conectado`);
});

Notificações em tempo real com Bull + Redis

const Queue = require('bull');
const notificationQueue = new Queue('notifications', 'redis://localhost:6379');

// Produtor
app.post('/api/notify', async (req, res) => {
  await notificationQueue.add({
    userId: req.body.userId,
    message: req.body.message
  });
  res.json({ success: true });
});

// Consumidor
notificationQueue.process(async (job) => {
  const { userId, message } = job.data;
  io.to(`user-${userId}`).emit('notification', message);
});

Testes de WebSockets com Jest

const io = require('socket.io-client');
const { createServer } = require('http');
const { Server } = require('socket.io');

describe('WebSocket Tests', () => {
  let server, clientSocket;

  beforeAll((done) => {
    const httpServer = createServer();
    const ioServer = new Server(httpServer);

    httpServer.listen(() => {
      const port = httpServer.address().port;
      clientSocket = io(`http://localhost:${port}`);
      clientSocket.on('connect', done);
    });

    server = { httpServer, ioServer };
  });

  afterAll(() => {
    clientSocket.close();
    server.httpServer.close();
  });

  test('deve receber mensagem de boas-vindas', (done) => {
    server.ioServer.on('connection', (socket) => {
      socket.emit('welcome', 'Conectado!');
    });

    clientSocket.on('welcome', (msg) => {
      expect(msg).toBe('Conectado!');
      done();
    });
  });
});

Referências