Python : Réseau
La plupart des informations sont tirées de : https://openclassrooms.com/fr/courses/235344-apprenez-a-programmer-en-python/234698-le-reseau
Doc python : https://docs.python.org/3/library/socket.html
Un client/serveur très simples pour comprendre le fonctionnement
Serveur:
# -*-coding:utf-8 -*
#Création d'un serveur
import socket #Le module socket contient les objets sockets
connexion_principale = socket.socket(socket.AF_INET, socket.SOCK_STREAM) #L'objet socket, AF_INET correspond à la famille d'adresses (ici, IP), et SOCK_STREAM correspond à TCP
#On va connecter le socket, avec la méthode bind. Elle prend en paramètre un tuple (nom_hote, port).
#Binder le socket, cela veut dire que l'on va déclarer que l'on utilise ce port (On va prendre un port dynamique).
connexion_principale.bind((, 12800))
#On va faire écouter notre socket : il n'est pas encore en LISTEN. On va donc utiliser la méthode listen.
#On lui passe en paramètre le nombre de connexions qu'il peut recevoir et refuser (en général 5).
connexion_principale.listen(5)
#Accepter une connexion venant du client
#C'est la dernière étape : on va utiliser la méthode accept, qui bloque le processus tant qu'aucun client n'est venu se connecter.
#La méthode accept renvoie deux choses : le socket connecté qui vient de se créer, ainsi que le port de connexion du client.
connexion_avec_client, infos_connexion = connexion_principale.accept()
#Cette méthode bloque le programme, on va passer au client.
#########
#Une fois que le client s'est connecté, on peut récupérer nos informations de connexion
print(infos_connexion)
##Faire communiquer nos sockets
#Pour faire communiquer nos sockets, on va utiliser la méthode send pour envoyer et recv pour recevoir.
#send renvoie le nombre de caractères envoyés.
connexion_avec_client.send(b"J'ai accepte la connexion")
##On utilise recv côté client.
###Fermer la connexion
connexion_avec_client.close()
Le client :
import socket
# -*-coding:utf-8 -*
###On va d'abord créer le client de la même façon que pour le serveur
connexion_avec_serveur = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
##On va connecter le client avec la méthode connect. Elle prend comme bind un tuple en paramètre, contenant le nom d'hôte et le
#numéro de port du serveur.
#Vu que les deux applications sont sur la même machine le nom d'hôte est localhost
connexion_avec_serveur.connect(('localhost', 12800))
#Notre serveur et notre client sont connectés.
#On peut retourner sur notre serveur
###On va recevoir ce qu'envoie le serveur. recv prend en paramètre le nombre de caractères à lire, en général 1024. On pourra
#récupérer un message en plusieurs fois.
msg_recu = connexion_avec_serveur.recv(1024)
print(msg_recu)
#Le message recu est affiché.
#Fermer la connexion
connexion_avec_serveur.close()
Un exemple de serveur que l'on va améliorer
À partir de ces connaissances, on va partir de l'exemple de serveur amélioré montré dans le cours d'openclassrooms pour ensuite l'améliorer.
Le cours nous donne un exemple de client/serveur suivant :
- Serveur tiré du cours
import socket
hote =
port = 12800
connexion_principale = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
connexion_principale.bind((hote, port))
connexion_principale.listen(5)
print("Le serveur écoute à présent sur le port {}".format(port))
connexion_avec_client, infos_connexion = connexion_principale.accept()
msg_recu = b""
while msg_recu != b"fin":
msg_recu = connexion_avec_client.recv(1024)
# L'instruction ci-dessous peut lever une exception si le message
# Réceptionné comporte des accents
print(msg_recu.decode())
connexion_avec_client.send(b"5 / 5")
print("Fermeture de la connexion")
connexion_avec_client.close()
connexion_principale.close()
import socket
hote = "localhost"
port = 12800
connexion_avec_serveur = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
connexion_avec_serveur.connect((hote, port))
print("Connexion établie avec le serveur sur le port {}".format(port))
msg_a_envoyer = b""
while msg_a_envoyer != b"fin":
msg_a_envoyer = input("> ")
# Peut planter si vous tapez des caractères spéciaux
msg_a_envoyer = msg_a_envoyer.encode()
# On envoie le message
connexion_avec_serveur.send(msg_a_envoyer)
msg_recu = connexion_avec_serveur.recv(1024)
print(msg_recu.decode()) # Là encore, peut planter s'il y a des accents
print("Fermeture de la connexion")
connexion_avec_serveur.close()
À noter : les méthode encode et décode:
- encode est une méthode de string : elle prend en paramètre un nom d'encodage (par défaut utf-8) et transforme (encode) la chaîne de caractères en chaîne bytes.
- decode fait l'inverse : c'est une méthode de bytes qui décode une chaine bytes pour en faire un string. Elle prend aussi un encodage en paramètre, généralement utf-8.
Améliorer tout ça...
Ce serveur a quelques défauts : d'abord il n'accepte qu'un seul client à la fois, et en plus chaque noeud est obligé d'attendre que l'autre lui ai parlé avant de répondre, ce qui n'est pas terrible. Et les erreurs ne sont pas gérées.
On va d'abord utiliser le module select.
Le module select
Ce module va nous permettre d'interroger plusieurs clients dans l'attente d'un message à recevoir, cela sans paralyser notre programme.
Pour schématiser, select va écouter sur une liste de clients et retourner au bout d'un certain temps. select renvoie alors la liste des clients qui ont un message à réceptionner. Il suffit alors de parcourir ces clients, de lire les messages en attente avec recv, et c'est bon !
Sur Linux, select peut servir à d'autre trucs que des sockets.
La théorie
La fonction select fait partie du module du même nom. Elle prend trois/quatres arguments en paramètre et en renvoie trois. Les arguments qu'elle prend sont :
- rlist : la liste des sockets en attente de lecture
- wlist : la liste des sockets en attente d'écriture
- xlist : la liste des sockets en attente d'une erreur (on ne s'y intéressera pas ici...)
- timeout : le délai pendant lequel la fonction avant de retourner. Si on ne précise rien la fonction retourne dès que l'un des sockets change d'état (prêt à être lu si il est dans rlist, par exemple), mais pas avant.
Nous nous intéresserons ici au premier et au quatrième paramètre, surtout. Le but est de mettre des sockets dans une liste et que select les surveille, et retourne dès que l'un d'entre eux est prêt à être lu. Cela évite de bloquer le programm, on peut ainsi recevoir des messages de n'importe quel client, peu importe l'ordre.
Pour ce qaui est de timeout, on sait déjà ce qu'il se passe si on ne précise rien. SI je précise un délai (en secondes), la fonction attendra ce délai avant de retourner, sauf si un socket est prêt à être lu auquel cas elle retournera avant. Si on met le timeout à 0, elle retournera tout de suite.
Select renvoie trois listes, pas les mêmes qu'en entrée :
- rlist, liste des clients prêt à être lus
- wlist, liste des clients prêts à être écrits
- xlist, liste des clients prêts à recevoir une erreur
Un exemple d'utilisation :
rlist, wlist, xlist = select.select(clients_connectes, [], [], 2)
Cette ligne écoute les sockets contenus dans clients_connectes. Elle retournera au maximum dans 2 secondes, ou plus tôt si un client envoie un message. La liste des clients ayant envoyé un message arrive dans rlist. On peut ensuite la parcourir et appeller recv sur chacun de ses sockets.
En pratique
Toujours tiré du cours, voici le serveur amélioré :
import socket
import select
hote =
port = 12800
connexion_principale = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
connexion_principale.bind((hote, port))
connexion_principale.listen(5)
print("Le serveur écoute à présent sur le port {}".format(port))
serveur_lance = True
clients_connectes = []
while serveur_lance:
# On va vérifier que de nouveaux clients ne demandent pas à se connecter
# Pour cela, on écoute la connexion_principale en lecture
# On attend maximum 50ms
connexions_demandees, wlist, xlist = select.select([connexion_principale],
[], [], 0.05)
for connexion in connexions_demandees:
connexion_avec_client, infos_connexion = connexion.accept()
# On ajoute le socket connecté à la liste des clients
clients_connectes.append(connexion_avec_client)
# Maintenant, on écoute la liste des clients connectés
# Les clients renvoyés par select sont ceux devant être lus (recv)
# On attend là encore 50ms maximum
# On enferme l'appel à select.select dans un bloc try
# En effet, si la liste de clients connectés est vide, une exception
# Peut être levée
clients_a_lire = []
try:
clients_a_lire, wlist, xlist = select.select(clients_connectes,
[], [], 0.05)
except select.error:
pass
else:
# On parcourt la liste des clients à lire
for client in clients_a_lire:
# Client est de type socket
msg_recu = client.recv(1024)
# Peut planter si le message contient des caractères spéciaux
msg_recu = msg_recu.decode()
print("Reçu {}".format(msg_recu))
client.send(b"5 / 5")
if msg_recu == "fin":
serveur_lance = False
print("Fermeture des connexions")
for client in clients_connectes:
client.close()
connexion_principale.close()
Le serveur accepte plusieurs clients et ne se bloque pas plus de 50 millisecondes.
À partir de cela, je suis sensée pouvoir faire un mini-tchat.