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.

Objectifs du chapitre

  • 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

Hide code cell source

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#

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))
 Version  Année   Connexions       Pipelining Header compress.               TLS
HTTP/0.9   1991   1 req/conn              Non              Non               Non
HTTP/1.0   1996   1 req/conn              Non              Non               Non
HTTP/1.1   1997 Persistantes  Optionnel (HoL)              Non         Optionnel
  HTTP/2   2015 Multiplexées          Streams            HPACK Quasi-obligatoire
  HTTP/3   2022     UDP/QUIC Streams sans HoL            QPACK    Intégré (QUIC)

HTTP/1.0 vs HTTP/1.1 : connexions persistantes#

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()
_images/3c66aec89a630f1a34b67854ab1ebc050b65f4a6475b6cffc00afdc3f1961789.png

Structure d’une requête HTTP#

# 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)
=== GET Request ===
GET /index.html HTTP/1.1
Host: example.com
User-Agent: Python-demo/1.0
Accept: text/html,application/json
Accept-Encoding: gzip, deflate
Connection: close


Taille : 161 octets

=== POST Request ===
POST /login HTTP/1.1
Host: example.com
User-Agent: Python-demo/1.0
Accept: text/html,application/json
Accept-Encoding: gzip, deflate
Connection: close
Content-Length: 30
Content-Type: application/x-www-form-urlencoded

username=alice&password=secret
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()
_images/5a4733c4984719c0b9f20f0d9d33e025d22249f074d2ea243740f6a577e5cf3d.png

Méthodes HTTP#

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))
Méthode Idempotente Corps requête Corps réponse                      Usage principal
    GET         Oui           Non           Oui                   Lire une ressource
   POST         Non           Oui           Oui        Créer / soumettre des données
    PUT         Oui           Oui           Oui      Remplacer une ressource entière
  PATCH         Non           Oui           Oui               Modifier partiellement
 DELETE         Oui     Optionnel     Non (204)              Supprimer une ressource
   HEAD         Oui           Non           Non    Obtenir les headers sans le corps
OPTIONS         Oui           Non           Oui Lister les méthodes acceptées (CORS)
CONNECT         Non           Non           Oui         Tunnel TCP (HTTPS via proxy)

Codes de statut HTTP#

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}")
────────────────────────────────────────────────────────────
  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
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()
_images/398fcf2e7a1de84e01ac3bda4e19158c8823b849f944b4a5bcac294b897008d3.png

Headers HTTP importants#

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))
                          Header   Direction                            Exemple / Description
                    Content-Type Req. + Rép.                  application/json; charset=utf-8
                          Accept     Requête                application/json, text/html;q=0.9
                   Authorization     Requête          Bearer eyJhbGci... | Basic dXNlcjpwYXNz
                   Cache-Control Req. + Rép.        no-store | max-age=3600 | must-revalidate
                            ETag     Réponse       "abc123def456" — empreinte de la ressource
                   If-None-Match     Requête       "abc123def456" — validation conditionnelle
             Cookie / Set-Cookie Req. / Rép.    sessionid=abc; HttpOnly; Secure; SameSite=Lax
CORS (Origin / Access-Control-*) Req. / Rép. Access-Control-Allow-Origin: https://example.com
               Transfer-Encoding     Réponse         chunked — envoi par morceaux (streaming)
                        Location     Réponse       https://example.com/new-url (avec 301/302)
# 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")
=== Requête HTTPS brute vers httpbin.org ===
Statut    : HTTP/1.1 200 OK
Headers   :
  date                          : Sat, 21 Mar 2026 11:16:47 GMT
  content-type                  : application/json
  content-length                : 176
  connection                    : close
  server                        : gunicorn/19.9.0
  access-control-allow-origin   : *
  access-control-allow-credentials: true
Corps     : 176 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#

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()
_images/c2063ecc710d26da7c3c3ac257fff248b7d368e727b13edb8c8df2a52d97e3c5.png

Frames HTTP/2#

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

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))
Type de frame Code                             Rôle
         DATA  0x0        Données du corps (stream)
      HEADERS  0x1  Headers HTTP compressés (HPACK)
     PRIORITY  0x2             Priorité d'un stream
   RST_STREAM  0x3                Annuler un stream
     SETTINGS  0x4       Paramètres de la connexion
 PUSH_PROMISE  0x5 Server push — prévenir le client
         PING  0x6   Keep-alive / mesure de latence
       GOAWAY  0x7   Fermer la connexion proprement
WINDOW_UPDATE  0x8  Contrôle de flux (flow control)
 CONTINUATION  0x9    Suite de HEADERS si fragmenté
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()
_images/15539dde6004faf0f8ca73a1a974740096e865df4120e1f6c1fc302677c05ec8.png

Client HTTP avec requests#

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}")
=== GET simple ===
Status     : 200 OK
URL finale : https://httpbin.org/get
Durée      : 918.4 ms
Encoding   : utf-8
Origin IP  : 78.240.107.246
Headers envoyés :
  Accept                        : */*
  Accept-Encoding               : gzip, deflate
  Host                          : httpbin.org
  User-Agent                    : python-requests/2.32.5
  X-Amzn-Trace-Id               : Root=1-69be7e20-23fd174b25be01d118467968
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}")
=== POST avec JSON ===
Status     : 200
JSON reçu  : {'action': 'login', 'timestamp': 1700000000, 'user': 'alice'}
Headers    : X-Custom-Header = demo

=== Gestion des redirections ===
Redirections suivies : 3
  1. 302 → /relative-redirect/2
  2. 302 → /relative-redirect/1
  3. 302 → /get
URL finale : https://httpbin.org/get (200)
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}")
=== Session HTTP (connexions persistantes) ===
  https://httpbin.org/get                  200 941.0 ms
  https://httpbin.org/uuid                 200 221.6 ms
  https://httpbin.org/ip                   200 455.1 ms
Total (avec session) : 1621.4 ms

=== Inspection des headers de réponse ===
Status : 200
  Access-Control-Allow-Credentials   : true
  Access-Control-Allow-Origin        : *
  Cache-Control                      : max-age=3600
  Connection                         : keep-alive
  Content-Length                     : 155
  Content-Type                       : application/json
  Date                               : Sat, 21 Mar 2026 11:16:55 GMT
  ETag                               : "abc123"
  Server                             : gunicorn/19.9.0
  X-Rate-Limit                       : 60
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}")
=== Authentification HTTP Basic ===
Basic Auth : 200 — {'authenticated': True, 'user': 'user'}

=== Gestion des cookies ===
Cookies après set : {'session_id': 'abc123', 'theme': 'dark'}
Cookies vus par le serveur : {'session_id': 'abc123', 'theme': 'dark'}

Visualisation comparative HTTP/1.1 vs HTTP/2#

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()
_images/1f7ce200462934a34583326cd7b10a725cc5cf31f34eb5892b5153342bb7e565.png

Cache HTTP et headers de validation#

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()
_images/257c08b5cd0683b398d9f611342f589a47bb8125b1b77a85600c9365829c5502.png

Résumé#

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()
_images/bc36e48e7ce8e80ef424acdc5b0cfef8b1f26589304f4b0c64fdb46d675228e1.png