Petite démo pragmatique d’un usage de WAMP en Python


L’API a changé depuis, j’ai donc mis à jour l’article pour refléter ces changements

Vu que dernièrement je vous ai bien gavé avec WAMP, ça mérite un tuto non ?

Il se trouve que l’équipe derrière WAMP a publié plus tôt que prévu une version de leurs libs contenant l’API flaskesque sur laquelle on bosse. L’idée est que même si on n’a pas encore les tests unitaires, on peut déjà jouer avec.

Maintenant il me fallait un projet sexy, histoire de donner envie. Donc j’ai fouillé dans ce qui se faisait côté temps réel (essentiellement du NodeJS et du Tornado, mais pas que) pour trouver l’inspiration.

Et j’ai trouvé un truc très sympa : un player vidéo piloté à distance.

En effet, n’est-il pas chiant de regarder une vidéo en ligne sur son ordi posé sur la commode pendant qu’on est enfoncé dans le canap ? Si on veut faire pause ou changer le son, il faut se lever, arg.

Les problèmes du tiers monde, c’est du pipi de chat à côté. Ils ont de la chance, eux, ils ne connaissent pas le streaming.

Voici donc le projet :

Une page avec un player HTML 5 et un QR code.

Capture d'écran de la démo, côté player

Pour simplifier la démo, on peut cliquer sur le QR code et avoir la télécommande dans un autre tab pour ceux qui n’ont pas de smartphone ou d’app de scan de QRCode.

Si on scanne le QR code avec son téléphone, il vous envoie sur une page avec une télécommande pour contrôler le player sans bouger votre cul :

Capture d'écrand de la démo, côté contrôles

Évidement, c’est basique. Je vais pas m’amuser à faire un produit complet juste pour un truc dont le code source ne sera même pas regardé par la plupart d’entre vous. Je vous connais, bandes de feignasses !

Et vous allez voir, c’est même pas dur à faire.

Démo en ligne:

La démo

Vous pouvez télécharger le code ici.

Pour comprendre ce qui va suivre, il va vous falloir les bases en prog Javascript et Python, ainsi que bien comprendre la notion de callback. Être à l’aise avec promises peut aider.

Et pour bien digérer ce paté, rien ne vaut un peu de son :

Le Chteumeuleu

Il va nous falloir deux pages Web, une pour le player vidéo, et une pour la télécommande.

Le player :

 
<!DOCTYPE html>
<html>
<head>
   <title>Video</title>
   <meta charset='utf-8'>
   <!-- Chargement des dépendances : autobahn pour WAMP
   et qrcode pour générer le QR code. Bien entendu, je
   vous invite à ne pas les hotlinker dans vos projets,
   mais pour la démo c'est plus simple. -->
   <script src="https://autobahn.s3.amazonaws.com/autobahnjs/latest/autobahn.min.jgz"
           type="text/javascript"></script>
   <script src="http://davidshimjs.github.com/qrcodejs/qrcode.min.js"
           type="text/javascript"></script>
 
   <style type="text/css">
      #vid {
         /* Taille de la video */
         width:427px;
         height:240px;
      }
      /* Centrage avec la méthode Rache */
      #container {
          width:427px;
          margin:auto;
      }
      #ctrllink {
          display:block;
          width:256px;
          margin:auto;
      }
   </style>
 
</head>
<body>
<div id="container">
  <p>
 
   <!-- Pareil, je hotlink la video, mais ne faites pas ça
   à la maison les enfants. Surtout que les perfs du
   serveur du W3C sont merdiques et ça bufferise à mort. -->
    <video id="vid"
           class="video-js vjs-default-skin"
           controls preload="auto"
           poster="http://media.w3.org/2010/05/sintel/poster.png" >
    <source id='ogv'
      src="http://media.w3.org/2010/05/sintel/trailer.ogv"
      type='video/ogg'>
    <source id='mp4'
      src="http://media.w3.org/2010/05/sintel/trailer.mp4"
      type='video/mp4'>
    <source id='webm'
      src="http://media.w3.org/2010/05/sintel/trailer.webm"
      type='video/webm'>
    </video>
  </p>
  <p>
    <a id="ctrllink" href="#" target="_blank">
      <span id="qrcode"></span>
    </a>
  </p>
 </div>
 
</body>

Et la télécommande :

 
<!DOCTYPE html>
<html>
<head>
  <title>Télécommande</title>
  <meta charset='utf-8'>
  <script src="https://autobahn.s3.amazonaws.com/autobahnjs/latest/autobahn.min.jgz"
         type="text/javascript"></script>
  <!-- Zoom du viewport sur mobile pour éviter d'avoir
       à le faire à la main. -->
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <style type="text/css">
    #controls {
      width:350px;
      margin:auto;
    }
    #controls button {
      font-size: 1em;
    }
    #controls input {
      vertical-align:middle;
       width: 200px;
       height:20px;
   }
  </style>
</head>
<body>
  <p id="controls">
    <!-- Marrant de se dire qu'en 2000, le JS inline était
         considéré comme démoniaque, et maintenant avec
         angularjs et cie, c'est exactement ce que tout
         le monde fait...
         Bref, on attache le clic sur nos contrôles à des
         méthodes de notre objet qui va se charger de la
         logique. -->
 
    <button id="play" onclick="control.togglePlay()">Play</button>
    <input id="volume"
                    onchange="control.volume(this.value)"
                    type="range">
  </p>
</body>

Rien d’incroyable. C’est du HTML, un peu de CSS, on charge les dépendances en JS. Classique.

Vu qu’on utilise des ressources hotlinkées par souci de simplicité, il vous faudra être connecté à Internet.

Setup du routeur

On va travailler avec Python 2.7 puisque Crossbar.io est uniquement en 2.7 et que je n’ai pas envie de vous faire installer deux versions de Python juste pour le tuto.

