Sockets Python#

Les sockets sont l’interface universelle entre les applications et la pile réseau du système d’exploitation. Dérivée de l’API POSIX Berkeley Sockets (BSD 4.2, 1983), elle est disponible sur tous les systèmes modernes et constitue la fondation sur laquelle reposent toutes les bibliothèques réseau Python : requests, asyncio, websockets

Objectifs du chapitre

  • Comprendre l’API socket POSIX et ses principales primitives

  • Implémenter un serveur TCP multi-clients avec threading

  • Maîtriser le multiplexage I/O avec select / selectors

  • Écrire un serveur asynchrone avec asyncio

  • Gérer correctement les erreurs et options socket

Hide code cell source

import matplotlib
import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
from matplotlib.patches import FancyArrowPatch, FancyBboxPatch, FancyArrow
import numpy as np
import pandas as pd
import seaborn as sns

sns.set_theme(style="whitegrid", palette="muted")
plt.rcParams.update({
    "figure.dpi": 110,
    "axes.titlesize": 13,
    "axes.labelsize": 11,
    "font.family": "sans-serif",
})

L’API socket POSIX#

Primitives fondamentales#

Appel système

Rôle

socket(family, type, proto)

Crée un nouveau socket, retourne un descripteur de fichier

bind(sockaddr)

Associe le socket à une adresse locale (IP + port)

listen(backlog)

Met le socket en mode écoute passive (TCP serveur)

accept()

Bloque jusqu’à l’arrivée d’un client, retourne un nouveau socket

connect(sockaddr)

Initie la connexion TCP (ou fixe l’adresse distante pour UDP)

send(data) / sendto(data, addr)

Envoie des données

recv(bufsize) / recvfrom(bufsize)

Reçoit des données

close() / shutdown()

Ferme le socket (avec ou sans vidage du tampon)

Familles et types de sockets#

import socket

# Afficher les familles disponibles
familles = {
    "AF_INET": (socket.AF_INET, "IPv4 — adresse (host, port)"),
    "AF_INET6": (socket.AF_INET6, "IPv6 — adresse (host, port, flowinfo, scope_id)"),
    "AF_UNIX": (socket.AF_UNIX, "Unix domain sockets — chemin fichier local"),
}

types = {
    "SOCK_STREAM": (socket.SOCK_STREAM, "TCP — flux ordonné, fiable, orienté connexion"),
    "SOCK_DGRAM": (socket.SOCK_DGRAM, "UDP — datagrammes, sans connexion"),
    "SOCK_RAW": (socket.SOCK_RAW, "Accès brut à la couche IP (root requis)"),
}

print("=== Familles de sockets ===")
for nom, (val, desc) in familles.items():
    print(f"  {nom:<12} = {int(val):2d}{desc}")

print("\n=== Types de sockets ===")
for nom, (val, desc) in types.items():
    print(f"  {nom:<15} = {int(val):2d}{desc}")

# Informations sur le socket TCP local
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.bind(("127.0.0.1", 0))  # Port 0 = assigné automatiquement
addr = s.getsockname()
print(f"\nSocket TCP créé, adresse locale : {addr[0]}:{addr[1]}")
s.close()
print("Socket fermé proprement.")
=== Familles de sockets ===
  AF_INET      =  2  → IPv4 — adresse (host, port)
  AF_INET6     = 10  → IPv6 — adresse (host, port, flowinfo, scope_id)
  AF_UNIX      =  1  → Unix domain sockets — chemin fichier local

=== Types de sockets ===
  SOCK_STREAM     =  1  → TCP — flux ordonné, fiable, orienté connexion
  SOCK_DGRAM      =  2  → UDP — datagrammes, sans connexion
  SOCK_RAW        =  3  → Accès brut à la couche IP (root requis)

Socket TCP créé, adresse locale : 127.0.0.1:43263
Socket fermé proprement.
fig, ax = plt.subplots(figsize=(13, 7))
ax.set_xlim(0, 14)
ax.set_ylim(0, 8)
ax.axis("off")
ax.set_title("Diagramme de séquence TCP — socket API", fontsize=14, fontweight="bold")

def box(ax, x, y, w, h, text, color, fontsize=9):
    rect = FancyBboxPatch((x, y), w, h, boxstyle="round,pad=0.1",
                          linewidth=1.5, edgecolor="#555", facecolor=color, alpha=0.9)
    ax.add_patch(rect)
    ax.text(x + w/2, y + h/2, text, ha="center", va="center",
            fontsize=fontsize, fontweight="bold", color="white")

def arrow_h(ax, x1, x2, y, label, color="#555555", lw=1.5):
    ax.annotate("", xy=(x2, y), xytext=(x1, y),
                arrowprops=dict(arrowstyle="->", color=color, lw=lw))
    ax.text((x1+x2)/2, y + 0.15, label, ha="center", fontsize=8, color=color)

