Python et le réseau

Vincent Bernat

Note

Cet ar­ticle a été pu­blié dans GNU/Linux Ma­ga­zine HS n° 40 en 2009. Il est re­pro­duit ici avec de lé­gères re­touches cos­métiques.

Comme de nom­breux lan­gages, Py­thon pro­pose tout l’ar­se­nal né­ces­saire pour construire des ap­pli­ca­tions ré­seau. La bi­blio­thèque stan­dard pro­pose de quoi écrire aussi bien des ser­veurs que des clients. Nous al­lons voir dans cet ar­ticle les dif­fé­rentes pos­si­bi­li­tés of­fertes par Py­thon dans ce domaine.

Cet ar­ticle se dé­com­pose en quatre par­ties. Nous al­lons déjà ex­plo­rer les pos­si­bi­li­tés of­fertes par Py­thon pour écrire des clients ré­seau à l’aide de la bi­blio­thèque stan­dard. En­suite, nous abor­de­rons la pro­blé­ma­tique des ser­veurs. Dans ces deux pre­mières par­ties, nous uti­li­se­rons des pri­mi­tives de haut ni­veau, mises à dis­po­si­tion par Py­thon, ac­cé­lé­rant gran­de­ment l’écri­ture des ap­pli­ca­tions. Nous ver­rons en­suite dans la troi­sième par­tie com­ment uti­li­ser les pri­mi­tives équi­va­lentes à ce qui se trouve dans les pro­grammes écrits en C. Py­thon per­met, en ef­fet, à l’aide de ces pri­mi­tives, de mi­mer le com­por­te­ment de tout ou­til ré­seau écrit ha­bi­tuel­le­ment en C. En­fin, afin de mon­trer une ap­proche toute dif­fé­rente, nous ex­plo­re­rons les pos­si­bi­li­tés du fra­me­work ré­seau Twis­ted dans la der­nière partie.

Écrire un client#

Py­thon dis­pose dans sa bi­blio­thèque stan­dard de très nom­breux mo­dules per­met­tant d’écrire très fa­ci­le­ment des clients pour une grande va­riété de pro­to­coles dont, par exemple, HTTP, FTP, POP, IMAP, SMTP et Telnet.

D’autres pro­to­coles sont sup­por­tés et il est tou­jours pos­sible d’écrire un mo­dule sup­por­tant son propre pro­to­cole ou de trou­ver son bon­heur parmi les mo­dules pro­po­sés par de tierces par­ties. Nous al­lons nous concen­trer sur quelques exemples d’uti­li­sa­tion de cer­tains des mo­dules ci­tés ci-dessus.

Ceux-ci sont des mo­dules de haut ni­veau. Ils ef­fec­tuent en une seule com­mande de nom­breuses tâches pour éta­blir la connexion sans qu’il soit né­ces­saire de s’oc­cu­per des dé­tails. Ainsi, par exemple, le mo­dule FTP ex­pose une simple fonc­tion pour en­voyer un fi­chier au ser­veur. Cette fonc­tion s’oc­cupe de né­go­cier l’ou­ver­ture du ca­nal de don­nées propre au ser­veur FTP et gère le mode ac­tif ou pas­sif au­to­ma­ti­que­ment. Au­tant de dé­tails dont on ne sou­haite pas for­cé­ment s’oc­cu­per quand on veut sim­ple­ment écrire un pro­gramme qui va dé­po­ser ou ré­cu­pé­rer quelques fi­chiers à l’aide du pro­to­cole FTP.

Nous ver­rons par la suite com­ment écrire des ap­pli­ca­tions de plus bas ni­veau et gé­rer l’en­semble des échanges réseau.

Un simple client HTTP#

Pour écrire un client HTTP, le plus simple est de faire ap­pel au mo­dule urllib2. Il n’est pas li­mité aux re­quêtes HTTP puisqu’il est des­tiné à gé­rer des URL et, à ce titre, sait prendre en compte d’autres pro­to­coles, comme FTP ou GOPHER et peut être étendu. Ce­pen­dant, sa prin­ci­pale uti­li­sa­tion reste HTTP. Parmi les fonc­tion­na­li­tés sup­por­tées par ce mo­dule, ci­tons la ges­tion de l’au­then­ti­fi­ca­tion, la ges­tion des ser­veurs man­da­taires (proxies), les re­di­rec­tions web et les co­okies. Nous n’al­lons pas ex­plo­rer l’en­semble des fonc­tion­na­li­tés dis­po­nibles. Aussi, pour ap­pro­fon­dir, il est conseillé d’al­ler lire la do­cu­men­ta­tion dis­po­nible en ligne sur ce module.

Com­men­çons par un client très simple ré­cu­pé­rant le contenu d’une page sur le web :

import urllib2

lm = urllib2.urlopen("http://www.gnulinuxmag.com/")
print(lm.read())

Ce pro­gramme très simple ré­cu­père la page web du site de GNU/Linux Mag et af­fiche celle-ci. Vous de­vriez ob­te­nir le code HTML de la page web de votre ma­ga­zine pré­féré. En cas de pro­blème lors de la ré­cu­pé­ra­tion de la page, vous re­ce­vrez une ex­cep­tion telle que URLError ou HTTPError. À vous de gé­rer cor­rec­te­ment ce cas.

Notre pre­mier client HTTP n’est pas très in­té­res­sant. À titre d’illus­tra­tion, nous al­lons écrire un script un peu plus utile. Ce­lui-ci va af­fi­cher les der­nières vul­né­ra­bi­li­tés CVE. C’est une sorte de mini-lec­teur RSS. Le flux est dis­po­nible en ligne. Comme il s’agit de XML, nous al­lons pou­voir le par­ser fa­ci­le­ment à l’aide d’une bi­blio­thèque XML. Py­thon 2.5 a in­té­gré le mo­dule ElementTree qui est très simple d’uti­li­sa­tion. Pour les uti­li­sa­teurs de Py­thon 2.4, il est pos­sible de l’ins­tal­ler sé­pa­ré­ment (sous De­bian, il s’agit du pa­quet python-celementtree).

import urllib2

try:
    import xml.etree.ElementTree as ET
except ImportError:
    import cElementTree as ET
cve = urllib2.urlopen("http://nvd.nist.gov/download/nvd-rss.xml")
root = ET.parse(cve).getroot()
titles = []
for i in root.findall("{http://purl.org/rss/1.0/}item"):
    titles.append(i.find("{http://purl.org/rss/1.0/}title").text)
print("\n".join(titles))

Ex­pli­quons un peu ce code. Nous ten­tons de char­ger le mo­dule XML ElementTree de Py­thon 2.5. S’il n’est pas trouvé, on se ra­bat sur le mo­dule cElementTree qui doit être ins­tallé. En­suite, nous ré­cu­pé­rons la page web qui nous in­té­resse, puis nous la four­nis­sons au par­seur XML. En­fin, nous ré­cu­pé­rons les élé­ments qui nous in­té­ressent : de­puis la ra­cine, nous re­cher­chons tous les élé­ments « item ». Dans ceux-ci, nous ne gar­dons que l’élé­ment « title ».

Note

