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.

Hide code cell source

import matplotlib
import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
import matplotlib.patheffects as pe
import numpy as np
import pandas as pd
import struct
import socket
import threading
import time
import seaborn as sns
sns.set_theme(style="whitegrid", palette="muted")
plt.rcParams.update({
    "figure.dpi": 120,
    "font.family": "DejaVu Sans",
    "axes.spines.top": False,
    "axes.spines.right": False,
})

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#

Hide code cell source

fig, ax = plt.subplots(figsize=(14, 9))
ax.set_xlim(0, 32)
ax.set_ylim(0, 11.5)
ax.axis("off")
ax.set_title("Structure de l'en-tête TCP (20 octets minimum, 32 bits par ligne)",
             fontsize=13, fontweight="bold", pad=12)

lignes_tcp = [
    (9.8, [("Port source\n16 bits",       16, "#e74c3c"),
           ("Port destination\n16 bits",  16, "#c0392b")]),
    (7.6, [("Numéro de séquence (SEQ)\n32 bits", 32, "#2980b9")]),
    (5.4, [("Numéro d'acquittement (ACK)\n32 bits", 32, "#27ae60")]),
    (3.2, [("Data\nOffset\n4 bits",   4, "#8e44ad"),
           ("Réservé\n3 bits",         3, "#9b59b6"),
           ("NS CWR ECE URG ACK PSH RST SYN FIN\n9 bits (flags)", 9, "#e67e22"),
           ("Fenêtre (RWND)\n16 bits", 16, "#f39c12")]),
    (1.0, [("Checksum\n16 bits",        16, "#16a085"),
           ("Pointeur Urgent\n16 bits", 16, "#1abc9c")]),
]

for y, champs in lignes_tcp:
    x = 0
    for label, bits, color in champs:
        width = (bits / 32) * 32
        rect = mpatches.FancyBboxPatch((x + 0.05, y + 0.05), width - 0.1, 1.7,
                                        boxstyle="round,pad=0.05",
                                        edgecolor="#444", facecolor=color, alpha=0.85, linewidth=1.2)
        ax.add_patch(rect)
        ax.text(x + width/2, y + 0.95, label, ha="center", va="center",
                fontsize=7.5, color="white", fontweight="bold")
        x += width

# Annotations drapeaux
flags_info = [
    ("SYN\nSynchronize", 23.5, 3.2, "#e67e22"),
    ("ACK\nAcknowledge", 20.0, 3.2, "#e67e22"),
    ("FIN\nFinish",      31.0, 3.2, "#e67e22"),
    ("RST\nReset",       26.5, 3.2, "#e67e22"),
    ("PSH\nPush",        17.0, 3.2, "#e67e22"),
]

# Numéros de bits
ax.text(0, 11.2, "0", ha="left", fontsize=8, color="#555")
ax.text(16, 11.2, "16", ha="center", fontsize=8, color="#555")
ax.text(32, 11.2, "31", ha="right", fontsize=8, color="#555")
ax.axhline(11.0, color="#cccccc", linewidth=0.8, linestyle="--")

plt.tight_layout()
plt.show()
_images/1b1a0091278c74404244ad4e7c16c6cd049e2f3b957d849a453601f506ae4192.png

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).

Hide code cell source

fig, ax = plt.subplots(figsize=(12, 9))
ax.set_xlim(0, 12)
ax.set_ylim(0, 10)
ax.axis("off")
ax.set_title("Three-Way Handshake TCP — Établissement de connexion",
             fontsize=13, fontweight="bold")

# Colonnes client / serveur
for x, label, etat in [(2, "Client", "CLOSED → SYN_SENT → ESTABLISHED"),
                        (10, "Serveur", "CLOSED → LISTEN → SYN_RCVD → ESTABLISHED")]:
    ax.add_patch(mpatches.FancyBboxPatch((x-1.2, 8.5), 2.4, 1.0,
                                          boxstyle="round,pad=0.1",
                                          edgecolor="#2c3e50", facecolor="#2c3e50", linewidth=2))
    ax.text(x, 9.0, label, ha="center", va="center",
            fontsize=12, fontweight="bold", color="white")
    ax.text(x, 8.35, etat, ha="center", va="center",
            fontsize=6.5, color="#555", fontstyle="italic")

