---
jupytext:
  text_representation:
    extension: .md
    format_name: myst
    format_version: 0.13
    jupytext_version: 1.16.0
kernelspec:
  name: python3
  display_name: Python 3
---

# HTTP/1.1 et HTTP/2

HTTP (HyperText Transfer Protocol) est le protocole applicatif qui propulse le World Wide Web. Depuis sa première version formelle en 1991 jusqu'à HTTP/2 en 2015, le protocole a évolué en réponse aux besoins croissants de performance et de sécurité. Comprendre HTTP en profondeur, c'est comprendre pourquoi certaines pages chargent en 100 ms et d'autres en 3 secondes — et savoir comment corriger cela.

```{admonition} Objectifs du chapitre
:class: note
- Comprendre les évolutions HTTP/1.0 → 1.1 → 2
- Maîtriser la structure des requêtes et réponses HTTP
- Connaître les méthodes, codes de statut et headers importants
- Comprendre le multiplexage HTTP/2 et la compression HPACK
- Implémenter des clients HTTP robustes avec `requests`
- Visualiser les différences de performance entre HTTP/1.1 et HTTP/2
```

```{code-cell} python3
:tags: [hide-input]
import matplotlib
import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
from matplotlib.patches import FancyBboxPatch, Rectangle
import numpy as np
import pandas as pd
import seaborn as sns
import socket
import ssl
import time
import random

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

## Évolution HTTP/1.0 → 1.1 → 2

```{code-cell} python3
evolution = pd.DataFrame({
    "Version": ["HTTP/0.9", "HTTP/1.0", "HTTP/1.1", "HTTP/2", "HTTP/3"],
    "Année": [1991, 1996, 1997, 2015, 2022],
    "RFC": ["—", "1945", "2616/7230", "7540", "9114"],
    "Connexions": ["1 req/conn", "1 req/conn", "Persistantes", "Multiplexées", "UDP/QUIC"],
    "Pipelining": ["Non", "Non", "Optionnel (HoL)", "Streams", "Streams sans HoL"],
    "Header compress.": ["Non", "Non", "Non", "HPACK", "QPACK"],
    "TLS": ["Non", "Non", "Optionnel", "Quasi-obligatoire", "Intégré (QUIC)"],
    "Priorités": ["Non", "Non", "Non", "Oui", "Oui (révisé)"],
})

print(evolution[["Version", "Année", "Connexions", "Pipelining",
                  "Header compress.", "TLS"]].to_string(index=False))
```

### HTTP/1.0 vs HTTP/1.1 : connexions persistantes

```{code-cell} python3
fig, axes = plt.subplots(1, 2, figsize=(14, 5.5))

# ── HTTP/1.0 : une connexion par requête ─────────────────────────────────────
ax1 = axes[0]
ax1.set_xlim(0, 10)
ax1.set_ylim(0, 9)
ax1.axis("off")
ax1.set_title("HTTP/1.0 — 1 connexion par requête", fontweight="bold", color="#E87A4C")

ressources = ["HTML", "CSS", "JS", "Image 1", "Image 2"]
y_start = 8.3
for i, ressource in enumerate(ressources):
    y = y_start - i * 1.5
    # TCP connect
    ax1.annotate("", xy=(8, y - 0.2), xytext=(2, y),
                arrowprops=dict(arrowstyle="->", color="#888888", lw=1.2))
    ax1.text(5, y + 0.1, "TCP SYN", ha="center", fontsize=7, color="#888888")
    # Request
    ax1.annotate("", xy=(8, y - 0.5), xytext=(2, y - 0.3),
                arrowprops=dict(arrowstyle="->", color="#4C9BE8", lw=1.5))
    ax1.text(5, y - 0.2, f"GET /{ressource}", ha="center", fontsize=7.5,
             color="#4C9BE8", fontweight="bold")
    # Response
    ax1.annotate("", xy=(2, y - 0.85), xytext=(8, y - 0.65),
                arrowprops=dict(arrowstyle="->", color="#54B87A", lw=1.5))
    ax1.text(5, y - 0.75, f"200 OK ({ressource})", ha="center", fontsize=7.5,
             color="#54B87A", fontweight="bold")
    # Close
    ax1.plot([2, 8], [y - 0.95, y - 0.95], "--", color="#CCCCCC", lw=0.8)

ax1.text(2, 0.5, "Client", ha="center", fontsize=10, fontweight="bold", color="#2C3E50")
ax1.text(8, 0.5, "Serveur", ha="center", fontsize=10, fontweight="bold", color="#2C3E50")
ax1.text(5, 0.1, f"5 ressources = 5 connexions TCP = 5× latence handshake",
         ha="center", fontsize=8.5, color="#E87A4C",
         bbox=dict(facecolor="#FFF3E0", edgecolor="#E87A4C", boxstyle="round,pad=0.2"))

# ── HTTP/1.1 : connexions persistantes ───────────────────────────────────────
ax2 = axes[1]
ax2.set_xlim(0, 10)
ax2.set_ylim(0, 9)
ax2.axis("off")
ax2.set_title("HTTP/1.1 — Connexion persistante (Keep-Alive)", fontweight="bold",
              color="#54B87A")

# 1 seul handshake
ax2.annotate("", xy=(8, 8.1), xytext=(2, 8.3),
            arrowprops=dict(arrowstyle="->", color="#888888", lw=1.5))
ax2.text(5, 8.45, "1 seul TCP SYN/SYN-ACK/ACK", ha="center", fontsize=8,
         color="#888888", fontweight="bold")