def label(ax, x, y, text, color="#333", fontsize=8.5):
    ax.text(x, y, text, ha="center", va="center", fontsize=fontsize,
            color=color, style="italic")

# Colonnes serveur / client
SERVER_X, CLIENT_X = 2, 11

# Titres
box(ax, 0.5, 7.2, 3, 0.6, "SERVEUR", "#2C3E50")
box(ax, 9.5, 7.2, 3, 0.6, "CLIENT", "#1A6B3C")

# Lignes verticales de vie
for x in [2, 11]:
    ax.plot([x, x], [0.2, 7.2], "--", color="#AAAAAA", lw=1)

# Étapes serveur
steps_server = [
    (6.8, "socket()"),
    (6.0, "bind('0.0.0.0', 8080)"),
    (5.2, "listen(128)"),
    (4.4, "accept()  ← bloque"),
    (2.6, "recv() / send()"),
    (1.4, "close()"),
]
for y, text in steps_server:
    box(ax, 0.5, y - 0.3, 3, 0.5, text, "#4C9BE8", fontsize=8.5)

# Étapes client
steps_client = [
    (6.8, "socket()"),
    (5.8, "connect('127.0.0.1', 8080)"),
    (3.8, "send(data)"),
    (2.6, "recv()"),
    (1.4, "close()"),
]
for y, text in steps_client:
    box(ax, 9.5, y - 0.3, 3, 0.5, text, "#54B87A", fontsize=8.5)

# Flèches de protocole
arrow_h(ax, CLIENT_X - 1.5, SERVER_X + 1.5, 5.7, "SYN", "#E87A4C", lw=2)
arrow_h(ax, SERVER_X + 1.5, CLIENT_X - 1.5, 5.0, "SYN-ACK", "#E87A4C", lw=2)
arrow_h(ax, CLIENT_X - 1.5, SERVER_X + 1.5, 4.3, "ACK", "#E87A4C", lw=2)
arrow_h(ax, CLIENT_X - 1.5, SERVER_X + 1.5, 3.7, "données", "#54B87A", lw=2)
arrow_h(ax, SERVER_X + 1.5, CLIENT_X - 1.5, 2.5, "réponse", "#4C9BE8", lw=2)
arrow_h(ax, CLIENT_X - 1.5, SERVER_X + 1.5, 1.8, "FIN", "#C96DD8", lw=1.5)
arrow_h(ax, SERVER_X + 1.5, CLIENT_X - 1.5, 1.1, "FIN-ACK", "#C96DD8", lw=1.5)

ax.text(7, 6.3, "Handshake TCP\n(3 voies)", ha="center", fontsize=9,
        color="#E87A4C", fontweight="bold",
        bbox=dict(boxstyle="round", facecolor="#FFF3E0", edgecolor="#E87A4C", alpha=0.8))

plt.tight_layout()
plt.show()
_images/7517f7aee582f8267686301ff7146e13639369fc20b611bedccf0b36e9a455e8.png

Serveur TCP simple#

import socket
import threading
import time

def simple_tcp_server(host="127.0.0.1", port=60001, max_clients=3):
    """Serveur TCP séquentiel (un seul client à la fois)."""
    server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    server.bind((host, port))
    server.listen(5)
    server.settimeout(4)
    print(f"[SERVER] En écoute sur {host}:{port}")

    clients_handled = 0
    while clients_handled < max_clients:
        try:
            conn, addr = server.accept()
            clients_handled += 1
            conn.settimeout(2)
            print(f"[SERVER] Connexion #{clients_handled} depuis {addr}")
            try:
                data = conn.recv(1024)
                if data:
                    response = f"OK:{data.decode()}"
                    conn.sendall(response.encode())
                    print(f"[SERVER] Reçu '{data.decode()}', renvoyé '{response}'")
            except socket.timeout:
                pass
            finally:
                conn.close()
        except socket.timeout:
            break

    server.close()
    print(f"[SERVER] Arrêté après {clients_handled} client(s)")


def simple_tcp_client(host="127.0.0.1", port=60001, message="Bonjour serveur"):
    """Client TCP simple."""
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.settimeout(3)
    try:
        s.connect((host, port))
        s.sendall(message.encode())
        data = s.recv(1024)
        print(f"[CLIENT] Envoyé '{message}', reçu '{data.decode()}'")
        return data.decode()
    except Exception as e:
        print(f"[CLIENT] Erreur : {e}")
        return None
    finally:
        s.close()


# Lancer serveur + 3 clients
t = threading.Thread(target=simple_tcp_server, kwargs={"max_clients": 3}, daemon=True)
t.start()
time.sleep(0.05)

for i in range(1, 4):
    simple_tcp_client(message=f"message_{i}")
    time.sleep(0.05)