# Lignes de vie
ax.plot([2, 2], [0.5, 8.5], color="#2c3e50", linewidth=1.5, linestyle="--", alpha=0.4)
ax.plot([10, 10], [0.5, 8.5], color="#2c3e50", linewidth=1.5, linestyle="--", alpha=0.4)

def fleche_segment(ax, x_src, x_dst, y, label, couleur, label_etat_src=None, label_etat_dst=None):
    ax.annotate("",
                xy=(x_dst, y - 0.5), xytext=(x_src, y),
                arrowprops=dict(arrowstyle="-|>", color=couleur, lw=2.5))
    xm = (x_src + x_dst) / 2
    ym = (y + y - 0.5) / 2
    ax.text(xm, ym + 0.15, label, ha="center", va="bottom", fontsize=9.5,
            color=couleur, fontweight="bold",
            bbox=dict(boxstyle="round,pad=0.3", fc="white", ec=couleur, alpha=0.9))
    if label_etat_src:
        x_off = -0.3 if x_src < x_dst else 0.3
        ax.text(x_src + x_off, y + 0.05, label_etat_src, ha="right" if x_src < x_dst else "left",
                va="center", fontsize=7.5, color=couleur, fontstyle="italic")
    if label_etat_dst:
        x_off = 0.3 if x_src < x_dst else -0.3
        ax.text(x_dst + x_off, y - 0.45, label_etat_dst, ha="left" if x_src < x_dst else "right",
                va="center", fontsize=7.5, color=couleur, fontstyle="italic")

# Étape 1 : SYN
fleche_segment(ax, 2, 10, 7.8,
               "SYN  [SEQ=1000]",
               "#e74c3c",
               label_etat_src="SYN_SENT",
               label_etat_dst="SYN_RCVD")

# Étape 2 : SYN-ACK
fleche_segment(ax, 10, 2, 6.2,
               "SYN-ACK  [SEQ=5000, ACK=1001]",
               "#2980b9",
               label_etat_dst="SYN_RCVD → ESTAB.")

# Étape 3 : ACK
fleche_segment(ax, 2, 10, 4.6,
               "ACK  [SEQ=1001, ACK=5001]",
               "#27ae60",
               label_etat_src="ESTABLISHED",
               label_etat_dst="ESTABLISHED")

# Données
fleche_segment(ax, 2, 10, 3.3,
               "DATA (HTTP GET)  [SEQ=1001]",
               "#8e44ad")

fleche_segment(ax, 10, 2, 2.0,
               "DATA (HTTP 200)  [SEQ=5001, ACK=1234]",
               "#e67e22")

# Annotations temporelles
for y, label in [(7.8, "t₁"), (6.2, "t₂"), (4.6, "t₃"), (3.3, "t₄"), (2.0, "t₅")]:
    ax.text(0.3, y, label, ha="center", va="center", fontsize=9, color="#7f8c8d")

ax.text(0.3, 8.8, "t", ha="center", fontsize=9, color="#7f8c8d", fontweight="bold")

# Légende connexion établie
ax.add_patch(mpatches.FancyBboxPatch((3.5, 4.0), 5.0, 0.4,
                                      boxstyle="round,pad=0.1",
                                      edgecolor="#27ae60", facecolor="#eafaf1",
                                      linewidth=2, linestyle="dashed"))
ax.text(6.0, 4.2, "Connexion ESTABLISHED — échange de données possible",
        ha="center", va="center", fontsize=8, color="#27ae60", fontweight="bold")

plt.tight_layout()
plt.show()
_images/817e33193beb5eeebf26f1468c254cbce3bfb2a02bcd2352e42ce6a3dd32c0c5.png

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).

