Implémentation de partage de fichiers à distance avec XML-RPC en Python

Voici un exercice intéressant pour développer une application P2P en Python, qui pourrait servir de base à un outil de téléchargement similaire à celui proposé par certains clients populaires. XML-RPC est un protocole de calcul distribué pour l'appel de procédure à distance (RPC), qui encapsule les fonctions appelées en XML et utilise le protocole HTTP comme mécanisme de transport [source: Wikipédia].

  1. Première approche simple : Ouvrez votre terminal et créez un fichier nommé serveurPython.py avec le contenu suivant :
from serveurXMLRPCSimple import SimpleXMLRPCServer
serveur = SimpleXMLRPCServer(("", 8080))  # Écoute sur toutes les interfaces locales
def double(valeur):
    return valeur * 2

serveur.register_function(double) # Enregistrement de la fonction sur le serveur
serveur.serve_forever() # Démarrage du serveur

  1. Dans un autre terminal, lancez l'interpréteur Python :
from clientXMLRPC import ServerProxy 
proxy = ServerProxy('http://localhost:8080')
proxy.double(5) # Appel de la fonction distante

  1. Implémentation complète pour le partage de fichiers : 3.1 Code du serveur (Serveur.py)
#coding=utf-8

from xmlrpclib import ServerProxy,Fault
from os.path import join, abspath,isfile
from SimpleXMLRPCServer import SimpleXMLRPCServer
from urlparse import urlparse
import sys

SimpleXMLRPCServer.allow_reuse_address = True

TAILLE_HISTOIRE_MAX = 6

ERREUR_NON_TRAITEE = 100
ACCES_REFUSE = 200

class ErreurRequeteNonTraitee(Fault):
    '''
    Exception pour les requêtes non gérées
    '''
    def __init__(self,message="Impossible de traiter la requête"):
        Fault.__init__(self, ERREUR_NON_TRAITEE, message)

class ErreurAccesRefuse(Fault):
    '''
    Exception lorsque l'utilisateur tente d'accéder à une ressource interdite
    '''
    def __init__(self, message="Accès refusé"):
        Fault.__init__(self, ACCES_REFUSE, message)

def dansRepertoire(repertoire,nom):
    '''
    Vérifie si le fichier spécifié se trouve dans le répertoire défini
    '''
    repertoire = abspath(repertoire)
    nom = abspath(nom)
    return nom.startswith(join(repertoire,''))
def getPort(url):
    '''
    Extrait le numéro de port depuis l'URL
    '''
    nom = urlparse(url)[1]
    parties = nom.split(':')
    return int(parties[-1])

class Noeud:

    def __init__(self, url, repertoire, secret):
        self.url = url
        self.repertoire = repertoire
        self.secret = secret
        self.connus = set()

    def traiterRequete(self, requete, historique = []):
        try:
            return self._gererRequete(requete)
        except ErreurRequeteNonTraitee:
            historique = historique + [self.url]
            if len(historique) > TAILLE_HISTOIRE_MAX: raise
            return self._diffuser(requete,historique)

    def salut(self,autre):
        self.connus.add(autre)
        return 0
    def recuperer(self, requete, secret):

        if secret != self.secret: raise
        resultat = self.traiterRequete(requete)
        f = open(join(self.repertoire, requete),'w')
        f.write(resultat)
        f.close()
        return 0

    def _lancer(self):
        s = SimpleXMLRPCServer(("",getPort(self.url)),logRequests=False)
        s.register_instance(self)
        s.serve_forever()

    def _gererRequete(self, requete):
        rep = self.repertoire
        nom = join(rep, requete)
        if not isfile(nom):raise ErreurRequeteNonTraitee
        if not dansRepertoire(rep,nom):raise ErreurAccesRefuse
        return open(nom).read()

    def _diffuser(self, requete, historique):

        for autre in self.connus.copy():
            if autre in historique: continue
            try:
                s = ServerProxy(autre)
                return s.traiterRequete(requete, historique)
            except Fault, f:
                if f.faultCode == ERREUR_NON_TRAITEE:pass
                else: self.connus.remove(autre)
            except:
                self.connus.remove(autre)

        raise ErreurRequeteNonTraitee

def principale():
    url, repertoire, secret = sys.argv[1:]
    n = Noeud(url,repertoire,secret)
    n._lancer()

if __name__ == '__main__': 
    principale()

Analysons d'abord les constantes définies : SimpleXMLRPCServer.allow_reuse_address indique que le port utilisé peut être réutilisé, c'est-à-dire que si vous fermez brusquement le serveur et le redémarrez, vous ne rencontrerez pas d'erreur d'occupation de port.

TAILLE_HISTOIRE_MAX = 6 limite le nombre de noeuds parcourus lors d'une recherche, pour éviter une recherche infinie.

ERREUR_NON_TRAITEE = 100 et ACCES_REFUSE = 200 sont des codes de retour.

Ensuite, examinons le fonctionnement d'un noeud. Le flux de ce code est le suivant : d'abord, démarrer le serveur pour les appels distants, avec l'interface Noeud. Dans la classe Noeud, trois méthodes sont exposées pour les appels distants : salut, recuperer et traiterRequete. La méthode salut ajoute les informations d'un noeud voisin au noeud actuel. La méthode recuperer est utilisée pour obtenir des données, tandis que traiterRequete est utilisée pour l'interaction entre les noeuds.

