Introdução
Os WebSockets permitem ligações bidirecionais e persistentes entre clientes e servidores, essenciais para funcionalidades em tempo real, como chat, notificações, atualizações em direto e edição colaborativa. Ao contrário do modelo pedido-resposta do HTTP, os WebSockets permitem que o servidor envie dados para os clientes instantaneamente. Este guia aborda padrões práticos de implementação de WebSockets.
Noções Básicas de WebSocket
Como Funcionam os WebSockets
- O cliente inicia um pedido de upgrade HTTP
- O servidor aceita e faz upgrade da ligação
- Inicia-se a comunicação bidirecional
- Qualquer uma das partes pode enviar mensagens a qualquer momento
- A ligação mantém-se até ser explicitamente encerrada
Quando Usar WebSockets
| Usar WebSockets | Usar HTTP/REST |
|---|
| Atualizações em tempo real | Dados em modelo pedido-resposta |
| Aplicações de chat | Operações CRUD |
| Notificações em direto | Uploads de ficheiros |
| Edição colaborativa | Atualizações pouco frequentes |
| Jogos | Conteúdo estático |
Node.js com Socket.IO
Configuração do Servidor
// server.js
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: 'http://localhost:3000',
methods: ['GET', 'POST'],
},
});
// Tratamento da ligação
io.on('connection', (socket) => {
console.log('Client connected:', socket.id);
// Tratar eventos do cliente
socket.on('message', (data) => {
console.log('Message received:', data);
// Difundir para todos os clientes
io.emit('message', data);
// Ou enviar para uma room específica
// io.to('room1').emit('message', data);
});
// Entrar na room
socket.on('join_room', (room) => {
socket.join(room);
console.log(`${socket.id} joined ${room}`);
socket.to(room).emit('user_joined', { userId: socket.id });
});
// Sair da room
socket.on('leave_room', (room) => {
socket.leave(room);
socket.to(room).emit('user_left', { userId: socket.id });
});
// Tratamento da desconexão
socket.on('disconnect', () => {
console.log('Client disconnected:', socket.id);
});
});
httpServer.listen(3001, () => {
console.log('Server running on port 3001');
});
Cliente (React)
// hooks/useSocket.js
import { useEffect, useState, useCallback } from 'react';
import { io } from 'socket.io-client';
const SOCKET_URL = 'http://localhost:3001';
export function useSocket() {
const [socket, setSocket] = useState(null);
const [isConnected, setIsConnected] = useState(false);
useEffect(() => {
const socketInstance = io(SOCKET_URL, {
transports: ['websocket'],
});
socketInstance.on('connect', () => {
setIsConnected(true);
console.log('Connected to server');
});
socketInstance.on('disconnect', () => {
setIsConnected(false);
console.log('Disconnected from server');
});
setSocket(socketInstance);
return () => {
socketInstance.disconnect();
};
}, []);
const sendMessage = useCallback((event, data) => {
if (socket) {
socket.emit(event, data);
}
}, [socket]);
return { socket, isConnected, sendMessage };
}
// components/Chat.jsx
import { useState, useEffect } from 'react';
import { useSocket } from '../hooks/useSocket';
export function Chat({ room }) {
const { socket, isConnected, sendMessage } = useSocket();
const [messages, setMessages] = useState([]);
const [input, setInput] = useState('');
useEffect(() => {
if (!socket) return;
// Entrar na room
socket.emit('join_room', room);
// Escutar mensagens
socket.on('message', (message) => {
setMessages((prev) => [...prev, message]);
});
socket.on('user_joined', ({ userId }) => {
setMessages((prev) => [...prev, { system: true, text: `${userId} joined` }]);
});
return () => {
socket.emit('leave_room', room);
socket.off('message');
socket.off('user_joined');
};
}, [socket, room]);
const handleSend = () => {
if (input.trim()) {
sendMessage('message', { room, text: input, timestamp: Date.now() });
setInput('');
}
};
return (
<div className="chat">
<div className="status">
{isConnected ? '🟢 Connected' : '🔴 Disconnected'}
</div>
<div className="messages">
{messages.map((msg, i) => (
<div key={i} className={msg.system ? 'system' : 'message'}>
{msg.text}
</div>
))}
</div>
<div className="input">
<input
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyPress={(e) => e.key === 'Enter' && handleSend()}
placeholder="Type a message..."
/>
<button onClick={handleSend}>Send</button>
</div>
</div>
);
}
PHP com Ratchet
Configuração do Servidor
composer require cboden/ratchet
// src/ChatServer.php
<?php
namespace App;
use Ratchet\MessageComponentInterface;
use Ratchet\ConnectionInterface;
class ChatServer implements MessageComponentInterface
{
protected $clients;
protected $rooms = [];
public function __construct()
{
$this->clients = new \SplObjectStorage;
}
public function onOpen(ConnectionInterface $conn)
{
$this->clients->attach($conn);
echo "New connection: {$conn->resourceId}\n";
}
public function onMessage(ConnectionInterface $from, $msg)
{
$data = json_decode($msg, true);
switch ($data['type']) {
case 'join':
$this->joinRoom($from, $data['room']);
break;
case 'message':
$this->broadcastToRoom($from, $data['room'], $data);
break;
}
}
protected function joinRoom(ConnectionInterface $conn, $room)
{
if (!isset($this->rooms[$room])) {
$this->rooms[$room] = new \SplObjectStorage;
}
$this->rooms[$room]->attach($conn);
// Notificar os restantes
foreach ($this->rooms[$room] as $client) {
if ($client !== $conn) {
$client->send(json_encode([
'type' => 'user_joined',
'userId' => $conn->resourceId,
]));
}
}
}
protected function broadcastToRoom(ConnectionInterface $from, $room, $data)
{
if (!isset($this->rooms[$room])) return;
$message = json_encode($data);
foreach ($this->rooms[$room] as $client) {
$client->send($message);
}
}
public function onClose(ConnectionInterface $conn)
{
$this->clients->detach($conn);
// Remover de todas as rooms
foreach ($this->rooms as $room => $clients) {
if ($clients->contains($conn)) {
$clients->detach($conn);
}
}
echo "Connection closed: {$conn->resourceId}\n";
}
public function onError(ConnectionInterface $conn, \Exception $e)
{
echo "Error: {$e->getMessage()}\n";
$conn->close();
}
}
// bin/server.php
<?php
require __DIR__ . '/../vendor/autoload.php';
use Ratchet\Server\IoServer;
use Ratchet\Http\HttpServer;
use Ratchet\WebSocket\WsServer;
use App\ChatServer;
$server = IoServer::factory(
new HttpServer(
new WsServer(
new ChatServer()
)
),
8080
);
echo "WebSocket server running on port 8080\n";
$server->run();
Laravel Broadcasting
Configuração
// config/broadcasting.php
'connections' => [
'pusher' => [
'driver' => 'pusher',
'key' => env('PUSHER_APP_KEY'),
'secret' => env('PUSHER_APP_SECRET'),
'app_id' => env('PUSHER_APP_ID'),
'options' => [
'cluster' => env('PUSHER_APP_CLUSTER'),
'useTLS' => true,
],
],
],
Difusão de Eventos
// app/Events/MessageSent.php
<?php
namespace App\Events;
use Illuminate\Broadcasting\Channel;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Broadcasting\PresenceChannel;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Queue\SerializesModels;
class MessageSent implements ShouldBroadcast
{
use InteractsWithSockets, SerializesModels;
public $message;
public $user;
public function __construct($message, $user)
{
$this->message = $message;
$this->user = $user;
}
public function broadcastOn()
{
return new PrivateChannel('chat.' . $this->message->room_id);
}
public function broadcastWith()
{
return [
'id' => $this->message->id,
'text' => $this->message->text,
'user' => [
'id' => $this->user->id,
'name' => $this->user->name,
],
'created_at' => $this->message->created_at->toIso8601String(),
];
}
}
Difusão a partir do Controller
// app/Http/Controllers/ChatController.php
public function sendMessage(Request $request)
{
$message = Message::create([
'room_id' => $request->room_id,
'user_id' => auth()->id(),
'text' => $request->text,
]);
broadcast(new MessageSent($message, auth()->user()))->toOthers();
return response()->json($message);
}
Cliente com Laravel Echo
import Echo from 'laravel-echo';
import Pusher from 'pusher-js';
window.Pusher = Pusher;
const echo = new Echo({
broadcaster: 'pusher',
key: process.env.PUSHER_APP_KEY,
cluster: process.env.PUSHER_APP_CLUSTER,
forceTLS: true,
});
// Escutar no canal privado
echo.private(`chat.${roomId}`)
.listen('MessageSent', (e) => {
console.log('New message:', e);
addMessage(e);
});
// Canal de presença (mostra quem está online)
echo.join(`chat.${roomId}`)
.here((users) => {
console.log('Users in room:', users);
})
.joining((user) => {
console.log('User joined:', user);
})
.leaving((user) => {
console.log('User left:', user);
});
Autenticação
Socket.IO com JWT
// Servidor
io.use((socket, next) => {
const token = socket.handshake.auth.token;
try {
const user = jwt.verify(token, process.env.JWT_SECRET);
socket.user = user;
next();
} catch (err) {
next(new Error('Authentication failed'));
}
});
// Cliente
const socket = io(SOCKET_URL, {
auth: {
token: localStorage.getItem('token'),
},
});
Escalabilidade com Redis
Socket.IO com Redis Adapter
import { createAdapter } from '@socket.io/redis-adapter';
import { createClient } from 'redis';
const pubClient = createClient({ url: 'redis://localhost:6379' });
const subClient = pubClient.duplicate();
await Promise.all([pubClient.connect(), subClient.connect()]);
io.adapter(createAdapter(pubClient, subClient));
Isto permite que vários servidores Socket.IO partilhem eventos.
Tratamento de Erros e Re-ligação
// Cliente
const socket = io(SOCKET_URL, {
reconnection: true,
reconnectionAttempts: 5,
reconnectionDelay: 1000,
reconnectionDelayMax: 5000,
timeout: 20000,
});
socket.on('connect_error', (error) => {
console.error('Connection error:', error);
});
socket.on('reconnect', (attemptNumber) => {
console.log('Reconnected after', attemptNumber, 'attempts');
});
socket.on('reconnect_failed', () => {
console.error('Reconnection failed');
// Mostrar erro ao utilizador
});
Boas Práticas
- Use rooms para mensagens direcionadas em vez de difundir para todos
- Implemente heartbeats para detetar ligações obsoletas
- Adicione autenticação para evitar acesso não autorizado
- Trate a re-ligação de forma adequada no cliente
- Use Redis adapter para escalabilidade horizontal
- Limite o tamanho das mensagens para evitar abusos
- Implemente rate limiting no envio de mensagens
Conclusão
Os WebSockets permitem comunicação em tempo real, essencial para aplicações modernas. Use Socket.IO para compatibilidade entre browsers, Laravel Broadcasting para aplicações Laravel, ou Ratchet para servidores PHP. Implemente autenticação adequada, tratamento de erros e estratégias de escalabilidade para uso em produção.