Websocket Python avec Crossbar

crossbar

Un serveur web fonctionne généralement dans un environnement dit "client-serveur": Un client (navigateur) fait une demande à un serveur et celui-ci lui retourne une réponse. La communication est dite unidirectionnelle, c'est à dire que le client est obligé de faire une demande au serveur pour mettre à jour une information. Le serveur ne peut intervenir de son initiative, il est passif, il attend les rêquetes des clients et y répond.

Je vais prendre un exemple souvent utilisé pour comprendre comment fonctionne un serveur web: le chat. Prenons le cas de deux personnes qui discutent à travers un site web. Leur communication se fait via un serveur et leur navigateur est obligé de faire des routines qui checkent toutes les X secondes si un nouveau message a été envoyé au serveur.

On se rend rapidement compte que non seulement la réponse n'est pas instantannée - elle depend des X secondes - mais en plus on sollicite énormément le serveur alors que ce n'est pas nécessaire. La plupart du temps le serveur renvoie une réponse vide.

Mais comment recevoir les messages en temps réel et recevoir directement la réponse du serveur sans avoir à le solliciter?

Websocket

Le protocole websocket a pour objectif de créer un canal de communication bidirectionnelle (ou full-duplex), c'est à dire un canal qui transporte l'information dans les deux sens.

Pour faire simple il fait autant que la logique "client-serveur" mais en plus il est capable d'envoyer des push directement au client (navigateur).

Dans l'exemple du chat vu précédemment il est donc possible d'envoyer une information directement au client lorsque le serveur reçoit un signal.

Crossbar

crossbar Il existe plusieurs soft pour travailler en websocket mais comme le sujet du site est Python et que Crossbar a été developpé en python, restons sur cette merveilleuse technologie. De plus Crossbar est très bien documenté et compatible avec d'autres langages comme le PHP, C++, etc.

Installer Crossbar

Pour installer crossbar sur Ubuntu: Installer Crossbar sur Ubuntu

Configurer Crossbar

Créez un dossier (qu'importe l'endroit) et créez un environnement virtuel:

mkdir cb
cd cb
virtualenv .
source bin/activate

Puis installez la lib Crossbar

pip install crossbar[all]

Et vérifiez que celui-ci a bien été installé

crossbar version

Premier projet Crossbar

On n'aime jamais partir de rien. Crossbar vous propose de découvir son soft avec quelques exemples dans la langage que vous désirez.

Lancez la commande suivante dans le dossier racine de votre projet:

crossbar init --template hello:python

On remarque qu'un dossier .crossbar a été crée ainsi qu'un dossier hello

Lançons maintenant notre serveur crossbar:

crossbar start

On remarque qu'un compteur a été lançé automatiquement. Si on ouvre un navigateur à l'adresse suivante: localhost:8080 non seulement nous avons une page web mais lorsque l'on ouvre notre console (F12 généralement) on remarque que le navigateur reçoit les données du serveur. Nous avons crée un communication bidirectionnelle sans rien faire! Bravo!

Comprendre Crossbar

Que cela fonctionne c'est une chose, maintenant essayons de comprendre le mécanisme de toute cette jolie technologie.

Tout d'abord nous allons nous intéresser au fichier .crossbar/config.json et aux lignes suivantes:

"components": [
    {
       "type": "class",
       "classname": "hello.hello.AppSession",
       "realm": "realm1",
       "transport": {
          "type": "websocket",
          "endpoint": {
             "type": "tcp",
             "host": "127.0.0.1",
             "port": 8080
          },
          "url": "ws://127.0.0.1:8080/ws"
       }
    }
 ]

Il existe donc un lien entre ws://127.0.0.1:8080/ws et hello.hello.AppSession. Etudions ce dernier:

# hello/hello.py

counter = 0
while True:

    # PUBLISH an event
    #
    yield self.publish('com.example.oncounter', counter)
    print("published to 'oncounter' with counter {}".format(counter))
    counter += 1

Nous avons trouvé notre compteur et nous savons maintenant comment envoyé un push.

Comment le navigateur récupère ces push envoyés? Etudions le code source de notre HTML et recherchons com.example.oncounter:

# hello/web/index.html

session.subscribe('com.example.oncounter', on_counter).then(
   function (sub) {
      console.log('subscribed to topic');
   },
   function (err) {
      console.log('failed to subscribe to topic', err);
   }
);

On remarque donc qu'il existe une notion de publish et de subscribe.

Publish & Subscribe

Avec le pattern subscribe publish il est donc possible:

1.) De s'abonner pour recevoir des évènements publiés.
2.) De publier des évènements que d'autres composants peuvent recevoir.

Register & Call

Crossbar propose également le pattern Register Call.
Tout composant peut:

1.) Créer une procédure que d'autres composants peuvent appeler.
2.) Appeler des procédures enregistrés par d'autres composants.

Reprenons l'exemple hello proposé par crossbar.

Notre register (en python):

# hello/hello.py

def add2(x, y):
    print("add2() called with {} and {}".format(x, y))
    return x + y

reg = yield self.register(add2, 'com.example.add2')

Et notre caller (en javascript):

# hello/web/index.html

session.call('com.example.add2', [x, 18]); 

L'inverse est possible également, c'est à dire mettre un caller dans python et un register en javascript.

Syntaxe

Crossbar fonctionne avec plusieurs langages, le site propose les exemples suivants pour comprendre la syntaxe des différentes actions:

