ZeroMQ, la super bibliothèque réseau

Publié le 21 septembre 2011 par Abouchard

Une fois n’est pas coutume, voici un article particulièrement technique.

Le développement d’applications connectées est quelque chose d’à la fois important et délaissé. Important car cela constitue la base de l’informatique moderne, et que c’est une chose qui est enseignée dans tous les cursus informatiques. Délaissé car l’écrasante majorité des applications connectées actuelles sont basées sur le protocole HTTP.

L’omniprésence du HTTP est globalement une très bonne chose : Pas besoin de redévelopper la roue (les serveurs Apache et IIS sont là depuis longtemps, Nginx et Lighttpd cherchent à prendre la relève), les développements sont facilités (le protocole est simple, sont utilisation est rapide), et un très grand choix d’applications est accessible de manière unifiée (il suffit de voir toutes les API REST disponibles). Cela a créé un écosystème applicatif dynamique.

Mais il y a aussi un inconvénient : Il devient difficile de sortir de ce modèle.

  • Soit les développeurs ne savent pas faire de développement réseau. Ils ont bien appris la théorie du client-serveur à l’école, mais ne l’ont jamais appliquée autrement qu’en faisant du code PHP, Java ou .NET qui tourne sur un serveur HTTP.
  • Soit les développeurs n’ont pas fait de développement réseau depuis tellement longtemps qu’ils ne savent même plus par où commencer. Ils ont perdu les compétences nécessaires, et doivent presque repartir de zéro.
  • Soit les développeurs deviennent fainéants et ne veulent plus s’embêter à réfléchir autrement qu’en faisant du client-serveur basique en utilisant le protocole HTTP.

Et pourtant, être capable de développer des applications qui utilisent le réseau de manière différente est vital. Cela ouvre l’esprit à de nouveaux types de programmes, ce qui est parfois nécessaire pour mettre en place une fonctionnalité qui ne peut pas se satisfaire d’un fonctionnement simpliste.

ZeroMQ

ZeroMQ est une bibliothèque permettant de créer des applications client-serveur. Elle a un triple intérêt :

  1. Elle offre des possibilités nouvelles. Plutôt que de se casser la tête dès qu’on veut sortir un peu du client-serveur habituel, ZeroMQ met à notre disposition tout un arsenal de nouveaux types de connexions. La liste est longue : du client-serveur classique ; de l’envoi de données asynchrones avec load-balacing ; du “publish-subscribe” ; de la connexion multiple simultanée et transparente ; de la communication inter-threads (au sein d’un même processus), inter-processus (au sein de la même machine) et/ou inter-machines (au sein du même réseau).
  2. Elle est très simple à utiliser. Là où il faudrait normalement des dizaines de lignes de code, les choses se développent en quelques lignes, tout en restant très faciles à comprendre et à relire.
  3. Elle est très performante. ZeroMQ génère ses propres threads pour gérer ses connexions, et intègre des optimisations qui la rende généralement plus rapide que si vous ouvrez vos propres sockets brutes (oui, j’ai eu du mal à le croire moi aussi, mais c’est véridique).

En plus de cela, ZeroMQ est disponible sur un grand nombre de langages de programmation (34 à ce jour), ce qui permet de faire communiquer facilement des programmes écrits en PHP, en C, en C++, en Java, en C#, en Lua, …

Il y a toutefois une spécificité, qui peut être un inconvénient dans certains cas : ZeroMQ ne peut communiquer qu’avec ZeroMQ. Cela veut dire qu’il n’est pas possible d’utiliser cette bibliothèque pour se connecter à un serveur HTTP, FTP ou n’importe quel autre type de serveur qui utilise des sockets TCP/IP. Cela est dû au fait que ZeroMQ utilise son propre protocole d’échange de données, qui lui donne justement ses «super-pouvoirs», mais qui empêche de l’utiliser comme une bibliothèque de connexion universelle.

Principes de base

