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

# WebSockets et Server-Sent Events

HTTP est fondamentalement un protocole **requête-réponse** : le client initie toujours la communication, le serveur répond, et la connexion peut être fermée. Ce modèle convient parfaitement aux pages web statiques ou aux API REST, mais se révèle inadapté pour les applications nécessitant du **temps réel** : messagerie instantanée, tableaux de bord en direct, jeux en ligne, flux de cotations boursières, notifications push.

Ce chapitre présente les deux approches majeures pour dépasser la barrière requête-réponse de HTTP : les **WebSockets** pour la communication bidirectionnelle plein-duplex, et les **Server-Sent Events (SSE)** pour les flux unidirectionnels serveur-vers-client. Nous commençons par analyser les stratégies historiques (polling, long polling) et leurs limites.

```{code-cell} python
:tags: [hide-input]

import numpy as np
import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
import matplotlib.lines as mlines
import seaborn as sns
import pandas as pd

sns.set_theme(style="whitegrid", palette="muted", font_scale=1.1)
np.random.seed(42)
```

## Les limites de HTTP pour le temps réel

### Polling classique

La stratégie la plus simple pour simuler du temps réel avec HTTP est le **polling** : le client envoie périodiquement des requêtes pour vérifier si de nouvelles données sont disponibles.

```
Client → Serveur : GET /nouvelles-donnees (t=0s)
Serveur → Client : 200 OK, rien de nouveau
Client → Serveur : GET /nouvelles-donnees (t=5s)
Serveur → Client : 200 OK, rien de nouveau
Client → Serveur : GET /nouvelles-donnees (t=10s)
Serveur → Client : 200 OK, nouvelle donnée ! {"valeur": 42}
```

**Inconvénients majeurs** :
- **Latence** : la donnée ne peut être reçue qu'au prochain cycle de polling. Avec un intervalle de 5s, la latence moyenne est de 2,5s.
- **Charge serveur** : des milliers de clients qui interrogent toutes les secondes génèrent un trafic énorme, même quand il n'y a rien de nouveau.
- **Gaspillage réseau** : la majorité des réponses sont vides ou indiquent "rien de nouveau".

### Long polling

Le **long polling** (ou Comet) est une amélioration : le serveur maintient la connexion ouverte jusqu'à ce qu'une nouvelle donnée soit disponible ou qu'un délai expire.

```
Client → Serveur : GET /evenements (maintient connexion ouverte)
... (le serveur attend qu'un événement se produise) ...
Serveur → Client : 200 OK {"message": "nouveau message"} (après 3s d'attente)
Client → Serveur : GET /evenements (immédiatement rouvert)
```

**Avantages** : latence réduite, moins de requêtes inutiles.
**Inconvénients** :
- Le serveur doit maintenir de nombreuses connexions HTTP en attente, consommant des ressources (threads, descripteurs de fichiers).
- La gestion du timeout, de la reconnexion, et des erreurs est complexe.
- HTTP/1.1 est conçu pour des connexions courtes ; les serveurs HTTP classiques (Apache prefork) allouent un thread par connexion.

