Le protocole WebSocket permet une communication bidirectionnelle et full-duplex entre un client et un serveur. La bibliothèque `Hoa\Websocket` permet de créer des serveurs et des clients WebSocket.

## Table des matières

## Introduction

Le protocole WebSocket est standardisé dans la RFC6455. Il permet à un client et à un serveur de communiquer ensemble. Cette communication est bidirectionnelle, cela signifie que le client peut envoyer des messages au serveur, et inversement. Le serveur n'envoie pas uniquement des réponses il peut envoyer un message spontanément. Cela change des habitudes du Web et de son protocole HTTP. Le protocole WebSocket est également full-duplex, c'est à dire que les données sont échangées simultanément dans les deux sens : ce n'est pas parce que le serveur a envoyé une donnée qui est en cours d'acheminement que le client ne peut pas envoyer de données à son tour. Le protocole WebSocket permet alors une forte interactivité entre le client et le serveur. Le client sera très souvent un navigateur. Notons que le schéma URI (voir la RFC3986) du protocole WebSocket est `ws://`.

Nous pouvons nous demander quelles sont les différences entre WebSocket et EventSource. Ces deux solutions sont en fait fondamentalement différentes : WebSocket permet une communication bidirectionnelle et full-duplex, alors que EventSource est une technologie basée sur le protocole HTTP et ne propose qu'une communication unidirectionnelle. Pour cet usage, un serveur EventSource est plus léger, plus simple et conçu pour être robuste aux déconnexions (voir la bibliothèque `Hoa\Eventsource`).

Le protocole WebSocket commence par une phase de handshake afin de permettre, par la suite, les échanges de messages sous forme de frames.

### Handshake et challenge

Pour démarrer une communication avec le protocole WebSocket, le client doit envoyer une requête HTTP au serveur en lui demandant de changer de protocole. Dans cette requête, le client insère un challenge, une sorte de petite énigme, que le serveur doit résoudre. S'il l'a résolu correctement, alors la communication démarrera.

Le fait de commencer par une requête HTTP n'est pas anodin. Cela permet au protocole WebSocket d'emprunter le même chemin que les requêtes HTTP, et ainsi, par exemple, traverser les proxys, les pare-feu etc. Cela facilite également le déploiement de ce protocole : pas besoin de lui réserver un port particulier, pas besoin d'avoir une configuration serveur particulière etc. Cela permet enfin d'utiliser une connexion sécurisée, à travers TLS. Dans ce cas, nous utilisons le schéma URI `wss://`.

### Frame et opcode

Les messages qui sont échangés entre le client et le serveur ne se font pas verbatim. En réalité, le message est encapsulé dans une frame : un paquet de bits qui a une forme particulière. Dans ce cas, nous trouverons des informations concernant le type du message, sa taille, des codes de vérifications etc. Nous trouverons un schéma explicatif dans la section 5.2, Base Framing Protocol de la spécification du protocole pour les plus curieux.

Le type du message est appelé opcode. C'est l'information la plus importante. Nous retrouverons ce terme dans ce chapitre plusieurs fois. Des constantes pour chaque opcode existent dans la classe `Hoa\Websocket\Connection` afin de simplifier leur utilisation. C'est cette classe qui s'assure du support du protocole.

### Historique

Il existe deux versions du protocole WebSocket dans la nature : la version standard et la version non-standard. La version standard est celle décrite dans la RFC6455. La dernière version non-standard porte le petit nom de draft-ietf-hybi-thewebsocketprotocol-00 (ou draft-hixie-thewebsocketprotocol-76), abrégé Hybi00. Cette version non-standard a plusieurs problèmes de sécurité importants mais elle est utilisée dans des langages comme Flash. Heureusement, elle disparaît de plus en plus et laisse la place à la RFC6455.

La bibliothèque `Hoa\Websocket` supporte ces deux versions. Elle permet à des clients supportant des versions différentes du protocole de communiquer quand même.

## Écrire un serveur

