Catégorie : Programmation     Tags :      

    Retrouvez cet article dans : Linux Magazine 100

    L’objectif de cet article est d’avoir une première approche de Twisted. Dans un premier temps, nous décrirons ce qu’est Twisted. Puis, nous verrons le coeur de celui-ci. Comme il est simple d’écrire une application serveur. Enfin, nous verrons comment mettre tout ça en pratique par l’écriture d’un programme de greylisting.
    Pour profiter de cet article, il est nécessaire d’avoir de solides compétences en Python [1]

    Présentation

    D’après le site web du projet : « Twisted est un moteur réseau événementiel écrit en Python et sous Licence MIT ». Cette licence (d’après Wikipédia [2]) est approuvée par OSI et compatible GPL. Pour faire simple, elle est proche de la licence BSD [3].
    Initialement, Twisted a été développé pour faire un RPG multijoueur ; il a bien dépassé ce rôle aujourd’hui. Twisted dispose d’une API très fournie.
    Voici les modules les plus intéressants :

    • twisted.protocols : contient des implémentations de beaucoup de protocoles. Les classiques comme SMTP, DNS, POP3, XMLRPC, etc. et d’autres plus funs comme OSCAR, SIP, MSN, etc.
    • twisted.internet : c’est le moteur asynchrone d’entrée/sortie et d’événements. C’est toute la partie reactor et callback que l’on va étudier en premier lieu.
    • twisted.web2 : le serveur web de Twisted, utile pour faire des interfaces graphiques pour vos applications ;-)
    • twisted.spread : module pour faire du développement distribué. Il permet d’aller plus loin que le simple développement client/serveur. Il est possible d’avoir des objets distants.
    • twisted.trial : la boite à outils pour faire des tests unitaires. Utilisée par Twisted lui-même.

    Je vous conseille d’aller jeter un petit coup d’oeil à l’API pour voir les autres modules existants [4].
    Dans cet article, nous verrons en profondeur la partie twisted.internet et un protocole module du twisted. protocols qui est le BasicListener.
    Avant d’entrer dans le vif du sujet, voici résumé des avantages/inconvénients concernant Twisted :
    Les avantages :

    • C’est du Python et ça c’est bon (ce point peut être un inconvénient pour certains).
    • L’API est de très haut niveau, du coup, on ne fait que se pencher sur les « vrais » problèmes.
    • L’API est récente, ainsi on profite des toutes dernières techniques de développement Objet (système de Factory, les interfaces par exemple).

    Les inconvénients :

    • L’API est assez peu documentée (hors code) et au début on a du mal à s’y retrouver.
    • Twisted semble en développement permanent, ainsi twisted.web n’était pas finalisé qu’il est passé obsolète, remplacé par twisted.web2.
    • Twisted reste complexe et il faut parfois tâtonner, lire la mailing-list [5] et du code pour trouver comment utiliser un module.
    • Enfin, Twisted (en particulier la partie twisted. internet) demande une nouvelle façon de concevoir son code. On ne peut pas passer facilement d’un code Python traditionnel à du code Python/Twisted.

    Réacteur et Callback

    Nous allons voir dans ce chapitre quel est le coeur de Twisted et ce qui change par rapport à un développement normal.

    Késako

    Le réacteur est l’ordonnanceur principal de Twisted. C’est lui qui déclenche les événements qui sont dans
    la file d’attente. Ces événements peuvent être un clic utilisateur sur l’interface graphique, un accès à une nouvelle page web de l’interface, une connexion réseau, etc.
    Pour donner un semblant de parallélisme, le développeur doit revoir sa façon de programmer. Il faut que le code soit segmenté en bouts de code non bloquants que nous appellerons callbacks. Par exemple, un serveur web codé en Twisted déclenche un callback lors de la connexion d’un client, un autre pour l’analyse de l’URL demandée et un troisième pour le traitement de la partie affichage.

    Dans un premier temps, tous les callbacks sont attachés à un objet de type Deferred sans être exécutés.
    Puis, lorsque le reactor le décide, le premier callback est appelé. À la fin de l’exécution de celui-ci, plutôt que d’exécuter le callback suivant, l’objet Deferred va rendre la main au reactor. Le reactor va jouer son rôle d’ordonnanceur et déclenchera plus tard le callback suivant. Ce jeu d’aller-retour entre les différents objets Deferred et le reactor permet à plusieurs clients d’avoir un traitement simultané de leurs requêtes.
    L’API de Twisted va nous permettre d’appeler des callbacks avec des fonctions qui sont normalement bloquantes (l’accès à une base de données, au filesystem, etc.). Bien sûr, ces fonctions sont exécutées dans un thread, mais c’est transparent pour le développeur.
    Le reactor possède une méthode callLater qui permet de demander l’exécution d’une fonction lorsque c’est possible à partir d’un instant t. La signature de cette méthode est la suivante : callLater( t, f, arg ) où t est le délai avant d’exécuter la méthode, f la méthode et arg l’éventuel argument.

    Exemples de code simple

    Nous allons voir dans cette partie l’utilisation basique des callbacks. Tout d’abord, un simple « hello world ».

    Hello word

    Un peu de code :

    01. # -*- python -*-
    02. # -*- coding: utf-8 -*-
    03.
    04. from twisted.internet import reactor
    05. from twisted.internet import defer
    06.
    07. def firstCallback( ref ) :
    08. print «Hello World. Callback, from «, ref
    09.
    11.
    12. if __name__ == ‘__main__’ :
    13. d = defer.Deferred()
    14. d.addCallback( callback=firstCallback )
    15. reactor.callLater( 0, d.callback, «init call» )
    16. reactor.callLater( 1, reactor.stop )
    17. reactor.run()
    • Lignes 7 à 9 : une fonction callback non bloquante, avec juste un affichage sur la sortie standard. Une fonction callback prend comme argument le retour de la fonction précédente ou le paramètre initial.
    • Lignes 13 et 14 : on crée un objet Deferred, puis on y ajoute un callback qui sera déclenché. Ici, on ne fait qu’enregistrer le callback, rien n’est encore exécuté.
    • Ligne 15 : on demande à l’ordonnanceur l’exécution du callback avec comme paramètre la chaîne init call. Le premier paramètre indique dans combien de temps (environ) la fonction doit s’exécuter. Ici c’est 0 seconde, soit le plus tôt possible.
    • Ligne 16 : on ajoute à l’ordonnanceur la fonction pour arrêter le réacteur dans 1 seconde.
    • Ligne 17 : enfin, on lance le réacteur. C’est à ce moment uniquement que le code se déclenche. D’abord par l’affichage, puis une seconde plus tard le programme rend la main.

    Flux d’exécution et flux d’erreur

    Explication théorique

    L’utilisation des callbacks va nous permettre d’avoir deux flux d’exécution. Un pour le fonctionnement normal et l’autre pour les erreurs. On peut ainsi représenter les deux flux :

    /img-articles/lm/100/twisted/fig-1.jpg

    Supposons que nous sommes dans le callback 1. Nous avons deux façons de sortir de cette fonction :
    1. Par un return (ou la fin de la fonction qui correspond à un return None). Le flux passe dans le callback 2 suivant en lui passant en paramètre l’objet retourné par le callback 1.
    2. Par la levée d’une exception ou par le retour d’un objet twisted.python.failure.Failure (causé par la méthode defer.fail par exemple). Le flux passe dans le errback 1 en lui passant en paramètre l’exception. Si, maintenant, nous sommes dans un errback, nous avons deux possibilités d’en sortir :
    3. Par un return. Cela signifie que le programme a réussi à résoudre le problème détecté dans le callback 1. Le flux d’exécution peut donc continuer.
    4. Par la levée d’une exception ou par le retour d’un objet twisted.python.failure.Failure.

    Dans ce cas, on n’a pas réussi à résoudre le problème.
    On passe dans le errback 2.

    Explication par l’exemple

    Voici un code pour illustrer le mécanisme expliqué précédemment. Ce code ne fait qu’empiler des callback.
    En fonction des _call_ok et _err_ok, le flux passe dans la partie exécution ou la partie d’erreur :

    01. # -*- python -*-
    02. # -*- coding: utf-8 -*-
    03.
    04. from twisted.internet import reactor, defer
    05.
    06.
    07. first_call_ok = 1
    08. first_err_ok = 0
    09.
    10. second_call_ok = 0
    11. second_err_ok = 0
    12.
    13. third_call_ok = 1
    14. third_err_ok = 0
    15.
    16.
    17. def firstCallback( ref ) :
    18. print «First Callback from «, ref
    19. if first_call_ok :
    20. return «First call»
    21. else :
    22. raise Exception( «First err from call1» )
    23.
    24.
    25. def firstErrback( ref ) :
    26. print «First Errback from «, ref.getErrorMessage()
    27. if first_err_ok :
    28. return «First err»
    29. else :
    30. raise Exception( «First err from err1» )
    31.
    32.
    33. def secondCallback( ref ) :
    34. if type( ref ) != type( «» ) : ref = ref.getErrorMessage()
    35. print «Second Callback from «, ref
    36. if second_call_ok :
    37. return «Second call»
    38. else :
    39. raise Exception( ‘Sec err’ )
    40.
    41.
    42. def secondErrback( ref ) :
    43. print «Second Errback from «, ref.getErrorMessage()
    44. if second_err_ok :
    45. return defer.succeed( ‘second fail’ )
    46. else :
    47. return defer.fail( Exception( «Second err» ) )
    48.
    49.
    50.
    51. def thirdCallback( ref ) :
    52. if type( ref ) != type( «» ) : ref = ref.getErrorMessage()
    53. print ( «Third Callback from « % ref )
    54. if third_call_ok :
    55. return «Third call»
    56. else :
    57. raise Exception( «Third Call» )
    58.
    59. def thirdErrback( ref ) :
    60. print «Third Errback from «, ref.getErrorMessage()
    61. if third_err_ok :
    62. return ‘Thrid Err’
    63. else :
    64. raise Exception( «Third err» )
    65.
    66.
    67. def fourBack( ref ) :
    68. if type( ref ) != type( «» ) : ref = ref.getErrorMessage()
    69. print «Four Call & Err back from», ref
    70.
    71.
    72. if __name__ == ‘__main__’ :
    73.
    74. d = defer.Deferred()
    75. d.addCallback( callback=firstCallback )
    76. d.addErrback( errback=firstErrback )
    77. d.addCallbacks( callback=secondCallback, errback=secondErrback )
    78. d.addCallbacks( callback=thirdCallback, errback=thirdErrback )
    79. d.addBoth( callback=fourBack )
    80.
    81. reactor.callLater( 0, d.callback, «init call» )
    82. reactor.callLater( 1, reactor.stop )
    83. reactor.run()
    • Lignes 7 à 14 : on initialise des paramètres qui nous permettront d’étudier le mécanisme des callback.
    • Lignes 17 à 22 : tous les callback vont avoir cette forme :
    • un affichage ;
    • un test du paramètre associé au callback ;
    • en fonction du résultat du test, on retourne un succès et on reste dans la chaîne d’appel ou on retourne une exception et on passe dans la chaîne des erreurs.
    • Lignes 25 à 30 : l’ensemble des errbacks va avoir cette forme :
    • un affichage : attention, on affiche une chaîne extraite d’un objet twisted.python.failure Failure ;
    • un test du paramètre associé au errback ;
    • en fonction du résultat du test, on retourne un succès et on repasse dans la chaîne d’appel ou on retourne une exception et on reste dans la chaîne des erreurs.
    • Lignes 42 à 47 : une petite finesse dans ce callback. Ici, on utilise une méthode de defer plutôt que de faire un return pour passer au callback suivant. Dans le cas d’une erreur, on retourne un appel à defer.fail avec une exception en paramètre. Ce callback n’a rien d’exceptionnel. Il permet juste de montrer une autre façon de faire.
    • Lignes 72 à 79 : le coeur du code. C’est ici que l’on crée notre file d’exécution.
    • ligne 74 : ajout du premier callback au deferred ;
    • ligne 75 : ajout du premier errback au deferred ;
    • lignes 77 et 78 : ajout des deuxième et troisième callback et errback au deferred ;
    • ligne 79 : ajout d’une fonction qui sera callback et errback.
    • Lignes 81 à 83 : on place dans l’ordonnanceur le déclenchement au plus tôt du premier callback du deferred et, une seconde après, l’arrêt du réacteur.

    Le schéma suivant représente le graphe de callbacks du code précédent. Les liens numérotés nous montrent la chaîne de callbacks résultant de le file d’exécution.

    /img-articles/lm/100/twisted/fig-2.jpg

     Ici, on a une petite finesse. Lors de la création des callback, on utilise une fonction qui permet d’ajouter un callback et un errback. Attention, le placement dans ce cas des callback et des errback dans le deferred n’est pas le même qu’en utilisant successivement les appels à addCallback et addErrback. Voici ce qu’on aurait eu si au lieu d’avoir l’ajout du premier callback et errback en deux étapes on l’avait en une fonction (addCallbacks) comme pour les autres.

    /img-articles/lm/100/twisted/fig-3.jpg

    On peut remarquer que le premier errback ne sert à rien.

    Création d’un serveur TCP

    Dans cette partie, nous allons voir la puissance de Twisted en ce qui concerne l’écriture d’applications réseau. Pour commencer, le traditionnel echo.

    Hello world

    Le code

    Le code pour le serveur TCP. Un client se connecte en mode texte (type telnet) et saisit une ligne. Le serveur l’affiche et déconnecte le client :

    01. # -*- python-mode -*-
    02. # -*- coding: utf-8 -*-
    03.
    04. from twisted.internet import protocol, reactor, defer
    05. from twisted.protocols import basic
    06.
    07. class WriteAndQuitProto( basic.LineReceiver ):
    08.
    09. def connectionMade(self):
    10. self.transport.write( «»»
    11. ------------------------------------------------------------
    12. Serveur Twisted TCP
    13. ------------------------------------------------------------
    14. «»»)
    15.
    16. def lineReceived( self, data ) :
    17. d = defer.Deferred()
    18. d.addCallback( self.write )
    19. reactor.callLater( 0, d.callback, data )
    20.
    21. def write( self, data ) :
    22. print «data: «, data
    23. self.transport.loseConnection()
    24.
    25.
    26. class WriteAndQuitFactory( protocol.ServerFactory ):
    27. protocol = WriteAndQuitProto
    28.
    29.
    30. if __name__ == ‘__main__’ :
    31. reactor.listenTCP( 1079, WriteAndQuitFactory() )
    32. print «Pour quitter faire Ctrl-C»
    33. reactor.run()

    Pour tester ce code, copiez-le dans un fichier tcp.py, puis exécutez-le en ligne de commandes (python tcp. py). Dans un autre terminal, faites telnet localhost 1079 et saisissez une ligne.

    Explication

    • Ligne 5 : on importe le module basic. Ce module contient l’ensemble des protocoles de Twisted.
    • Lignes 7 à 23 : écriture d’une classe WriteAndQuitProto qui implémente write et surcharge connectionMade et lineReceived :
      • connectionMade est exécuté à chaque fois qu’une nouvelle connexion s’établit. On en profite pour faire une bannière à notre serveur.
      • lineReceived est exécuté à chaque fois qu’une ligne est envoyée au serveur. C’est ici qu’on choisit d’initialiser notre graphe de callback et de l’exécuter.
      • write : le callback ne fait rien d’autre que d’écrire sur la sortie standard la ligne envoyée par le client. Ensuite la connexion est coupée.
    • Lignes 26 à 27 : le principe des factories est utilisé ici. Une factory est une classe qui instancie des objets dont le type n’est pas forcement défini auparavant. On a donc une classe WriteAndQuitProto qui est commune à l’ensemble des connexions. Pour chaque nouvelle connexion, un nouvel objet WriteAndQuitProto est instancié par la factory.
    • Lignes 30 à 33 : on lie la factory au port 1079 par l’intermédiaire du reactor et on lance le réacteur.

    On voit que c’est très simple de faire un serveur TCP qui agit en fonction des lignes de texte qu’il reçoit. Je vous propose donc de voir un cas concret avec une implémentation de greylisting pour lutter contre le spam.

    Cas concret : un greylisting

    Le graphe de callbacks que nous avons codé dans la première partie ne reflète pas vraiment la réalité. D’habitude, il y a plus de callback que de errbacks.

    On ajoute des errbacks uniquement si on est capable de traiter les erreurs levées. Si on ne peut pas les traiter, alors ce n’est pas la peine d’avoir des errbacks intermédiaires. Enfin, il est toujours bon d’ajouter un dernier errback générique qui ne fait qu’indiquer une erreur.
    Pour illustrer tout ça, nous allons voir comment mettre en place un graphe de callback pour le cas du greylisting.

    C’est quoi le greylisting

    Le greylisting est une méthode pour rechercher les spammeurs qui n’utilisent pas de réel serveur de mail, mais des programmes d’envoi en masse. Pour cela, à chaque nouveau mail, on utilise un code d’erreur SMTP niveau 400 indiquant à l’expéditeur qu’on est surchargé et qu’il faut faire l’envoi plus tard. Le serveur de mail retient cette tentative (en stockant la trace dans une base de données par exemple). Pour l’expéditeur, deux cas se présentent :

    • L’expéditeur est un serveur mail : dans ce cas, il connaît ce code et sait le traiter. Typiquement, le relais émetteur stocke le mail dans une file d’attente et tentera de renvoyer le mail plus tard (1/2 h par exemple).
    • L’expéditeur est un sale spammeur : dans ce cas, ce type d’erreur n’est pas traité et le mail est perdu, mais ce n’est pas grave vu que c’est un spam.

    Lorsque ce mail sera réémis par le relais distant, notre serveur retrouvera la trace qu’il en a gardée et il sera accepté.
    La technique est assez efficace, mais elle a l’inconvénient d’allonger le délai de réception d’environ une demieheure, ce qui est un peu long. Pour limiter cela, on stocke dans une base de données des informations concernant les mails qui ont passé avec succès l’étape précédente. Si on reçoit un mail et qu’il y a déjà ses caractéristiques dans la base de données, on le laisse
    passer immédiatement.
    Il ne reste qu’à faire un nettoyage de cette base pour éviter qu’elle ne gonfle à l’infini.
    Voici une structure possible pour celle-ci, se basant sur une seule table contenant un tuple par mail :

    • Adresse de l’expéditeur. Type adresse mail.
    • Adresse du serveur expéditeur. Type adresse IP.
    • Destinataire du mail. Type adresse mail.
    • Heure d‘envoi du mail. Type time.

    La clef étant les 3 premiers éléments.
    Pour que le greylisting soit efficace, il faut que celui-ci se fasse en amont de la réception du mail, avant même que son corps ne soit envoyé. Dans Postfix (le fantastique MTA), le paramètre check_policy_service permet de faire cela. Lorsqu’il est renseigné, Postfix fait une demande à un programme extérieur (via TCP) avant d’accepter la réception de celui-ci. Voici par exemple les paramètres que Postfix envoie

    request=smtpd_access_policy
    protocol_state=RCPT
    protocol_name=SMTP
    helo_name=some.domain.tld
    queue_id=8045F2AB23
    sender=foo@bar.tld
    recipient=bar@foo.tld
    client_address=1.2.3.4
    client_name=another.domain.tld
    instance=123.456.7
    sasl_method=plain
    sasl_username=you
    sasl_sender=
    size=12345
    [empty line]

    On a ici toutes les informations que l’on souhaite. En fonction du résultat de notre recherche, le programme devra répondre :

    action=defer_if_permit Service temporarily unavailable
    [empty line]

    si le mail n’a pas été trouvé dans la base de données.
    Ou

    action=dunno
    [empty line]

    si c’est ok et qu’on accepte le mail.

    Graphe de callback

    Pour chaque ligne reçue, nous la stockons dans un dictionnaire qu’on appelle mail_value. Une fois qu’on a une ligne vide, c’est qu’on a reçu l’ensemble des paramètres. On ne garde que ceux qui nous intéressent sans mail et on lance la fonction launch qui initialise et déclenche la chaîne de callback. Voici le schéma des callbacks :

    /img-articles/lm/100/twisted/fig-4.jpg

    Et le code associé :

    def launch( self ) :
    d = defer.Deferred()
    d.addCallback( self.search )
    d.addCallback( self.analyse )
    d.addCallback( self.add )
    d.addCallback( self.console_message)
    d.addErrback( self.response_error )
    d.addCallback( self.response )
    d.addErrback( self.generic_error )
    reactor.callLater( 0, d.callback, None )

    Ici, à la fin de notre graphe de callbacks, on place un errback générique qui récupère toutes les erreurs pour les afficher à la console. Ainsi, on est sûr de ne pas louper d’exception qui serait levée.
    De même, il n’y a pas de lien entre errback response_ error et response. response_error retourne au serveur de mail de laisser passer le message. La réponse au serveur est effectuée dans cette méthode.

    Avec le reste autour, on obtient :

    # -*- python-mode -*-
    # -*- coding: utf-8 -*-
    from twisted.internet import reactor, defer, protocol
    from twisted.protocols.basic import LineReceiver
    import time
    FIND_OK = 0
    FIND_NOK = 1
    ADD = 2
    DELAY = 30 # Temps a attendre (en seconde)
    class GreyListingProto( LineReceiver ):
    delimiter = ‘\n’
    def __init__( self ) :
    self.mail_value = {}
    def lineReceived( self, line ) :
    if not line == «» :
    value = line.split( ‘=’ )
    self.mail_value[ value[0] ] = value[1]
    else :
    self.mail = ( self.mail_value[ ‘sender’ ],
    self.mail_value[ ‘client_address’ ],
    self.mail_value[ ‘recipient’ ],
    time.time() )
    reactor.callLater( 0, self.launch )
    def search( self, ref ) :
    for entry in self.factory.maildb :
    if entry[ :3 ] == self.mail[ :3 ] :
    return entry
    return None
    def analyse( self, entry ) :
    print «entry : «, entry
    if entry is not None :
    print «%f > %f» % (self.mail[ 3 ], entry[ 3 ] + DELAY )
    if self.mail[ 3 ] > entry[ 3 ] + DELAY :
    return FIND_OK
    else :
    return FIND_NOK
    return ADD
    def add( self, todo ) :
    if todo != ADD :
    return todo
    self.factory.maildb.append( self.mail )
    return todo
    def console_message( self, todo ):
    if todo == FIND_OK :
    print «Ok, found the mail and it’s OK»
    elif todo == FIND_NOK :
    print «Ok, found the mail but delay is not expired»
    elif todo == ADD :
    print «It’s the first time we see that mail»
    else :
    print «Hum... What have we to do ?»
    return todo
    def response( self, todo ) :
    if todo == FIND_OK :
    self.transport.write( «action=dunno\r\n» )
    else :
    self.transport.write( «action=defer_if_permit Service temporarily un available\r\n» )
    self.transport.write( «\r\n» )
    self.transport.loseConnection()
    def response_error( self, error ) :
    self.transport.write( «action=dunno\r\n» )
    self.transport.write( «\n» )
    self.transport.loseConnection()
    return defer.fail( error )
    def generic_error( self, error ) :
    print «Error !»
    print error
    def launch( self ) :
    d = defer.Deferred()
    d.addCallback( self.search )
    d.addCallback( self.analyse )
    d.addCallback( self.add )
    d.addCallback( self.console_message)
    d.addErrback( self.response_error )
    d.addCallback( self.response )
    d.addErrback( self.generic_error )
    reactor.callLater( 0, d.callback, None )
    class GreyListingFactory( protocol.ServerFactory ):
    protocol = GreyListingProto
    maildb = []
    if __name__ == ‘__main__’ :
    reactor.listenTCP( 1025, GreyListingFactory() )
    print «Pour quitter faire Ctr-C»
    reactor.run()

    Remarques

    On peut voir qu’avec une centaine de lignes de code, on peut écrire un programme de greylisting. En plus, nous n’avons pas de problème d’accès concurrent à la base de données, car nous n’utilisons pas de thread, les points d’arrêts du parallélisme sont contrôlés. Il nous ne reste plus qu’à rendre le code plus robuste, à avoir une base de données persistante en cas d’arrêt du service et une routine pour nettoyer la base périodiquement.

    Conclusion

    Dans cet article, on a pu voir rapidement la puissance de Twisted, en particulier le système de callbacks qui nous donne un semblant de parallélisme sans faire trop d’effort et en nous affranchissant des problèmes classiques de verrou et d’accès concurrent. De même, l’implémentation d’un protocole est extrêmement simplifiée par l’API de Twisted.

    Liens:

    Retrouvez cet article dans : Linux Magazine 100

    Posté par Sylvain de Tilly (stilly) | Signature : Sylvain de Tilly | Article paru dans Creative Commons License

    Laissez une réponse

    Vous devez avoir ouvert une session pour écrire un commentaire.


    • Il y a actuellement

    • 633 articles/billets en ligne.