ressources2 = ["HTML", "CSS", "JS", "Image 1", "Image 2"]
y_base = 7.5
for i, ressource in enumerate(ressources2):
    y = y_base - i * 1.3
    ax2.annotate("", xy=(8, y - 0.2), xytext=(2, y),
                arrowprops=dict(arrowstyle="->", color="#4C9BE8", lw=1.5))
    ax2.text(5, y + 0.1, f"GET /{ressource}", ha="center", fontsize=7.5,
             color="#4C9BE8", fontweight="bold")
    ax2.annotate("", xy=(2, y - 0.5), xytext=(8, y - 0.3),
                arrowprops=dict(arrowstyle="->", color="#54B87A", lw=1.5))
    ax2.text(5, y - 0.4, f"200 OK ({ressource})", ha="center", fontsize=7.5,
             color="#54B87A", fontweight="bold")

ax2.text(2, 0.5, "Client", ha="center", fontsize=10, fontweight="bold", color="#2C3E50")
ax2.text(8, 0.5, "Serveur", ha="center", fontsize=10, fontweight="bold", color="#2C3E50")
ax2.text(5, 0.1, "Connection: keep-alive — header Host: obligatoire",
         ha="center", fontsize=8.5, color="#54B87A",
         bbox=dict(facecolor="#F0FFF4", edgecolor="#54B87A", boxstyle="round,pad=0.2"))

plt.tight_layout()
plt.show()
```

## Structure d'une requête HTTP

```{code-cell} python3
# Construction manuelle d'une requête HTTP/1.1
def build_http_request(method: str, path: str, host: str,
                        headers: dict = None, body: str = "") -> str:
    """Construit une requête HTTP/1.1 brute."""
    lines = [f"{method} {path} HTTP/1.1", f"Host: {host}"]
    default_headers = {
        "User-Agent": "Python-demo/1.0",
        "Accept": "text/html,application/json",
        "Accept-Encoding": "gzip, deflate",
        "Connection": "close",
    }
    if headers:
        default_headers.update(headers)
    if body:
        default_headers["Content-Length"] = str(len(body.encode()))
        default_headers["Content-Type"] = "application/x-www-form-urlencoded"
    for k, v in default_headers.items():
        lines.append(f"{k}: {v}")
    lines.append("")  # Ligne vide = fin des headers
    if body:
        lines.append(body)
    return "\r\n".join(lines)

# Exemples de requêtes
req_get = build_http_request("GET", "/index.html", "example.com")
print("=== GET Request ===")
print(req_get)
print(f"\nTaille : {len(req_get)} octets")

print("\n=== POST Request ===")
req_post = build_http_request("POST", "/login", "example.com",
                               body="username=alice&password=secret")
print(req_post)
```

```{code-cell} python3
fig, ax = plt.subplots(figsize=(13, 5))
ax.set_xlim(0, 14)
ax.set_ylim(0, 8)
ax.axis("off")
ax.set_title("Anatomie d'une requête HTTP/1.1", fontsize=13, fontweight="bold")

# Colonne gauche : requête brute
raw_lines = [
    "POST /api/v1/login HTTP/1.1",
    "Host: api.example.com",
    "Content-Type: application/json",
    "Authorization: Bearer eyJhbGci...",
    "Accept: application/json",
    "Content-Length: 42",
    "Connection: keep-alive",
    "",
    '{"username": "alice", "password": "secret"}',
]

colors_lines = [
    "#E87A4C",  # Ligne de démarrage
    "#4C9BE8",  # Host
    "#4C9BE8",  # Content-Type
    "#4C9BE8",  # Authorization
    "#4C9BE8",  # Accept
    "#4C9BE8",  # Content-Length
    "#4C9BE8",  # Connection
    "#AAAAAA",  # Ligne vide
    "#54B87A",  # Corps
]

annotations = [
    "← Méthode  URL  Version",
    "← Header obligatoire (HTTP/1.1)",
    "← Type MIME du corps",
    "← Authentification Bearer (JWT)",
    "← Types acceptés en réponse",
    "← Taille du corps en octets",
    "",
    "← Ligne vide = fin des headers",
    "← Corps de la requête (JSON)",
]

for i, (line, color, annot) in enumerate(zip(raw_lines, colors_lines, annotations)):
    y = 7.2 - i * 0.72
    rect = FancyBboxPatch((0.3, y - 0.3), 7.2, 0.55,
                          boxstyle="round,pad=0.05", linewidth=1,
                          edgecolor=color, facecolor=color, alpha=0.2 if color == "#AAAAAA" else 0.12)
    ax.add_patch(rect)
    ax.text(0.5, y + 0.02, line, fontsize=8.5, color=color, fontweight="bold",
            fontfamily="monospace")
    if annot:
        ax.text(7.7, y + 0.02, annot, fontsize=8, color="#555555", style="italic")

# Légende des couleurs
legend_items = [
    ("#E87A4C", "Ligne de démarrage (request line)"),
    ("#4C9BE8", "Headers HTTP"),
    ("#54B87A", "Corps de la requête (body)"),
]
for i, (color, label) in enumerate(legend_items):
    ax.add_patch(FancyBboxPatch((0.3 + i*4.5, 0.2), 4.2, 0.45,
                                boxstyle="round,pad=0.05", facecolor=color, alpha=0.25,
                                edgecolor=color, linewidth=1.5))
    ax.text(2.5 + i*4.5, 0.43, label, ha="center", fontsize=8.5, color=color, fontweight="bold")