La classe `Hoa\Websocket\Server` permet d'écrire un serveur manipulant le protocole WebSocket. Cette classe hérite de `Hoa\Websocket\Connection`. La communication s'effectue à travers un serveur de socket. Nous utiliserons la classe `Hoa\Socket\Server` (de la bibliothèque `Hoa\Socket`) pour remplir ce rôle.

Le protocole WebSocket fonctionne en TCP, ainsi nous allons démarrer un serveur WebSocket en local sur le port 8889 :

    $server = new Hoa\Websocket\Server(
        new Hoa\Socket\Server('tcp://127.0.0.1:8889')
    );

Toutefois, nous pouvons utiliser l'URI `ws://127.0.0.1:8889` directement à la place de `tcp://127.0.0.1:8889`. Cela a un avantage lorsque nous utilisons `wss://` pour une connexion sécurisée car `Hoa\Websocket` saura que la connexion devra être sécurisée et le fera à votre place. Vous n'aurez pas à manipuler TLS, activer le cryptage sur certaines connections etc. Ainsi :

    $server = new Hoa\Websocket\Server(
        new Hoa\Socket\Server('ws://127.0.0.1:8889')
    );

Maintenant, voyons comment interagir avec ce serveur.

### Écouteurs

La classe `Hoa\Websocket\Connection` propose six écouteurs :

- `open`, quand une connexion est ouverte ;
- `message`, quand un message est reçu ;
- `binary-message`, quand un message binaire est reçu ;
- `ping`, quand un ping est reçu
- `error`, quand une erreur s'est produite ;
- `close`, quand une connexion se ferme.

Pour les écouteurs `message` et `binary-message`, il n'y a qu'une seule donnée associée : `message`, qui contient sans surprise le message reçu.

Pour l'écouteur `ping`, nous trouvons aussi la donnée `message`. Notons que le pong se fait automatiquement avant de déclencher l'écouteur.

Pour l'écouteur `error`, nous trouvons la donnée `exception` qui contient une exception (pas nécessairement `Hoa\Websocket\Exception\Exception`, cela peut-être par exemple `Hoa\Socket\Exception`). L'écouteur est déclenché après que la connexion ait été fermée.

L'écouteur `close` a deux données associées : `code` pour le code et `reason` qui explique la raison de cette fermeture avec un message court. Nous trouverons les codes de fermetures standards sous forme de constantes `CLOSE_*` dans la classe `Hoa\Websocket\Connection`. Par exemple, `Hoa\Websocket\Connection::CLOSE_NORMAL` symbolise une fermeture de connexion normale, sans erreur, alors que `Hoa\Websocket\Connection::CLOSE_MESSAGE_ERROR` symbolise une fermeture de connexion suite à un message mal formé. Cet écouteur est déclenché après que la connexion ait été fermée.

### Échanges de messages

Complétons notre exemple pour, dans l'écouteur `message`, renvoyer au client tous les messages qu'il nous envoie de façon à créer un écho. Pour cela, nous allons utiliser la méthode `Hoa\Websocket\Connection::send`. Une fois que notre écouteur est positionné, nous pouvons démarrer le serveur à l'aide de la méthode `Hoa\Websocket\Connection::run`. Ainsi :

    $server->on('message', function (Hoa\Event\Bucket $bucket) {
        $data = $bucket->getData();

        echo 'message: ', $data['message'], "\n";
        $bucket->getSource()->send($data['message']);

        return;
    });

    $server->run();

