Close search
Hoa

Hack book de Hoa\Eventsource

EventSource, ou Server-Sent Events, est une technologie permettant à un serveur HTTP d'envoyer des événements à un client. La bibliothèque Hoa\Eventsource permet de créer un serveur EventSource.

Table des matières

  1. Introduction
  2. Flux d'événements
    1. Données et événements
    2. Reconnexion
    3. Identifiant
  3. Type et acceptation
  4. Conclusion

Introduction

La technologie EventSource est un standard du W3C. Elle permet à un serveur d'envoyer des événements (ou notifications selon le vocabulaire utilisé) à un client. Ces événements sont constitués de données et, potentiellement, d'identifiants.

Nous pouvons nous demander quelles sont les différences entre EventSource et WebSocket. Ces deux solutions sont en fait fondamentalement différentes : EventSource est une technologie basée sur le protocole HTTP et ne propose qu'une communication unidirectionnelle. Pour un usage full-duplex et bidirectionnel nous lui préférerons le protocole WebSocket (voir la bibliothèque Hoa\Websocket). EventSource se base sur le mode chunked d'HTTP permettant au serveur d'envoyer une réponse morceau après morceau (voir la section 3.6.1, Chunked Transfer Coding de la RFC2616) ; aussi, un serveur EventSource est-il plus léger, plus simple et est conçu pour être robuste aux déconnexions.

Flux d'événements

La classe Hoa\Eventsource\Server permet de créer un serveur EventSource. Pour le démarrer, il suffit d'instancier la classe. Ainsi, dans Server.php :

$server = new Hoa\Eventsource\Server();

Écrivons maintenant un client HTML très simple pour exécuter notre serveur, dans index.html. Nous allons uniquement utiliser l'objet EventSource et écrire des écouteurs pour les événements open et message :

<pre id="output"></pre>
<script>
var output = document.getElementById('output');