plt.tight_layout()
plt.show()
```

## Méthodes HTTP

```{code-cell} python3
methodes = pd.DataFrame({
    "Méthode": ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS", "CONNECT"],
    "Idempotente": ["Oui", "Non", "Oui", "Non", "Oui", "Oui", "Oui", "Non"],
    "Corps requête": ["Non", "Oui", "Oui", "Oui", "Optionnel", "Non", "Non", "Non"],
    "Corps réponse": ["Oui", "Oui", "Oui", "Oui", "Non (204)", "Non", "Oui", "Oui"],
    "Usage principal": [
        "Lire une ressource",
        "Créer / soumettre des données",
        "Remplacer une ressource entière",
        "Modifier partiellement",
        "Supprimer une ressource",
        "Obtenir les headers sans le corps",
        "Lister les méthodes acceptées (CORS)",
        "Tunnel TCP (HTTPS via proxy)"
    ]
})

print(methodes.to_string(index=False))
```

## Codes de statut HTTP

```{code-cell} python3
codes = {
    "1xx — Informatif": [
        (100, "Continue", "Le serveur a reçu les headers, le client peut envoyer le corps"),
        (101, "Switching Protocols", "Upgrade vers WebSocket ou HTTP/2"),
    ],
    "2xx — Succès": [
        (200, "OK", "Requête réussie"),
        (201, "Created", "Ressource créée (POST/PUT)"),
        (204, "No Content", "Succès sans corps de réponse (DELETE)"),
        (206, "Partial Content", "Réponse partielle (Range requests — streaming)"),
    ],
    "3xx — Redirection": [
        (301, "Moved Permanently", "Redirection permanente — cacheab"),
        (302, "Found", "Redirection temporaire"),
        (304, "Not Modified", "Contenu inchangé — utiliser le cache"),
        (307, "Temporary Redirect", "Comme 302 mais préserve la méthode"),
        (308, "Permanent Redirect", "Comme 301 mais préserve la méthode"),
    ],
    "4xx — Erreur client": [
        (400, "Bad Request", "Requête malformée"),
        (401, "Unauthorized", "Authentification requise"),
        (403, "Forbidden", "Accès refusé (authentifié mais non autorisé)"),
        (404, "Not Found", "Ressource introuvable"),
        (405, "Method Not Allowed", "Méthode non supportée"),
        (409, "Conflict", "Conflit de ressource (ex : doublon)"),
        (422, "Unprocessable Entity", "Validation échouée (REST APIs)"),
        (429, "Too Many Requests", "Rate limiting dépassé"),
    ],
    "5xx — Erreur serveur": [
        (500, "Internal Server Error", "Erreur générique côté serveur"),
        (502, "Bad Gateway", "Réponse invalide du backend"),
        (503, "Service Unavailable", "Serveur surchargé ou en maintenance"),
        (504, "Gateway Timeout", "Timeout de l'upstream"),
    ],
}

for categorie, items in codes.items():
    print(f"\n{'─'*60}")
    print(f"  {categorie}")
    print(f"{'─'*60}")
    for code, name, desc in items:
        print(f"  {code}  {name:<30} {desc}")
```

```{code-cell} python3
fig, axes = plt.subplots(1, 2, figsize=(14, 5))

# ── Répartition des codes par catégorie ──────────────────────────────────────
ax1 = axes[0]
categories = ["1xx\nInfo", "2xx\nSuccès", "3xx\nRedir.", "4xx\nErreur C", "5xx\nErreur S"]
counts = [3, 12, 8, 25, 10]  # Nombre de codes dans chaque catégorie
colors_codes = ["#AAAAAA", "#54B87A", "#4C9BE8", "#E87A4C", "#C96DD8"]
bars = ax1.bar(categories, counts, color=colors_codes, edgecolor="white", width=0.6)
ax1.set_ylabel("Nombre de codes définis")
ax1.set_title("Codes de statut HTTP — répartition", fontweight="bold")
for bar, v in zip(bars, counts):
    ax1.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 0.3,
             str(v), ha="center", fontsize=11, fontweight="bold")
ax1.grid(axis="y", alpha=0.4)

# ── Codes les plus courants (fréquence dans les logs web) ────────────────────
ax2 = axes[1]
common_codes = ["200 OK", "304 Not\nModified", "404 Not\nFound", "301 Moved\nPerm.",
                "403 Forbid.", "500 Server\nError", "302 Found", "429 Rate\nLimit"]
frequencies = [68, 12, 8, 4, 3, 2, 2, 1]
colors_f = ["#54B87A", "#4C9BE8", "#E87A4C", "#4C9BE8",
            "#E87A4C", "#C96DD8", "#4C9BE8", "#F0C040"]
bars2 = ax2.bar(common_codes, frequencies, color=colors_f, edgecolor="white", width=0.6)
ax2.set_ylabel("Fréquence typique (%)")
ax2.set_title("Codes les plus fréquents dans les logs web", fontweight="bold")
ax2.tick_params(axis="x", labelsize=8.5)
ax2.grid(axis="y", alpha=0.4)
for bar, v in zip(bars2, frequencies):
    ax2.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 0.3,
             f"{v}%", ha="center", fontsize=9, fontweight="bold")

