websocket chat-live

Creare una live chat in php con le Websocket e la libreria Ratchet

websocket
Che cosa sono i websocket?

Come sappiamo Internet è stata sviluppata secondo un'architettura client-server in modo che la connessione sia veloce e generalmente half-duplex, ossia il client richiede la connessione al server, invia la richiesta ed ottiene la risposta e stop.

Nell’evoluzione delle applicazioni web dal 2004 in poi, Ajax è diventato una tecnologia estremamente utile per recuperare i dati da un server in modo asincrono, cioè non contemporaneamente alla richiesta http che il browser esegue al momento dell'apertura di una pagina web.

Cosicché gli utenti che  precedentemente dovevano aggiornare o cambiare pagina per visualizzare il contenuto aggiornato, con ajax e con poco codice JavaScript si poteva effettuare la richesta asincorna al server, creando così applicazioni a singola pagina e RIA(rich internet application). HTTP è sempre rimasto quello che è, dato che è nato per supportare trasmissioni di pacchetti attraverso connessioni usa e getta.
Giacchè il limite di Ajax era quello del polling ossia che il client doveva sempre effettuare delle richieste cicliche al server, per mantenere aggornate automaticamente le pagine web, risultò non proprio una tecnologia ottimale per creare live chat o comunque sistemi live in quanto le risorse di banda risulterebbero limitate per l'eccesso di richieste nei casi di grosso traffico web, risultando quindi inefficiente.  Per tal motivo nascono le specifiche websocket già supportabili dal protocollo tcp/ip.

La specifica WebSocket è stata completata con l’avvento di HTML5 e di una serie di tecnologie ad esso correlate per l’open web. Ora è una specifica stabile e supportato da browser moderni come Chrome, Firefox, Safari e Internet Explorer 10.

La connessione continua TCP consente agli sviluppatori di realizzare giochi online molto reattivi e connessi in modo molto più efficiente, sia per l’utilizzo delle risorse di client e server sia in fase di sviluppo, utilizzando un flusso “nativo” invece di un sistema di interrogazione a polling.


Il modello RFC6455 

Tecnicamente parlando, un WebSocket è una connessione TCP persistente, bi-direzionale full-duplex, garantita da un sistema di handshaking client-key ed un modello di sicurezza origin-based. Il sistema inotre maschera le trasmissioni dati per evitare lo sniffing di pacchetti di testo in chiaro.

Vediamo cosa significa questa definizione punto per punto:

Caratteristica Descrizione
Bi-directional il client è connesso al server, e il server è connesso al client. Ciascuno può ricevere eventi come collegato e scollegato e possono inviare dati all’altro.
Full duplex il server e il client può inviare dati nello stesso momento senza collisioni.
TCP Protocollo sottostante tutte le comunicazioni Internet, che fornisce un meccanismo affidabile per trasportare un flusso di byte da un’applicazione all’altra.
Client-key handshake il client invia al server una chiave segreta di 16 byte con codifica base64. Il server poi aggiunge una stringa (anche detta “magic string” e specificata nel protocollo come “258EAFA5-E914-47DA-95CA-C5AB0DC85B11”) e rimanda il risultato, elaborato con SHA1, al client. In questo modo, il client può essere certo che il server cui aveva inviato la sua chiave è lo stesso che apre la connessione.
Sicurezza origin-based L’origine della richiesta WebSocket viene verificata dal server per determinare che provenga da un dominio autorizzato. Il server può rifiutare le connessioni socket da domini non attendibili.
Trasmissione dati mascherata Il cliente inva una chiave di 4 byte per l’offuscamento nella trama iniziale di ogni messaggio. Essa è utilizzata per effettuare uno XOR bit a bit tra dati e chiave. Ciò aiuta a prevenire lo sniffing di dati This helps to prevent data sniffing, poiché un malintenzionato dovrebbe essere capace di determinare il byte di inizio del messaggio per poterlo decrittare.

In altre parole questo sistema ci permette di utilizzare delle connessioni full-duplex ossia il server può inviare ai client connessi delle risposte senza che questi hanno inviato delle richieste.
Vediamo come implementare un server websocket con php e le librerie Ratchet.

Per prima cosa bisogna scaricare la libreria, e il modo migliore è quello di utilizzare composer, per chi non lo ha, consiglio di scaricarlo e seguire le istruzioni come riportato al link di sopra. Una volta scaricato composer basta seguire le istruzioni del link seguente:
https://socketo.me/docs/install

php ~/composer.phar require cboden/ratchet
oppure se hai installato o meno il comando composer
php composer require cboden/ratchet
Una volta scaricata la libreria, per la nostra prima applicazione chat bisogna creare due script:
Uno rappresenta il nostro entry point, il server che deve essere attivato, che salveremo nella cartella bin che dobbiamo creare all'interno della cartella scaricata con composer, dove è presente il file di configurazione composer.json. Questo script a sua volta richiama la classe che implementa i metodi principali di una chat che verrà salvata nella cartella src da creare allo stesso livello della cartella bin.
Il file composer.json va implementato per permettere l'autoload e il riconoscimento dei namespace secondo due metodiche standard Psr-0 o Psr-4.

Ecco le differenze principali:

