
Tre strade: Ratchet in puro Composer, i socket nativi senza dipendenze, oppure OpenSwoole come estensione PECL. Tutte richiedono un processo permanente, quindi un VPS o un container, mai un hosting condiviso. Il criterio di scelta misurato è la memoria per connessione: 22,4 KB con Ratchet, 5,3 KB in nativo, quasi nulla con OpenSwoole.
PHP sa tenere aperte delle connessioni WebSocket, a patto che tu accetti un modello di esecuzione che non ha per impostazione predefinita: un processo che gira in continuo invece di uno script che nasce e muore a ogni richiesta. Questo articolo costruisce un server di echo con i tre approcci disponibili nel 2026, Ratchet, i socket nativi, OpenSwoole, e li confronta su misure prese sulla stessa macchina.
Cosa sapere prima di iniziare
Un server WebSocket non si mette online come un sito PHP. Servono un processo permanente, un supervisore che lo riavvii quando cade e un server frontale che chiuda il TLS e inoltri la connessione. Su un hosting condiviso è quasi sempre impossibile: i processi di lunga durata sono vietati, e l’installazione di estensioni PECL pure. Serve un VPS o un container.
Secondo punto: un processo PHP che gira per giorni non ha più la rete di sicurezza del riavvio. Una perdita di memoria finisce per uccidere il servizio. Le variabili statiche, i log accumulati in memoria e le connessioni al database lasciate aperte diventano problemi veri.
Ratchet, l’approccio a libreria
Ratchet è la libreria WebSocket storica di PHP, costruita sul loop di eventi di ReactPHP. Si installa senza compilare niente.
composer require cboden/ratchetAttenzione ai namespace: i separatori sono backslash, e gli esempi che circolano in rete li perdono spesso nel passaggio da un editor all’altro. Senza, il codice solleva un errore fatale.
<?php
declare(strict_types=1);
namespace MyApp;
use Ratchet\ConnectionInterface;
use Ratchet\MessageComponentInterface;
use SplObjectStorage;
final class Chat implements MessageComponentInterface
{
private SplObjectStorage $clients;
public function __construct()
{
$this->clients = new SplObjectStorage();
}
public function onOpen(ConnectionInterface $conn): void
{
$this->clients->attach($conn);
echo "Connexion {$conn->resourceId}\n";
}
public function onMessage(ConnectionInterface $from, $msg): void
{
foreach ($this->clients as $client) {
if ($from !== $client) {
$client->send($msg);
}
}
}
public function onClose(ConnectionInterface $conn): void
{
$this->clients->detach($conn);
}
public function onError(ConnectionInterface $conn, \Throwable $e): void
{
error_log('WebSocket : ' . $e->getMessage());
$conn->close();
}
}La classe vive in src/ sotto il namespace MyApp: dichiaratelo a Composer, altrimenti server.php si ferma con Class "MyApp\Chat" not found.
{
"autoload": {
"psr-4": {
"MyApp\\": "src/"
}
}
}composer dump-autoloadIl tipo di $e merita una nota. In un file con namespace MyApp;, scrivere Exception $e indica \MyApp\Exception, una classe che non esiste, e la firma non corrisponde più all’interfaccia. Serve il backslash iniziale, \Throwable oppure \Exception.
<?php
declare(strict_types=1);
require __DIR__ . '/vendor/autoload.php';
use Ratchet\Server\IoServer;
use Ratchet\Http\HttpServer;
use Ratchet\WebSocket\WsServer;
use MyApp\Chat;
$server = IoServer::factory(
new HttpServer(new WsServer(new Chat())),
8080,
'0.0.0.0',
);
$server->run();php server.phpUna precisazione sull’invio a tutti i client: IoServer non ha nessun metodo broadcast(). Si verifica per introspezione.
$r = new ReflectionClass(Ratchet\Server\IoServer::class);
echo implode(', ', array_map(fn($m) => $m->getName(), $r->getMethods(ReflectionMethod::IS_PUBLIC)));
// __construct, factory, run, handleConnect, handleData, handleEnd, handleErrorL’invio a tutti si fa scorrendo a mano la collezione di connessioni, come in onMessage() qui sopra.
Lo stato del progetto
È il punto che dovrebbe pesare nella decisione. cboden/ratchet è alla versione 0.4.4, pubblicata il 14 dicembre 2021. ratchet/rfc6455, il suo cuore protocollare, è del 9 dicembre 2021. Quasi cinque anni senza una release. Le dipendenze ReactPHP, invece, sono aggiornate (react/socket 1.17.0, novembre 2025).
Conseguenza concreta su PHP 8.5: il semplice caricamento delle classi emette tre avvisi di deprecazione.
Deprecated: Ratchet\Server\IoServer::__construct(): Implicitly marking parameter
$loop as nullable is deprecated, the explicit nullable type must be used instead
Deprecated: Ratchet\Http\HttpServerInterface::onOpen(): Implicitly marking parameter
$request as nullable is deprecated, the explicit nullable type must be used instead
Deprecated: Ratchet\WebSocket\WsServer::onOpen(): Implicitly marking parameter
$request as nullable is deprecated, the explicit nullable type must be used insteadIl server funziona, ma sporca i log e niente garantisce che sopravviva a PHP 9. Se parti oggi con un progetto nuovo, guarda anche ReactPHP direttamente, o AmPHP: entrambi sono mantenuti attivamente.
Senza nessuna dipendenza
Il protocollo WebSocket sta in poco codice: un handshake HTTP, poi un formato di frame binario. Per un’esigenza semplice, scriverlo da sé evita di aggiungere tredici pacchetti al progetto.
<?php
declare(strict_types=1);
$serveur = stream_socket_server('tcp://0.0.0.0:8081', $errno, $errstr);
if (!$serveur) {
fwrite(STDERR, "Écoute impossible : {$errstr}\n");
exit(1);
}
stream_set_blocking($serveur, false);
$clients = [];
$prets = [];
function poigneeDeMain($socket): bool
{
$requete = fread($socket, 8192);
if (!$requete || !preg_match('#Sec-WebSocket-Key:\s*(\S+)#i', $requete, $m)) {
return false;
}
// La costante qui sotto è imposta dalla RFC 6455
$accept = base64_encode(sha1($m[1] . '258EAFA5-E914-47DA-95CA-C5AB0DC85B11', true));
fwrite($socket, "HTTP/1.1 101 Switching Protocols\r\n"
. "Upgrade: websocket\r\nConnection: Upgrade\r\n"
. "Sec-WebSocket-Accept: {$accept}\r\n\r\n");
return true;
}
function lireTrame($socket): ?string
{
$entete = fread($socket, 2);
if ($entete === false || strlen($entete) < 2) {
return null;
}
$second = ord($entete[1]);
$masquee = (bool) ($second & 0x80);
$taille = $second & 0x7F;
if ($taille === 126) {
$taille = unpack('n', fread($socket, 2))[1];
} elseif ($taille === 127) {
$taille = unpack('J', fread($socket, 8))[1];
}
$masque = $masquee ? fread($socket, 4) : '';
$donnees = '';
while (strlen($donnees) < $taille) {
$bloc = fread($socket, $taille - strlen($donnees));
if ($bloc === '' || $bloc === false) {
break;
}
$donnees .= $bloc;
}
if ($masquee) {
for ($i = 0; $i < strlen($donnees); $i++) {
$donnees[$i] = $donnees[$i] ^ $masque[$i % 4];
}
}
return $donnees;
}
function ecrireTrame($socket, string $charge): void
{
$taille = strlen($charge);
$trame = chr(0x81); // FIN + opcode testo
if ($taille < 126) {
$trame .= chr($taille);
} elseif ($taille < 65536) {
$trame .= chr(126) . pack('n', $taille);
} else {
$trame .= chr(127) . pack('J', $taille);
}
fwrite($socket, $trame . $charge);
}
while (true) {
$lecture = array_merge([$serveur], $clients);
$ecriture = $exception = null;
if (@stream_select($lecture, $ecriture, $exception, 0, 200000) === false) {
continue;
}
foreach ($lecture as $socket) {
if ($socket === $serveur) {
$nouveau = @stream_socket_accept($serveur, 0);
if ($nouveau) {
stream_set_blocking($nouveau, false);
$clients[(int) $nouveau] = $nouveau;
$prets[(int) $nouveau] = false;
}
continue;
}
$id = (int) $socket;
if (!($prets[$id] ?? false)) {
if (poigneeDeMain($socket)) {
$prets[$id] = true;
} else {
fclose($socket);
unset($clients[$id], $prets[$id]);
}
continue;
}
// Svuotare tutti i frame in attesa, non uno solo per giro di loop
stream_set_blocking($socket, true);
$ferme = false;
do {
$message = lireTrame($socket);
if ($message === null || $message === '') {
$ferme = true;
break;
}
ecrireTrame($socket, $message); // echo
$meta = stream_get_meta_data($socket);
} while (($meta['unread_bytes'] ?? 0) > 0);
stream_set_blocking($socket, false);
if ($ferme) {
fclose($socket);
unset($clients[$id], $prets[$id]);
}
}
}Il commento sullo svuotamento dei frame in attesa non è un dettaglio. La nostra prima versione trattava un solo messaggio per client e per giro di loop: il throughput misurato crollava, e la colpa era del nostro loop, non dei socket di PHP. È il tipo di errore che un banco di prova scopre e che una lettura del codice lascia passare.
Questo server resta volutamente incompleto. Non gestisce né i frame di controllo (ping, pong, close), né la frammentazione, né i payload binari, né TLS. Per un uso reale bisogna aggiungerli o passare a una libreria.
OpenSwoole
OpenSwoole è un’estensione in C che dà a PHP un vero server asincrono multiprocesso. Si installa con PECL e richiede quindi un accesso da amministratore alla macchina.
pecl install openswoole
docker-php-ext-enable openswoole # oppure extension=openswoole.so in php.ini<?php
declare(strict_types=1);
$serveur = new OpenSwoole\WebSocket\Server('0.0.0.0', 8082);
$serveur->set([
'worker_num' => 1,
'log_level' => OpenSwoole\Constant::LOG_ERROR,
]);
$serveur->on('message', function (OpenSwoole\WebSocket\Server $srv, $frame) {
$srv->push($frame->fd, $frame->data); // echo
});
$serveur->on('close', function () {});
$serveur->start();Il codice è il più corto dei tre, e l’invio a tutti i client arriva già dall’estensione. In cambio, l’estensione impone la sua presenza sulla macchina e il proprio modello di processi.
Il client, lato browser
const socket = new WebSocket('wss://exemple.com/ws');
socket.addEventListener('open', () => {
console.log('connexion ouverte');
socket.send('Bonjour serveur');
});
socket.addEventListener('message', (e) => {
console.log('reçu :', e.data);
});
socket.addEventListener('close', (e) => {
console.log('fermée', e.code, e.reason);
// riconnessione con attesa crescente
});
socket.addEventListener('error', () => {
console.error('erreur de transport');
});In produzione l’URL è in wss://, non in ws://: da una pagina HTTPS il browser rifiuta una connessione WebSocket in chiaro. E la riconnessione non è automatica: il codice qui sopra deve riaprire la connessione quando scatta close, con un’attesa che cresce a ogni tentativo.
Le misure
I tre server implementano lo stesso echo, sulla stessa macchina, ciascuno da solo durante la propria misura: un container limitato a 1 core e 512 MB, PHP 8.5.10, Docker su macOS. Il client è uno script PHP che parla il protocollo direttamente.
Memoria per connessione
È la misura più stabile e la più decisiva. Si aprono 500 connessioni, si tengono aperte, e si legge VmRSS del processo server prima e durante.
| Server | A riposo | Con 500 connessioni | Costo per connessione |
|---|---|---|---|
| Ratchet 0.4.4 | 23.700 KB | 34.940 KB | 22,4 KB |
| Socket nativi | 22.880 KB | 25.540 KB | 5,3 KB |
| OpenSwoole 26.2.0 | 26.512 KB | 26.140 KB | ≈ 0 KB |
Il dato di OpenSwoole non è un errore: l’estensione alloca la sua tabella di connessioni all’avvio, il che spiega insieme la base più alta e l’assenza di crescita dopo. Ratchet costa quattro volte di più per connessione dei socket nudi, il prezzo degli oggetti ReactPHP e PSR-7 impilati dietro ogni client. A 10.000 connessioni, lo scarto tra Ratchet e il nativo supera i 170 MB.
Latenza di andata e ritorno
50 client connessi, 800 messaggi da 64 byte inviati uno alla volta sul primo client, aspettando l’echo prima di mandare il successivo.
| Server | p50 | p95 | p99 | Connessione |
|---|---|---|---|---|
| Ratchet 0.4.4 | 0,27 ms | 1,88 ms | 3,90 ms | 1,83 ms |
| Socket nativi | 0,03 ms | 0,25 ms | 0,43 ms | 1,52 ms |
| OpenSwoole 26.2.0 | 0,03 ms | 0,75 ms | 3,02 ms | 0,76 ms |
Tutti e tre rispondono in meno di un millisecondo alla mediana. Lo scarto sulla coda della distribuzione dice molto più della mediana: è la coda che si sente davvero in un’interfaccia.
Throughput: la misura che non decide
50 client in andata e ritorno stretto per sei secondi, cinque passate per server, in messaggi scambiati al secondo.
| Server | Passate | Mediana |
|---|---|---|
| Ratchet 0.4.4 | 25.450 · 31.667 · 32.400 · 33.933 · 36.067 | 32.400 |
| Socket nativi | 25.258 · 27.375 · 33.425 · 35.975 · 46.417 | 33.425 |
| OpenSwoole 26.2.0 | 22.333 · 23.175 · 25.050 · 32.317 · 34.192 | 25.050 |
Gli intervalli si sovrappongono completamente: da 22.000 a 46.000 messaggi al secondo, tutti i server messi insieme, con punte del 40 % di scarto tra due passate dello stesso server. Questo banco di prova non permette di classificarli sul throughput. La macchina di test, una VM Docker su macOS che condivide i core con il resto del sistema, fa troppo rumore. Preferiamo dirlo piuttosto che pubblicare una classifica che la misura non sostiene. Su un server dedicato e con un generatore di carico distribuito lo scarto sarebbe forse netto, non l’abbiamo misurato.
Quello che si può affermare: tutti e tre reggono ampiamente diverse decine di migliaia di messaggi al secondo su un solo core. Per la quasi totalità delle applicazioni, il throughput non è il criterio di scelta.
Scegliere
| Ratchet | Socket nativi | OpenSwoole | |
|---|---|---|---|
| Installazione | Composer | niente | PECL, accesso root |
| Dipendenze aggiunte | 19 pacchetti | 0 | 1 estensione |
| Protocollo completo | sì | da scrivere | sì |
| Memoria / connessione | 22,4 KB | 5,3 KB | ≈ 0 |
| Ultima release | dic. 2021 | — | attiva |
| Avvisi di deprecazione su 8.5 | 3 | 0 | 0 |
| Hosting condiviso | no | no | no |
In pratica: OpenSwoole se controlli la macchina e punti a molte connessioni simultanee. I socket nativi per un’esigenza semplice e ben delimitata, o per capire il protocollo. Ratchet se vuoi restare in puro Composer, accettando una libreria ferma dal 2021, e in quel caso confrontala prima con ReactPHP o AmPHP, che sono mantenuti.
E prima di tutto: se il bisogno è spingere notifiche senza uno scambio bidirezionale intenso, i Server-Sent Events passano da HTTP ordinario, attraversano tutti i proxy senza configurazione e si riconnettono da soli. Spesso sono lo strumento giusto, e non richiedono nessuno di questi tre server.
Per gli scambi richiesta-risposta classici, una richiesta cURL resta lo strumento adatto. Lato client, il tutorial su come creare una chat online in JavaScript mostra l’altro capo della connessione. Il resto dei tutorial PHP è raccolto nell’hub Sviluppo web.
Errori frequenti
use RatchetServerIoServer; al posto di use Ratchet\Server\IoServer; produce Fatal error: Uncaught Error: Class "IoServer" not found, preceduto da quattro avvisi su use senza effetto.namespace MyApp;, scrivere Exception $e indica \MyApp\Exception. Servono \Throwable o \Exception.Implicitly marking parameter as nullable is deprecated al solo caricamento delle classi. Ultima release a dicembre 2021.wss://.