Il nous faut avant tout un serveur HTTP pour servir les fichiers HTML et un routeur WAMP. On installe donc Crossbar.io :

pip install crossbar

Ca va aussi installer autobahn, twisted et tout le bordel.

On va ensuite dans le dossier qui contient ses fichiers HTML, et on créé le fichier de config de Crossbar.io avec un petit :

crossbar init

Vous noterez la création d’un dossier .crossbar qui contient un fichier config.json. C’est la config de crossbar. Videz moi ce fichier, on va le remplir avec notre config :

{
   "workers": [
      {
         "type": "router",
         "realms": [
            {
               "name": "realm1",
               "roles": [
                  {
                     "name": "anonymous",
                     "permissions": [
                        {
                           "uri": "*",
                           "publish": true,
                           "subscribe": true,
                           "call": true,
                           "register": true
                        }
                     ]
                  }
               ]
            }
         ],
         "transports": [
            {
               "type": "web",
               "endpoint": {
                  "type": "tcp",
                  "port": 8080,
                  "interface": "0.0.0.0"
               },
               "paths": {
                  "/": {
                     "type": "static",
                     "directory": ".."
                  },
                  "ws": {
                     "type": "websocket",
                  }
               }
            }
         ]
      }
   ]
}

Crossbar est en effet un gestionnaire de processus : il ne gère vraiment rien lui même. Il démarre d’autres processus, appelés workers, à qui il délègue le travail.

On définit dans ce fichier de config quels processus (les workers) lancer quand Crossbar.io démarre. Les valeurs qu’on utilise disent de créer un seul worker de type “router”, c’est à dire un worker capable de gérer les entrées et les sorties WAMP. Hey oui, le routeur n’est qu’un worker comme les autres :)

Il y a d’autres sortes de workers, mais aujourd’hui on s’en branle.

Dans notre config du worker router, on crée d’abord un realm, qui est juste un namespace avec des permissions. Si un client WAMP se connecte à ce routeur, il doit choisir un realm (qui est juste un nom), et il ne peut parler qu’avec les clients du même realm. C’est une cloture quoi.

Dans un realm, on définit des roles qui déclarent quelles opérations PUB/SUB et RPC on a le droit de faire. Ici on dit que tout le monde (anonymous) a le droit de tout faire sur toutes les urls (“uri”: ‘*”) histoire de pas se faire chier. Si on met en prod, évidement on va se pencher sur la sécurité et faire ça plus proprement.

"realms": [
{
   "name": "realm1",
   "roles": [
      {
         "name": "anonymous",
         "permissions": [
            {
               "uri": "*",
               "publish": true,
               "subscribe": true,
               "call": true,
               "register": true
            }
         ]
      }
   ]
}
],

Puis on définit les transports, c’est à dire sur quoi notre worker va ouvrir ses oreilles pour écouter les messages entrant :

"transports": [
            {
               "type": "web",
               "endpoint": {
                  "type": "tcp",
                  "port": 8080,
                  "interface": "0.0.0.0"
               },
               "paths": {
                  "/": {
                     "type": "static",
                     "directory": ".."
                  },
                  "ws": {
                     "type": "websocket",
                  }
               }
            }
        ]

Encore une fois on en déclare un seul, de type “web”. Ce transport peut écouter HTTP et Websocket sur le même port. On lui dit d’écouter sur “0.0.0.0:8080” :


“endpoint”: {
“type”: “tcp”,
“port”: 8080,
“interface”: “0.0.0.0”
},

Ensuite on dit que si quelqu’un arrive sur “/”, on sert en HTTP les fichiers statiques histoire que nos pages Web soient servies :

"/": {
 "type": "static",
 "directory": ".."
},

Si on arrive sur “/ws”, on route les requêtes WAMP via Websocket :

"ws": {
 "type": "websocket",
}

Le routeur est prêt, on lance Crossbar.io :

$ crossbar start
2015-01-07 20:02:55+0700 [Controller  26914] Log opened.
2015-01-07 20:02:55+0700 [Controller  26914] ============================== Crossbar.io ==============================
 
2015-01-07 20:02:55+0700 [Controller  26914] Crossbar.io 0.9.12-2 starting
2015-01-07 20:02:55+0700 [Controller  26914] Running on CPython using EPollReactor reactor
2015-01-07 20:02:55+0700 [Controller  26914] Starting from node directory /home/sam/Work/sametmax/code_des_articles/2014/juin/video_remote/.crossbar
2015-01-07 20:02:55+0700 [Controller  26914] Starting from local configuration '/home/sam/Work/sametmax/code_des_articles/2014/juin/video_remote/.crossbar/config.json'
2015-01-07 20:02:55+0700 [Controller  26914] Warning, could not set process title (setproctitle not installed)
2015-01-07 20:02:55+0700 [Controller  26914] Warning: process utilities not available
2015-01-07 20:02:55+0700 [Controller  26914] No WAMPlets detected in enviroment.
2015-01-07 20:02:55+0700 [Controller  26914] Starting Router with ID 'worker1' ..
2015-01-07 20:02:55+0700 [Controller  26914] Entering reactor event loop ...
2015-01-07 20:02:55+0700 [Router      26917] Log opened.
2015-01-07 20:02:55+0700 [Router      26917] Warning: could not set worker process title (setproctitle not installed)
2015-01-07 20:02:55+0700 [Router      26917] Running under CPython using EPollReactor reactor
2015-01-07 20:02:56+0700 [Router      26917] Entering event loop ..
2015-01-07 20:02:56+0700 [Router      26917] Warning: process utilities not available
2015-01-07 20:02:56+0700 [Controller  26914] Router with ID 'worker1' and PID 26917 started
2015-01-07 20:02:56+0700 [Controller  26914] Router 'worker1': realm 'realm1' started
2015-01-07 20:02:56+0700 [Controller  26914] Router 'worker1': role 'role1' started on realm 'realm1'
2015-01-07 20:02:56+0700 [Router      26917] Site starting on 8080
2015-01-07 20:02:56+0700 [Controller  26914] Router 'worker1': transport 'transport1' started