plt.tight_layout()
plt.show()
```

## Headers HTTP importants

```{code-cell} python3
headers_importants = pd.DataFrame({
    "Header": [
        "Content-Type", "Accept", "Authorization", "Cache-Control",
        "ETag", "If-None-Match", "Cookie / Set-Cookie",
        "CORS (Origin / Access-Control-*)", "Transfer-Encoding", "Location"
    ],
    "Direction": [
        "Req. + Rép.", "Requête", "Requête", "Req. + Rép.",
        "Réponse", "Requête", "Req. / Rép.",
        "Req. / Rép.", "Réponse", "Réponse"
    ],
    "Exemple / Description": [
        "application/json; charset=utf-8",
        "application/json, text/html;q=0.9",
        "Bearer eyJhbGci... | Basic dXNlcjpwYXNz",
        "no-store | max-age=3600 | must-revalidate",
        '\"abc123def456\" — empreinte de la ressource',
        '\"abc123def456\" — validation conditionnelle',
        "sessionid=abc; HttpOnly; Secure; SameSite=Lax",
        "Access-Control-Allow-Origin: https://example.com",
        "chunked — envoi par morceaux (streaming)",
        "https://example.com/new-url (avec 301/302)"
    ]
})

print(headers_importants.to_string(index=False))
```

```{code-cell} python3
# Démonstration : requête HTTP brute avec socket + SSL
def raw_https_get(hostname: str, path: str = "/", timeout: float = 8) -> dict:
    """Effectue une requête HTTPS brute et parse la réponse."""
    ctx = ssl.create_default_context()
    result = {"status": None, "headers": {}, "body_size": 0}
    try:
        with socket.create_connection((hostname, 443), timeout=timeout) as raw:
            with ctx.wrap_socket(raw, server_hostname=hostname) as s:
                request = (
                    f"GET {path} HTTP/1.1\r\n"
                    f"Host: {hostname}\r\n"
                    "User-Agent: Python-raw/1.0\r\n"
                    "Accept: */*\r\n"
                    "Connection: close\r\n"
                    "\r\n"
                )
                s.sendall(request.encode())

                response = b""
                while True:
                    chunk = s.recv(8192)
                    if not chunk:
                        break
                    response += chunk
                    if len(response) > 50000:
                        break

        # Parser la réponse
        header_part, _, body = response.partition(b"\r\n\r\n")
        lines = header_part.decode("utf-8", errors="replace").split("\r\n")
        result["status"] = lines[0] if lines else "?"
        for line in lines[1:]:
            if ":" in line:
                k, _, v = line.partition(":")
                result["headers"][k.strip().lower()] = v.strip()
        result["body_size"] = len(body)

    except Exception as e:
        result["error"] = str(e)
    return result


print("=== Requête HTTPS brute vers httpbin.org ===")
resp = raw_https_get("httpbin.org", "/headers")
print(f"Statut    : {resp.get('status', '?')}")
print(f"Headers   :")
for k, v in list(resp.get("headers", {}).items())[:8]:
    print(f"  {k:<30}: {v[:60]}")
print(f"Corps     : {resp.get('body_size', 0)} octets")
```

## HTTP/2 : multiplexage et HPACK

HTTP/2 (RFC 7540) résout les limitations fondamentales de HTTP/1.1 en introduisant un protocole binaire avec multiplexage des streams.

### Problème du Head-of-Line Blocking (HoL) en HTTP/1.1

```{code-cell} python3
fig, axes = plt.subplots(2, 1, figsize=(13, 7))

# ── HTTP/1.1 avec pipelining — HoL Blocking ──────────────────────────────────
ax1 = axes[0]
ax1.set_xlim(0, 20)
ax1.set_ylim(-0.5, 3.5)
ax1.set_title("HTTP/1.1 — 6 connexions parallèles max, Head-of-Line Blocking",
              fontweight="bold", color="#E87A4C")
ax1.set_xlabel("Temps (ms)")
ax1.set_yticks([0, 1, 2])
ax1.set_yticklabels(["Conn. 3", "Conn. 2", "Conn. 1"])
ax1.grid(axis="x", alpha=0.3)

conn_tasks = [
    # connexion, ressource, start, duration, color
    (0, "HTML (bloquant !)", 1, 5, "#E87A4C"),
    (0, "JS 1 (attend HTML)", 6, 3, "#F0C040"),
    (0, "CSS (attend JS)", 9, 2, "#F0C040"),
    (1, "JS 2", 1, 4, "#4C9BE8"),
    (1, "Image 1", 5, 6, "#4C9BE8"),
    (2, "Image 2", 1, 8, "#54B87A"),
]

for conn, label, start, dur, color in conn_tasks:
    ax1.barh(conn, dur, left=start, color=color, edgecolor="white", height=0.5)
    ax1.text(start + dur/2, conn, label, ha="center", va="center",
             fontsize=7.5, fontweight="bold", color="white")

ax1.axvline(14, color="#C96DD8", lw=2, linestyle="--")
ax1.text(14.1, 3.1, "Chargement complet ≈ 14ms", fontsize=9, color="#C96DD8", fontweight="bold")

# ── HTTP/2 — Multiplexage sur 1 connexion ────────────────────────────────────
ax2 = axes[1]
ax2.set_xlim(0, 20)
ax2.set_ylim(-0.5, 6.5)
ax2.set_title("HTTP/2 — 1 connexion, streams multiplexés simultanément",
              fontweight="bold", color="#54B87A")
ax2.set_xlabel("Temps (ms)")
ax2.set_yticks(range(6))
ax2.set_yticklabels([f"Stream {i+1}" for i in range(6)])
ax2.grid(axis="x", alpha=0.3)