Nous uti­li­sons une no­ta­tion cu­rieuse pour trou­ver les élé­ments en ques­tion. Cette no­ta­tion est due à l’uti­li­sa­tion des es­paces de nom­mage XML dans le fi­chier XML en ques­tion. En ef­fet, il est pos­sible de mé­lan­ger à l’in­té­rieur d’un même fi­chier XML plu­sieurs vo­ca­bu­laires, comme du XHTML avec du RSS. ElementTree per­met de gé­rer ces es­paces de nom­mage en uti­li­sant la no­ta­tion que l’on peut trou­ver dans notre exemple. Dans ce­lui-ci, {http://​purl.​org/​rss/​1.0/} in­dique l’es­pace de nom­mage RSS. Une no­ta­tion plus ha­bi­tuelle se­rait rss:​title avec une dé­cla­ra­tion du pré­fixe dans le pré­am­bule XML.

Notre pro­gramme est déjà un peu plus utile. Nous pour­rions l’amé­lio­rer en gé­rant cor­rec­te­ment les ex­cep­tions qui peuvent sur­ve­nir, aussi bien lors de la ré­cu­pé­ra­tion du fi­chier XML que lors de son in­ter­pré­ta­tion. Tou­te­fois, ce n’est pas le but de cet article.

Si nous vou­lions en­voyer un for­mu­laire pour en ré­cu­pé­rer le contenu, c’est une opé­ra­tion qua­si­ment aussi simple :

import urllib, urllib2

content = urllib2.urlopen(
    "http://www.example.com/form", data=urllib.urlencode({"p1": 6, "p2": "alpha"})
)

Lorsque le pa­ra­mètre data est uti­lisé, urllib2 va uti­li­ser la mé­thode POST au lieu de la mé­thode GET avec les don­nées four­nies. Celles-ci doivent être co­dées en une chaîne com­pré­hen­sible par l’ap­pli­ca­tion qui va re­ce­voir les don­nées. Dans notre exemple, on doit en­voyer les don­nées sous la forme p2=​alpha&p1=6. C’est le rôle de la mé­thode urlencode du mo­dule urllib.

Un client SMTP#

Re­ve­nons à notre pre­mier exemple. Ima­gi­nons que nous vou­lons en­voyer les vul­né­ra­bi­li­tés CVE par mail plu­tôt que de les af­fi­cher sur l’écran. C’est le rôle du mo­dule smtplib. Ce mo­dule va s’adres­ser à un ser­veur SMTP de fa­çon à lui en­voyer un message.

smtplib se contente de gé­rer la par­tie SMTP. Il est né­ces­saire de lui four­nir un mail res­pec­tant la RFC 822. Dans sa bi­blio­thèque stan­dard, Py­thon dis­pose du mo­dule email per­met­tant de créer de tels mes­sages. Dans notre pro­gramme pré­cé­dent, nous rem­pla­çons l’af­fi­chage fi­nal par la construc­tion du mail :

import smtplib
from email.MIMEText import MIMEText

msg = MIMEText("\n".join(titles)
msg['Subject'] = 'Latest CVE'
msg['From'] = 'cve@example.com'
msg['To'] = 'user@example.com'

msg re­pré­sente le mes­sage que nous vou­lons en­voyer en SMTP. L’uti­li­sa­tion de smtplib est très simple :

s = smtplib.SMTP()
s.connect()
s.sendmail("cve-bounces@example.com", ["user@example.com"], msg.as_string())
s.close()

Comme nous ne don­nons au­cun pa­ra­mètre, ni au construc­teur SMTP, ni à la mé­thode connect, la connexion va s’ef­fec­tuer sur l’IP 127.0.0.1. Il est donc né­ces­saire d’avoir un ser­veur SMTP fonc­tion­nel tour­nant en lo­cal. Dans le cas contraire, on peut pas­ser en pre­mier pa­ra­mètre du construc­teur de l’ob­jet SMTP (ou de la mé­thode connect) le nom du ser­veur SMTP à utiliser.

Nous in­di­quons à deux en­droits dif­fé­rents l’adresse de l’ex­pé­di­teur et celle du des­ti­na­taire. En fait, le pro­to­cole SMTP fait une dis­tinc­tion entre l’en­ve­loppe et le mes­sage. Lorsque vous re­ce­vez un mail, vous ne voyez plus que le mes­sage (qui com­prend les en-têtes et le corps). Lorsque le mail voyage de ser­veurs SMTP en ser­veurs SMTP, c’est l’en­ve­loppe qui est uti­li­sée. Le mes­sage est gé­né­ra­le­ment ignoré. Une fois le mes­sage ar­rivé, l’en­ve­loppe est dé­truite. Ainsi, vous pou­vez mettre ce que vous vou­lez dans les champs To: et From:. Ce qui im­porte est ce qui est donné à la fonc­tion sendmail. Dans notre exemple, nous avons uti­lisé cve-bounces@example.​com comme adresse dans l’en­ve­loppe. Ainsi, nous re­ce­vrons les bounces à cette adresse (ceux-ci sont en­voyés à l’ex­pé­di­teur in­di­qué dans l’en­ve­loppe) et les éven­tuelles ré­ponses du des­ti­na­taire à l’adresse cve@example.​com qui est l’adresse qui ap­pa­raî­tra dans le client mail.

En­core une fois, notre pro­gramme reste très ba­sique et ne gère pas les ex­cep­tions qui peuvent survenir.

Un client FTP#

Le mo­dule ftplib per­met d’écrire fa­ci­le­ment un client FTP. Bien que ce pro­to­cole soit par­ti­cu­liè­re­ment peu sé­cu­risé, c’est en­core le pro­to­cole le plus cou­rant quand il s’agit de trans­fé­rer des don­nées chez un hé­ber­geur. C’est aussi le pro­to­cole uti­lisé quand vous dis­po­sez d’un ser­veur dé­dié et que votre hé­ber­geur vous four­nit un es­pace de sto­ckage. Pla­çons notre exemple pra­tique dans ce der­nier cas : vous ef­fec­tuez des sau­ve­gardes jour­na­lières de votre ser­veur dé­dié dans un ré­per­toire par­ti­cu­lier. Vous dé­si­rez main­te­nant trans­fé­rer ces sau­ve­gardes sur l’es­pace qui vous est al­loué sur le ser­veur FTP de l’hé­bergeur.

Il se­rait bien sûr tout à fait pos­sible de scrip­ter un client FTP exis­tant. Tou­te­fois, ce client aura sans doute du mal à ri­va­li­ser avec les pos­si­bi­li­tés of­fertes par Py­thon. De plus, nous sommes dans un hors-sé­rie Python !

Nous sup­po­sons que les sau­ve­gardes à trans­fé­rer sont de la forme « nom.xx » où « xx » est un nu­méro d’ordre. Par exemple « web.15 », « archives.17 » et ainsi de suite. Nous al­lons écrire une fonc­tion pre­nant en pa­ra­mètre le nom et le nu­méro d’ordre et qui va ef­fec­tuer les ac­tions suivantes :

  • vé­ri­fier si le fi­chier existe déjà et l’en­voyer dans le cas contraire ;
  • sup­pri­mer les fi­chiers de même nom, mais dont le nu­méro d’ordre est trop vieux.

Voici une pro­position :

import re
from ftplib import FTP

dst = "/backup"
ftp_host = "ftp.example.com"
ftp_login = "login"
ftp_password = "password"
keep = 5

def upload_ftp(backups):
    ftp = FTP(ftp_host, ftp_login, ftp_password)
    remotes = ftp.nlst()
    for f in backups:
        name = "%s.%d" % (f, backups[f])
        # Envoie le fichier s'il n'est pas present
        if name not in remotes:
            print("Envoi de %s" % name)
            ftp.storbinary("STOR %s" % name, file("%s/%s" % (dst, name)))
        # Supprime les anciens fichiers
        for r in remotes:
            mo = re.match(r"%s\.([0-9]+)$" % f, r)
            if mo:
                current = int(mo.group(1))
                if current < backups[f] - keep + 1:
                    print("Supprime %s" % r)
                    ftp.delete(r)
                else:
                    print("Conserve %s" % r)
    ftp.quit()

La va­riable backups at­ten­due par la fonc­tion est un dic­tion­naire dont les clefs sont les noms et les va­leurs sont les nu­mé­ros d’ordre. Par exemple :

{ 'web': 15, 'archives': 17 }

Que fait-on ? On crée d’abord un ob­jet FTP en don­nant le nom de l’hôte, le lo­gin et le mot de passe. En l’ab­sence des deux der­niers ren­sei­gne­ments, c’est une connexion ano­nyme qui sera ef­fec­tuée. La mé­thode nlst de l’ob­jet ob­tenu per­met d’avoir la liste des fi­chiers du ré­per­toire cou­rant. Cette liste nous per­met­tra de sa­voir si les fi­chiers que l’on dé­sire en­voyer existent déjà et quels sont les fi­chiers à effacer.

En­suite, pour chaque fi­chier de ba­ckup, on vé­ri­fie que le fi­chier n’existe pas. Dans ce cas, on l’en­voie à l’aide de la mé­thode storbinary. Cette mé­thode va ef­fec­tuer les ac­tions suivantes :

  • pas­ser en mode binaire,
  • né­go­cier le ca­nal de don­nées (ac­tif, pas­sif, choix du port à uti­liser, …)
  • en­voyer le contenu du fichier

On lui donne en pa­ra­mètre la com­mande à en­voyer au ser­veur. Celle-ci doit com­men­cer par STOR et être sui­vie du nom que le fi­chier doit avoir sur le ser­veur. Le se­cond pa­ra­mètre de la mé­thode est le fi­chier à en­voyer. Il est pos­sible de don­ner tout ce qui peut res­sem­bler à un fi­chier pour Py­thon. Par exemple, il se­rait pos­sible de don­ner le ré­sul­tat de urllib2.​urlopen() !

En­suite, on re­garde tous les fi­chiers pré­sents sur le ser­veur et on sup­prime les plus vieux avec la mé­thode delete.

Ainsi, bien que le pro­to­cole FTP soit as­sez com­plexe à im­plé­men­ter de zéro, le mo­dule ftplib per­met de s’en sor­tir très sim­ple­ment avec quelques mé­thodes simples. Comme pour les autres exemples, il convient de gé­rer au mieux les erreurs…

Et le reste ?#

Il reste beau­coup de pro­to­coles non ex­plo­rés. Pour cer­tains d’entre eux, Py­thon pro­pose un mo­dule mâ­chant le tra­vail. Il convient donc de lire la do­cu­men­ta­tion as­so­ciée, car chaque mo­dule pro­pose une in­ter­face dif­fé­rente. Si vous ren­con­trez un pro­to­cole qui ne dis­pose pas d’un mo­dule dans la bi­blio­thèque stan­dard de Py­thon, il vous reste plu­sieurs pistes :

  • des­cendre d’un cran et uti­li­ser le mo­dule socket, comme nous le ver­rons par la suite ;
  • uti­li­ser le mo­dule telnetlib si le pro­to­cole uti­lise TCP et est orienté texte ;
  • trou­ver un mo­dule externe ;
  • uti­li­ser Twis­ted qui sup­porte des pro­to­coles sup­plé­mentaires.

Lorsque l’on dé­sire tes­ter un ser­veur, il n’est pas rare d’uti­li­ser la com­mande telnet. Le pro­to­cole tel­net est en ef­fet ex­trê­me­ment ba­sique et le client tel­net fourni sur de nom­breux sys­tèmes per­met donc de dia­lo­guer avec un ser­veur SMTP, POP, NNTP, etc. Il est donc pos­sible d’uti­li­ser le mo­dule telnetlib de Py­thon dans des si­tua­tions si­mi­laires. Il convient ce­pen­dant de no­ter que le pro­to­cole tel­net dis­pose d’un en­semble de com­mandes que l’on in­voque en en­voyant une sé­quence spé­ci­fique. Cette sé­quence peut en­trer en conflit avec le pro­to­cole que vous ten­tez d’im­plé­men­ter. Il est donc ab­so­lu­ment né­ces­saire de s’as­su­rer que l’on uti­lise le mo­dule telnetlib uni­que­ment avec un ser­veur tel­net ou un ser­veur qui ne va pas uti­li­ser cette sé­quence d’échappement.

Le mo­dule telnetlib four­nit cer­taines mé­thodes pra­tiques per­met­tant d’at­tendre telle ou telle sé­quence de ca­rac­tères. Tou­te­fois, pour les rai­sons in­vo­quées ci-des­sus, il est pré­fé­rable de lui pré­fé­rer les mé­thodes de plus bas ni­veau que nous ver­rons par la suite cou­plées avec un mo­dule tel que pexpect qui four­nira les mêmes services.

Concer­nant les mo­dules ex­ternes, il existe de nom­breux choix, y com­pris des mo­dules en­trant en concur­rence avec les mo­dules four­nis en stan­dard dans Py­thon. La bi­blio­thèque stan­dard de Py­thon n’a pas la pré­ten­tion d’être ex­haus­tive (aussi bien au ni­veau des pro­to­coles pro­po­sés que de la ri­chesse fonc­tion­nelle pour cha­cun d’eux) et il ne faut donc pas hé­si­ter à pio­cher dans les mo­dules four­nis par la com­mu­nauté. Par exemple, si on dé­sire bé­né­fi­cier des ser­vices d’un client SSH, le mo­dule paramiko est un bon can­di­dat à considérer.

Écrire un serveur#

Nous ve­nons de voir que Py­thon pro­po­sait un très grand nombre de mo­dules pour écrire des clients. En ce qui concerne l’écri­ture de ser­veurs, l’offre est beau­coup moins four­nie. Si vous dé­si­rez écrire un ser­veur FTP, par exemple, il va fal­loir soit uti­li­ser un mo­dule tiers, soit vous re­trous­ser les manches pour écrire les choses à par­tir de zéro.

Tou­te­fois, Py­thon dis­pose d’un ser­veur SMTP, ainsi que d’un ser­veur HTTP. Nous n’abor­de­rons pas ici le cas du ser­veur SMTP. Py­thon dis­pose éga­le­ment d’un mo­dule per­met­tant d’écrire des ser­veurs gé­né­riques. Nous al­lons abor­der ces deux der­niers modules.

Un serveur HTTP#

Dans sa bi­blio­thèque stan­dard, Py­thon pro­pose quelques mo­dules pour écrire des ser­veurs Web. Ces mo­dules s’ar­ti­culent au­tour de la classe HTTPServer du mo­dule BaseHTTPServer. Chaque re­quête d’un client HTTP est prise en charge par une ins­tance de BaseHTTPRequestHandler.

Servir des fichiers statiques#

Le mo­dule SimpleHTTPServer four­nit une classe hé­ri­tant de BaseHTTPRequestHandler et per­met­tant de mettre à dis­po­si­tion des fichiers.

Par exemple, si l’on dé­sire ser­vir le contenu du ré­per­toire public_​html dans son ré­per­toire per­son­nel, il est pos­sible d’écrire le code suivant :

#!/usr/bin/env python
import os
from BaseHTTPServer import HTTPServer
from SimpleHTTPServer import SimpleHTTPRequestHandler

os.chdir(os.path.expanduser("~/public_html"))
httpd = HTTPServer(("", 8080), SimpleHTTPRequestHandler)
httpd.serve_forever()

Re­gar­dons en dé­tail les deux der­nières lignes. Nous ins­tan­cions la classe HTTPServer avec deux pa­ra­mètres. Le pre­mier pa­ra­mètre in­dique, sous forme d’un tuple, sur quelle adresse et quel port le ser­veur doit écou­ter. Nous four­nis­sons une chaîne vide pour l’adresse, ce qui si­gni­fie que nous écou­tons sur toutes les adresses. Nous au­rions pu éga­le­ment pré­ci­ser une IP comme 127.0.0.1. Le se­cond pa­ra­mètre est une classe qui sera ins­tan­ciée avec trois pa­ra­mètres pour chaque re­quête et dont le rôle sera de trai­ter la re­quête en ques­tion. Une fois le ser­veur Web ins­tan­cié, nous lui de­man­dons de ré­pondre aux re­quêtes. C’est le but de la der­nière ligne.

La classe SimpleHTTPRequestHandler est tel­le­ment simple qu’il n’existe pas de moyen tri­vial de choi­sir le ré­per­toire qui sera servi. Nous chan­geons donc de ré­per­toire avant de ser­vir les re­quêtes. Une fois ce script lancé, vous pou­vez le tes­ter avec votre na­vi­ga­teur en uti­li­sant l’URL http://localhost:8080. Vous ob­tien­drez le contenu du ré­per­toire public_​html dans votre ré­per­toire personnel.

En quelques lignes, vous ob­te­nez un ser­veur Web fonc­tion­nel. Bien que rus­tique, il sup­porte HTTP/0.9, HTTP/1.0 et HTTP/1.1 et dis­pose de quelques sé­cu­ri­tés em­pê­chant les at­taques ba­sées sur l’ajout de « .. » dans le che­min. Tou­te­fois, il est très peu per­for­mant (il ne ré­pond qu’à une seule re­quête à la fois).

Servir des pages dynamiques#

Il y a peu d’in­té­rêt à ne ser­vir que des pages sta­tiques. Pour créer un mini-site dy­na­mique, il suf­fit d’écrire une classe hé­ri­tant de BaseHTTPRequestHandler. Cette classe doit im­plé­men­ter des mé­thodes telles que do_​GET, do_​HEAD, do_​POST, etc. Voyons un pre­mier exemple simple.

#!/usr/bin/env python
from BaseHTTPServer import HTTPServer, BaseHTTPRequestHandler

class MyRequestHandler(BaseHTTPRequestHandler):
    def do_GET(self):
        self.send_response(200)
        self.send_header("Content-type", "text/html")
        self.end_headers()
        self.wfile.write("""<html><body><h1>Hello world !</h1></body></html>""")

httpd = HTTPServer(("", 8080), MyRequestHandler)
httpd.serve_forever()

Nous avons écrit notre propre classe pour ré­pondre aux re­quêtes. Nous ne sa­vons ré­pondre qu’aux re­quêtes GET et nous ré­pon­dons in­va­ria­ble­ment la même page HTML. La classe BaseHTTPRequestHandler four­nit un cer­tain nombre de mé­thodes pour nous fa­ci­li­ter la tâche. Avec la mé­thode send_​response, nous en­voyons le code de ré­ponse 200 in­di­quant que la page a été trou­vée. Nous en­voyons en­suite, à l’aide de send_​header, l’en-tête HTTP Content-Type in­di­quant que le contenu de la ré­ponse est du HTML. En­suite, la mé­thode end_​headers se contente d’ins­crire une ligne blanche. Nous sommes alors libres d’en­voyer le contenu qui nous plaît. Pour cela, l’at­tri­but wfile est mis à dis­po­si­tion. Il suf­fit d’écrire de­dans pour ré­pondre au client.

Es­sayez d’ac­cé­der à l’URL http://​localhost:​8080/​test/ et vous de­vriez ob­te­nir une page in­di­quant « Hello world ! ». Pas très dy­na­mique. Mais vous pou­vez en­suite faire tout ce que vous vou­lez. Par exemple, ré­écri­vons la classe MyRequestHandler de cette façon :

class MyRequestHandler(BaseHTTPRequestHandler):
    visiteur = 0

    def do_GET(self):
        self.send_response(200)
        self.send_header("Content-type", "text/html")
        self.end_headers()
        MyRequestHandler.visiteur += 1
        self.wfile.write("""<html><body><h1>Hello world !</h1>
Vous avez demandé la page %s. Votre IP est %s. Vous êtes le visiteur %d.
</body></html>""" % (self.path, self.client_address[0], MyRequestHandler.visiteur))

Nous ob­te­nons une page dy­na­mique don­nant la page de­man­dée, l’IP du client et af­fi­chant un comp­teur. Il est à no­ter que la classe est ré­ins­tan­ciée pour le trai­te­ment de chaque re­quête. Pour que le comp­teur per­dure entre les re­quêtes, on en a donc fait un at­tri­but de la classe et non un at­tri­but de l’ins­tance, ce qui ex­plique que l’on uti­lise MyRequestHandler.​visiteur et non self.​visiteur.

Bien en­tendu, la bi­blio­thèque stan­dard de Py­thon n’est pas adap­tée pour écrire toute une ap­pli­ca­tion Web. Vous de­vriez co­der vous-même la ges­tion des ses­sions, la ges­tion des co­okies, le dé­co­dage des ar­gu­ments ou en­core la cor­res­pon­dance entre les pages de­man­dées et les fonc­tions ap­pe­lées. En plus, vous de­vez écrire vous-même du HTML. Un fra­me­work comme Django ou Twis­ted vous « offre » gra­tui­te­ment de telles fonc­tion­nalités.

Un serveur TCP#

La classe HTTPServer hé­rite en fait d’une classe plus gé­né­rique per­met­tant de pro­gram­mer des ser­veurs ar­bi­traires. La bi­blio­thèque stan­dard de Py­thon pro­pose en ef­fet des classes pour fa­ci­li­ter l’écri­ture de ser­veurs TCP ou UDP. Ces classes se si­tuent dans le mo­dule SocketServer. On y trouve par exemple la classe TCPServer que nous al­lons mettre à pro­fit pour écrire un pe­tit ser­veur per­met­tant d’ob­te­nir quelques in­for­ma­tions sur le système.

Nous dé­fi­nis­sons un pro­to­cole très simple pour ce ser­veur : suite à la connexion, l’uti­li­sa­teur peut pas­ser des com­mandes telles que uptime et df. Le ser­veur ren­voie alors les ré­ponses à ces com­mandes, puis ferme la connexion. Ce n’est pas sans rap­pe­ler le fonc­tion­ne­ment de base d’un ser­veur HTTP.

Comme pour l’écri­ture d’un ser­veur HTTP, nous de­vons dé­fi­nir une ins­tance de TCPServer et lui four­nir en ar­gu­ment une classe qui sera ins­tan­ciée à chaque re­quête. Voyons dans un pre­mier temps com­ment mettre sim­ple­ment en œuvre TCPServer. Nous ver­rons par la suite com­ment construire le ser­veur que nous ve­nons de décrire.

#!/usr/bin/env python
import os
from SocketServer import TCPServer, StreamRequestHandler

class MyRequestHandler(StreamRequestHandler):
    def handle(self):
        self.wfile.write("Votre est IP est %s. Au revoir\n" % self.client_address[0])

tcpd = TCPServer(("", 2020), MyRequestHandler)
tcpd.serve_forever()

La struc­ture est très proche de notre ser­veur HTTP ! Nous dé­fi­nis­sons une classe hé­ri­tant de StreamRequestHandler qui sera ins­tan­ciée pour chaque connexion et qui de­vra prendre en charge celle-ci du dé­but à la fin. Nous créons en­suite le ser­veur TCP en ins­tan­ciant TCPServer et nous lui de­man­dons de ser­vir en boucle les connexions qui peuvent survenir.

Vous pou­vez tes­ter ce ser­veur très simple à l’aide du pro­gramme nc :

$ nc localhost 2020
Votre est IP est 127.0.0.1. Au revoir

Pour chaque connexion, le ser­veur TCP va ins­tan­cier MyRequestHandler et va ap­pe­ler, entre autres, la mé­thode handle. Lorsque cette mé­thode ter­mine, la connexion est fer­mée. Pour réa­li­ser notre ser­veur TCP tel que nous l’avons dé­fini, nous al­lons ré­écrire ainsi notre programme :

#!/usr/bin/env python
import os
import time
import commands
from SocketServer import TCPServer, StreamRequestHandler

class MyRequestHandler(StreamRequestHandler):
    def handle(self):
        self.wfile.write("Que puis-je faire pour vous ?\n")
        ligne = self.rfile.readline().strip()
        if hasattr(self, "do_%s" % ligne):
            getattr(self, "do_%s" % ligne)()
        else:
            self.wfile.write("Commande inconnue.")
        self.wfile.write("\n")

    def do_uptime(self):
        status, uptime = commands.getstatusoutput("uptime")
        self.wfile.write(uptime)

    def do_df(self):
        status, df = commands.getstatusoutput("df -h")
        self.wfile.write(df)

TCPServer.allow_reuse_address = True
tcpd = TCPServer(("", 2020), MyRequestHandler)
tcpd.serve_forever()

Nous avons mo­di­fié la classe MyRequestHandler. Dé­sor­mais, nous li­sons une ligne (nous at­ten­dons donc une en­trée du client), puis nous cher­chons une fonc­tion cor­res­pon­dant à la com­mande de­man­dée que nous exé­cu­tons. Si cette fonc­tion n’existe pas, nous ren­voyons un mes­sage gé­né­rique. Il est donc fa­cile d’ajou­ter des commandes.

Cet exemple sim­pliste ne gère pas les er­reurs, et en cas de pro­blèmes sur­ve­nant dans une ins­tance de MyRequestHandler, la connexion est sim­ple­ment ter­mi­née, mais le ser­veur reste plei­ne­ment fonc­tionnel.

Notre ser­veur a un gros dé­faut : il ne peut ser­vir qu’une seule connexion à la fois. Es­sayez la ma­ni­pu­la­tion sui­vante : ou­vrez deux ter­mi­naux. Connec­tez-vous sur le ser­veur de­puis le pre­mier, ne ta­pez au­cune com­mande, puis es­sayez de vous connec­ter. Tant que la connexion ne sera pas cou­pée sur le pre­mier ter­mi­nal, vous ne ver­rez pas la ban­nière ap­pa­raître sur le se­cond ! En ef­fet, votre ser­veur ne gère qu’une seule connexion si­multanée.

Il y a plu­sieurs so­lu­tions pour gé­rer plu­sieurs connexions si­mul­ta­nées. Parmi celles-ci, il y a l’uti­li­sa­tion des threads ou l’uti­li­sa­tion de pro­ces­sus mul­tiples. Dans le pre­mier cas, un thread sera créé pour prendre en charge chaque nou­velle connexion. Le se­cond cas est iden­tique, mais des pro­ces­sus se­ront uti­li­sés au lieu d’uti­li­ser des threads.

Py­thon pro­pose deux classes dé­ri­vées de TCPServer : ThreadingTCPServer et ForkingTCPServer. Au lieu d’ins­tan­cier sim­ple­ment TCPServer, ins­tan­ciez l’une ou l’autre de ces classes. Vous pou­vez alors gé­rer plu­sieurs connexions si­mul­ta­né­ment. Essayez !

Nous sa­vons dé­sor­mais aussi bien écrire des clients que des ser­veurs en uti­li­sant les dif­fé­rents mo­dules four­nis en stan­dard dans Py­thon. Nous al­lons main­te­nant voir qu’il est éga­le­ment pos­sible de des­cendre d’un cran et d’uti­li­ser les mêmes pri­mi­tives qu’en C pour écrire des clients et des ser­veurs. Cela peut s’avé­rer utile dans cer­tains cas, par exemple pour écrire un ser­veur sans uti­li­ser ni threads, ni pro­ces­sus mul­tiples, mais en gé­rant tout de même plu­sieurs connexions si­mul­ta­né­ment. Ou sim­ple­ment pour s’amuser.

Utiliser les primitives de bas niveau#

Lorsqu’il s’agit d’écrire des clients ou des ser­veurs en C, on en re­vient sou­vent à quelques pri­mi­tives de base, à sa­voir l’API BSD so­ckets. Il existe bien sûr quelques fra­me­works de plus haut ni­veau fa­ci­li­tant la tâche, no­tam­ment quand on a be­soin d’in­té­grer le ser­veur ou le client dans un tool­kit gra­phique (tel que QT ou GTK), mais la plu­part du temps, on uti­lise des fonc­tions telles que socket(), bind(), listen(), connect(), accept(), etc. On trou­vera une des­crip­tion dans la page de ma­nuel ip (en sec­tion 7), ainsi que dans les pages de ma­nuel udp et tcp (tou­jours en section 7).

Pour écrire un client TCP (en UDP, c’est un peu dif­fé­rent, mais cela n’a pas beau­coup d’im­por­tance pour notre exemple), il faut :

  • ou­vrir une so­cket avec socket() ;
  • éta­blir une connexion avec un ser­veur dis­tant avec connect() ;
  • en­voyer des don­nées avec send() ou en re­ce­voir avec recv() ;
  • ter­mi­ner avec close().

Pour un ser­veur TCP, on uti­li­sera les fonc­tions suivantes :

  • ou­vrir une so­cket avec socket() ;
  • al­louer un port lo­cal avec bind() ;
  • écou­ter sur ce port avec listen() ;
  • ac­cep­ter une connexion en­trante avec accept() qui per­met d’ob­te­nir une nou­velle so­cket propre au client ;
  • en­voyer des don­nées avec send() ou en re­ce­voir avec recv() ;
  • ter­mi­ner la connexion avec le client à l’aide de close() ;
  • fer­mer la so­cket du ser­veur avec close().

Écrire un client#

Re­pre­nons nos exemples sim­plistes. Com­ment pour­rions-nous écrire un client HTTP en C avec les pri­mi­tives dé­crites ci-des­sus ? Le code ci-des­sous re­pré­sente un client sim­pliste. Tou­te­fois, il ne s’agit pas d’un hors-sé­rie sur le C. Nous vou­lons uni­que­ment vous mon­trer un pro­to­type d’ap­pli­ca­tion à titre d’exemple.

#include <stdio.h>
#include <stdlib.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>

int main() {
  int s;
  struct sockaddr_in serv;
  char request[] = "GET / HTTP/1.0\r\nHost:www.gnulinuxmag.com\r\n\r\n";
  char buffer[200];
  serv.sin_family = AF_INET;
  serv.sin_port = htons(80);
  serv.sin_addr.s_addr = inet_addr("91.121.166.153");
  if ((s = socket(PF_INET, SOCK_STREAM, 0)) < 0) {
    perror("socket(): ");
    exit(1);
  }
  if (connect(s, (struct sockaddr *)&serv,
              (socklen_t)sizeof(struct sockaddr_in)) < 0) {
    perror("connect(): ");
    exit(1);
  }
  if (send(s, request, sizeof(request), 0) < 0) {
    perror("send(): ");
    exit(1);
  }
  if (recv(s, buffer, sizeof(buffer), 0) < 0) {
    perror("recv(): ");
    exit(1);
  }
  buffer[199] = '\0';
  printf("Nous avons reçu ceci:\n\n%s\n\n", buffer);
  return 0;
}

Com­men­tons un peu notre code. Com­men­çons par l’ap­pel socket(). Ce­lui-ci est des­tiné à créer un point de com­mu­ni­ca­tion qui per­met­tra d’en­voyer et de re­ce­voir des don­nées. Nous de­vons en pré­ci­ser le do­maine (une so­cket pour IPv4 ici avec PF_​INET), le type (SOCK_​STREAM qui si­gni­fie en fait que l’on dé­sire une com­mu­ni­ca­tion fiable et en IPv4, le sys­tème com­prend alors TCP) et un nu­méro de pro­to­cole qui est in­utile donc mis à 0, car il n’y a plus rien à choisir.

Notre so­cket n’est connec­tée à rien. Si on es­sayait d’en­voyer des don­nées des­sus, le sys­tème n’ac­cep­te­rait pas. Nous al­lons la connec­ter à un ser­veur dis­tant avec la pri­mi­tive connect(). Si vous n’êtes pas fa­mi­lier avec le C, la ligne cor­res­pon­dante peut sem­bler énig­ma­tique. connect() prend comme pre­mier pa­ra­mètre la so­cket que nous ve­nons d’ob­te­nir. Le se­cond pa­ra­mètre est une struc­ture in­di­quant quel ser­veur nous vou­lons contac­ter. Nous avons rem­pli cette struc­ture dans les pre­mières lignes du pro­gramme en pré­ci­sant la fa­mille (IPv4), le port cible avec les oc­tets dans l’ordre ré­seau (qui est l’in­verse de ce qu’on a sur x86), d’où l’uti­li­sa­tion de htons() qui ef­fec­tue la conver­sion, ainsi que l’adresse de l’hôte. Nous avons mis l’IP en dur dans le pro­gramme. Il au­rait fallu dans le cas contraire ef­fec­tuer en plus une ré­so­lu­tion de nom. Les choses se com­pliquent un peu. La fonc­tion at­tend un type struct sockaddr et nous lui four­nis­sons un type struct sockaddr_in (qui cor­res­pond au type pour faire de l’IPv4). Les no­ta­tions com­pli­quées sont donc là pour faire la conver­sion pour que le com­pi­la­teur soit content. Si on avait voulu cau­ser en IPv6, on au­rait uti­lisé struct sockaddr_in6.

Si tout se passe bien, nous voici connec­tés. On peut alors uti­li­ser les fonc­tions send() et recv() pour en­voyer des don­nées ou en re­ce­voir. Nous en­voyons donc une re­quête HTTP construite à la main et re­ce­vons les 200 pre­miers oc­tets en ré­ponse. Com­pi­lons et testons :

$ gcc web1.c -o web1
$ ./web1
Nous avons reçu ceci:

HTTP/1.1 200 OK
Date: Sat, 10 May 2008 12:46:10 GMT
Server: Apache/2.2.6 (Debian) DAV/2 PHP/5.2.3-1+b1
X-Powered-By: PHP/5.2.3-1+b1
Last-Modified: Tue, 29 Apr 2008 10:01:15 GMT
Cache-Control: mu

Notre buf­fer n’est pas as­sez grand pour re­ce­voir plus que le dé­but des en-têtes. Mais, voilà notre client HTTP en C fini. Mais at­ten­dez, ne sommes-nous pas dans un hors-sé­rie Py­thon ? Nous al­lons donc le conver­tir ra­pi­de­ment en Py­thon pour ré­pa­rer cette mé­prise. Il va fal­loir uti­li­ser les fonc­tions conte­nues dans le mo­dule socket. Le ré­sul­tat a alors un air de fa­mille cer­tain avec la ver­sion en C.

import socket

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0)
s.connect(("213.186.60.195", 80))
s.send("GET / HTTP/1.0\r\nHost:www.gnulinuxmag.com\r\n\r\n")
buf = s.recv(200)
print("Nous avons recu ceci:\n\n%s\n\n" % buf)

Nous com­men­çons donc par créer notre so­cket. Les pa­ra­mètres sont les mêmes qu’en C. Les constantes fai­sant par­tie du mo­dule socket, nous de­vons les cher­cher dans cet es­pace de nom­mage. En­suite, nous connec­tons notre so­cket au ser­veur dis­tant. Ici, pas de conver­sions com­pli­quées à faire, on donne un couple conte­nant l’IP et le port. La pri­mi­tive connect du mo­dule socket va faire un peu plus que d’ap­pe­ler sim­ple­ment la fonc­tion socket en C.

En­suite, nous en­voyons notre re­quête et re­ce­vons la ré­ponse que nous af­fi­chons. Testons :

$ python ./web4.py
Nous avons recu ceci:

HTTP/1.1 200 OK
Date: Sat, 10 May 2008 13:03:51 GMT
Server: Apache/2.2.6 (Debian) DAV/2 PHP/5.2.3-1+b1
X-Powered-By: PHP/5.2.3-1+b1
Last-Modified: Tue, 29 Apr 2008 10:01:15 GMT
Cache-Control: mus

Le ré­sul­tat est iden­tique, mais nous avons tout de même éco­no­misé beau­coup de lignes de code par rap­port à la ver­sion en C. Nous n’avons plus à gé­rer ma­nuel­le­ment les er­reurs, car le mé­ca­nisme d’ex­cep­tion le fait pour nous.

No­tons ce­pen­dant que ce pro­gramme Py­thon ne cor­res­pond pas tout à fait à l’exemple donné avec urllib2 en dé­but d’ar­ticle. En ef­fet, si le ser­veur avait ré­pondu avec une re­di­rec­tion, le pro­gramme uti­li­sant urllib2 au­rait suivi au­to­ma­ti­que­ment cette re­di­rec­tion tan­dis que notre pro­gramme ne l’au­rait pas fait.

Écrire un serveur#

Pas­sons main­te­nant à l’écri­ture d’un ser­veur. Comme notre pre­mier exemple de ser­veur en Py­thon, il va sim­ple­ment ac­cep­ter la connexion, af­fi­cher l’IP du client, puis fer­mer la connexion. Nous al­lons écrire di­rec­te­ment notre pro­gramme en Py­thon, ce qui nous fa­ci­lite bien évi­dem­ment la vie :

import socket

s = socket.socket()
s.bind(("", 2020))
s.listen(5)
while True:
    conn, client = s.accept()
    conn.send("Votre IP est %s\n" % client[0])
    conn.close()

Comme pour le client, nous créons une so­cket. In­utile de de­man­der une so­cket IPv4 de type SOCK_​STREAM : ce sont les pa­ra­mètres par dé­faut de la fonc­tion socket. Nous in­di­quons en­suite quel port nous al­lons uti­li­ser. Nous in­di­quons que nous al­lons écou­ter sur toutes les in­ter­faces dis­po­nibles sur le port 2020. En C, nous au­rions dû ren­sei­gner une struc­ture et uti­li­ser INADDR_​ANY au lieu de la chaîne vide. Si l’on vou­lait res­treindre l’usage du ser­veur à l’or­di­na­teur lo­cal, il au­rait fallu uti­li­ser 127.0.0.1 comme pre­mier élé­ment du tuple passé à bind. En­suite, nous in­di­quons au sys­tème d’ex­ploi­ta­tion de mettre à l’écoute notre so­cket. Nous in­di­quons au sys­tème d’ex­ploi­ta­tion de mettre en at­tente pour nous jusqu’à 5 connexions. Ainsi, si notre pro­gramme est trop oc­cupé pour ac­cep­ter les nou­velles connexions, le sys­tème d’ex­ploi­ta­tion les met­tra en at­tente. Au-delà de 5, les connexions se­ront refusées.

Nous pas­sons en­suite dans une boucle in­fi­nie pour trai­ter les connexions de chaque client qui pour­rait se pré­sen­ter à nous. Nous uti­li­sons la fonc­tion accept. En C, celle-ci re­tourne une so­cket qui per­met de dia­lo­guer avec le client et passe l’adresse du client en mo­di­fiant les deux der­niers ar­gu­ments. En Py­thon, les deux in­for­ma­tions sont sim­ple­ment re­tour­nées sous forme d’un tuple.

Note

Pour un ser­veur, il y a donc une so­cket par client. Si on uti­lise TCP, chaque so­cket cor­res­pond à une IP et un port. Il faut donc deux so­ckets pour faire une connexion : celle du ser­veur (IP du ser­veur, port sur le­quel le ser­veur écoute) et la so­cket du client (IP du client, port sur le­quel le client at­tend les ré­ponses, ha­bi­tuel­le­ment un port élevé aléa­toire). Avant de pou­voir uti­li­ser une so­cket, il faut l’as­so­cier à ce couple. C’est l’uti­lité de la fonc­tion bind. Cette fonc­tion est au­to­ma­ti­que­ment ap­pe­lée par connect dans le cas d’un client si on l’a omis, ce qui ex­plique son ab­sence dans notre exemple pour le client HTTP. Il existe d’autres types de so­cket. Par exemple, une so­cket Unix nom­mée uti­lise le nom d’un fi­chier plu­tôt qu’un couple IP/port. Pour com­mu­ni­quer sur la même so­cket, deux pro­ces­sus doivent s’as­so­cier au même che­min. Le lec­teur in­té­ressé par ces his­toires de so­cket peut se re­por­ter aux pages de ma­nuel de so­cket, ip, tcp, udp, raw et unix dans la section 7.

Notre ser­veur est plus fra­gile que le ser­veur pré­senté pré­cé­dem­ment. S’il y a une er­reur dans la boucle, le ser­veur se ter­mine. Ce qui n’était pas le cas en uti­li­sant la classe TCPServer.

En­fin, notre ser­veur ne traite qu’une connexion à la fois. Ce n’est pas pro­blé­ma­tique pour notre exemple sim­pliste, mais si vous at­ten­dez une en­trée de l’uti­li­sa­teur, per­sonne ne pourra se connec­ter en at­ten­dant ! Mo­di­fions notre pro­gramme pour qu’il ac­cepte une en­trée de l’uti­lisateur.

#!/usr/bin/env python
import os
import commands
import socket

s = socket.socket()
s.bind(("", 2021))
s.listen(5)
while True:
    conn, client = s.accept()
    conn.send("Que puis-je faire pour vous ?\n")
    ordre = conn.recv(30).strip()
    if ordre == "uptime":
        status, uptime = commands.getstatusoutput("uptime")
        conn.send("%s\n" % uptime)
    elif ordre == "df":
        status, df = commands.getstatusoutput("df -h")
        conn.send("%s\n" % df)
    else:
        conn.send("Je n'ai rien compris.\n")
    conn.close()

Le dé­but est iden­tique à la ver­sion pré­cé­dente. Nous en­voyons en­suite un prompt pour de­man­der à l’uti­li­sa­teur d’en­trer une ac­tion. Nous ren­con­trons en­suite une dif­fé­rence fonc­tion­nelle im­por­tante avec la ver­sion uti­li­sant les pri­mi­tives de plus haut ni­veau : nous li­sons au plus 30 ca­rac­tères. Si nous vou­lions lire des lignes en­tières pro­pre­ment, nous de­vrions faire le dé­cou­page et l’as­sem­blage nous-même alors que Py­thon met­tait à dis­po­si­tion une classe dé­diée à cet usage. Pour notre exemple, cela n’a pas beau­coup d’im­por­tance. La suite est sans surprise.

Vous pou­vez re­faire le test : ou­vrez deux ter­mi­naux, connec­tez-vous sur le ser­veur de l’un, puis connec­tez-vous de l’autre. Le se­cond ar­rivé n’aura pas le prompt tant que le pre­mier n’a pas ter­miné la connexion. Dans le cas pré­cé­dent, nous avions ré­solu le pro­blème en sub­sti­tuant sim­ple­ment TCPServer par une classe dé­ri­vée gé­rant un pro­ces­sus par connexion. Ici, une telle ma­ni­pu­la­tion n’est pas aussi simple : il va fal­loir gé­rer cet as­pect nous-même. Nous al­lons uti­li­ser la pri­mi­tive fork dis­po­nible dans le mo­dule os. Il existe bien sûr d’autres so­lu­tions, dont l’uti­li­sa­tion du mo­dule threading.

#!/usr/bin/env python
import os
import commands
import socket

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0)
s.bind(("0.0.0.0", 2022))
s.listen(5)
while True:
    conn, client = s.accept()
    if os.fork():
        # Nous sommes dans le pere
        conn.close()
    else:
        # Nous sommes dans le fils
        conn.send("Que puis-je faire pour vous ?\n")
        ordre = conn.recv(30).strip()
        if ordre == "uptime":
            status, uptime = commands.getstatusoutput("uptime")
            conn.send("%s\n" % uptime)
        elif ordre == "df":
            status, df = commands.getstatusoutput("df -h")
            conn.send("%s\n" % df)
        else:
            conn.send("Je n'ai rien compris.\n")
        conn.close()
        os._exit(0)

