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 :

  1. attend une connexion de la part du client ;
  2. accepte la connexion quand le client se connecte ;
  3. échange des informations avec le client ;
  4. ferme la connexion.

Le client :

  1. se connecte au serveur ;
  2. échange des informations avec le serveur ;
  3. 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 type bytes (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 (type bytes) 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;

 

ATTENTION
Les données échangées sont de type 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 bb'Bonjour !'

  • Conversion str → bytesbytes("donnée", "utf-8") → b'donn\xc3\xa9e'
  • Conversion bytes → strb'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.

ATTENTION

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.

ATTENTION

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 …

 

Écrire une fonction 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’exception socket.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 
    ...
Utiliser cette fonction pour réaliser un programme de chat.

 

Applications

Programmes en réseau

Vous aimerez aussi...

1 réponse

  1. Euler niemet dit :

    vraiment merci votre cours a été d’une très grande importance pour moi
    encore en merci

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *