WebSockets com Ratchet

1. Introdução ao WebSocket e Ratchet

WebSockets representam uma evolução significativa na comunicação web, permitindo conexões bidirecionais persistentes entre cliente e servidor. Diferentemente do HTTP tradicional, onde o cliente inicia requisições e aguarda respostas, o WebSocket estabelece um canal contínuo onde ambas as partes podem enviar dados a qualquer momento, sem overhead de headers HTTP em cada troca.

O Ratchet é uma biblioteca PHP que implementa o protocolo WebSocket de forma assíncrona, construída sobre o ReactPHP. Ela permite criar aplicações em tempo real sem depender de tecnologias como Node.js, mantendo todo o ecossistema PHP disponível.

Casos de uso típicos incluem:
- Sistemas de chat em tempo real
- Notificações push para dashboards administrativos
- Atualizações de dados financeiros ou de monitoramento
- Jogos multiplayer baseados em navegador
- Ferramentas de colaboração em tempo real

2. Instalação e Configuração do Ambiente

Requisitos mínimos:
- PHP 7.4 ou superior
- Composer instalado globalmente
- Extensões PHP: pcntl, sockets, mbstring

Instalação via Composer:

composer require cboden/ratchet

Estrutura básica de projeto:

meu-chat/
├── composer.json
├── src/
│   └── Chat.php
└── server.php

3. Componentes Fundamentais do Ratchet

O coração do Ratchet é a interface MessageComponentInterface, que define quatro métodos essenciais:

<?php
use Ratchet\MessageComponentInterface;
use Ratchet\ConnectionInterface;

class ChatServer implements MessageComponentInterface {
    protected $clients;

    public function __construct() {
        $this->clients = new \SplObjectStorage();
    }

    public function onOpen(ConnectionInterface $conn) {
        $this->clients->attach($conn);
        echo "Nova conexão: {$conn->resourceId}\n";
    }

    public function onMessage(ConnectionInterface $from, $msg) {
        foreach ($this->clients as $client) {
            if ($from !== $client) {
                $client->send($msg);
            }
        }
    }

    public function onClose(ConnectionInterface $conn) {
        $this->clients->detach($conn);
        echo "Conexão {$conn->resourceId} encerrada\n";
    }

    public function onError(ConnectionInterface $conn, \Exception $e) {
        echo "Erro: {$e->getMessage()}\n";
        $conn->close();
    }
}

O SplObjectStorage gerencia as conexões ativas, permitindo adicionar, remover e iterar sobre objetos de conexão de forma eficiente.

O servidor é iniciado combinando IoServer e HttpServer:

<?php
use Ratchet\Server\IoServer;
use Ratchet\Http\HttpServer;
use Ratchet\WebSocket\WsServer;

require 'vendor/autoload.php';

$server = IoServer::factory(
    new HttpServer(
        new WsServer(
            new ChatServer()
        )
    ),
    8080
);

$server->run();

4. Construindo um Servidor WebSocket Simples

Vamos implementar um servidor de chat funcional com broadcasting:

<?php
namespace MeuChat;

use Ratchet\MessageComponentInterface;
use Ratchet\ConnectionInterface;

class Chat implements MessageComponentInterface {
    protected $clients;
    private $usuarios = [];

    public function __construct() {
        $this->clients = new \SplObjectStorage();
    }

    public function onOpen(ConnectionInterface $conn) {
        $this->clients->attach($conn);
        $this->usuarios[$conn->resourceId] = 'Anônimo';

        $conn->send(json_encode([
            'tipo' => 'sistema',
            'mensagem' => 'Bem-vindo ao chat!'
        ]));

        $this->broadcast(json_encode([
            'tipo' => 'entrada',
            'mensagem' => "Usuário {$conn->resourceId} entrou no chat"
        ]), $conn);
    }

    public function onMessage(ConnectionInterface $from, $msg) {
        $dados = json_decode($msg, true);

        if (isset($dados['nome'])) {
            $this->usuarios[$from->resourceId] = $dados['nome'];
            return;
        }

        $resposta = json_encode([
            'tipo' => 'mensagem',
            'usuario' => $this->usuarios[$from->resourceId],
            'conteudo' => $dados['mensagem'],
            'timestamp' => date('H:i:s')
        ]);

        $this->broadcast($resposta);
    }

    public function onClose(ConnectionInterface $conn) {
        $this->clients->detach($conn);
        $nome = $this->usuarios[$conn->resourceId] ?? 'Anônimo';
        unset($this->usuarios[$conn->resourceId]);

        $this->broadcast(json_encode([
            'tipo' => 'saida',
            'mensagem' => "$nome saiu do chat"
        ]));
    }