Setup du client

Pour cette démo, le serveur n’a pas grand chose à faire. On pourrait en fait la faire sans aucun code Python, mais ça va nous simplifier la vie et donner un peut de grain à moudre pour le tuto.

En effet, on a deux problématiques que le serveur va résoudre facilement pour nous : créer un ID unique pour le player et récupérer l’IP sur le réseau local.

L’ID, c’est simplement que si plusieurs personnes lancent en même temps un player, on ne veut pas que les télécommandes puissent lancer un ordre à un autre player que le sien. On pourrait utiliser un timestamp, mais ils sont contigus, n’importe quel script kiddies pourrait faire un script pour foutre la merde. On va donc créer un ID unique qui ne soit pas facilement prévisible. Javascript n’a rien pour faire ça en natif, et c’est un peu con de charger une lib de plus pour ça alors que Python peut le faire pour nous.

L’IP, c’est parce qu’il faut donner l’adresse de notre machine contient notre routeur. Et le téléphone qui sert de télécommande doit se connecter à ce routeur. Il faut donc qu’il connaisse l’adresse de celui-ci, donc on va la mettre dans notre QR code.

Cela veut dire aussi que le téléphone doit être sur le même réseau local pour que ça fonctionne. Donc mettez votre téléphone en Wifi, pas en 3G.

Voilà ce que donne notre code WAMP côté serveur :

# -*- coding: utf-8 -*-
 
from autobahn.twisted.wamp import Application
 
import socket
import uuid
 
# Comme pour flask, l'objet app
# est ce qui lie tous les éléments
# de notre code ensemble. On lui donne
# un nom, ici "demo"
app = Application('demo')
# Bien que l'app va démarrer un serveur
# pour nous, l'app est bien un CLIENT
# du serveur WAMP. Le serveur démarré
# automatiquement n'est qu'une facilité
# pour le dev. En prod on utiliserait
# crossbar.
 
# Juste un conteneur pour y mettre notre IP
app._data = {}
 
# On déclare que cette fonction sera appelée
# quand l'app se sera connectée au serveur WAMP.
# Ceci permet de lancer du code juste après
# le app.run() que l'on voit en bas du fichier.
# '_' est une convention en Python pour dire
# "ce nom n'a aucune importance, c'est du code
# jetable qu'on utilisera une seule fois".
@app.signal('onjoined')
def _():
   # On récupère notre adresse IP sur le réseau local
   # C'est une astuce qui demande de se connecter et donc
   #  à une IP externe, on a besoin d'une connexion internet.
   s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
   s.connect(("8.8.8.8", 80))
   # On stocke l'adresse IP locale dans un conteneur
   # qui sera accessible partout ailleur.
   app._data['LOCAL_IP'] = s.getsockname()[0]
   s.close()
 
# On déclare que la fonction "ip()" est appelable
# via RCP. Ce qui veut dire que tout autre client
# WAMP peut obtenir le résultat de cette fonction.
# Donc on va pouvoir l'appeler depuis notre navigateur.
# Comme notre app s'appelle "demo" et notre fonction
# s'appelle "ip", un client pourra l'appeler en faisant
# "demo.ip".
@app.register()
def ip():
   # On ne fait que retourner l'IP locale. Rien de fou.
   return app._data['LOCAL_IP']
 
# Je voulais appeler cette fonction distante "uuid", mais ça
# override le module Python uuid. Ce n'est pas une bonne
# idée. Je l'appelle donc 'get_uuid' mais je déclare le
# namespace complet dans register(). Un client WAMP pourra donc
# bien l'appeler via "demo.uuid".
# Notez que ce namespace doit toujours s'écrire
# truc.machine.bidule. Pas truc/machin ou truc:machin.
# ou truc et bidule.MACHIN.
@app.register('demo.uuid')
def get_uuid():
   # Retourne un UUID, sans les tirets.
   # ex: b27f7e9360c04efabfae5ac21a8f4e3c
   return str(uuid.uuid4()).replace('-', '')
 
# On lance notre client qui va se connecter au
# routeur.
if __name__ == '__main__':
    app.run(url="ws://127.0.0.1:8080/ws")
# On ne peut rien mettre comme code ici, il faut le
# mettre dans @app.signal('onjoined') si on veut
# entrer du code après que l'app soit lancée.

Et on lance notre app dans un autre terminal:

python app.py

Nous avons maintenant Crossbar.io qui tourne d’une console, et le client Python qui tourne dans une seconde console, connecté au routeur.

Le lecteur vidéo