1. Ad esempio se si definisce che lo spazio dei nomi di Acme\Foo\ è anchored in src/ ,

  • Con PSR-0 significa cercherà Acme\Foo\Bar in src/Acme/Foo/Bar.php
  • Mentre in PSR-4 lo cercherà in src/Bar.php .

2. PSR-4 non converte le sottolineature in separatori di directory

3. Non è possibile utilizzare PSR-4 se non si utilizzano gli spazi dei nomi

4. PSR-0 non funziona anche se il nome di classe è diverso dal nome del file, ad esempio considerando l'esempio precedente:

  • Acme\Foo\Bar ---> src/Acme/Foo/Bar.php (per la classe Bar) funzionerà
  • Acme\Foo\Bar ---> src/Acme/Foo/Bar2.php (per la classe Bar) non funziona
Quindi se qualora doveste avere problemi di riconoscimento delle classi dovete tenere a mente lo standard che state usando nel file composer.json.
come ad esempio:

{
    "autoload": {
        "psr-4": {
            "MyApp\\": "src"
        }
    },
    "require": {
        "cboden/ratchet": "^0.4"
    }
}
Il composer.json di sopra quindi ancorerà il namespace MyApp\ alla cartella src.
Implementando i 4 metodi: onOpen,onClose, onError,OnMessage
possiamo rispondere agi eventi che vengono innescati dall'apertura e chiusura della connessione, e dall'invio di un messaggio da parte di un client e da eventuali errori.


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

class Chat implements MessageComponentInterface {
    protected $clients;

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

    public function onOpen(ConnectionInterface $conn) {
        // Store the new connection to send messages to later
        $this->clients->attach($conn);

        echo "New connection! ({$conn->resourceId})\n";
    }

    public function onMessage(ConnectionInterface $from, $msg) {
        $numRecv = count($this->clients) - 1;
        echo sprintf('Connection %d sending message "%s" to %d other connection%s' . "\n"
            , $from->resourceId, $msg, $numRecv, $numRecv == 1 ? '' : 's');

        foreach ($this->clients as $client) {
            if ($from !== $client) {
                // The sender is not the receiver, send to each client connected
                $client->send($msg);
            }
        }
    }

    public function onClose(ConnectionInterface $conn) {
        // The connection is closed, remove it, as we can no longer send it messages
        $this->clients->detach($conn);

        echo "Connection {$conn->resourceId} has disconnected\n";
    }

    public function onError(ConnectionInterface $conn, \Exception $e) {
        echo "An error has occurred: {$e->getMessage()}\n";

        $conn->close();
    }
}
Questi metodi sono già sufficienti per creare un sistema di messaggistica live.
Se diamo un'occhiata veloce vediamo che l'oggetto SplObjectStorage gestisce le connessioni al server, facendo l'attach o il detach rispettivamente alle varie connessioni in apertura e chiusura.
Si può osservare inoltre nell'evento onMessage, quando il server riceve il messaggio da parte di un client connesso rimanda, il messaggio stesso a tutti client  conessi facendo il foreach dell'oggetto di cui sopra.
Questo script verrà posizionato nella cartella src e verrà chiamato Chat.php, ma per lo standard psr-4  dovrebbe andare bene anche chat.php. 

Adesso creiamo lo script del server vero e proprio che posizioneremo nella cartella bin che chiameremo come vogliamo, ad esempio, chat-server.php


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

    require dirname(__DIR__) . '/vendor/autoload.php';

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

    $server->run();
Questo script richiama lo autoload.php che permetterà di caricare le classi dinamicamente e in automatico, poi possiamo vedere che viene istanziato un oggetto dal metodo statico dello IoServer, factory che a sua volta riceve come parametri oggetto httpServer il quale riceve WsServer(web socket server),ed infine l'oggetto della nostra classe Chat. Istanziato l'oggetto server, con il metodo run() viene messo in ascolto. Per attivarlo bisognerà caricarlo attraverso la CLI, cioè l'interfaccia a linea di comando o da terminale tramite il comando php, come il seguente:

$ php bin/chat-server.php
Una volta eseguito lo script questo risulterà in ascolto, fino all'interruzione dello stesso, potete anche creare un demone per controllare lo script e mantenerlo attivo o chiuderlo attraverso i comandi, un esempio può essere fatto seguendo la guida seguente: https://code.i-harness.com/it/q/1f13ae.

A questo punto come facciamo a  connetterci al server?

Semplice per testarlo basta solo aprire più browser, andare nella loro console , l'interfaccia a linea di comando, ed inserire il codice simile a quello che segue, che non fa altro che creare la connessione con il server websocket creando l'oggetto javascript WebSocket ed indicando l'host o l'ip del server con la relativa porta, ad esempio come segue, se il server sta sul locale e sulla porta 8080. Inoltre impostiamo anche la risposta all'evento onmessage sulla console.
var conn = new WebSocket('ws://localhost:8080');
conn.onopen = function(e) {
    console.log("Connection established!");
};

conn.onmessage = function(e) {
    console.log(e.data);
};

A questo punto riceveremo su ciascuna console il messaggio di connessione avvenuta o il messaggio di errore.
Se la connessione è stabilita basta scrivere da una delle console aperte il seguente comando:
conn.send('Hello World!');

In questo modo verrà inviato un messaggio da una console che sarà recapitato a tutte le altre console connesse allo stesso server.