Chapitre 5 — TCP : fiabilité et contrôle de flux#
TCP (Transmission Control Protocol, RFC 793) est le protocole de transport sur lequel repose la quasi-totalité des communications fiables d’Internet : HTTP, HTTPS, SSH, SMTP, FTP. Son rôle est de fournir un canal de communication fiable, ordonné et contrôlé au-dessus d’IP, qui lui est fondamentalement non fiable.
Le segment TCP#
Un segment TCP est l’unité de données de la couche transport. Son en-tête fait 20 octets minimum (sans options).
Structure de l’en-tête TCP#
Description des champs#
Champ |
Taille |
Rôle |
|---|---|---|
Port source |
16 bits |
Port du processus émetteur (1–65535) |
Port destination |
16 bits |
Port du service destinataire |
Numéro de séquence (SEQ) |
32 bits |
Position du premier octet de ce segment dans le flux |
Numéro d’acquittement (ACK) |
32 bits |
SEQ du prochain octet attendu de l’autre côté |
Data Offset |
4 bits |
Taille de l’en-tête en mots de 32 bits (min=5 → 20 octets) |
Flags |
9 bits |
SYN, ACK, FIN, RST, PSH, URG, ECE, CWR, NS |
Fenêtre (RWND) |
16 bits |
Taille de la fenêtre de réception (contrôle de flux) |
Checksum |
16 bits |
Intégrité de l’en-tête + données |
Pointeur urgent |
16 bits |
Offset vers les données urgentes (si URG=1) |
import struct
FLAGS_TCP = {
"FIN": 0x001, "SYN": 0x002, "RST": 0x004, "PSH": 0x008,
"ACK": 0x010, "URG": 0x020, "ECE": 0x040, "CWR": 0x080, "NS": 0x100,
}
def construire_segment_tcp(port_src: int, port_dst: int, seq: int, ack: int,
flags: list[str], fenetre: int,
payload: bytes = b"") -> bytes:
"""
Construit un segment TCP (sans pseudo-en-tête pour le checksum — simplifié).
"""
flags_val = 0
for f in flags:
flags_val |= FLAGS_TCP.get(f.upper(), 0)
data_offset = 5 # 20 octets (pas d'options)
offset_flags = (data_offset << 12) | flags_val
en_tete = struct.pack(">HHIIHHHH",
port_src,
port_dst,
seq,
ack,
offset_flags,
fenetre,
0, # Checksum (0 = non calculé ici)
0, # Pointeur urgent
)
return en_tete + payload
def decoder_segment_tcp(data: bytes) -> dict:
"""Décode un segment TCP."""
(port_src, port_dst, seq, ack,
offset_flags, fenetre, checksum, urgent) = struct.unpack(">HHIIHHHH", data[:20])
data_offset = (offset_flags >> 12) & 0x0F
flags_val = offset_flags & 0x1FF
flags_actifs = [nom for nom, bit in FLAGS_TCP.items() if flags_val & bit]
payload = data[data_offset * 4:]
return {
"Port source": port_src,
"Port destination":port_dst,
"SEQ": seq,
"ACK": ack,
"Data Offset": f"{data_offset} × 4 = {data_offset*4} octets",
"Flags": ", ".join(flags_actifs) if flags_actifs else "aucun",
"Fenêtre": fenetre,
"Payload": payload.decode("ascii", errors="replace") if payload else "(vide)",
}
# SYN initial du client
syn = construire_segment_tcp(
port_src=54321, port_dst=80,
seq=1000, ack=0,
flags=["SYN"], fenetre=65535
)
print("Segment SYN (client → serveur) :")
for k, v in decoder_segment_tcp(syn).items():
print(f" {k:<20} : {v}")
print()
# SYN-ACK du serveur
syn_ack = construire_segment_tcp(
port_src=80, port_dst=54321,
seq=5000, ack=1001,
flags=["SYN", "ACK"], fenetre=8192
)
print("Segment SYN-ACK (serveur → client) :")
for k, v in decoder_segment_tcp(syn_ack).items():
print(f" {k:<20} : {v}")
Segment SYN (client → serveur) :
Port source : 54321
Port destination : 80
SEQ : 1000
ACK : 0
Data Offset : 5 × 4 = 20 octets
Flags : SYN
Fenêtre : 65535
Payload : (vide)
Segment SYN-ACK (serveur → client) :
Port source : 80
Port destination : 54321
SEQ : 5000
ACK : 1001
Data Offset : 5 × 4 = 20 octets
Flags : SYN, ACK
Fenêtre : 8192
Payload : (vide)
Établissement de connexion : le 3-way handshake#
Avant tout échange de données, TCP établit une connexion en 3 étapes (three-way handshake). Cela permet aux deux parties de synchroniser leurs numéros de séquence initiaux (ISN — Initial Sequence Number).
Pourquoi 3 étapes et pas 2 ?
Avec seulement 2 étapes (SYN + SYN-ACK), le client ne confirmerait pas la réception du SYN-ACK. Le serveur ne saurait pas si le client a reçu ses paramètres (ISN notamment). Le 3e paquet (ACK) confirme que la communication bidirectionnelle est possible et que les deux ISN sont connus des deux côtés.
Fermeture de connexion : 4-way handshake#
La fermeture TCP nécessite 4 étapes car chaque sens de la connexion doit être fermé indépendamment (la connexion est full-duplex).
L’état TIME_WAIT#
Après avoir envoyé le dernier ACK, le client entre dans l’état TIME_WAIT pendant 2 × MSL (Maximum Segment Lifetime, typiquement 30–120 secondes, soit 60–240 s au total). Cela permet :
De s’assurer que le dernier ACK est bien arrivé (retransmission possible si le serveur renvoie un FIN)
D’éviter que les anciens segments d’une connexion précédente arrivent dans une nouvelle connexion sur le même quadruplet (src IP, src port, dst IP, dst port)
Contrôle de flux : la fenêtre glissante#
TCP garantit que l’émetteur ne sature pas le récepteur grâce au contrôle de flux. Le récepteur annonce sa fenêtre de réception (Receiver Window, RWND) dans chaque segment ACK : c’est la quantité de données qu’il peut encore recevoir en mémoire tampon.
Window Scaling#
Le champ RWND est sur 16 bits (max 65 535 octets). Pour les réseaux à haute bande passante et grand délai (satellites, WAN longue distance), cette limite est insuffisante. L’option Window Scale (RFC 7323) permet de multiplier la fenêtre par un facteur de 2^n (jusqu’à 2^14 = 16 384), portant la fenêtre effective à ~1 Go.
Contrôle de congestion#
Le contrôle de flux protège le récepteur. Le contrôle de congestion protège le réseau lui-même contre la surcharge. TCP adapte son débit en fonction des signes de congestion (pertes de paquets, délais).
La fenêtre de congestion (CWND)#
La quantité de données qu’un émetteur peut avoir en vol est limitée par le minimum de RWND (receiver window) et CWND (congestion window) :
Algorithmes de contrôle de congestion#
Slow Start : Au démarrage, CWND commence à 1 MSS et double à chaque RTT (croissance exponentielle) jusqu’à atteindre le seuil ssthresh.
Congestion Avoidance : Quand CWND ≥ ssthresh, croissance linéaire (+1 MSS par RTT).
Fast Retransmit : Dès réception de 3 ACKs dupliqués, retransmission immédiate sans attendre le timeout.
Fast Recovery : Après Fast Retransmit, au lieu de repartir de Slow Start, CWND est réduit de moitié (pas à 1 MSS).
---------------------------------------------------------------------------
NotImplementedError Traceback (most recent call last)
Cell In[7], line 67
63 ax.text(40, max(cwnds)*0.95, "3 ACKs dup.\n(Fast Retransmit)", ha="center", fontsize=8,
64 color="#e67e22", bbox=dict(boxstyle="round", fc="white", ec="#e67e22", alpha=0.8))
66 # Légende manuelle des phases
---> 67 ax.add_patch(mpatches.Patch(color="#e74c3c", alpha=0.4, label="Slow Start"))
68 ax.add_patch(mpatches.Patch(color="#2980b9", alpha=0.4, label="Congestion Avoidance"))
69 ax.plot([], [], color="#f39c12", linestyle="--", label="ssthresh")
File ~/legacy_workspace/notebooks/.venv/lib/python3.13/site-packages/matplotlib/axes/_base.py:2492, in _AxesBase.add_patch(self, p)
2490 if p.get_clip_path() is None:
2491 p.set_clip_path(self.patch)
-> 2492 self._update_patch_limits(p)
2493 self._children.append(p)
2494 p._remove_method = self._children.remove
File ~/legacy_workspace/notebooks/.venv/lib/python3.13/site-packages/matplotlib/axes/_base.py:2510, in _AxesBase._update_patch_limits(self, patch)
2507 if (isinstance(patch, mpatches.Rectangle) and
2508 ((not patch.get_width()) and (not patch.get_height()))):
2509 return
-> 2510 p = patch.get_path()
2511 # Get all vertices on the path
2512 # Loop through each segment to get extrema for Bezier curve sections
2513 vertices = []
File ~/legacy_workspace/notebooks/.venv/lib/python3.13/site-packages/matplotlib/patches.py:652, in Patch.get_path(self)
650 def get_path(self):
651 """Return the path of this patch."""
--> 652 raise NotImplementedError('Derived must override')
NotImplementedError: Derived must override
États TCP : diagramme complet#
Une connexion TCP peut se trouver dans l’un des 11 états définis par la RFC 793. La machine d’états pilote les transitions en fonction des segments reçus/envoyés et des appels système.
Code Python : socket TCP client/serveur#
import socket
import threading
import time
def serveur_tcp(hote: str = "127.0.0.1", port: int = 9999,
messages_recus: list = None):
"""Serveur TCP simple : reçoit et renvoie les messages en majuscules."""
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as srv:
srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
srv.bind((hote, port))
srv.listen(1)
srv.settimeout(3.0)
print(f"[Serveur] En écoute sur {hote}:{port}...")
try:
conn, addr = srv.accept()
with conn:
print(f"[Serveur] Connexion de {addr}")
while True:
data = conn.recv(1024)
if not data:
break
message = data.decode("utf-8")
reponse = message.upper()
print(f"[Serveur] Reçu : {message!r} → Renvoi : {reponse!r}")
conn.sendall(reponse.encode("utf-8"))
if messages_recus is not None:
messages_recus.append(message)
except socket.timeout:
print("[Serveur] Timeout, fermeture.")
def client_tcp(hote: str = "127.0.0.1", port: int = 9999,
messages: list = None):
"""Client TCP simple : envoie des messages et affiche les réponses."""
time.sleep(0.2) # Laisse le serveur démarrer
if messages is None:
messages = ["bonjour", "réseau TCP", "couche transport"]
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as cli:
cli.connect((hote, port))
print(f"[Client] Connecté à {hote}:{port}")
for msg in messages:
cli.sendall(msg.encode("utf-8"))
reponse = cli.recv(1024).decode("utf-8")
print(f"[Client] Envoyé : {msg!r} → Reçu : {reponse!r}")
time.sleep(0.05)
print("[Client] Fermeture de la connexion.")
# Lancement du serveur dans un thread séparé
messages_log = []
thread_srv = threading.Thread(target=serveur_tcp,
kwargs={"messages_recus": messages_log},
daemon=True)
thread_srv.start()
# Lancement du client
client_tcp(messages=["bonjour", "réseau TCP", "couche transport", "fin"])
thread_srv.join(timeout=4)
print(f"\n[Bilan] {len(messages_log)} message(s) traité(s) par le serveur.")
Inspection des options TCP#
def analyser_options_tcp(options_bytes: bytes) -> list:
"""
Analyse les options TCP présentes dans l'espace optionnel de l'en-tête.
Retourne une liste de dictionnaires décrivant chaque option.
"""
options_connues = {
0: ("EOL", "End of Options List"),
1: ("NOP", "No-Operation"),
2: ("MSS", "Maximum Segment Size"),
3: ("WSOPT", "Window Scale"),
4: ("SACK Perm", "SACK Permitted"),
5: ("SACK", "Selective Acknowledgment"),
8: ("Timestamps", "Timestamps"),
19: ("MD5 Sig", "TCP MD5 Signature"),
29: ("Multipath", "Multipath TCP"),
30: ("TFO", "TCP Fast Open"),
}
options_parsées = []
i = 0
while i < len(options_bytes):
kind = options_bytes[i]
nom, desc = options_connues.get(kind, (f"Option {kind}", "Inconnue"))
if kind == 0: # EOL
options_parsées.append({"Type": 0, "Nom": nom, "Description": desc})
break
elif kind == 1: # NOP
options_parsées.append({"Type": 1, "Nom": nom, "Description": desc})
i += 1
else:
if i + 1 >= len(options_bytes):
break
longueur = options_bytes[i + 1]
valeur = options_bytes[i + 2: i + longueur] if longueur > 2 else b""
valeur_hex = valeur.hex(" ") if valeur else "(vide)"
options_parsées.append({
"Type": kind,
"Nom": nom,
"Description": desc,
"Longueur": longueur,
"Valeur (hex)":valeur_hex,
})
i += longueur
return options_parsées
# Exemple : options TCP typiques d'un SYN (MSS=1460, WSOPT=7, SACK Perm, Timestamps, NOP)
options_syn = bytes([
2, 4, 0x05, 0xB4, # MSS = 1460
1, # NOP
3, 3, 7, # Window Scale = 7 (×128)
1, # NOP
1, # NOP
8, 10, 0x00, 0x12, 0x34, 0x56, 0x00, 0x00, 0x00, 0x00, # Timestamps
4, 2, # SACK Permitted
])
print("Options TCP d'un segment SYN typique :")
print("-" * 55)
for opt in analyser_options_tcp(options_syn):
print(f" Type {opt['Type']:>3} ({opt['Nom']:<12}) : {opt.get('Description', '')}", end="")
if "Valeur (hex)" in opt:
print(f" | valeur = {opt['Valeur (hex)']}", end="")
print()
Résumé#
Points clés à retenir
L”en-tête TCP contient les ports, les numéros SEQ/ACK, les flags et la taille de fenêtre RWND.
Le 3-way handshake (SYN / SYN-ACK / ACK) établit la connexion et synchronise les ISN.
La fermeture est un 4-way (FIN / ACK / FIN / ACK) suivi d’un état TIME_WAIT de 2×MSL.
La fenêtre glissante (RWND) contrôle le flux pour protéger le récepteur.
Le contrôle de congestion (CWND) protège le réseau via Slow Start, Congestion Avoidance et Fast Retransmit.
TCP CUBIC (Linux) récupère plus vite après une congestion qu’un TCP Reno classique.
La machine d’états TCP définit 11 états (CLOSED, LISTEN, SYN_SENT, ESTABLISHED, TIME_WAIT…).