Hide code cell source

fig, ax = plt.subplots(figsize=(12, 8))
ax.set_xlim(0, 12)
ax.set_ylim(0, 9)
ax.axis("off")
ax.set_title("Fermeture TCP — 4-way handshake et état TIME_WAIT",
             fontsize=13, fontweight="bold")

for x, label in [(2, "Client (actif)"), (10, "Serveur (passif)")]:
    ax.add_patch(mpatches.FancyBboxPatch((x-1.2, 7.8), 2.4, 0.9,
                                          boxstyle="round,pad=0.1",
                                          edgecolor="#2c3e50", facecolor="#2c3e50"))
    ax.text(x, 8.25, label, ha="center", va="center",
            fontsize=11, fontweight="bold", color="white")

ax.plot([2, 2], [0.5, 7.8], color="#2c3e50", linewidth=1.5, linestyle="--", alpha=0.4)
ax.plot([10, 10], [0.5, 7.8], color="#2c3e50", linewidth=1.5, linestyle="--", alpha=0.4)

etapes = [
    (2, 10, 7.1, "FIN  [SEQ=1200]",         "#e74c3c",  "FIN_WAIT_1", "CLOSE_WAIT"),
    (10, 2, 5.7, "ACK  [ACK=1201]",          "#27ae60",  "FIN_WAIT_2", ""),
    (10, 2, 4.3, "FIN  [SEQ=5800]",          "#e67e22",  "LAST_ACK",   ""),
    (2, 10, 2.9, "ACK  [ACK=5801]",          "#2980b9",  "TIME_WAIT",  "CLOSED"),
]

for x_src, x_dst, y, label, color, etat_src, etat_dst in etapes:
    ax.annotate("", xy=(x_dst, y - 0.5), xytext=(x_src, y),
                arrowprops=dict(arrowstyle="-|>", color=color, lw=2.5))
    ax.text((x_src+x_dst)/2, (y + y-0.5)/2 + 0.12, label,
            ha="center", va="bottom", fontsize=9.5, color=color, fontweight="bold",
            bbox=dict(boxstyle="round,pad=0.3", fc="white", ec=color, alpha=0.9))
    if etat_src:
        ax.text(x_src + (-0.3 if x_src < x_dst else 0.3), y + 0.1,
                etat_src, ha="right" if x_src < x_dst else "left",
                va="center", fontsize=7.5, color=color, fontstyle="italic")
    if etat_dst:
        ax.text(x_dst + (0.3 if x_src < x_dst else -0.3), y - 0.45,
                etat_dst, ha="left" if x_src < x_dst else "right",
                va="center", fontsize=7.5, color=color, fontstyle="italic")

# TIME_WAIT
ax.add_patch(mpatches.FancyBboxPatch((0.5, 1.2), 2.8, 1.2,
                                      boxstyle="round,pad=0.1",
                                      edgecolor="#e74c3c", facecolor="#fdebd0", linewidth=2,
                                      linestyle="dashed"))
ax.text(1.9, 1.8, "TIME_WAIT\n2 × MSL ≈ 60–240 s", ha="center", va="center",
        fontsize=8, color="#e74c3c", fontweight="bold")
ax.text(1.9, 0.9, "Protège contre les\nsegments résiduels", ha="center",
        fontsize=7.5, color="#7f8c8d", fontstyle="italic")

plt.tight_layout()
plt.show()
_images/28734da8dd0a74033b9ee6350e970f5fb37e595f0f62dd2e5e5f363141a48ad9.png

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 :

  1. De s’assurer que le dernier ACK est bien arrivé (retransmission possible si le serveur renvoie un FIN)

  2. 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.

Hide code cell source

fig, axes = plt.subplots(2, 1, figsize=(13, 10))
fig.suptitle("Fenêtre glissante TCP — Contrôle de flux", fontsize=13, fontweight="bold")

