Skip to content

Programmation réseau - Les sockets

Ce chapitre agrémenté d'exercices pratiques, à l’interface entre le module de réseau et le module de systèmes d’exploitation, présente l’utilisation des ressources réseaux par un processus, en utilisant les sockets.

Les sockets demandent une communication client-serveur. Le serveur (qui normalement propose un service) est à l'écoute, dans l'attente de client. Le client initie la connexion en s'adressant le premier au serveur.

Sur le serveur, l’acceptation de plusieurs clients simultanés passe par un fonctionnement multitâche, en créant un thread par client.

Les sockets (connecteurs en français), service proposé par le système d’exploitation, sont un composant logiciel jouant le jeu de driver de haut niveau traitant la couche réseau et la couche transport (la pile TCP/IP) pour proposer ainsi une interface logiciel simplifiée aux applications pour l’accès au réseau. Les couches liaison de données et physiques sont gérées par la carte réseau.

Les sockets, un service proposé par le système d'exploitation

Les sockets, un service proposé par le système d'exploitation

Notons que les sockets, développés dans les années 80, permettent de communiquer à travers le réseau mais permettent également la communication entre 2 processus sur la même machine (IPC : Inter Process Communication).

Sur un système Unix, un socket est un fichier avec les fonctions associées pour l’ouvrir, le fermer, y lire et y écrire. Les fonctions des sockets restent un différentes de celles des fichiers (recv et send remplacent read et write notamment). En communication inter processus, chaque processus a son propre socket pour la communication.

L’interface de programmation entre les sockets et l’application (Socket API) est la bibliothèque socket fournie pour de nombreux langages (C et ses dérivés, java, python…). En python, le programme importera donc la bibliothèque socket (import socket).

La commande netstat permet d’afficher les sockets ouverts, sous Windows comme sous Linux.

Le TP peut se faire avec une communication entre 2 machines différentes dont on connaît les adresses IP, ou sur une seule machine en utilisant alors pour le client et pour le serveur l’adresse localhost (adresse auto-désignant la machine) 127.0.0.1 :

Configuration réseau pour le TP avec 2 machines

Configuration réseau pour le TP avec 2 machines

Configuration réseau pour le TP avec 1 machines

Configuration réseau pour le TP avec 1 machine

Présentation des sockets

Une adresse socket est l'association d'une adresse IP, d'un protocole de transport (généralement TCP ou UDP) et d'un port. On peut distinguer :

  • Datagram socket (en UDP usuellement), sans connexion
  • Stream socket (en TCP usuellement), avec une connexion
  • Raw socket, sans protocole de transport. Échanges de paquets IP. C’est notamment le type de socket utilisé pour les messages ICMP.

Sur le serveur, le socket est à l’écoute (mode listenning).

A chaque connexion par un client, un serveur TCP crée un thread (ou un process) spécifique, avec un socket propre. Une session est établie, le socket est en established state. Les sockets client et serveur forment alors une socket pair.

Sockets et Flux TCP pour la création et la fermeture d'une connexion

Sockets et Flux TCP pour la création et la fermeture d'une connexion

Les adresses

Les adresses de sockets sont des tuples comprenant : * L’adresse IP, sous la forme d’une chaîne de caractère, * Le port, sous la forme d’un nombre entier.

exemple : adresse_socket_serveur = ("127.0.0.1", 8080) Si la machine est associée à une URL dans les serveurs DNS, il est aussi possible d’indiquer l’adresse sous la forme d’une URL « machine18.eea.ens-paris-saclay.fr »

La bibliothèque socket

On présente ici quelques fonctions de la bibliothèque socket utilisée et détaillée par la suite. La description exhaustive des paramètres des différentes fonctions est disponible sur https://docs.python.org/3/library/socket.html .

  • la fonction socket() renvoie un objet socket, elle est donc utilisée pour créer un tel objet. Elle reçoit deux paramètres :
    • La famille de socket. Pour des sockets classiques sur IP, on utilise AF_INET ou AF_INET6, plus adapté à IPv6. Des familles bluetooth (AF_BLUETOOTH) ou plus bas niveau (AF_PACKET) sont aussi disponibles entre autres.
    • Le type de socket renvoie aux-types présentés ci-dessus. SOCK_DGRAM et SOCK_STREAM seront donc les plus utilisés.
    • Exemple : socket_serveur=socket.socket(socket.AF_INET,socket.SOCK_STREAM).
  • la méthode close() sert à fermer le socket, notamment pour libérer le port.
    Ex : socket_serveur.close()