Nous allons maintenant tester notre serveur en créant un client HTML très simple :

    &lt;input type="text" id="input" placeholder="Message…" />
    &lt;hr />
    &lt;pre id="output">&lt;/pre>

    &lt;script>
      var host   = 'ws://127.0.0.1:8889';
      var socket = null;
      var input  = document.getElementById('input');
      var output = document.getElementById('output');
      var print  = function (message) {
          var samp       = document.createElement('samp');
          samp.innerHTML = message + '\n';
          output.appendChild(samp);

          return;
      };

      input.addEventListener('keyup', function (evt) {
          if (13 === evt.keyCode) {
              var msg = input.value;

              if (!msg) {
                  return;
              }

              try {
                  socket.send(msg);
                  input.value = '';
                  input.focus();
              } catch (e) {
                  console.log(e);
              }

              return;
          }
      });

      try {
          socket = new WebSocket(host);
          socket.onopen = function () {
              print('connection is opened');
              input.focus();

              return;
          };
          socket.onmessage = function (msg) {
              print(msg.data);

              return;
          };
          socket.onclose = function () {
              print('connection is closed');

              return;
          };
      } catch (e) {
          console.log(e);
      }
    &lt;/script>

À la ligne 6, nous déclarons l'adresse du serveur WebSocket en utilisant le protocole `ws`. À la ligne 45, nous utilisons l'objet `WebSocket`, et nous lui attachons des écouteurs, fortement semblables à ceux de `Hoa\Websocket\Connection` !

Pour tester, il suffit de démarrer le serveur :

    $ php Server.php

Puis, d'ouvrir le client avec son navigateur préféré. Chaque message envoyé au serveur nous revient à l'identique, nous avons bien un écho.

### Diffusions de messages

Pour l'instant, le client parle avec le serveur et le serveur lui répond, mais ça ne reste qu'un dialogue. Le serveur a pourtant toutes les connexions en mémoire. Nous sommes donc capable de diffuser un message à tous les clients connectés. Pour cela, nous allons utiliser la méthode `Hoa\Websocket\Connection::broadcast` qui va envoyer un message à tous les autres clients connectés, ainsi :

    $server->on('message', function (Hoa\Event\Bucket $bucket) {
        $data = $bucket->getData();

        echo 'message: ', $data['message'], "\n";
        $bucket->getSource()->broadcast($data['message']);

        return;
    });

Et voilà ! C'est aussi simple que ça. Redémarrons le serveur, et ouvrons plusieurs clients. Chaque message envoyé sera diffusé à tous les autres ! Notre exemple est devenu un outil de messagerie instantannée.

Il faut comprendre que le serveur de socket `Hoa\Socket\Server` travaille avec des nœuds, c'est à dire un objet qui représente une connexion ouverte. Dans un écouteur, pour connaître le nœud courant qui a déclenché l'appel à cet écouteur, nous devons appeler la méthode `Hoa\Websocket\Connection::getConnection` pour obtenir le serveur de socket, puis `Hoa\Socket\Server::getCurrentNode`. Similairement, nous avons la méthode `Hoa\Socket\Server::getNodes` pour obtenir tous les nœuds. La méthode `Hoa\Websocket\Connection::broadcast` vient en réalité de la bibliothèque `Hoa\Socket` et cache cette complexité. Il est préférable d'utiliser cette méthode pour des raisons de performance et de compatibilité.

### Fermeture

Pour fermer la connexion avec le client, nous utilisons la méthode `Hoa\Websocket\Connection::close`. Elle est très similaire à `Hoa\Websocket\Connection::send`. Ses arguments sont :

- `code` : le code de fermeture, voir les constantes `Hoa\Websocket\Connection::CLOSE_*` (`CLOSE_NORMAL` par défaut) ;
- `reason` : un message court expliquant la raison de la fermeture (`null` par défaut) ;
- `node` : le nœud qui va fermer la connexion (`null`, par défault, indique le nœud courant).

Par exemple, quand nous recevons le message `I love you`, nous fermerons la connexion en expliquant pourquoi, sinon nous faisons un simple écho du message :

    $server->on('message', function (Hoa\Event\Bucket $bucket) {
        $data = $bucket->getData();

        if ('I love you' === $data['message']) {
            $bucket->getSource()->close(
                Hoa\Websocket\Connection::CLOSE_NORMAL,
                'Thank you but my heart is already taken, bye bye!'
            );

            return;
        }

        $bucket->getSource()->send($data['message']);

        return;
    });