```{code-cell} python
:tags: [hide-input]

# Comparaison visuelle : polling, long polling, WebSocket, SSE
fig, axes = plt.subplots(2, 2, figsize=(14, 10))
fig.suptitle("Stratégies de communication temps réel avec HTTP", fontsize=14, fontweight="bold")

def dessiner_diagramme(ax, titre, messages, couleur_client="#2196F3", couleur_serveur="#F44336"):
    ax.set_xlim(0, 10)
    ax.set_ylim(-0.5, len(messages) + 0.5)
    ax.set_title(titre, fontsize=11, fontweight="bold")
    ax.axis("off")

    ax.axvline(x=2, color=couleur_client, linewidth=2, ymin=0, ymax=1)
    ax.axvline(x=8, color=couleur_serveur, linewidth=2, ymin=0, ymax=1)
    ax.text(2, len(messages) + 0.3, "Client", ha="center", fontsize=10,
            fontweight="bold", color=couleur_client)
    ax.text(8, len(messages) + 0.3, "Serveur", ha="center", fontsize=10,
            fontweight="bold", color=couleur_serveur)

    for i, (direction, label, color, style) in enumerate(messages):
        y = len(messages) - 1 - i
        if direction == "→":
            x1, x2 = 2.1, 7.9
        else:
            x1, x2 = 7.9, 2.1
        ax.annotate("", xy=(x2, y), xytext=(x1, y),
                    arrowprops=dict(arrowstyle="->", color=color, lw=1.8,
                                   linestyle=style if style != "solid" else "-"))
        mid_x = 5
        ax.text(mid_x, y + 0.15, label, ha="center", fontsize=7.5, color=color)

# Polling
msgs_polling = [
    ("→", "GET /data (t=0s)", "#FF9800", "solid"),
    ("←", "204 No Content", "#9E9E9E", "solid"),
    ("→", "GET /data (t=1s)", "#FF9800", "solid"),
    ("←", "204 No Content", "#9E9E9E", "solid"),
    ("→", "GET /data (t=2s)", "#FF9800", "solid"),
    ("←", "204 No Content", "#9E9E9E", "solid"),
    ("→", "GET /data (t=3s)", "#FF9800", "solid"),
    ("←", "200 OK {\"msg\": \"hello\"}", "#4CAF50", "solid"),
    ("→", "GET /data (t=4s)", "#FF9800", "solid"),
    ("←", "204 No Content", "#9E9E9E", "solid"),
]
dessiner_diagramme(axes[0][0], "Polling (requêtes périodiques)", msgs_polling)

# Long Polling
msgs_longpoll = [
    ("→", "GET /data (connexion maintenue)", "#FF9800", "solid"),
    ("←", "... attente (3s) ...", "#9E9E9E", "dashed"),
    ("←", "200 OK {\"msg\": \"hello\"}", "#4CAF50", "solid"),
    ("→", "GET /data (reconnexion immédiate)", "#FF9800", "solid"),
    ("←", "... attente (2s) ...", "#9E9E9E", "dashed"),
    ("←", "200 OK {\"msg\": \"world\"}", "#4CAF50", "solid"),
    ("→", "GET /data (reconnexion immédiate)", "#FF9800", "solid"),
    ("←", "... attente ...", "#9E9E9E", "dashed"),
]
dessiner_diagramme(axes[0][1], "Long Polling (connexion maintenue)", msgs_longpoll)

# WebSocket
msgs_ws = [
    ("→", "GET /ws (Upgrade: websocket)", "#4CAF50", "solid"),
    ("←", "101 Switching Protocols", "#4CAF50", "solid"),
    ("→", "Frame: PING", "#2196F3", "solid"),
    ("←", "Frame: PONG", "#2196F3", "solid"),
    ("←", "Frame: TEXT {\"msg\": \"hello\"}", "#4CAF50", "solid"),
    ("→", "Frame: TEXT {\"ack\": true}", "#2196F3", "solid"),
    ("←", "Frame: TEXT {\"msg\": \"world\"}", "#4CAF50", "solid"),
    ("→", "Frame: CLOSE", "#F44336", "solid"),
    ("←", "Frame: CLOSE", "#F44336", "solid"),
]
dessiner_diagramme(axes[1][0], "WebSocket (full-duplex bidirectionnel)", msgs_ws)

# SSE
msgs_sse = [
    ("→", "GET /events (Accept: text/event-stream)", "#9C27B0", "solid"),
    ("←", "200 OK (Content-Type: text/event-stream)", "#9C27B0", "solid"),
    ("←", "data: {\"msg\": \"hello\"}\\n\\n", "#4CAF50", "solid"),
    ("←", "data: {\"msg\": \"world\"}\\n\\n", "#4CAF50", "solid"),
    ("←", "id: 42\\ndata: {\"value\": 99}\\n\\n", "#4CAF50", "solid"),
    ("←", ": ping (commentaire keepalive)", "#9E9E9E", "dashed"),
    ("←", "data: {\"msg\": \"fin\"}\\n\\n", "#4CAF50", "solid"),
]
dessiner_diagramme(axes[1][1], "Server-Sent Events (flux serveur → client)", msgs_sse)

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

## WebSocket : protocole et handshake

### Le handshake HTTP Upgrade

WebSocket (RFC 6455) démarre par un handshake HTTP ordinaire, puis **upgraide** la connexion vers le protocole WebSocket. L'avantage est que WebSocket peut passer à travers les proxys HTTP et les pare-feux qui autorisent HTTP/HTTPS sur les ports 80/443.

**Requête du client** :
```http
GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Extensions: permessage-deflate
```

**Réponse du serveur** :
```http
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Sec-WebSocket-Protocol: chat
```

Le code de statut **101 Switching Protocols** signale que la connexion HTTP est désormais une connexion WebSocket plein-duplex.

### Calcul du Sec-WebSocket-Accept

La validation cryptographique du handshake utilise SHA-1 :

```
Sec-WebSocket-Accept = Base64(SHA1(Sec-WebSocket-Key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"))
```

La valeur `258EAFA5-...` est un GUID fixé dans la RFC. Ce mécanisme évite qu'un serveur HTTP classique accepte accidentellement une connexion WebSocket.

```{code-cell} python
:tags: [hide-input]

import hashlib
import base64

def calculer_ws_accept(key: str) -> str:
    """Calcule le Sec-WebSocket-Accept à partir du Sec-WebSocket-Key."""
    GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
    concatene = key + GUID
    sha1 = hashlib.sha1(concatene.encode("utf-8")).digest()
    return base64.b64encode(sha1).decode("utf-8")

# Exemple de la RFC 6455
key_exemple = "dGhlIHNhbXBsZSBub25jZQ=="
accept = calculer_ws_accept(key_exemple)
print(f"Sec-WebSocket-Key  : {key_exemple}")
print(f"Sec-WebSocket-Accept : {accept}")
print(f"RFC 6455 attendu   : s3pPLMBiTxaQ9kYGzzhZRbK+xOo=")
print(f"Correspondance     : {accept == 's3pPLMBiTxaQ9kYGzzhZRbK+xOo='}")
```

### Format des frames WebSocket

Après le handshake, les données sont échangées sous forme de **frames** WebSocket. Le format binaire d'une frame est :

```
 0               1               2               3
 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7
+-+-+-+-+-------+-+-------------+-------------------------------+
|F|R|R|R| opcode|M| Payload len |    Extended payload length    |
|I|S|S|S|  (4)  |A|     (7)    |             (16/64)           |
|N|V|V|V|       |S|             |                               |
| |1|2|3|       |K|             |                               |
+-+-+-+-+-------+-+-------------+-------------------------------+
|     Extended payload length continued, if payload len == 127  |
+--------------------------------+-------------------------------+
|          Masking-key (si MASK=1, 4 octets)                    |
+--------------------------------+-------------------------------+
|          Payload Data                                         |
+---------------------------------------------------------------+
```

Champs principaux :
- **FIN** : 1 si c'est le dernier fragment
- **Opcode** : type de frame (voir tableau)
- **MASK** : 1 si les données sont masquées (obligatoire pour client→serveur)
- **Payload length** : longueur des données

**Opcodes principaux** :
| Opcode | Nom | Description |
|--------|-----|-------------|
| 0x0 | Continuation | Fragment de message |
| 0x1 | Text | Texte UTF-8 |
| 0x2 | Binary | Données binaires |
| 0x8 | Close | Fermeture de connexion |
| 0x9 | Ping | Keepalive (ping) |
| 0xA | Pong | Réponse au ping |

```{code-cell} python
:tags: [hide-input]

# Visualisation : format binaire d'une frame WebSocket
fig, ax = plt.subplots(figsize=(13, 4))
ax.set_xlim(0, 32)
ax.set_ylim(-1, 5)
ax.axis("off")
ax.set_title("Format d'une frame WebSocket (message texte court)", fontsize=12, fontweight="bold")

# Champs de la frame
champs = [
    # (x_start, largeur, label, valeur, couleur)
    (0,  1, "FIN\n(1b)", "1", "#4CAF50"),
    (1,  3, "RSV\n(3b)", "000", "#9E9E9E"),
    (4,  4, "Opcode\n(4b)", "0001\n(Text)", "#2196F3"),
    (8,  1, "MASK\n(1b)", "1\n(client)", "#FF9800"),
    (9,  7, "Payload\nlen (7b)", "0000101\n(=5)", "#9C27B0"),
    (16, 16, "Masking Key (32 bits = 4 octets)", "0x37FA213D", "#F44336"),
]

for x, w, label, valeur, color in champs:
    rect = mpatches.FancyBboxPatch((x, 2.2), w - 0.1, 1.5,
                                    boxstyle="round,pad=0.05",
                                    facecolor=color, edgecolor="white",
                                    alpha=0.85, linewidth=2)
    ax.add_patch(rect)
    ax.text(x + w/2, 3.5, label, ha="center", va="center",
            fontsize=7, color="white", fontweight="bold")
    ax.text(x + w/2, 2.6, valeur, ha="center", va="center",
            fontsize=7, color="white")

# Payload masqué
rect_payload = mpatches.FancyBboxPatch((0, 0.3), 32 - 0.1, 1.5,
                                        boxstyle="round,pad=0.05",
                                        facecolor="#607D8B", edgecolor="white",
                                        alpha=0.85, linewidth=2)
ax.add_patch(rect_payload)
ax.text(16, 1.05, "Payload Data (5 octets, masqués XOR avec Masking Key)\n\"Hello\" → 0x7f 0x9f 0x4d 0x51 0x58",
        ha="center", va="center", fontsize=9, color="white")

ax.text(16, -0.5, "Taille totale de la frame : 2 (header) + 4 (masque) + 5 (payload) = 11 octets",
        ha="center", fontsize=9, color="#37474F", style="italic")

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

### Masquage des frames client→serveur

Le standard RFC 6455 impose que toutes les frames envoyées par le **client** soient **masquées** avec un masque de 4 octets aléatoires. Ce masque est inclus dans la frame et chaque octet de payload est XORé avec l'octet correspondant du masque (cycliquement).

Ce masquage protège contre les attaques par **cache poisoning** : un attaquant contrôlant du contenu dans le navigateur ne peut pas injecter des données WebSocket qui ressembleraient à des réponses HTTP valides, même si un proxy cache est interposé.

Le serveur ne masque **jamais** ses frames.

## Code Python : serveur et client WebSocket

### Serveur broadcast avec la bibliothèque websockets

```python
import asyncio
import websockets
import json
from datetime import datetime

# Ensemble des connexions actives
CONNEXIONS = set()

async def gestionnaire(websocket):
    """Gère une connexion WebSocket individuelle."""
    CONNEXIONS.add(websocket)
    client_addr = websocket.remote_address
    print(f"[+] Nouveau client : {client_addr} — {len(CONNEXIONS)} connecté(s)")

    try:
        # Envoi d'un message de bienvenue
        await websocket.send(json.dumps({
            "type": "bienvenue",
            "message": f"Connecté au serveur. {len(CONNEXIONS)} client(s) en ligne.",
            "timestamp": datetime.now().isoformat(),
        }))

        async for message in websocket:
            # Diffusion à tous les clients connectés
            data = json.loads(message)
            print(f"[{client_addr}] → {data}")

            # Broadcast à tous sauf l'expéditeur
            reponse = json.dumps({
                "type": "message",
                "de": str(client_addr),
                "contenu": data.get("contenu", ""),
                "timestamp": datetime.now().isoformat(),
            })

            if CONNEXIONS:
                await asyncio.gather(
                    *[ws.send(reponse) for ws in CONNEXIONS if ws != websocket],
                    return_exceptions=True,
                )

    except websockets.exceptions.ConnectionClosed as e:
        print(f"[-] Client déconnecté : {client_addr} (code={e.code})")
    finally:
        CONNEXIONS.discard(websocket)

async def main():
    async with websockets.serve(gestionnaire, "localhost", 8765) as server:
        print("Serveur WebSocket en écoute sur ws://localhost:8765")
        await server.wait_closed()

asyncio.run(main())
```

### Client WebSocket avec ping/pong

```python
import asyncio
import websockets
import json
import time

async def client_chat():
    """Client WebSocket avec keepalive ping/pong automatique."""
    uri = "ws://localhost:8765"

    async with websockets.connect(
        uri,
        ping_interval=20,    # Envoi d'un PING toutes les 20s
        ping_timeout=10,     # Timeout si PONG non reçu en 10s
        close_timeout=5,
    ) as ws:
        print(f"Connecté à {uri}")

        # Réception du message de bienvenue
        bienvenue = await ws.recv()
        print(f"Serveur : {json.loads(bienvenue)['message']}")

        # Envoi de messages et réception simultanée
        async def envoyer():
            for i in range(5):
                msg = json.dumps({"contenu": f"Message #{i+1}"})
                await ws.send(msg)
                print(f"Envoyé : Message #{i+1}")
                await asyncio.sleep(1)

        async def recevoir():
            async for message in ws:
                data = json.loads(message)
                if data["type"] == "message":
                    print(f"Reçu de {data['de']} : {data['contenu']}")

        # Lancement concurrent de l'envoi et de la réception
        await asyncio.gather(envoyer(), recevoir())

asyncio.run(client_chat())
```

### Mesure de latence WebSocket

```python
import asyncio
import websockets
import time
import statistics

async def mesurer_latence(n_mesures=100):
    """Mesure la latence aller-retour d'une connexion WebSocket."""
    async with websockets.connect("ws://localhost:8765") as ws:
        latences = []

        for i in range(n_mesures):
            debut = time.perf_counter()
            await ws.send(json.dumps({"type": "ping", "t": debut}))
            reponse = await ws.recv()
            fin = time.perf_counter()
            latences.append((fin - debut) * 1000)  # ms

        print(f"Latence sur {n_mesures} messages :")
        print(f"  Moyenne : {statistics.mean(latences):.2f} ms")
        print(f"  Médiane : {statistics.median(latences):.2f} ms")
        print(f"  P95     : {sorted(latences)[int(0.95*n_mesures)]:.2f} ms")
        print(f"  Min/Max : {min(latences):.2f} / {max(latences):.2f} ms")

asyncio.run(mesurer_latence())
```

```{code-cell} python
:tags: [hide-input]

# Simulation de latences WebSocket vs HTTP polling
fig, axes = plt.subplots(1, 2, figsize=(14, 5))

np.random.seed(42)
n = 500

# Simulation de latences (ms)
latences_ws = np.abs(np.random.normal(2.5, 0.8, n))           # WebSocket local
latences_ws_reseau = np.abs(np.random.normal(15, 5, n))        # WebSocket réseau
latences_polling_1s = np.random.uniform(0, 1000, n)            # Polling 1s
latences_polling_5s = np.random.uniform(0, 5000, n)            # Polling 5s
latences_longpoll = np.abs(np.random.exponential(300, n))      # Long polling

ax1 = axes[0]
data_plot = {
    "WebSocket\n(local)": latences_ws,
    "WebSocket\n(réseau)": latences_ws_reseau,
    "Long polling": latences_longpoll[:n],
    "Polling 1s": latences_polling_1s,
    "Polling 5s": latences_polling_5s,
}

positions = range(len(data_plot))
colors = ["#4CAF50", "#8BC34A", "#FF9800", "#F44336", "#9C27B0"]

bp = ax1.boxplot(list(data_plot.values()), positions=list(positions),
                 patch_artist=True, showfliers=False,
                 medianprops=dict(color="white", linewidth=2))

for patch, color in zip(bp["boxes"], colors):
    patch.set_facecolor(color)
    patch.set_alpha(0.8)

ax1.set_xticks(list(positions))
ax1.set_xticklabels(list(data_plot.keys()), fontsize=9)
ax1.set_ylabel("Latence (ms)", fontsize=11)
ax1.set_title("Distribution des latences\npar méthode temps réel", fontsize=11)
ax1.set_yscale("log")
ax1.grid(True, alpha=0.3, axis="y")

# Graphique 2 : consommation bande passante
ax2 = axes[1]
clients = [10, 100, 1000, 10000]
# Messages/min par client
bw_polling = [c * 60 * 0.5 for c in clients]            # 60 req/min, 500 octets chacune
bw_longpoll = [c * 3 * 0.5 for c in clients]            # ~3 req/min
bw_ws = [c * 60 * 0.02 for c in clients]                 # Frame WebSocket ~20 octets keepalive

ax2.loglog(clients, bw_polling, "o-", label="Polling (1s)", color="#F44336", linewidth=2, markersize=7)
ax2.loglog(clients, bw_longpoll, "s-", label="Long polling", color="#FF9800", linewidth=2, markersize=7)
ax2.loglog(clients, bw_ws, "^-", label="WebSocket", color="#4CAF50", linewidth=2, markersize=7)

ax2.set_xlabel("Nombre de clients connectés", fontsize=11)
ax2.set_ylabel("Bande passante serveur (Ko/min)", fontsize=11)
ax2.set_title("Bande passante serveur\nselon la méthode et le nombre de clients", fontsize=11)
ax2.legend(fontsize=10)
ax2.grid(True, alpha=0.3, which="both")

# Conversion Ko
ax2_labels = ax2.get_yticks()
ax2.yaxis.set_major_formatter(plt.FuncFormatter(lambda x, _: f"{x/1000:.0f}Mo" if x >= 1000 else f"{x:.0f}Ko"))

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

## Server-Sent Events (SSE)

### Principe et format

Les **Server-Sent Events** (W3C, maintenant partie des standards HTML Living Standard) permettent au serveur d'envoyer un flux continu d'événements à un client HTTP. Contrairement à WebSocket, la communication est **unidirectionnelle** (serveur → client). Le client peut envoyer des données au serveur uniquement via des requêtes HTTP séparées.

Le client ouvre une requête HTTP avec `Accept: text/event-stream` et le serveur envoie un flux de données tant que la connexion est ouverte :

```http
GET /events HTTP/1.1
Host: example.com
Accept: text/event-stream
Cache-Control: no-cache
```

```http
HTTP/1.1 200 OK
Content-Type: text/event-stream
Cache-Control: no-cache
Connection: keep-alive
X-Accel-Buffering: no
```

**Format du flux SSE** :
```
: commentaire (ignoré par le client, utile comme keepalive)\n
\n
id: 1\n
event: temperature\n
data: {"capteur": "A1", "valeur": 23.4}\n
\n
id: 2\n
event: temperature\n
data: {"capteur": "A1", "valeur": 23.6}\n
\n
retry: 3000\n
\n
```

Chaque événement est séparé par une **ligne vide**. Les champs possibles sont :
- `data:` : données de l'événement (peut s'étendre sur plusieurs lignes)
- `event:` : type de l'événement (si omis, type par défaut `message`)
- `id:` : identifiant de l'événement
- `retry:` : délai de reconnexion en ms

### Reconnexion automatique et Last-Event-ID

L'un des grands avantages de SSE est sa **reconnexion automatique** gérée par le navigateur. Si la connexion est perdue, le navigateur attend `retry` millisecondes (par défaut 3000 ms) puis se reconnecte automatiquement en envoyant l'en-tête `Last-Event-ID` avec le dernier identifiant reçu. Le serveur peut ainsi reprendre le flux depuis le bon point.

```javascript
// Côté JavaScript (navigateur)
const evtSource = new EventSource("/events");

evtSource.addEventListener("temperature", (event) => {
    const data = JSON.parse(event.data);
    console.log(`Température : ${data.valeur}°C (capteur ${data.capteur})`);
});

evtSource.onerror = (err) => {
    console.error("Erreur SSE, reconnexion automatique dans 3s...");
};
```

### Serveur SSE avec http.server

```python
import http.server
import threading
import time
import json
import random
from datetime import datetime

class SSEHandler(http.server.BaseHTTPRequestHandler):
    """Serveur SSE minimaliste avec la stdlib Python."""

    def log_message(self, format, *args):
        pass  # Silence des logs HTTP

    def do_GET(self):
        if self.path == "/events":
            self.send_sse_stream()
        elif self.path == "/":
            self.send_html_page()
        else:
            self.send_error(404)

    def send_sse_stream(self):
        """Envoie un flux SSE de mesures de température simulées."""
        self.send_response(200)
        self.send_header("Content-Type", "text/event-stream")
        self.send_header("Cache-Control", "no-cache")
        self.send_header("Connection", "keep-alive")
        self.send_header("Access-Control-Allow-Origin", "*")
        self.end_headers()

        event_id = 0
        try:
            while True:
                event_id += 1
                temperature = 20 + random.gauss(0, 2)
                humidite = 50 + random.gauss(0, 5)

                # Format SSE : id, event, data, ligne vide
                evenement = (
                    f"id: {event_id}\n"
                    f"event: mesure\n"
                    f"data: {json.dumps({'t': temperature, 'h': humidite, 'ts': datetime.now().isoformat()})}\n"
                    f"\n"
                )
                self.wfile.write(evenement.encode("utf-8"))
                self.wfile.flush()

                # Commentaire keepalive toutes les 5 secondes
                if event_id % 5 == 0:
                    keepalive = ": keepalive\n\n"
                    self.wfile.write(keepalive.encode("utf-8"))
                    self.wfile.flush()

                time.sleep(1)

        except (BrokenPipeError, ConnectionResetError):
            print(f"Client déconnecté après {event_id} événements")

    def send_html_page(self):
        """Page HTML consommant le flux SSE."""
        html = """<!DOCTYPE html>
<html><head><title>SSE Demo</title></head>
<body>
<h1>Flux SSE temps réel</h1>
<div id="data">En attente...</div>
<script>
const es = new EventSource('/events');
es.addEventListener('mesure', e => {
    const d = JSON.parse(e.data);
    document.getElementById('data').innerHTML =
        `T: ${d.t.toFixed(1)}°C | H: ${d.h.toFixed(1)}% | ${d.ts}`;
});
</script>
</body></html>"""
        self.send_response(200)
        self.send_header("Content-Type", "text/html; charset=utf-8")
        self.end_headers()
        self.wfile.write(html.encode("utf-8"))

# Lancement du serveur dans un thread séparé
serveur = http.server.HTTPServer(("localhost", 8080), SSEHandler)
thread = threading.Thread(target=serveur.serve_forever, daemon=True)
thread.start()
print("Serveur SSE démarré sur http://localhost:8080")
print("Flux d'événements sur http://localhost:8080/events")
# serveur.shutdown()  # Pour arrêter
```

### Client SSE en Python

```python
import httpx
import json

def consommer_sse(url: str, max_events: int = 10):
    """Consomme un flux SSE avec httpx."""
    print(f"Connexion au flux SSE : {url}")

    with httpx.stream("GET", url, headers={"Accept": "text/event-stream"}) as response:
        print(f"Statut : {response.status_code}")
        print(f"Content-Type : {response.headers.get('content-type')}")

        buffer = ""
        n_events = 0

        for chunk in response.iter_text():
            buffer += chunk

            # Traitement des événements complets (séparés par \n\n)
            while "\n\n" in buffer:
                evenement, buffer = buffer.split("\n\n", 1)
                if evenement.strip():
                    lignes = evenement.strip().split("\n")
                    event = {}
                    for ligne in lignes:
                        if ligne.startswith(":"):
                            continue  # Commentaire
                        if ":" in ligne:
                            cle, _, valeur = ligne.partition(":")
                            event[cle.strip()] = valeur.strip()

                    if "data" in event:
                        n_events += 1
                        print(f"Événement #{n_events} [{event.get('event', 'message')}] : {event['data']}")
                        try:
                            data = json.loads(event["data"])
                            print(f"  → T={data['t']:.1f}°C, H={data['h']:.1f}%")
                        except (json.JSONDecodeError, KeyError):
                            pass

                    if n_events >= max_events:
                        print(f"\nArrêt après {max_events} événements.")
                        return

consommer_sse("http://localhost:8080/events")
```

```{code-cell} python
:tags: [hide-input]

# Simulation d'un flux SSE et visualisation
import matplotlib.animation
from matplotlib.lines import Line2D

# Données simulées d'un flux SSE de capteurs
np.random.seed(42)
n_points = 60  # 60 secondes de données

temps = np.arange(n_points)
temp_a = 22 + np.cumsum(np.random.normal(0, 0.3, n_points))
temp_b = 19 + np.cumsum(np.random.normal(0, 0.2, n_points))
humidite = 55 + np.cumsum(np.random.normal(0, 1, n_points))
humidite = np.clip(humidite, 20, 90)

fig, axes = plt.subplots(2, 1, figsize=(13, 6), sharex=True)
fig.suptitle("Simulation d'un flux SSE : données de capteurs IoT (60s)", fontsize=12, fontweight="bold")

ax1, ax2 = axes

ax1.plot(temps, temp_a, "-", color="#F44336", linewidth=2, label="Capteur A (salon)")
ax1.plot(temps, temp_b, "-", color="#2196F3", linewidth=2, label="Capteur B (chambre)")
ax1.fill_between(temps, temp_a - 0.5, temp_a + 0.5, alpha=0.2, color="#F44336")
ax1.fill_between(temps, temp_b - 0.5, temp_b + 0.5, alpha=0.2, color="#2196F3")

# Annotation des événements SSE
for t in range(0, n_points, 5):
    ax1.axvline(x=t, color="gray", linestyle=":", alpha=0.3)
    ax1.text(t, ax1.get_ylim()[0] if ax1.get_ylim() else 18,
             f"id:{t//5+1}", fontsize=6, color="gray", ha="center")

ax1.set_ylabel("Température (°C)", fontsize=11)
ax1.legend(fontsize=10, loc="upper right")
ax1.grid(True, alpha=0.3)

ax2.plot(temps, humidite, "-", color="#4CAF50", linewidth=2, label="Humidité (%)")
ax2.fill_between(temps, humidite - 2, humidite + 2, alpha=0.2, color="#4CAF50")
ax2.axhline(y=60, color="#FF9800", linestyle="--", linewidth=1.5, alpha=0.7, label="Seuil alerte (60%)")
ax2.set_xlabel("Temps (secondes)", fontsize=11)
ax2.set_ylabel("Humidité (%)", fontsize=11)
ax2.legend(fontsize=10)
ax2.grid(True, alpha=0.3)

# Simuler des événements SSE dans le format texte
plt.tight_layout()
plt.show()

# Afficher un exemple de flux SSE
print("\nExemple de flux SSE généré :")
print("=" * 50)
for i in range(3):
    event_id = i + 1
    t = temp_a[i]
    h = humidite[i]
    print(f"id: {event_id}")
    print(f"event: mesure")
    print(f'data: {{"capteurA": {t:.1f}, "capteurB": {temp_b[i]:.1f}, "humidite": {h:.1f}}}')
    print()
```

## Comparaison WebSocket vs SSE vs Polling

```{code-cell} python
:tags: [hide-input]

# Tableau de comparaison
fig, ax = plt.subplots(figsize=(14, 6))
ax.axis("off")

colonnes = ["Critère", "Polling", "Long Polling", "WebSocket", "SSE"]
lignes = [
    ["Direction", "Client → Serveur", "Client → Serveur", "Bidirectionnel", "Serveur → Client"],
    ["Protocole", "HTTP standard", "HTTP standard", "WS (Upgrade HTTP)", "HTTP standard"],
    ["Support navigateur", "Universel", "Universel", "Très large (IE10+)", "Large (pas IE natif)"],
    ["Latence", "Élevée (1-5s)", "Faible-moyenne", "Très faible (<10ms)", "Faible (<100ms)"],
    ["Connexions serveur", "Courtes, répétées", "Maintenues", "Maintenues (WS)", "Maintenues (HTTP)"],
    ["Reconnexion auto", "Par logique appli.", "Par logique appli.", "Bibliothèque", "Native (EventSource)"],
    ["Load balancer", "Facile (stateless)", "Difficile (sticky)", "Difficile (sticky)", "Possible (stateless)"],
    ["Cas d'usage", "Polling rare", "Notifications", "Chat, jeux, collab.", "Notifications, flux"],
    ["Complexité serveur", "Faible", "Moyenne", "Élevée", "Faible-Moyenne"],
    ["Overhead réseau", "Très élevé", "Moyen", "Très faible", "Faible"],
]

couleurs = []
for i, ligne in enumerate(lignes):
    row_colors = []
    for j, cell in enumerate(ligne):
        if j == 0:
            row_colors.append("#ECEFF1")
        elif j == 3:  # WebSocket
            row_colors.append("#E8F5E9")
        elif j == 4:  # SSE
            row_colors.append("#E3F2FD")
        elif i % 2 == 0:
            row_colors.append("#FAFAFA")
        else:
            row_colors.append("#F5F5F5")
    couleurs.append(row_colors)

table = ax.table(
    cellText=lignes,
    colLabels=colonnes,
    cellLoc="center",
    loc="center",
    cellColours=couleurs,
)
table.auto_set_font_size(False)
table.set_fontsize(8.5)
table.scale(1, 1.7)

col_colors = ["#37474F", "#F44336", "#FF9800", "#2E7D32", "#1565C0"]
for j, color in enumerate(col_colors):
    table[0, j].set_facecolor(color)
    table[0, j].set_text_props(color="white", fontweight="bold")

ax.set_title("Comparaison des méthodes de communication temps réel", fontsize=13, fontweight="bold", pad=20)
plt.tight_layout()
plt.show()
```

## Cas d'usage et choix architectural

### Quand choisir WebSocket ?

WebSocket est le choix approprié quand :
- La communication est **bidirectionnelle** avec un volume élevé de messages dans les deux sens (chat, collaboration en temps réel, jeux multijoueurs)
- La latence est critique (trading, jeux de tir à la première personne)
- Le serveur doit recevoir fréquemment des données du client (position du curseur, frappe au clavier)

**Exemples** : Slack, Discord, Google Docs (curseurs collaboratifs), jeux HTML5, plateformes de trading.

### Quand choisir SSE ?

SSE est idéal quand :
- Le flux est **unidirectionnel** : le serveur envoie des mises à jour que le client consomme
- La simplicité est prioritaire : SSE fonctionne sur HTTP/2 naturellement (pas d'upgrade nécessaire)
- La robustesse aux déconnexions est critique (reconnexion automatique native)
- Derrière un load balancer standard (pas besoin de sticky sessions)

**Exemples** : tableaux de bord de monitoring, flux de prix, notifications push, logs en direct, progression de tâches longues.

```{admonition} SSE sur HTTP/2
:class: note

Sur HTTP/2, SSE bénéficie du multiplexage : plusieurs flux SSE de différentes sources peuvent partager la même connexion TCP. Sur HTTP/1.1, le navigateur est limité à 6 connexions par domaine, ce qui restreint le nombre de flux SSE simultanés. HTTP/2 lève cette limitation.
```

### Quand garder le polling ?

Le polling reste valide quand :
- La fréquence de mise à jour est basse (toutes les minutes ou plus)
- La simplicité d'implémentation prime
- L'infrastructure ne supporte pas les connexions longues (certains proxys d'entreprise)
- La logique est simple et sans état côté serveur

```{code-cell} python
:tags: [hide-input]

# Visualisation : arbre de décision pour le choix du protocole
fig, ax = plt.subplots(figsize=(12, 8))
ax.set_xlim(0, 12)
ax.set_ylim(0, 9)
ax.axis("off")
ax.set_title("Guide de choix : Polling / Long Polling / SSE / WebSocket", fontsize=12, fontweight="bold")

def noeud(ax, x, y, texte, couleur="#2196F3", radius=0.55, fontsize=8.5):
    cercle = plt.Circle((x, y), radius, color=couleur, alpha=0.85, zorder=5)
    ax.add_patch(cercle)
    ax.text(x, y, texte, ha="center", va="center", fontsize=fontsize,
            color="white", fontweight="bold", zorder=6, wrap=True,
            multialignment="center")

def fleche(ax, x1, y1, x2, y2, label="", couleur="gray"):
    ax.annotate("", xy=(x2, y2), xytext=(x1, y1),
                arrowprops=dict(arrowstyle="->", color=couleur, lw=1.5))
    if label:
        mx, my = (x1+x2)/2, (y1+y2)/2
        ax.text(mx + 0.1, my, label, fontsize=8, color=couleur)

def boite(ax, x, y, texte, couleur="#4CAF50", w=1.5, h=0.7):
    rect = mpatches.FancyBboxPatch((x - w/2, y - h/2), w, h,
                                    boxstyle="round,pad=0.1",
                                    facecolor=couleur, edgecolor="white",
                                    alpha=0.9, zorder=5)
    ax.add_patch(rect)
    ax.text(x, y, texte, ha="center", va="center", fontsize=9,
            color="white", fontweight="bold", zorder=6)

# Questions / noeuds
noeud(ax, 6, 8.2, "Besoin\ntemps réel ?", "#37474F", radius=0.7, fontsize=8)
noeud(ax, 2.5, 6.5, "Fréquence\nfaible ?\n(>1 min)", "#607D8B", fontsize=7.5)
noeud(ax, 9.5, 6.5, "Communication\nbidirectionnelle ?", "#607D8B", fontsize=7.5)
noeud(ax, 8, 4.5, "Volume élevé\nclient→serveur ?", "#607D8B", fontsize=7.5)

# Feuilles
boite(ax, 2.5, 4.8, "Polling", "#F44336")
boite(ax, 4.5, 4.8, "Long Polling", "#FF9800")
boite(ax, 7, 2.5, "SSE", "#4CAF50")
boite(ax, 10, 2.5, "WebSocket", "#2196F3")

# Flèches
fleche(ax, 6, 7.5, 2.5, 7.2, "Oui", "#4CAF50")
fleche(ax, 6, 7.5, 9.5, 7.2, "Non\n(rare)", "#F44336")

fleche(ax, 2.5, 5.8, 2.5, 5.3, "Oui", "#4CAF50")
fleche(ax, 2.5, 5.8, 4.5, 5.3, "Non", "#F44336")

fleche(ax, 9.5, 5.8, 8, 5.0, "Non", "#4CAF50")
fleche(ax, 9.5, 5.8, 10, 4.8, "Oui", "#F44336")

fleche(ax, 8, 4.0, 7, 2.9, "Non", "#4CAF50")
fleche(ax, 8, 4.0, 10, 2.9, "Oui", "#F44336")

ax.text(6, 0.5, "SSE : simple, HTTP natif, reconnexion auto  |  WebSocket : faible latence, bidirectionnel",
        ha="center", fontsize=9, color="#37474F", style="italic")

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

## Résumé

Ce chapitre a présenté les deux protocoles majeurs pour la communication temps réel sur le web.

**WebSocket** offre un canal bidirectionnel plein-duplex à faible latence, idéal pour les applications interactives. Son handshake HTTP Upgrade (101 Switching Protocols) permet le passage à travers les pare-feux. Le masquage des frames côté client protège contre les attaques par cache poisoning.

**Server-Sent Events** sont plus simples, natifs HTTP, avec reconnexion automatique intégrée. Ils excellent pour les flux unidirectionnels (notifications, dashboards) et fonctionnent mieux avec les load balancers standard et HTTP/2.

Le polling reste pertinent pour des cas simples à faible fréquence, mais consomme des ressources inutiles à haute fréquence. Le long polling améliore la latence mais complexifie la gestion des connexions côté serveur.