Avec ZeroMQ, on manipule des “sockets”. C’est un peu comme les sockets BSD habituelles, sauf qu’elles offrent une abstraction permettant de manipuler aussi bien des sockets réseau que des communications inter-threads ou inter-processus.

Par la force des choses, ZeroMQ reste basé sur les fondements de la communication client-serveur. Donc, quand on crée une socket ZMQ, il faut déterminer si elle va “écouter” (ce sera donc un serveur) ou si elle va se “connecter” (ce sera donc un client). Mais ZeroMQ fait une telle abstraction des choses qu’au final cela n’est pas si important. Vous pouvez avoir un “point stable” de votre application (que l’on pourrait donc considérer comme un serveur) qui va se connecter à plusieurs programmes éphémères (assimilables à des clients).

Si vous avez le temps, je vous conseille fortement de regarder la vidéo ci-dessous. Ian Barber y explique les concepts-clés de ZeroMQ. Ça dure une heure, mais ça vaut la peine. Pour vous faciliter le suivi de la conférence, vous trouverez ses slides en-dessous.

Exemple simple

Voici un petit exemple simple de client-serveur en PHP, inspiré de la conférence ci-dessus.

Le code du serveur :

$ctx = new ZMQContext();
$server = new ZMQSocket($ctx, ZMQ::SOCKET_REP);
$server->bind("tcp://*:11111");

while (true) {
   $message = $server->recv();
    $reply = strrev($message);
   $server->send($reply);
}

On commence par créer un “contexte ZeroMQ”, qui sert à la bibliothèque pour faire ses initialisations. On crée ensuite une socket, en indiquant le type REP (“reply”, cette socket répond aux requêtes qui lui sont envoyées), puis on l’attache au port n°11111. Nous avons donc créé un serveur qui va accepter les connexions provenant du port 11111, depuis n’importe quelle interface réseau (d’où l’étoile devant le numéro de port). On voit bien qu’ils s’agit d’une socket TCP-IP grâce au préfixe «tcp://».

Le serveur tournera ensuite indéfiniment. Quand une connexion s’établit, il récupère le message qui lui a été envoyé, inverse le sens d’écriture de la chaîne reçue, puis la renvoie au client.

Le code du client :

$ctx = new ZMQContext();
$req = new ZMQSocket($ctx, ZMQ::SOCKET_REQ);
$req->connect("tcp://localhost:11111");

$req->send("bonjour");
$response = $req->recv();
print("Réponse :  '$response'\n");

On crée là encore un contexte, puis une socket de type REQ (“request”, cette socket va effectuer une requête). La socket est ensuite connectée au port 11111 sur la machine locale.
Le message “bonjour” est envoyé au serveur. La réponse est récupérée et affichée. Normalement, cela devrait donner comme résultat le texte suivant :

Réponse : 'ruojnob'

Cet exemple très simple montre à quel point ZeroMQ est facile d’utilisation.

Vous aurez remarqué que le serveur n’est capable que de traiter une seule requête à la fois. Dans le cas où le traitement est simple et rapide, cela ne pose pas de problème ; ZeroMQ place les requêtes entrantes dans une file, et le serveur va les traiter les unes après les autres.
Par contre, dans le cas où les traitements prennent du temps à être effectués, il est possible de mettre en place des stratégies variées :

  • Faire du multi-processus, en “forkant” le serveur à chaque connexion entrante.
  • Préparer à l’avance un certain nombre de processus “travailleurs” (je n’ai pas trouvé de meilleur terme pour traduire “worker”). Le serveur se contente alors de faire le passe-plat entre les clients et les travailleurs.

Communications request-reply

Le premier type de socket géré par ZeroMQ vous est maintenant familier grâce à l’exemple précédent. Les socket de type REQ (request) sont destinées à se connecter à des sockets de type REP (reply). Quand une socket REQ envoie un message à une socket REP, elle attend que cette dernière lui réponde. Tant que la réponse n’aura pas été reçue, il ne sera pas possible d’envoyer de message supplémentaire ; c’est du client-serveur basique.

Je vais toutefois en profiter pour vous montrer deux spécificités de ZeroMQ.