Après avoir ac­cepté une connexion, nous uti­li­sons la fonc­tion fork. Cette fonc­tion va du­pli­quer le pro­ces­sus ac­tuel et les deux co­pies tour­ne­ront en même temps avec une très lé­gère dif­fé­rence entre les deux. Dans le fils, fork re­tourne 0, tan­dis que dans le père, elle re­tourne le PID du fils. Cette dif­fé­rence nous per­met d’exé­cu­ter du code dif­fé­rent dans les deux cas.

Si nous sommes dans le père, nous fer­mons sim­ple­ment la so­cket et on re­passe dans la boucle pour ac­cep­ter un nou­veau client. La fer­me­ture de la so­cket n’en­traîne pas la fer­me­ture de la connexion, car la so­cket est tou­jours ou­verte dans le fils. La connexion ne sera ter­mi­née que quand cette so­cket sera fermée.

Dans le fils, nous fai­sons notre tra­vail comme pré­cé­dem­ment. À la fin, nous ter­mi­nons le pro­ces­sus avec la fonc­tion _exit du mo­dule os. Cette fonc­tion évite tous les ef­fets de bord de la fonc­tion exit qui pour­raient im­pac­ter le fils.

Vous pou­vez dé­sor­mais re­faire le test : nous pou­vons gé­rer plu­sieurs connexions à la fois. Notre exemple n’est pas par­fait : nous lais­sons des pro­ces­sus zom­bis un peu par­tout. Il fau­drait que le père ac­quiesce la mort de chaque fils avec waitpid. La fa­çon la plus simple pour faire ceci est de mettre en place un ges­tion­naire de si­gnal. Tou­te­fois, cela sort lar­ge­ment du cadre de cet article.