Il nous faut maintenant définir le comportement de notre lecteur vidéo, un client WAMP Javascript. Il s’agit essentiellement de se connecter au serveur WAMP, et d’échanger des messages via RPC ou PUB/SUB :

  var player = {};
  var url;
  /* On va utiliser du pur JS histoire de pas mélanger
    des notions de jQuery dans le tas. Je ne vais
    PAS utiliser les best practices sinon vous allez
    être noyés dans des détails */
 
  /* Lancer le code une fois que la page est chargée */
  window.addEventListener("load", function(){
 
    /* Connexion au serveur WAMP. J'utilise
       les valeurs par défaut du serveur de
       dev. On ouvre explicitement la connection
       à la fin du script. */
    var connection = new autobahn.Connection({
       url: 'ws://' + window.location.hostname + ':8080/ws',
       realm: 'realm1'
    });
 
    /* Lancer ce code une fois que la connexion
       est réussie. Notez que je ne gère pas
       les erreurs dans dans une APP JS, c'est
       un puits sans fond. */
    connection.onopen = function (session) {
 
      /* Appel de la fonction ip() sur le serveur */
      session.call('demo.ip')
 
      /* Une fois qu'on a récupéré l'IP,
         on peut fabriquer l'URL de notre
         projet et on appelle la fonction
         get_uuid() du serveur */
      .then(function(ip){
        url = 'http://' + ip + ':8000';
        return session.call('demo.uuid');
      })
 
      /* Une fois qu'on a l'UUID, on peut commencer
         à gérer la partie télécommande */
      .then(function(uuid){
 
        /* Création du QR code avec le lien pointant
           sur la bonne URL. On met l'ID dans le hash. */
        var controlUrl = url + '/control.html#' + uuid;
        var codeDiv = document.getElementById("qrcode");
        new QRCode(codeDiv, controlUrl);
        var ctrllink = document.getElementById("ctrllink");
        ctrllink.href = controlUrl;
 
        /* Notre travail consiste essentiellement à
           manipuler cet élément */
        var video = document.getElementById("vid");
 
        /* On attache déclare 4 fonctions comme étant
           appelable à distance. Ces fonctions sont
           appelables en utilisant le nom composé
           de notre ID et de l'action qu'on souhaite
           faire. Ex:
           'b27f7e9360c04efabfae5ac21a8f4e3c.play'
           pour appeler "play" sur notre session. */
        session.register(uuid + '.play', function(){
           video.play();
        });
 
        session.register(uuid + '.pause', function(){
           video.pause();
        });
 
        session.register(uuid + '.volume', function(val){
           video.volume = val[0];
        });
 
        session.register(uuid + '.status', function(val){
          return {
            'playing': !video.paused,
            'volume': video.volume
          };
        });
 
 
 
       /* Quelqu'un peut très bien
           appuyer sur play directement sur cette page.
 
          Il faut donc réagir si l'utilisateur le fait,
          publier un événement via WAMP pour permettre
          à notre télécommande de se mettre à jour
          */
       video.addEventListener('play', function(){
         /* On publie un message indiquant que
            le player a recommencé à lire la vidéo.
            */
         session.publish(uuid + '.play');
       });
 
        video.addEventListener('pause', function(){
          session.publish(uuid + '.pause');
        });
 
        video.addEventListener('volumechange', function(){
          session.publish(uuid + '.volume', [video.volume]);
        });
 
     });
    };
 
    /* Ouverture de la connection une fois que tous les
       callbacks sont bien en place.*/
    connection.open();
  });

Code de la télécommande

La télécommande est notre dernier client WAMP (on peut avoir plein de clients WAMP, ne vous inquiétez, ça tient 6000 connections simultanées sur un tout petit Raspberry PI).

Son code a pour but d’envoyer des ordres au player HTML5, mais aussi de mettre à jour son UI si le player change d’état.

/* L'objet qui se charge de la logique de nos
   controles play/pause et changement de
   volume.
   Rien de fou, il change l'affichage
   du bouton et du slider selon qu'on
   est en pause/play et la valeur du
   volume.
   */
var control = {
   playing: false,
   setPlaying: function(val){
      control.playing = val;
      var button = window.document.getElementById('play');
      if (!val){
         button.innerHTML = 'Play'
      } else {
         button.innerHTML = 'Pause';
      }
   },
   setVolume: function(val){
      var slider = window.document.getElementById('volume');
      slider.value = val;
   }
};
window.onload = function(){
  var connection = new autobahn.Connection({
    url: 'ws://' + window.location.hostname + ':8080/ws',
    realm: 'realm1'
  });
 
  connection.onopen = function (session) {
 
    /* Récupération de l'ID dans le hash de l'URL */
    var uuid = window.location.hash.replace('#', '');
 
    /* Mise à jour des controles selon le status actuel
       du player grace à un appel RPC vers notre autre
       page. */
    session.call(uuid + '.status').then(function(status){
 
      control.setPlaying(status['playing']);
      control.setVolume(status['volume'])
 
      /* On attache l'appui sur les contrôles à
         un appel de la fonction play() sur le
         player distant. L'uuid nous permet
         de n'envoyer l'événement que sur le
         bon player. */
      control.togglePlay = function() {
        if (control.playing){
          session.call(uuid + '.pause');
          control.setPlaying(false);
        } else {
          session.call(uuid + '.play');
          control.setPlaying(true);
        }
      };
 
      control.volume = function(val){
        session.call(uuid + '.volume', [val / 100]);
      };
 
      /* On ajoute un callback sur les événements
         de changement de status du player. Si
         quelqu'un fait play/pause ou change le
         volume, on veut mettre à jour la page. */
      session.subscribe(uuid + '.play', function(){
        control.setPlaying(true);
      });
 
      session.subscribe(uuid + '.pause', function(){
        control.setPlaying(false);
      });
 
      session.subscribe(uuid + '.volume', function(val){
        control.setVolume(val[0] * 100);
      });
    });
  };
 
  connection.open();
};

En résumé

Voici à quoi ressemble le projet final :

.
├── app.py
├── control.html
├── .crossbar
│   └── config.json
└── index.html
Schéma de fonctionnement de la démo

Bien que l’app Python lance le serveur automatiquement et de manière invisible, c’est bien un composant à part.

Pour ce projet, on aura utilisé :

  • WAMP: le protocole qui permet de faire communiquer en temps réel des parties d’application via RPC et PUB/SUB.
  • Autobahn.js: une lib pour créer des clients WAMP en javascript.
  • Autobahn.py: une lib pour créer des clients WAMP en Python.
  • Crossbar.io: un routeur WAMP.

Il y a pas mal de notions à prendre en compte.

D’abord, le RPC.