# ---- Visualisation de la fenêtre glissante ----
ax = axes[0]
ax.set_xlim(0, 20)
ax.set_ylim(-1, 3)
ax.axis("off")

segments = list(range(1, 18))  # numéros de segments

# État : [ACKed | Sent (in flight) | Window | Not yet sent]
acked     = list(range(1, 6))      # 1–5 : acquittés
in_flight = list(range(6, 10))     # 6–9 : envoyés, pas encore ACKés
fenetre   = list(range(10, 14))    # 10–13 : dans la fenêtre, pas encore envoyés
non_envoye= list(range(14, 18))    # 14+ : hors fenêtre

couleurs = {
    "acked":      ("#27ae60", "ACKé"),
    "in_flight":  ("#e67e22", "Envoyé (in-flight)"),
    "fenetre":    ("#2980b9", "Peut envoyer"),
    "non_envoye": ("#bdc3c7", "Hors fenêtre"),
}

y_seg = 1.5
w = 1.05
for i, seg in enumerate(segments):
    if seg in acked:
        color, label = "#27ae60", "acked"
    elif seg in in_flight:
        color, label = "#e67e22", "in_flight"
    elif seg in fenetre:
        color, label = "#2980b9", "fenetre"
    else:
        color, label = "#bdc3c7", "non_envoye"

    rect = mpatches.FancyBboxPatch((i * w + 0.3, y_seg - 0.4), w - 0.05, 0.8,
                                    boxstyle="round,pad=0.04",
                                    edgecolor="#555", facecolor=color, alpha=0.85, linewidth=1.2)
    ax.add_patch(rect)
    ax.text(i * w + 0.3 + (w-0.05)/2, y_seg, str(seg), ha="center", va="center",
            fontsize=9, color="white", fontweight="bold")

# Légende
legend_items = [
    ("#27ae60", "ACKé"),
    ("#e67e22", "Envoyé (ACK en attente)"),
    ("#2980b9", "Prêt à envoyer (dans RWND)"),
    ("#bdc3c7", "Hors fenêtre"),
]
for i, (color, label) in enumerate(legend_items):
    ax.add_patch(mpatches.FancyBboxPatch((0.3 + i * 4.5, 0.1), 0.6, 0.45,
                                          boxstyle="round,pad=0.04",
                                          edgecolor="#555", facecolor=color, alpha=0.85))
    ax.text(1.1 + i * 4.5, 0.32, label, va="center", fontsize=8, color="#333")

# Flèche fenêtre
ax.annotate("", xy=(13.5 * w + 0.3, y_seg + 0.6), xytext=(5.5 * w + 0.3, y_seg + 0.6),
            arrowprops=dict(arrowstyle="<->", color="#e74c3c", lw=2.0))
ax.text((5.5 + 13.5)/2 * w + 0.3, y_seg + 0.78, "Fenêtre de réception (RWND = 8 segments)",
        ha="center", va="bottom", fontsize=9, color="#e74c3c", fontweight="bold")

# Pointeur d'envoi
ax.annotate("", xy=(5.5 * w + 0.3, y_seg - 0.55), xytext=(5.5 * w + 0.3, y_seg - 0.95),
            arrowprops=dict(arrowstyle="-|>", color="#27ae60", lw=2))
ax.text(5.5 * w + 0.3, y_seg - 1.0, "SND.UNA\n(dernier ACK)", ha="center",
        fontsize=7.5, color="#27ae60")

ax.set_title("Fenêtre glissante : état courant", fontsize=10, fontweight="bold")

# ---- Évolution temporelle ----
ax2 = axes[1]
ax2.set_xlim(0, 12)
ax2.set_ylim(-0.5, 7)
ax2.set_xlabel("Temps")
ax2.set_ylabel("Numéro de séquence")
ax2.set_title("Échange de segments avec acquittements", fontsize=10, fontweight="bold")

