Python : Communications réseaux

20 juillet 2014 rdorigny 0 commentaires

Difficile de faire sans les communications réseaux, en effet de nos jours tout est connecté! Bientôt une multitude d'objets connectés nous entourera, nous ne sommes qu'au début de quelque chose qui nous dépassera vite j'en ai peur...

La programmation réseau est donc une nécessitée, et bien évidemment, le langage Python propose tout la panoplie des fonctionnalités pour programmer une application connectée. Voyons un peu comment cela fonctionne et ce qu'il est possible de faire.



1)Les sockets

Je ne vais pas reprendre la théorie des sockets et je ferais abstraction du support réseau en considérant que les couches basses fonctionnent correctement.

Disons simplement, que dans le monde du réseau il y a deux types d'item : le serveur et le client. C'est deux là discutent par l'intermédiaire d'un port de communication autour d'un adressage IP, mais là je ne vous apprend rien?

Le mécanisme qui permet la programmation réseau est le socket, pour aller plus loin sur ce sujet vous trouverez plus d'information ici.

2)Création d'un serveur

Le script reprend un serveur sans prendre en compte en l'aspect multithread que nous verrons par la suite.
import socket, sys HOST = '192.168.66.13' PORT = 50000 counter =0 # compteur de connexions actives # 1) creation du socket : mySocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # 2) liaison du socket a une adresse precise : try: mySocket.bind((HOST, PORT)) except socket.error: print("La liaison du socket a l'adresse choisie a echoue.") sys.exit while 1: # 3) Attente de la requete de connexion d'un client : print("Serveur pret, en attente de requetes ...") mySocket.listen(2) # 4) Etablissement de la connexion : connexion, adresse = mySocket.accept() counter +=1 print("Client connecte, adresse IP %s, port %s" % (adresse[0], adresse[1])) # 5) Dialogue avec le client : msgServeur ="Vous etes connecte au serveur Marcel. Envoyez vos messages." connexion.send(msgServeur.encode("Utf8")) msgClient = connexion.recv(1024).decode("Utf8") while 1: print("C>", msgClient) if msgClient.upper() == "FIN" or msgClient =="": break msgServeur = input("S> ") connexion.send(msgServeur.encode("Utf8")) msgClient = connexion.recv(1024).decode("Utf8") # 6) Fermeture de la connexion : connexion.send("fin".encode("Utf8")) print("Connexion interrompue.") connexion.close() ch = input("<R>ecommencer <T>erminer ? ") if ch.upper() =='T': break

Ligne 4 : Le module socket contient toutes les fonctions et les classes nécessaires pour construire de sprogrammes communicants. Comme nous allons le voir dans les lignes suivantes, l'établissement de la communication comporte six étapes.
• Lignes 6-7 : Ces deux variables définissent l'identité du serveur, telle qu'on l'intégrera au socket. HOST doit contenir une chaîne de caractères indiquant l'adresse IP du serveur sous la forme décimale habituelle, ou encore le nom DNS de ce même serveur (mais à la condition qu'un mécanisme de résolution des noms ait été mis en place sur le réseau). PORT doit contenir un entier, à savoir le numéro d'un port qui ne soit pas déjà utilisé pour un autre usage, et de préférence une valeur supérieure à 1024.
• Lignes 10-11 : Première étape du mécanisme d'interconnexion. On instancie un objet de la classe socket(), en précisant deux options qui indiquent le type d'adresses choisi (nous utiliserons des adresses de type «Internet ») ainsi que la technologie de transmission (datagrammes ou connexion continue (stream) : nous avons décidé d'utiliser cette dernière).
• Lignes 13 à 18 : Seconde étape. On tente d'établir la liaison entre le socket et le port de communication. Si cette liaison ne peut être établie (port de communication occupé, par exemple, ou nom de machine incorrect), le programme se termine sur un message d'erreur. Remarque concernant la ligne 15 : la méthode bind() du socket attend un argument du type tuple, raison pour laquelle nous devons enfermer nos deux variables dans une double paire de parenthèses.
• Ligne 20 : Notre programme serveur étant destiné à fonctionner en permanence dans l'attente des requêtes de clients potentiels, nous le lançons dans une boucle sans fin.
• Lignes 21 à 23 : Troisième étape. Le socket étant relié à un port de communication, il peut à présent se préparer à recevoir les requêtes envoyées par les clients. C'est le rôle de la méthode listen(). L'argument qu'on lui transmet indique le nombre maximum de connexions à accepter en parallèle. Nous verrons plus loin comment gérer celles-ci.
• Lignes 25 à 28 : Quatrième étape. Lorsqu'on fait appel à sa méthode accept(), le socket attend indéfiniment qu'une requête se présente. Le script est donc interrompu à cet endroit, un peu comme il le serait si nous faisions appel à une fonction input() pour attendre une entrée clavier. Si une requête est réceptionnée, la méthode accept() renvoie un tuple de deux éléments : le premier est la référence d'un nouvel objet de la classe socket() (104) , qui sera la véritable interface de communication entre le client et le serveur, et le second un autre tuple contenant les coordonnées de ce client (son adresse IP et le no de port qu'il utilise lui même).
• Lignes 30 à 33 : Cinquième étape. La communication proprement dite est établie. Les méthodes send() et recv() du socket servent évidemment à l'émission et à la réception des messages, qui doivent impérativement être des chaînes d'octets. À l'émission, il faut donc prévoir explicitement la conversion des chaînes de caractères en données de type bytes, et faire l'inverse à la réception. Remarques : la méthode send() renvoie le nombre d'octets expédiés. L'appel de la méthode recv() doit comporter un argument entier indiquant le nombre maximum d'octets à réceptionner en une fois. Les octets surnuméraires sont mis en attente dans un tampon, ils sont transmis lorsque la même méthode recv() est appelée à nouveau.
• Lignes 34 à 40 : Cette nouvelle boucle sans fin maintient le dialogue jusqu'à ce que le client décide d'envoyer le mot « fin » ou une simple chaîne vide. Les écrans des deux machines afficheront chacune l'évolution de ce dialogue.
• Lignes 42 à 45 : Sixième étape. Fermeture de la connexion.

3)Création d'un client