Cela permet à un client de dire “les autres clients peuvent appeler cette fonction à distance”. On l’utilise pour exposer ip() et get_uuid() sur notre serveur et notre Javascript peut donc les appeler. Mais on l’utilise AUSSI pour qu’une des pages (le player) expose play(), pause() et volume() et que l’autre page (notre télécommande) puisse les utiliser.

La grosse différence, c’est que ip() peut être appelé par tous les clients en utilisant “demo.ip” alors que play() ne peut être appelé que par les clients qui connaissent l’ID du player, puisqu’il faut utiliser “<id>.play”.

Ensuite, il y a le PUB/SUB.

Cela permet à un client de dire “j’écoute tous les messages adressés à ce nom”. Et un autre client peut envoyer un message (on appelle ça aussi un événement, c’est pareil) sur ce nom, de telle sorte que tous les clients abonnés le reçoivent.

On l’utilise pour que notre télécommande dise “j’écoute tous les messages qui concernent les changements de status du player.” De l’autre côté, quand on clique sur un contrôle du player, on envoie un message précisant si le volume a changé, ou si on a appuyé sur play/pause. La télécommande peut ainsi mettre son UI à jour et refléter par exemple, la nouvelle valeur du volume.

Cela résume bien les usages principaux de ces deux outils :

  • RPC permet de donner un ordre ou récupérer une information.
  • PUB/SUB permet de (se) tenir au courant d’un événement.

Voici le workflow de notre projet :

  • On lance un serveur WAMP.
  • On connecte des clients dessus (du code Python ou Js dans notre exemple).
  • Les clients déclarent les fonctions qu’ils exposent en RPC et les événements qu’ils écoutent en PUB/SUB.
  • Ensuite on réagit aux actions utilisateurs et on fait les appels RPC et les publications PUB/SUB en conséquence.

Si vous virez tous les commentaires, vous verrez que le code est en fait vraiment court pour une application aussi complexe.

Encore une fois, il est possible de le faire sans WAMP, ce sera juste plus compliqué. Je vous invite à essayer de le faire pour vous rendre compte. Avec PHP, Ruby ou une app WSGI, c’est pas marrant du tout. Avec NodeJs, c’est plus simple, mais il faut quand même se taper la logique de gestion RPC et PUB/SUB à la main ou installer pas mal de libs en plus.

WAMP rend ce genre d’app triviale à écrire. Enfin triviale parce que là j’ignore tous les edge cases, évidemment. Pour un produit solide, il faut toujours suer un peu.

Les limites du truc

C’est du Python 2.7. Bientôt on pourra le faire avec asyncio et donc Python 3.4, mais malheureusement sans le serveur de dev.

Heureusement, Twisted est en cours de portage vers Python 3, et donc tout finira par marcher en 3.2+.

C’est du HTML5, mais bien entendu, rien ne vous empêche de faire ça avec du Flash si ça vous amuse.

C’est du WebSocket, mais on peut utiliser un peu de Flash pour simuler WebSocket pour les vieux navigateurs qui ne le supportent pas.

Non, la vraie limite c’est encore la jeunesse du projet : pas d’autoreload pour le serveur (super chiant de devoir le faire à la main à chaque fois qu’on modifie le code) et les erreurs côté serveur se lisent dans la console JS, et pas dans le terminal depuis lequel on a lancé le serveur. Plein de petits détails comme ça.