À quoi ça sert ?#

À la vue des exemples pré­sen­tés, on peut se de­man­der pour­quoi nous vou­drions uti­li­ser les pri­mi­tives de bas ni­veau. En ef­fet, le code est plus long, moins py­tho­nesque, com­porte des li­mi­ta­tions et des fra­gi­li­tés sup­plé­men­taires. Que de dé­fauts. Y a-t-il un in­té­rêt en de­hors de l’as­pect édu­ca­tif ? Faire ap­pel aux pri­mi­tives de bas ni­veau peut être né­ces­saire quand on es­saie de sor­tir du cadre des pri­mi­tives de haut ni­veau. Par exemple, le ser­veur TCP fourni par Py­thon per­met de n’uti­li­ser qu’un seul pro­ces­sus (mais de ne gé­rer qu’une seule connexion à la fois dans ce cas), d’uti­li­ser des threads ou bien d’uti­li­ser plu­sieurs pro­ces­sus. Ima­gi­nons qu’au­cune de ces ap­proches ne nous convienne. Nous vou­lons être ca­pable de gé­rer plu­sieurs connexions, mais un seul thread et un seul pro­ces­sus. Si, si, c’est pos­sible, mais il faut uti­li­ser les pri­mi­tives de bas niveau !

Il existe plu­sieurs mé­thodes pour ar­ri­ver à un tel ré­sul­tat. L’une d’entre elles consiste à uti­li­ser le mo­dule select conte­nant la fonc­tion select qui per­met de sur­veiller une liste de des­crip­teurs de fi­chier en at­ten­dant qu’un évé­ne­ment se pro­duise sur l’un d’eux. Il s’agit en­core d’une fonc­tion d’as­sez bas ni­veau dont les fonc­tion­na­li­tés sont équi­va­lentes à la fonc­tion select() dis­po­nible en C. Elle a été ren­due un brin py­tho­nesque en gé­rant les ex­ceptions.