try {
    var source    = new EventSource('Server.php');
    source.onopen = function () {
        output.appendChild(document.createElement('hr'));

        return;
    };
    source.onmessage = function (evt) {
        var samp       = document.createElement('samp');
        samp.innerHTML = evt.data + '\n';
        output.appendChild(samp);

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

Voyons maintenant comment envoyer des événements et les données associées.

Données et événements

Pour envoyer des données, nous allons utiliser la méthode Hoa\Eventsource\Server::send, qui prend en premier argument la donnée à envoyer. Cette donnée peut contenir des retours à la ligne de plusieurs natures : \n, \r et même \r\n. Dans notre serveur, nous allons écrire une infinité de message et ce, toutes les secondes :

while (true) {
    $server->send(time());
    sleep(1);
}

Nous pouvons observer le résultat en ouvrant le client dans notre navigateur préféré. Attention à bien démarrer un serveur HTTP.

Toutes les données arrivent au client sans distinction particulière (notons néanmoins que l'ordre est préservé). Pour l'instant, les données sont de simples messages. Ce que nous aimerions, c'est trier ces données en les associant à des noms d'événement. Par exemple, pour associer toutes les données à l'événement tick, nous écrirons :

while (true) {
    $server->tick->send(time());
    sleep(1);
}

Sur l'instance de notre serveur, nous appelons un attribut du nom de notre événement, puis notre méthode Hoa\Eventsource\Server::send. Si l'événement porte un nom plus compliqué, nous pouvons utiliser la syntaxe avec des accolades (veillez dans ce cas à vous assurer que votre client supporte ce type d'événements). Par exemple, pour le nom ti-ck, nous écrirons $server->{'ti-ck'}->send(time()).

Si nous précisons un nom d'événement pour nos données, nous devons modifier le client en conséquence en utilisant addEventListener au lieu de onmessage :

        return;
    };
    source.addEventListener('tick', function (evt) {
        var samp       = document.createElement('samp');
        samp.innerHTML = evt.data + '\n';

Relançons notre client. Le message est bien capturé pour un événement particulier. Nous ne sommes pas limités , ni en nombre de données, ni en nombre d'événements.

Reconnexion

Lorsque la connexion est interrompue (parce que le client perd la connexion au réseau par exemple, ou lorsque le serveur coupe la connexion), le client va essayer de se reconnecter après un certain temps (la spécification conseille autour de quelques secondes). Nous pouvons indiquer ce délai au client depuis le serveur en utilisant la méthode Hoa\Eventsource\Server::setReconnectionTime et en lui donnant un nombre de millisecondes. Cette méthode peut être utilisée à tout moment, et autant de fois que nécessaire. Nous allons par exemple indiquer au client de se reconnecter en cas de déconnexion après 10 secondes exactement :

$server->setReconnectionTime(10000);

Un temps négatif n'aura aucun effet.

Cette méthode a un intérêt tout particulier lorsque nous savons quand va arriver un prochain événement (pour des flux de nouvelles, pour des jeux ou autre). Nous pouvons alors terminer la connexion depuis le serveur, en ayant au préalable indiqué au client de se reconnecter après un délai imparti pour recevoir un nouvel événement. Pendant que le serveur est déconnecté, le serveur HTTP est déchargé d'une connexion, ce qui permet de libérer des ressources.

Identifiant

Quand nous envoyons des données sur le client, nous pouvons les associer à des identifiants. Le client va automatiquement se rappeler du dernier identifiant reçu et le renvoyer au serveur lors d'une reconnection. Cela permet de marquer des étapes. Pour connaître le dernier identifiant reçu par le client, nous avons la méthode Hoa\Eventsource\Server::getLastId, et pour envoyer un nouvel identifiant au client, nous avons le second argument de la méthode Hoa\Eventsource\Server::send.

Prenons un exemple : notre serveur ne fera plus une boucle infinie, mais bornée aléatoirement. Une fois arrivé à la fin du programme, le serveur va quitter, et donc couper la connexion. Le client va se reconnecter automatiquement après un laps de temps de son choix, ou celui défini par le serveur, et donner alors le dernier identifiant qu'il aura reçu. Notre serveur va auto-incrémenter l'identifiant et l'envoyer au client (nous sommes obligés d'émettre un message car le client n'expose pas les identifiants) :

$id = $server->getLastId() ?: 0;
$server->tick->send('last ID is ' . $id);
++$id;

for ($i = mt_rand(2, 5); $i >= 0; --$i) {
    $server->tick->send(time(), $id);
    sleep(1);
}

L'identifiant n'est pas forcément un nombre : c'est une chaîne de caractères. Si l'identifiant est nul ou vide, cela va réinitialiser le dernier identifiant du client à sa valeur d'origine.

Type et acceptation

Le type d'un serveur EventSource est donné par la constante Hoa\Eventsource\Server::MIME_TYPE, soit la chaîne text/event-stream. Pour que le serveur s'exécute, le client doit accepter ce type, c'est à dire que l'en-tête HTTP Accept doit être présente et doit contenir text/event-stream. Si ce n'est pas le cas, le serveur enverra le code 406 en status (voir la section 10.4.7, 406 Not Acceptable de la RFC2616). En plus, il lèvera une exception Hoa\Eventsource\Exception depuis son constructeur. Il est possible de la capturer et d'afficher notre propre erreur, comme ceci :

try {
    $server = new Hoa\Eventsource\Server();
} catch (Hoa\Eventsource\Exception $e) {
    echo
        'You must send a request with ',
        '“Accept: ', Hoa\Eventsource\Server::MIME_TYPE, '”.', "\n";
    exit;
}

// …

Nous pouvons tester ce comportement avec cURL. Dans le premier cas, nous n'acceptons que text/html :

$ curl -H 'Accept: text/html' http://127.0.0.1:8888/Server.php --verbose
* About to connect() to 127.0.0.1 port 8888 (#0)
*   Trying 127.0.0.1... connected
* Connected to 127.0.0.1 (127.0.0.1) port 8888 (#0)
> GET /Server.php HTTP/1.1
> User-Agent: curl/a.b.c (…) libcurl/d.e.f
> Host: 127.0.0.1:8888
> Accept: text/html
>
< HTTP/1.1 406 Not Acceptable
< Date: …
< Server: …
< Content-Type: text/plain
< Content-Length: 62
<
You must send a request with “Accept: text/event-stream”.
* Connection #0 to host 127.0.0.1 left intact
* Closing connection #0

Dans le second cas, nous acceptons text/event-stream :

$ curl -H 'Accept: text/event-stream' http://127.0.0.1:8888/Server.php --verbose
* About to connect() to 127.0.0.1 port 8888 (#0)
*   Trying 127.0.0.1... connected
* Connected to 127.0.0.1 (127.0.0.1) port 8888 (#0)
> GET /Server.php HTTP/1.1
> User-Agent: curl/a.b.c (…) libcurl/d.e.f
> Host: 127.0.0.1:8888
> Accept: text/event-stream
>
< HTTP/1.1 200 OK
< Date: …
< Server: …
< Transfer-Encoding: identity, chunked
< Cache-Control: no-cache
< Content-Type: text/event-stream
<
data: last ID is 0

data: 1365685831
id: 1

data: 1365685832
id: 1

data: 1365685833
id: 1

* Connection #0 to host 127.0.0.1 left intact
* Closing connection #0

Le serveur Hoa\Eventsource\Server comprend aussi */* dans l'en-tête Accept, c'est à dire tous les types.

Conclusion

La bibliothèque Hoa\Eventsource permet de créer des serveurs EventSource. Ces derniers permettent d'envoyer des événements sur un client. La communication est unidirectionnelle ; pour une communication bidirectionnelle, il faudra se tourner vers Hoa\Websocket.

Une erreur ou une suggestion sur la documentation ? Les contributions sont ouvertes !

Comments

menu