# Handshake TLS partagé
for y in range(6):
    ax2.barh(y, 1.5, left=0, color="#CCCCCC", edgecolor="white", height=0.5)
    ax2.text(0.75, y, "TLS", ha="center", va="center", fontsize=7, color="#555")

h2_tasks = [
    (0, "HTML", 1.5, 5, "#E87A4C"),
    (1, "JS 2", 1.5, 4, "#4C9BE8"),
    (2, "Image 2", 1.5, 8, "#54B87A"),
    (3, "JS 1", 1.5, 3, "#4C9BE8"),
    (4, "Image 1", 1.5, 6, "#54B87A"),
    (5, "CSS", 1.5, 2, "#C96DD8"),
]
for stream, label, start, dur, color in h2_tasks:
    ax2.barh(stream, dur, left=start, color=color, edgecolor="white", height=0.5)
    ax2.text(start + dur/2, stream, label, ha="center", va="center",
             fontsize=7.5, fontweight="bold", color="white")

ax2.axvline(9.5, color="#C96DD8", lw=2, linestyle="--")
ax2.text(9.6, 6.1, "Chargement complet ≈ 9.5ms", fontsize=9, color="#C96DD8", fontweight="bold")

plt.tight_layout()
plt.show()
```

### Frames HTTP/2

HTTP/2 est un protocole **binaire** : toutes les communications sont découpées en frames.

```{code-cell} python3
frames_h2 = pd.DataFrame({
    "Type de frame": ["DATA", "HEADERS", "PRIORITY", "RST_STREAM",
                      "SETTINGS", "PUSH_PROMISE", "PING", "GOAWAY",
                      "WINDOW_UPDATE", "CONTINUATION"],
    "Code": ["0x0", "0x1", "0x2", "0x3", "0x4", "0x5", "0x6", "0x7", "0x8", "0x9"],
    "Rôle": [
        "Données du corps (stream)",
        "Headers HTTP compressés (HPACK)",
        "Priorité d'un stream",
        "Annuler un stream",
        "Paramètres de la connexion",
        "Server push — prévenir le client",
        "Keep-alive / mesure de latence",
        "Fermer la connexion proprement",
        "Contrôle de flux (flow control)",
        "Suite de HEADERS si fragmenté"
    ]
})

print(frames_h2.to_string(index=False))
```

```{code-cell} python3
fig, axes = plt.subplots(1, 2, figsize=(14, 5))

# ── Structure d'une frame HTTP/2 ─────────────────────────────────────────────
ax = axes[0]
ax.set_xlim(0, 10)
ax.set_ylim(0, 6)
ax.axis("off")
ax.set_title("Structure d'une frame HTTP/2 (9 octets d'en-tête)", fontweight="bold")

frame_fields = [
    (0, 3, 1.0, "Longueur (24 bits)\nTaille du payload", "#4C9BE8"),
    (3, 1, 1.0, "Type\n(8 bits)", "#E87A4C"),
    (4, 1, 1.0, "Flags\n(8 bits)", "#54B87A"),
    (5, 4, 1.0, "Stream ID (31 bits)\n+ réservé (1 bit)", "#C96DD8"),
]

x = 0.5
for offset, width, height, label, color in frame_fields:
    w = width * 2.2
    r = FancyBboxPatch((x, 3.5), w, 1.2, boxstyle="round,pad=0.08",
                       linewidth=1.5, edgecolor="white", facecolor=color, alpha=0.85)
    ax.add_patch(r)
    ax.text(x + w/2, 4.1, label, ha="center", va="center",
            fontsize=8, fontweight="bold", color="white")
    x += w + 0.1

# Payload
r2 = FancyBboxPatch((0.5, 1.8), 9.0, 1.2, boxstyle="round,pad=0.08",
                    linewidth=1.5, edgecolor="#888", facecolor="#F0F4F8", alpha=0.9)
ax.add_patch(r2)
ax.text(5, 2.4, "Payload (0 à 16 384 octets max par défaut — négociable)",
        ha="center", va="center", fontsize=9, fontweight="bold", color="#333")

ax.text(5, 0.8, "En-tête HTTP/2 : 9 octets (vs 20+ pour TCP, 8 pour UDP)",
        ha="center", fontsize=9, color="#555",
        bbox=dict(facecolor="#F5F5F5", edgecolor="#AAAAAA", boxstyle="round,pad=0.2"))

# ── HPACK — Compression des headers ─────────────────────────────────────────
ax2 = axes[1]
ax2.set_xlim(0, 10)
ax2.set_ylim(0, 7)
ax2.axis("off")
ax2.set_title("HPACK — Compression des headers HTTP/2", fontweight="bold")

hpack_items = [
    (6.5, "Table statique (61 entrées)\nHeaders standards pré-définis\n:method: GET → index 2\n:status: 200 → index 8",
     "#4C9BE8"),
    (3.5, "Table dynamique\nHeaders récurrents mémorisés\npar client et serveur\n(window size configurable)",
     "#54B87A"),
    (1.0, "Huffman coding\nCompression bit-level\ndes valeurs de headers\n(~30% de gain supplémentaire)",
     "#E87A4C"),
]

for y, text, color in hpack_items:
    r = FancyBboxPatch((0.5, y - 1.1), 9, 1.3, boxstyle="round,pad=0.1",
                       linewidth=1.5, edgecolor=color, facecolor=color, alpha=0.85)
    ax2.add_patch(r)
    ax2.text(5, y - 0.45, text, ha="center", va="center",
             fontsize=8, fontweight="bold", color="white")