Com­ment al­lons-nous pro­cé­der ? Nous al­lons main­te­nir nous-même la liste des clients se connec­tant et sur­veiller avec la fonc­tion select leur ac­ti­vité. Si un client en­voie des don­nées, la fonc­tion select nous l’in­dique et nous ef­fec­tuons les ac­tions né­ces­saires. Nous ne se­rons ainsi pas blo­qués si un client de­vient in­ac­tif. La fonc­tion select at­tend une liste de des­crip­teurs (les so­ckets sont des des­crip­teurs) et rend la main quand n’im­porte le­quel de ces des­crip­teurs de­vient dis­po­nible (pour un des­crip­teur en lec­ture, cela si­gni­fie qu’il y a de nou­velles don­nées à lire).

#!/usr/bin/env python
import os
import commands
import socket
import select

s = socket.socket()
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.bind(("", 2022))
s.listen(5)
conns = [s]
while True:
    rlist, wlist, xlist = select.select(conns, [], [])
    for r in rlist:
        if r == s:
            conn, client = s.accept()
            conn.send("Que puis-je faire pour vous ?\n")
            conns.append(conn)
        else:
            ordre = r.recv(30).strip()
            if ordre == "uptime":
                status, uptime = commands.getstatusoutput("uptime")
                r.send("%s\n" % uptime)
            elif ordre == "df":
                status, df = commands.getstatusoutput("df -h")
                r.send("%s\n" % df)
            else:
                conn.send("Je n'ai rien compris.\n")
            r.close()
            conns.remove(r)

Exa­mi­nons le code pro­posé à par­tir de l’ap­pel à select. Select ac­cepte comme pa­ra­mètres trois en­sembles de des­crip­teurs : ceux sur les­quels on at­tend des don­nées, ceux pour les­quels on at­tend une dis­po­ni­bi­lité en écri­ture et ceux pour les­quels on at­tend des évè­ne­ments tels que la fer­me­ture de la connexion ou une er­reur. Nous ne ren­sei­gnons que le pre­mier type de des­crip­teurs. L’ap­pel de select sera donc blo­quant tant que l’un des des­crip­teurs contenu dans la liste conns n’aura pas de don­nées dis­po­nibles. Lorsque nous ac­cep­tons une nou­velle connexion, nous l’ajou­tons dans cette liste. Quand nous fer­mons une connexion, nous la re­ti­rons. Nous ajou­tons aussi notre so­cket ser­veur dans cette liste. Si une nou­velle connexion peut être ac­cep­tée, select re­tour­nera éga­le­ment la main. La fonc­tion ren­voie trois en­sembles : l’en­semble des des­crip­teurs prêts en lec­ture, ceux en écri­ture et ceux en condi­tion ex­cep­tion­nelle. Cha­cun de ces en­sembles est un sous-en­semble des pa­ra­mètres. Ainsi, dans rlist, nous ob­te­nons un sous-en­semble de conns.

Nous avons donc dans rlist, le sous-en­semble des des­crip­teurs de conns, à par­tir des­quels nous pou­vons au moins lire un oc­tet sans être blo­qué. Si l’un des membres est la so­cket, cela si­gni­fie que nous pou­vons ac­cep­ter une nou­velle connexion. Pour les autres, on ef­fec­tue les ac­tions ha­bi­tuelles.

Tes­tez ce pro­gramme pour vé­ri­fier qu’il fonc­tionne comme at­tendu. Vous pou­vez en ef­fet ou­vrir plu­sieurs connexions vers le ser­veur alors qu’un unique pro­ces­sus tourne. Tou­te­fois, ce pro­gramme a plu­sieurs dé­fauts im­portants :

  • Nous uti­li­sons select uni­que­ment en lec­ture, les ap­pels à send peuvent blo­quer (ce qui ne doit pas ar­ri­ver sur une socket).
  • Nous comp­tons sur le fait que nous re­ce­vons la com­mande en en­tier du pre­mier coup. Avec TCP, ce n’est pas ga­ranti. Si le client en­voyait la com­mande ca­rac­tère par ca­rac­tère, ce ne se­rait pas le cas. Il fau­drait donc re­cons­ti­tuer la com­mande avant de pro­cé­der à une action.
  • Si la com­mande à exé­cu­ter est longue, notre pro­gramme est blo­qué pen­dant son exé­cu­tion. Comme il n’y a qu’un seul thread et un seul pro­ces­sus, cela si­gni­fie que tout est blo­qué. Il fau­drait uti­li­ser select sur la sor­tie stan­dard de ces pro­grammes, ce qui va com­pli­quer le code.

Cor­ri­ger ces dé­fauts né­ces­site plu­sieurs lignes de code sup­plé­men­taire. Nous n’al­lons pas les étu­dier ici. Cet exemple nous per­met d’in­tro­duire la sec­tion sui­vante de cet ar­ticle. Il existe des fra­me­works en Py­thon qui per­mettent d’écrire du code asyn­chrone (c’est-à-dire du code évi­tant tout ap­pel blo­quant). Parmi ceux-ci, il y a Twisted !

Utiliser Twisted#

Twis­ted se dé­fi­nit comme étant le mo­teur de votre In­ter­net. Il a deux ca­rac­té­ris­tiques es­sen­tielles :

  • C’est un fra­me­work ré­seau très com­plet per­met­tant de construire à la fois des clients et des ser­veurs uti­li­sant des pro­to­coles très va­riés : HTTP, SMTP, IMAP, POP, SSH, DNS, FTP, IRC…
  • Il est asyn­chrone et pi­loté par les évè­ne­ments, c’est-à-dire que l’on as­so­cie des ac­tions à exé­cu­ter lorsque des évè­ne­ments sur­viennent (comme l’ar­ri­vée d’un paquet).

Le sup­port d’un très grand nombre de pro­to­coles est un atout pour Twis­ted. Non seule­ment, il sup­porte plus de pro­to­coles que la bi­blio­thèque stan­dard de Py­thon, mais, en plus, ces pro­to­coles sont le plus sou­vent dis­po­nibles en tant que client et ser­veur. En­fin, il s’agit le plus sou­vent d’im­plé­men­ta­tions très com­plètes. Par exemple, le ser­veur SSH sup­porte SFTP et le client SSH est ca­pable d’uti­li­ser un agent. C’est un point très im­por­tant pour les dé­ve­lop­peurs de Twis­ted : il s’agit de four­nir des im­plé­men­ta­tions pou­vant être uti­li­sées en pro­duction.

L’as­pect asyn­chrone de Twis­ted peut être dé­rou­tant. Quand une ac­tion est blo­quante, on four­nit une fonc­tion qui sera exé­cu­tée quand cette ac­tion sera ter­mi­née. Ainsi, quand on veut lire un pa­quet ré­seau, on four­nit une fonc­tion qui sera ap­pe­lée avec le pa­quet reçu en pa­ra­mètre. Le mo­teur de Twis­ted ap­pel­lera lui-même cette fonc­tion quand le pa­quet sera reçu. En at­ten­dant, il peut gé­rer d’autres as­pects, comme ac­cep­ter une nou­velle connexion. Les avan­tages d’une telle ap­proche sont multiples :

  • On n’a pas à gé­rer les pro­blèmes de com­mu­ni­ca­tion in­ter­pro­ces­sus ou les pro­blèmes de concur­rence dans les threads.
  • Le pas­sage à l’échelle est plus pré­vi­sible ; il n’y a par exemple pas de pro­blème à gé­rer 200 000 connexions si­mul­ta­nées (en de­hors de la consom­ma­tion mé­moire et pro­ces­seur), alors que ce se­rait pro­blé­ma­tique si chaque connexion de­vait être gé­rée par un processus.
  • Les per­for­mances peuvent être meilleures. En ef­fet, une por­tion de code ne rend la main que quand elle ef­fec­tue une ac­tion blo­quante (sou­vent liée au ré­seau). Le reste du temps, elle uti­lise 100% du pro­ces­seur. Il faut voir que Twis­ted est un fra­me­work ré­seau. Cette ef­fi­ca­cité s’en­tend donc sur les ap­pli­ca­tions ré­seau. S’il vous ve­nait à l’idée d’écrire une base de don­nées avec Twis­ted, les mé­ca­nismes pro­po­sés ne sont pas aussi ef­fi­caces. Par exemple, Twis­ted ne gère pas les lec­tures asyn­chrones de fi­chiers sur le disque.

Si vous n’êtes pas convaincu des per­for­mances de Twis­ted, ci­tons deux pro­duits libres l’uti­li­sant et of­frant des per­for­mances re­mar­quables grâce à celui-ci :

  • Flu­mo­tion, un ser­veur de streaming.
  • ZenOSS, un lo­gi­ciel de sur­veillance, concur­rent de Na­gios ou Zabbix.

Uti­li­ser Twis­ted ap­porte aussi quelques in­con­vénients :

  • La pro­gram­ma­tion asyn­chrone peut être dif­fi­cile à ap­pré­hen­der. Elle se tra­duit dans Twis­ted par la né­ces­sité de dé­fi­nir de nom­breuses fonc­tions pour ré­agir aux évè­ne­ments. Le code ne se lit donc pas d’un seul bloc. Il existe tou­te­fois une pos­si­bi­lité d’écrire du code à l’ap­pa­rence plus clas­sique en uti­li­sant les nou­veaux ité­ra­teurs de Py­thon 2.5.
  • Il n’y a qu’un seul pro­ces­sus, qu’un seul thread. On n’ex­ploite donc pas les ma­chines avec plu­sieurs cœurs. Ceux-ci sont tou­te­fois dis­po­nibles pour les autres pro­ces­sus (la base de don­nées par exemple). Twis­ted offre ce­pen­dant la pos­si­bi­lité de gé­rer plu­sieurs pro­ces­sus ou plu­sieurs threads, mais, dans ce cas, on se prive de cer­tains avan­tages de la pro­gram­ma­tion asynchrone.
  • La pro­gram­ma­tion asyn­chrone rend dif­fi­cile la com­pré­hen­sion des tra­ce­backs, ce qui peut rendre as­sez pé­nible la mise au point des ap­pli­ca­tions : quand une ex­cep­tion sur­vient, si elle ap­pa­raît lors du trai­te­ment d’un ap­pel dif­féré, le contexte cor­res­pon­dant à cet ap­pel dif­féré n’est plus dis­ponible.
  • La do­cu­men­ta­tion manque cruel­le­ment d’exemples. Il est par­fois as­sez dif­fi­cile de sa­voir de quelle fa­çon il faut agen­cer les dif­fé­rentes briques mises à dis­po­si­tion pour ar­ri­ver au ré­sul­tat voulu. La do­cu­men­ta­tion contient quelques très bons tu­to­riels, mais au-delà, il fau­dra sou­vent al­ler lire le code.
  • Twis­ted est très com­plet et veut s’adap­ter à la plu­part des si­tua­tions. Il uti­lise alors des concepts de très hauts ni­veaux que l’on ne voit pas for­cé­ment dans d’autres pro­jets : les in­ter­faces, les com­po­sants, les fa­briques, etc. Il dis­pose de nom­breuses fonc­tion­na­li­tés per­met­tant par exemple de gé­rer l’au­then­ti­fi­ca­tion aussi bien à l’aide d’un fi­chier plat que d’une base de don­nées. Le même mo­teur d’au­then­ti­fi­ca­tion peut être uti­lisé pour ob­te­nir aussi bien une en­tité re­pré­sen­tant un uti­li­sa­teur pour un ser­veur web (as­so­cié à une ses­sion) qu’un en­semble de boîtes mails. Twis­ted uti­lise les concepts ci­tés pré­cé­dem­ment pour im­bri­quer pro­pre­ment les choses, mais ceux-ci peuvent être dif­fi­ciles à aborder.

Il est im­pos­sible d’abor­der Twis­ted dans son in­té­gra­lité en un seul ar­ticle. Un pré­cé­dent nu­méro de GNU/Linux Ma­ga­zine conte­nait déjà une très bonne in­tro­duc­tion à Twis­ted. Nous al­lons es­sayer d’uti­li­ser une ap­proche dif­fé­rente en pré­sen­tant des exemples simples cor­res­pon­dant à ce que l’on a pu faire au­pa­ra­vant sans Twis­ted. Cer­tains exemples se­ront en­ri­chis de fa­çon à dé­mon­trer la puis­sance de Twisted.

La programmation asynchrone avec Twisted#

Pour nos lec­teurs qui n’au­raient pas lu l’in­tro­duc­tion à Twis­ted pré­cé­dem­ment ci­tée, nous al­lons rap­pe­ler ici les ru­di­ments de la pro­gram­ma­tion asyn­chrone avec Twisted.

Nous avons déjà écrit une ap­pli­ca­tion asyn­chrone uti­li­sant l’ap­pel sys­tème select. Twis­ted n’ex­pose pas une mé­thode de ce type. Quand votre pro­gramme va ef­fec­tuer une ac­tion po­ten­tiel­le­ment blo­quante, vous al­lez ap­pe­ler une fonc­tion qui va im­mé­dia­te­ment vous re­tour­ner un ob­jet Deferred. Vous al­lez en­suite pou­voir at­ta­cher des ac­tions à cet ob­jet. Ces ac­tions se­ront ef­fec­tuées soit quand la don­née at­ten­due est dis­po­nible, soit quand une er­reur est sur­ve­nue. Le mo­teur de Twis­ted, ap­pelé un ré­ac­teur, tient à jour une liste de l’en­semble de ces ob­jets et exé­cute les mé­thodes qui y sont rat­ta­chées se­lon les évè­ne­ments qui sur­viennent.

Pour mieux com­prendre ce prin­cipe, re­pre­nons notre tout pre­mier exemple. Nous le re­pro­dui­sons ici :

import urllib2

lm = urllib2.urlopen('http://www.gnulinuxmag.com/')
print(lm.read())

