Nos programmes peuvent à présent effectuer des tâches complexes et peuvent s'interfacer entre eux par le biais de fichiers ou de bases de données. Voyons à présent comment faire communiquer plusieurs programmes fonctionnant sur des ordinateurs différents via le réseau informatique. L'objectif de ce chapitre est d'aborder la communication entre plusieurs ordinateurs avec le mécanisme de sockets.
Un socket, que nous pouvons traduire par connecteur réseau, est une interface aux services réseaux offerte par le système d'exploitation permettant d'exploiter facilement le réseau. Cela permet d'initier une session TCP, d'envoyer et de recevoir des données par cette session. Nous utiliserons pour ce chapitre le module socket.
Nous travaillerons avec deux scripts, le serveur permettant d'écouter les demandes des clients et d'y répondre. Le client se connectera sur le serveur pour demander le service. Il est possible d'exécuter à la fois le client et le serveur sur un même ordinateur. Pour cela, il vous suffit de renseigner 127.0.0.1 comme adresse IP pour la partie client.
Nous allons commencer par construire une application serveur très simple qui reçoit les connexions clients sur le port désigné, envoie un texte lors de la connexion, affiche ce que le client lui envoie et ferme la connexion.
#!/usr/bin/env python3 import socket serveur = socket.socket(socket.AF_INET, socket.SOCK_STREAM) serveur.bind(('', 50000)) # Écoute sur le port 50000 serveur.listen(5) while True: client, infosClient = serveur.accept() print("Client connecté. Adresse " + infosClient[0]) requete = client.recv(255) # Reçoit 255 octets. Vous pouvez changer pour recevoir plus de données print(requete.decode("utf-8")) reponse = "Bonjour, je suis le serveur" client.send(reponse.encode("utf-8")) print("Connexion fermée") client.close() serveur.close()
Vous remarquerez la présence de l'instruction encode("utf-8"). Cette indication demande à Python de convertir la chaîne de caractères en flux binaire UTF-8 pour permettre son émission sur le réseau. L'instruction decode("utf-8") permet d'effectuer l'opération inverse. Nous ne pouvons pas tester le programme tant que nous n'avons pas écrit la partie cliente.
#!/usr/bin/env python3 import socket adresseIP = "127.0.0.1" # Ici, le poste local port = 50000 # Se connecter sur le port 50000 client = socket.socket(socket.AF_INET, socket.SOCK_STREAM) client.connect((adresseIP, port)) print("Connecté au serveur") client.send("Bonjour, je suis le client".encode("utf-8")) reponse = client.recv(255) print(reponse.decode("utf-8")) print("Connexion fermée") client.close()
Cela fonctionne mais le serveur présente un défaut, il ne peux pas gérer plusieurs clients à la fois. Nous allons y remédier dans la suite de ce chapitre.
Nous abordons cette technique pour pouvoir élaborer un serveur pouvant gérer plusieurs connexions client en même temps. Tout d'abord, nous allons nous familiariser avec le module threading qui met en œuvre le multithread pour les fonctions et les objets Python. Voici un exemple simple montrant le fonctionnement de cette technique :
#!/usr/bin/env python3 import threading def compteur(nomThread): for i in range(3): print(nomThread + " : " + str(i)) threadA = threading.Thread(None, compteur, None, ("Thread A",), {}) threadB = threading.Thread(None, compteur, None, ("Thread B",), {}) threadA.start() threadB.start()
Voici un des résultats possibles lors de l'exécution du script ci-dessus. On observe que l'affichage du thread A et B sont confondus :
Thread A : 0 Thread A : 1 Thread B : 0 Thread B : 1 Thread A : 2 Thread B : 2
Nous allons détailler la fonction créant le thread : threading.Thread(groupe, cible, nom, arguments, dictionnaireArguments) dont voici les arguments :
Voici donc la combinaison du serveur de socket vu précédemment et la technique du multithreading pour obtenir un serveur plus complexe créant un nouveau thread à chaque client connecté. Nous avons apporté une petite modification au client car désormais, le client demande à l'utilisateur de saisir un message qui sera affiché sur le serveur. Ce dernier répondra à tous les messages du client jusqu'à ce que l'utilisateur saisisse le mot FIN sur le client ce qui termine la connexion. Voici le script serveur :
#!/usr/bin/env python3 import socket import threading threadsClients = [] def instanceServeur (client, infosClient): adresseIP = infosClient[0] port = str(infosClient[1]) print("Instance de serveur prêt pour " + adresseIP + ":" + port) message = "" while message.upper() != "FIN": message = client.recv(255).decode("utf-8") print("Message reçu du client " + adresseIP + ":" + port + " : " + message) client.send("Message reçu".encode("utf-8")) print("Connexion fermée avec " + adresseIP + ":" + port) client.close() serveur = socket.socket(socket.AF_INET, socket.SOCK_STREAM) serveur.bind(('', 50000)) # Écoute sur le port 50000 serveur.listen(5) while True: client, infosClient = serveur.accept() threadsClients.append(threading.Thread(None, instanceServeur, None, (client, infosClient), {})) threadsClients[-1].start() serveur.close()
Et voici le nouveau script client :
#!/usr/bin/env python3 import socket adresseIP = "127.0.0.1" # Ici, le poste local port = 50000 # Se connecter sur le port 50000 client = socket.socket(socket.AF_INET, socket.SOCK_STREAM) client.connect((adresseIP, port)) print("Connecté au serveur") print("Tapez FIN pour terminer la conversation. ") message = "" while message.upper() != "FIN": message = input("> ") client.send(message.encode("utf-8")) reponse = client.recv(255) print(reponse.decode("utf-8")) print("Connexion fermée") client.close()
À chaque nouvelle connexion d'un client, le serveur crée un thread dédié à ce client, ce qui permet au programme principal d'attendre la connexion d'un nouveau client. Chaque client peut alors avoir une conversation avec le serveur sans bloquer les autres conversations.
Voici un exemple de l'affichage sur le serveur :
Instance de serveur prêt pour 127.0.0.1:57282 Message reçu du client 127.0.0.1:57282 : Je suis client 1 Instance de serveur prêt pour 127.0.0.1:57365 Message reçu du client 127.0.0.1:57365 : Je suis client 2 Message reçu du client 127.0.0.1:57282 : On Message reçu du client 127.0.0.1:57365 : peux Message reçu du client 127.0.0.1:57282 : envoyer Message reçu du client 127.0.0.1:57365 : des Message reçu du client 127.0.0.1:57282 : messages Message reçu du client 127.0.0.1:57365 : ensemble Message reçu du client 127.0.0.1:57282 : FIN Connexion fermée avec 127.0.0.1:57282 Message reçu du client 127.0.0.1:57365 : FIN Connexion fermée avec 127.0.0.1:57365
Nous allons ici utiliser la bibliothèque http.server pour créer rapidement un serveur Web capable de servir des fichiers à un navigateur Web. Le script ci-dessous montre comment créer cela en spécifiant le numéro de port sur lequel notre serveur Web va écouter et quel dossier il va servir.
import http.server portEcoute = 80 # Port Web par défaut adresseServeur = ("", portEcoute) serveur = http.server.HTTPServer handler = http.server.CGIHTTPRequestHandler handler.cgi_directories = ["/tmp"] # On sert le dossier /tmp print("Serveur actif sur le port ", portEcoute) httpd = serveur(adresseServeur, handler) httpd.serve_forever()
Ce serveur retournera par défaut le fichier index.html du dossier servi. Si ce fichier n'existe pas, le serveur retournera la liste des fichiers du dossier.
De plus en plus de programmes utilisent et proposent aujourd'hui des interfaces de programmation nommées API. Cela permet de standardiser l'interaction entre les différentes applications et de découper les différentes fonctionnalités d'une application en divers modules qui communiquent ensemble avec ces interfaces.
Nous allons voir comment utiliser des services Web proposés pour enrichir nos applications Python en utilisant le module urllib.request. Notre premier exemple sera d'afficher la position de la Station Spatiale Internationale (ISS) qui nous est fournie par l'API http://api.open-notify.org/iss-now.json qui nous renvoie un texte au format JSON sous cette forme :
{ "iss_position": { "longitude": "-100.8325", "latitude": "-12.0631" }, "timestamp": 1493971107, "message": "success" }
Voici le code source permettant de récupérer la donnée et en récupérer les parties utiles :
import urllib.request import json requete = urllib.request.Request('http://api.open-notify.org/iss-now.json') # La requête de l'API reponse = urllib.request.urlopen(requete) # Récupérer le fichier JSON donneesBrut = reponse.read().decode("utf-8") # Décoder le texte reçu donneesJSON = json.loads(donneesBrut) # Décoder le fichier JSON position = donneesJSON["iss_position"] print("La station spatiale internationale est située à une longitude " + position["longitude"] + " et à une latitude " + position["latitude"] + ". ")
Nous allons utiliser une seconde API permettant de trouver les communes associées à un code postal. L'API http://api.zippopotam.us/FR/XXXXX où XXXXX est le code postal recherché. Voici un exemple de données retournées :
{ "post code": "21000", "country": "France", "country abbreviation": "FR", "places": [ { "place name": "Dijon", "longitude": "5.0167", "state": "Bourgogne", "state abbreviation": "A1", "latitude": "47.3167" } ] }
Voici le programme permettant d'afficher les communes associées à un code postal :
import urllib.request import json codePostal = input("Entrez le code postal : ") requete = urllib.request.Request('http://api.zippopotam.us/FR/' + codePostal) reponse = urllib.request.urlopen(requete) donneesBrut = reponse.read().decode("utf-8") donneesJSON = json.loads(donneesBrut) listeCommunes = donneesJSON["places"] print("Voici les communes ayant pour code postal " + codePostal + " : ") for commune in listeCommunes: print(" - " + commune["place name"])
Vous êtes nouvellement embauché dans une banque pour mettre au point le système de communication entre les distributeurs automatiques et le système central de gestion des comptes. Votre travail est de développer les applications sur ces deux systèmes pour permettre aux distributeurs de communiquer avec le système central pour effectuer les transactions.
On souhaite créer une base de données SQLite3 stockant les soldes des comptes, les informations les concernant, ainsi que les transactions effectuées. On utilisera un serveur de socket pour effectuer les communications entre les deux systèmes. Voici les différents messages pris en charge avec (C→S) un message du client vers le serveur et (S→C) un message du serveur vers le client :
Voici le schéma UML de la base de données de la banque :
La direction de la banque vous fournit les fichiers CSV contenant :
Toute opération doit être précédée d'une vérification du code PIN. Les exercices suivants permettront d'effectuer cela en scindant le travail en différentes sous-tâches. Chaque tâche fera l'objet d'un nouveau programme.
Écrivez un programme permettant de créer sur le serveur une base de données SQLite3 nommée banque.db et de créer la structure de table adaptée au stockage des données. Importez le contenu des fichiers CSV dans cette base.
Écrivez le programme du serveur central de la banque écoutant sur le port 50000.
Écrivez le programme des distributeurs automatiques de la banque.
#!/usr/bin/env python3 import socket adresseIP = "127.0.0.1" # Ici, le poste local port = 50000 # Se connecter sur le port 50000 while True: client = socket.socket(socket.AF_INET, socket.SOCK_STREAM) client.connect((adresseIP, port)) print("Bienvenue dans la banque Python") nocompte = input("Entrez votre numéro de compte : ") pin = input("Entrez votre code PIN : ") client.send(("TESTPIN " + nocompte + " " + pin).encode("utf-8")) reponse = client.recv(255).decode("utf-8") if reponse == "TESTPIN OK": print("Bienvenue ! ") print(" Opérations : ") print("1 - Dépôt") print("2 - Retrait") print("3 - Transfert") print("4 - Historique des opérations") print("5 - Solde du compte") operation = input("Entrez l'opération que vous souhaitez : ") if operation == "1": montant = input("Entrez le montant à déposer : ") client.send(("DEPOT " + nocompte + " " + montant).encode("utf-8")) reponse = client.recv(255).decode("utf-8") print("Dépot effectué") elif operation == "2": montant = input("Entrez le montant à retirer : ") montant = str(- float(montant)) # Le montant doit être négatif client.send(("RETRAIT " + nocompte + " " + montant).encode("utf-8")) reponse = client.recv(255).decode("utf-8") if reponse == "RETRAIT OK": print("Retrait effectué") else: print("Retrait refusé") elif operation == "3": montant = input("Entrez le montant à transferer : ") nocompteDestination = input("Entrez le numéro de compte du bénéficiaire : ") client.send(("TRANSERT " + nocompte + " " + nocompteDestination + " " + montant).encode("utf-8")) reponse = client.recv(255).decode("utf-8") if reponse == "TRANSERT OK": print("Transfert effectué") else: print("Transfert refusé") elif operation == "4": client.send(("HISTORIQUE " + nocompte).encode("utf-8")) historique = client.recv(4096).decode("utf-8").replace("HISTORIQUE ","") # On transfert un grand volume de données print(historique) elif operation == "5": client.send(("SOLDE " + nocompte).encode("utf-8")) solde = client.recv(4096).decode("utf-8").replace("SOLDE ","") print("Le solde du compte est de " + solde) else: print("Vos identifiants sont incorrects") print("Au revoir !") client.close()