# Résultat
ax2.text(5, 0.4, "Résultat : headers compressés de 50–90% vs HTTP/1.1",
         ha="center", fontsize=9.5, fontweight="bold", color="#2C3E50",
         bbox=dict(facecolor="#EFF7FF", edgecolor="#4C9BE8", boxstyle="round,pad=0.3"))

plt.tight_layout()
plt.show()
```

## Client HTTP avec requests

```{code-cell} python3
try:
    import requests
    REQUESTS_AVAILABLE = True
except ImportError:
    REQUESTS_AVAILABLE = False
    print("requests non installé — démonstration avec urllib.request")

if REQUESTS_AVAILABLE:
    import requests
    from requests.adapters import HTTPAdapter
    import urllib3

    # Désactiver les avertissements SSL dans certains contextes de démo
    # urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)

    # ── GET simple ───────────────────────────────────────────────────────────
    print("=== GET simple ===")
    try:
        resp = requests.get("https://httpbin.org/get", timeout=10)
        print(f"Status     : {resp.status_code} {resp.reason}")
        print(f"URL finale : {resp.url}")
        print(f"Durée      : {resp.elapsed.total_seconds()*1000:.1f} ms")
        print(f"Encoding   : {resp.encoding}")
        data = resp.json()
        print(f"Origin IP  : {data.get('origin', '?')}")
        print(f"Headers envoyés :")
        for k, v in list(data.get("headers", {}).items())[:5]:
            print(f"  {k:<30}: {v[:50]}")
    except Exception as e:
        print(f"Erreur : {e}")
```

```{code-cell} python3
if REQUESTS_AVAILABLE:
    # ── POST JSON ────────────────────────────────────────────────────────────
    print("=== POST avec JSON ===")
    try:
        payload = {"user": "alice", "action": "login", "timestamp": 1700000000}
        resp = requests.post(
            "https://httpbin.org/post",
            json=payload,
            headers={"X-Custom-Header": "demo"},
            timeout=10
        )
        print(f"Status     : {resp.status_code}")
        data = resp.json()
        print(f"JSON reçu  : {data.get('json', '?')}")
        print(f"Headers    : X-Custom-Header = {data.get('headers', {}).get('X-Custom-Header', '?')}")
    except Exception as e:
        print(f"Erreur : {e}")

    # ── Redirections ─────────────────────────────────────────────────────────
    print("\n=== Gestion des redirections ===")
    try:
        resp = requests.get("https://httpbin.org/redirect/3", timeout=10, allow_redirects=True)
        print(f"Redirections suivies : {len(resp.history)}")
        for i, r in enumerate(resp.history, 1):
            print(f"  {i}. {r.status_code} → {r.headers.get('Location', '?')}")
        print(f"URL finale : {resp.url} ({resp.status_code})")
    except Exception as e:
        print(f"Erreur : {e}")
```

```{code-cell} python3
if REQUESTS_AVAILABLE:
    # ── Session et keep-alive ─────────────────────────────────────────────────
    print("=== Session HTTP (connexions persistantes) ===")
    session = requests.Session()
    session.headers.update({"User-Agent": "Python-demo/2.0"})
    session.timeout = 10

    urls = [
        "https://httpbin.org/get",
        "https://httpbin.org/uuid",
        "https://httpbin.org/ip",
    ]
    t0 = time.time()
    for url in urls:
        try:
            r = session.get(url, timeout=10)
            print(f"  {url:<40} {r.status_code} {r.elapsed.total_seconds()*1000:.1f} ms")
        except Exception as e:
            print(f"  {url:<40} Erreur : {e}")

    session.close()
    print(f"Total (avec session) : {(time.time()-t0)*1000:.1f} ms")

    # ── Headers de réponse importants ─────────────────────────────────────────
    print("\n=== Inspection des headers de réponse ===")
    try:
        resp = requests.get("https://httpbin.org/response-headers"
                            "?Cache-Control=max-age%3D3600"
                            "&ETag=%22abc123%22"
                            "&X-Rate-Limit=60", timeout=10)
        print(f"Status : {resp.status_code}")
        for k, v in sorted(resp.headers.items()):
            print(f"  {k:<35}: {v[:70]}")
    except Exception as e:
        print(f"Erreur : {e}")

else:
    # Fallback urllib
    import urllib.request, json
    print("=== GET via urllib.request ===")
    try:
        req = urllib.request.Request("https://httpbin.org/get",
                                     headers={"User-Agent": "Python-urllib/1.0"})
        with urllib.request.urlopen(req, timeout=10) as resp:
            print(f"Status : {resp.status}")
            data = json.loads(resp.read())
            print(f"Origin : {data.get('origin', '?')}")
    except Exception as e:
        print(f"Erreur : {e}")
```

```{code-cell} python3
if REQUESTS_AVAILABLE:
    # ── Authentification et cookies ───────────────────────────────────────────
    print("=== Authentification HTTP Basic ===")
    try:
        resp = requests.get("https://httpbin.org/basic-auth/user/pass",
                            auth=("user", "pass"), timeout=10)
        print(f"Basic Auth : {resp.status_code} — {resp.json()}")
    except Exception as e:
        print(f"Erreur : {e}")

    print("\n=== Gestion des cookies ===")
    try:
        with requests.Session() as s:
            # Définir un cookie
            r1 = s.get("https://httpbin.org/cookies/set?session_id=abc123&theme=dark",
                        timeout=10, allow_redirects=True)
            print(f"Cookies après set : {dict(s.cookies)}")

            # Vérifier que les cookies sont renvoyés
            r2 = s.get("https://httpbin.org/cookies", timeout=10)
            print(f"Cookies vus par le serveur : {r2.json().get('cookies', {})}")
    except Exception as e:
        print(f"Erreur : {e}")
