# Nom du fichier: python_cheats/cheatsheets/socket.txt

Voici le contenu complet du fichier:

```txt
# Fichier: python_cheats/cheatsheets/socket.txt
# Cheatsheet Socket Python - Guide Ultra-Détaillé pour Grands Débutants


[OK] CONCEPTS FONDAMENTAUX (EXPLICATIONS TRÈS DÉTAILLÉES)

# === QU'EST-CE QU'UN SOCKET ? ===

# Imagine que tu veux que deux programmes communiquent entre eux
# Exemple: Un programme sur ton ordinateur et un autre sur un serveur distant
# Comment font-ils pour s'échanger des messages?

# ANALOGIE: Le téléphone
# - Un socket = Un téléphone
# - Une connexion socket = Un appel téléphonique
# - Envoyer des données = Parler dans le téléphone
# - Recevoir des données = Écouter l'autre personne

# DÉFINITION TECHNIQUE:
# Un socket = Point de terminaison (endpoint) pour la communication réseau
# = Interface qui permet à deux programmes de communiquer
# = Peut être sur la même machine ou sur des machines différentes

# Pourquoi "socket" (prise électrique)?
# Comme une prise électrique qui connecte deux appareils,
# Un socket connecte deux programmes pour qu'ils échangent des données


# === TYPES DE COMMUNICATION ===

# Il existe 2 types principaux de sockets:

# 1. STREAM SOCKETS (TCP - Transmission Control Protocol)
# = Communication fiable, ordonnée, orientée connexion
# Analogie: Un appel téléphonique
# - Tu dois d'abord établir la connexion (composer le numéro)
# - Les messages arrivent dans l'ordre
# - Si un message est perdu, il est renvoyé automatiquement
# - La connexion reste ouverte pendant toute la conversation
# Exemple d'usage: 
#   - Navigateurs web (HTTP/HTTPS)
#   - Email (SMTP)
#   - Transfert de fichiers (FTP)
#   - Chat en temps réel

# 2. DATAGRAM SOCKETS (UDP - User Datagram Protocol)
# = Communication rapide, sans connexion, non garantie
# Analogie: Envoyer des lettres par la poste
# - Tu n'établis pas de connexion avant d'envoyer
# - Les lettres peuvent arriver dans le désordre
# - Certaines lettres peuvent être perdues
# - Plus rapide que TCP car pas de vérification
# Exemple d'usage:
#   - Streaming vidéo/audio
#   - Jeux en ligne (où la vitesse prime)
#   - DNS (résolution de noms de domaine)
#   - VoIP (appels vocaux sur Internet)


# === VOCABULAIRE RÉSEAU ESSENTIEL ===

# IP ADDRESS (Adresse IP)
# = Identifiant unique d'un appareil sur un réseau
# Analogie: Adresse postale d'une maison
# Formats:
#   - IPv4: 192.168.1.1 (4 nombres de 0 à 255 séparés par des points)
#   - IPv6: 2001:0db8:85a3:0000:0000:8a2e:0370:7334 (plus long, plus moderne)
# Adresses spéciales:
#   - 127.0.0.1 (localhost) = ton propre ordinateur
#   - 0.0.0.0 = écoute sur toutes les interfaces réseau disponibles
#   - 192.168.x.x = adresses privées (réseau local)

# PORT
# = Numéro qui identifie un service/programme spécifique sur une machine
# Analogie: Numéro d'appartement dans un immeuble
# - L'adresse IP = l'immeuble
# - Le port = l'appartement spécifique
# Plage: 0 à 65535
# Types de ports:
#   - 0-1023: Ports système/bien connus (HTTP=80, HTTPS=443, FTP=21, SSH=22)
#   - 1024-49151: Ports enregistrés (usage spécifique par des applications)
#   - 49152-65535: Ports dynamiques/privés (usage temporaire)
# Exemple complet:
#   192.168.1.100:8080
#   └─────┬─────┘ └┬┘
#     Adresse IP   Port

# LOCALHOST
# = Ton propre ordinateur
# Adresse IP: 127.0.0.1
# Nom d'hôte: localhost
# Usage: Tester des applications réseau localement
# Quand tu te connectes à localhost, tu te connectes à toi-même!

# CLIENT vs SERVEUR
# CLIENT:
# = Programme qui INITIE la connexion
# = Demande des services
# Exemple: Navigateur web, client email
# Actions: Se connecte, envoie des requêtes, reçoit des réponses

# SERVEUR:
# = Programme qui ATTEND les connexions
# = Fournit des services
# Exemple: Serveur web, serveur de base de données
# Actions: Écoute, accepte les connexions, traite les requêtes, répond

# BINDING (Liaison)
# = Associer un socket à une adresse IP et un port spécifiques
# Le serveur "bind" pour dire: "J'écoute sur cette adresse et ce port"
# Exemple: bind(('0.0.0.0', 8080)) = "J'écoute sur toutes les interfaces, port 8080"

# LISTENING (Écoute)
# = Le serveur attend activement des connexions entrantes
# Après le bind, le serveur fait listen() pour accepter les connexions

# ACCEPT (Acceptation)
# = Le serveur accepte une connexion cliente
# Retourne un nouveau socket dédié à cette connexion spécifique

# CONNECT (Connexion)
# = Le client établit une connexion avec le serveur
# Le client fait connect() pour se connecter à un serveur


# === COMMENT ÇA MARCHE? (FLUX COMPLET TCP) ===

# CÔTÉ SERVEUR:
# 1. Créer un socket
#    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
#    = "Je crée un téléphone"

# 2. Binder (lier) à une adresse et un port
#    sock.bind(('0.0.0.0', 8080))
#    = "Mon numéro de téléphone est le 8080"

# 3. Écouter les connexions entrantes
#    sock.listen(5)
#    = "Je suis prêt à recevoir jusqu'à 5 appels en attente"

# 4. Accepter une connexion
#    client_sock, addr = sock.accept()
#    = "Je décroche le téléphone"

# 5. Recevoir des données
#    data = client_sock.recv(1024)
#    = "J'écoute ce que le client me dit"

# 6. Envoyer des données
#    client_sock.send(b"Hello Client!")
#    = "Je réponds au client"

# 7. Fermer la connexion
#    client_sock.close()
#    = "Je raccroche"

# CÔTÉ CLIENT:
# 1. Créer un socket
#    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

# 2. Se connecter au serveur
#    sock.connect(('192.168.1.100', 8080))
#    = "Je compose le numéro du serveur"

# 3. Envoyer des données
#    sock.send(b"Hello Server!")
#    = "Je parle au serveur"

# 4. Recevoir des données
#    data = sock.recv(1024)
#    = "J'écoute la réponse du serveur"

# 5. Fermer la connexion
#    sock.close()
#    = "Je raccroche"


# === POURQUOI UTILISER LES SOCKETS? ===

# 1. COMMUNICATION RÉSEAU
#    - Créer des applications client-serveur
#    - Chat en temps réel
#    - Jeux multijoueurs
#    - Applications distribuées

# 2. TRANSFERT DE DONNÉES
#    - Partager des fichiers entre ordinateurs
#    - Synchroniser des données
#    - Streaming de contenu

# 3. SERVICES RÉSEAU
#    - Créer des serveurs web
#    - API REST
#    - Microservices
#    - IoT (Internet des Objets)

# 4. PROTOCOLES PERSONNALISÉS
#    - Créer ton propre protocole de communication
#    - Optimiser pour des besoins spécifiques


[OK] INSTALLATION & IMPORTS

# === BONNE NOUVELLE: SOCKET EST INCLUS DANS PYTHON! ===

# Le module socket fait partie de la bibliothèque standard Python
# = Pas besoin d'installer quoi que ce soit!
# = Disponible dès l'installation de Python

# Vérifier que socket est disponible:
python -c "import socket; print('Socket disponible!')"
# Affiche: Socket disponible!


# === IMPORTS ESSENTIELS ===

import socket

# Import complet pour tous les exemples
import socket
import sys
import time
import threading
from datetime import datetime


# === CONSTANTES IMPORTANTES ===

# Familles d'adresses
socket.AF_INET       # IPv4 (le plus courant)
socket.AF_INET6      # IPv6
socket.AF_UNIX       # Sockets Unix locaux (communication inter-processus)

# Types de sockets
socket.SOCK_STREAM   # TCP (fiable, connexion établie)
socket.SOCK_DGRAM    # UDP (rapide, sans connexion)
socket.SOCK_RAW      # Accès direct au protocole réseau (avancé)

# Niveaux de protocole
socket.SOL_SOCKET    # Options au niveau socket
socket.IPPROTO_TCP   # Options TCP
socket.IPPROTO_UDP   # Options UDP

# Options de socket
socket.SO_REUSEADDR  # Réutiliser une adresse immédiatement
socket.SO_KEEPALIVE  # Garder la connexion active
socket.SO_BROADCAST  # Permettre les broadcasts
socket.SO_RCVBUF     # Taille du buffer de réception
socket.SO_SNDBUF     # Taille du buffer d'envoi


[OK] CRÉATION D'UN SOCKET (EXPLICATIONS TRÈS DÉTAILLÉES)

# === SYNTAXE GÉNÉRALE ===

sock = socket.socket(family, type, proto)

# Paramètres:
# family: Famille d'adresses (AF_INET pour IPv4)
# type: Type de socket (SOCK_STREAM pour TCP, SOCK_DGRAM pour UDP)
# proto: Protocole (optionnel, généralement 0 pour auto-détection)


# === EXEMPLE #1: SOCKET TCP (LE PLUS COURANT) ===

import socket

# Créer un socket TCP/IP
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

# Explications ligne par ligne:
# socket.socket(...) = Fonction qui crée un nouveau socket
# socket.AF_INET = Famille IPv4 (adresses comme 192.168.1.1)
# socket.SOCK_STREAM = Type TCP (connexion fiable, ordonnée)

# À ce stade:
# - Le socket est créé
# - Il n'est pas encore lié à une adresse
# - Il n'est pas connecté
# - C'est juste un "téléphone" prêt à être utilisé

print(f"Socket créé: {sock}")
# Affiche quelque chose comme: <socket.socket fd=3, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0>


# === EXEMPLE #2: SOCKET UDP ===

# Créer un socket UDP
udp_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)

# Différence avec TCP:
# socket.SOCK_DGRAM = Type UDP (rapide, sans garantie de livraison)
# Usage: Quand la vitesse est plus importante que la fiabilité


# === EXEMPLE #3: AVEC CONTEXT MANAGER (RECOMMANDÉ!) ===

# Utiliser 'with' pour gérer automatiquement la fermeture

with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
    # Utiliser le socket ici
    print("Socket ouvert")
    # Pas besoin de sock.close(), c'est automatique!

# Avantages:
# - Le socket est fermé automatiquement à la fin du bloc
# - Même en cas d'erreur, le socket est fermé
# - Code plus propre et plus sûr


# === EXEMPLE #4: GESTION D'ERREURS ===

import socket

try:
    # Créer le socket
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    print("Socket créé avec succès!")
    
except socket.error as err:
    # Erreur lors de la création
    print(f"Erreur création socket: {err}")
    sys.exit(1)

finally:
    # Toujours fermer le socket
    if 'sock' in locals():
        sock.close()
        print("Socket fermé")


# === PROPRIÉTÉS D'UN SOCKET ===

sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

# Famille d'adresses
print(f"Famille: {sock.family}")
# Affiche: AddressFamily.AF_INET

# Type de socket
print(f"Type: {sock.type}")
# Affiche: SocketKind.SOCK_STREAM

# Protocole
print(f"Protocole: {sock.proto}")
# Affiche: 0 (auto-détecté)

# File descriptor (identifiant système)
print(f"FD: {sock.fileno()}")
# Affiche: 3 (ou autre nombre)


[OK] SERVEUR TCP COMPLET (EXEMPLE ULTRA-DÉTAILLÉ)

# === SERVEUR ECHO BASIQUE ===

# Ce serveur reçoit des messages et les renvoie (echo)

import socket

def serveur_echo_simple():
    """
    Serveur echo qui renvoie tout ce qu'il reçoit
    """
    
    # === ÉTAPE 1: CRÉER LE SOCKET ===
    
    # Créer un socket TCP
    serveur = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    
    print("[OK] Socket serveur créé")
    
    
    # === ÉTAPE 2: CONFIGURER LES OPTIONS ===
    
    # SO_REUSEADDR: Permet de réutiliser l'adresse immédiatement
    # Sans ça, après un crash, il faut attendre avant de relancer
    serveur.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    
    print("[OK] Options configurées")
    
    
    # === ÉTAPE 3: BINDER (LIER) À UNE ADRESSE ===
    
    # Choisir l'adresse et le port
    HOST = '0.0.0.0'  # Écouter sur toutes les interfaces
    PORT = 8080       # Port d'écoute
    
    # Lier le socket
    serveur.bind((HOST, PORT))
    
    print(f"[OK] Serveur lié à {HOST}:{PORT}")
    
    # Explications:
    # bind() prend un tuple (adresse, port)
    # '0.0.0.0' = écoute sur toutes les interfaces réseau
    # Si tu mets '127.0.0.1', seul localhost peut se connecter
    
    
    # === ÉTAPE 4: ÉCOUTER LES CONNEXIONS ===
    
    # Commencer à écouter
    # Le paramètre (5) = nombre max de connexions en attente
    serveur.listen(5)
    
    print("[OK] Serveur en écoute...")
    print(f"  Attendant des connexions sur le port {PORT}")
    
    
    # === ÉTAPE 5: BOUCLE PRINCIPALE ===
    
    try:
        while True:
            # === ÉTAPE 5a: ACCEPTER UNE CONNEXION ===
            
            print("\n[HOURGLASS_WITH_FLOWING_SAND] En attente de connexion...")
            
            # accept() bloque jusqu'à ce qu'un client se connecte
            client, adresse = serveur.accept()
            
            print(f"[OK] Client connecté depuis {adresse[0]}:{adresse[1]}")
            
            # Explications:
            # client = nouveau socket dédié à ce client
            # adresse = tuple (IP_client, port_client)
            
            
            # === ÉTAPE 5b: RECEVOIR DES DONNÉES ===
            
            try:
                while True:
                    # Recevoir jusqu'à 1024 octets
                    data = client.recv(1024)
                    
                    # Si data est vide, le client a fermé la connexion
                    if not data:
                        print("  Client a fermé la connexion")
                        break
                    
                    # Décoder les bytes en string
                    message = data.decode('utf-8')
                    print(f"  [MESSAGE] Reçu: {message.strip()}")
                    
                    
                    # === ÉTAPE 5c: ENVOYER LA RÉPONSE (ECHO) ===
                    
                    # Renvoyer les mêmes données
                    client.send(data)
                    print(f"  [SORTIE] Envoyé: {message.strip()}")
            
            except Exception as e:
                print(f"  [X] Erreur: {e}")
            
            finally:
                # === ÉTAPE 5d: FERMER LA CONNEXION CLIENT ===
                client.close()
                print("  [OK] Connexion client fermée")
    
    except KeyboardInterrupt:
        print("\n\n[ATTENTION]  Arrêt du serveur...")
    
    finally:
        # === ÉTAPE 6: FERMER LE SERVEUR ===
        serveur.close()
        print("[OK] Serveur fermé")


# Lancer le serveur
if __name__ == "__main__":
    serveur_echo_simple()


# === EXEMPLE D'EXÉCUTION ===

# Terminal 1 (Serveur):
# $ python serveur.py
# [OK] Socket serveur créé
# [OK] Options configurées
# [OK] Serveur lié à 0.0.0.0:8080
# [OK] Serveur en écoute...
#   Attendant des connexions sur le port 8080
#
# [HOURGLASS_WITH_FLOWING_SAND] En attente de connexion...
# [OK] Client connecté depuis 127.0.0.1:54321
#   [MESSAGE] Reçu: Hello Server!
#   [SORTIE] Envoyé: Hello Server!
#   Client a fermé la connexion
#   [OK] Connexion client fermée


[OK] CLIENT TCP COMPLET (EXEMPLE ULTRA-DÉTAILLÉ)

# === CLIENT ECHO BASIQUE ===

import socket

def client_echo_simple():
    """
    Client qui se connecte au serveur echo et envoie un message
    """
    
    # === ÉTAPE 1: CRÉER LE SOCKET ===
    
    client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    
    print("[OK] Socket client créé")
    
    
    # === ÉTAPE 2: SE CONNECTER AU SERVEUR ===
    
    # Adresse du serveur
    HOST = '127.0.0.1'  # localhost (serveur sur la même machine)
    PORT = 8080         # Port du serveur
    
    try:
        # Connexion au serveur
        print(f"[HOURGLASS_WITH_FLOWING_SAND] Connexion à {HOST}:{PORT}...")
        client.connect((HOST, PORT))
        print("[OK] Connecté au serveur!")
        
        # Explications:
        # connect() prend un tuple (adresse, port)
        # Bloque jusqu'à ce que la connexion soit établie ou échoue
        
        
        # === ÉTAPE 3: ENVOYER UN MESSAGE ===
        
        message = "Hello Server!"
        
        # Encoder le string en bytes
        data = message.encode('utf-8')
        
        # Envoyer les données
        client.send(data)
        print(f"[SORTIE] Envoyé: {message}")
        
        # Explications:
        # send() envoie des bytes (pas des strings!)
        # encode('utf-8') convertit string -> bytes
        
        
        # === ÉTAPE 4: RECEVOIR LA RÉPONSE ===
        
        # Recevoir jusqu'à 1024 octets
        reponse = client.recv(1024)
        
        # Décoder les bytes en string
        message_recu = reponse.decode('utf-8')
        print(f"[MESSAGE] Reçu: {message_recu}")
        
        # Explications:
        # recv(1024) reçoit jusqu'à 1024 octets
        # decode('utf-8') convertit bytes -> string
    
    except ConnectionRefusedError:
        print("[X] Connexion refusée. Le serveur est-il lancé?")
    
    except Exception as e:
        print(f"[X] Erreur: {e}")
    
    finally:
        # === ÉTAPE 5: FERMER LA CONNEXION ===
        client.close()
        print("[OK] Connexion fermée")


# Lancer le client
if __name__ == "__main__":
    client_echo_simple()


# === EXEMPLE D'EXÉCUTION ===

# Terminal 2 (Client):
# $ python client.py
# [OK] Socket client créé
# [HOURGLASS_WITH_FLOWING_SAND] Connexion à 127.0.0.1:8080...
# [OK] Connecté au serveur!
# [SORTIE] Envoyé: Hello Server!
# [MESSAGE] Reçu: Hello Server!
# [OK] Connexion fermée


[OK] SERVEUR MULTI-CLIENT (AVEC THREADS)

# === POURQUOI LES THREADS? ===

# Le serveur basique ne peut gérer qu'UN client à la fois
# Pendant qu'il traite un client, les autres doivent attendre
# Solution: Utiliser des threads pour gérer plusieurs clients simultanément

# Analogie:
# - Sans threads = Un serveur avec un seul employé
# - Avec threads = Un serveur avec plusieurs employés (un par client)


# === SERVEUR MULTI-CLIENT ===

import socket
import threading

def gerer_client(client_socket, adresse):
    """
    Fonction qui gère un client spécifique
    Exécutée dans un thread séparé
    """
    
    print(f"[THREAD] Nouveau client: {adresse[0]}:{adresse[1]}")
    
    try:
        while True:
            # Recevoir des données
            data = client_socket.recv(1024)
            
            # Client a fermé la connexion
            if not data:
                break
            
            # Afficher le message
            message = data.decode('utf-8')
            print(f"[{adresse[0]}:{adresse[1]}] {message.strip()}")
            
            # Renvoyer le message (echo)
            client_socket.send(data)
    
    except Exception as e:
        print(f"[ERREUR] {adresse}: {e}")
    
    finally:
        # Fermer la connexion
        client_socket.close()
        print(f"[THREAD] Client déconnecté: {adresse[0]}:{adresse[1]}")


def serveur_multithread():
    """
    Serveur qui gère plusieurs clients simultanément
    """
    
    # Créer le socket serveur
    serveur = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    serveur.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    
    # Binder
    HOST = '0.0.0.0'
    PORT = 8080
    serveur.bind((HOST, PORT))
    
    # Écouter
    serveur.listen(5)
    
    print(f"[OK] Serveur multi-client démarré sur {HOST}:{PORT}")
    print("  En attente de connexions...\n")
    
    try:
        while True:
            # Accepter une connexion
            client, adresse = serveur.accept()
            
            print(f"[OK] Nouvelle connexion: {adresse[0]}:{adresse[1]}")
            
            # Créer un nouveau thread pour ce client
            thread = threading.Thread(
                target=gerer_client,
                args=(client, adresse),
                daemon=True  # Thread daemon: s'arrête quand le programme principal s'arrête
            )
            
            # Démarrer le thread
            thread.start()
            
            # Le serveur continue immédiatement à attendre d'autres clients
            # Pendant que le thread gère le client actuel
            
            print(f"  Threads actifs: {threading.active_count()}")
    
    except KeyboardInterrupt:
        print("\n\n[ATTENTION]  Arrêt du serveur...")
    
    finally:
        serveur.close()
        print("[OK] Serveur fermé")


if __name__ == "__main__":
    serveur_multithread()


# === AVANTAGES DES THREADS ===

# [OK] Plusieurs clients en même temps
# [OK] Pas d'attente pour les nouveaux clients
# [OK] Chaque client a sa propre session
# [OK] Un client qui crashe n'affecte pas les autres


# === EXEMPLE D'EXÉCUTION AVEC PLUSIEURS CLIENTS ===

# Terminal 1 (Serveur):
# $ python serveur_multithread.py
# [OK] Serveur multi-client démarré sur 0.0.0.0:8080
#   En attente de connexions...
#
# [OK] Nouvelle connexion: 127.0.0.1:54321
#   Threads actifs: 2
# [THREAD] Nouveau client: 127.0.0.1:54321
# [OK] Nouvelle connexion: 127.0.0.1:54322
#   Threads actifs: 3
# [THREAD] Nouveau client: 127.0.0.1:54322
# [127.0.0.1:54321] Hello from client 1
# [127.0.0.1:54322] Hello from client 2


[OK] CLIENT INTERACTIF (AVEC INPUT)

# === CLIENT AVEC BOUCLE D'ENVOI ===

import socket

def client_interactif():
    """
    Client qui permet d'envoyer plusieurs messages
    """
    
    # Créer et connecter
    client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    
    HOST = '127.0.0.1'
    PORT = 8080
    
    try:
        client.connect((HOST, PORT))
        print(f"[OK] Connecté à {HOST}:{PORT}")
        print("Tapez vos messages (ou 'quit' pour quitter)\n")
        
        while True:
            # Demander un message à l'utilisateur
            message = input("Vous> ")
            
            # Quitter si l'utilisateur tape "quit"
            if message.lower() == 'quit':
                break
            
            # Envoyer le message
            client.send(message.encode('utf-8'))
            
            # Recevoir la réponse
            reponse = client.recv(1024)
            print(f"Serveur> {reponse.decode('utf-8')}\n")
    
    except ConnectionRefusedError:
        print("[X] Connexion refusée")
    
    except Exception as e:
        print(f"[X] Erreur: {e}")
    
    finally:
        client.close()
        print("[OK] Déconnecté")


if __name__ == "__main__":
    client_interactif()


# === EXEMPLE D'UTILISATION ===

# $ python client_interactif.py
# [OK] Connecté à 127.0.0.1:8080
# Tapez vos messages (ou 'quit' pour quitter)
#
# Vous> Hello!
# Serveur> Hello!
#
# Vous> How are you?
# Serveur> How are you?
#
# Vous> quit
# [OK] Déconnecté


[OK] SERVEUR UDP (SANS CONNEXION)

# === DIFFÉRENCE AVEC TCP ===

# TCP:
# - Établir une connexion (accept/connect)
# - Envoyer/recevoir sur la connexion
# - Fermer la connexion

# UDP:
# - Pas de connexion
# - Envoyer directement à une adresse
# - Recevoir directement des données


# === SERVEUR UDP BASIQUE ===

import socket

def serveur_udp():
    """
    Serveur UDP simple
    """
    
    # === ÉTAPE 1: CRÉER UN SOCKET UDP ===
    
    # SOCK_DGRAM = UDP (au lieu de SOCK_STREAM pour TCP)
    serveur = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    
    print("[OK] Socket UDP créé")
    
    
    # === ÉTAPE 2: BINDER À UNE ADRESSE ===
    
    HOST = '0.0.0.0'
    PORT = 9090
    
    serveur.bind((HOST, PORT))
    
    print(f"[OK] Serveur UDP lié à {HOST}:{PORT}")
    print("  En attente de messages...\n")
    
    
    # === ÉTAPE 3: BOUCLE DE RÉCEPTION ===
    
    try:
        while True:
            # Recevoir des données
            # recvfrom() retourne (données, adresse_émetteur)
            data, adresse = serveur.recvfrom(1024)
            
            # Explications:
            # data = les données reçues (bytes)
            # adresse = tuple (IP_émetteur, port_émetteur)
            
            message = data.decode('utf-8')
            print(f"[MESSAGE] Reçu de {adresse[0]}:{adresse[1]}: {message}")
            
            
            # Envoyer une réponse
            reponse = f"Echo: {message}"
            serveur.sendto(reponse.encode('utf-8'), adresse)
            
            # Explications:
            # sendto() envoie à une adresse spécifique
            # Pas besoin de connexion établie!
    
    except KeyboardInterrupt:
        print("\n\n[ATTENTION]  Arrêt du serveur...")
    
    finally:
        serveur.close()
        print("[OK]Serveur fermé")


if __name__ == "__main__":
    serveur_udp()


# === CLIENT UDP ===

import socket

def client_udp():
    """
    Client UDP simple
    """
    
    # Créer un socket UDP
    client = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    
    # Adresse du serveur
    HOST = '127.0.0.1'
    PORT = 9090
    
    try:
        # Envoyer un message
        message = "Hello UDP Server!"
        client.sendto(message.encode('utf-8'), (HOST, PORT))
        print(f"[SORTIE] Envoyé: {message}")
        
        # Recevoir la réponse
        data, serveur_adresse = client.recvfrom(1024)
        reponse = data.decode('utf-8')
        print(f"[MESSAGE] Reçu: {reponse}")
    
    except Exception as e:
        print(f"[X] Erreur: {e}")
    
    finally:
        client.close()


if __name__ == "__main__":
    client_udp()


# === DIFFÉRENCES CLÉS UDP vs TCP ===

# ┌──────────────────┬────────────────────┬─────────────────────┐
# │    Aspect        │        TCP         │        UDP          │
# ├──────────────────┼────────────────────┼─────────────────────┤
# │ Connexion        │ Oui (connect)      │ Non                 │
# │ Fiabilité        │ Garantie           │ Non garantie        │
# │ Ordre            │ Préservé           │ Non garanti         │
# │ Vitesse          │ Plus lent          │ Plus rapide         │
# │ Overhead         │ Plus élevé         │ Minimal             │
# │ Méthodes envoi   │ send()             │ sendto()            │
# │ Méthodes réc.    │ recv()             │ recvfrom()          │
# │ Use case         │ Web, email, FTP    │ Streaming, jeux     │
# └──────────────────┴────────────────────┴─────────────────────┘


[OK] OPTIONS DE SOCKET (CONFIGURATION AVANCÉE)

# === SYNTAXE GÉNÉRALE ===

sock.setsockopt(level, optname, value)

# Paramètres:
# level: Niveau du protocole (SOL_SOCKET, IPPROTO_TCP, etc)
# optname: Nom de l'option
# value: Valeur à définir


# === OPTION #1: SO_REUSEADDR (TRÈS IMPORTANT!) ===

import socket

sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

# Activer SO_REUSEADDR
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)

# Pourquoi c'est important?
# Sans SO_REUSEADDR:
# 1. Tu lances ton serveur sur le port 8080
# 2. Tu l'arrêtes (Ctrl+C)
# 3. Tu essaies de le relancer immédiatement
# 4. [X] Erreur: "Address already in use"
# 5. Tu dois attendre 1-2 minutes

# Avec SO_REUSEADDR:
# 1. Tu lances ton serveur
# 2. Tu l'arrêtes
# 3. Tu le relances immédiatement
# 4. [OK] Ça marche!

# Usage: TOUJOURS utiliser pour les serveurs!


# === OPTION #2: SO_KEEPALIVE (GARDER LA CONNEXION ACTIVE) ===

sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

# Activer keep-alive
sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)

# Pourquoi?
# TCP envoie périodiquement des "pings" pour vérifier que la connexion est vivante
# Si l'autre côté ne répond pas, la connexion est fermée
# Utile pour détecter les clients/serveurs qui ont crashé


# === OPTION #3: SO_RCVBUF & SO_SNDBUF (TAILLE DES BUFFERS) ===

sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

# Augmenter le buffer de réception à 64KB
sock.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, 65536)

# Augmenter le buffer d'envoi à 64KB
sock.setsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF, 65536)

# Pourquoi?
# Buffers plus grands = meilleures performances pour gros transferts
# Par défaut: généralement 8KB-16KB
# Pour streaming vidéo, transfert de fichiers: augmenter!


# === OPTION #4: TCP_NODELAY (DÉSACTIVER L'ALGORITHME DE NAGLE) ===

import socket

sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

# Désactiver Nagle
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)

# Qu'est-ce que l'algorithme de Nagle?
# Par défaut, TCP groupe les petits paquets pour envoyer moins de paquets
# Réduit l'overhead réseau mais augmente la latence

# Quand désactiver Nagle?
# - Applications temps réel (jeux, chat)
# - Quand la latence est critique
# - Quand tu envoies beaucoup de petits messages

# Quand laisser Nagle activé?
# - Transfert de gros fichiers
# - Applications non temps réel


# === OPTION #5: SO_BROADCAST (POUR UDP) ===

sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)

# Activer le broadcast
sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)

# Pourquoi?
# Permet d'envoyer des messages à TOUS les appareils sur le réseau local
# Usage: Découverte de services, synchronisation


# === LIRE LES OPTIONS ===

sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

# Lire la valeur d'une option
valeur = sock.getsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR)
print(f"SO_REUSEADDR: {valeur}")

# Taille du buffer de réception
buffer_size = sock.getsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF)
print(f"Buffer réception: {buffer_size} octets")


[OK] TIMEOUTS (GESTION DU TEMPS D'ATTENTE)

# === POURQUOI LES TIMEOUTS? ===

# Sans timeout:
# - recv() bloque INDÉFINIMENT si le client ne répond pas
# - connect() bloque INDÉFINIMENT si le serveur ne répond pas
# = Ton programme peut se bloquer pour toujours!

# Avec timeout:
# - Après X secondes, une exception est levée
# - Tu peux gérer l'erreur et continuer


# === DÉFINIR UN TIMEOUT ===

import socket

sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

# Définir un timeout de 5 secondes
sock.settimeout(5.0)

# Maintenant:
# - recv() attend max 5 secondes
# - connect() attend max 5 secondes
# - accept() attend max 5 secondes


# === EXEMPLE: CLIENT AVEC TIMEOUT ===

import socket

def client_avec_timeout():
    client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    
    # Timeout de 3 secondes
    client.settimeout(3.0)
    
    try:
        print("[HOURGLASS_WITH_FLOWING_SAND] Connexion au serveur...")
        client.connect(('192.168.1.100', 8080))
        print("[OK] Connecté!")
        
        # Envoyer un message
        client.send(b"Hello")
        
        # Attendre la réponse (max 3 secondes)
        print("[HOURGLASS_WITH_FLOWING_SAND] Attente de la réponse...")
        data = client.recv(1024)
        print(f"[OK] Reçu: {data.decode()}")
    
    except socket.timeout:
        print("[X] Timeout! Le serveur ne répond pas.")
    
    except ConnectionRefusedError:
        print("[X] Connexion refusée")
    
    finally:
        client.close()


# === EXEMPLE: SERVEUR AVEC TIMEOUT ===

import socket

def serveur_avec_timeout():
    serveur = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    serveur.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    serveur.bind(('0.0.0.0', 8080))
    serveur.listen(5)
    
    # Timeout de 10 secondes sur accept()
    serveur.settimeout(10.0)
    
    print("[OK] Serveur en écoute")
    print("  Timeout accept: 10 secondes\n")
    
    try:
        while True:
            try:
                print("[HOURGLASS_WITH_FLOWING_SAND] En attente de connexion (timeout 10s)...")
                client, adresse = serveur.accept()
                print(f"[OK] Client connecté: {adresse}")
                
                # Gérer le client...
                client.close()
            
            except socket.timeout:
                print("[ALARM_CLOCK] Timeout! Aucun client pendant 10s")
                # Continue la boucle
    
    except KeyboardInterrupt:
        print("\n[ATTENTION]  Arrêt...")
    
    finally:
        serveur.close()


# === TIMEOUT INFINI vs 0 vs N SECONDES ===

# Timeout infini (défaut)
sock.settimeout(None)  # ou ne pas appeler settimeout()
# recv() bloque jusqu'à recevoir des données

# Timeout 0 (non-bloquant)
sock.settimeout(0.0)
# recv() retourne immédiatement (ou lève BlockingIOError)

# Timeout N secondes
sock.settimeout(5.0)
# recv() attend max 5 secondes


# === OBTENIR LE TIMEOUT ACTUEL ===

sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(5.0)

timeout = sock.gettimeout()
print(f"Timeout actuel: {timeout} secondes")
# Affiche: Timeout actuel: 5.0 secondes


[OK] MODE NON-BLOQUANT (AVANCÉ)

# === BLOQUANT vs NON-BLOQUANT ===

# Mode BLOQUANT (défaut):
# - recv() attend jusqu'à recevoir des données
# - send() attend jusqu'à ce que toutes les données soient envoyées
# - accept() attend jusqu'à ce qu'un client se connecte

# Mode NON-BLOQUANT:
# - Les fonctions retournent IMMÉDIATEMENT
# - Si pas de données: lève BlockingIOError
# - Permet de faire autre chose en attendant


# === ACTIVER LE MODE NON-BLOQUANT ===

import socket

sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

# Méthode 1: setblocking
sock.setblocking(False)

# Méthode 2: settimeout(0.0)
sock.settimeout(0.0)


# === EXEMPLE: SERVEUR NON-BLOQUANT ===

import socket
import time

def serveur_non_bloquant():
    serveur = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    serveur.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    serveur.bind(('0.0.0.0', 8080))
    serveur.listen(5)
    
    # Mode non-bloquant
    serveur.setblocking(False)
    
    print("[OK] Serveur non-bloquant en écoute\n")
    
    clients = []  # Liste des clients connectés
    
    try:
        while True:
            # === ACCEPTER DE NOUVEAUX CLIENTS ===
            try:
                client, adresse = serveur.accept()
                client.setblocking(False)
                clients.append((client, adresse))
                print(f"[OK] Nouveau client: {adresse}")
            
            except BlockingIOError:
                # Pas de nouveau client, c'est normal
                pass
            
            
            # === RECEVOIR DES DONNÉES DES CLIENTS EXISTANTS ===
            for client, adresse in clients[:]:  # Copie de la liste
                try:
                    data = client.recv(1024)
                    
                    if not data:
                        # Client a fermé
                        clients.remove((client, adresse))
                        client.close()
                        print(f"  Client déconnecté: {adresse}")
                    else:
                        message = data.decode('utf-8')
                        print(f"  [{adresse}] {message.strip()}")
                        
                        # Echo
                        client.send(data)
                
                except BlockingIOError:
                    # Pas de données pour ce client, continuer
                    pass
                
                except Exception as e:
                    print(f"  Erreur client {adresse}: {e}")
                    clients.remove((client, adresse))
                    client.close()
            
            
            # === FAIRE AUTRE CHOSE ===
            # Le serveur peut faire autre chose ici!
            # print("Travail en arrière-plan...")
            time.sleep(0.1)  # Petite pause pour ne pas saturer le CPU
    
    except KeyboardInterrupt:
        print("\n[ATTENTION]  Arrêt...")
    
    finally:
        for client, _ in clients:
            client.close()
        serveur.close()


# === AVANTAGES MODE NON-BLOQUANT ===

# [OK] Gérer plusieurs clients sans threads
# [OK] Faire du travail en arrière-plan
# [OK] Plus efficace pour beaucoup de connexions

# === INCONVÉNIENTS ===

# [X] Code plus complexe
# [X] Boucle infinie qui consomme du CPU
# [X] Gestion manuelle des erreurs BlockingIOError


[OK] SELECT (MULTIPLEXAGE I/O)

# === PROBLÈME AVEC LE MODE NON-BLOQUANT ===

# Le serveur non-bloquant fait une boucle infinie
# = Consomme 100% du CPU même si rien ne se passe!

# SOLUTION: select()
# = Attend passivement jusqu'à ce qu'un socket soit prêt
# = Ne consomme pas de CPU en attendant


# === COMMENT MARCHE SELECT? ===

# select() surveille plusieurs sockets
# Retourne quand AU MOINS UN socket est prêt:
# - Prêt à lire (données disponibles)
# - Prêt à écrire (peut envoyer des données)
# - A une erreur


# === SYNTAXE SELECT ===

import select

readable, writable, exceptional = select.select(
    rlist,     # Liste des sockets à surveiller pour lecture
    wlist,     # Liste des sockets à surveiller pour écriture
    xlist,     # Liste des sockets à surveiller pour erreurs
    timeout    # Timeout en secondes (None = infini)
)

# Retourne:
# readable: Liste des sockets prêts à être lus
# writable: Liste des sockets prêts à être écrits
# exceptional: Liste des sockets avec erreurs


# === EXEMPLE: SERVEUR AVEC SELECT ===

import socket
import select

def serveur_avec_select():
    # Créer le socket serveur
    serveur = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    serveur.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    serveur.bind(('0.0.0.0', 8080))
    serveur.listen(5)
    
    print("[OK] Serveur avec select en écoute\n")
    
    # Liste de tous les sockets à surveiller
    sockets_list = [serveur]
    
    # Dictionnaire pour stocker les adresses des clients
    clients = {}
    
    try:
        while True:
            # === ATTENDRE QU'UN SOCKET SOIT PRÊT ===
            
            # select() bloque jusqu'à ce qu'un socket soit prêt
            readable, _, exceptional = select.select(
                sockets_list,  # Surveiller ces sockets pour lecture
                [],            # Pas de surveillance d'écriture
                sockets_list,  # Surveiller les erreurs
                None           # Pas de timeout (attendre indéfiniment)
            )
            
            
            # === TRAITER LES SOCKETS PRÊTS ===
            
            for sock in readable:
                # === NOUVEAU CLIENT ===
                if sock is serveur:
                    client, adresse = serveur.accept()
                    sockets_list.append(client)
                    clients[client] = adresse
                    print(f"[OK] Nouveau client: {adresse}")
                
                # === DONNÉES D'UN CLIENT EXISTANT ===
                else:
                    try:
                        data = sock.recv(1024)
                        
                        if not data:
                            # Client a fermé
                            print(f"  Client déconnecté: {clients[sock]}")
                            sockets_list.remove(sock)
                            del clients[sock]
                            sock.close()
                        else:
                            # Echo
                            message = data.decode('utf-8')
                            print(f"  [{clients[sock]}] {message.strip()}")
                            sock.send(data)
                    
                    except Exception as e:
                        print(f"  Erreur: {e}")
                        sockets_list.remove(sock)
                        del clients[sock]
                        sock.close()
            
            
            # === TRAITER LES ERREURS ===
            
            for sock in exceptional:
                print(f"  Erreur sur socket: {clients.get(sock, 'inconnu')}")
                sockets_list.remove(sock)
                if sock in clients:
                    del clients[sock]
                sock.close()
    
    except KeyboardInterrupt:
        print("\n[ATTENTION]  Arrêt...")
    
    finally:
        for sock in sockets_list:
            sock.close()


# === AVANTAGES DE SELECT ===

# [OK] Pas de threads nécessaires
# [OK] Ne consomme pas de CPU en attente
# [OK] Gère facilement beaucoup de connexions
# [OK] Code plus simple que non-bloquant pur

# === INCONVÉNIENTS ===

# [X] Limité à 1024 sockets sur certains systèmes
# [X] Plus lent que epoll/kqueue pour beaucoup de sockets


[OK] TRANSFERT DE FICHIERS

# === ENVOYER UN FICHIER (CLIENT -> SERVEUR) ===

import socket
import os

def client_envoyer_fichier(fichier_path):
    """
    Envoie un fichier au serveur
    """
    
    # Vérifier que le fichier existe
    if not os.path.exists(fichier_path):
        print(f"[X] Fichier introuvable: {fichier_path}")
        return
    
    # Connexion au serveur
    client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    client.connect(('127.0.0.1', 8080))
    
    print(f"[OK] Connecté au serveur")
    
    # Obtenir le nom et la taille du fichier
    nom_fichier = os.path.basename(fichier_path)
    taille_fichier = os.path.getsize(fichier_path)
    
    print(f"[FICHIER] Fichier: {nom_fichier}")
    print(f"[PACKAGE] Taille: {taille_fichier} octets")
    
    # === PROTOCOLE D'ENVOI ===
    
    # 1. Envoyer le nom du fichier (encodé en bytes)
    client.send(nom_fichier.encode('utf-8'))
    client.recv(1024)  # Attendre l'accusé de réception
    
    # 2. Envoyer la taille du fichier
    client.send(str(taille_fichier).encode('utf-8'))
    client.recv(1024)  # Attendre l'accusé
    
    # 3. Envoyer le contenu du fichier
    with open(fichier_path, 'rb') as f:
        envoyé = 0
        
        while True:
            # Lire un bloc de 4KB
            chunk = f.read(4096)
            
            if not chunk:
                break
            
            # Envoyer le bloc
            client.send(chunk)
            envoyé += len(chunk)
            
            # Afficher la progression
            pourcentage = (envoyé / taille_fichier) * 100
            print(f"  Envoi: {pourcentage:.1f}%", end='\r')
    
    print(f"\n[OK] Fichier envoyé: {envoyé} octets")
    
    client.close()


# === RECEVOIR UN FICHIER (SERVEUR) ===

import socket
import os

def serveur_recevoir_fichier():
    """
    Reçoit un fichier du client
    """
    
    serveur = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    serveur.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    serveur.bind(('0.0.0.0', 8080))
    serveur.listen(1)
    
    print("[OK] Serveur en écoute pour recevoir des fichiers\n")
    
    try:
        while True:
            client, adresse = serveur.accept()
            print(f"[OK] Client connecté: {adresse}")
            
            try:
                # === PROTOCOLE DE RÉCEPTION ===
                
                # 1. Recevoir le nom du fichier
                nom_fichier = client.recv(1024).decode('utf-8')
                client.send(b"OK")  # Accusé de réception
                print(f"[FICHIER] Nom: {nom_fichier}")
                
                # 2. Recevoir la taille
                taille_fichier = int(client.recv(1024).decode('utf-8'))
                client.send(b"OK")  # Accusé
                print(f"[PACKAGE] Taille: {taille_fichier} octets")
                
                # 3. Recevoir le contenu
                dossier_reception = "fichiers_recus"
                os.makedirs(dossier_reception, exist_ok=True)
                
                chemin_fichier = os.path.join(dossier_reception, nom_fichier)
                
                with open(chemin_fichier, 'wb') as f:
                    reçu = 0
                    
                    while reçu < taille_fichier:
                        # Recevoir un bloc
                        chunk = client.recv(4096)
                        
                        if not chunk:
                            break
                        
                        f.write(chunk)
                        reçu += len(chunk)
                        
                        # Afficher la progression
                        pourcentage = (reçu / taille_fichier) * 100
                        print(f"  Réception: {pourcentage:.1f}%", end='\r')
                
                print(f"\n[OK] Fichier reçu: {chemin_fichier}")
            
            except Exception as e:
                print(f"[X] Erreur: {e}")
            
            finally:
                client.close()
    
    except KeyboardInterrupt:
        print("\n[ATTENTION]  Arrêt...")
    
    finally:
        serveur.close()


# === UTILISATION ===

# Terminal 1 (Serveur):
# $ python serveur_fichier.py
# [OK] Serveur en écoute pour recevoir des fichiers

# Terminal 2 (Client):
# $ python client_fichier.py
# [OK] Connecté au serveur
# [FICHIER] Fichier: image.jpg
# [PACKAGE] Taille: 2048000 octets
#   Envoi: 100.0%
# [OK] Fichier envoyé: 2048000 octets


[OK] CHAT CLIENT-SERVEUR COMPLET

# === SERVEUR DE CHAT ===

import socket
import threading

class ChatServeur:
    def __init__(self, host='0.0.0.0', port=9999):
        self.host = host
        self.port = port
        self.serveur = None
        self.clients = []  # Liste des (socket, adresse, pseudo)
        self.running = True
    
    def broadcast(self, message, expediteur=None):
        """
        Envoie un message à tous les clients sauf l'expéditeur
        """
        for client_sock, _, _ in self.clients:
            if client_sock != expediteur:
                try:
                    client_sock.send(message.encode('utf-8'))
                except:
                    pass
    
    def gerer_client(self, client_sock, adresse):
        """
        Gère un client spécifique
        """
        pseudo = None
        
        try:
            # Demander le pseudo
            client_sock.send(b"Entrez votre pseudo: ")
            pseudo = client_sock.recv(1024).decode('utf-8').strip()
            
            # Ajouter à la liste des clients
            self.clients.append((client_sock, adresse, pseudo))
            
            # Annoncer l'arrivée
            message_arrivee = f"\n[SERVEUR] {pseudo} a rejoint le chat!\n"
            print(message_arrivee.strip())
            self.broadcast(message_arrivee, client_sock)
            
            # Envoyer le message de bienvenue
            bienvenue = f"\nBienvenue {pseudo}! {len(self.clients)} utilisateur(s) connecté(s).\n"
            client_sock.send(bienvenue.encode('utf-8'))
            
            # Boucle de réception des messages
            while self.running:
                data = client_sock.recv(1024)
                
                if not data:
                    break
                
                message = data.decode('utf-8').strip()
                
                if message:
                    # Formater et broadcaster le message
                    message_complet = f"[{pseudo}] {message}\n"
                    print(message_complet.strip())
                    self.broadcast(message_complet, client_sock)
        
        except Exception as e:
            print(f"[ERREUR] {adresse}: {e}")
        
        finally:
            # Retirer le client
            for item in self.clients[:]:
                if item[0] == client_sock:
                    self.clients.remove(item)
                    break
            
            # Annoncer le départ
            if pseudo:
                message_depart = f"\n[SERVEUR] {pseudo} a quitté le chat.\n"
                print(message_depart.strip())
                self.broadcast(message_depart)
            
            client_sock.close()
    
    def demarrer(self):
        """
        Démarre le serveur de chat
        """
        self.serveur = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self.serveur.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        self.serveur.bind((self.host, self.port))
        self.serveur.listen(5)
        
        print(f"[OK] Serveur de chat démarré sur {self.host}:{self.port}")
        print("  En attente de connexions...\n")
        
        try:
            while self.running:
                client_sock, adresse = self.serveur.accept()
                print(f"[OK] Nouvelle connexion: {adresse}")
                
                # Créer un thread pour ce client
                thread = threading.Thread(
                    target=self.gerer_client,
                    args=(client_sock, adresse),
                    daemon=True
                )
                thread.start()
        
        except KeyboardInterrupt:
            print("\n[ATTENTION]  Arrêt du serveur...")
        
        finally:
            self.running = False
            for client_sock, _, _ in self.clients:
                client_sock.close()
            self.serveur.close()
            print("[OK] Serveur fermé")


if __name__ == "__main__":
    serveur = ChatServeur()
    serveur.demarrer()


# === CLIENT DE CHAT ===

import socket
import threading
import sys

class ChatClient:
    def __init__(self, host='127.0.0.1', port=9999):
        self.host = host
        self.port = port
        self.client = None
        self.running = True
    
    def recevoir(self):
        """
        Thread qui reçoit les messages du serveur
        """
        while self.running:
            try:
                message = self.client.recv(1024).decode('utf-8')
                
                if message:
                    # Afficher le message (avec retour à la ligne avant le prompt)
                    print(f"\r{message}", end='')
                    print("Vous> ", end='', flush=True)
                else:
                    break
            
            except Exception as e:
                if self.running:
                    print(f"\n[X] Erreur de réception: {e}")
                break
    
    def envoyer(self):
        """
        Thread qui envoie les messages au serveur
        """
        while self.running:
            try:
                message = input("Vous> ")
                
                if message.lower() == '/quit':
                    self.running = False
                    break
                
                if message:
                    self.client.send(message.encode('utf-8'))
            
            except KeyboardInterrupt:
                self.running = False
                break
            
            except Exception as e:
                print(f"[X] Erreur d'envoi: {e}")
                break
    
    def connecter(self):
        """
        Se connecte au serveur de chat
        """
        try:
            self.client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
            self.client.connect((self.host, self.port))
            
            print(f"[OK]Connecté au serveur {self.host}:{self.port}\n")
            
            # Thread pour recevoir les messages
            thread_reception = threading.Thread(target=self.recevoir, daemon=True)
            thread_reception.start()
            
            # Thread principal pour envoyer
            self.envoyer()
        
        except ConnectionRefusedError:
            print("[X] Connexion refusée. Le serveur est-il lancé?")
        
        except Exception as e:
            print(f"[X] Erreur: {e}")
        
        finally:
            self.running = False
            if self.client:
                self.client.close()
            print("\n[OK] Déconnecté")


if __name__ == "__main__":
    client = ChatClient()
    client.connecter()


# === UTILISATION ===

# Terminal 1 (Serveur):
# $ python chat_serveur.py
# [OK] Serveur de chat démarré sur 0.0.0.0:9999
#   En attente de connexions...
#
# [OK] Nouvelle connexion: ('127.0.0.1', 54321)
# [SERVEUR] Alice a rejoint le chat!
# [OK] Nouvelle connexion: ('127.0.0.1', 54322)
# [SERVEUR] Bob a rejoint le chat!
# [Alice] Salut tout le monde!
# [Bob] Hey Alice!

# Terminal 2 (Client Alice):
# $ python chat_client.py
# [OK] Connecté au serveur 127.0.0.1:9999
# Entrez votre pseudo: Alice
# Bienvenue Alice! 1 utilisateur(s) connecté(s).
# Vous> Salut tout le monde!
# [Bob] Hey Alice!
# Vous>

# Terminal 3 (Client Bob):
# $ python chat_client.py
# [OK] Connecté au serveur 127.0.0.1:9999
# Entrez votre pseudo: Bob
# Bienvenue Bob! 2 utilisateur(s) connecté(s).
# [Alice] Salut tout le monde!
# Vous> Hey Alice!


[OK] GESTION DES ERREURS COURANTES

# === ERREUR #1: "Address already in use" ===

# Erreur complète:
# OSError: [Errno 98] Address already in use

# Cause:
# Le port est déjà utilisé par un autre programme
# Ou le port n'est pas encore libéré après un crash

# Solutions:

# Solution 1: Utiliser SO_REUSEADDR (RECOMMANDÉ)
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sock.bind(('0.0.0.0', 8080))

# Solution 2: Attendre quelques minutes que le port se libère

# Solution 3: Trouver et tuer le processus qui utilise le port
# Linux/Mac:
# $ lsof -i :8080
# $ kill -9 <PID>

# Windows:
# > netstat -ano | findstr :8080
# > taskkill /PID <PID> /F

# Solution 4: Utiliser un port différent
sock.bind(('0.0.0.0', 8081))  # Au lieu de 8080


# === ERREUR #2: "Connection refused" ===

# Erreur complète:
# ConnectionRefusedError: [Errno 111] Connection refused

# Cause:
# Le serveur n'est pas lancé
# Ou le serveur écoute sur une adresse/port différent
# Ou un firewall bloque la connexion

# Solutions:

# Solution 1: Vérifier que le serveur est lancé
# Terminal 1: python serveur.py
# Terminal 2: python client.py

# Solution 2: Vérifier l'adresse et le port
# Serveur: bind(('0.0.0.0', 8080))
# Client: connect(('127.0.0.1', 8080))  # Même port!

# Solution 3: Vérifier le firewall
# Linux:
# $ sudo ufw allow 8080

# Windows:
# Panneau de configuration > Pare-feu > Autoriser une app


# === ERREUR #3: "Broken pipe" ===

# Erreur complète:
# BrokenPipeError: [Errno 32] Broken pipe

# Cause:
# Tu essaies d'envoyer des données alors que la connexion est fermée
# L'autre côté a fermé le socket

# Solutions:

# Solution 1: Vérifier que le socket est encore ouvert avant d'envoyer
try:
    sock.send(b"Hello")
except BrokenPipeError:
    print("Connexion fermée")
    sock.close()

# Solution 2: Utiliser select() pour vérifier si le socket est prêt
import select

readable, writable, exceptional = select.select([], [sock], [], 0)
if sock in writable:
    sock.send(b"Hello")
else:
    print("Socket pas prêt")


# === ERREUR #4: "socket.timeout" ===

# Erreur complète:
# socket.timeout: timed out

# Cause:
# Le timeout a expiré sans recevoir de données

# Solutions:

# Solution 1: Augmenter le timeout
sock.settimeout(10.0)  # Au lieu de 5.0

# Solution 2: Gérer l'exception
try:
    data = sock.recv(1024)
except socket.timeout:
    print("Timeout! Réessayer...")
    # Réessayer ou abandonner

# Solution 3: Utiliser un timeout infini
sock.settimeout(None)


# === ERREUR #5: "Permission denied" ===

# Erreur complète:
# PermissionError: [Errno 13] Permission denied

# Cause:
# Tu essaies d'utiliser un port privilégié (< 1024) sans être root

# Solutions:

# Solution 1: Utiliser un port > 1024
sock.bind(('0.0.0.0', 8080))  # Au lieu de 80

# Solution 2: Lancer avec sudo (Linux/Mac)
# $ sudo python serveur.py

# Solution 3: Donner les permissions nécessaires
# Linux:
# $ sudo setcap CAP_NET_BIND_SERVICE=+eip /usr/bin/python3


# === ERREUR #6: "Too many open files" ===

# Erreur complète:
# OSError: [Errno 24] Too many open files

# Cause:
# Tu as trop de sockets ouverts sans les fermer
# Limite système atteinte

# Solutions:

# Solution 1: Toujours fermer les sockets
client, addr = serveur.accept()
try:
    # Traiter le client
    pass
finally:
    client.close()  # IMPORTANT!

# Solution 2: Utiliser context manager
with socket.socket() as sock:
    # Utiliser sock
    pass  # Fermé automatiquement

# Solution 3: Augmenter la limite système (Linux/Mac)
# $ ulimit -n 4096


[OK] BONNES PRATIQUES

# === PRATIQUE #1: TOUJOURS UTILISER SO_REUSEADDR ===

# [OK] BON
serveur = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
serveur.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
serveur.bind(('0.0.0.0', 8080))

# [X] MAUVAIS
serveur = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
serveur.bind(('0.0.0.0', 8080))  # Peut échouer après un crash


# === PRATIQUE #2: TOUJOURS FERMER LES SOCKETS ===

# [OK] BON - Avec context manager
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
    sock.connect(('127.0.0.1', 8080))
    sock.send(b"Hello")
# Fermé automatiquement

# [OK] BON - Avec try/finally
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
try:
    sock.connect(('127.0.0.1', 8080))
    sock.send(b"Hello")
finally:
    sock.close()  # Toujours exécuté

# [X] MAUVAIS
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect(('127.0.0.1', 8080))
sock.send(b"Hello")
# Pas de close()! Fuite de ressource!


# === PRATIQUE #3: GÉRER LES EXCEPTIONS ===

# [OK] BON
try:
    sock.connect(('192.168.1.100', 8080))
except ConnectionRefusedError:
    print("Serveur inaccessible")
except socket.timeout:
    print("Timeout")
except Exception as e:
    print(f"Erreur: {e}")

# [X] MAUVAIS
sock.connect(('192.168.1.100', 8080))
# Pas de gestion d'erreur! Le programme crashe


# === PRATIQUE #4: UTILISER DES TIMEOUTS ===

# [OK] BON
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(5.0)  # 5 secondes max
try:
    sock.connect(('192.168.1.100', 8080))
except socket.timeout:
    print("Timeout de connexion")

# [X] MAUVAIS
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect(('192.168.1.100', 8080))
# Peut bloquer indéfiniment!


# === PRATIQUE #5: ENCODER/DÉCODER CORRECTEMENT ===

# [OK] BON
message = "Héllo Wörld"
sock.send(message.encode('utf-8'))  # String -> bytes

data = sock.recv(1024)
message = data.decode('utf-8')  # Bytes -> string

# [X] MAUVAIS
sock.send("Hello")  # TypeError! send() veut des bytes


# === PRATIQUE #6: GÉRER LES CONNEXIONS FERMÉES ===

# [OK] BON
data = sock.recv(1024)
if not data:
    print("Connexion fermée")
    sock.close()
else:
    # Traiter les données
    pass

# [X] MAUVAIS
data = sock.recv(1024)
message = data.decode('utf-8')  # Peut crasher si data est vide!


# === PRATIQUE #7: UTILISER DES THREADS POUR SERVEUR MULTI-CLIENT ===

# [OK] BON
def gerer_client(client_sock, addr):
    # Gérer ce client
    pass

while True:
    client, addr = serveur.accept()
    thread = threading.Thread(target=gerer_client, args=(client, addr), daemon=True)
    thread.start()

# [X] MAUVAIS
while True:
    client, addr = serveur.accept()
    # Gérer le client ici
    # = Les autres clients doivent attendre!


# === PRATIQUE #8: VALIDER LES DONNÉES REÇUES ===

# [OK] BON
data = sock.recv(1024)
try:
    message = data.decode('utf-8')
    if len(message) > 1000:
        print("Message trop long")
    elif not message.strip():
        print("Message vide")
    else:
        # Traiter le message
        pass
except UnicodeDecodeError:
    print("Données invalides")

# [X] MAUVAIS
data = sock.recv(1024)
message = data.decode('utf-8')
# Pas de validation! Peut crasher ou avoir des données malicieuses


# === PRATIQUE #9: LOGGER LES ÉVÉNEMENTS ===

# [OK] BON
import logging

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

logger.info(f"Client connecté: {adresse}")
logger.error(f"Erreur de connexion: {e}")

# [X] MAUVAIS
print(f"Client connecté: {adresse}")
# Difficile à déboguer en production


# === PRATIQUE #10: NE PAS BLOQUER LE THREAD PRINCIPAL ===

# [OK] BON
# Utiliser des threads pour les tâches longues
thread = threading.Thread(target=longue_tache)
thread.start()

# Ou utiliser select() pour le multiplexage
readable, _, _ = select.select([sock], [], [], timeout)

# [X] MAUVAIS
while True:
    data = sock.recv(1024)  # Bloque indéfiniment
    # L'application entière est bloquée!


[OK] EXEMPLES AVANCÉS

# === EXEMPLE #1: SERVEUR HTTP MINIMALISTE ===

import socket

def serveur_http_simple():
    """
    Serveur HTTP minimaliste qui répond "Hello World"
    """
    serveur = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    serveur.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    serveur.bind(('0.0.0.0', 8080))
    serveur.listen(5)
    
    print("[OK] Serveur HTTP sur http://localhost:8080\n")
    
    try:
        while True:
            client, addr = serveur.accept()
            
            # Recevoir la requête HTTP
            requete = client.recv(1024).decode('utf-8')
            print(f"Requête de {addr}:")
            print(requete[:100])  # Afficher les 100 premiers caractères
            
            # Créer la réponse HTTP
            reponse_http = """HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8

<!DOCTYPE html>
<html>
<head>
    <title>Hello World</title>
</head>
<body>
    <h1>Hello World!</h1>
    <p>Ceci est un serveur HTTP minimaliste en Python.</p>
</body>
</html>
"""
            
            # Envoyer la réponse
            client.send(reponse_http.encode('utf-8'))
            client.close()
    
    except KeyboardInterrupt:
        print("\n[ATTENTION]  Arrêt...")
    finally:
        serveur.close()


# === EXEMPLE #2: PORT SCANNER ===

import socket
from concurrent.futures import ThreadPoolExecutor

def scanner_port(host, port, timeout=1):
    """
    Teste si un port est ouvert
    """
    try:
        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        sock.settimeout(timeout)
        resultat = sock.connect_ex((host, port))
        sock.close()
        
        if resultat == 0:
            return port, True
        else:
            return port, False
    except:
        return port, False


def scanner_ports(host, ports):
    """
    Scanne plusieurs ports en parallèle
    """
    print(f"[RECHERCHE] Scan de {host}...\n")
    
    # Utiliser ThreadPoolExecutor pour paralléliser
    with ThreadPoolExecutor(max_workers=50) as executor:
        resultats = executor.map(lambda p: scanner_port(host, p), ports)
    
    # Afficher les résultats
    ports_ouverts = []
    for port, ouvert in resultats:
        if ouvert:
            ports_ouverts.append(port)
            print(f"[OK] Port {port}: OUVERT")
    
    print(f"\n[GRAPHIQUE] {len(ports_ouverts)} port(s) ouvert(s) sur {len(ports)} testé(s)")
    
    return ports_ouverts


# Utilisation
if __name__ == "__main__":
    # Scanner les ports communs
    ports_communs = [21, 22, 23, 25, 80, 443, 3306, 5432, 8080, 8443]
    scanner_ports('127.0.0.1', ports_communs)


# === EXEMPLE #3: PROXY TCP SIMPLE ===

import socket
import threading

def proxy_tcp(host_source, port_source, host_dest, port_dest):
    """
    Proxy TCP qui redirige les connexions
    """
    
    def transferer(sock_source, sock_dest, direction):
        """
        Transfère les données d'un socket à l'autre
        """
        try:
            while True:
                data = sock_source.recv(4096)
                if not data:
                    break
                sock_dest.send(data)
                print(f"{direction}: {len(data)} octets")
        except Exception as e:
            print(f"Erreur transfert: {e}")
        finally:
            sock_source.close()
            sock_dest.close()
    
    # Créer le serveur proxy
    serveur = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    serveur.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    serveur.bind((host_source, port_source))
    serveur.listen(5)
    
    print(f"[OK] Proxy {host_source}:{port_source} -> {host_dest}:{port_dest}\n")
    
    try:
        while True:
            client, addr = serveur.accept()
            print(f"[OK] Nouvelle connexion: {addr}")
            
            # Se connecter à la destination
            destination = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
            destination.connect((host_dest, port_dest))
            
            # Threads pour transférer dans les deux sens
            thread_client_dest = threading.Thread(
                target=transferer,
                args=(client, destination, "Client->Dest"),
                daemon=True
            )
            thread_dest_client = threading.Thread(
                target=transferer,
                args=(destination, client, "Dest->Client"),
                daemon=True
            )
            
            thread_client_dest.start()
            thread_dest_client.start()
    
    except KeyboardInterrupt:
        print("\n[ATTENTION]  Arrêt...")
    finally:
        serveur.close()


# Utilisation:
# Rediriger localhost:8000 -> google.com:80
# proxy_tcp('127.0.0.1', 8000, 'google.com', 80)


# === EXEMPLE #4: HEARTBEAT (VÉRIFICATION DE CONNEXION) ===

import socket
import time
import threading

class ClientAvecHeartbeat:
    def __init__(self, host, port):
        self.host = host
        self.port = port
        self.sock = None
        self.running = True
        self.derniere_reponse = time.time()
    
    def heartbeat(self):
        """
        Envoie périodiquement un ping au serveur
        """
        while self.running:
            try:
                # Envoyer un ping toutes les 5 secondes
                time.sleep(5)
                self.sock.send(b"PING")
                print("[BEATING_HEART] Ping envoyé")
                
                # Vérifier si on a reçu une réponse récemment
                if time.time() - self.derniere_reponse > 15:
                    print("[X] Pas de réponse depuis 15s! Reconnexion...")
                    self.reconnecter()
            
            except Exception as e:
                print(f"[X] Erreur heartbeat: {e}")
                self.reconnecter()
    
    def recevoir(self):
        """
        Reçoit les messages du serveur
        """
        while self.running:
            try:
                data = self.sock.recv(1024)
                
                if not data:
                    print("[X] Serveur déconnecté")
                    break
                
                message = data.decode('utf-8')
                
                if message == "PONG":
                    self.derniere_reponse = time.time()
                    print("[BEATING_HEART] Pong reçu")
                else:
                    print(f"[MESSAGE] {message}")
            
            except Exception as e:
                print(f"[X] Erreur réception: {e}")
                break
    
    def reconnecter(self):
        """
        Tente de se reconnecter au serveur
        """
        print("[SYNC] Reconnexion...")
        
        if self.sock:
            self.sock.close()
        
        tentative = 0
        while self.running and tentative < 5:
            try:
                self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
                self.sock.connect((self.host, self.port))
                self.derniere_reponse = time.time()
                print("[OK] Reconnecté!")
                return
            
            except Exception as e:
                tentative += 1
                print(f"[X] Tentative {tentative}/5 échouée: {e}")
                time.sleep(2)
        
        print("[X] Impossible de se reconnecter")
        self.running = False
    
    def connecter(self):
        """
        Connexion initiale
        """
        try:
            self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
            self.sock.connect((self.host, self.port))
            print(f"[OK] Connecté à {self.host}:{self.port}\n")
            
            # Lancer les threads
            thread_heartbeat = threading.Thread(target=self.heartbeat, daemon=True)
            thread_reception = threading.Thread(target=self.recevoir, daemon=True)
            
            thread_heartbeat.start()
            thread_reception.start()
            
            # Garder le programme actif
            while self.running:
                time.sleep(1)
        
        except Exception as e:
            print(f"[X] Erreur: {e}")
        
        finally:
            self.running = False
            if self.sock:
                self.sock.close()


[OK] SÉCURITÉ DES SOCKETS

# === DANGER #1: DÉNI DE SERVICE (DoS) ===

# Problème: Un client malveillant peut ouvrir beaucoup de connexions

# Protection:

def serveur_avec_limite_connexions():
    serveur = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    serveur.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    serveur.bind(('0.0.0.0', 8080))
    serveur.listen(5)
    
    connexions_par_ip = {}
    MAX_CONNEXIONS_PAR_IP = 3
    
    while True:
        client, addr = serveur.accept()
        ip = addr[0]
        
        # Compter les connexions de cette IP
        if ip not in connexions_par_ip:
            connexions_par_ip[ip] = 0
        
        connexions_par_ip[ip] += 1
        
        # Vérifier la limite
        if connexions_par_ip[ip] > MAX_CONNEXIONS_PAR_IP:
            print(f"[X] Trop de connexions depuis {ip}")
            client.close()
            continue
        
        # Gérer le client...


# === DANGER #2: BUFFER OVERFLOW ===

# Problème: Recevoir trop de données peut saturer la mémoire

# [X] MAUVAIS
data = b""
while True:
    chunk = sock.recv(4096)
    if not chunk:
        break
    data += chunk  # Peut devenir ÉNORME!

# [OK] BON
MAX_DATA_SIZE = 1024 * 1024  # 1MB max
data = b""
while len(data) < MAX_DATA_SIZE:
    chunk = sock.recv(4096)
    if not chunk:
        break
    data += chunk
    
    if len(data) > MAX_DATA_SIZE:
        print("[X] Trop de données!")
        break


# === DANGER #3: INJECTION DE COMMANDES ===

# Problème: Un client malveillant envoie des commandes dangereuses

# [X] MAUVAIS
data = sock.recv(1024)
commande = data.decode('utf-8')
os.system(commande)  # TRÈS DANGEREUX!

# [OK] BON
data = sock.recv(1024)
commande = data.decode('utf-8').strip()

# Liste blanche de commandes autorisées
commandes_valides = ['status', 'ping', 'info']

if commande in commandes_valides:
    # Exécuter la commande sécurisée
    pass
else:
    print(f"[X] Commande invalide: {commande}")


# === DANGER #4: PAS DE CHIFFREMENT ===

# Problème: Les données sont envoyées en clair sur le réseau

# Solution: Utiliser SSL/TLS (voir section suivante)


[OK] SSL/TLS (SOCKETS SÉCURISÉS)

# === POURQUOI SSL/TLS? ===

# Socket normal:
# - Données en clair
# - N'importe qui peut les lire (sniffing)
# - Pas d'authentification

# Socket SSL/TLS:
# - Données chiffrées
# - Impossible de les lire sans la clé
# - Authentification du serveur (et optionnellement du client)


# === CRÉER UN SOCKET SSL (CLIENT) ===

import socket
import ssl

def client_ssl():
    # Créer un socket normal
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    
    # Créer un contexte SSL
    context = ssl.create_default_context()
    
    # Wrapper le socket avec SSL
    sock_ssl = context.wrap_socket(sock, server_hostname='www.google.com')
    
    # Se connecter (connexion sécurisée)
    sock_ssl.connect(('www.google.com', 443))
    
    print(f"[OK] Connexion SSL établie")
    print(f"  Version: {sock_ssl.version()}")
    print(f"  Cipher: {sock_ssl.cipher()}")
    
    # Envoyer une requête HTTPS
    requete = b"GET / HTTP/1.1\r\nHost: www.google.com\r\n\r\n"
    sock_ssl.send(requete)
    
    # Recevoir la réponse
    reponse = sock_ssl.recv(4096)
    print(f"\n[MESSAGE] Réponse ({len(reponse)} octets):")
    print(reponse[:200].decode('utf-8', errors='ignore'))
    
    sock_ssl.close()


# === CRÉER UN SERVEUR SSL ===

import socket
import ssl

def serveur_ssl():
    # Créer un socket normal
    serveur = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    serveur.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    serveur.bind(('0.0.0.0', 8443))
    serveur.listen(5)
    
    # Créer un contexte SSL
    context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
    
    # Charger le certificat et la clé privée
    # (Tu dois générer ces fichiers d'abord)
    context.load_cert_chain(certfile='server.crt', keyfile='server.key')
    
    print("[OK] Serveur SSL en écoute sur port 8443\n")
    
    while True:
        client, addr = serveur.accept()
        
        # Wrapper avec SSL
        client_ssl = context.wrap_socket(client, server_side=True)
        
        print(f"[OK] Client SSL connecté: {addr}")
        
        # Utiliser client_ssl comme un socket normal
        data = client_ssl.recv(1024)
        print(f"[MESSAGE] Reçu: {data.decode('utf-8')}")
        
        client_ssl.send(b"Echo SSL: " + data)
        client_ssl.close()


# === GÉNÉRER UN CERTIFICAT AUTO-SIGNÉ (POUR TEST) ===

# Ouvre un terminal et exécute:
# $ openssl req -x509 -newkey rsa:4096 -keyout server.key -out server.crt -days 365 -nodes

# Cela crée:
# - server.key: Clé privée
# - server.crt: Certificat (valide 365 jours)

# NOTE: Pour la production, utilise un certificat d'une autorité de certification (Let's Encrypt, etc)


[OK] SOCKET UNIX (COMMUNICATION LOCALE)

# === QU'EST-CE QU'UN SOCKET UNIX? ===

# Socket Unix = Socket pour communication inter-processus (IPC) LOCALE
# = Plus rapide que TCP car pas de stack réseau
# = Utilisé pour communiquer entre programmes sur la MÊME machine

# Exemples d'usage:
# - Docker communique avec le daemon via socket Unix
# - PostgreSQL propose une connexion via socket Unix
# - Systemd utilise des sockets Unix


# === SERVEUR SOCKET UNIX ===

import socket
import os

def serveur_unix():
    # Chemin du socket Unix
    socket_path = '/tmp/mon_socket.sock'
    
    # Supprimer le socket s'il existe déjà
    if os.path.exists(socket_path):
        os.remove(socket_path)
    
    # Créer un socket Unix
    serveur = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
    
    # Binder au chemin
    serveur.bind(socket_path)
    serveur.listen(5)
    
    print(f"[OK] Serveur Unix socket: {socket_path}\n")
    
    try:
        while True:
            client, _ = serveur.accept()
            print("[OK] Client connecté")
            
            data = client.recv(1024)
            print(f"[MESSAGE] Reçu: {data.decode('utf-8')}")
            
            client.send(b"Echo: " + data)
            client.close()
    
    except KeyboardInterrupt:
        print("\n[ATTENTION]  Arrêt...")
    
    finally:
        serveur.close()
        os.remove(socket_path)


# === CLIENT SOCKET UNIX ===

import socket

def client_unix():
    socket_path = '/tmp/mon_socket.sock'
    
    client = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
    client.connect(socket_path)
    
    print(f"[OK] Connecté au socket {socket_path}")
    
    client.send(b"Hello Unix Socket!")
    data = client.recv(1024)
    print(f"[MESSAGE] Reçu: {data.decode('utf-8')}")
    
    client.close()


# === AVANTAGES SOCKET UNIX ===

# [OK] Plus rapide que TCP (pas de stack réseau)
# [OK] Sécurité: Seulement local (pas accessible depuis le réseau)
# [OK] Permissions filesystem (chmod, chown)

# === INCONVÉNIENTS ===

# [X] Seulement sur la même machine
# [X] Pas disponible sur Windows (AF_UNIX non supporté avant Python 3.9)


[OK] PERFORMANCES & OPTIMISATION

# === ASTUCE #1: RÉUTILISER LES SOCKETS ===

# [X] MAUVAIS
for i in range(1000):
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.connect(('127.0.0.1', 8080))
    sock.send(b"Hello")
    sock.close()
    # Créer/fermer 1000 sockets = très lent!

# [OK] BON
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect(('127.0.0.1', 8080))
for i in range(1000):
    sock.send(b"Hello")
    # Réutiliser le même socket = rapide!
sock.close()


# === ASTUCE #2: AUGMENTER LES BUFFERS ===

sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

# Buffers par défaut: ~8KB
# Pour gros transferts, augmenter!
sock.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, 1024 * 1024)  # 1MB
sock.setsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF, 1024 * 1024)  # 1MB


# === ASTUCE #3: DÉSACTIVER NAGLE POUR LATENCE ===

# Pour applications temps réel (jeux, chat)
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)


# === ASTUCE #4: UTILISER sendall() ===

# [X] MAUVAIS
data = b"A" * 10000
sent = sock.send(data)
# send() peut ne pas tout envoyer!
# sent peut être < len(data)

# [OK] BON
data = b"A" * 10000
sock.sendall(data)
# sendall() boucle jusqu'à tout envoyer


# === ASTUCE #5: RECV EN BOUCLE POUR GROS MESSAGES ===

def recevoir_exactement(sock, n_octets):
    """
    Reçoit exactement n_octets
    """
    data = b""
    while len(data) < n_octets:
        chunk = sock.recv(n_octets - len(data))
        if not chunk:
            raise ConnectionError("Connexion fermée")
        data += chunk
    return data


# Utilisation:
# Envoyer la taille puis les données
taille = 1000000
sock.sendall(str(taille).zfill(10).encode())  # "0001000000"
sock.sendall(b"A" * taille)

# Recevoir la taille puis les données
taille_str = recevoir_exactement(sock, 10)
taille = int(taille_str.decode())
data = recevoir_exactement(sock, taille)


[OK] DEBUGGING & MONITORING

# === AFFICHER LES INFORMATIONS D'UN SOCKET ===

import socket

sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.bind(('127.0.0.1', 8080))

# Informations générales
print(f"Famille: {sock.family}")          # AddressFamily.AF_INET
print(f"Type: {sock.type}")               # SocketKind.SOCK_STREAM
print(f"Protocole: {sock.proto}")         # 0
print(f"FD: {sock.fileno()}")             # 3 (file descriptor)
print(f"Timeout: {sock.gettimeout()}")    # None
print(f"Blocking: {sock.getblocking()}")  # True

# Adresse locale (après bind)
print(f"Adresse locale: {sock.getsockname()}")  # ('127.0.0.1', 8080)

# Adresse distante (après connect - pour client)
# print(f"Adresse distante: {sock.getpeername()}")


# === LOGGER TOUS LES ÉVÉNEMENTS ===

import socket
import logging

logging.basicConfig(
    level=logging.DEBUG,
    format='%(asctime)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)

def serveur_avec_logs():
    serveur = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    serveur.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    
    logger.info("Socket créé")
    
    serveur.bind(('0.0.0.0', 8080))
    logger.info("Bind sur 0.0.0.0:8080")
    
    serveur.listen(5)
    logger.info("En écoute (backlog=5)")
    
    while True:
        logger.debug("En attente de connexion...")
        client, addr = serveur.accept()
        logger.info(f"Client connecté: {addr}")
        
        try:
            data = client.recv(1024)
            logger.debug(f"Reçu {len(data)} octets de {addr}")
            
            client.send(data)
            logger.debug(f"Envoyé {len(data)} octets à {addr}")
        
        except Exception as e:
            logger.error(f"Erreur avec {addr}: {e}")
        
        finally:
            client.close()
            logger.info(f"Connexion fermée: {addr}")


# === MESURER LES PERFORMANCES ===

import socket
import time

def mesurer_debit(host, port, taille_data=1024*1024):
    """
    Mesure le débit d'envoi/réception
    """
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.connect((host, port))
    
    data = b"A" * taille_data
    
    # Mesurer l'envoi
    debut = time.time()
    sock.sendall(data)
    duree_envoi = time.time() - debut
    
    debit_envoi = (taille_data / duree_envoi) / (1024 * 1024)  # MB/s
    print(f"[SORTIE] Débit envoi: {debit_envoi:.2f} MB/s")
    
    # Mesurer la réception
    debut = time.time()
    recu = 0
    while recu < taille_data:
        chunk = sock.recv(4096)
        recu += len(chunk)
    duree_reception = time.time() - debut
    
    debit_reception = (taille_data / duree_reception) / (1024 * 1024)
    print(f"[ENTREE] Débit réception: {debit_reception:.2f} MB/s")
    
    sock.close()


[OK] RESSOURCES & LIENS

# Documentation officielle Python socket:
# https://docs.python.org/3/library/socket.html

# Guides spécifiques:
# TCP/IP: https://www.rfc-editor.org/rfc/rfc793
# UDP: https://www.rfc-editor.org/rfc/rfc768
# SSL/TLS: https://docs.python.org/3/library/ssl.html

# Tutoriels avancés:
# Socket Programming HOWTO: https://docs.python.org/3/howto/sockets.html
# Beej's Guide (en anglais): https://beej.us/guide/bgnet/

# Outils de debugging:
# - netcat (nc): Tester les connexions
# - tcpdump: Capturer le trafic réseau
# - wireshark: Analyser les paquets
# - netstat: Voir les connexions actives

# Alternatives haut niveau:
# - asyncio: Sockets asynchrones
# - twisted: Framework réseau complet
# - tornado: Serveur web asynchrone
# - Flask/Django: Frameworks web (cachent les sockets)


[OK] CHECKLISTE FINALE

# [OK] Créer un socket: socket.socket(AF_INET, SOCK_STREAM)
# [OK] Toujours utiliser SO_REUSEADDR pour les serveurs
# [OK] Toujours fermer les sockets (with ou try/finally)
# [OK] Gérer les exceptions (ConnectionRefusedError, timeout, etc)
# [OK] Utiliser des timeouts pour éviter les blocages infinis
# [OK] Encoder/décoder correctement (utf-8)
# [OK] Valider les données reçues
# [OK] Utiliser des threads pour serveur multi-client
# [OK] Logger les événements importants
# [OK] Sécuriser avec SSL/TLS en production
# [OK] Limiter les connexions par IP (protection DoS)
# [OK] Tester localement avant de déployer


[OK] RÉSUMÉ COMPLET

# SOCKET = Point de terminaison pour la communication réseau

# TYPES:
# - TCP (SOCK_STREAM): Fiable, connexion, ordonné
# - UDP (SOCK_DGRAM): Rapide, sans connexion, non garanti

# SERVEUR TCP:
# 1. socket() -> créer
# 2. bind() -> lier à une adresse
# 3. listen() -> écouter
# 4. accept() -> accepter les connexions
# 5. recv()/send() -> échanger des données
# 6. close() -> fermer

# CLIENT TCP:
# 1. socket() -> créer
# 2. connect() -> se connecter au serveur
# 3. send()/recv() -> échanger des données
# 4. close() -> fermer

# OPTIONS IMPORTANTES:
# - SO_REUSEADDR: Réutiliser l'adresse
# - TCP_NODELAY: Désactiver Nagle
# - SO_RCVBUF/SO_SNDBUF: Taille des buffers

# MODES:
# - Bloquant (défaut): attend jusqu'à succès
# - Non-bloquant: retourne immédiatement
# - Select: multiplexage I/O

# SÉCURITÉ:
# - SSL/TLS pour chiffrer
# - Valider les données
# - Limiter les connexions
# - Logger les événements

# C'EST FINI! Tu maîtrises maintenant les sockets Python! [BRAVO]
```