t.join(timeout=6)
[SERVER] En écoute sur 127.0.0.1:60001
[SERVER] Connexion #1 depuis ('127.0.0.1', 38086)
[SERVER] Reçu 'message_1', renvoyé 'OK:message_1'
[CLIENT] Envoyé 'message_1', reçu 'OK:message_1'
[SERVER] Connexion #2 depuis ('127.0.0.1', 38094)
[SERVER] Reçu 'message_2', renvoyé 'OK:message_2'
[CLIENT] Envoyé 'message_2', reçu 'OK:message_2'
[SERVER] Connexion #3 depuis ('127.0.0.1', 38098)
[SERVER] Reçu 'message_3', renvoyé 'OK:message_3'
[CLIENT] Envoyé 'message_3', reçu 'OK:message_3'
[SERVER] Arrêté après 3 client(s)

Serveur TCP multi-clients avec threading#

Un serveur séquentiel ne peut traiter qu’un seul client à la fois. La solution classique est de créer un thread par connexion.

import socket
import threading
import time

class ThreadedTCPServer:
    """Serveur TCP qui crée un thread par connexion cliente."""

    def __init__(self, host="127.0.0.1", port=60002):
        self.host = host
        self.port = port
        self._lock = threading.Lock()
        self._clients = {}
        self._client_count = 0
        self._stop_event = threading.Event()
        self.server_sock = None

    def _handle_client(self, conn: socket.socket, addr: tuple, client_id: int):
        """Traite la connexion d'un client dans son propre thread."""
        print(f"[THREAD-{client_id}] Client {addr} connecté")
        conn.settimeout(2.0)
        messages_received = 0

        try:
            while not self._stop_event.is_set():
                try:
                    data = conn.recv(1024)
                    if not data:
                        break  # Client fermé
                    messages_received += 1
                    decoded = data.decode(errors="replace").strip()
                    response = f"[SERVEUR → client {client_id}] Echo: {decoded}\n"
                    conn.sendall(response.encode())
                    print(f"[THREAD-{client_id}] '{decoded}' → répondu")
                except socket.timeout:
                    break
        except Exception as e:
            print(f"[THREAD-{client_id}] Erreur : {e}")
        finally:
            conn.close()
            with self._lock:
                self._clients.pop(client_id, None)
            print(f"[THREAD-{client_id}] Déconnecté ({messages_received} msg traités)")

    def start(self, max_accept=6):
        self.server_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self.server_sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        self.server_sock.bind((self.host, self.port))
        self.server_sock.listen(10)
        self.server_sock.settimeout(4)
        print(f"[SERVEUR] Démarré sur {self.host}:{self.port} (threading)")

        accepted = 0
        while accepted < max_accept:
            try:
                conn, addr = self.server_sock.accept()
                accepted += 1
                with self._lock:
                    self._client_count += 1
                    cid = self._client_count
                    self._clients[cid] = conn
                t = threading.Thread(target=self._handle_client,
                                     args=(conn, addr, cid), daemon=True)
                t.start()
            except socket.timeout:
                break

        self._stop_event.set()
        self.server_sock.close()
        print(f"[SERVEUR] Arrêté (total : {self._client_count} connexions)")


def threaded_client(host, port, client_id, messages):
    """Client qui envoie plusieurs messages."""
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.settimeout(3)
    try:
        s.connect((host, port))
        for msg in messages:
            s.sendall((msg + "\n").encode())
            time.sleep(0.02)
            try:
                resp = s.recv(1024).decode()
                print(f"  [C{client_id}] ← {resp.strip()}")
            except socket.timeout:
                pass
    except Exception as e:
        print(f"  [C{client_id}] Erreur : {e}")
    finally:
        s.close()


# Démarrer le serveur
server = ThreadedTCPServer(port=60002)
server_thread = threading.Thread(target=server.start, kwargs={"max_accept": 6}, daemon=True)
server_thread.start()
time.sleep(0.1)

# 3 clients simultanés
client_threads = []
for i in range(1, 4):
    t = threading.Thread(
        target=threaded_client,
        args=("127.0.0.1", 60002, i, [f"msg_A_{i}", f"msg_B_{i}"]),
        daemon=True
    )
    client_threads.append(t)
    t.start()

for t in client_threads:
    t.join(timeout=5)
server_thread.join(timeout=6)
[SERVEUR] Démarré sur 127.0.0.1:60002 (threading)
[THREAD-1] Client ('127.0.0.1', 38706) connecté
[THREAD-2] Client ('127.0.0.1', 38716) connecté
[THREAD-1] 'msg_A_1' → répondu
[THREAD-2] 'msg_A_2' → répondu
[THREAD-3] Client ('127.0.0.1', 38730) connecté
[THREAD-3] 'msg_A_3' → répondu
  [C1] ← [SERVEUR → client 1] Echo: msg_A_1