Nous pouvons modifier notre client pour qu'il nous affiche le code et la raison d'une fermeture :

          socket.onclose = function (e) {
              print(
                  'connection is closed (' + e.code + ' ' +
                  (e.reason || '—no reason—') + ')'
              );

              return;
          };

Il est préférable de toujours utiliser cette méthode pour fermer une connexion plutôt que de fermer directement la connexion TCP.

## Message

Nous avons deux façons d'envoyer des messages : soit en un seul morceau si nous avons le message en entier, soit en plusieurs morceaux. Notre message peut aussi contenir autre chose que du texte, il peut contenir une donnée binaire. Dans ce cas, nous parlons de message binaire.

### Fragmentation

Pour envoyer un message en un seul bloc, nous utilisons la méthode `Hoa\Websocket\Connection::send` comme nous l'avons vu dans les sections précédentes :

    $server->on('message', function (Hoa\Event\Bucket $bucket) {
        $bucket->getSource()->send('foobar');

        return;
    });

Cette méthode comporte en réalité quatre arguments :

- `message` : le message ;
- `node` : le nœud qui va envoyer le message (`null`, par défaut, indique le nœud courant) ;
- `opcode` : l'opcode, c'est à dire le type de la frame (`Hoa\Websocket\Connection::OPCODE_TEXT_FRAME` par défaut) ;
- `fin` : indique si le message est terminé ou pas (`true` par défaut).

Nous allons utiliser tous les arguments en essayant d'envoyer un message fragmenté.

Dans notre exemple, nous avons envoyé un message en entier, ce qui est le cas le plus courant. Si nous envoyons un très long message, nous utiliserons également cette même méthode. Toutefois, il peut arriver que nous ayons le message morceau après morceau et nous sommes alors incapable de l'envoyer en entier. Par exemple, si le message, de taille indéterminée, est lu sur un flux et que nous voulons ensuite l'envoyer au client, nous n'allons pas attendre d'avoir tout le message : nous allons envoyer chaque morceau directement au client. Dans ce cas, nous parlons de messages fragmentés.

Nous allons utiliser les deux opcodes suivants : `OPCODE_TEXT_FRAME` pour le premier fragment, puis `OPCODE_CONTINUATION_FRAME` pour tous les suivants. À chaque fois, nous allons préciser que le message n'est pas terminé à l'aide de l'argument `fin` qui sera à `false`, sauf pour le dernier fragment où `fin` sera à `true`.

L'utilisateur final derrière le client ne recevra pas des messages fragmentés, mais le message en entier une fois que le dernier fragment aura été reçu. Côté serveur, cela nous évite de surcharger la mémoire avec des données « en transit » et aussi de surcharger le réseau avec un gros message. Nous envoyons les données dès que nous les avons et c'est le client qui s'occupe de reconstituer le message. Le serveur opère de la même façon lorsqu'il reçoit un message fragmenté. Entre deux fragments, le serveur peut aussi traiter d'autres tâches. Il est donc plus intéressant d'utiliser les fragments plutôt que de temporiser le message.

Passons à un exemple. Nous allons envoyer le message `foobarbaz` fragmenté en trois parties. Nous pouvons imaginer que nous lisons ces données sur une socket par exemple, et que les données viennent au fur et à mesure. Ainsi :

    $server->on('message', function (Hoa\Event\Bucket $bucket) {
        $self = $bucket->getSource();

        $self->send(
            'foo',
            null,
            Hoa\Websocket\Connection::OPCODE_TEXT_FRAME,
            false // not the end…
        );
        echo 'sent foo', "\n";
        sleep(1);

        $self->send(
            'bar',
            null,
            Hoa\Websocket\Connection::OPCODE_CONTINUATION_FRAME,
            false // not the end…
        );
        echo 'sent bar', "\n";
        sleep(1);

        $self->send(
            'baz',
            null,
            Hoa\Websocket\Connection::OPCODE_CONTINUATION_FRAME,
            true // the end!
        );
        echo 'sent baz, over', "\n";

        return;
    });