Com­ment faire avec Twis­ted ? Il y a ma­ni­fes­te­ment une ac­tion blo­quante qui est la lec­ture de la page Web. Twis­ted va nous sim­pli­fier la vie en of­frant un équi­valent de urllib2, mais adapté à son mo­teur. Voici un pro­gramme si­mi­laire uti­li­sant Twisted :

from twisted.web.client import getPage
from twisted.internet import reactor

def printContents(content):
    print(content)
    reactor.stop()

deferred = getPage("http://www.gnulinuxmag.com/")
deferred.addCallback(printContent)
reactor.run()

C’est un peu plus long. Voyons pas à pas com­ment cela fonc­tionne. Twis­ted dis­pose d’une bi­blio­thèque per­met­tant d’écrire fa­ci­le­ment un client web. Nous im­por­tons la mé­thode qui nous in­té­resse. Nous im­por­tons aussi le mo­teur de Twis­ted : le ré­ac­teur. Ce­lui-ci dis­pose de deux mé­thodes : run et stop. La pre­mière mé­thode per­met de dé­mar­rer le ré­ac­teur. Dé­mar­rer le ré­ac­teur per­met d’amor­cer une boucle in­fi­nie qui va gé­rer les dif­fé­rents évè­ne­ments. Cette boucle est si­mi­laire à ce que l’on trouve avec Qt et GTK. Il existe d’ailleurs un ré­ac­teur com­pa­tible avec la boucle de GTK. Cette boucle s’in­ter­rompt quand on ap­pelle la mé­thode stop. Il n’est pas conseillé de ten­ter de la re­dé­mar­rer de nou­veau avec run. L’ap­pel à stop in­ter­rompt gé­né­ra­le­ment le pro­gramme et il ne sera plus pos­sible d’uti­li­ser Twis­ted par la suite.

Igno­rons pour le mo­ment la fonc­tion printContent. Nous ap­pe­lons donc la fonc­tion getPage avec en pa­ra­mètre la page que nous sou­hai­tons ré­cu­pé­rer. Cette ac­tion n’est pas im­mé­diate. Il faut ou­vrir une so­cket ré­seau, ré­soudre le nom de do­maine, ef­fec­tuer une re­quête http, puis at­tendre la ré­ponse. La fonc­tion getPage fait tout ceci, tout comme le fai­sait la fonc­tion urlopen. Tou­te­fois, cette der­nière ne ren­dait pas la main avant d’avoir pu contac­ter le ser­veur. Avec Twis­ted, les fonc­tions qui ef­fec­tuent des ac­tions po­ten­tiel­le­ment blo­quantes rendent im­mé­dia­te­ment la main. Elles ren­voient un ob­jet Deferred au­quel il est pos­sible d’at­ta­cher des actions.

Dans notre exemple, nous ré­cu­pé­rons cet ob­jet Deferred dans une va­riable et nous lui at­ta­chons un callback qui est une fonc­tion qui sera ap­pe­lée quand les don­nées se­ront dis­po­nibles. Cet at­ta­che­ment se fait à l’aide de la mé­thode addCallback de l’ob­jet Deferred. Elle prend comme pre­mier pa­ra­mètre une fonc­tion qui est ici printContent. On peut lui don­ner des pa­ra­mètres sup­plé­men­taires qui se­ront pas­sés à la fonc­tion printContent.

Lorsque le contenu de la page web est dis­po­nible, le ré­ac­teur va ap­pe­ler la fonc­tion printContent avec comme pre­mier pa­ra­mètre le contenu de la page web. De ma­nière gé­né­rale, la fonc­tion dé­cla­rée en callback re­ce­vra en pre­mier pa­ra­mètre la don­née at­ten­due. Elle peut prendre des pa­ra­mètres sup­plé­men­taires qui de­vront alors être four­nis lors de l’ap­pel à addCallback.

Notre fonc­tion printContent af­fiche la page ob­te­nue, puis stoppe le ré­ac­teur. Cette der­nière ac­tion per­met au pro­gramme de se terminer.

Re­gar­dons de nou­veau le tout pre­mier pro­gramme de cet ar­ticle. Que se passe-t-il si l’URL de­man­dée n’existe pas ? Par exemple, si la page de­man­dée ren­voie une er­reur 404 ? Nous ob­te­nons une ex­cep­tion. Nous pou­vons la trai­ter comme le montre cet exemple :

import urllib2

try:
    lm = urllib2.urlopen("http://www.gnulinuxmag.com/")
    print(lm.read())
except:
    print("Impossible de récupérer la page")

Que se passe-t-il avec Twis­ted ? Il ne se passe rien, le pro­gramme ne rend pas la main, mais n’af­fiche rien non plus. Si on l’in­ter­rompt avec Ctrl+C, il nous dit :

Unhandled error in Deferred:
Traceback (most recent call last):
Failure: twisted.web.error.Error: 404 Not Found

Twis­ted n’ap­pelle la fonc­tion printContent qu’en cas de suc­cès. En cas d’er­reur, il faut lui four­nir une autre fonc­tion à ap­pe­ler pour gé­rer cette er­reur. Nous per­dons alors par­tiel­le­ment le mé­ca­nisme d’ex­cep­tions de Py­thon. Il est tou­jours pos­sible de lan­cer des ex­cep­tions, mais, pour les cap­tu­rer, il faut at­ta­cher des fonc­tions conçues à cet ef­fet. Mo­di­fions notre exemple.

from twisted.web.client import getPage
from twisted.internet import reactor

def printContent(content):
    print(content)
    reactor.stop()

def handleError(error):
    print("Une erreur est survenue :")
    print(error)
    reactor.stop()

deferred = getPage("http://www.gnulinuxmag.com/ooo")
deferred.addCallback(printContent)
deferred.addErrback(handleError)
reactor.run()

Nous avons donc dé­claré une nou­velle fonc­tion pre­nant en pa­ra­mètre l’er­reur ob­te­nue (il s’agit en fait d’une ins­tance de la classe Failure). Nous at­ta­chons cette fonc­tion à l’ob­jet Deferred ob­tenu pré­cé­dem­ment avec la mé­thode addErrback.

Pour fi­nir cette in­tro­duc­tion sur la pro­gram­ma­tion asyn­chrone avec Twis­ted, il reste à dé­cou­vrir une der­nière sub­ti­lité. Il est pos­sible d’en­chaî­ner les callback :

from twisted.web.client import getPage
from twisted.internet import reactor

def printContent(content):
    print(content[:100])
    return 18

def addTwo(number):
    return number + 2

def printResult(number):
    print("J'ai obtenu %d" % number)

def finalCallback(dummy):
    print("Fini!")
    reactor.stop()

deferred = getPage("http://www.gnulinuxmag.com/")
deferred.addCallback(printContent)
deferred.addCallback(addTwo)
deferred.addCallback(printResult)
deferred.addBoth(finalCallback)
reactor.run()

Notre pro­gramme va donc ré­cu­pé­rer la page in­di­quée. En cas de suc­cès, il va af­fi­cher les pre­miers ca­rac­tères de cette page. Tou­jours en cas de suc­cès, la fonc­tion addTwo va être ap­pe­lée. Elle va prendre en pa­ra­mètre ce qui a été re­tourné par la fonc­tion printContent. En cas de suc­cès, c’est la fonc­tion printResult qui va être ap­pe­lée. En­fin, dans tous les cas, on ap­pelle la fonc­tion finalCallback qui ar­rête le ré­ac­teur. En lan­çant ce pro­gramme, nous obtenons :

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1
J'ai obtenu 20
Fini!

Il est ainsi pos­sible de chaî­ner les callback. Chaque callback uti­lise le ré­sul­tat du callback pré­cé­dent. Un callback peut éga­le­ment re­tour­ner un Deferred. Dans ce cas, le callback sui­vant ne sera ap­pelé que quand le contenu as­so­cié à ce Deferred sera dis­po­nible et aura comme pa­ra­mètre le contenu en ques­tion. Il est aussi pos­sible de lan­cer une ex­cep­tion ou de re­tour­ner un ob­jet Failure. Dans ce cas, c’est le pro­chain callback d’er­reur dans la chaîne qui sera appelé.

L’illus­tra­tion 1 pré­sente le com­por­te­ment at­tendu pour le pseudo code sui­vant. La flèche rouge est sui­vie en cas d’er­reur (ex­cep­tion ou ob­jet Failure ren­voyé par la fonc­tion), tan­dis que la flèche verte est sui­vie dans les autres cas.

deferred.addCallbacks(callback1, errback1)
deferred.addCallbacks(callback2, errback2)
deferred.addCallbacks(callback3, errback3)

La fonc­tion addCallbacks per­met d’en­re­gis­trer à la fois un callback et un errback. C’est un pe­tit peu dif­fé­rent du code suivant :

deferred.addCallback(callback1)
deferred.addErrback(errback1)

En ef­fet, dans le pre­mier cas, errback1 ne trai­tera que les er­reurs pro­ve­nant de getPage, tan­dis que, dans le se­cond cas, elle trai­tera éga­le­ment les er­reurs qui ont pu sur­ve­nir dans callback1. Pour ré­su­mer, si un callback ou un errback émet une ex­cep­tion ou re­tourne un ob­jet Failure, c’est le pro­chain errback dans la chaîne qui va s’en oc­cu­per. Dans le cas in­verse, c’est le pro­chain callback dans la chaîne qui prend le relais.

Callbacks and errbacks handling in Twisted with potential crossovers between
the two paths
Enchaînement des callbacks

Écrire un serveur avec Twisted#

Nous al­lons, dans un pre­mier temps, nous consa­crer à l’écri­ture d’un ser­veur équi­valent à ce­lui que nous avions écrit pour illus­trer l’uti­li­sa­tion de select. Pour ce faire, nous al­lons nous ai­der du pro­gramme twistd fourni avec Twis­ted. Ce der­nier va gé­rer un cer­tain nombre de choses pour nous, dont la ges­tion du ré­ac­teur (dé­mar­rage et ar­rêt), la mise en dé­mon et les logs.

Découverte de twistd#

Le pro­gramme twistd peut prendre en en­trée dif­fé­rents types de fi­chiers. Nous al­lons nous concen­trer sur l’op­tion per­met­tant de lui four­nir un fi­chier Py­thon à éva­luer. Dans ce cas, il faut dé­fi­nir une va­riable application qui sera une ins­tance de twisted.​applications.​service.​Application. L’exemple mi­ni­mal est alors le sui­vant (que l’on ap­pelle exemple.py) :

from twisted.application import service

application = service.Application("exemple")

Es­sayons de le lan­cer pour voir ce qui se passe :

$ twistd -n -o -y exemple.py
2008-06-14 18:54:32+0200 [-] Log opened.
2008-06-14 18:54:32+0200 [-] twistd 8.1.0 (/usr/bin/python 2.5.2) starting up
2008-06-14 18:54:32+0200 [-] reactor class: <class 'twisted.internet.selectreactor.SelectReactor'>

Et si on ap­puie sur Ctrl+C, l’ap­pli­ca­tion s’ar­rête pro­prement :

2008-06-14 18:55:11+0200 [-] Received SIGINT, shutting down.
2008-06-14 18:55:11+0200 [-] Main loop terminated.
2008-06-14 18:55:11+0200 [-] Server Shut Down.

Nous avons donné les pa­ra­mètres -n, -o et -y à twistd. Le pre­mier in­dique que l’on ne veut pas qu’il se trans­forme en dé­mon de fa­çon à gar­der la main. Dans le cas contraire, il tour­ne­rait en tâche de fond et les logs se­raient écrits dans sys­log. Le se­cond per­met de lui dire de ne pas sau­ver l’état de l’ap­pli­ca­tion lors de la sor­tie. En ef­fet, Twis­ted pro­pose des fonc­tion­na­li­tés per­met­tant de faire un snap­shot d’une ap­pli­ca­tion pour la re­dé­mar­rer au même en­droit par la suite. En­fin, le der­nier pa­ra­mètre in­dique que nous al­lons four­nir un fi­chier Python.

Lors du dé­mar­rage, twistd in­dique quel ré­ac­teur il uti­lise (ce­lui à base de select dans notre cas). Il en existe d’autres. Consul­tez la page de ma­nuel pour sa­voir com­ment choi­sir un autre réacteur.

Premiers pas vers un serveur fonctionnel#

Notre ap­pli­ca­tion est pour le mo­ment peu in­té­res­sante : nous avons créé une ap­pli­ca­tion qui ne fait rien. Nous al­lons lui at­ta­cher un ser­vice. Ce ser­vice va écou­ter sur un port et, pour chaque client se connec­tant, il va de­man­der à une en­tité spé­ciale (Factory) de créer une ins­tance de Protocol qui va dis­cu­ter avec le client. Quand plu­sieurs clients sont connec­tés, ils au­ront cha­cun une ins­tance de Protocol, mais il n’y aura qu’une seule ins­tance de Factory. Quand il n’y a au­cun client, l’ins­tance de Factory est en­core là. Dans la pra­tique, cette ins­tance per­met de sto­cker des in­for­ma­tions per­sis­tantes (que les clients peuvent éven­tuel­le­ment mo­difier).

Voyons ce que cela donne. Avant d’écrire notre ser­veur de com­mandes, écri­vons un ser­veur qui dit « bon­jour », ré­pète tout ce que nous lui en­voyons et tient le compte du nombre de clients. Voici le code !

from twisted.application import internet, service
from twisted.internet.protocol import Protocol, Factory

class SimpleProtocol(Protocol):
    def connectionMade(self):
        self.factory.numClients += 1
        self.transport.write("Bonjour ! Il y a %d clients.\n" % self.factory.numClients)

    def connectionLost(self, reason):
        self.factory.numClients -= 1

    def dataReceived(self, data):
        self.transport.write(data)