47 thoughts on “Petite démo pragmatique d’un usage de WAMP en Python

  • artyprog

    Merci beaucoup Sam.

    Je ne suis pas encore en mesure de tester
    ton exemple.

    Mais j’ai juste une question, un peu stupide sans doute, pourquoi ne pas utiliser directement crossbar ?
    Est-ce uniquement une question d’installation, ou y a t-il un réel avantage à utiliser le serveur de dev ?

    D’autre part, pourras-tu, à l’occasion, poster l’exemple pour mise en prod ?

    Cordialement

  • Laurent

    Tain chapeau les gars pour tous les articles que vous postez. Y’a des fois ou vous êtes pas “aware” ?
    Quand je vois votre boulot et que moi en ce moment j’arrive à rien… Je vois passer plein de trucs intéressant je n’arrive pas à les lire et si éventuellement je les lis je n’arrive pas à les comprendre.

  • Sam Post author

    @artyprog: je suis en train de voir si je peux le mettre en prod, mais ça me fait chier de config un serveur pour ça. J’utilise pas crossbar parce que ça fait des notions en plus à expliquer, et là le tuto est déjà très long. C’est juste trop pratique de pouvoir lancer un serveur de dev automatiquement.

    Si j’ai le temps, je vais voir pour un exemple de mise en prod. Si j’ai le temps. Et la motive. Parce que je t’avoues que l’article, là, il m’a mis à genoux pour la journée.

    @Laurent: les technos vont très vites, même pour moi. J’arrive plus à tout suivre depuis quelques mois, c’est effrayant.

  • Sam Post author

    Arf, moi et deux correcteurs qui éditent en même temps le même article, ça fait n’importe quoi :)

    Bon, je modifie le titre de l’article, il promet une introduction alors que c’est plus une démo, et pas un truc pas à pas bien décortiqué. Je voudrais pas faire de publicité mensongère.

  • Zanguu

    Typo dans le paragraphe résumé : Autohan.py; Il manque un b.

    Aussi, je trouve ça assez frustrant de toujours remettre crossbar sur le tapis dés que tu parle de serveur, j’aurais plus vu une section à la fin du genre : “et pour utiliser ça en prod bien sur vous prenez pas le serveur de dev mais crossbar”.
    Là ça fait plutôt “je connais telle techno qui déchire sa maman mais je vous montrerais pas parce que c’est trop d’explication” à chaque paragraphe.
    (Ça à l’air un peu méchant quand je me relis, mais c’était censé être constructif)

    J’aime bien l’article (et même que j’ai lu tout le code).
    J’avais du mal à cerner l’utilité exacte de WAMP et l’intérêt de l’API flaskesque, mais maintenant c’est bien plus clair et tu m’as presque fait aimer le js avec la clarté du .then().

    Ah et petite déception, la vidéo de la démo est SFW, c’est déstabilisant.

  • bobuss

    Hello !
    Encore un super article ! merci

    Juste, j’ai dû
    pip install twisted
    pour lancer le programme ; autobahn ne semble pas embarquer twisted dans ses dépendances.

  • Sam Post author

    @Zanguu: ouai mais t’as vu la taille de l’article. Si je parle d’autobahn, c’est comme parler de nginx quand tu fais un tuto sur django, c’est trop. Avec django, tu utilises le serveur de dev, et avec WAMP aussi. Mais bon, je vais voir, pour les guerriers, si je peux pas rajouter un paragraphe de mise en prod.

    @bobuss: bien vu merci. En fait autobahn à 3 dépendances possible : trollus, asyncio ou twisted. Si on précise pas, il installe aucune. J’ai corrigé la ligne d’installation.

  • bobuss

    re,

    la prochaine fois, j’opterai pour la pull-request …

    dans le code du player, ne pas oublier de passer la valeur du volume dans le publish.


    video.addEventListener('volumechange', function(){
    session.publish(uuid + '.volume', [video.volume]);
    });

    et dans le code de la télécommande, dans le setVolume, c’est l’attribut value du slider

    slider.value = val;

    puis dans le callback du .status, pour setter à la bonne valeur le volume

    control.setVolume(status['volume'] * 100)

    et enfin, dans le subscribe .volume, lire la donnée qu’on envoie depuis le player, et qui est un tableau


    session.subscribe(uuid + '.volume', function(val){
    control.setVolume(val[0] * 100);
    });

    Encore merci pour l’introduction, ça promet pour la suite !

  • Sam Post author

    Merci beaucoup, j’ai rajouté le volume après coup et c’est vrai que je me suis pas relu.

  • Zanguu

    @Sam, justement avec Django tu mettrais en avant le serveur de dev pour la démo et tu préciserais à la fin qu’il ne faut pas l’utiliser pour de la prod et qu’il y a des solutions comme nginx pour la prod. Sans forcement expliquer comment le configurer.
    Alors que là tu relances “sans arrêt” le fait qu’il faudrait plutôt utiliser crossbar en prod comme si ça te frustrait de ne pas l’utiliser et du coup c’est frustrant pour le lecteur (ou juste pour moi, mais je suis bizarre alors on s’en fout).

    Je demandais pas l’explication pour mettre ça en prod, il y a déjà une intro à crossbar et je suppose qu’un petit tuto est dans les drafts.

    Et puis le point essentiel de mon dernier commentaire : C’est un très bon article qui m’a permis de beaucoup mieux appréhender WAMP et c’est l’essentiel.

  • Sam Post author

    @Zanguu: ah, pourtant, vu que c’est moi qui ait intégré le serveur de Dev, je suis pas trop fustré sur la question :)

  • Krypted

    Just génial ce tuto. On est vraiment gâtés. Je vais tester ça moi même sur un projet perso.

    2 petites questions :

    – Si j’ai bien compris, à chaque

     session.register(uuid + '.play', func)

    on enregistre un évènement sur le serveur WAMP.

    Est-ce que ces événements sont supprimés au bout d’un moment? Parce que j’imagine que sur un serveur qui tourne pendant longtemps ça doit prendre pas mal de ressources.

    – Si j’ai un client javascript qui dit qu’il register uuid. Est-ce que ça veut dire que le routeur wamp va envoyer toutes les nouvelles demandes RPC vers moi ?

    Bon après j’imagine qu’il doit y avoir des moyens de spécifier

  • Sam Post author

    Est-ce que ces événements sont supprimés au bout d’un moment?

    Dès qu’il n’y a plus de client qui fournit un callback pour ce namespace, il est supprimé du serveur. Mais bon, c’est vraiment faible comme consommation, ça risque pas grand chose.

    – Si j’ai un client javascript qui dit qu’il register uuid. Est-ce que ça veut dire que le routeur wamp va envoyer toutes les nouvelles demandes RPC vers moi ?

    Il me semble que le dernier arrivé est servit, mais il faudrait que je check. Dans notre cas, ça ne peut pas arriver facilement, l’ID est unique et jetable. Dans d’autres cas, il faut adapter sa solution à sa problématique.

    Bon après j’imagine qu’il doit y avoir des moyens de spécifier

    C’est à ça que servent les realms. On peut dire “ce realm là n’a pas le droit de déclarer un callback”.

  • Zanguu

    Question con (mais vraiment hein), il y a une limite à l’imbrication des callbacks ?
    Exemple, pour être plus clair :

    session.register(uuid + '.volume.up', func)
    session.register(uuid + '.volume.down', func)
    session.register(uuid + '.volume.bass', func)
    session.register(uuid + '.volume.mute', func)

    // exagération
    session.register(uuid + '.audio.language.default.add.main', func)

    L’intérêt est assez réduit pour le dernier bien sur mais pour le mute par exemple je trouve ça plus clair qu’un uuid.mute_volume.

  • Sam Post author

    Tu veux dire l’imbrication des namespaces. Non au contraire, ta version est bien plus propre, c’est juste que j’ai laissé ça de côté pour simplifier la démo.

  • manatlan

    Wouaw, just wouawww …
    J’avais déjà joué avec les websockets, avec nodejs, meteorjs et twisted … jadis.
    Je lisais avec grand intérêt tes posts sur WAMP, qui me permettait juste d’effleurer le potentiel du bousin. Mais sans jamais vraiment y plonger (l’api “pas flaskiest” ne me branchait pas du tout).
    J’ai récupéré tes sources, j’ai fait des register, des subscribe et des publish, de clients js et python : et just wouaw! ça appel/event dans tous les sens avec une simplicité déconcertante …
    Je n’ose même pas imaginer le potentiel de ce truc WAMP.
    Merci d’avoir posté ce post… Je crois que je suis tombé dedans : FAN !

  • manatlan

    Avec les nouvelles API “flaskiest” d’autobahn, on peut créer facilement un serveur.
    Et sauf erreur de ma part, j’ai beau matté le code, il n’y a pas encore d’api simple pour créer un client simple (juste pour consommer du serveur) ?

  • Sam Post author

    En fait, l’objet Application est un client simple.

    Pour ne pas démarrer de serveur, il suffit de faire :

    app.run(standalone=False)
  • artyprog

    Je viens de tester, c’est vraiment impressionnant
    Autobahn et Domotique devraient faire très bon ménage…

  • Robert

    Quand on y pense, c’est vraiment horrible d’écrire et du lire du Javascript, même si j’aime bien le concept de promises.

    J’ai de mon côté abandonné le dev web, fatigué des frameworks qui sortent tous les 4 matins, fatigué de la prédominance du Javascript, langage que je trouve imbitable, rien qu’à voir comment nos navigateurs rament à cause de lui, ça me dégoûte de plus en plus. Voir aussi toutes les appli de start up en carton qui nous pondent des milliers d’applications inutiles sur le net, de la merde en paquet de 10, je préfère encore le site à gifs animés de DarkVador (pour les plus vieux d’entre nous qui connaissaient).

    Entre nodejs, angular, jquery, meteor et tout le bin’s, on a du code final sale, non homogène et difficile à maintenir, une horreur : débogage pourri, gestion des erreurs pourrie…D’ailleurs les dev du web ne sont plus dev, ils deviennent comme nous, les admins, de simples intégrateurs de solutions, cela donne des appli au périmètre non maîtrisé et difficile à consolider dans la durée.

    Egalement horrible est le fait qu’on lie les lib javascript dynamiquement, dans le sens où on pointe une url…si on fait pas cela on se retrouve avec des api non mises à jour qui changent tous les 4 matins, difficile à maintenir. Si on le fait, on est obligé d’avoir une connexion au net pour l’application et les règles de firewall qui vont bien pour la production, bref ça aussi je trouve que c’est sale.

    L’écosystème JS, ça me fait penser à la TV d’aujourd’hui, que des programmes de merde, mais que finalement tout le monde regarde car c’est hype et à la mode et surtout qu’on a tous la tête dans le guidon car le peur du décrochage technique est réelle et anxiogène.

    Je vous le dit, dans 5 ans, sametmax feront du full JS, il n’y aura plus que cela…et des appli encore plus pourries que celles d’aujourd’hui.

    Sinon j’ai bien aimé votre article, même si le RPC a au moins 20 ans et que le pattern pub/sub on le connaît aussi depuis longtemps, donc rien de nouveau et la longue vie du marketing hype continue…

    Vivement le retour du VBScript ;) !

  • Sam Post author

    @artyprog: à la base ça a été créé pour l’Internet des objets, il se trouve que ça marche très bien pour le web aussi.

    @Robert: y a pas que la prog web dans la vie, donc si la prog web devient full JS, on ira voir ailleurs.

    Par ailleurs, comme je le dis dans les autres articles sur WAMP, ni RPC ni PUB/SUB ne sont nouveaux, ce qui est nouveau c’est :

    – le fait qu’on ait RPC __ET__ PUB/SUB dans le même protocole
    – la simplicité de mise en oeuvre
    – le fait que ça marche dans le navigateur
    – le fait que ça marche entre plusieurs techno (browser, python, nodejs, android, etc)

    WAMP ne permet, je le répète, en rien de faire quelque chose qu’on ne pouvait pas faire avant. Mais quelle techno le permet ? Par contre, il permet de le faire vraiment plus facilement.

  • rogererens

    Thanks for having put time into this tutorial!

    One sentence I did not understand (OK, it was translated by Google):

    Soon we may do so with asyncio Python 3.4, but unfortunately without the dev server.

    Do you mean to say that in a simple development environment

    python -m SimpleHTTPServer

    can not be replaced by

    python -m http.server ?

    If so, could you explain that in some more detail?

  • Sam Post author

    Without the WAMP dev server, which currently only exists for twisted.

    I’m going to provide a translation of this tutorial on tavendo’s blog soon. Cool that you could use GT to read it though. I never thought it would be good enough.

  • Tobias

    Hi Sam, thanks for another great post! This kind of hands-on, down to earth examples is highly welcome.

    A note regarding “running in production”: you can (obviously) have Crossbar run without proxy in front, and serving everything, including static Web. This is what we regularily do. Works great. No Apache, Nginx, whatever needed. Less wheels means less stuff can go wild.

    Docs are here: https://github.com/crossbario/crossbar/wiki#going-to-production

    Includes description on how to run on port 80 etc.

  • G-rom

    Wow super Sam ! Ça a de la gueule avec l’API à la flask et effectivement ça a l’air super simple.

    Je vais attendre que ça mûrisse un peu pour jouer avec en attendant, si jamais ça t’intéresse, ma config de base c’est RabbitMQ en broker de message, et stomp.js + sockjs pour le support des vieux navigateurs sans websockets. RabbitMQ a l’avantage d’avoir plein de clients dans plein de langages et de supporter (via plugin) STOMP via Websocket. Avec ça c’est assez simple de faire du PUB/SUB ou du RPC.

  • Foxmask

    Question conne : est-ce que ça a du sens d’utiliser cette techno pour interconnecter des services tiers dont on ne maîtrise pas le moment où les données arrivent? Genre des données tombent dans un flux RSS. Comment en être averti instantanément ?

  • G-rom

    Il te faudra forcément un lecteur de flux qui balance un événement sur un channel auquel une de tes app clientes aura souscrit.

    Genre un lecteur robot qui parse les flux des sites à intervalles régulier, ou qui souscrit à des event de ces sites des fois qu’ils en proposent (hey peut être qu’un jour ils feront du WAMP aussi de leur côté)

  • Sam Post author

    @Foxmask : le RSS est une techno basée sur le polling, donc tu es obligé, comme dit G-ROM, de faire un robot qui tourne en background et vérifie régulièrement tous ces flux. Après, utiliser cette techno va avoir deux avantages : c’est asynchrone, donc tu peux faire 1000 requêtes à 1000 flux en parallèle, et une fois que tu as l’info, tu peux la pusher à tous les autres clients en même temps immédiatement.

    Typiquement, pour ton trigger happy, ça veut dire que tu peux lancer des triggers non bloquants en même temps, et le premier qui retourne un résultat peut déclencher la propagation du dit résultat à tous les clients.

  • L'africain

    Les problèmes du tiers monde, c’est du pipi de chat à côté. Ils ont de la chance, eux, ils ne connaissent pas le streaming.
    On est en 2014 est vous avez toujours des idées aussi arriérée. Bravo! Je suis resté bouche bée aprés cette phrase.

  • Sam Post author

    On est en 2014, et il y a encore des gens qui ne comprennent pas le second degré. Bravo! Je vous colle un tampon après cette phrase.

  • SuzyOne

    Bonjour,

    Très bon article, j’ai néanmoins quelques questions :

    – peut-on utiliser cette récente API (bonne idée) en mode client ? Pour faire du publish vite fait ?

    – est-ce que dans le fichier de configuration de crossbar il ne faut pas mettre plutôt cela “url”: “ws://127.0.0.1:8080/ws” au lieu de “url”: “ws://127.0.0.1/ws” ?

    Merci.

  • Sam Post author

    Oui, cette API marche très bien pour une client, il suffit de faire app.run(standalone=False) et le serveur n’est pas démarré.

    Pour le port, je ne suis pas sûr. Je pense que la déclaration de port au dessus est suffisante. A vérifier.

  • Sigmun

    Bonjour,
    je ne suis pas du tout, mais alors pas du tout web-dev ou sys-admin. Pourtant, il y a une question qui, au risque de vous faire rire (mais bon, si en plus on peut se marrer, c’est déjà pas mal), me turlupine : est-ce que ca peut servir en remplacement de module python comme Pyro ? Dans mon cas, je me sers de Pyro pour faire du couplage de code, et même si j’adore, ça me contraint à utiliser du python pour l’interfaçage. Je me demandais je pouvais pas rendre ça plus souple…

    Sinon, super blog… Je ne fais que commencer à le défricher, mais je sens que je vais y passer du temps ! (heureusement que j’ai un collègue dans mon bureau, sinon il n’y aurait pas que la rubrique “Programmation” ;-))

  • keiser1080

    sam,
    super tuto domage qu’il n’y a pas de publish et suscribe cote python.
    l’API flaskesque est utilisable pour le publish ?
    j’ai chercher app.session pour faire un app.session.publish mais j’ai pas trouver de doc

  • Sam Post author

    Ouai y a l’api flaskesque maintenant. Faut chercher sur le blog, j’avais fais un tuto quand c’est sorti.

  • Sam Post author

    Je leur ai déjà reporté mais ils s’en occupent pas. C’est pour ça qu’ils ont besoin de quelqu’un je pense.

  • Stef42

    1er post et vraiment merci pour tout ce que vous faites.

    J’ai découvert WAMP grâce la publication des slides.

    Désolé, mais pour moi c’est bluffant !!!

    J’ai réussi à faire tourner la démo non sans mal :

    app.py semble ne pas lancer de serveur WAMP. Le programme bloque et CTRL+C indique qu’il ne peut pas se connecter.
    ca fonctionne dès qu’on lance le routeur crossbar (cf https://github.com/tavendo/AutobahnPython/issues/304)
    app.py :

    app.run(url="ws://localhost:8080/ws")

    les fichiers HTML : ‘:8080/’ remplacé par ‘:8080/ws’

    @keiser1080 : le subscribe en Python fonctionne. Mais toujours pas de publish.

    app.py :

    @app.subscribe('lecteur.volume')

    def onevent1(x):

    print("Volume du lecteur : ", x)

    index.html : publication d’un nouveau PUB sans l’UUID (je ne sais pas comment faire référence à uuid.volume qui existe déjà.

    session.publish('lecteur.volume', [video.volume]);

    La classe autobahn.twisted.wamp.Application a pas mal changé.

    Et en effet, les exemples fournis ne suivent pas trop le code (je sais c’est dur :-) )

    app.run(standalone=False)

    ne fonctionne plus par exemple.

  • Sam Post author

    Depuis la rédaction de l’article, l’API de crossbar a changé, et le routeur intégré n’existe plus dans autobahn. Je vais mettre à jour l’article.

  • Vincent

    Yop yop,

    Juste un truc, j’ai un peu galéré à faire fonctionner l’exemple. J’ai du rajouter /ws dans le app.run :

    app.run(url=u”ws://localhost:8080/ws”)

    Et dans le coup idem dans le JS aussi :

    url: ‘ws://’ + window.location.hostname + ‘:8080/ws’,

    Est-ce que j’ai loupé quelque chose ou est-ce qu’il faut mettre l’article à jour ? :)

    Merci en tout cas !

  • Vincent

    Sans vouloir être pénible, je pense que t’as oublié de modifier le code de la télécommande :-)

  • Sam Post author

    Au contraire, au contraire, tu m’aides beaucoup. J’ai tellement d’articles sur le blog, sans l’aide des lectures ils seraient tous complètement inutiles.

Comments are closed.

Des questions Python sans rapport avec l'article ? Posez-les sur IndexError.