[THREAD-1] 'msg_B_1' → répondu
  [C2] ← [SERVEUR → client 2] Echo: msg_A_2
[THREAD-2] 'msg_B_2' → répondu
  [C3] ← [SERVEUR → client 3] Echo: msg_A_3
[THREAD-3] 'msg_B_3' → répondu
  [C1] ← [SERVEUR → client 1] Echo: msg_B_1
  [C2] ← [SERVEUR → client 2] Echo: msg_B_2
[THREAD-1] Déconnecté (2 msg traités)
[THREAD-2] Déconnecté (2 msg traités)
  [C3] ← [SERVEUR → client 3] Echo: msg_B_3
[THREAD-3] Déconnecté (2 msg traités)
[SERVEUR] Arrêté (total : 3 connexions)

Multiplexage I/O avec selectors#

Le threading a un coût : chaque thread consomme de la mémoire (~8 Mo de stack par défaut) et génère du changement de contexte. Pour des centaines ou des milliers de connexions, le multiplexage I/O est plus efficace.

import selectors
import socket
import threading
import time

class SelectorTCPServer:
    """Serveur TCP non-bloquant avec selectors (I/O multiplexé)."""

    def __init__(self, host="127.0.0.1", port=60003):
        self.host = host
        self.port = port
        self.sel = selectors.DefaultSelector()
        self._buffers = {}      # socket → buffer d'entrée
        self._stop = False
        self.stats = {"accepted": 0, "messages": 0}

    def _accept(self, server_sock):
        conn, addr = server_sock.accept()
        conn.setblocking(False)
        self._buffers[conn] = b""
        self.sel.register(conn, selectors.EVENT_READ, self._read)
        self.stats["accepted"] += 1
        print(f"[SELECTOR] Client #{self.stats['accepted']} connecté depuis {addr}")

    def _read(self, conn):
        data = conn.recv(1024)
        if data:
            self._buffers[conn] += data
            # Traiter les messages terminés par '\n'
            while b"\n" in self._buffers[conn]:
                line, self._buffers[conn] = self._buffers[conn].split(b"\n", 1)
                if line:
                    response = b"ECHO[sel]:" + line + b"\n"
                    conn.sendall(response)
                    self.stats["messages"] += 1
                    print(f"[SELECTOR] '{line.decode()}' → répondu")
        else:
            # Connexion fermée par le client
            self.sel.unregister(conn)
            conn.close()

    def start(self, max_events=30):
        server_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        server_sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        server_sock.bind((self.host, self.port))
        server_sock.listen(10)
        server_sock.setblocking(False)
        self.sel.register(server_sock, selectors.EVENT_READ, self._accept)
        print(f"[SELECTOR] Serveur démarré sur {self.host}:{self.port}")

        events_processed = 0
        while events_processed < max_events and not self._stop:
            events = self.sel.select(timeout=3.0)
            if not events:
                break
            for key, mask in events:
                callback = key.data
                callback(key.fileobj)
                events_processed += 1

        self.sel.close()
        print(f"[SELECTOR] Arrêté — {self.stats['accepted']} clients, "
              f"{self.stats['messages']} messages")


def selector_client(host, port, cid, messages):
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.settimeout(3)
    try:
        s.connect((host, port))
        for msg in messages:
            s.sendall((msg + "\n").encode())
            time.sleep(0.03)
            try:
                resp = s.recv(1024).decode().strip()
                print(f"  [C{cid}] ← {resp}")
            except socket.timeout:
                pass
    except Exception as e:
        print(f"  [C{cid}] Erreur : {e}")
    finally:
        s.close()


sel_server = SelectorTCPServer(port=60003)
st = threading.Thread(target=sel_server.start, kwargs={"max_events": 30}, daemon=True)
st.start()
time.sleep(0.1)

client_threads = []
for i in range(1, 4):
    t = threading.Thread(
        target=selector_client,
        args=("127.0.0.1", 60003, i, [f"hello_{i}", f"world_{i}"]),
        daemon=True
    )
    client_threads.append(t)
    t.start()

for t in client_threads:
    t.join(timeout=5)
st.join(timeout=6)
[SELECTOR] Serveur démarré sur 127.0.0.1:60003
[SELECTOR] Client #1 connecté depuis ('127.0.0.1', 45946)
[SELECTOR] Client #2 connecté depuis ('127.0.0.1', 45960)
[SELECTOR] 'hello_1' → répondu
[SELECTOR] 'hello_2' → répondu
[SELECTOR] Client #3 connecté depuis ('127.0.0.1', 45974)
[SELECTOR] 'hello_3' → répondu
  [C1] ← ECHO[sel]:hello_1
  [C2] ← ECHO[sel]:hello_2
  [C3] ← ECHO[sel]:hello_3
