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.

Créer un serveur socket

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.

Créer un client socket

L'application cliente présentée ici permet de se connecter à un serveur spécifié sur un port désigné. Une fois la connexion établie, il enverra un message au serveur et affichera le message que le serveur lui enverra en retour.
#!/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.

L'exécution de fonctions en parallèle : le multithread

Le multithread permet l'exécution de plusieurs opérations simultanément sur les mêmes ressources matérielles (ici, l'ordinateur). Les différents threads sont traités à tour de rôle par l'ordinateur pendant un temps très court ce qui donne cette impression d'exécution parallèle.

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 :

groupe
Doit être à None, réservé à un usage futur.
cible
Le nom de la fonction à exécuter.
nom
Le nom du thread (facultatif, peut être défini à None).
arguments
Un tuple donnant les arguments de la fonction cible.
dictionnaireArguments
Un dictionnaire donnant les arguments de la fonction cible. Avec l'exemple ci-dessus, on utiliserait {nomThread="Thread A"}.

Créer un serveur socket acceptant plusieurs clients

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

Créer un serveur Web

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.

Utiliser des services Web

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/XXXXXXXXXX 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"])

Exercices

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 :

banque

La direction de la banque vous fournit les fichiers CSV contenant :

Liste des clients clients.csv
Télécharger le fichier
Liste des comptes comptes.csv
Télécharger le fichier
Liste des opérations operations.csv
Télécharger le fichier

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.

  1. É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.

    Démarrer l'activité avec Python Studio

  2. Écrivez le programme du serveur central de la banque écoutant sur le port 50000.

    #!/usr/bin/env python3
    import socket
    import sqlite3
    import threading 
    threadsClients = []
    
    def connexionBaseDeDonnees():
    	baseDeDonnees = sqlite3.connect("banque.db")
    	curseur = baseDeDonnees.cursor()
    	return baseDeDonnees, curseur
    
    def testpin(nocompte, pinuser):
    	baseDeDonnees, curseur = connexionBaseDeDonnees()
    	curseur.execute("SELECT codePIN FROM Comptes WHERE idCompte = ?",(nocompte,))
    	pincompte = curseur.fetchone()[0]
    	baseDeDonnees.close()
    	if pincompte == pinuser:
    		return True
    	else:
    		return False
    
    def solde(nocompte):
    	baseDeDonnees, curseur = connexionBaseDeDonnees()
    	curseur.execute("SELECT solde FROM Comptes WHERE idCompte = ?",(nocompte,))
    	soldeCompte = curseur.fetchone()[0]
    	baseDeDonnees.close()
    	return soldeCompte
    
    def retrait(nocompte, montant):
    	#Le montant est négatif
    	baseDeDonnees, curseur = connexionBaseDeDonnees()
    	montant = float(montant)
    	soldeCompte = solde(nocompte)
    	if soldeCompte < montant or montant >= 0:
    		baseDeDonnees.close()
    		return False
    	else:
    		nouveauSolde = soldeCompte+montant
    		curseur.execute("UPDATE Comptes SET solde = ? WHERE idCompte = ?",(nouveauSolde,nocompte))
    		curseur.execute("INSERT INTO Operations (dateOperation, idCompte, libelleOperation, montant) VALUES (DATE('NOW'), ?, ?, ?)",(nocompte, "Retrait", montant))
    		baseDeDonnees.commit()
    		baseDeDonnees.close()
    		return True
    
    def transfert(nocompteSource, nocompteDestination, montant):
    	#Le montant est positif
    	baseDeDonnees, curseur = connexionBaseDeDonnees()
    	montant = float(montant)
    	soldeCompteSource = solde(nocompteSource)
    	if soldeCompteSource < montant or montant <= 0:
    		baseDeDonnees.close()
    		return False
    	else:
    		nouveauSoldeSource = soldeCompteSource-montant
    		curseur.execute("UPDATE Comptes SET solde = ? WHERE idCompte = ?",(nouveauSoldeSource,nocompteSource))
    		curseur.execute("INSERT INTO Operations (dateOperation, idCompte, libelleOperation, montant) VALUES (DATE('NOW'), ?, ?, ?)",(nocompteSource, "Virement", -montant))
    		soldeCompteDestination = solde(nocompteDestination)
    		nouveauSoldeDestination = soldeCompteDestination+montant
    		curseur.execute("UPDATE Comptes SET solde = ? WHERE idCompte = ?",(nouveauSoldeDestination,nocompteDestination))
    		curseur.execute("INSERT INTO Operations (dateOperation, idCompte, libelleOperation, montant) VALUES (DATE('NOW'), ?, ?, ?)",(nocompteDestination, "Virement", montant))
    		baseDeDonnees.commit()
    		baseDeDonnees.close()
    		return True
    
    def depot(nocompte, montant):
    	#Le montant est positif
    	baseDeDonnees, curseur = connexionBaseDeDonnees()
    	montant = float(montant)
    	soldeCompte = solde(nocompte)
    	nouveauSolde = soldeCompte+montant
    	curseur.execute("UPDATE Comptes SET solde = ? WHERE idCompte = ?",(nouveauSolde,nocompte))
    	curseur.execute("INSERT INTO Operations (dateOperation, idCompte, libelleOperation, montant) VALUES (DATE('NOW'), ?, ?, ?)",(nocompte, "Dépôt", montant))
    	baseDeDonnees.commit()
    	baseDeDonnees.close()
    	return True
    
    def historique(nocompte):
    	baseDeDonnees, curseur = connexionBaseDeDonnees()
    	curseur.execute("SELECT dateOperation, libelleOperation, montant FROM Operations WHERE idCompte = ? ORDER BY dateOperation DESC LIMIT 10;",(nocompte,))
    	historiqueCSV = "\"dateOperation\";\"libelleOperation\";\"montant\"\n"
    	for ligne in curseur.fetchall():
    		historiqueCSV += "\"" + ligne[0] + "\";\"" + ligne[1] + "\";\"" + str(ligne[2]) + "\"\n"
    	return historiqueCSV
    
    def instanceServeur (client, infosClient):
    	adresseIP = infosClient[0]
    	port = str(infosClient[1])
    	print("Instance de serveur prêt pour " + adresseIP + ":" + port)
    	actif = True
    	while actif:
    		message = client.recv(255).decode("utf-8").upper().split(" ")
    		pret = False
    		if message[0] == "TESTPIN":
    			if testpin(message[1], message[2]):
    				client.send("TESTPIN OK".encode("utf-8"))
    				message = client.recv(255).decode("utf-8").upper().split(" ")
    				if message[0] == "RETRAIT":
    					if retrait(message[1], message[2]):
    						client.send("RETRAIT OK".encode("utf-8"))
    					else:
    						client.send("RETRAIT NOK".encode("utf-8"))
    				elif message[0] == "DEPOT":
    					depot(message[1], message[2])
    					client.send("DEPOT OK".encode("utf-8"))
    				elif message[0] == "SOLDE":
    					soldeCompte = solde(message[1])
    					client.send(("SOLDE " + str(soldeCompte)).encode("utf-8"))
    				elif message[0] == "TRANSFERT":
    					if transfert(message[1], message[2], message[3]):
    						client.send("TRANSFERT OK".encode("utf-8"))
    					else:
    						client.send("TRANSFERT NOK".encode("utf-8"))
    				elif message[0] == "HISTORIQUE":
    					historiqueCSV = historique(message[1])
    					client.send(("HISTORIQUE " + historiqueCSV).encode("utf-8"))
    				else:
    					client.send("ERROPERATION".encode("utf-8"))
    			else:
    				client.send("TESTPIN NOK".encode("utf-8"))
    		else:
    			client.send("ERROPERATION".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()
  3. É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()