# Définition d'un client réseau rudimentaire # Ce client dialogue avec un serveur ad hoc import socket, sys HOST = '192.168.66.13' PORT = 50000 # 1) création du socket : mySocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # 2) envoi d'une requête de connexion au serveur : try: mySocket.connect((HOST, PORT)) except socket.error: print("La connexion a échoue.") sys.exit() print("Connexion etablie avec le serveur.") # 3) Dialogue avec le serveur : msgServeur = mySocket.recv(1024).decode("Utf8") while 1: if msgServeur.upper() == "FIN" or msgServeur =="": break print("S>", msgServeur) msgClient = input("C> ") mySocket.send(msgClient.encode("Utf8")) msgServeur = mySocket.recv(1024).decode("Utf8") # 4) Fermeture de la connexion : print("Connexion interrompue.") mySocket.close()

• Le début du script est similaire à celui du serveur. L'adresse IP et le port de communication doivent être ceuxdu serveur.
• Lignes 12 à 18 : On ne crée cette fois qu'un seul objet socket, dont on utilise la méthode connect() pour envoyer la requête de connexion.
• Lignes 20 à 33 : Une fois la connexion établie, on peut dialoguer avec le serveur en utilisant les méthodes send() et recv() déjà décrites plus haut pour celui-ci.

4)Utilisation des threads

Les threads sont des processus légers qui permettent le travail multi-tâche, notamment dans notre cas de traiter plusieurs connexions simultanées.

4.1)Client réseau à base de threads

# Definition d'un client reseau gerant en parallele l'emission # et la reception des messages (utilisation de 2 THREADS). host = '192.168.66.10' port = 46000 import socket, sys, threading class ThreadReception(threading.Thread): """objet thread gerant la reception des messages""" def __init__(self, conn): threading.Thread.__init__(self) self.connexion = conn # ref. du socket de connexion def run(self): while 1: message_recu = self.connexion.recv(1024).decode("Utf8") print("*" + message_recu + "*") if not message_recu or message_recu.upper() =="FIN": break # Le thread <reception> se termine ici. # On force la fermeture du thread <emission> : th_E._stop() print("Client arrete. Connexion interrompue.") self.connexion.close() class ThreadEmission(threading.Thread): """objet thread gerant l'emission des messages""" def __init__(self, conn): threading.Thread.__init__(self) self.connexion = conn # ref. du socket de connexion def run(self): while 1: message_emis = input() self.connexion.send(message_emis.encode("Utf8")) # Programme principal - Etablissement de la connexion : connexion = socket.socket(socket.AF_INET, socket.SOCK_STREAM) try: connexion.connect((host, port)) except socket.error: print("La connexion a echoue.") sys.exit() print("Connexion etablie avec le serveur.") # Dialogue avec le serveur : on lance deux threads pour gerer # independamment l'emission et la reception des messages : th_E = ThreadEmission(connexion) th_R = ThreadReception(connexion) th_E.start() th_R.start()