[SELECTOR] 'world_1' → répondu
[SELECTOR] 'world_2' → répondu
[SELECTOR] 'world_3' → répondu
  [C1] ← ECHO[sel]:world_1
  [C2] ← ECHO[sel]:world_2
  [C3] ← ECHO[sel]:world_3
[SELECTOR] Arrêté — 3 clients, 6 messages

Serveur TCP asynchrone avec asyncio#

asyncio offre la concurrence sans threads grâce à la boucle d’événements et aux coroutines.

import asyncio

async def asyncio_handle_client(reader: asyncio.StreamReader,
                                 writer: asyncio.StreamWriter):
    """Coroutine de traitement d'un client asyncio."""
    addr = writer.get_extra_info("peername")
    print(f"[ASYNCIO] Client connecté depuis {addr}")
    messages_count = 0

    try:
        while True:
            try:
                data = await asyncio.wait_for(reader.readline(), timeout=2.0)
            except asyncio.TimeoutError:
                break
            if not data:
                break
            msg = data.decode(errors="replace").strip()
            if not msg:
                continue
            messages_count += 1
            response = f"ASYNC_ECHO:{msg}\n"
            writer.write(response.encode())
            await writer.drain()
            print(f"[ASYNCIO] Traité : '{msg}'")
    except Exception as e:
        print(f"[ASYNCIO] Erreur : {e}")
    finally:
        writer.close()
        try:
            await writer.wait_closed()
        except Exception:
            pass
        print(f"[ASYNCIO] Client {addr} déconnecté ({messages_count} messages)")


async def asyncio_server_main(host="127.0.0.1", port=60004, stop_after=3.0):
    """Lance le serveur asyncio pour stop_after secondes."""
    server = await asyncio.start_server(asyncio_handle_client, host, port)
    print(f"[ASYNCIO] Serveur démarré sur {host}:{port}")
    async with server:
        await asyncio.wait_for(server.serve_forever(), timeout=stop_after)


async def asyncio_client(host="127.0.0.1", port=60004,
                          cid=1, messages=None):
    """Client asyncio."""
    if messages is None:
        messages = ["hello", "async"]
    reader, writer = await asyncio.open_connection(host, port)
    for msg in messages:
        writer.write((msg + "\n").encode())
        await writer.drain()
        try:
            response = await asyncio.wait_for(reader.readline(), timeout=2.0)
            print(f"  [AC{cid}] ← {response.decode().strip()}")
        except asyncio.TimeoutError:
            print(f"  [AC{cid}] timeout")
    writer.close()
    try:
        await writer.wait_closed()
    except Exception:
        pass


async def demo_asyncio():
    """Démonstration serveur + clients asyncio."""
    PORT = 60004
    server_task = asyncio.create_task(
        asyncio_server_main(port=PORT, stop_after=4.0)
    )
    await asyncio.sleep(0.1)

    client_tasks = [
        asyncio_client(port=PORT, cid=i, messages=[f"msg_{i}_A", f"msg_{i}_B"])
        for i in range(1, 4)
    ]
    try:
        await asyncio.gather(*client_tasks)
    except Exception:
        pass

    server_task.cancel()
    try:
        await server_task
    except (asyncio.CancelledError, asyncio.TimeoutError):
        pass
    print("[ASYNCIO] Démonstration terminée")


# Exécuter la démonstration asyncio
try:
    asyncio.run(demo_asyncio())
except Exception as e:
    print(f"Erreur asyncio : {e}")
Erreur asyncio : asyncio.run() cannot be called from a running event loop
/tmp/ipykernel_10962/1125004395.py:95: RuntimeWarning: coroutine 'demo_asyncio' was never awaited
  print(f"Erreur asyncio : {e}")
RuntimeWarning: Enable tracemalloc to get the object allocation traceback

Options socket#

Les options socket permettent de contrôler finement le comportement des connexions.

import socket

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

print("=== Options socket importantes ===\n")

# SO_REUSEADDR : permettre de réutiliser l'adresse immédiatement après fermeture
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
val = s.getsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR)
print(f"SO_REUSEADDR   = {val}  (réutiliser l'adresse après TIME_WAIT)")

# SO_KEEPALIVE : envoyer des sondes TCP keepalive pour détecter les connexions mortes
s.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
val = s.getsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE)
print(f"SO_KEEPALIVE   = {val}  (sondes keepalive actives)")

# SO_RCVBUF / SO_SNDBUF : taille des tampons d'envoi/réception
s.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, 65536)
rcvbuf = s.getsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF)
print(f"SO_RCVBUF      = {rcvbuf} octets  (tampon réception)")

s.setsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF, 65536)
sndbuf = s.getsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF)
print(f"SO_SNDBUF      = {sndbuf} octets  (tampon envoi)")

