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
requestsVisualiser les différences de performance entre HTTP/1.1 et HTTP/2
É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()
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()
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()
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()
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()
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()
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()
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()