Dans l’exemple, nous avons une architecture qui ressemble à ceci :

Le serveur écoute sur un point d’entrée, et le client s’y connecte. Mais il aurait été possible de connecter la socket ZMQ du serveur à plusieurs points d’entrée :

Il est très simple de modifier notre serveur, pour lui demander d’écouter sur plusieurs ports TCP et sur un fichier IPC :

$ctx = new ZMQContext();
$server = new ZMQSocket($ctx, ZMQ::SOCKET_REP);
$server->bind("tcp://*:11111");
$server->bind("tcp://*:22222");
$server->bind("ipc://file");

while (true) {
   $message = $server->recv();
    $reply = strrev($message);
   $server->send($reply);
}

Comme vous pouvez le voir, la seule modification tient dans l’ajout de deux attachements supplémentaire de la socket ZMQ. Ainsi, sans autre modification de code, le serveur écoutera désormais sur trois points d’entrée au lieu d’un seul.

Autre chose : comme je l’expliquais quand je vous parlais des principes de base de ZeroMQ, le sens de connexion des sockets ZMQ n’a pas vraiment de rapport avec le sens de communication de vos applications.
On pourrait très bien réécrire le client et le serveur de manière à obtenir l’architecture suivante :

Le serveur :

$ctx = new ZMQContext();
$server = new ZMQSocket($ctx, ZMQ::SOCKET_REP);
$server->connect("tcp://localhost:11111");

while (true) {
   $message = $server->recv();
    $reply = strrev($message);
   $server->send($reply);
}

Le client :

$ctx = new ZMQContext();
$req = new ZMQSocket($ctx, ZMQ::SOCKET_REQ);
$req->bind("tcp://*:11111");

$req->send("bonjour");
$response = $req->recv();
print("Réponse :  '$response'\n");

Comme vous pouvez le voir, j’ai juste interverti les instructions d’écoute et de connexion. Mais comment cela peut-il fonctionner ? C’est la magie de ZeroMQ. Quand le serveur se lance, il tente de se connecter sur le port demandé ; tant que personne ne répondra à sa tentative de connexion, il attendra sagement. Dès que le client se met à écouter sur le port en question, le serveur s’y connecte. Une fois la connexion effectuée et gérée par ZeroMQ, l’application peut communiquer ; le client envoie sa requête, le serveur y répond.

Alors évidemment, cette «bizarrerie» peut sembler inutile. Un serveur qui se connecte, à quoi cela peut-il bien servir ? Il faut juste comprendre que grâce à ZeroMQ, la notion de client-serveur devient obsolète. D’un côté, il y a la notion de connexion (quel programme attend les connexions, quel programme se connecte) ; de l’autre côté, il y a la notion de communication (quel programme envoie des données, quel programme les reçoit).
Celui qui attend les connexions ne doit pas nécessairement être celui qui reçoit les données ; celui qui se connecte n’est pas obligatoirement celui qui envoie les données.

Communications push-pull

Le second type de communication géré par ZeroMQ est de type «pipeline». Un programme envoie des données, un autre les reçoit. C’est une communication unidirectionnelle asynchrone : l’émetteur n’a pas besoin d’attendre que le message ait été réceptionné avant de continuer son travail ; c’est du «fire and forget».

Nous allons donc avoir d’un côté une socket “PUSH”, correspondant à l’envoi de données, et de l’autre une socket “PULL”, qui reçoit des données. La plupart du temps, une socket “PULL” sera mise en place au niveau d’un serveur qui accepte des connexions venant de plusieurs clients qui feront du “PUSH”.

Voici le code d’un serveur qui se contente d’écrire sur sa sortie standard les chaînes qui lui sont envoyées :

$ctx = new ZMQContext();
$receiver = new ZMQSocket($ctx, ZMQ::SOCKET_PULL);
$receiver->bind("tcp://*:1234");

while (true) {
   $message = $receiver->recv();
   print("$message\n");
}

Voici le code d’un client, qui envoie au serveur l’heure courante dix fois de suite, en faisant une pause de 3 secondes entre chaque envoi :