class SimpleFactory(Factory):
    protocol = SimpleProtocol

    def __init__(self):
        self.numClients = 0

application = service.Application("exemple")
myfactory = SimpleFactory()
myservice = internet.TCPServer(2022, myfactory)
myservice.setServiceParent(application)

Nous dé­fi­nis­sons deux classes. La pre­mière hé­rite de la classe Protocol et va donc dis­cu­ter di­rec­te­ment avec le client. La mé­thode connectionMade est ap­pe­lée lorsque la connexion est éta­blie, la mé­thode connectionLost quand celle-ci est cou­pée et, en­fin, dataReceived est ap­pelé lorsque des don­nées sont re­çues. Une ins­tance de cette classe contient une ré­fé­rence vers l’ins­tance de Factory dont elle dé­pend. Cela nous per­met de sto­cker le nombre de clients. Elle contient éga­le­ment une ré­fé­rence vers une ins­tance d’un ob­jet qui va nous per­mettre d’en­voyer réel­le­ment des don­nées au client. Cet ob­jet dis­pose d’une mé­thode write que nous uti­li­sons pour ren­voyer au client les don­nées que l’on a reçues.

La se­conde classe est la fa­brique (ou l’usine) qui va four­nir des ins­tances de Protocol pour chaque client. Elle doit conte­nir une mé­thode buildProtocol ren­voyant une ins­tance de Protocol ca­pable de dia­lo­guer avec le client. Nous hé­ri­tons cette mé­thode de Factory. Celle-ci va ins­tan­cier l’at­tri­but protocol que nous avons ini­tia­lisé à SimpleProtocol. Cette mé­thode est aussi res­pon­sable de lier l’ins­tance de Factory à l’ins­tance de Protocol créée.

En­fin, après avoir dé­fini notre ap­pli­ca­tion, nous ins­tan­cions notre fa­brique et nous créons un ser­vice qui uti­li­sera cette fa­brique pour ob­te­nir une ins­tance de Protocol à même de dis­cu­ter avec le client. Nous rat­ta­chons ce ser­vice à notre ap­pli­ca­tion afin qu’il soit dé­marré au lan­ce­ment du client.

Voyons ce que cela donne !

$ twistd -noy exemple2.py
2008-06-14 19:15:12+0200 [-] Log opened.
2008-06-14 19:15:12+0200 [-] twistd 8.1.0 (/usr/bin/python 2.5.2) starting up
2008-06-14 19:15:12+0200 [-] reactor class: <class 'twisted.internet.selectreactor.SelectReactor'>
2008-06-14 19:15:12+0200 [-] __builtin__.SimpleFactory starting on 2022
2008-06-14 19:15:12+0200 [-] Starting factory <__builtin__.SimpleFactory instance at 0x18cc2d8>

Et de­puis un autre ter­mi­nal, nous pou­vons nous connecter.

$ nc localhost 2022
Bonjour ! Il y a 1 clients.
Allo ?
Allo ?

Si vous vous connec­tez de­puis d’autres ter­mi­naux, le nombre de clients va aug­men­ter. Tout va pour le mieux.

Une parenthèse sur les concepts de Twisted#

Comme in­di­qué par ailleurs, Twis­ted uti­lise beau­coup de concepts ob­jets aux­quels il faut s’ha­bi­tuer. En­core une fois, les ar­ticles d’in­tro­duc­tion de Twis­ted sont très im­por­tants à lire pour bien com­prendre cha­cun des prin­cipes uti­li­sés. Si vous êtes fa­mi­liers avec la no­tion de « De­sign Pat­terns », vous avez re­connu la no­tion de fa­brique uti­li­sée. Sans en­trer dans les dé­tails, le motto d’une telle ap­proche est le dé­cou­plage maxi­mal : le ser­vice ne sait pas créer di­rec­te­ment le pro­to­cole, il fait ap­pel à un in­ter­mé­diaire qui sait. Le fait de ne pas lier le ser­vice au pro­to­cole peut sem­bler d’abord peu na­tu­rel, mais on gagne par la suite en flexi­bi­lité : cet in­ter­mé­diaire peut par exemple dé­ci­der de ne pas four­nir le pro­to­cole im­mé­dia­te­ment ou de four­nir un pro­to­cole dif­fé­rent se­lon le client (par exemple, s’il y a trop de clients).

Un deuxième concept au­quel il faut s’ha­bi­tuer est l’uti­li­sa­tion d’in­ter­faces. Nous avons vu ci-des­sus que nous de­vions four­nir au ser­vice une ins­tance de Factory dis­po­sant de la mé­thode buildProtocol. C’est en fait in­exact. En règle gé­né­rale, Twis­ted ne de­mande pas que les ob­jets soient des ins­tances de classes par­ti­cu­lières. Il de­mande de lui four­nir une ins­tance qui res­pecte une in­ter­face, c’est-à-dire qui pos­sède un cer­tain nombre de mé­thodes et d’at­tri­buts. Pour créer un ser­vice, il faut donc four­nir un ob­jet qui res­pecte l’in­ter­face IProtocolFactory. On peut uti­li­ser ou hé­ri­ter de la classe Factory, mais ce n’est pas obli­ga­toire. Il existe des in­ter­faces qui n’ont pas de classe ca­no­nique. Dans ce cas, pour créer une classe qui res­pecte une telle in­ter­face, il faut donc écrire cha­cune des mé­thodes dé­cla­rées dans l’in­ter­face et in­di­quer que la classe im­plé­mente l’in­ter­face vou­lue (avec implements). Pour une in­tro­duc­tion sur les in­ter­faces, le lec­teur est in­vité à se ré­fé­rer à l’ar­ticle sur la Zope Com­ponent Ar­chi­tec­ture de ce nu­méro. À no­ter ce­pen­dant que Twis­ted n’uti­lise qu’une par­tie de cette ar­chi­tec­ture et ré­im­plé­mente cer­taines par­ties (le re­gistre des adap­ta­teurs par exemple). Aussi, il est conseillé de lire éga­le­ment l’in­tro­duc­tion aux in­ter­faces et adap­ta­teurs de la do­cu­men­ta­tion de Twis­ted. C’est une lec­ture in­dis­pen­sable quand on veut al­ler un peu plus loin dans Twisted.

La do­cu­men­ta­tion de l’API de Twis­ted in­dique pour chaque ob­jet quelle est l’in­ter­face qu’il res­pecte et, pour chaque in­ter­face, quelles sont les classes qui im­plé­mentent cette in­terface.

Cette no­tion d’in­ter­face par­ti­cipe aussi au dé­cou­plage entre ob­jets. En de­man­dant sim­ple­ment un ob­jet res­pec­tant une in­ter­face don­née, on se per­met de chan­ger l’im­plé­men­ta­tion fa­ci­le­ment. C’est si­mi­laire à l’uti­li­sa­tion des classes abs­traites (qui sont des classes dont on doit hé­ri­ter et im­plé­men­ter les dif­fé­rentes mé­thodes dé­clarées).

Première implémentation du serveur de commandes#

Nous al­lons main­te­nant nous at­ta­quer à une pre­mière im­plé­men­ta­tion du ser­veur de com­mandes. Si nous adap­tons le code que nous avions uti­lisé pour pré­sen­ter la fonc­tion select, nous ob­te­nons alors le code suivant :

import commands
from twisted.protocols.basic import LineReceiver
from twisted.application import internet, service
from twisted.internet.protocol import Protocol, Factory

class SimpleProtocol(LineReceiver):
    delimiter = "\n"

    def connectionMade(self):
        self.transport.write("Que puis-je faire pour vous ?\n")

    def lineReceived(self, line):
        ordre = line.strip()
        if ordre == "uptime":
            status, uptime = commands.getstatusoutput("uptime")
            self.transport.write("%s\n" % uptime)
        elif ordre == "df":
            status, df = commands.getstatusoutput("df -h")
            self.transport.write("%s\n" % df)
        else:
            self.transport.write("Je n'ai rien compris.\n")
        self.transport.loseConnection()

application = service.Application("exemple")
myfactory = Factory()
myfactory.protocol = SimpleProtocol
myservice = internet.TCPServer(2022, myfactory)
myservice.setServiceParent(application)

Afin de rac­cour­cir le code, nous avons uti­lisé un pe­tit rac­courci évi­tant de dé­cla­rer une classe hé­ri­tant de Factory. Nous créons sim­ple­ment une ins­tance de Factory et adap­tons l’at­tri­but protocol.

Autre nou­veauté, nous n’hé­ri­tons pas sim­ple­ment de Protocol, mais d’une de ses spé­cia­li­sa­tions, LineReceiver, qui offre la pos­si­bi­lité de re­ce­voir les com­mandes ligne par ligne. En ef­fet, la mé­thode dataReceived de Protocol ne nous ga­ran­tis­sait pas que nous al­lions re­ce­voir une et une seule ligne à chaque fois. Ici, pas de pro­blème. Cette classe pro­pose la mé­thode lineReceived qui est ap­pe­lée avec la ligne re­çue en pa­ra­mètre. Le reste du code est alors si­mi­laire à ce que nous avions fait dans la pré­sen­ta­tion de select.

L’ap­pli­ca­tion semble se com­por­ter exac­te­ment de la même fa­çon. Tou­te­fois, elle a aussi les mêmes dé­fauts. L’uti­li­sa­tion du mo­dule commands est blo­quante. Si ja­mais une des com­mandes est blo­quante (par exemple si df met du temps à rendre la main en rai­son de par­tages NFS), notre ap­pli­ca­tion est fi­gée : il est im­pos­sible d’ou­vrir de nou­velles connexions ou d’uti­li­ser les connexions existantes.

Nous n’avons pas pré­senté de ver­sion cor­ri­geant ce dé­faut quand nous uti­li­sions sim­ple­ment select, car c’était quelque chose d’as­sez pé­nible. Avec Twis­ted, c’est beau­coup plus simple ! En ef­fet, ce der­nier dis­pose d’un en­semble de classes nous ai­dant à réa­li­ser ceci. Tout d’abord, on peut dia­lo­guer avec un pro­ces­sus ex­terne comme avec un client ré­seau. Nous pour­rions donc créer un pro­to­cole qui li­rait tout ce que le pro­ces­sus nous en­voie et l’en­voyer au client. Mais, il y a en­core plus simple ! Twis­ted dis­pose déjà d’une fonc­tion ca­pable de faire de telles opé­ra­tions. Voyons cela :

from twisted.protocols.basic import LineReceiver
from twisted.application import internet, service
from twisted.internet.protocol import Protocol, Factory
from twisted.internet import utils

class SimpleProtocol(LineReceiver):
    delimiter = "\n"

    def connectionMade(self):
        self.transport.write("Que puis-je faire pour vous ?\n")

    def lineReceived(self, line):
        ordre = line.strip()
        if ordre == "uptime":
            output = utils.getProcessOutput("/usr/bin/uptime")
            output.addCallbacks(self.sendOutput, self.sendError)
        elif ordre == "df":
            output = utils.getProcessOutput("/bin/df", ("-h",))
            output.addCallbacks(self.sendOutput, self.sendError)
        else:
            self.transport.write("Je n'ai rien compris.\n")
            self.transport.loseConnection()

    def sendOutput(self, output):
        self.transport.write(output)
        self.transport.loseConnection()

    def sendError(self, reason):
        self.transport.write("Uh?\n")
        self.transport.loseConnection()

application = service.Application("exemple")
myfactory = Factory()
myfactory.protocol = SimpleProtocol
myservice = internet.TCPServer(2022, myfactory)
myservice.setServiceParent(application)

Nous avons uti­lisé la fonc­tion getProcessOutput. Cette fonc­tion nous per­met d’in­tro­duire pour la pre­mière fois un ob­jet Deferred dans une ap­pli­ca­tion réelle. En ef­fet, elle re­tourne un ob­jet Deferred étant donné que l’exé­cu­tion de l’ap­pli­ca­tion est blo­quante. Ainsi, pen­dant que l’ap­pli­ca­tion s’exé­cute, le ré­ac­teur va ef­fec­tuer d’autres tâches telles que l’ac­cueil de nou­veaux clients ou le trai­te­ment de clients sup­plé­mentaires.

Pour que ce Deferred ait un ef­fet, nous at­ta­chons des callback. La mé­thode sendOutput en cas de suc­cès et la mé­thode sendError en cas d’échec (par exemple, si la com­mande n’existe pas). Lorsque les ré­sul­tats sont dis­po­nibles, la mé­thode sendOutput sera ap­pe­lée avec comme ar­gu­ment le ré­sul­tat de la com­mande. Nous écri­vons le ré­sul­tat et cou­pons la connexion.

No­tez que, dans la mé­thode lineReceived, nous ne cou­pons pas la connexion en fin de mé­thode (sauf en cas de com­mande non re­con­nue). En ef­fet, si tel était le cas, la connexion se­rait fer­mée avant d’avoir pu af­fi­cher le ré­sul­tat (et ceci dans tous les cas ; le code n’est ja­mais pré­empté !).

Nous avons réa­lisé notre pre­mière ap­pli­ca­tion to­ta­le­ment asyn­chrone avec Twis­ted. Ce n’était pas si dif­fi­cile, n’est-ce pas ?

Et la sécurité ?#

