Aller au contenu

TP : Mini-Chat en Python

Contexte

Vous êtes développeur chez une startup qui souhaite créer une alternative légère à Discord. Votre mission : implémenter un système de messagerie instantanée en utilisant les sockets Python pour comprendre concrètement le fonctionnement des protocoles TCP et UDP.

Ce TP vous permettra de manipuler les concepts vus en cours : adresses IP, ports, protocoles de transport, et modèle client-serveur.


Objectifs

  • Comprendre le fonctionnement des sockets réseau
  • Implémenter une communication client-serveur en TCP
  • Comparer TCP et UDP en pratique
  • Manipuler les adresses IP et les ports

Prérequis

import socket

Le module socket est inclus dans Python, aucune installation nécessaire.


Partie 1 : Découverte des sockets

1.1. Qu'est-ce qu'un socket ?

Un socket est un point de terminaison pour envoyer ou recevoir des données sur un réseau. C'est comme une prise électrique : vous branchez votre programme dessus pour communiquer.

Un socket est identifié par : - Une adresse IP (quelle machine) - Un port (quelle application sur cette machine)

1.2. Récupérer son adresse IP

Créer un programme qui affiche votre nom d'hôte et votre adresse IP locale.

import socket

def afficher_info_reseau():
    """Affiche les informations réseau de la machine."""
    # À compléter
    pass

# Test
afficher_info_reseau()

Fonctions utiles : - socket.gethostname() : retourne le nom de la machine - socket.gethostbyname(nom) : retourne l'adresse IP associée au nom

Sortie attendue :

Nom de la machine : MonPC
Adresse IP locale : 192.168.1.42

1.3. Résolution DNS

Créer une fonction qui résout un nom de domaine en adresse IP (comme nslookup).

def resoudre_dns(domaine):
    """
    Résout un nom de domaine en adresse IP.

    :param domaine: (str) Le nom de domaine à résoudre
    :return: (str) L'adresse IP correspondante
    """
    # À compléter
    pass

# Tests
print(resoudre_dns("www.google.fr"))
print(resoudre_dns("www.wikipedia.org"))

Partie 2 : Serveur TCP simple

2.1. Création du serveur

Un serveur TCP fonctionne en plusieurs étapes : 1. Créer un socket 2. Lier le socket à une adresse et un port (bind) 3. Écouter les connexions (listen) 4. Accepter une connexion (accept) 5. Recevoir/Envoyer des données 6. Fermer la connexion

import socket

def creer_serveur_tcp(port):
    """
    Crée un serveur TCP qui attend une connexion et affiche le message reçu.

    :param port: (int) Le port d'écoute
    """
    # Créer le socket TCP
    serveur = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

    # Permettre la réutilisation de l'adresse
    serveur.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)

    # Lier à toutes les interfaces sur le port spécifié
    serveur.bind(('', port))

    # Écouter (max 1 connexion en attente)
    serveur.listen(1)

    print(f"Serveur en écoute sur le port {port}...")

    # À compléter : accepter une connexion et recevoir un message

    pass

# Lancer le serveur sur le port 12345
creer_serveur_tcp(12345)

Fonctions à utiliser : - connexion, adresse = serveur.accept() : accepte une connexion - donnees = connexion.recv(1024) : reçoit jusqu'à 1024 octets - donnees.decode('utf-8') : convertit les octets en texte - connexion.close() : ferme la connexion

2.2. Création du client

import socket

def creer_client_tcp(adresse_serveur, port, message):
    """
    Crée un client TCP qui envoie un message au serveur.

    :param adresse_serveur: (str) L'adresse IP du serveur
    :param port: (int) Le port du serveur
    :param message: (str) Le message à envoyer
    """
    # Créer le socket TCP
    client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

    # À compléter : se connecter et envoyer le message

    pass

# Envoyer un message au serveur local
creer_client_tcp('127.0.0.1', 12345, "Bonjour serveur !")

Fonctions à utiliser : - client.connect((adresse, port)) : se connecte au serveur - client.send(message.encode('utf-8')) : envoie le message

2.3. Test

  1. Ouvrir deux terminaux
  2. Dans le premier, lancer le serveur
  3. Dans le second, lancer le client
  4. Observer le message reçu par le serveur

Partie 3 : Chat bidirectionnel

3.1. Serveur de chat

Modifier le serveur pour qu'il puisse : - Recevoir un message du client - Répondre au client - Continuer la conversation jusqu'à ce que le client envoie "quit"