Les instructions `sleep` permettent d'émuler une latence réseau ou quelque chose du genre. À chaque appel de la méthode `send`, les données sont effectivement envoyées au client, ce n'est pas un tampon côté serveur.

### Encodage

Tous les messages échangés doivent être au format UTF-8 (voir la RFC3629). Si les messages provenant du client ne sont pas conformes, `Hoa\Websocket\Connection` fermera la connexion de façon appropriée avec le code `Hoa\Websocket\Connection::CLOSE_MESSAGE_ERROR`, nous n'avons rien à faire de spécial. Par conséquent, tous les messages reçus dans nos écouteurs sont au bon encodage.

En revanche, `Hoa\Websocket\Connection` vérifie que les messages à destination du client sont dans le bon encodage. Si l'encodage n'est pas approprié, alors une exception `Hoa\Websocket\Exception\InvalidMessage` sera levée, ce qui fermera la connexion et déclenchera l'écouteur `error` si elle n'est pas capturée à temps.

### Binaire

Il est également possible d'envoyer des données binaires, bien plus compactes que des données textuelles. Nous parlons alors de messages binaires. Nous allons toujours utiliser la méthode `Hoa\Websocket\Connection::send` mais avec l'opcode `OPCODE_BINARY_FRAME`. Cela n'a de sens que dans l'écouteur `binary-message`, c'est à dire dans un « échange binaire » entre le client et le serveur. Nous pouvons imaginer le client qui envoie des coordonnées et le serveur qui lui en redonne d'autres (échange fort probable pour un jeu de plateau par exemple) :

    $server->on('binary-message', function (Hoa\Event\Bucket $bucket) {
        $data                          = $bucket->getData();
        $message                       = $data['message'];
        $point                         = [];
        list($point['x'], $point['y']) = array_values(unpack('nx/ny', $message));

        // compute a next point.

        $bucket->getSource()->send(
            pack('nn', $point['x'], $point['y']),
            null,
            Hoa\Websocket\Connection::OPCODE_BINARY_FRAME
        );

        return;
    });

Les fonctions `pack` et `unpack` seront des alliés précieux dans ce cas.

Notons que les messages binaires peuvent également être fragmentés. Il faut utiliser l'opcode `OPCODE_BINARY_FRAME` à la place de `OPCODE_TEXT_FRAME` puis continuer avec `OPCODE_CONTINUATION_FRAME` comme nous l'avons appris.

## Écrire un client

La classe `Hoa\Websocket\Client` permet d'écrire un client manipulant le protocole WebSocket. Cette classe hérite de `Hoa\Websocket\Connection`, tout comme `Hoa\Websocket\Server`. La communication s'effectue à travers un client de socket. Nous utiliserons la classe `Hoa\Socket\Client` (de la bibliothèque `Hoa\Socket`) pour remplir ce rôle.

Autant `Hoa\Websocket\Server` est capable de traiter avec des clients supportant plusieurs versions du protocole WebSocket, autant `Hoa\Websocket\Client` utilise uniquement le protocole de la RFC6455. C'est à dire que le client n'est capable de parler qu'avec un serveur supportant la RFC6455.

### Démarrer une connexion avec le serveur

Comme pour le serveur, le client hérite de `Hoa\Websocket\Connection`. Nous retrouvons alors les mêmes méthodes `send`, `close`, `run` etc., ainsi que les mêmes écouteurs. Et comme pour le serveur, les écouteurs doivent être positionnés sur le client.

Ainsi, pour démarrer un client, nous écrirons :

    $client = new Hoa\Websocket\Client(
        new Hoa\Socket\Client('ws://127.0.0.1:8889')
    );
    $client->on('message', function (Hoa\Event\Bucket $bucket) {
        $data = $bucket->getData();
        echo 'received message: ', $data['message'], "\n";

        return;
    });

Le client peut fonctionner en mode loop, comme le serveur, avec la méthode `run`. Dans ce cas, nous devrons écrire :

    $client->run();

