Nous allons mettre ici en pratique le protocole TCP, en réalisant une communication rudimentaire au travers de deux scripts simples en Python. Il y aura d’un côté un script pour la partie serveur TCP, et un pour la partie client.
Potentiellement, ces deux scripts peuvent communiquer de n’importe quel point du monde connecté à Internet, pour peu que les règles de routage et de pare-feu le permettent.
Ici, on va utiliser deux VM Debian GUI, placées dans le même réseau local.
Nous allons composer dans un premier temps le script serveur (nommé par exemple tcp_serveur.py). Son rôle est d’abord d’attendre la connexion d’un client, puis de dialoguer ensuite avec lui. Procédons étape par étape.
Ouvrez un nouveau script dans votre éditeur préféré :
nano tcp_serveur.py
En premier lieu, on insère la ligne de shebang pour préciser le langage du script :
#!/usr/bin/env python3
On commence par importer les modules Python socket
(qui gère le protocole TCP) et sys
(qui gère l’interaction avec le système hôte) :
import socket, sys
On définit deux variables correspondant à l’adresse IP du serveur, et le numéro de port TCP d’écoute. Il faut bien entendu adapter l'adresse IP à votre environnement ; quant au numéro de port, il peut être choisi librement, s’il n’est pas déjà utilisé sur votre machine. Il est codé sur 16 bits, il est donc compris entre 1 et 65535 ; cependant, on le choisira entre 1024 et 49151 pour respecter les règles de l'IANA :
HOST = '192.168.100.48'
PORT = 20000
On peut également utiliser une chaîne vide pour l'ip (
HOST = ''
), le serveur écoutera alors sur toutes ses interfaces.
On crée un objet mySocket
depuis la classe socket
du module socket
; les paramètres indiquent qu’il s’agit d’un socket IPv4 (AF_INET
), en mode stream (SOCK_STREAM
), qui est le mode courant :
# 1. Création du socket :
mySocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
Ensuite on demande à cet objet mySocket
de se connecter (bind
) à la pile TCP/IP du système d’exploitation hôte, avec le mécanisme Python try
/except
de gestion des erreurs : en cas d’erreur justement, on affiche un message, et on arrête le programme :
# 2. Liaison du socket à l'interface (identifiée par son IP) :
try:
mySocket.bind((HOST, PORT))
except socket.error:
print("La liaison du socket à l'adresse choisie a échoué.")
sys.exit()
Le serveur est désormais prêt. Ajoutons ensuite une boucle de traitement (a priori infinie) avec comme première étape de positionner le socket en écoute. Le paramètre de listen
indique qu'une seule connexion entrante est autorisée :
# Une boucle "infinie"
while True:
# 3. Attente de la requête de connexion d'un client :
print("Serveur prêt, en attente d'une requête ...")
mySocket.listen(1)
Dès qu’un client va se connecter, mySocket accepte la connexion et nous récupérons deux variables : connexion
qui identifie cette nouvelle connexion, et adresse
qui est un tuple contenant l’adresse IP du client : adresse[0]
et le numéro de port du client : adresse[1]
:
# 4. Etablissement de la connexion :
connexion, adresse = mySocket.accept()
print("Client connecté : adresse IP ",adresse[0],"- port",adresse[1])
On envoie ensuite un message de bienvenue au client :
# 5. Dialogue avec le client :
msgServeur = "Vous êtes connecté au serveur. Envoyez vos messages !"
connexion.send(msgServeur.encode("UTF8"))
Puis on créée une boucle dans laquelle alternativement on reçoit un message du client (connexion.recv
) et on émet un message au client (connexion.send
). On sort de la boucle quand le client envoie un message vide ou le mot FIN
.
Récupération du premier message (dans la limite de 1024 octets), et conversion en code UTF8 (on convertit un flux binaire brut en une chaîne de caractères) :
# réception du segment :
msgClient = connexion.recv(1024).decode("UTF8")
On boucle ; on affiche le message reçu du client (précédé d’un petit prompt C >
:
# on marque le début de la boucle et on affiche le message reçu :
while True:
print("C >", msgClient)
Si le client a envoyé le mot fin
ou un message vide, on sort de la boucle :
# jusqu'à ce qu'on ait le texte "fin" ou un message vide :
if msgClient.upper() == "FIN" or msgClient =="":
break
On saisit un message côté serveur, avec une invite S >
, puis on l’envoie sur la connexion, en indiquant que c’est du texte encodé en UTF8 (encodage standard des chaînes de caractère avec Python 3) :
# saisie côté serveur et envoi au client :
msgServeur = input("S > ")
connexion.send(msgServeur.encode("UTF8"))
Puis de nouveau on attend un message du client, et la boucle est bouclée :
# on attend un nouveau message du client, et on boucle :
msgClient = connexion.recv(1024).decode("UTF8")
En sortie de cette seconde boucle, on ferme la connexion, non sans envoyer un dernier message au client :
# 6. Fermeture de la connexion :
connexion.send("Au revoir !".encode("UTF8"))
print("La connexion est terminée.")
connexion.close()
Enfin on demande sur la console du serveur si on continue ou pas. Si l’opérateur saisit la touche t, alors on sort de la première boucle, et le programme est terminé :
# on relance le serveur avec "r" :
ch = input("<R>ecommencer ou <T>erminer ? ")
if ch.upper() =='T':
break
On enregistre le script, on le rend executable, puis on le lance :
chmod u+x tcp_server.py
./tcp_server.py
De la même façon, nous allons composer le script client, nommé par exemple tcp_client. Son rôle est d’abord d’établir la connexion sur un serveur en écoute (comme ci-dessus), puis de dialoguer ensuite avec lui. Procédons étape par étape. D'abord on édite ce nouveau script, depuis une autre machine connectée au réseau :
nano tcp_client.py
Le début ressemble à ce qui a été fait pour le serveur. Après le traditionnel shebang, on commence par importer les modules socket
et sys
, puis on déclare les variables correspondant ici à l’adresse IP et au port du serveur à joindre ; on crée le socket, et on lui demande d’établir une connexion avec le serveur (c’est la méthode connect()
et non plus bind()
qui est utilisée) :
#!/usr/bin/env python3
import socket, sys
HOST = '192.168.100.48'
PORT = 20000
mySocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
try:
mySocket.connect((HOST, PORT))
except socket.error:
print("La connexion au serveur a échoué.")
sys.exit()
print("Connexion établie avec le serveur.")
Pour établir la connexion, on se souvient que le client lui aussi ouvre un port TCP de son côté ; n'hésitez pas à observer tout cela avec wireshark sur l'une des deux machines.
Désormais, selon un schéma identique (boucle) à celui utilisé pour le serveur, on communique avec lui. On reçoit le message de bienvenue :
msgServeur = mySocket.recv(1024).decode("utf8")
Puis on boucle ; on affiche le message reçu du client (précédé d’un petit prompt S >
, puis on saisit un message à envoyer au serveur.
NB : on voit ici que le serveur peut lui aussi mettre fin à la connexion en envoyant le mot fin
:
while True:
if msgServeur.upper() == "FIN" or msgServeur =="":
break
print("S >", msgServeur)
msgClient = input("C > ")
mySocket.send(msgClient.encode("utf8"))
msgServeur = mySocket.recv(1024).decode("utf8")
Enfin, on gère la fin de la connexion :
print("Connexion interrompue.")
mySocket.close()
Désormais, il est possible de faire communiquer le Serveur et le Client. Le serveur étant lancé de son côté, on exécute notre script client :
chmod u+x tcp_client.py
./tcp_client.py
Attention au pare-feu de la machine serveur, s'il est présent, il doit autoriser les connexions entrantes TCP sur le port prévu.
La connexion que nous avons mise en place n’est pas chiffrée. Il suffit d’utiliser Wireshark pour s’en rendre compte.
De plus elle n'autorise qu'un seul fil de discussion, ce qui dans la vraie vie poserait quelques soucis (imaginez un site web n'acceptant qu'une seule connexion...). Il faut pour cela travailler dans un mode multi-threading ; plus d'information ici : https://docs.python.org/fr/3/howto/sockets.html
Enfin, une amélioration intéressante consisterait donc à mettre en place le chiffrement du lien, à l’aide d’un protocole adapté, comme TLS.
#!/usr/bin/env python3
# Définition d'un serveur tcp/ip rudimentaire
# Ce serveur attend la connexion tcp d'un client, pour entamer un dialogue avec lui
import socket, sys
HOST = '192.168.100.48'
PORT = 50000
# 1) création du socket :
mySocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 2) liaison du socket à une adresse précise :
try:
mySocket.bind((HOST, PORT))
except socket.error:
print("La liaison du socket à l'adresse choisie a échoué.")
sys.exit()
while True:
# 3) Attente de la requête de connexion d'un client :
print("Serveur prêt, en attente d'une requête ...")
mySocket.listen(1)
# 4) Etablissement de la connexion :
connexion, adresse = mySocket.accept()
print("Client connecté : adresse IP ",adresse[0],"- port",adresse[1])
# 5) Dialogue avec le client :
msgServeur = "Vous êtes connecté au serveur. Envoyez vos messages !"
connexion.send(msgServeur.encode("UTF8"))
msgClient = connexion.recv(1024).decode("UTF8")
while True:
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("Au revoir !".encode("UTF8"))
print("La connexion est terminée.")
connexion.close()
ch = input("<R>ecommencer ou <T>erminer ? ")
if ch.upper() =='T':
break
#!/usr/bin/env python3
# Définition d'un client tcp/ip rudimentaire
# Ce client se connecte et dialogue avec un serveur ad hoc
import socket, sys
HOST = '192.168.1.191'
PORT = 50500
# 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 échoué.")
sys.exit()
print("Connexion établie avec le serveur.")
# 3) Dialogue avec le serveur :
msgServeur = mySocket.recv(1024).decode("utf8")
while True:
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()