On ajoute quelques méthodes communes aux sockets réseau, côté client comme côté serveur :

  • socket.recv(bufsize[, flags]).
  • socket.recv(bufsize) : le socket renvoie les données reçues. Il est mieux pour bufsize de prendre une petite puissance de 2 (4096 par exemple).
  • socket.send(bytes) : le socket, connecté à un socket distant, envoie les donnée.
  • socket.close() : le socket est fermé et libère les ressources. Lors de la mort du processus ayant donné naissance au socket, celui-ci est automatiquement fermé. Il est préférable de le fermer proprement.

Première connexion

Création du programme serveur

Les méthodes associées à l’objet socket sur un serveur :

  • socket.bind(address) : le socket se lie à une adresse,
  • socket.listen() : le socket se place en attente de connexion à accepter,
  • socket.accept() : le socket accepte une connexion et renvoie une paire (nouveau_socket, adresse)nouveau_socket est utilisable pour la communication et adresse est l’adresse du socket avec qui cette connexion est établie.
    Le programme serveur commence par la création du socket et termine par la fermeture du socket.

Programme serveur pour une première connexion

Programme serveur pour une première connexion

On remarque que le serveur, à partir de la ligne 14, est toujours à l'écoute. Il ne ferme son socket serveur qu'à la fin du programme (qui n'arrive jamais avec la boucle while(True)).

Une fois la connexion établie, le serveur en ligne 19 attend des données du client. Il envoie ensuite des données et ferme la connexion.

Création du programme côté client

Les méthodes associées à l’objet socket sur un client :

  • socket.settimeout(value) : la durée limite d’attente pour une connexion prend le nombre de secondes indiqué en paramètre (un nombre réel). Si ce nombre est 0, la connexion devient une tentative non bloquante.
  • socket.connect(address) : le socket tente de se connecter à un autre socket dont l’adresse est indiquée en paramètre. Une durée maximale (timeout) limite la durée de la tentative.

Programme client pour une première connexion

Programme client pour une première connexion

Le client doit être exécuté en parallèle de l'exécution du serveur pour que la communication soit possible. Si le client et le serveur sont sur des machines séparées, cela ne pose pas de problème. Si le client et le serveur sont 2 processus d'une même machine, la solution dépend de l'environnement de développement :

  • Avec Spyder, on peut ouvrir plusieurs consoles,
  • Avec Thonny, il est possible d'exécuter l'un des scripts dans un autre terminal (Menu Exécuter > Exécuter le script dans un terminal),
  • Il est toujours possible de lancer un programme python directement depuis une console python nom_du_programme.py, sans débogueur dans ce cas.

Dès que la connexion est valide, le client envoie des données au serveur (ligne 16). Il attend ensuite le données du serveur puis ferme sa connexion. Il n'y a donc pas avec ces deux programmes de risque qu'un des deux acteurs n'envoie des données à une machine ayant fermé sa connexion.

Observation des échanges

Le module réseau présente l'utilisation du logiciel d'analyse réseau Wireshark. Si on le maitrise, on peut l'utiliser pour observer les échanges entre le serveur et le client. Sur la même machine, le serveur est d’abord lancé sur la console 1 de Spyder (ou sur une instance de Thonny) et le client sur une autre console de Spyder (ou une autre instance de Thonny) Le logiciel Wireshark permet d’observer les échanges sur l’interface loopback, en filtrant sur le port TCP utilisé (8081 ici) :

  • L’établissement de la connexion TCP (108 à 110).
  • Les envois de messages et les acquittements associés (111/112 et 113/114).
  • La fermeture de la connexion (115 à 117).

Acquisition des échanges entre 2 processus d’une même machine à l'aide du logiciel Wireshark

Acquisition des échanges entre 2 processus d’une même machine à l'aide du logiciel Wireshark

Echanges TCP entre un PC client d'adresse 192.168.1.32 et une Raspberry Pi Serveur d'adresse 192.168.1.30. Le code du client indique l’adresse du serveur

Echanges TCP entre un PC client d'adresse 192.168.1.32 et une Raspberry Pi Serveur d'adresse 192.168.1.30. Le code du client indique l’adresse du serveur

Fermeture manuelle des sockets

Pour un bon fonctionnement, le client et le serveur doivent fermer les connexions avant de s'arrêter.

Lors d’un arrêt brutal du serveur ou du client, leur socket continue parfois à s’accaparer le port (c'est le cas avec Spyder mais rarement avec Thonny). C’est pourquoi il faut fermer le socket manuellement, en reprenant l’instruction close du programme. 2 méthodes sont possibles :

1. Clic droit sur la ligne `nom_du_socket.close()` et Exécuter la ligne courante.  
2. Recopie dans la console de l’instruction `nom_du_socket.close()`.

image-8

Si le port est toujours reconnu comme utilisé, la commande sudo netstat -tulpn permet de connaître et tuer le processus utilisant le port.

Affichage des ports ouverts et des processus associés

Affichage des ports ouverts et des processus associés

Sinon, le redémarrage de l’environnement de développement et la suppression par l’OS des processus et sockets associés permet normalement de le libérer.

Fonctions clients et serveur de haut niveau

On peut noter aussi 2 fonctions de haut niveau, s’appuyant sur la bibliothèque socket et les méthodes bind et connect :

  • socket.create_connection(address[, timeout[, source_address]]), utilisée par les clients pour se connecter à un serveur. Ex :
    addr = ("127.0.0.1", 8080). socket_client = socket.create_connection(addr).
  • socket.create_server(address, *, family=AF_INET, backlog=None, reuse_port = False, dualstack_ipv6=False). exemple : addr=("",50000) socket_serveur = socket.create_server(addr).

Serveur multi-tâche

Dans l’exemple précédent, le serveur ne peut accepter qu’une seule connexion à la fois. Pour accepter plusieurs connexion simultanée, il faut lui donner la possibilité de communiquer avec plusieurs socket clients. Pour cela, on crée un processus léger (thread) pour chaque connexion.

Programme serveur

Programme serveur TCP multithread

Programme serveur TCP multithread

Une instance de la fonction instanceServeur est appelée à chaque création de socket_client (ligne 37). On remarque ligne 16, que la fonction instanceServeur tourne dans une boucle while tant que le terme "FIN" n'est pas apparu dans une trame. Si "FIN" apparaît dans un message reçu du client (ligne 22), on sort de la boucle while, la fonction instanceServeur se termine et le socket créé pour ce client est fermé.

Programme client

Il est possible de lancer plusieurs clients, dont le programme ne tient pas compte du multithreading, le client étant mono-thread. Le programme ajoute juste l’annonce de la fin de la connexion, pour annoncer qu'il n'a plus rien à dire au serveur et donc clôt la connexion.

Programme client TCP multithread

Programme client TCP multithread

Observation des échanges

Avec wireshark là encore, on peut observer les échanges entre le serveur et les clients que l'on lance les uns après les autres. Les consoles affichent les numéros des processus et des threads (ou processus légérs), wireshark montre quant à lui les ports retenus par les clients pour la connexion.

console du serveur

console du serveur

Console du client 1

Console du client 1

Console du client 2

Console du client 2

Echanges observés via Wireshark. On distingue les ports TCP des 3 clients

Echanges observés via Wireshark. On distingue les ports TCP des 3 clients

Exercice – L’âge du capitaine

L’objectif est de créer un jeu en réseau avec un serveur de jeu et plusieurs clients jouant de manière isolée (il n’y a pas d’interactions entre les tâches). Pour faire simple, le jeu est l’âge du capitaine : le serveur génère un âge et répond par plus ou moins aux joueurs qui doivent trouver l’âge. A chaque ouverture de connexion, le serveur génère un nombre aléatoire et envoie un message d’accueil. A chaque consigne correcte, le serveur envoie un message et ferme la connexion. Le client doit aussi fermer sa connexion.

Jeu en réseau de l'âge du capitaine avec un serveur Raspberry Pi, un client PC et un client machine virtuelle

Jeu en réseau de l'âge du capitaine avec un serveur Raspberry Pi, un client PC et un client machine virtuelle

Communication UDP

UDP n’utilise pas de connexion. Client comme serveur ont alors un socket qui envoie et reçoit, sans connexion préalable. Ils sont de type SOCK_DGRAM. L’utilisation des tâches n’est plus nécessaire. Ce chapitre plus simple introduit la structure de contrôle try / except. En effet, lors de la demande de création d’un socket, il arrive que la création échoue, par exemple parce que le port est déjà utilisé. La structure de contrôle try / except permet de gérer ce potentiel échec directement dans le programme. * try : {instructions risquant de générer une exception} * except : {instructions à exécuter si une exception est apparue}

Le programme serveur

Programme du serveur UDP

Programme du serveur UDP

On remarque sur le programme du serveur l’utilisation des fonctions sendto et recvfrom, adaptées aux communications UDP.

Le programme client

Programme du client UDP

Programme du client UDP

Exercice – Morpion en réseau

Créer un jeu multijoueur, le morpion par exemple, en TCP et en UDP. Expliquer alors les avantages et inconvénients des 2 protocoles de transport pour ce type d’application.

Bibliographie