Ou alors, pour un échange séquentiel, nous devons appeler manuellement la méthode `Hoa\Websocket\Client::connect` :

    $client->connect();

Si le serveur ne supporte pas le bon protocole, une exception `Hoa\Websocket\Exception\BadProtocol` sera levée.

### Échanges et diffusions de messages

Pour envoyer un message, nous utiliserons la méthode `Hoa\Websocket\Connection::send`. Son fonctionnement a été décrit précédemment pour le serveur. Il est identique.

A contrario, pour recevoir un message, nous utiliserons la méthode `Hoa\Websocket\Client::receive`. Les messages reçus du serveur vont déclencher les écouteurs. Ainsi, nous allons envoyer un message au serveur, puis nous allons attendre une réponse, ceci deux fois de suite :

    $client->send('foobar');
    $client->receive();

    $client->send('bazqux');
    $client->receive();

    $client->close();

La méthode `Hoa\Websocket\Client::receive` n'a aucun argument.

### Précision sur le handshake et l'hôte

Pour que le handshake soit complet, il est nécessaire d'envoyer l'en-tête HTTP `Host`, représentant le nom de l'hôte. Lorsque le client est exécuté à travers un serveur HTTP, l'hôte de ce serveur sera utilisé s'il est disponible. Sinon, s'il n'est pas disponible, ou si nous exécutons le client en ligne de commande par exemple, nous devons préciser un hôte avec la méthode `Hoa\Websocket\Client::setHost`, avant de connecter le client (avant l'appel de `Hoa\Websocket\Connection::run` ou `Hoa\Websocket\Client::connect`) ; ainsi :

    $client->setHost('hoa-project.net');
    $client->connect();

    // …

Pour savoir si l'hôte est connu, nous pouvons utiliser la méthode `Hoa\Websocket\Client::getHost`. Elle retournera `null` si le nom de l'hôte est introuvable. Ou sinon, au moment du handshake, une exception `Hoa\Websocket\Exception\Exception` sera levée.

## Nœud personnalisé

Les classes `Hoa\Socket\Server` et `Hoa\Socket\Client` travaillent avec des nœuds : des objets qui représentent une connexion ouverte. La classe de base pour représenter un nœud est `Hoa\Socket\Node`. La bibliothèque `Hoa\Websocket` propose son propre nœud : `Hoa\Websocket\Node`. Nous pouvons encore étendre cette classe pour ajouter et manipuler des informations sur une connexion.

Par exemple, dans le cas d'une messagerie, nous pourrions stocker le pseudo du client :

    class ChatNode extends Hoa\Websocket\Node
    {
        protected $_pseudo = null;

        public function setPseudo ($pseudo)
        {
            $old           = $this->_pseudo;
            $this->_pseudo = $pseudo;

            return $old;
        }

        public function getPseudo()
        {
            return $this->_pseudo;
        }
    }

Pour préciser au serveur ou au client de socket d'utiliser notre classe de nœud, nous devons utiliser la méthode `Hoa\Socket\Server::setNodeName` ou `Hoa\Socket\Client::setNodeName` de cette manière :

    $server = new Hoa\Websocket\Server(
        new Hoa\Socket\Server('ws://127.0.0.1:8889')
    );
    $server->getConnection()->setNodeName('ChatNode');

Et après, dans nos écouteurs, nous pourrons utiliser notre méthode `getPseudo` par exemple :

    $server->on('message', function (Hoa\Event\Bucket $bucket) {
        $node = $bucket->getSource()->getConnection()->getCurrentNode();

        var_dump($node->getPseudo());

        // …
    });

Si vous mettez en place un protocole en utilisant le canal des WebSockets entre vos clients et votre serveur, les nœuds personnalisés seront très utiles pour stocker quelques informations récurrentes.

## Conclusion

La bibliothèque `Hoa\Websocket` permet de créer des serveurs et des clients WebSocket pour plus d'interactivité dans vos applications. Le serveur est facilement extensible avec la notion de nœud, qui facilite le stockage et la manipulation de données utiles pour créer son propre protocole.