$ctx = new ZMQContext();
$sender = new ZMQSocket($ctx, ZMQ::SOCKET_PUSH);
$send->connect("tcp://localhost:1234");

for ($i = 0; $i < 10; $i++) {
   $sender->send(date('c'));
   sleep(3);
}

Vous pouvez exécuter autant de client que vous le souhaitez, le serveur affichera les messages au fur et à mesure qu’il les recevra.

Communication publish-subscribe

Le troisième type de communication géré par ZeroMQ est un modèle d’abonnement. Un programme émet des messages, qui sont envoyés à tous ceux qui s’y sont abonnés préalablement.

Pour reprendre un modèle client-serveur traditionnel, on aura souvent un serveur qui émet des messages, et plusieurs clients qui s’y connectent pour recevoir les messages.

Voici un exemple de programme qui publie des données. Il envoie la date et heure courante toutes les 10 secondes :

$ctx = new ZMQContext();
$publisher = new ZMQSocket($ctx, ZMQ::SOCKET_PUB);
$publisher->bind("tcp://*:1234");

while (true) {
    $message = date("c");
    $publisher->send($message);
}

Et voici le code d’un programme qui s’abonne au premier, et qui va écrire sur sa sortie standard toutes les données qu’il recevra :

$ctx = new ZMQContext();
$subscriber = new ZMQSocket($ctx, ZMQ::SOCKET_SUB);
$subscriber->connect("tcp://localhost:1234");
$subscriber->setSockOpt(ZMQ::SOCKOPT_SUBSCRIBE, "");

while (true) {
    $message = $subscriber->recv();
    print("Date : '$message'\n");
}

Comme vous pouvez le voir, cela reste toujours aussi simple. Ne vous stressez pas à cause de l’instruction setSockOpt() ; elle est nécessaire et peut se révéler très utile, mais je vous laisse découvrir ça par vous-même.

Load-balancing

Nous venons de voir que le cas du publish-subscribe est très spécifique. Le programme qui émet des données n’a ouvert qu’une seule socket, et plusieurs programmes vont recevoir les messages envoyés.

Dans certains cas, on voudrait au contraire pouvoir communiquer avec plusieurs programmes, en leur envoyant des données successivement et non pas simultanément. Cela peut être très pratique dans le cas d’un programme qui doit donner des ordres à des “travailleurs” (voir plus haut).

La marche à suivre est très simple : Après avoir créé une socket PUSH, il suffit de la connecter à plusieurs autres sockets PULL. Par la suite, à chaque fois qu’un message sera envoyé sur cette socket, il le sera sur un destinataire différent (en tournant en boucle sur l’ensemble des destinataires disponibles).

L’avantage de ce fonctionnement, c’est que ZeroMQ gère complètement le mécanisme. Qu’il y ait une connexion ou plusieurs dizaines/centaines, une fois que la connexion est faite le programme se contente d’envoyer des messages comme d’habitude. C’est très pratique et très puissant.

Pour aller plus loin

ZeroMQ est tellement simple à mettre en œuvre et rapide dans son exécution qu’il est conseillé de ne pas hésiter à l’utiliser en mélangeant plusieurs types de communications, pour former des applications complexes.

Par exemple, si vous avez besoin de créer une file de message, il suffit d’avoir un programme qui réceptionne des messages d’un côté, et qui les redistribue séquentiellement de l’autre.

ZeroMQ offre aussi un mécanisme de «polling», qui permet à un programme d’ouvrir plusieurs socket ZeroMQ en même temps, mais aussi des sockets classiques ou des descripteurs de fichiers (n’oubliez pas que l’entrée standard est un descripteur de fichier), puis d’attendre qu’une donnée arrive sur l’une de ces entrées, pour ensuite les traiter.

Que vous ayez besoin de créer une architecture distribuée ou simplement pour votre veille technologique, je ne peux que vous inciter à lire la documentation de ZeroMQ. Elle est assez longue à lire, mais ça en vaut la peine.