echanges = [
    # (t_src, t_dst, seq, type, x_src, x_dst)
    (0.5, 1.5,  1, "DATA",  1.5, 10.5, "#2980b9"),
    (1.0, 2.0,  2, "DATA",  1.5, 10.5, "#2980b9"),
    (1.5, 2.5,  3, "DATA",  1.5, 10.5, "#2980b9"),
    (2.0, 2.8,  3, "ACK",  10.5,  1.5, "#27ae60"),
    (2.5, 3.3,  4, "DATA",  1.5, 10.5, "#2980b9"),
    (2.5, 3.3,  5, "DATA",  1.5, 10.5, "#e67e22"),
    (3.8, 4.5,  5, "ACK",  10.5,  1.5, "#27ae60"),
    (4.5, 5.5,  6, "DATA",  1.5, 10.5, "#2980b9"),
    (5.0, 5.8,  6, "ACK",  10.5,  1.5, "#27ae60"),
]

for t_src, t_dst, seq, typ, x_src, x_dst, color in echanges:
    ax2.annotate("", xy=(x_dst, t_dst), xytext=(x_src, t_src),
                 arrowprops=dict(arrowstyle="-|>", color=color, lw=1.8))
    ax2.text((x_src+x_dst)/2, (t_src+t_dst)/2 + 0.05,
             f"{'SEQ' if typ == 'DATA' else 'ACK'}={seq}",
             ha="center", va="bottom", fontsize=7.5, color=color)

ax2.set_yticks([])
ax2.axvline(1.5, color="#2c3e50", linewidth=2, linestyle="dashed", alpha=0.5)
ax2.axvline(10.5, color="#2c3e50", linewidth=2, linestyle="dashed", alpha=0.5)
ax2.text(1.5, 6.7, "Client", ha="center", fontsize=10, fontweight="bold", color="#2c3e50")
ax2.text(10.5, 6.7, "Serveur", ha="center", fontsize=10, fontweight="bold", color="#2c3e50")

plt.tight_layout()
plt.show()
_images/9f235ea69ad191a770eee711adb3e3ae641fb8d7504cb985a810e1638a39c863.png

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) :

\[\text{Données en vol} \leq \min(\text{RWND}, \text{CWND})\]

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).

Hide code cell source

fig, axes = plt.subplots(1, 2, figsize=(14, 7))
fig.suptitle("Contrôle de congestion TCP", fontsize=13, fontweight="bold")

