Les sockets du Python
Sources : https://openclassrooms.com/fr/courses/235344-apprenez-a-programmer-en-python/234698-gerez-les-reseaux
https://realpython.com/python-sockets/
Architecture Client-Serveur
Dans l’architecture Client-Serveur, on trouve en général un serveur et plusieurs clients. Le serveur est une machine qui traite les requêtes du client et lui envoie éventuellement une réponse.
Il y a donc deux types d’application installés sur les machines :
- l’application « serveur » : écoute en attendant des connexions des clients ;
- les applications « client » : se connectent au serveur.
Les étapes d’une communication Client-Serveur
Le serveur :
- attend une connexion de la part du client ;
- accepte la connexion quand le client se connecte ;
- échange des informations avec le client ;
- ferme la connexion.
Le client :
- se connecte au serveur ;
- échange des informations avec le serveur ;
- ferme la connexion.
Établissement de la connexion
Pour que le client se connecte au serveur, il lui faut deux informations :
- L’adresse IP du serveur, ou son nom d’hôte (host name), qui identifie une machine sur Internet ou sur un réseau local.
Les noms d’hôtes permettent de représenter des adresses IP de façon plus claire
par exemple ‘google.fr’ est plus facile à retenir que l’adresse IP correspondante 74.125.224.84
- Le numéro de port associé à l’application qui doit gérer les requêtes sur le serveur.
Pour que le serveur réponde au client, il lui faut également deux informations :
- Le nom d’hôte (host name), du client ;
- Le numéro de port associé à l’application qui doit recevoir les réponses sur le client.
Serveur simple en Python
Le module socket
de Python permet de gérer les connexions par socket.
Import de la bibliothèque socket
:
import socket
Un socket est un objet qui permet d’ouvrir une connexion avec une machine, locale ou distante, et d’échanger avec elle.
Il faut maintenant créer les deux applications :
Application Serveur
Création d’un objet socket
:
socket_ecoute = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
Le constructeur de l’objet socket
attend deux paramètres :
- la famille d’adresses :
socket.AF_INET
= adresses Internet de type IPv4 ; - le type du socket :
socket.SOCK_STREAM
= protocole TCP.
Liaison du socket d’écoute à un port :
Le socket doit à présent être lié à un port de la machine, à l’aide de la méthode .bind()
, qui attend un tuple au format (nom_d_hote, port)
.
socket_ecoute.bind(('', port_d_ecoute))
Remarque : on met une chaîne vide (
''
) comme nom d’hôte, ce qui est équivalent à l’adresse IP'0.0.0.0'
. Cela signifie que le socket est lié à toutes les interfaces locales (pour rappel : une machine peut avoir plusieurs interfaces réseau …).
port_d_ecoute
peut être n’importe quel port du serveur, ou presque : certains ports sont réservés à des protocoles particuliers (HTTP : port 80, SSH, port 22, …). Les ports libres sont ceux supérieurs à 1024.
Mettre le socket d’écoute à l’état d’écoute (LISTEN) :
socket_ecoute.listen()
À partir de là, le serveur écoute, sur le port port_d_ecoute
, les demandes de connexion de la part des clients.
Accepter une connexion venant du client :
Lorsqu’un client se connecte, le serveur est sensé accepter la connexion, avec la méthode .accept()
:
connexion_client, adresse_client = socket_ecoute.accept()
L’exécution du programme est alors bloquée tant qu’un client ne fait pas de demande de connexion.
Dès que cela se produit, .accept()
renvoie :
connexion_client
: un nouvel objet socket = le socket de communication, celui qui permet l’échange de données avec client ;adresse_client
: l’adresse du client, au format(nom_d_hote, port)
.
C’est par ce nouveau socket que vont passer les échanges de données.
Fermeture des connexions :
Lorsque les échanges sont terminés, il faut fermer les connexions :
connexion_client.close() socket_ecoute.close()
Application Client
Coté client, il faut également créer le socket qui va donner l’accès au serveur :
connexion_serveur = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
Ensuite, il suffit de demander une connexion au serveur (dont on connait le nom d’hôte et le port d’écoute) à l’aide de la méthode .connect()
:
connexion_serveur.connect((nom_hote_serveur, port_d_ecoute_serveur))
Remarque : il ne faut pas confondre :
- la méthode
.bind()
, coté serveur, qui établit le lien, localement, entre le socket d’écoute et le port de la machine ;- la méthode
.connect()
, coté client, envoie une demande de connexion au serveur.
Fermeture de la connexion :
Lorsque les échanges sont terminés, il faut fermer la connexion avec le serveur :
connexion_serveur.close()
Envoi et réception des données
Une fois la communication entre client et serveur établie, chacun peut envoyer et recevoir des données.
En Python, les sockets possèdent deux méthodes pour faire cela :
.send()
: pour envoyer (send) des données :
.send()
attend un argument de typebytes
(chaîne d’octets) (à ne pas confondre avec une chaîne de caractères !! voir encadré plus bas)
et renvoie le nombre d’octets effectivement envoyés..recv()
: pour recevoir (receive) des données :
.recv()
attend comme argument un entier donnant la taille maximale de la chaîne d’octets à recevoir
et renvoie la chaîne d’octets (typebytes
) effectivement reçue.
Attention : .recv()
est une méthode bloquante : l’exécution de programme s’arrête jusqu’à ce que des données soient reçues.
- si les données envoyées sont de taille inférieure à la taille maximale spécifiée, les données reçues feront exactement la taille des donnés envoyées ;
- si les données envoyées sont de taille supérieure à la taille maximale spécifiée, les données reçues auront la taille maximale spécifiée et il faudra recommencer un
.recv()
pour « récupérer » le reste des données envoyées;
bytes
!!
En Python, les chaînes d’octets (bytes
) sont représentées comme des chaînes de caractères (limités au codage ascii), préfixées par un b
: b'Bonjour !'
- Conversion
str
→bytes
:bytes("donnée", "utf-8")
→b'donn\xc3\xa9e'
- Conversion
bytes
→str
:b'donn\xc3\xa9e'.decode()
→"donnée"
Exemples :
Émission coté client :
connexion_serveur.send(b'Bonjour serveur !")
Réception coté serveur :
message = connexion_client.recv(1024) print(message)
Implémenter en Python un programme « Serveur » qui :
- créé un socket d’écoute et l’associe au port 63000
- le met à l’état d’écoute et attend qu’un client s’y connecte
- affiche l’adresse du client qui vient de se connecter
- attend la réception d’un message de la part du client
- le renvoie aussitôt au client en rajoutant à la fin :
"\nPrésent !"
- ferme les connexions
Implémenter en Python un programme « Client » qui :
- crée un socket
- demande une connexion au serveur d’adresse
('localhost', 63000)
‘localhost'
est le nom d’hôte qui désigne l’interface de bouclage (loopback interface), c’est à dire la machine locale. Cela correspond à l’adresse IP'127.0.0.1'
)
Le client et le serveur sont sur la même machine ici ! - envoie un message
"Serveur es-tu là ?"
- affiche la réponse du serveur
- ferme la connexion
Améliorations
Déclaration with ... as ...
:
Le fait d’être obligé de fermer une connexion après l’avoir ouverte pose certains problèmes : si le programme se termine anormalement, par exemple, la connexion peut ne pas se fermer correctement et cela pourrait poser des problèmes.
Pour bien faire, il faudrait intercepter les éventuelles exceptions (erreurs) :
connexion_serveur = socket.socket(socket.AF_INET, socket.SOCK_STREAM) try: connexion_serveur.connect((nom_hote_serveur, port_d_ecoute_serveur)) connexion_serveur.send(donnees_envoyees) donnees_recues = connexion_serveur.recv(1024) print(donnees_recues) finally: connexion_serveur.close()
Mais il existe une structure plus simple à utiliser, la déclaration with ... as ...
: :
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as connexion_serveur: connexion_serveur.connect((nom_hote_serveur, port_d_ecoute_serveur)) connexion_serveur.send(donnees_envoyees) donnees_recues = connexion_serveur.recv(1024) print(donnees_recues)
Méthode .sendall()
Supposons qu’une des applications envoie le message suivant :
client.send(b'Bonjour !')
Et que l’autre application essaye de recevoir ce message à l’aide de l’instruction suivante :
m = serveur.recv(1024)
Il est possible que pour une raison ou une autre le message reçu soit incomplet (par exemple : b'Bon'
)
Coté réception, il n’est pas possible de savoir si un message est complet ou non.
D’autre part, pour le même message envoyé, si l’application qui reçoit limite la taille maximale des données reçues :
m = serveur.recv(5)
Alors le message reçu sera naturellement tronqué : b'Bonjo'
. L’application qui reçoit doit s’assurer qu’il ne reste aucune données à recevoir.
Les applications sont chargées de vérifier que toutes les données ont été envoyées ; si une partie seulement des données a été transmise, l’application doit tenter de renvoyer les données restantes.
Par conséquent il faut gérer les échanges de cette manière (par exemple) :
Réception | Envoi |
donnees = b'' while True: d = conn.recv(buff_size) if not d or len(d) < buff_size: break donnees += d |
n = len(donnees) while n > 0: n = conn.send(donnees) donnees = donnees[n:] |
Mais il existe une méthode d’envoi un peu plus simple : la méthode .sendall()
envoie l’intégralité des données. Elle appelle .send()
autant de fois que nécessaire pour cela.
Coté « réception », il n’y a toujours pas d’information assurant que toutes les données sont arrivées. Pour transférer de grandes quantités de données (de taille supérieure au buffer), il faut utiliser un protocole qui annonce à l’avance la taille des données à transférer.
Par groupe de 2 personnes, implémenter en Python deux programmes (« Client » et « Serveur ») qui communiquent, entre deux machines différentes, selon le protocole suivant :
Une fois la connexion établie, entre le client et le serveur, les deux utilisateurs peuvent échanger des messages :
- coté client, l’utilisateur est invité à saisir un message, puis ce message est envoyé au serveur
- coté serveur, l’utilisateur reçoit le message du client, puis est invité à son tour à répondre
- la discussion continue ainsi jusqu’à ce qu’un des participants envoie le message
"fini"
- la connexion coté client est alors fermée et le serveur est remis dans l’état d’attente
Architecture Pair à Pair
Dans une architecture pair à pair (peer to peer ou P2P), les deux machines n’ont pas de rôle différent : elles sont à la fois, ou alternativement, client et serveur.
Dans le programme précédent, les boucles d’échange des données sont très semblables sur les deux machines :
- réception d’une requête
- traitement
- envoie d’une réponse
De plus, si l’on ne prévoit pas que d’autres clients se connectent sur le serveur, le socket d’écoute n’est plus nécessaire.
On peut donc envisager un unique algorithme de connexion lancé sur deux machines :
- Le programme A, qui démarre le premier, tente de se connecter à l’autre : la connexion échoue car l’autre programme n’a pas démarré ou bien n’a pas de socket d’écoute
- Le programme A ouvre un socket d’écoute …
- Le programme B, qui démarre ensuite, tente à son tour de se connecter au programme A : la connexion réussit car le programme A est à l’écoute ;
- Le programme A obtient un socket pour communiquer avec le programme B.
- Les échanges peuvent commencer …
connexion(host)
qui effectue la connexion avec une machine de nom d’hôte (ou adresse IP) host
, et qui renvoie le socket de connexion avec cette machine.Le programme qui démarre le premier ne pourra pas se connecter au second : la méthode
.connect()
va donc provoquer une erreur identifiée par l’exceptionsocket.timeout
.Pour éviter que le programme ne s’arrête il faut intercepter cette exception à l’aide d’une structure
try: ... except: ...
:try: tentative de connexion except socket.timeout: # interception de l'exception socket.timout création d'un socket d'écoute ...
vraiment merci votre cours a été d’une très grande importance pour moi
encore en merci