Ad­met­tons que nous vou­lions mettre notre ap­pli­ca­tion sur In­ter­net afin de pou­voir consul­ter à dis­tance l’es­pace disque. Ac­tuel­le­ment, toutes les in­for­ma­tions passent en clair et il n’y a pas d’au­then­ti­fi­ca­tion. Nous al­lons sim­ple­ment uti­li­ser SSL pour se pro­té­ger. Nous en pro­fi­tons pour de­man­der un mot de passe avant d’ac­cep­ter une com­mande. Voici le nou­veau code :

from twisted.protocols.basic import LineReceiver
from twisted.application import internet, service
from twisted.internet.protocol import Protocol, Factory
from twisted.internet import utils, ssl

class SimpleProtocol(LineReceiver):
    delimiter = "\n"

    def connectionMade(self):
        self.transport.write("Le mot de passe ?\n")
        self.authenticated = 0

    def lineReceived(self, line):
        if self.authenticated == 0:
            password = line.strip()
            if password != "toto":
                self.transport.loseConnection()
            else:
                self.transport.write("Que puis-je faire pour vous ?\n")
                self.authenticated = 1
            return
        ordre = line.strip()
        if ordre == "uptime":
            output = utils.getProcessOutput("/usr/bin/uptime")
            output.addCallbacks(self.sendOutput, self.sendError)
        elif ordre == "df":
            output = utils.getProcessOutput("/bin/df", ("-h",))
            output.addCallbacks(self.sendOutput, self.sendError)
        else:
            self.transport.write("Je n'ai rien compris.\n")
            self.transport.loseConnection()

    def sendOutput(self, output):
        self.transport.write(output)
        self.transport.loseConnection()

    def sendError(self, reason):
        self.transport.write("Uh?\n")
        self.transport.loseConnection()

application = service.Application("exemple")
myfactory = Factory()
myfactory.protocol = SimpleProtocol
myservice = internet.TCPServer(2022, myfactory)
myservice.setServiceParent(application)
sslcontext = ssl.DefaultOpenSSLContextFactory("/tmp/privkey.pem", "/tmp/cacert.pem")
sslservice = internet.SSLServer(2023, myfactory, sslcontext)
sslservice.setServiceParent(application)

Nous avons ajouté un peu de code pour de­man­der un mot de passe au dé­but de la mé­thode lineReceived. En­suite, nous avons ajouté un ser­vice à notre ap­pli­ca­tion : ce sont les trois der­nières lignes. Pour tes­ter, vous de­vez créer la paire de clefs et le cer­ti­fi­cat au­to­si­gné de cette façon :

$ cd /tmp
$ openssl req -new -x509 -keyout privkey.pem -out cacert.pem -days 1000

Et pour tester :

$ openssl s_client -host localhost -port 2023

Simple, n’est-ce pas ? Nous com­men­çons à voir les avan­tages du dé­cou­plage. Nous avons conservé le même pro­to­cole et avons ajouté un nou­veau ser­vice en­cap­su­lant notre pro­to­cole dans SSL. Le ser­vice TCP est tou­jours dis­ponible.

Ajouter une interface Web#

Pour­quoi ne pas ajou­ter une in­ter­face Web ? Twis­ted vous per­met de créer fa­ci­le­ment un ser­veur Web. Voici ce que nous de­vons ajou­ter à notre code :

from twisted.web import server, resource

class SimpleWeb(resource.Resource):
    isLeaf = True

    def render_GET(self, request):
        ordre = request.path[1:]
        if ordre == "":
            return """<html><ul>
<li><a href="/df">df</a></li>
<li><a href="/uptime">uptime</a></li>
</ul></html>"""
        if ordre == "uptime":
            output = utils.getProcessOutput("/usr/bin/uptime")
            output.addCallback(self.sendOutput, request)
        elif ordre == "df":
            output = utils.getProcessOutput("/bin/df", ("-h",))
            output.addCallback(self.sendOutput, request)
        else:
            return """<html>Je n'ai rien compris</html>"""
        return server.NOT_DONE_YET

    def sendOutput(self, data, request):
        request.write("<html>Voici le resultat : <pre>%s</pre></html>" % data)
        request.finish()

website = server.Site(SimpleWeb())
webservice = internet.TCPServer(2024, website)
webservice.setServiceParent(application)

Le code n’est pas un mo­dèle de beauté. Nous au­rions dû pré­pa­rer une classe sé­pa­rée qui au­rait per­mis de mu­tua­li­ser la ges­tion des dif­fé­rentes com­mandes entre la par­tie web et la par­tie ligne de com­mande. Tou­te­fois, nous conser­vons cette ap­proche plus simple. Nous ins­tan­cions donc une res­source que nous trans­for­mons en un ser­vice que nous at­ta­chons à notre ap­pli­ca­tion. Cette res­source dis­pose d’une mé­thode render_​GET ap­pe­lée à chaque fois que l’on ef­fec­tue un GET. Cette mé­thode prend en pa­ra­mètre une ins­tance de Request que nous uti­li­sons pour ré­cu­pé­rer le che­min de­mandé, mais aussi pour répondre.

Il y a alors deux ma­nières de ré­pondre. Si la ré­ponse est prête, on peut sim­ple­ment re­tour­ner une chaîne qui sera alors trans­mise au client pour af­fi­chage dans son na­vi­ga­teur. Tou­te­fois, quand la re­quête n’est pas prête, nous de­vons re­tour­ner une va­leur spé­ciale (server.​NOT_​DONE_​YET). Dans ce cas, on peut ap­pe­ler la mé­thode write de l’ob­jet Request au­tant de fois que l’on veut et ter­mi­ner par l’ap­pel de la mé­thode finish. C’est ce que nous fai­sons dans notre callback.

Note

C’est une ap­proche qui n’est pas co­hé­rente avec la phi­lo­so­phie de Twis­ted. Il au­rait été plus na­tu­rel de re­tour­ner un ob­jet Deferred. Comme Rome, Twis­ted ne s’est pas construit en un jour. C’est un dé­faut qui est cor­rigé dans la nou­velle gé­né­ra­tion de ser­veurs Web pour Twisted.

Pour tes­ter, di­ri­gez votre na­vi­ga­teur vers http://localhost:2024.

Le fra­me­work web de Twis­ted est re­la­ti­ve­ment com­plet. Il est pos­sible d’as­so­cier des classes à cer­taines URL de fa­çon à mo­du­la­ri­ser la construc­tion du site web. Il reste ce­pen­dant as­sez ba­sique. Il ne contient par exemple pas de mo­teur de tem­plates pour construire des pages web plus pro­pre­ment. Une nou­velle ver­sion est en cours de dé­ve­lop­pe­ment (web2). Il n’est ce­pen­dant pas conseillé de l’uti­li­ser. Pro­gres­si­ve­ment, les points forts de cette ver­sion se­ront in­té­grés dans le mo­dule ac­tuel. En at­ten­dant, la mé­thode la plus conseillée est d’uti­li­ser Ne­vow, un fra­me­work web com­plet s’ap­puyant sur Twisted.

Un accès via SSH ?#

Twis­ted pro­pose un mo­dule ap­pelé conch pour écrire des clients ou des ser­veurs SSH. Voyons com­ment ra­jou­ter un ac­cès SSH. SSH est un pro­to­cole com­pli­qué pro­po­sant de nom­breuses pos­si­bi­li­tés : di­verses mé­thodes d’au­then­ti­fi­ca­tion, pos­si­bi­lité d’ou­vrir plu­sieurs ca­naux, ou­ver­ture d’un ter­mi­nal, exé­cu­tion d’une com­mande à dis­tance, etc. Twis­ted tente de sim­pli­fier son uti­li­sa­tion tout en per­met­tant d’ex­ploi­ter toutes les sub­ti­li­tés. On au­rait pu es­pé­rer quelque chose d’aussi simple que d’écou­ter sur un port en TCP, mais, mal­heu­reu­se­ment, un tel ni­veau d’abs­trac­tion n’existe pas. Twis­ted pro­pose un mo­dule ca­chant la ma­jeure par­tie de la com­plexité pour une uti­li­sa­tion simple. On ne peut tou­te­fois pas fa­ci­le­ment uti­li­ser notre classe SimpleProtocol exis­tante, car le pro­to­cole at­tendu doit res­pec­ter l’in­ter­face IterminalProtocol. Il n’existe a priori pas d’adap­ta­teur entre les deux in­ter­faces pour le mo­ment. Nous al­lons en­core une fois du­pli­quer du code par souci de sim­pli­cité. Voici la par­tie SSH de notre ap­plication :

from twisted.conch.manhole_ssh import ConchFactory, TerminalRealm
from twisted.cred import portal, checkers
from twisted.conch import avatar, interfaces, recvline
from twisted.conch.ssh import session
from twisted.conch.insults import insults
from zope.interface import implements

class MySSHProtocol(recvline.HistoricRecvLine):
    def lineReceived(self, line):
        ordre = line.strip()
        if ordre == "uptime":
            output = utils.getProcessOutput("/usr/bin/uptime")
            output.addCallbacks(self.sendOutput, self.sendError)
        elif ordre == "df":
            output = utils.getProcessOutput("/bin/df", ("-h",))
            output.addCallbacks(self.sendOutput, self.sendError)
        elif ordre == "quit":
            self.terminal.loseConnection()
        else:
            self.terminal.write("Je n'ai rien compris.")
            self.terminal.nextLine()

    def sendOutput(self, output):
        self.terminal.write(output)

    def sendError(self, reason):
        self.terminal.write("Uh?\n")

tr = TerminalRealm()
tr.chainedProtocolFactory.protocolFactory = MySSHProtocol
p = portal.Portal(tr)
users = {"admin": "toto", "root": "hello"}
p.registerChecker(checkers.InMemoryUsernamePasswordDatabaseDontUse(**users))
sshfactory = ConchFactory(p)
sshservice = internet.TCPServer(2025, sshfactory)
sshservice.setServiceParent(application)

Com­men­çons par la fin. Twis­ted pro­pose toute une in­fra­struc­ture pour gé­rer l’au­then­ti­fi­ca­tion de ma­nière aussi gé­né­rique que pos­sible. Cette in­fra­struc­ture n’est pas li­mi­tée à SSH, mais peut être réuti­li­sée pour un ser­veur Web par exemple. La do­cu­men­ta­tion de Twis­ted pro­pose une in­tro­duc­tion com­plète à celle-ci. La pièce cen­trale est l’ob­jet Portal. Ce­lui-ci va in­ter­agir avec le pro­to­cole pour ob­te­nir les élé­ments d’iden­ti­fi­ca­tion de l’uti­li­sa­teur. Il va en­suite les sou­mettre à dif­fé­rents mé­ca­nismes de vé­ri­fi­ca­tion. Ici, nous n’en avons ins­tan­cié qu’un seul : la vé­ri­fi­ca­tion par mot de passe avec une base de don­nées conte­nue dans un dic­tion­naire. Une fois l’uti­li­sa­teur au­then­ti­fié, l’ob­jet Portal va s’adres­ser à un ob­jet Realm qui doit lui re­tour­ner un ava­tar qui est la re­pré­sen­ta­tion de l’uti­li­sa­teur adapté au contexte de­mandé (ici un uti­li­sa­teur SSH).

Nous ins­tan­cions donc di­vers ob­jets pour construire l’ob­jet Factory qui sera uti­lisé pour construire le ser­vice. Lors des ins­tan­cia­tions, nous trou­vons une oc­ca­sion pour at­ta­cher notre pro­to­cole. Ce der­nier est qua­si­ment iden­tique au pro­to­cole uti­lisé pour les connexions en clair et les connexions SSL. Le trans­port est sub­sti­tué par un ter­mi­nal qui né­ces­site quelques pré­cau­tions sup­plé­men­taires. Étant donné que ce der­nier est ef­facé avant de fer­mer la connexion, nous ac­cep­tons plu­sieurs com­mandes et nous ajou­tons une com­mande quit.

Conch est consi­déré comme l’un des mo­dules les plus com­pli­qués de Twis­ted. Il su­bit ac­tuel­le­ment d’im­por­tantes re­fontes afin de le rendre plus co­hé­rent avec les autres mo­dules. Nous sommes tou­te­fois par­ve­nus à écrire notre ser­veur SSH en quelques lignes. Ainsi, nous avons une ap­pli­ca­tion sé­cu­ri­sée, sup­por­tant une au­then­ti­fi­ca­tion par mot de passe et pou­vant fa­ci­le­ment être éten­due pour sup­por­ter en plus une au­then­ti­fi­ca­tion par clefs.

Conclusion#

Py­thon est un lan­gage de pre­mier choix pour écrire tout type d’ap­pli­ca­tions ré­seau. Il dis­pose dans sa bi­blio­thèque stan­dard de nom­breux mo­dules pour ai­der à l’écri­ture de celles-ci et est ca­pable de four­nir aussi bien des pri­mi­tives de haut ni­veau que des pri­mi­tives de bas ni­veau, proches des pri­mi­tives of­fertes en C. Il existe éga­le­ment de nom­breuses contri­bu­tions pour en­ri­chir les fonc­tion­na­li­tés exis­tantes en sup­por­tant no­tam­ment de nom­breux pro­to­coles sup­plé­mentaires.

Ces ca­pa­ci­tés, com­bi­nées à la pro­duc­ti­vité et à la li­si­bi­lité qui ca­rac­té­risent Py­thon, vous per­met­tront de conce­voir des ap­pli­ca­tions ex­ploi­tant di­rec­te­ment ou in­di­rec­te­ment le réseau.

Pour des pro­jets am­bi­tieux, n’hé­si­tez pas à je­ter un œil à Twis­ted qui, si vous vous adap­tez au pa­ra­digme asyn­chrone, vous per­met­tra de bé­né­fi­cier de sa ver­sa­ti­lité et de sa ro­bustesse.