• Remarque générale : Dans cet exemple, nous avons décidé de créer deux objets threads indépendants du thread principal, afin de bien mettre en évidence les mécanismes. Notre programme utilise donc trois threads en tout, alors que le lecteur attentif aura remarqué que deux pourraient suffire. En effet : le thread principal ne sert en définitive qu'à lancer les deux autres ! Il n'y a cependant aucun intérêt à limiter le nombre de threads. Au contraire : à partir du moment où l'on décide d'utiliser cette technique, il faut en profiter pour compartimenter l'application en unités bien distinctes.
• Ligne 7 : Le module threading contient la définition de toute une série de classes intéressantes pour gérer les threads. Nous n'utiliserons ici que la seule classe Thread(), mais une autre sera exploitée plus loin (la classe Lock()), lorsque nous devrons nous préoccuper de problèmes de synchronisation entre différents threads concurrents.
• Lignes 9 à 25 : Les classes dérivées de la classe Thread() contiendront essentiellement une méthode run(). C'est dans celle-ci que l'on placera la portion de programme spécifiquement confiée au thread. Il s'agira souvent d'une boucle répétitive, comme ici. Vous pouvez parfaitement considérer le contenu de cette méthode comme un script indépendant, qui s'exécute en parallèle avec les autres composants de votre application. Lorsque ce code a été complètement exécuté, le thread se referme.
• Lignes 16 à 20 : Cette boucle gère la réception des messages. À chaque itération, le flux d'instructions s'interrompt à la ligne 17 dans l'attente d'un nouveau message, mais le reste du programme n'est pas figé pour autant : les autres threads continuent leur travail indépendamment.
• Ligne 19 : La sortie de boucle est provoquée par la réception d'un message 'fin' (en majuscules ou en minuscules), ou encore d'un message vide (c'est notamment le cas si la connexion est coupée par le partenaire). Quelques instructions de « nettoyage » sont alors exécutées, et puis le thread se termine.
• Ligne 23 : Lorsque la réception des messages est terminée, nous souhaitons que le reste du programme se termine lui aussi. Il nous faut donc forcer la fermeture de l'autre objet thread, celui que nous avons mis en place pour gérer l'émission des messages. Cette fermeture forcée peut être obtenue à l'aide de la méthode _stop() (107) .
• Lignes 27 à 36 : Cette classe définit donc un autre objet thread, qui contient cette fois une boucle de répétition perpétuelle. Il ne pourra donc se terminer que contraint et forcé par la méthode décrite au paragraphe précédent. À chaque itération de cette boucle, le flux d'instructions s'interrompt à la ligne 35 dans l'attente d'une entrée clavier, mais cela n'empêche en aucune manière les autres threads de faire leur travail.
• Lignes 38 à 45 : Ces lignes sont reprises à l'identique des scripts précédents.
• Lignes 47 à 52 : Instanciation et démarrage des deux objets threads enfants. Veuillez noter qu'il est recommandé de provoquer ce démarrage en invoquant la méthode intégrée start(), plutôt qu'en faisant appel directement à la méthode run() que vous aurez définie vous-même. Sachez également que vous ne pouvez invoquer start() qu'une seule fois (une fois arrêté, un objet thread ne peut pas être redémarré).

4.2)Serveur réseau à base de threads