```

## Visualisation comparative HTTP/1.1 vs HTTP/2

```{code-cell} python3
import random

def simulate_page_load(n_resources: int, protocol: str, rtt_ms: float = 50,
                        seed: int = 42) -> dict:
    """
    Simule le chargement d'une page avec n_resources ressources.
    - HTTP/1.1 : max 6 connexions parallèles, HoL blocking
    - HTTP/2   : toutes les ressources en parallèle sur 1 connexion
    """
    rng = random.Random(seed)
    resource_times = [max(5, rng.gauss(80, 30)) for _ in range(n_resources)]

    if protocol == "HTTP/1.1":
        # 1 handshake TCP + TLS = 2.5 RTT = 125 ms amortis sur 6 connexions
        # Séquentiel dans chaque connexion
        max_parallel = 6
        handshake_cost = 2.5 * rtt_ms  # par "batch" de connexions
        total = handshake_cost  # handshake initial
        chunks = [resource_times[i:i+max_parallel]
                  for i in range(0, len(resource_times), max_parallel)]
        for chunk in chunks:
            total += max(chunk)
        # HoL: si le 1er resource d'une conn est lent, les suivants attendent
        # (simplifié)
        total += rng.gauss(20, 10) * (n_resources // max_parallel)

    else:  # HTTP/2
        # 1 seul handshake TLS 1.3 (1 RTT)
        handshake_cost = 1.5 * rtt_ms
        # Toutes les ressources en parallèle (limité par la bande passante simulée)
        total = handshake_cost + max(resource_times)
        # Légère pénalité pour la compression/décompression
        total += rng.gauss(5, 2)

    return {
        "protocol": protocol,
        "resources": n_resources,
        "total_ms": max(0, total),
        "resource_times": resource_times,
    }

# Comparaison sur différentes tailles de pages
results = []
for n in [5, 10, 20, 40, 80]:
    for proto in ["HTTP/1.1", "HTTP/2"]:
        r = simulate_page_load(n, proto)
        results.append(r)

df_results = pd.DataFrame(results)

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

# ── Temps de chargement vs nombre de ressources ──────────────────────────────
ax1 = axes[0]
for proto, color in [("HTTP/1.1", "#E87A4C"), ("HTTP/2", "#54B87A")]:
    subset = df_results[df_results["protocol"] == proto]
    ax1.plot(subset["resources"], subset["total_ms"], "o-",
             color=color, label=proto, linewidth=2.5, markersize=8)

ax1.set_xlabel("Nombre de ressources de la page")
ax1.set_ylabel("Temps de chargement simulé (ms)")
ax1.set_title("HTTP/1.1 vs HTTP/2 — temps de chargement\nselon la taille de la page",
              fontweight="bold")
ax1.legend(fontsize=10)
ax1.grid(alpha=0.4)

# ── Gain HTTP/2 ───────────────────────────────────────────────────────────────
ax2 = axes[1]
resources_vals = [5, 10, 20, 40, 80]
gains = []
for n in resources_vals:
    t11 = df_results[(df_results["protocol"] == "HTTP/1.1") &
                     (df_results["resources"] == n)]["total_ms"].values[0]
    t2  = df_results[(df_results["protocol"] == "HTTP/2") &
                     (df_results["resources"] == n)]["total_ms"].values[0]
    gains.append((t11 - t2) / t11 * 100)

colors_g = ["#54B87A" if g > 0 else "#E87A4C" for g in gains]
bars = ax2.bar([str(n) for n in resources_vals], gains, color=colors_g,
               edgecolor="white", width=0.55)
ax2.set_xlabel("Nombre de ressources")
ax2.set_ylabel("Gain HTTP/2 (%)")
ax2.set_title("Gain de performance de HTTP/2\nvs HTTP/1.1 (simulé)", fontweight="bold")
ax2.grid(axis="y", alpha=0.4)
for bar, g in zip(bars, gains):
    ax2.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 0.5,
             f"{g:.1f}%", ha="center", fontsize=10, fontweight="bold")
ax2.axhline(0, color="#888", lw=1)

plt.tight_layout()
plt.show()
```

## Cache HTTP et headers de validation

```{code-cell} python3
fig, axes = plt.subplots(1, 2, figsize=(14, 5))

# ── Cache-Control ─────────────────────────────────────────────────────────────
ax1 = axes[0]
ax1.set_xlim(0, 10)
ax1.set_ylim(0, 8)
ax1.axis("off")
ax1.set_title("Cache-Control — Directives essentielles", fontweight="bold")

directives = [
    ("max-age=3600", "#4C9BE8", "Cache valide pendant 3600 secondes"),
    ("no-cache", "#F0C040", "Revalider avant chaque utilisation\n(ETag / If-None-Match)"),
    ("no-store", "#E87A4C", "Ne jamais mettre en cache\n(données sensibles)"),
    ("public", "#54B87A", "Cacheable par tout proxy/CDN"),
    ("private", "#C96DD8", "Cache navigateur uniquement\n(données personnalisées)"),
    ("immutable", "#888888", "Jamais modifié pour ce max-age\n(assets versionnés)"),
]

