UDP : rapidité et multicast#
UDP (User Datagram Protocol) est le protocole de transport frugal d’Internet. Défini dans la RFC 768 (1980), il tient en moins d’une page : pas de connexion, pas d’accusé de réception, pas de reordering — juste l’envoi brut de datagrammes. Cette apparente pauvreté est en réalité sa plus grande force : UDP est le choix de prédilection partout où la latence prime sur la fiabilité — streaming vidéo, jeux en ligne, VoIP, DNS, NTP.
Objectifs du chapitre
Comprendre la structure minimale du datagramme UDP
Savoir choisir entre UDP et TCP selon le cas d’usage
Maîtriser le multicast et le broadcast UDP
Implémenter un client/serveur UDP complet en Python
Découvrir les protocoles qui ajoutent de la fiabilité au-dessus d’UDP (QUIC, KCP)
Structure du datagramme UDP#
Le datagramme UDP est d’une simplicité remarquable : 8 octets d’en-tête, point final. Comparez avec les 20 à 60 octets de l’en-tête TCP.
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Port source | Port destination |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Longueur | Checksum |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Données ... |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
Les quatre champs :
Champ |
Taille |
Description |
|---|---|---|
Port source |
16 bits |
Port de l’émetteur (0 si non utilisé) |
Port destination |
16 bits |
Port du récepteur |
Longueur |
16 bits |
Taille totale en-tête + données (min 8, max 65535) |
Checksum |
16 bits |
Contrôle d’intégrité (optionnel en IPv4, obligatoire en IPv6) |
La taille maximale d’un datagramme UDP est donc 65 535 − 8 = 65 527 octets de données. En pratique, on reste généralement sous la MTU Ethernet (1500 octets) pour éviter la fragmentation IP.
import struct
def parse_udp_header(raw: bytes) -> dict:
"""Parse les 8 premiers octets d'un datagramme UDP."""
if len(raw) < 8:
raise ValueError("Données trop courtes pour un en-tête UDP")
src_port, dst_port, length, checksum = struct.unpack("!HHHH", raw[:8])
return {
"port_source": src_port,
"port_destination": dst_port,
"longueur": length,
"checksum": f"0x{checksum:04X}",
"données_octets": len(raw) - 8,
}
def build_udp_header(src_port: int, dst_port: int, payload: bytes) -> bytes:
"""Construit un en-tête UDP (checksum = 0 pour simplification)."""
length = 8 + len(payload)
checksum = 0
return struct.pack("!HHHH", src_port, dst_port, length, checksum)
# Exemple : simuler un paquet DNS (port 53)
payload = b"\x00\x01\x01\x00\x00\x01\x00\x00\x00\x00\x00\x00" # DNS query simplifiée
header = build_udp_header(54321, 53, payload)
raw_packet = header + payload
print("=== En-tête UDP ===")
print(f"Octets bruts (hex) : {header.hex(' ')}")
parsed = parse_udp_header(raw_packet)
for k, v in parsed.items():
print(f" {k:25s} = {v}")
print(f"\nTaille totale du paquet : {len(raw_packet)} octets")
print(f"Overhead en-tête UDP : {8/len(raw_packet)*100:.1f}%")
=== En-tête UDP ===
Octets bruts (hex) : d4 31 00 35 00 14 00 00
port_source = 54321
port_destination = 53
longueur = 20
checksum = 0x0000
données_octets = 12
Taille totale du paquet : 20 octets
Overhead en-tête UDP : 40.0%
fig, axes = plt.subplots(1, 2, figsize=(13, 4))
# ── Diagramme de l'en-tête UDP ──────────────────────────────────────────────
ax = axes[0]
ax.set_xlim(0, 32)
ax.set_ylim(-0.5, 2.5)
ax.set_aspect("equal")
ax.axis("off")
ax.set_title("Structure de l'en-tête UDP (8 octets)", fontweight="bold")
champs = [
(0, 16, "Port source\n(16 bits)", "#4C9BE8"),
(16, 16, "Port destination\n(16 bits)", "#E87A4C"),
(0, 16, "Longueur\n(16 bits)", "#54B87A"),
(16, 16, "Checksum\n(16 bits)", "#C96DD8"),
]
positions = [(0, 1), (16, 1), (0, 0), (16, 0)]
for (x, y), (_, w, label, color) in zip(positions, champs):
rect = FancyBboxPatch((x + 0.2, y + 0.1), w - 0.4, 0.8,
boxstyle="round,pad=0.05", linewidth=1.5,
edgecolor="white", facecolor=color, alpha=0.85)
ax.add_patch(rect)
ax.text(x + w/2, y + 0.5, label, ha="center", va="center",
fontsize=9, fontweight="bold", color="white")
# Étiquettes bits
for b in [0, 8, 16, 24, 31]:
ax.text(b if b < 31 else 31.5, 2.2, str(b), ha="center", va="center",
fontsize=7, color="gray")
ax.text(16, 2.45, "Bits 0–31", ha="center", fontsize=10, color="#333333")
# ── Comparaison des tailles d'en-têtes ──────────────────────────────────────
ax2 = axes[1]
protocoles = ["UDP", "TCP (min)", "TCP (max)", "IPv4 (min)", "Ethernet"]
tailles = [8, 20, 60, 20, 14]
colors = ["#54B87A", "#4C9BE8", "#4C9BE8", "#E87A4C", "#C96DD8"]
bars = ax2.barh(protocoles, tailles, color=colors, edgecolor="white", height=0.55)
for bar, val in zip(bars, tailles):
ax2.text(bar.get_width() + 0.5, bar.get_y() + bar.get_height()/2,
f"{val} octets", va="center", fontsize=10, fontweight="bold")
ax2.set_xlabel("Taille de l'en-tête (octets)")
ax2.set_title("Comparaison des tailles d'en-têtes", fontweight="bold")
ax2.set_xlim(0, 75)
ax2.grid(axis="x", alpha=0.4)
ax2.tick_params(axis="y", labelsize=10)
plt.tight_layout()
plt.show()
TCP vs UDP : tableau de comparaison#
Quand choisir UDP plutôt que TCP ?
Choisissez UDP quand : (1) la latence est critique et une retransmission serait pire que la perte, (2) vous envoyez de petits messages indépendants, (3) vous faites du multicast/broadcast, (4) vous implémentez votre propre mécanisme de fiabilité (QUIC, KCP, RTP).
comparaison = {
"Critère": [
"Connexion", "Fiabilité", "Ordre de livraison",
"Contrôle de flux", "Contrôle de congestion",
"En-tête", "Latence", "Débit possible",
"Multicast / Broadcast", "Use cases typiques"
],
"TCP": [
"Orienté connexion (handshake 3 voies)",
"Garantie (retransmissions)",
"Garanti (numéros de séquence)",
"Oui (fenêtre glissante)",
"Oui (slow start, CUBIC…)",
"20–60 octets",
"Plus élevée (+RTT handshake)",
"Limité par fenêtre et RTT",
"Non",
"HTTP, FTP, SSH, SMTP, base de données"
],
"UDP": [
"Sans connexion",
"Aucune (best-effort)",
"Non garanti",
"Non",
"Non",
"8 octets",
"Minimale (pas de handshake)",
"Limité seulement par la bande passante",
"Oui",
"DNS, DHCP, NTP, VoIP, streaming, jeux"
]
}
df = pd.DataFrame(comparaison)
print(df.to_string(index=False))
Critère TCP UDP
Connexion Orienté connexion (handshake 3 voies) Sans connexion
Fiabilité Garantie (retransmissions) Aucune (best-effort)
Ordre de livraison Garanti (numéros de séquence) Non garanti
Contrôle de flux Oui (fenêtre glissante) Non
Contrôle de congestion Oui (slow start, CUBIC…) Non
En-tête 20–60 octets 8 octets
Latence Plus élevée (+RTT handshake) Minimale (pas de handshake)
Débit possible Limité par fenêtre et RTT Limité seulement par la bande passante
Multicast / Broadcast Non Oui
Use cases typiques HTTP, FTP, SSH, SMTP, base de données DNS, DHCP, NTP, VoIP, streaming, jeux
fig, ax = plt.subplots(figsize=(13, 6))
ax.axis("off")
table = ax.table(
cellText=df.values,
colLabels=df.columns,
cellLoc="left",
loc="center",
)
table.auto_set_font_size(False)
table.set_fontsize(8.5)
table.scale(1, 1.8)
# Style header
for j in range(len(df.columns)):
table[0, j].set_facecolor("#2C3E50")
table[0, j].set_text_props(color="white", fontweight="bold")
# Style lignes alternées
for i in range(1, len(df) + 1):
for j in range(len(df.columns)):
if i % 2 == 0:
table[i, j].set_facecolor("#F0F4F8")
if j == 1:
table[i, j].set_facecolor("#D6E8FF")
elif j == 2:
table[i, j].set_facecolor("#D6FFE8")
ax.set_title("Comparaison TCP vs UDP", fontsize=14, fontweight="bold", pad=20)
plt.tight_layout()
plt.show()
Cas d’usage d’UDP#
DNS — Domain Name System#
DNS utilise UDP sur le port 53 pour les requêtes standards. Une requête DNS est typiquement inférieure à 512 octets — parfaitement adaptée à un datagramme UDP. La latence de résolution est directement impactée par le protocole de transport.
# Simulation : construction d'une requête DNS minimale en UDP
def build_dns_query(domain: str) -> bytes:
"""Construit une requête DNS A minimale (format simplifié)."""
# En-tête DNS
transaction_id = 0x1234
flags = 0x0100 # Requête standard, récursion désirée
qdcount = 1 # 1 question
ancount = nscount = arcount = 0
header = struct.pack("!HHHHHH", transaction_id, flags,
qdcount, ancount, nscount, arcount)
# Question : encodage QNAME
qname = b""
for label in domain.split("."):
encoded = label.encode()
qname += bytes([len(encoded)]) + encoded
qname += b"\x00" # terminateur
qtype = 1 # A record
qclass = 1 # IN (Internet)
question = qname + struct.pack("!HH", qtype, qclass)
return header + question
query = build_dns_query("example.com")
udp_header = build_udp_header(54321, 53, query)
full_packet = udp_header + query
print(f"Taille requête DNS (payload) : {len(query)} octets")
print(f"En-tête UDP : 8 octets")
print(f"Paquet total : {len(full_packet)} octets")
print(f"Hex du paquet :")
print(" ".join(f"{b:02x}" for b in full_packet))
Taille requête DNS (payload) : 29 octets
En-tête UDP : 8 octets
Paquet total : 37 octets
Hex du paquet :
d4 31 00 35 00 25 00 00 12 34 01 00 00 01 00 00 00 00 00 00 07 65 78 61 6d 70 6c 65 03 63 6f 6d 00 00 01 00 01
NTP, DHCP, VoIP, jeux en ligne#
cas_usage = {
"Protocole": ["DNS", "DHCP", "NTP", "VoIP (RTP)", "Jeux en ligne",
"Streaming (RTSP/RTP)", "TFTP", "SNMP"],
"Port": ["53", "67/68", "123", "5004–5005", "Variable",
"Variable", "69", "161/162"],
"Taille typique": ["<512 o", "<576 o", "48 o", "160–320 o",
"50–1400 o", "188–1316 o", "512 o max", "<512 o"],
"Raison UDP": [
"Requête/réponse rapide, retry applicatif",
"Pas encore d'adresse IP (broadcast)",
"Précision temporelle, faible latence",
"Perte acceptable, ordre optionnel",
"Latence critique, jitter toléré",
"Perte d'images acceptable",
"Simplicité, embarqué",
"Polling léger"
]
}
df_usage = pd.DataFrame(cas_usage)
print(df_usage.to_string(index=False))
Protocole Port Taille typique Raison UDP
DNS 53 <512 o Requête/réponse rapide, retry applicatif
DHCP 67/68 <576 o Pas encore d'adresse IP (broadcast)
NTP 123 48 o Précision temporelle, faible latence
VoIP (RTP) 5004–5005 160–320 o Perte acceptable, ordre optionnel
Jeux en ligne Variable 50–1400 o Latence critique, jitter toléré
Streaming (RTSP/RTP) Variable 188–1316 o Perte d'images acceptable
TFTP 69 512 o max Simplicité, embarqué
SNMP 161/162 <512 o Polling léger
Multicast UDP#
Le multicast permet d’envoyer un paquet à un groupe de destinataires simultanément, sans dupliquer les données sur chaque lien. C’est l’antithèse de l’unicast (un émetteur, un récepteur) et du broadcast (tous les hôtes du sous-réseau).
Adresses multicast IPv4#
Les adresses multicast IPv4 sont dans la plage 224.0.0.0 – 239.255.255.255 (classe D).
Plage |
Portée |
Exemples |
|---|---|---|
224.0.0.0/24 |
Lien local (TTL=1) |
224.0.0.1 (tous hôtes), 224.0.0.2 (tous routeurs) |
224.0.1.0/24 |
Internet global |
224.0.1.1 (NTP) |
232.0.0.0/8 |
Source-specific multicast (SSM) |
IPTV professionnel |
239.0.0.0/8 |
Administratif (scope limité) |
IPTV local, mises à jour |
IGMP — Internet Group Management Protocol#
Les hôtes utilisent IGMP pour rejoindre ou quitter un groupe multicast. Les routeurs écoutent ces messages pour savoir sur quels liens diffuser le trafic.
fig, axes = plt.subplots(1, 2, figsize=(14, 5))
# ── Diagramme multicast vs unicast vs broadcast ──────────────────────────────
ax = axes[0]
ax.set_xlim(0, 10)
ax.set_ylim(0, 8)
ax.axis("off")
ax.set_title("Unicast vs Multicast vs Broadcast", fontweight="bold")
def draw_host(ax, x, y, label, color="#4C9BE8", size=0.35):
circle = plt.Circle((x, y), size, color=color, zorder=4)
ax.add_patch(circle)
ax.text(x, y - 0.6, label, ha="center", va="top", fontsize=7.5)
def draw_arrow(ax, x1, y1, x2, y2, color, lw=1.5):
ax.annotate("", xy=(x2, y2), xytext=(x1, y1),
arrowprops=dict(arrowstyle="->", color=color, lw=lw))
# Émetteur commun
draw_host(ax, 1.5, 4, "Émetteur", "#E87A4C")
# Unicast (haut)
for i, (x, y, lbl) in enumerate([(3.5, 6.5, "H1"), (3.5, 5.5, "H2"), (3.5, 4.5, "H3")]):
c = "#4C9BE8" if i == 0 else "#CCCCCC"
draw_host(ax, x, y, lbl, c)
draw_arrow(ax, 1.85, 4, x - 0.35, y, "#4C9BE8" if i == 0 else "#DDDDDD",
lw=2 if i == 0 else 0.8)
ax.text(3.5, 7.3, "Unicast\n(1 flux / destinataire)", ha="center", fontsize=8,
color="#4C9BE8", fontweight="bold")
# Multicast (milieu droit)
for i, (x, y, lbl) in enumerate([(6.5, 6.8, "H1"), (6.5, 5.2, "H2"), (8.5, 6.8, "H3")]):
draw_host(ax, x, y, lbl, "#54B87A")
draw_arrow(ax, 1.85, 4.1, x - 0.35, y, "#54B87A", lw=2)
draw_host(ax, 8.5, 5.2, "H4", "#CCCCCC")
draw_arrow(ax, 1.85, 4.0, 8.15, 5.2, "#DDDDDD", lw=0.8)
ax.text(7.5, 7.6, "Multicast\n(1 flux → groupe)", ha="center", fontsize=8,
color="#54B87A", fontweight="bold")
# Broadcast (bas)
for x, y, lbl in [(3.5, 2.5, "H1"), (3.5, 1.5, "H2"), (5.5, 2.5, "H3"), (5.5, 1.5, "H4")]:
draw_host(ax, x, y, lbl, "#C96DD8")
draw_arrow(ax, 1.85, 3.9, x - 0.35, y, "#C96DD8", lw=1.5)
ax.text(4.5, 0.7, "Broadcast\n(tous les hôtes)", ha="center", fontsize=8,
color="#C96DD8", fontweight="bold")
# ── Plages d'adresses multicast ──────────────────────────────────────────────
ax2 = axes[1]
plages = ["224.0.0.0/24\n(lien local)", "224.0.1.0/24\n(global)",
"232.0.0.0/8\n(SSM)", "233.0.0.0/8\n(GLOP)",
"239.0.0.0/8\n(administratif)"]
tailles = [256, 256, 16_777_216, 16_777_216, 16_777_216]
colors_m = ["#E87A4C", "#4C9BE8", "#54B87A", "#C96DD8", "#F0C040"]
bars = ax2.bar(plages, [np.log2(t) for t in tailles], color=colors_m,
edgecolor="white", width=0.6)
ax2.set_ylabel("log₂(nombre d'adresses)")
ax2.set_title("Plages d'adresses multicast IPv4", fontweight="bold")
ax2.set_ylim(0, 27)
for bar, t in zip(bars, tailles):
ax2.text(bar.get_x() + bar.get_width()/2,
bar.get_height() + 0.3,
f"{t:,}", ha="center", fontsize=8)
ax2.tick_params(axis="x", labelsize=8)
ax2.grid(axis="y", alpha=0.4)
plt.tight_layout()
plt.show()
Exemple Python : rejoindre un groupe multicast#
import socket
import struct
import threading
import time
MULTICAST_GROUP = "239.255.0.1"
MULTICAST_PORT = 50007
def multicast_sender(message: str, ttl: int = 1):
"""Envoie un message vers un groupe multicast."""
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, ttl)
try:
sock.sendto(message.encode(), (MULTICAST_GROUP, MULTICAST_PORT))
print(f"[SENDER] Envoyé : '{message}' → {MULTICAST_GROUP}:{MULTICAST_PORT}")
finally:
sock.close()
def multicast_receiver(timeout: float = 2.0):
"""Reçoit depuis un groupe multicast (version non-bloquante pour démo)."""
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sock.bind(("", MULTICAST_PORT))
# Rejoindre le groupe multicast
mreq = struct.pack("4sL", socket.inet_aton(MULTICAST_GROUP),
socket.INADDR_ANY)
sock.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP, mreq)
sock.settimeout(timeout)
try:
data, addr = sock.recvfrom(1024)
print(f"[RECEIVER] Reçu : '{data.decode()}' depuis {addr[0]}:{addr[1]}")
return data.decode()
except socket.timeout:
print("[RECEIVER] Timeout — aucun message reçu dans les délais")
return None
finally:
# Quitter le groupe
sock.setsockopt(socket.IPPROTO_IP, socket.IP_DROP_MEMBERSHIP, mreq)
sock.close()
# Démonstration en loopback
received = []
def receiver_thread():
result = multicast_receiver(timeout=3.0)
if result:
received.append(result)
t = threading.Thread(target=receiver_thread, daemon=True)
t.start()
time.sleep(0.1) # Laisser le receiver s'initialiser
multicast_sender("Bonjour groupe multicast !")
t.join(timeout=4)
print(f"\nRésultat : {len(received)} message(s) reçu(s)")
[SENDER] Envoyé : 'Bonjour groupe multicast !' → 239.255.0.1:50007[RECEIVER] Reçu : 'Bonjour groupe multicast !' depuis 10.23.39.254:39594
Résultat : 1 message(s) reçu(s)
Broadcast UDP#
Le broadcast envoie un paquet à tous les hôtes d’un sous-réseau. L’adresse 255.255.255.255 est un broadcast limité (non routé). L’adresse de subnet-directed broadcast (ex: 192.168.1.255 pour 192.168.1.0/24) est routée dans le sous-réseau mais pas au-delà.
Limitations du broadcast
Les routeurs ne transmettent pas les broadcasts entre sous-réseaux
Génère du trafic sur tous les hôtes du sous-réseau, même non intéressés
Utilisé principalement pour DHCP (discover) et ARP
Limité à IPv4 — IPv6 remplace le broadcast par des adresses multicast spécifiques (ex:
ff02::1)
# Exemple : envoi en broadcast UDP (nécessite SO_BROADCAST)
def broadcast_sender(message: str, port: int = 50008):
"""Envoie un message en broadcast UDP (local)."""
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
sock.settimeout(1)
try:
sock.sendto(message.encode(), ("255.255.255.255", port))
print(f"[BROADCAST] Envoyé : '{message}' → 255.255.255.255:{port}")
except Exception as e:
print(f"[BROADCAST] Erreur : {e}")
finally:
sock.close()
broadcast_sender("DHCP Discover (simulation)")
[BROADCAST] Envoyé : 'DHCP Discover (simulation)' → 255.255.255.255:50008
Simulation de perte de paquets#
UDP ne retransmet pas les paquets perdus. Pour comprendre l’impact, simulons un canal avec perte et mesurons le taux de livraison.
import random
def simulate_udp_channel(num_packets: int, loss_rate: float, seed: int = 42):
"""
Simule l'envoi de num_packets datagrammes UDP sur un canal
avec un taux de perte loss_rate (0.0 à 1.0).
"""
rng = random.Random(seed)
sent = 0
received = 0
lost = 0
latencies = []
for seq in range(num_packets):
sent += 1
# Perte aléatoire
if rng.random() < loss_rate:
lost += 1
else:
received += 1
# Latence simulée : exponentielle centrée autour de 20 ms
latency = max(1, rng.gauss(20, 5))
latencies.append(latency)
return {
"envoyés": sent,
"reçus": received,
"perdus": lost,
"taux_livraison_%": received / sent * 100,
"latence_moy_ms": np.mean(latencies) if latencies else 0,
"latence_p99_ms": np.percentile(latencies, 99) if latencies else 0,
}
scenarios = [
("LAN local", 0.001),
("WiFi domestique", 0.02),
("4G mobile", 0.05),
("3G dégradé", 0.10),
("Réseau saturé", 0.20),
]
print(f"{'Scénario':<20} {'Envoyés':>10} {'Reçus':>8} {'Perdus':>8} "
f"{'Livraison':>12} {'Lat. moy.':>12}")
print("-" * 78)
results = {}
for nom, loss in scenarios:
r = simulate_udp_channel(1000, loss)
results[nom] = r
print(f"{nom:<20} {r['envoyés']:>10} {r['reçus']:>8} {r['perdus']:>8} "
f"{r['taux_livraison_%']:>11.1f}% {r['latence_moy_ms']:>10.1f} ms")
Scénario Envoyés Reçus Perdus Livraison Lat. moy.
------------------------------------------------------------------------------
LAN local 1000 999 1 99.9% 19.8 ms
WiFi domestique 1000 980 20 98.0% 19.9 ms
4G mobile 1000 960 40 96.0% 19.9 ms
3G dégradé 1000 903 97 90.3% 19.9 ms
Réseau saturé 1000 806 194 80.6% 19.7 ms
fig, axes = plt.subplots(1, 2, figsize=(13, 4.5))
noms = list(results.keys())
livraisons = [results[n]["taux_livraison_%"] for n in noms]
latences = [results[n]["latence_moy_ms"] for n in noms]
perdus_pct = [100 - l for l in livraisons]
# ── Taux de livraison ────────────────────────────────────────────────────────
ax1 = axes[0]
x = np.arange(len(noms))
width = 0.38
b1 = ax1.bar(x - width/2, livraisons, width, label="Reçus", color="#54B87A", edgecolor="white")
b2 = ax1.bar(x + width/2, perdus_pct, width, label="Perdus", color="#E87A4C", edgecolor="white")
ax1.set_ylabel("Pourcentage de paquets (%)")
ax1.set_title("Taux de livraison UDP selon le réseau", fontweight="bold")
ax1.set_xticks(x)
ax1.set_xticklabels(noms, rotation=25, ha="right", fontsize=9)
ax1.set_ylim(0, 110)
ax1.legend(fontsize=9)
ax1.grid(axis="y", alpha=0.4)
for bar in b1:
h = bar.get_height()
ax1.text(bar.get_x() + bar.get_width()/2, h + 1, f"{h:.1f}%",
ha="center", va="bottom", fontsize=8)
# ── Comparaison latence TCP vs UDP ───────────────────────────────────────────
ax2 = axes[1]
rng = random.Random(1)
n = 200
udp_lat = [max(0.5, rng.gauss(20, 4)) for _ in range(n)]
# TCP ajoute ~RTT pour le handshake et peut avoir des retransmissions
tcp_lat = [max(1, rng.gauss(22, 6)) + (rng.expovariate(0.2) if rng.random() < 0.05 else 0)
for _ in range(n)]
ax2.hist(udp_lat, bins=30, alpha=0.7, color="#54B87A", label=f"UDP (moy={np.mean(udp_lat):.1f} ms)")
ax2.hist(tcp_lat, bins=30, alpha=0.7, color="#4C9BE8", label=f"TCP (moy={np.mean(tcp_lat):.1f} ms)")
ax2.axvline(np.mean(udp_lat), color="#2E8A50", linestyle="--", lw=2)
ax2.axvline(np.mean(tcp_lat), color="#2E5FA3", linestyle="--", lw=2)
ax2.set_xlabel("Latence (ms)")
ax2.set_ylabel("Nombre de paquets")
ax2.set_title("Distribution des latences TCP vs UDP\n(simulation)", fontweight="bold")
ax2.legend(fontsize=9)
ax2.grid(alpha=0.4)
plt.tight_layout()
plt.show()
UDP avec fiabilité applicative#
Plusieurs protocoles modernes réimplémentent la fiabilité au-dessus d’UDP, évitant les blocages head-of-line de TCP tout en gardant la souplesse d’UDP.
QUIC#
QUIC (RFC 9000) est le protocole de transport développé par Google, maintenant standardisé. Il tourne sur UDP et fournit :
Multiplexage de streams sans blocage head-of-line
Chiffrement TLS 1.3 intégré (0-RTT ou 1-RTT)
Migration de connexion (changement d’IP sans interruption)
Contrôle de congestion pluggable
QUIC est la base de HTTP/3 (anciennement HTTP-over-QUIC).
KCP#
KCP est un protocole de fiabilité applicative optimisé pour la latence. Il offre un contrôle fin sur les paramètres de retransmission, particulièrement apprécié dans les jeux en ligne compétitifs.
# Visualisation : overhead et latence des protocoles
protocols = ["TCP", "UDP brut", "UDP + QUIC", "UDP + KCP", "UDP + ARQ custom"]
overhead_bytes = [40, 8, 36, 24, 16] # en-tête approximatif
latency_setup_ms = [100, 0, 50, 0, 0] # latence d'établissement (1 RTT = 100 ms)
reliability = [100, 0, 100, 99, 95] # % fiabilité
fig, axes = plt.subplots(1, 3, figsize=(14, 4.5))
colors_p = ["#4C9BE8", "#54B87A", "#E87A4C", "#C96DD8", "#F0C040"]
for ax, values, ylabel, title in zip(
axes,
[overhead_bytes, latency_setup_ms, reliability],
["Octets d'en-tête", "Latence d'établissement (ms)", "Fiabilité (%)"],
["Overhead des en-têtes", "Latence d'établissement\n(basé sur 1 RTT = 100 ms)", "Fiabilité de livraison"]
):
bars = ax.bar(protocols, values, color=colors_p, edgecolor="white", width=0.6)
ax.set_ylabel(ylabel)
ax.set_title(title, fontweight="bold")
ax.set_xticklabels(protocols, rotation=25, ha="right", fontsize=9)
ax.grid(axis="y", alpha=0.4)
for bar, v in zip(bars, values):
ax.text(bar.get_x() + bar.get_width()/2, bar.get_height() + max(values)*0.01,
str(v), ha="center", va="bottom", fontsize=9, fontweight="bold")
plt.tight_layout()
plt.show()
/tmp/ipykernel_10906/3971976881.py:20: UserWarning: set_ticklabels() should only be used with a fixed number of ticks, i.e. after set_ticks() or using a FixedLocator.
ax.set_xticklabels(protocols, rotation=25, ha="right", fontsize=9)
/tmp/ipykernel_10906/3971976881.py:20: UserWarning: set_ticklabels() should only be used with a fixed number of ticks, i.e. after set_ticks() or using a FixedLocator.
ax.set_xticklabels(protocols, rotation=25, ha="right", fontsize=9)
/tmp/ipykernel_10906/3971976881.py:20: UserWarning: set_ticklabels() should only be used with a fixed number of ticks, i.e. after set_ticks() or using a FixedLocator.
ax.set_xticklabels(protocols, rotation=25, ha="right", fontsize=9)
Client/serveur UDP complet#
import socket
import threading
import time
import random
def udp_echo_server(host: str = "127.0.0.1", port: int = 55555,
loss_rate: float = 0.0, max_messages: int = 5):
"""
Serveur UDP echo avec simulation optionnelle de perte de paquets.
S'arrête après avoir traité max_messages messages.
"""
server_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
server_sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server_sock.settimeout(3.0)
server_sock.bind((host, port))
print(f"[SERVER] En écoute sur {host}:{port} (perte simulée : {loss_rate*100:.0f}%)")
count = 0
while count < max_messages:
try:
data, addr = server_sock.recvfrom(4096)
count += 1
if random.random() < loss_rate:
print(f"[SERVER] Paquet #{count} de {addr} PERDU (simulation)")
continue
msg = data.decode(errors="replace")
response = f"ECHO:{msg}"
server_sock.sendto(response.encode(), addr)
print(f"[SERVER] #{count} reçu de {addr}: '{msg}' → renvoyé")
except socket.timeout:
break
server_sock.close()
print("[SERVER] Arrêté")
def udp_client(host: str = "127.0.0.1", port: int = 55555,
messages: list = None, timeout: float = 1.0):
"""Client UDP avec timeout par message."""
if messages is None:
messages = ["Bonjour", "UDP", "est", "rapide", "!"]
client_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
client_sock.settimeout(timeout)
stats = {"envoyés": 0, "reçus": 0, "timeouts": 0}
for i, msg in enumerate(messages, 1):
client_sock.sendto(msg.encode(), (host, port))
stats["envoyés"] += 1
try:
data, _ = client_sock.recvfrom(4096)
stats["reçus"] += 1
print(f"[CLIENT] #{i} → '{msg}' | ← '{data.decode()}'")
except socket.timeout:
stats["timeouts"] += 1
print(f"[CLIENT] #{i} → '{msg}' | ← TIMEOUT")
client_sock.close()
print(f"\n[CLIENT] Stats : {stats}")
return stats
# Lancer le serveur en arrière-plan
messages = ["Bonjour", "monde", "UDP", "est", "simple"]
server_thread = threading.Thread(
target=udp_echo_server,
kwargs={"loss_rate": 0.0, "max_messages": len(messages)},
daemon=True
)
server_thread.start()
time.sleep(0.05)
# Lancer le client
stats = udp_client(messages=messages)
server_thread.join(timeout=5)
[SERVER] En écoute sur 127.0.0.1:55555 (perte simulée : 0%)
[SERVER] #1 reçu de ('127.0.0.1', 38143): 'Bonjour' → renvoyé
[CLIENT] #1 → 'Bonjour' | ← 'ECHO:Bonjour'
[SERVER] #2 reçu de ('127.0.0.1', 38143): 'monde' → renvoyé
[CLIENT] #2 → 'monde' | ← 'ECHO:monde'
[SERVER] #3 reçu de ('127.0.0.1', 38143): 'UDP' → renvoyé
[CLIENT] #3 → 'UDP' | ← 'ECHO:UDP'
[SERVER] #4 reçu de ('127.0.0.1', 38143): 'est' → renvoyé
[CLIENT] #4 → 'est' | ← 'ECHO:est'
[SERVER] #5 reçu de ('127.0.0.1', 38143): 'simple' → renvoyé
[CLIENT] #5 → 'simple' | ← 'ECHO:simple'
[SERVER] Arrêté
[CLIENT] Stats : {'envoyés': 5, 'reçus': 5, 'timeouts': 0}
# Même chose avec simulation de perte
print("=== Simulation avec 30% de perte ===\n")
messages2 = ["Hello", "UDP", "avec", "pertes", "simulées"]
server_thread2 = threading.Thread(
target=udp_echo_server,
kwargs={"loss_rate": 0.3, "max_messages": len(messages2)},
daemon=True
)
server_thread2.start()
time.sleep(0.05)
stats2 = udp_client(messages=messages2, timeout=0.5)
server_thread2.join(timeout=5)
=== Simulation avec 30% de perte ===
[SERVER] En écoute sur 127.0.0.1:55555 (perte simulée : 30%)
[SERVER] #1 reçu de ('127.0.0.1', 59565): 'Hello' → renvoyé
[CLIENT] #1 → 'Hello' | ← 'ECHO:Hello'
[SERVER] #2 reçu de ('127.0.0.1', 59565): 'UDP' → renvoyé
[CLIENT] #2 → 'UDP' | ← 'ECHO:UDP'
[SERVER] #3 reçu de ('127.0.0.1', 59565): 'avec' → renvoyé
[CLIENT] #3 → 'avec' | ← 'ECHO:avec'
[SERVER] #4 reçu de ('127.0.0.1', 59565): 'pertes' → renvoyé
[CLIENT] #4 → 'pertes' | ← 'ECHO:pertes'
[SERVER] Paquet #5 de ('127.0.0.1', 59565) PERDU (simulation)
[SERVER] Arrêté
[CLIENT] #5 → 'simulées' | ← TIMEOUT
[CLIENT] Stats : {'envoyés': 5, 'reçus': 4, 'timeouts': 1}
Résumé#
fig, ax = plt.subplots(figsize=(12, 5))
ax.axis("off")
ax.set_title("Récapitulatif — UDP : rapidité et multicast", fontsize=14, fontweight="bold", pad=15)
resume = [
["En-tête UDP", "8 octets seulement (src port, dst port, longueur, checksum)"],
["Sans connexion", "Pas de handshake — envoi immédiat, latence minimale"],
["Best-effort", "Pas de retransmission, pas de garantie d'ordre"],
["Multicast", "Groupes 224.0.0.0–239.255.255.255, gestion par IGMP"],
["Broadcast", "255.255.255.255 ou subnet-directed, non routé entre sous-réseaux"],
["QUIC", "Fiabilité + multiplexage + TLS 1.3, base de HTTP/3"],
["Use cases", "DNS, DHCP, NTP, VoIP, streaming, jeux en ligne"],
]
table = ax.table(
cellText=resume,
colLabels=["Concept", "Détail"],
cellLoc="left",
loc="center",
colWidths=[0.25, 0.65]
)
table.auto_set_font_size(False)
table.set_fontsize(10)
table.scale(1, 1.9)
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")
plt.tight_layout()
plt.show()