# Definition d'un serveur reseau gerant un systeme de CHAT simplifie. # Utilise les threads pour gerer les connexions clientes en parallele. HOST = '192.168.66.10' PORT = 46000 import socket, sys, threading class ThreadClient(threading.Thread): '''derivation d'un objet thread pour gerer la connexion avec un client''' def __init__(self, conn): threading.Thread.__init__(self) self.connexion = conn def run(self): # Dialogue avec le client : nom = self.getName() # Chaque thread possede un nom while 1: msgClient = self.connexion.recv(1024).decode("Utf8") if not msgClient or msgClient.upper() =="FIN": break message = "%s> %s" % (nom, msgClient) print(message) # Faire suivre le message a tous les autres clients : for cle in conn_client: if cle != nom: # ne pas le renvoyer a l'emetteur conn_client[cle].send(message.encode("Utf8")) # Fermeture de la connexion : self.connexion.close() # couper la connexion cote serveur del conn_client[nom] # supprimer son entree dans le dictionnaire print("Client %s deconnecte." % nom) # Le thread se termine ici # Initialisation du serveur - Mise en place du socket : mySocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) try: mySocket.bind((HOST, PORT)) except socket.error: print("La liaison du socket a l'adresse choisie a echoue.") sys.exit() print("Serveur pret, en attente de requetes ...") mySocket.listen(5) # Attente et prise en charge des connexions demandees par les clients : conn_client = {} # dictionnaire des connexions clients while 1: connexion, adresse = mySocket.accept() # Creer un nouvel objet thread pour gerer la connexion : th = ThreadClient(connexion) th.start() # Memoriser la connexion dans le dictionnaire : it = th.getName() # identifiant du thread conn_client[it] = connexion print("Client %s connecte, adresse IP %s, port %s." % (it, adresse[0], adresse[1])) # Dialogue avec le client : msg ="Vous etes connecte. Envoyez vos messages." connexion.send(msg.encode("Utf8"))

Lignes 35 à 43 : L'initialisation de ce serveur est identique à celle du serveur rudimentaire décrit au début du présent chapitre.
• Ligne 46 : Les références des différentes connexions doivent être mémorisées. Nous pourrions les placer dans une liste, mais il est plus judicieux de les placer dans un dictionnaire, pour deux raisons : la première est que nous devrons pouvoir ajouter ou enlever ces références dans n'importe quel ordre, puisque les clients se connecteront et se déconnecteront à leur guise. La seconde est que nous pouvons disposer aisément d'un identifiant unique pour chaque connexion, lequel pourra servir de clé d'accès dans un dictionnaire. Cet identifiant nous sera en effet fourni automatiquement par la classe Thread().
• Lignes 47 à 51 : Le programme commence ici une boucle de répétition perpétuelle, qui va constamment attendre l'arrivée de nouvelles connexions. Pour chacune de celles-ci, un nouvel objet ThreadClient() est créé, lequel pourra s'occuper d'elle indépendamment de toutes les autres.
• Lignes 52 à 54 : Obtention d'un identifiant unique à l'aide de la méthode getName(). Nous pouvons profiter ici du fait que Python attribue automatiquement un nom unique à chaque nouveau thread : ce nom convient bien comme identifiant (ou clé) pour retrouver la connexion correspondante dans notre dictionnaire. Vous pourrez constater qu'il s'agit d'une chaîne de caractères, de la forme « Thread-N » (N étant le numéro d'ordre du thread).
• Lignes 15 à 17 : Gardez bien à l'esprit qu'il se créera autant d'objets ThreadClient() que de connexions, et que tous ces objets fonctionneront en parallèle. La méthode getName() peut alors être utilisée au sein d'un quelconque de ces objets pour retrouver son identité particulière. Nous utiliserons cette information pour distinguer la connexion courante de toutes les autres (voir ligne 26).
• Lignes 18 à 23 : L'utilité du thread est de réceptionner tous les messages provenant d'un client particulier. Il faut donc pour cela une boucle de répétition perpétuelle, qui ne s'interrompra qu'à la réception du message spécifique : « fin », ou encore à la réception d'un message vide (cas où la connexion est coupée par le partenaire).
• Lignes 24 à 27 : Chaque message reçu d'un client doit être ré-expédié à tous les autres. Nous utilisons ici une boucle for pour parcourir l'ensemble des clés du dictionnaire des connexions, lesquelles nous permettent ensuite de retrouver les connexions elles-mêmes. Un simple test (à la ligne 26) nous évite de ré-expédier le message au client d'où il provient.
• Ligne 31 : Lorsque nous fermons un socket de connexion, il est préférable de supprimer sa référence dans le dictionnaire, puisque cette référence ne peut plus servir. Et nous pouvons faire cela sans précaution particulière, car les éléments d'un dictionnaire ne sont pas ordonnés (nous pouvons en ajouter ou en enlever dans n'importe quel ordre).

Conclusion

Voilà! Vous avez les bases pour programmer des applications réseau sous python. A noter que l'on retrouve un code assez proche du C système Linux.











Pseudonyme (obligatoire) :
Adresse mail (obligatoire) :
Site web :




© 2017 www.doritique.fr par Robert DORIGNY