for i, (directive, color, desc) in enumerate(directives):
    y = 7.2 - i * 1.1
    r = FancyBboxPatch((0.5, y - 0.4), 3.5, 0.7, boxstyle="round,pad=0.1",
                       linewidth=1.5, edgecolor=color, facecolor=color, alpha=0.85)
    ax1.add_patch(r)
    ax1.text(2.25, y - 0.05, directive, ha="center", va="center",
             fontsize=9, fontweight="bold", color="white",
             fontfamily="monospace")
    ax1.text(4.3, y - 0.05, desc, ha="left", va="center",
             fontsize=8, color="#333333")

# ── Validation ETag / If-None-Match ──────────────────────────────────────────
ax2 = axes[1]
ax2.set_xlim(0, 10)
ax2.set_ylim(0, 8)
ax2.axis("off")
ax2.set_title("Validation conditionnelle — ETag", fontweight="bold")

etapes = [
    (2, 8, "1ère requête", "#4C9BE8", "→"),
    (7, 7.2, "200 OK + ETag: \"abc123\"\nContent-Length: 50000 o", "#54B87A", "←"),
    (2, 6, "2e requête (cache expiré)\nIf-None-Match: \"abc123\"", "#4C9BE8", "→"),
    (7, 5.2, "304 Not Modified\n(corps vide — 0 octet)", "#54B87A", "←"),
    (2, 4.0, "Navigateur utilise\nle contenu en cache", "#888888", ""),
    (2, 2.8, "Si contenu changé :\n200 OK + nouvel ETag\n+ nouveau corps", "#E87A4C", ""),
]

CLIENT_X2, SERVER_X2 = 1.5, 8.5
ax2.plot([CLIENT_X2, CLIENT_X2], [1.5, 8.5], "--", color="#CCCCCC", lw=1)
ax2.plot([SERVER_X2, SERVER_X2], [1.5, 8.5], "--", color="#CCCCCC", lw=1)
ax2.text(CLIENT_X2, 8.6, "Client", ha="center", fontsize=9, fontweight="bold")
ax2.text(SERVER_X2, 8.6, "Serveur", ha="center", fontsize=9, fontweight="bold")

msg_pos = [
    (CLIENT_X2, SERVER_X2, 8.0, "GET /page.html", "#4C9BE8"),
    (SERVER_X2, CLIENT_X2, 7.0, "200 OK + ETag", "#54B87A"),
    (CLIENT_X2, SERVER_X2, 5.8, "GET + If-None-Match", "#4C9BE8"),
    (SERVER_X2, CLIENT_X2, 4.8, "304 Not Modified", "#54B87A"),
]

for x1, x2, y, label, color in msg_pos:
    ax2.annotate("", xy=(x2, y), xytext=(x1, y),
                arrowprops=dict(arrowstyle="->", color=color, lw=1.8))
    ax2.text((x1+x2)/2, y + 0.2, label, ha="center", fontsize=8.5,
             color=color, fontweight="bold")

ax2.text(5, 3.6, "304 : pas de corps → économie de bande passante",
         ha="center", fontsize=8.5, color="#E87A4C",
         bbox=dict(facecolor="#FFF3E0", edgecolor="#E87A4C", boxstyle="round,pad=0.2"))
ax2.text(5, 2.8, "Last-Modified / If-Modified-Since : alternative à ETag",
         ha="center", fontsize=8, color="#555555",
         bbox=dict(facecolor="#F8F8F8", edgecolor="#AAAAAA", boxstyle="round,pad=0.2"))

plt.tight_layout()
plt.show()
```

## Résumé

```{code-cell} python3
fig, ax = plt.subplots(figsize=(12, 6))
ax.axis("off")
ax.set_title("Récapitulatif — HTTP/1.1 et HTTP/2", fontsize=14, fontweight="bold", pad=15)

resume = [
    ["HTTP/1.1 (RFC 7230)", "Connexions persistantes, Host: obligatoire, pipelining (HoL)"],
    ["HTTP/2 (RFC 7540)", "Multiplexage binaire, HPACK, server push, 1 connexion TLS"],
    ["Structure requête", "Ligne de démarrage + headers + ligne vide + corps optionnel"],
    ["Méthodes idempotentes", "GET, PUT, DELETE, HEAD, OPTIONS — safe : GET, HEAD"],
    ["Codes importants", "200 OK / 201 Created / 304 Not Modified / 404 / 429 / 503"],
    ["Cache-Control", "max-age, no-cache, no-store, public, private, immutable"],
    ["ETag / If-None-Match", "Validation conditionnelle — 304 évite de retransmettre le corps"],
    ["Authorization", "Bearer <token> | Basic base64(user:pass) | Digest"],
    ["CORS", "Access-Control-Allow-Origin — protection cross-origin côté navigateur"],
    ["requests.Session()", "Réutilise les connexions TCP/TLS — performance améliorée"],
    ["HoL Blocking HTTP/1.1", "1 ressource bloquée = attente pour les suivantes sur la même conn."],
    ["HTTP/2 multiplexage", "N streams simultanés sur 1 connexion — pas de HoL applicatif"],
]

table = ax.table(
    cellText=resume,
    colLabels=["Concept", "Description"],
    cellLoc="left",
    loc="center",
    colWidths=[0.28, 0.62]
)
table.auto_set_font_size(False)
table.set_fontsize(9)
table.scale(1, 1.65)

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()
```