# ---- Simulation TCP Reno ----
def simuler_tcp_reno(n_rtt: int = 60) -> tuple:
    """Simule l'évolution de CWND avec TCP Reno (Slow Start + Congestion Avoidance)."""
    cwnd = 1
    ssthresh = 32
    rtts = [0]
    cwnds = [cwnd]
    ssstreshs = [ssthresh]
    phases = ["slow_start"]

    events = {20: "loss_timeout", 40: "loss_3ack"}  # Événements simulés

    for rtt in range(1, n_rtt):
        phase = phases[-1]
        event = events.get(rtt)

        if event == "loss_timeout":
            ssthresh = max(cwnd // 2, 2)
            cwnd = 1
            phases.append("slow_start")
        elif event == "loss_3ack":
            ssthresh = max(cwnd // 2, 2)
            cwnd = ssthresh  # Fast Recovery : pas de retour à 1
            phases.append("congestion_avoidance")
        else:
            if phase == "slow_start":
                cwnd = min(cwnd * 2, ssthresh + 1)
                if cwnd >= ssthresh:
                    phases.append("congestion_avoidance")
                else:
                    phases.append("slow_start")
            else:
                cwnd += 1
                phases.append("congestion_avoidance")

        rtts.append(rtt)
        cwnds.append(cwnd)
        ssstreshs.append(ssthresh)

    return rtts, cwnds, ssstreshs, phases

rtts, cwnds, ssstreshs, phases = simuler_tcp_reno(60)

ax = axes[0]
# Colorer les phases
couleurs_phases = {"slow_start": "#e74c3c", "congestion_avoidance": "#2980b9"}
for i in range(len(rtts)-1):
    phase = phases[i]
    ax.fill_between(rtts[i:i+2], 0, cwnds[i:i+2],
                    color=couleurs_phases.get(phase, "#ccc"), alpha=0.15)
    ax.plot(rtts[i:i+2], cwnds[i:i+2],
            color=couleurs_phases.get(phase, "#ccc"), linewidth=2)

ax.plot(rtts, ssstreshs, color="#f39c12", linewidth=1.5, linestyle="--",
        label="ssthresh")
ax.axvline(20, color="#e74c3c", linewidth=1.5, linestyle=":", alpha=0.7)
ax.axvline(40, color="#e67e22", linewidth=1.5, linestyle=":", alpha=0.7)
ax.text(20, max(cwnds)*0.95, "Timeout\n(loss)", ha="center", fontsize=8,
        color="#e74c3c", bbox=dict(boxstyle="round", fc="white", ec="#e74c3c", alpha=0.8))
ax.text(40, max(cwnds)*0.95, "3 ACKs dup.\n(Fast Retransmit)", ha="center", fontsize=8,
        color="#e67e22", bbox=dict(boxstyle="round", fc="white", ec="#e67e22", alpha=0.8))

# Légende manuelle des phases
ax.add_patch(mpatches.Patch(color="#e74c3c", alpha=0.4, label="Slow Start"))
ax.add_patch(mpatches.Patch(color="#2980b9", alpha=0.4, label="Congestion Avoidance"))
ax.plot([], [], color="#f39c12", linestyle="--", label="ssthresh")
ax.legend(fontsize=9)

ax.set_xlabel("Nombre de RTT")
ax.set_ylabel("CWND (MSS)")
ax.set_title("Évolution de CWND — TCP Reno")
ax.grid(True, alpha=0.3)

# ---- Comparaison Reno vs CUBIC ----
def simuler_cubic(n_rtt: int = 60) -> list:
    """Approximation simplifiée de TCP CUBIC."""
    cwnd = 1.0
    ssthresh = 32
    W_max = 0
    t_congestion = 0
    C = 0.4
    cwnds = [cwnd]

    for rtt in range(1, n_rtt):
        event = {20: "loss_timeout", 40: "loss_3ack"}.get(rtt)

        if event:
            W_max = cwnd
            ssthresh = max(cwnd * 0.7, 2)  # CUBIC : β = 0.7
            cwnd = ssthresh
            t_congestion = rtt
        else:
            if cwnd < ssthresh:
                cwnd = min(cwnd * 2, ssthresh + 1)
            else:
                # Phase CUBIC
                t = rtt - t_congestion
                W_cubic = C * (t - (W_max * 0.3 / C) ** (1/3)) ** 3 + W_max
                cwnd = max(cwnd + 1, W_cubic)

        cwnds.append(cwnd)

    return cwnds

ax2 = axes[1]
cubic_cwnds = simuler_cubic(60)
ax2.plot(rtts, cwnds, color="#e74c3c", linewidth=2, label="TCP Reno", alpha=0.85)
ax2.plot(range(len(cubic_cwnds)), cubic_cwnds, color="#2980b9", linewidth=2,
         label="TCP CUBIC", linestyle="--", alpha=0.85)
ax2.axvline(20, color="#e74c3c", linewidth=1, linestyle=":", alpha=0.5)
ax2.axvline(40, color="#e67e22", linewidth=1, linestyle=":", alpha=0.5)
ax2.legend(fontsize=10)
ax2.set_xlabel("Nombre de RTT")
ax2.set_ylabel("CWND (MSS)")
ax2.set_title("Reno vs CUBIC — comportement comparé")
ax2.grid(True, alpha=0.3)
ax2.text(0.5, 0.05,
         "CUBIC : récupération plus rapide grâce\nà une fonction cubique du temps",
         transform=ax2.transAxes, fontsize=8, color="#2980b9",
         bbox=dict(boxstyle="round", fc="white", ec="#2980b9", alpha=0.8))

plt.tight_layout()
plt.show()
---------------------------------------------------------------------------
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
_images/79d7e143499a267af86842ac554286ca3000838462712fa773abfbae183a52d1.png

É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.

Hide code cell source

fig, ax = plt.subplots(figsize=(13, 12))
ax.set_xlim(0, 14)
ax.set_ylim(0, 13)
ax.axis("off")
ax.set_title("Machine d'états TCP — Tous les états et transitions",
             fontsize=13, fontweight="bold")

etats = {
    "CLOSED":       (7.0, 12.0),
    "LISTEN":       (3.0,  9.5),
    "SYN_SENT":     (11.0,  9.5),
    "SYN_RCVD":     (3.0,  7.0),
    "ESTABLISHED":  (7.0,  5.0),
    "FIN_WAIT_1":   (11.0,  3.5),
    "FIN_WAIT_2":   (11.0,  1.8),
    "CLOSE_WAIT":   (3.0,  3.5),
    "LAST_ACK":     (3.0,  1.8),
    "CLOSING":      (7.0,  3.0),
    "TIME_WAIT":    (11.0,  0.3),
}

couleurs_etats = {
    "CLOSED":       "#2c3e50",
    "LISTEN":       "#8e44ad",
    "SYN_SENT":     "#e74c3c",
    "SYN_RCVD":     "#c0392b",
    "ESTABLISHED":  "#27ae60",
    "FIN_WAIT_1":   "#e67e22",
    "FIN_WAIT_2":   "#e67e22",
    "CLOSE_WAIT":   "#2980b9",
    "LAST_ACK":     "#2980b9",
    "CLOSING":      "#f39c12",
    "TIME_WAIT":    "#e74c3c",
}

# Dessiner les états
for etat, (x, y) in etats.items():
    color = couleurs_etats[etat]
    ax.add_patch(mpatches.FancyBboxPatch((x - 1.1, y - 0.35), 2.2, 0.7,
                                          boxstyle="round,pad=0.08",
                                          edgecolor=color, facecolor=color, alpha=0.85, linewidth=2))
    ax.text(x, y, etat, ha="center", va="center",
            fontsize=9, fontweight="bold", color="white")

# Transitions
transitions = [
    # (src, dst, label, couleur, position_label_offset)
    ("CLOSED",    "LISTEN",    "passive open\n(bind/listen)", "#8e44ad", (-1.0, 0.0)),
    ("CLOSED",    "SYN_SENT",  "active open\n/SYN",          "#e74c3c", (1.0, 0.0)),
    ("LISTEN",    "SYN_RCVD",  "rcv SYN\n/SYN+ACK",         "#c0392b", (0.0, -0.3)),
    ("SYN_SENT",  "ESTABLISHED","rcv SYN+ACK\n/ACK",         "#27ae60", (0.5, 0.5)),
    ("SYN_RCVD",  "ESTABLISHED","rcv ACK",                    "#27ae60", (0.5, 0.5)),
    ("ESTABLISHED","FIN_WAIT_1","close\n/FIN",                "#e67e22", (1.0, 0.0)),
    ("ESTABLISHED","CLOSE_WAIT","rcv FIN\n/ACK",              "#2980b9", (-1.0, 0.0)),
    ("FIN_WAIT_1","FIN_WAIT_2","rcv ACK",                     "#e67e22", (0.5, 0.0)),
    ("FIN_WAIT_1","CLOSING",   "rcv FIN\n/ACK",               "#f39c12", (-0.5, 0.0)),
    ("FIN_WAIT_2","TIME_WAIT", "rcv FIN\n/ACK",               "#e74c3c", (0.5, 0.0)),
    ("CLOSE_WAIT","LAST_ACK",  "close\n/FIN",                 "#2980b9", (0.0, -0.3)),
    ("LAST_ACK",  "CLOSED",    "rcv ACK",                     "#2c3e50", (-1.0, 0.5)),
    ("CLOSING",   "TIME_WAIT", "rcv ACK",                     "#f39c12", (0.5, 0.0)),
    ("TIME_WAIT", "CLOSED",    "timeout\n(2×MSL)",            "#e74c3c", (0.5, 0.0)),
]

for src, dst, label, color, offset in transitions:
    x1, y1 = etats[src]
    x2, y2 = etats[dst]
    ax.annotate("", xy=(x2, y2), xytext=(x1, y1),
                arrowprops=dict(arrowstyle="-|>", color=color, lw=1.8,
                               connectionstyle="arc3,rad=0.1"))
    xm = (x1 + x2) / 2 + offset[0]
    ym = (y1 + y2) / 2 + offset[1]
    ax.text(xm, ym, label, ha="center", va="center", fontsize=6.5,
            color=color, fontstyle="italic",
            bbox=dict(boxstyle="round,pad=0.15", fc="white", ec=color, alpha=0.8))

# Légende
ax.text(0.3, 0.8, "États client (initiateur)", fontsize=8.5, color="#e74c3c", fontweight="bold")
ax.text(0.3, 0.4, "États serveur (passif)", fontsize=8.5, color="#2980b9", fontweight="bold")
ax.text(0.3, 0.0, "État normal d'échange", fontsize=8.5, color="#27ae60", fontweight="bold")

plt.tight_layout()
plt.show()

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é#

Hide code cell source

fig, ax = plt.subplots(figsize=(13, 7))
ax.axis("off")

data = {
    "Mécanisme": [
        "3-way handshake",
        "4-way close + TIME_WAIT",
        "Numéros de séquence",
        "Numéros d'acquittement",
        "Fenêtre glissante (RWND)",
        "Slow Start",
        "Congestion Avoidance",
        "Fast Retransmit",
        "TCP CUBIC",
    ],
    "Rôle": [
        "Établissement de la connexion, synchronisation ISN",
        "Fermeture ordonnée, protection contre segments résiduels",
        "Ordonnancement et détection des pertes",
        "Confirmation cumulative des octets reçus",
        "Contrôle de flux : empêche de saturer le récepteur",
        "Démarrage lent : CWND double par RTT jusqu'à ssthresh",
        "CWND += 1 MSS par RTT une fois ssthresh atteint",
        "Retransmission sur 3 ACKs dupliqués, sans attendre le timeout",
        "Récupération cubique après congestion (Linux défaut depuis 2.6.19)",
    ],
    "Signal/Valeur": [
        "SYN → SYN-ACK → ACK",
        "FIN → ACK → FIN → ACK, 2×MSL ≈ 60–240 s",
        "32 bits, ISN aléatoire pour sécurité",
        "ACK = prochain octet attendu",
        "max 65 535 o (×128 avec Window Scale)",
        "CWND = 1 MSS → doublé chaque RTT",
        "Croissance linéaire +1 MSS/RTT",
        "ssthresh = CWND/2, CWND = ssthresh",
        "β = 0.7 (vs 0.5 pour Reno), standard Linux",
    ],
}

df = pd.DataFrame(data)
tbl = ax.table(cellText=df.values, colLabels=df.columns,
               cellLoc="left", loc="center",
               colWidths=[0.22, 0.45, 0.33])
tbl.auto_set_font_size(False)
tbl.set_fontsize(8.0)
tbl.scale(1, 2.05)

for (row, col), cell in tbl.get_celld().items():
    if row == 0:
        cell.set_facecolor("#2c3e50")
        cell.set_text_props(color="white", fontweight="bold")
    elif row % 2 == 0:
        cell.set_facecolor("#eaf4fb")
    else:
        cell.set_facecolor("white")
    cell.set_edgecolor("#cccccc")

ax.set_title("Récapitulatif du chapitre 5 — TCP", fontsize=13, fontweight="bold", pad=15)
plt.tight_layout()
plt.show()

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…).