    public function onError(ConnectionInterface $conn, \Exception $e) {
        echo "Erro: {$e->getMessage()}\n";
        $conn->close();
    }

    private function broadcast($mensagem, ?ConnectionInterface $excluir = null) {
        foreach ($this->clients as $client) {
            if ($excluir === null || $client !== $excluir) {
                $client->send($mensagem);
            }
        }
    }
}

5. Roteamento e Múltiplos Canais

O Ratchet permite rotear diferentes endpoints usando Router:

<?php
use Ratchet\Http\HttpServer;
use Ratchet\Server\IoServer;
use Ratchet\WebSocket\WsServer;
use Ratchet\Http\Router;
use React\EventLoop\Factory;
use React\Socket\Server;
use Symfony\Component\Routing\Route;
use Symfony\Component\Routing\RouteCollection;

$loop = Factory::create();

$chat = new ChatServer();
$notificacoes = new NotificationServer();

$routes = new RouteCollection();
$routes->add('chat', new Route('/chat', [
    '_controller' => new WsServer($chat)
]));
$routes->add('notificacoes', new Route('/notifications', [
    '_controller' => new WsServer($notificacoes)
]));

$router = new Router($routes);
$httpServer = new HttpServer($router);

$socket = new Server('0.0.0.0:8080', $loop);
$server = new IoServer($httpServer, $socket, $loop);

$loop->run();

6. Integração com Aplicações PHP Existentes

Autenticação via JWT:

<?php
use Firebase\JWT\JWT;
use Firebase\JWT\Key;

class SecureChat implements MessageComponentInterface {
    public function onOpen(ConnectionInterface $conn) {
        $queryString = $conn->httpRequest->getUri()->getQuery();
        parse_str($queryString, $params);

        if (!isset($params['token'])) {
            $conn->close();
            return;
        }

        try {
            $decoded = JWT::decode($params['token'], new Key('sua-chave-secreta', 'HS256'));
            $conn->usuarioId = $decoded->sub;
            $this->clients->attach($conn);
        } catch (\Exception $e) {
            $conn->close();
        }
    }
}

Envio de eventos do servidor HTTP para WebSocket:

<?php
// No Laravel, por exemplo
use Ratchet\Client\Connector;
use React\EventLoop\Factory;

$loop = Factory::create();
$connector = new Connector($loop);

$connector('ws://localhost:8080/notifications', [], ['Origin' => 'http://localhost'])
    ->then(function($conn) use ($evento) {
        $conn->send(json_encode([
            'tipo' => 'novo_pedido',
            'dados' => $evento->toArray()
        ]));
        $conn->close();
    }, function($e) {
        echo "Erro: {$e->getMessage()}\n";
    });

$loop->run();

7. Boas Práticas e Considerações de Performance

Gerenciamento de memória:
- Sempre remova conexões fechadas do SplObjectStorage
- Evite armazenar objetos grandes nas propriedades de conexão
- Use periodicamente gc_collect_cycles() em aplicações de longa duração

Escalabilidade:
- Para múltiplos servidores, utilize um mecanismo de pub/sub como Redis
- Configure balanceadores de carga com sticky sessions baseadas em IP
- Monitore o uso de memória e número de conexões ativas

Logging e monitoramento:

<?php
class LoggedChat implements MessageComponentInterface {
    private $logger;

    public function __construct() {
        $this->logger = new \Monolog\Logger('websocket');
        $this->logger->pushHandler(new \Monolog\Handler\StreamHandler('php://stdout'));
    }

    public function onOpen(ConnectionInterface $conn) {
        $this->logger->info('Nova conexão', [
            'id' => $conn->resourceId,
            'ip' => $conn->remoteAddress
        ]);
    }
}

8. Conclusão e Próximos Passos

O Ratchet oferece uma solução madura para implementar WebSockets em PHP, permitindo criar aplicações em tempo real sem abandonar o ecossistema PHP. Sua integração com frameworks como Laravel e Symfony é direta, e a arquitetura baseada em eventos facilita a manutenção e evolução do código.

Para projetos que exigem maior performance, considere alternativas como Swoole (extensão PHP nativa para async) ou ReactPHP (base do próprio Ratchet). Para equipes familiarizadas com JavaScript, Node.js com Socket.IO pode ser uma opção viável.

Recursos para aprofundamento incluem a documentação oficial do Ratchet, exemplos no GitHub e tutoriais sobre integração com bancos de dados e sistemas de fila.

Referências