Dans la méthode recuperer, on vérifie d'abord si le mot de passe est correct, puis on appelle sa propre méthode traiterRequete pour trouver les données. Voyons la méthode traiterRequete : elle appelle d'abord la méthode privée _gererRequete pour une recherche locale, et si elle ne trouve rien, elle diffuse via _diffuser vers tous les noeuds connus. Notez le paramètre historique : chaque diffusion transmet cet historique, qui sert à deux fins : éviter d'envoyer des doublons aux mêmes noeuds, et limiter la longueur de la chaîne de connexions.

Après avoir compris les fonctionnalités de base d'un serveur de noeuds, examinons le code de la classe de contrôle pour la gestion du serveur.

3.2 Code du client (Client.py)

#coding=utf-8

from xmlrpclib import ServerProxy, Fault
from cmd import Cmd
from random import choice
from string import lowercase
from serveur import Noeud,ERREUR_NON_TRAITEE  # Import du serveur précédent
from threading import Thread
from time import sleep

import sys

TEMPS_DEPART = 0.1
LONGUEUR_SECRET = 100

def chaineAleatoire(longueur):
    caracteres = []
    lettres = lowercase[:26]
    while longueur > 0:
        longueur -= 1
        caracteres.append(choice(lettres))
    return ''.join(caracteres)

class Client(Cmd):
    prompt = '> '

    def __init__(self, url, repertoire, fichierUrl):

        Cmd.__init__(self)
        self.secret = chaineAleatoire(LONGUEUR_SECRET)
        n = Noeud(url, repertoire, self.secret)
        t = Thread(target = n._lancer)
        t.setDaemon(True)
        t.start()

        sleep(TEMPS_DEPART)
        self.serveur = ServerProxy(url)
        for ligne in open(fichierUrl):
            ligne = ligne.strip()
            self.serveur.salut(ligne)

    def do_recuperer(self, arg):
        try:
            self.serveur.recuperer(arg,self.secret)
        except Fault,f:
            if f.faultCode != ERREUR_NON_TRAITEE: raise
            print "Impossible de trouver le fichier",arg

    def do_quitter(self,arg):
        print
        sys.exit()

    do_FIN = do_quitter

def principale():
    fichierUrl, repertoire, url = sys.argv[1:]
    client = Client(url, repertoire, fichierUrl)
    client.cmdloop()

if __name__ == '__main__':
    principale()

Analysons ce code. Les paramètres initiaux sont faciles à comprendre. Il y d'abord une fonction pour générer une chaîne aléatoire, quelle est son utilisation ? Principalement pour empêcher les appels non autorisés au serveur de noeuds contrôlé par cette classe. Ce secret n'a pas besoin d'être mémorisé car nous avons un accès légal au client. :-)

Ce code fournit globalement une interface de ligne de commande visible. En héritant de la classe cmd, il analyse les commandes saisies. Par exemple, lorsque le programme s'exécute et affiche l'invite de commande, si vous tapez recuperer, il appelle la méthode do_recuperer et passe les paramètres. La méthode do_recuperer appelle la méthode recuperer du serveur de noeuds pour obtenir la ressource. La méthode do_quitter est simple : elle accepte la commande quitter pour quitter le programme.

Lors de l'initialisation du programme, il y a un point important à noter : il lit les données du fichier passé en paramètre urlfile, qui contient les adresses URL des noeuds. Après lecture, le programme ajoute ces adresses aux noeuds voisins pour un accès ultérieur. Cependant, ce programme présente encore des imperfections : si vous modifiez le fichier de configuration d'URL pendant l'exécution, il ne lira pas les nouvelles adresses de noeud ajoutées. Cette modification est simple : il suffit de placer le code d'obtention de l'URL dans la méthode do_recuperer.

Avant d'exécuter le programme, quelques préparations sont nécessaires. Créez d'abord deux dossiers, A et C. Dans le dossier C, créez un fichier B.txt. Dans les dossiers A et C, créez les fichiers urlsA.txt et urlsC.txt. Dans urlsA.txt, écrivez : http://localhost:4243. Ouvrez ensuite deux terminaux.

Dans le premier terminal, tapez :

python client.py urlsA.txt A http://localhost:4242 

Appuyez sur Entrée, l'invite de commande devrait apparaître. Tapez recuperer B.txt et appuyez sur Entrée. Vous devriez voir le message "Impossible de trouver le fichier B.txt".

Dans le deuxième terminal, tapez :

python client.py urlsC.txt C http://localhost:4243

Appuyez sur Entrée. Tapez à nouveau recuperer B.txt et appuyez sur Entrée. Rien ne devrait se passer, ce qui indique que le fichier existe. Revenez au premier terminal et tapez à nouveau recuperer B.txt. Si vous avez suivi mes suggestions pour modifier le code, vous ne devriez pas voir d'erreur. Sans modification, vous devrez quitter le programme avec la commande quitter, le redémarrer, puis taper recuperer B.txt. Ensuite, vérifiez dans le dossier A si le fichier B.txt a été téléchargé.

PS : le programme ci-dessus ne peut transférer que des fichiers texte. Les fichiers volumineux ou d'autres formats ne peuvent pas être transférés. Après recherche, il est possible d'utiliser la fonction Binary de la bibliothèque xmrlpclib. La méthode d'utilisation est la suivante : d'abord, importer xmlrpclib avec import xmlrpclib. Dans la méthode _gererRequete de la classe serveur, modifier la dernière ligne return open(nom).read() en return xmlrpclib.Binary(open(nom,'rb').read()). Ensuite, modifier f.write(resultat) en f.write(resultat.data) dans la méthode recuperer. De plus, le mode d'écriture du fichier doit être changé en wb.

Étiquettes: XML-RPC Python partage de fichiers distribué P2P

Publié le 3 juin à 01h23