En javascript

// 1) Abonner le script à un sujet pour recevoir des évènements 
function onhello(args) {
   console.log("Got event:", args[0]);
}
session.subscribe('com.myapp.hello', onhello);

// 2) Publier un évènement
session.publish('com.myapp.hello', ['Hello, world!']);

// 3) Inscrire un procédure pour pouvoir l'appeler à distance
function add2(args) {
   return args[0] + args[1];
}
session.register('com.myapp.add2', add2);

// 4) Appeler une procedure à ditance
session.call('com.myapp.add2', [2, 3]).then(
   function (result) {
      console.log("Got result:", result);
   }
);

Et en python:

# 1) Abonner le script à un sujet pour recevoir des évènements 
def onhello(msg):
   print("Got event: {}".format(msg))
session.subscribe(onhello, 'com.myapp.hello')

# 2) Publier un évènement
session.publish('com.myapp.hello', 'Hello, world!')

# 3) Inscrire un procédure pour pouvoir l'appeler à distance
def add2(x, y):
   return x + y
session.register(add2, 'com.myapp.add2');

# 4) Appeler une procedure à ditance
result = yield from session.call('com.myapp.add2', 2, 3)
print("Got result: {}".format(result))

Realms

crossbar Crossbar utilise les realms comme domaine pour séparer le routing et l'administration.

Chaque session entre Crossbar et un client est toujours associé à un realm spécifique. Les clients qui ne sont pas dans le meme realm ne peuvent communiquer entre eux.

Prenons un exemple:

Client1 est associé à realm1
Client2 est associé à realm1
Client3 est associé à realm2

Client2 et Client3 sont abonnés à l'évènement com.exemple.mytopic.
Client1 publie l'évènement, seul Client2 recevra l'évènement car il est dans le même domaine.

Il est donc nécessaire d'indiquer le realm sur le client:

var connection = new autobahn.Connection({url: 'ws://127.0.0.1:9000/', realm: 'realm1'});

Et la config:

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

L'autentification

Crossbar prend en charge les sessions d'authentification WAMP. Il en existe plusieurs, nous verrons comment utiliser WAMP-Challenge-Response-Authentication que l'on peut abbréger en WAMP-CRA.

WAMP-CRA est un mécanisme d'authentification challenge-response utilisant un secret partagé entre le serveur et le client. Le secret ne se déplace jamais dans le canal donc WAMP-CRA peut être utilisé dans des connexions non TLS. WAMP-CRA prend également en charge l'utilisation des salt password.

# authenticator.MyAuthenticator

from twisted.internet.defer import inlineCallbacks
from autobahn.twisted.wamp import ApplicationSession
from autobahn.wamp.exception import ApplicationError

class MyAuthenticator(ApplicationSession):

   USERDB = {
      'joe': {
         # these are required:
         'secret': 'secret2',  # the secret/password to be used
         'role': 'frontend'    # the auth role to be assigned when authentication succeeds
      },
      'peter': {
         # use salted passwords

         # autobahn.wamp.auth.derive_key(secret.encode('utf8'), salt.encode('utf8')).decode('ascii')
         'secret': 'prq7+YkJ1/KlW1X0YczMHw==',
         'role': 'frontend',
         'salt': 'salt123',
         'iterations': 100,
         'keylen': 16
      }
   }

   @inlineCallbacks
   def onJoin(self, details):

      def authenticate(realm, authid, details):
         print("authenticate called: realm = '{}', authid = '{}', details = '{}'".format(realm, authid, details))

         if authid in self.USERDB:
            # return a dictionary with authentication information ...
            return self.USERDB[authid]
         else:
            raise ApplicationError("com.example.no_such_user", "could not authenticate session - no such user {}".format(authid))

      try:
         yield self.register(authenticate, 'com.example.authenticate')
         print("custom WAMP-CRA authenticator registered")
      except Exception as e:
         print("could not register custom WAMP-CRA authenticator: {0}".format(e))

Configurons notre realm

"realms": [
{
 "name": "realm1",
 "roles": [
  {
   "name": "authenticator",
   "permissions": [
      {
       "uri": "com.example.authenticate",
       "register": true
      }
   ]
  }
}

Transport

"transports": [
...
 "ws": {
     "type": "websocket",
     "auth": {
        "wampcra": {
           "type": "dynamic",
           "authenticator": "com.example.authenticate"
        }
     }
  }
]

Et les composants:

"components": [
  {
     "type": "class",
     "classname": "authenticator.MyAuthenticator",
     "realm": "realm1",
     "role": "authenticator"
  },
]

Django

Si vous voulez lancer un évènement depuis Django:

pip install crossbarconnect
import crossbarconnect

client = crossbarconnect.Client("http://127.0.0.1:8080/notify")
client.publish("mon_event", "1")
"realms": [
  {
  "name": "realm1",
  "roles": [
    {
       "name": "anonymous",
       "permissions": [
          {
             "uri": "*",
             "publish": true,
             "subscribe": true,
             "call": true,
             "register": true
          }
       ]
    }
  }
]
"transports": [
  ...
  "notify": {
    "type": "publisher",
    "realm": "realm1",
    "role": "anonymous",
    "options" : { 

    "require_ip" : ["127.0.0.1"]

    }
  }
]

Plus d'informations sur Crossbar: Crossbar offciel

Dernière mise à jour le 15 avril 2015