def serveur_chat(port):
    """
    Serveur de chat interactif.
    """
    serveur = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    serveur.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    serveur.bind(('', port))
    serveur.listen(1)

    print(f"Serveur de chat en attente sur le port {port}...")

    connexion, adresse = serveur.accept()
    print(f"Connexion de {adresse[0]}:{adresse[1]}")

    # À compléter : boucle de conversation
    # - Recevoir un message
    # - Si le message est "quit", terminer
    # - Sinon, demander une réponse et l'envoyer

    pass

3.2. Client de chat

def client_chat(adresse_serveur, port):
    """
    Client de chat interactif.
    """
    client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    client.connect((adresse_serveur, port))

    print(f"Connecté au serveur {adresse_serveur}:{port}")
    print("Tapez 'quit' pour quitter.\n")

    # À compléter : boucle de conversation
    # - Demander un message à l'utilisateur
    # - Envoyer le message
    # - Si "quit", terminer
    # - Sinon, attendre et afficher la réponse

    pass

Partie 4 : Comparaison TCP vs UDP

4.1. Serveur UDP

UDP est "connectionless" : pas besoin d'établir une connexion avant d'envoyer des données.

def serveur_udp(port):
    """
    Serveur UDP simple.
    """
    serveur = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)  # DGRAM = UDP
    serveur.bind(('', port))

    print(f"Serveur UDP en écoute sur le port {port}...")

    while True:
        # recvfrom retourne les données ET l'adresse de l'expéditeur
        donnees, adresse = serveur.recvfrom(1024)
        message = donnees.decode('utf-8')
        print(f"[{adresse[0]}:{adresse[1]}] {message}")

        if message.lower() == "quit":
            break

    serveur.close()

4.2. Client UDP

def client_udp(adresse_serveur, port):
    """
    Client UDP simple.
    """
    client = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)

    while True:
        message = input("Message (quit pour quitter) : ")
        # sendto envoie directement sans connexion préalable
        client.sendto(message.encode('utf-8'), (adresse_serveur, port))

        if message.lower() == "quit":
            break

    client.close()

4.3. Analyse comparative

Compléter le tableau suivant après avoir testé les deux versions :

Critère TCP UDP
Connexion préalable ... ...
Fonction d'envoi ... ...
Garantie de livraison ... ...
Ordre des messages ... ...
Utilisation typique ... ...

Partie 5 : Mini-serveur multi-clients (Bonus)

5.1. Problème

Le serveur actuel ne peut gérer qu'un seul client. Pour accepter plusieurs clients simultanément, il faut utiliser les threads.

import socket
import threading

clients = []

def gerer_client(connexion, adresse):
    """Gère la communication avec un client."""
    print(f"[+] Nouveau client : {adresse}")
    clients.append(connexion)

    while True:
        try:
            message = connexion.recv(1024).decode('utf-8')
            if not message or message.lower() == "quit":
                break

            # Diffuser le message à tous les clients
            for client in clients:
                if client != connexion:
                    client.send(f"[{adresse[0]}] {message}".encode('utf-8'))
        except:
            break

    clients.remove(connexion)
    connexion.close()
    print(f"[-] Client déconnecté : {adresse}")


def serveur_multi_clients(port):
    """Serveur acceptant plusieurs clients."""
    serveur = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    serveur.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    serveur.bind(('', port))
    serveur.listen(5)

    print(f"Serveur multi-clients sur le port {port}")

    while True:
        connexion, adresse = serveur.accept()
        thread = threading.Thread(target=gerer_client, args=(connexion, adresse))
        thread.start()

5.2. Client amélioré

Créer un client qui peut recevoir des messages tout en permettant d'en envoyer (utiliser un thread pour la réception).


Partie 6 : Questions de synthèse

  1. Expliquer la différence entre SOCK_STREAM (TCP) et SOCK_DGRAM (UDP).

  2. Pourquoi le serveur utilise-t-il bind() alors que le client utilise connect() ?

  3. Que se passe-t-il si deux programmes essaient d'utiliser le même port simultanément ?

  4. Pourquoi utilise-t-on 127.0.0.1 (localhost) pour les tests ?

  5. Dans quel cas préféreriez-vous UDP à TCP pour une application de chat ?


Barème indicatif

Partie Points
Partie 1 : Découverte 3
Partie 2 : Serveur/Client TCP 5
Partie 3 : Chat bidirectionnel 4
Partie 4 : Comparaison TCP/UDP 4
Partie 5 : Multi-clients (Bonus) 2
Partie 6 : Questions 2
Total 20

Auteur : Florian Mathieu

Licence CC BY NC

Licence Creative Commons
Ce cours est mis à disposition selon les termes de la Licence Creative Commons Attribution - Pas d'Utilisation Commerciale - Partage dans les Mêmes Conditions 4.0 International.