# TCP_NODELAY : désactiver l'algorithme de Nagle (important pour VoIP, jeux temps-réel)
s.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
nodelay = s.getsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY)
print(f"TCP_NODELAY    = {nodelay}  (Nagle désactivé — utile pour temps réel)")

# SO_LINGER : comportement à la fermeture du socket
import struct
linger = struct.pack("ii", 1, 5)  # actif, 5 secondes
s.setsockopt(socket.SOL_SOCKET, socket.SO_LINGER, linger)
print(f"SO_LINGER      = activé, 5 s  (attente vidage tampon à close())")

s.close()
print("\nSocket fermé.")
=== Options socket importantes ===

SO_REUSEADDR   = 1  (réutiliser l'adresse après TIME_WAIT)
SO_KEEPALIVE   = 1  (sondes keepalive actives)
SO_RCVBUF      = 131072 octets  (tampon réception)
SO_SNDBUF      = 131072 octets  (tampon envoi)
TCP_NODELAY    = 1  (Nagle désactivé — utile pour temps réel)
SO_LINGER      = activé, 5 s  (attente vidage tampon à close())

Socket fermé.

Gestion des erreurs socket#

import socket
import errno

erreurs_courantes = {
    "ECONNREFUSED": {
        "code": errno.ECONNREFUSED,
        "cause": "Port fermé ou aucun service en écoute",
        "exemple": "connect() vers un port non ouvert",
        "solution": "Vérifier que le serveur est démarré"
    },
    "ETIMEDOUT": {
        "code": errno.ETIMEDOUT,
        "cause": "Pas de réponse dans le délai imparti",
        "exemple": "connect() ou recv() avec timeout dépassé",
        "solution": "Augmenter le timeout, vérifier la connectivité réseau"
    },
    "EADDRINUSE": {
        "code": errno.EADDRINUSE,
        "cause": "Adresse ou port déjà utilisé",
        "exemple": "bind() sur un port occupé",
        "solution": "Utiliser SO_REUSEADDR ou changer de port"
    },
    "ECONNRESET": {
        "code": errno.ECONNRESET,
        "cause": "Connexion réinitialisée par le pair (RST)",
        "exemple": "recv() après fermeture brutale",
        "solution": "Gérer l'exception, fermer le socket"
    },
    "EPIPE": {
        "code": errno.EPIPE,
        "cause": "Écriture sur socket fermé",
        "exemple": "send() après close() côté distant",
        "solution": "Capturer BrokenPipeError, fermer le socket local"
    },
}

print(f"{'Erreur':<16} {'Code':>6}  {'Cause'}")
print("-" * 72)
for nom, info in erreurs_courantes.items():
    print(f"{nom:<16} {info['code']:>6}  {info['cause']}")

# Simulation : ECONNREFUSED
print("\n=== Simulation ECONNREFUSED ===")
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.settimeout(1)
try:
    s.connect(("127.0.0.1", 1))  # Port 1 — normalement fermé
    print("Connexion réussie (inattendu)")
except ConnectionRefusedError as e:
    print(f"ConnectionRefusedError capturée : {e}")
except OSError as e:
    print(f"OSError capturée : {e}")
finally:
    s.close()

# Simulation : ETIMEDOUT
print("\n=== Simulation timeout ===")
s2 = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s2.settimeout(0.1)
try:
    s2.recvfrom(1024)
except socket.timeout:
    print("socket.timeout capturé : aucune donnée dans 100 ms")
finally:
    s2.close()
Erreur             Code  Cause
------------------------------------------------------------------------
ECONNREFUSED        111  Port fermé ou aucun service en écoute
ETIMEDOUT           110  Pas de réponse dans le délai imparti
EADDRINUSE           98  Adresse ou port déjà utilisé
ECONNRESET          104  Connexion réinitialisée par le pair (RST)
EPIPE                32  Écriture sur socket fermé

=== Simulation ECONNREFUSED ===
ConnectionRefusedError capturée : [Errno 111] Connection refused

=== Simulation timeout ===
socket.timeout capturé : aucune donnée dans 100 ms
# Patron robuste pour un client TCP
def robust_tcp_client(host: str, port: int, message: str,
                       timeout: float = 5.0, retries: int = 3) -> str | None:
    """
    Client TCP robuste avec gestion des erreurs et retry.
    Retourne la réponse du serveur ou None en cas d'échec.
    """
    for attempt in range(1, retries + 1):
        s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        s.settimeout(timeout)
        try:
            s.connect((host, port))
            s.sendall(message.encode())
            chunks = []
            while True:
                try:
                    chunk = s.recv(4096)
                    if not chunk:
                        break
                    chunks.append(chunk)
                    if len(chunks) >= 1:  # Pour la démo, arrêt après 1 chunk
                        break
                except socket.timeout:
                    break
            return b"".join(chunks).decode(errors="replace")
        except ConnectionRefusedError:
            print(f"  Tentative {attempt}/{retries} : connexion refusée")
        except socket.timeout:
            print(f"  Tentative {attempt}/{retries} : timeout")
        except OSError as e:
            print(f"  Tentative {attempt}/{retries} : OS error {e}")
        finally:
            try:
                s.shutdown(socket.SHUT_RDWR)
            except OSError:
                pass
            s.close()

    print(f"Échec après {retries} tentatives")
    return None

# Test sur un port inexistant pour voir le retry
result = robust_tcp_client("127.0.0.1", 9, "test", timeout=0.3, retries=2)
print(f"Résultat : {result!r}")
  Tentative 1/2 : connexion refusée
  Tentative 2/2 : connexion refusée
Échec après 2 tentatives
Résultat : None

Visualisation : états des connexions TCP#

fig, axes = plt.subplots(1, 2, figsize=(14, 5.5))

# ── Machine à états TCP ──────────────────────────────────────────────────────
ax = axes[0]
ax.set_xlim(-0.5, 4.5)
ax.set_ylim(-0.5, 9.5)
ax.axis("off")
ax.set_title("États d'une connexion TCP", fontweight="bold")

etats = {
    "CLOSED":       (2, 9),
    "LISTEN":       (0.5, 7),
    "SYN_SENT":     (3.5, 7),
    "SYN_RCVD":     (0.5, 5),
    "ESTABLISHED":  (2, 3),
    "FIN_WAIT_1":   (3.5, 2),
    "CLOSE_WAIT":   (0.5, 2),
    "FIN_WAIT_2":   (3.5, 1),
    "LAST_ACK":     (0.5, 1),
    "TIME_WAIT":    (3.5, 0),
}

colors_states = {
    "CLOSED": "#888888",
    "LISTEN": "#4C9BE8",
    "SYN_SENT": "#54B87A",
    "SYN_RCVD": "#4C9BE8",
    "ESTABLISHED": "#E87A4C",
    "FIN_WAIT_1": "#C96DD8",
    "CLOSE_WAIT": "#C96DD8",
    "FIN_WAIT_2": "#C96DD8",
    "LAST_ACK": "#C96DD8",
    "TIME_WAIT": "#888888",
}

for etat, (x, y) in etats.items():
    color = colors_states.get(etat, "#888888")
    rect = FancyBboxPatch((x - 0.7, y - 0.25), 1.4, 0.5,
                          boxstyle="round,pad=0.05", linewidth=1.5,
                          edgecolor="#555", facecolor=color, alpha=0.85)
    ax.add_patch(rect)
    ax.text(x, y, etat, ha="center", va="center", fontsize=7.5,
            fontweight="bold", color="white")

transitions = [
    ("CLOSED", "LISTEN", "listen()", "left"),
    ("CLOSED", "SYN_SENT", "connect()", "right"),
    ("LISTEN", "SYN_RCVD", "SYN reçu", "left"),
    ("SYN_SENT", "ESTABLISHED", "SYN-ACK", "right"),
    ("SYN_RCVD", "ESTABLISHED", "ACK reçu", "left"),
    ("ESTABLISHED", "FIN_WAIT_1", "close() actif", "right"),
    ("ESTABLISHED", "CLOSE_WAIT", "FIN reçu", "left"),
    ("FIN_WAIT_1", "FIN_WAIT_2", "ACK reçu", "right"),
    ("CLOSE_WAIT", "LAST_ACK", "close()", "left"),
    ("FIN_WAIT_2", "TIME_WAIT", "FIN reçu", "right"),
    ("LAST_ACK", "CLOSED", "ACK reçu", "left"),
    ("TIME_WAIT", "CLOSED", "2MSL", "right"),
]

for src, dst, label, side in transitions:
    x1, y1 = etats[src]
    x2, y2 = etats[dst]
    ax.annotate("", xy=(x2, y2 + 0.25 if y2 < y1 else y2 - 0.25),
                xytext=(x1, y1 - 0.25 if y1 > y2 else y1 + 0.25),
                arrowprops=dict(arrowstyle="->", color="#555555", lw=1.2,
                                connectionstyle="arc3,rad=0.1"))
    mx, my = (x1 + x2)/2, (y1 + y2)/2
    ax.text(mx + (0.15 if side == "right" else -0.15), my, label,
            ha="center", fontsize=6.5, color="#333333",
            bbox=dict(facecolor="white", edgecolor="none", alpha=0.7, pad=1))

# ── Modèle threading vs async ────────────────────────────────────────────────
ax2 = axes[1]
ax2.set_xlim(0, 10)
ax2.set_ylim(0, 8)
ax2.axis("off")
ax2.set_title("Threading vs selectors vs asyncio", fontweight="bold")

modeles = [
    (1.5, 6.5, "Thread par\nconnexion", "#4C9BE8",
     "Parallélisme réel\nSimple à coder\nCoût mémoire élevé\n(1 thread ≈ 8 Mo)"),
    (5, 6.5, "Selectors\n(I/O mux)", "#54B87A",
     "1 thread, N sockets\nÉvénementiel\nPlus complexe\nTrès efficace"),
    (8.5, 6.5, "asyncio\n(coroutines)", "#E87A4C",
     "Concurrence coop.\nawait/async\nÉcosystème riche\nRequiert Python 3.5+"),
]

for x, y, title, color, details in modeles:
    box_rect = FancyBboxPatch((x - 1.2, y - 0.3), 2.4, 0.8,
                              boxstyle="round,pad=0.1", linewidth=2,
                              edgecolor=color, facecolor=color, alpha=0.85)
    ax2.add_patch(box_rect)
    ax2.text(x, y + 0.1, title, ha="center", va="center", fontsize=9,
             fontweight="bold", color="white")
    ax2.text(x, y - 1.8, details, ha="center", va="center", fontsize=7.5,
             color="#333333",
             bbox=dict(facecolor="#F8F9FA", edgecolor=color, alpha=0.85,
                       boxstyle="round,pad=0.3"))

ax2.text(5, 7.8, "Stratégies de concurrence pour serveurs TCP",
         ha="center", fontsize=10, fontweight="bold", color="#2C3E50")

# Ligne de comparaison performance
labels_perf = ["1K conns", "10K conns", "100K conns"]
threading_mem = [8000, 80000, 800000]  # Mo estimés
selectors_mem = [10, 15, 25]
async_mem = [20, 30, 50]

ax3_inset = ax2.inset_axes([0.05, 0.0, 0.9, 0.35])
x_p = np.arange(3)
ax3_inset.bar(x_p - 0.25, [t/1000 for t in threading_mem], 0.25,
              label="Threading (Mo)", color="#4C9BE8", alpha=0.8)
ax3_inset.bar(x_p, selectors_mem, 0.25,
              label="Selectors (Mo)", color="#54B87A", alpha=0.8)
ax3_inset.bar(x_p + 0.25, async_mem, 0.25,
              label="asyncio (Mo)", color="#E87A4C", alpha=0.8)
ax3_inset.set_yscale("log")
ax3_inset.set_xticks(x_p)
ax3_inset.set_xticklabels(labels_perf, fontsize=7)
ax3_inset.set_ylabel("Mém. (Mo)", fontsize=7)
ax3_inset.legend(fontsize=6.5, loc="upper left")
ax3_inset.set_title("Consommation mémoire estimée", fontsize=7.5)
ax3_inset.grid(axis="y", alpha=0.4)

plt.tight_layout()
plt.show()
_images/f95c88f98a3d8219d199d0d03e0930b5ac0fdadaf3c3b8ef1e4e455af12bea44.png

Résumé#

fig, ax = plt.subplots(figsize=(12, 5.5))
ax.axis("off")
ax.set_title("Récapitulatif — Sockets Python", fontsize=14, fontweight="bold", pad=15)

resume = [
    ["socket(AF_INET, SOCK_STREAM)", "Crée un socket TCP IPv4"],
    ["bind() + listen() + accept()", "Séquence serveur TCP"],
    ["connect() + send() + recv()", "Séquence client TCP"],
    ["SO_REUSEADDR", "Évite EADDRINUSE au redémarrage du serveur"],
    ["TCP_NODELAY", "Désactive Nagle — réduit la latence pour le temps réel"],
    ["threading.Thread par conn.", "Concurrence simple, coût élevé (1 thread ≈ 8 Mo)"],
    ["selectors.DefaultSelector()", "Multiplexage I/O — 1 thread, N sockets"],
    ["asyncio.start_server()", "Concurrence coopérative avec async/await"],
    ["ECONNREFUSED / ETIMEDOUT", "Erreurs fréquentes — toujours gérer avec try/except"],
    ["shutdown(SHUT_RDWR)", "Fermeture propre — vider le tampon avant close()"],
]

table = ax.table(
    cellText=resume,
    colLabels=["API / Concept", "Description"],
    cellLoc="left",
    loc="center",
    colWidths=[0.35, 0.55]
)
table.auto_set_font_size(False)
table.set_fontsize(9.5)
table.scale(1, 1.75)

for j in range(2):
    table[0, j].set_facecolor("#2C3E50")
    table[0, j].set_text_props(color="white", fontweight="bold")
for i in range(1, len(resume) + 1):
    for j in range(2):
        if i % 2 == 0:
            table[i, j].set_facecolor("#F5F7FA")
        if j == 0:
            table[i, j].set_text_props(fontweight="bold", color="#2C3E50", fontsize=8.5)

plt.tight_layout()
plt.show()
_images/169edae902841e552b1032a4bfc139f8c0e45e2f0664f4f9df11a060884e51d8.png