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
asyncioGérer correctement les erreurs et options socket
L’API socket POSIX#
Primitives fondamentales#
Appel système |
Rôle |
|---|---|
|
Crée un nouveau socket, retourne un descripteur de fichier |
|
Associe le socket à une adresse locale (IP + port) |
|
Met le socket en mode écoute passive (TCP serveur) |
|
Bloque jusqu’à l’arrivée d’un client, retourne un nouveau socket |
|
Initie la connexion TCP (ou fixe l’adresse distante pour UDP) |
|
Envoie des données |
|
Reçoit des données |
|
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()
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()
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()