4 Évaluation 1
Déroulement de la séance
- Évaluation sur machine : 3 heures, soit la totalité de la séance.
- La correction commentée sera faite au début de la séance 5.
4.1 Modalités
Conditions
| Durée | 3 heures |
| Lieu | Salle de TP, sur les machines de l’école |
| Programme | Séances 1, 2 et 3 |
| Autorisé | Ce cours (disponible sur les machines), vos notes manuscrites, votre éditeur, Python |
| Interdit | Internet, messagerie, assistant IA (Copilot et autres), tout échange entre étudiants |
| Réseau | coupé : tout s’exécute sur votre seule machine, en 127.0.0.1, sur plusieurs ports |
| Barème | Partie 1 : 10 points — Partie 2 : 10 points |
Comme le réseau est coupé, ssh et scp sont inutilisables. Les sujets sont conçus pour cela : plusieurs processus sur votre machine, chacun sur son port, suffisent à constituer un système distribué. Entraînez-vous de cette façon dès les TP.
Ce qui est rendu
Vous rendez un unique fichier texte qui contient tout : vos réponses aux questions et l’intégralité de votre code. Pendant l’épreuve vous travaillez normalement, dans de vrais fichiers .py que vous exécutez et testez ; à la fin, vous les recopiez dans ce fichier unique.
Le format est imposé et dépouillé automatiquement. Il est décrit intégralement ci-dessous, et la même page figurera en tête du sujet le jour de l’évaluation. Entraînez-vous à le respecter dès les examens blancs : c’est gratuit, et cela vous évitera de perdre des points bêtement.
4.3 Examen blanc no1
Le sujet qui suit a la forme exacte de l’évaluation : même durée, même structure en deux parties, même barème, même type de questions. Son contenu, en revanche, sera différent le jour de l’évaluation : le protocole à implémenter, les commandes et les questions ne seront pas ceux-ci. Ne cherchez donc pas à apprendre ce corrigé par cœur : entraînez-vous à produire le raisonnement et le code.
Traitez ce sujet en conditions réelles (3 h, sans Internet, réseau coupé, avec ce cours seul) avant la séance 4, et rendez-le au format balisé décrit ci-dessus : c’est aussi cela qui s’apprend. Le corrigé est en annexe, Section 4.4 : ne le consultez qu’après.
Partie 1 — Questions (10 points)
Question 1 — Le déroulé d’une connexion 1,5 pt
On donne les six appels suivants, dans le désordre :
listen recv bind accept socket connect
Donnez l’ordre correct des appels côté serveur, et indiquez lequel n’est pas utilisé par le serveur.
Donnez l’ordre correct côté client.
accept()renvoie deux valeurs. Lesquelles ? À quoi sert chacune ? Que se passerait-il si l’on faisaitrecvsur le socket d’écoute au lieu du socket renvoyé ?
Question 2 — Octets et texte 1,5 pt
Soit le code client suivant :
client.sendall("Bonjour")
reponse = client.recv(1024)
print("Recu : " + reponse)Ce code contient deux erreurs de même nature. Lesquelles ? Donnez le message d’erreur exact que Python affiche pour la première.
Corrigez le code.
Le serveur envoie la chaîne
"café". Combien d’octets circulent sur le réseau ? Justifiez.
Question 3 — Le flux d’octets 2 pts
Un serveur exécute :
connexion.sendall(b"ALPHA")
connexion.sendall(b"BETA")
connexion.sendall(b"GAMMA")Le client exécute print(client.recv(1024)) trois fois de suite.
Un étudiant affirme : « le client affichera
b'ALPHA',b'BETA',b'GAMMA'». Est-ce garanti ? Justifiez en une phrase précise sur ce que TCP garantit et ne garantit pas.Donnez deux affichages différents, tous deux possibles.
Le client fait maintenant
client.recv(3). Combien d’octets au maximum peut-il recevoir ? Au minimum ?Citez les deux techniques permettant de retrouver les frontières entre messages, et donnez pour chacune un inconvénient.
Question 4 — Une boucle qui tourne mal 1,5 pt
tout = b""
while True:
morceau = connexion.recv(1024)
tout += morceau
if len(tout) >= 100:
break
print(tout.decode())Le client se connecte, envoie 20 octets, puis ferme la connexion. Décrivez précisément le comportement du serveur.
Quelle est la valeur de
morceaudans cette situation, et que signifie-t-elle ?Corrigez la boucle.
Question 5 — Concurrence 1,5 pt
total = 0
def servir(connexion):
global total
valeur = int(recevoir_message(connexion)["valeur"])
total = total + valeur
connexion.close()Trente clients envoient chacun la valeur 1, simultanément.
Quelle valeur finale attend-on pour
total? Quelle valeur peut-on réellement obtenir ? Comment s’appelle ce phénomène ?Expliquez en détaillant les étapes élémentaires de
total = total + valeur.Corrigez le code.
On remplace les threads par un serveur itératif (sans thread). Le bug disparaît-il ? Quel nouveau problème apparaît ?
Question 6 — Timeout et pannes 1 pt
Un programme interroge 200 machines avec un ThreadPoolExecutor(max_workers=100). Cinq machines sont éteintes. Aucun settimeout n’a été posé.
Que se passe-t-il ? Le programme finit-il par se terminer ?
Où placer le
settimeoutexactement, et pourquoi avant leconnect?On choisit un timeout de 0,05 s. Quel est le risque ?
Question 7 — Comprendre un protocole 1 pt
Un serveur reçoit les octets suivants :
00 00 00 0B 7B 22 74 79 70 65 22 3A 22 50 49 4E 47 22 7D
Sachant que le protocole utilisé est celui de protocole.py :
Quelle est la taille annoncée de la charge utile ?
Combien d’octets le serveur doit-il lire après l’en-tête ?
Bonus : les octets
7B 22 74 79 70 65 22 3A 22 50 49 4E 47 22 7Dcorrespondent à un texte ASCII. Reconstituez-le (7B={,22=",3A=:,7D=}).Bonus : comparez le nombre d’octets annoncé par l’en-tête au nombre d’octets réellement fournis. Que va-t-il se passer côté serveur ?
Partie 2 — Programmation (10 points)
Critères d’évaluation
| Critère | Pts | Attendu |
|---|---|---|
| Framing | 3 | Longueur préfixée "!I", recevoir_exactement correcte, dans les deux sens |
| Commandes | 2 | PING, STATS, QUIT conformes à la spécification |
| Concurrence | 2 | Plusieurs clients simultanés, plusieurs requêtes par connexion |
| Robustesse | 2 | Timeout, aucune requête ne fait tomber le serveur, connexions fermées |
| Client de test | 1 | Interroge plusieurs serveurs en parallèle, tolère un serveur absent |
Un service de statistiques distribué
Vous devez écrire trois fichiers, à recopier ensuite dans votre fichier de rendu sous les balises #=== FICHIER … ===.
1. protocole.py
Le module de framing vu en cours, avec les trois fonctions envoyer_message(sock, objet), recevoir_exactement(sock, n) et recevoir_message(sock). Messages JSON, longueur préfixée sur 4 octets en "!I".
2. serveur_stats.py
Un serveur concurrent qui écoute sur un port de votre choix et répond aux requêtes suivantes :
| Requête | Réponse |
|---|---|
{"type": "PING"} |
{"type": "PONG"} |
{"type": "STATS", "nombres": [...]} |
{"type": "OK", "n": ..., "min": ..., "max": ..., "moyenne": ...} |
{"type": "QUIT"} |
Ferme proprement la connexion |
Contraintes :
- plusieurs clients doivent être servis simultanément ;
- une connexion peut porter plusieurs requêtes successives ;
- toute requête invalide (type inconnu, champ manquant, liste vide, valeur non numérique) donne
{"type": "ERREUR", "message": ...}; - le serveur ne doit jamais s’arrêter à cause d’un client.
3. client_stats.py
Un client qui interroge en parallèle une liste de serveurs donnée dans un fichier serveurs.txt (format 127.0.0.1:port, un par ligne), avec un timeout de 3 secondes.
Pour chaque serveur, il envoie la même liste de nombres et récupère les statistiques. Il affiche ensuite un tableau, puis la ligne "k/n serveurs ont repondu". Un serveur injoignable ne doit pas interrompre le programme.
Test attendu. Lancez trois serveurs sur trois ports différents, mettez-les dans serveurs.txt, ajoutez-y une entrée volontairement fermée (127.0.0.1:45999, où rien n’écoute), et vérifiez que votre client affiche bien 3/4 serveurs ont repondu.
4.4 Corrigés
Partie 1
a) 0,5 pt Serveur : socket bind listen accept recv. connect n’est pas utilisé par le serveur.
b) 0,25 pt Client : socket connect recv (et sendall).
c) 0,75 pt accept() renvoie (connexion, adresse). La connexion est un nouveau socket dédié au dialogue avec ce client ; l’adresse est le couple (IP, port) du client, utile pour la journalisation. Faire recv sur le socket d’écoute est une erreur : ce socket ne transporte aucune donnée applicative, il ne sert qu’à accepter des connexions ; Python lève une OSError.
a) 0,75 pt Les deux erreurs sont des confusions entre str et bytes : sendall exige des bytes, et "Recu : " + reponse concène un str avec des bytes. Message pour la première : TypeError: a bytes-like object is required, not 'str'.
b) 0,25 pt client.sendall("Bonjour".encode()) et print("Recu : " + reponse.decode()).
c) 0,5 pt "café" fait 4 caractères mais 5 octets en UTF-8 : le é est codé sur deux octets. Le nombre de caractères et le nombre d’octets ne coïncident pas dès qu’on sort de l’ASCII — une raison de plus pour préfixer par la longueur en octets.
a) 0,5 pt Non, ce n’est pas garanti. TCP garantit que les octets arrivent tous et dans l’ordre ; il ne préserve pas les frontières entre les appels à sendall, car il transporte un flux d’octets, pas des messages.
b) 0,5 pt Deux affichages possibles parmi d’autres : b'ALPHABETAGAMMA', b'', b'' (coalescence totale) ; ou b'ALPHABETA', b'GAMMA', b'' (coalescence partielle).
c) 0,5 pt Au maximum 3 octets, au minimum 1 octet (recv ne renvoie jamais 0 octet sauf fermeture, où il renvoie b"").
d) 0,5 pt Délimiteur : simple et lisible, mais impose d’échapper le délimiteur s’il apparaît dans les données, et convient mal au binaire. Longueur préfixée : transparente au contenu, mais illisible à l’œil nu et il faut se prémunir d’une taille annoncée aberrante.
a) 0,5 pt Le serveur reçoit 20 octets, puis recv renvoie b"" indéfiniment. tout n’atteint jamais 100 octets, donc la boucle ne s’arrête jamais : boucle infinie consommant 100 % d’un cœur.
b) 0,5 pt morceau vaut b"", ce qui signifie que le pair a fermé la connexion — et non qu’il n’y a pas de données pour l’instant.
c) 0,5 pt
tout = b""
while len(tout) < 100:
morceau = connexion.recv(1024)
if not morceau:
break # ou lever une exception
tout += morceaua) 0,25 pt On attend 30 ; on peut obtenir n’importe quelle valeur entre 1 et 30. C’est une condition de course (race condition).
b) 0,5 pt total = total + valeur se décompose en : lire total, calculer la somme, écrire total. Si deux threads lisent 10 en même temps, tous deux écrivent 11 au lieu de 12 : un incrément est perdu.
c) 0,5 pt
verrou = threading.Lock()
def servir(connexion):
global total
valeur = int(recevoir_message(connexion)["valeur"])
with verrou:
total += valeur
connexion.close()d) 0,25 pt Oui, le bug disparaît : un seul thread, donc aucune concurrence. Mais le serveur redevient itératif : les 30 clients sont servis l’un après l’autre, et le débit s’effondre. On a échangé un bug de correction contre un problème de performance.
a) 0,4 pt Les threads qui tentent de joindre les 5 machines éteintes restent bloqués dans connect pendant le délai par défaut du système (souvent 1 à 2 minutes). Le programme finit par se terminer, mais très tard, et en occupant 5 threads du pool pour rien.
b) 0,3 pt Juste après socket.socket(...), donc avant connect : le timeout doit être armé avant l’opération que l’on veut borner, et connect est précisément l’appel le plus long en cas de machine injoignable.
c) 0,3 pt Avec 0,05 s, on risque de déclarer injoignables des machines parfaitement vivantes mais momentanément lentes ou distantes : ce sont des faux positifs. C’est le dilemme du délai, sur lequel nous reviendrons plus tard dans le cours.
a) 0,25 pt 00 00 00 0B en hexadécimal vaut 11 en décimal.
b) 0,25 pt 11 octets, puisque c’est ce qu’annonce l’en-tête. Le serveur obéit à l’en-tête, pas à ce qu’il devine du contenu.
c) 0,25 pt {"type":"PING"}.
d) 0,25 pt Le texte fait 15 octets, alors que l’en-tête en annonce 11 : l’émetteur a menti, ou s’est trompé.
Le serveur lit donc 11 octets et obtient {"type":"P, sur lequel json.loads échoue — d’où l’importance du try/except autour de la désérialisation. Pire : les 4 octets restants (ING"}) demeurent dans le flux et seront interprétés comme le début de l’en-tête du message suivant. La connexion est désynchronisée : tout ce qui suit est faux.
C’est la raison pour laquelle, face à un message incohérent, un serveur sérieux ne tente pas de se rattraper : il ferme la connexion.
Partie 2
C’est exactement le module de la séance 3, sans une ligne de différence. Si vous avez dû le réinventer le jour de l’épreuve, c’est le signe qu’il fallait le savoir par cœur.
protocole.py
"""Envoi/reception de messages JSON avec longueur prefixee."""
import json
import struct
TAILLE_ENTETE = 4 # "!I" = 4 octets
TAILLE_MAX = 100 * 1024 * 1024 # garde-fou : 100 Mo
def envoyer_message(sock, objet):
charge = json.dumps(objet).encode("utf-8")
sock.sendall(struct.pack("!I", len(charge)) + charge)
def recevoir_exactement(sock, nb_octets):
morceaux = b""
while len(morceaux) < nb_octets:
morceau = sock.recv(nb_octets - len(morceaux))
if not morceau:
raise ConnectionError("Connexion fermee prematurement")
morceaux += morceau
return morceaux
def recevoir_message(sock):
entete = recevoir_exactement(sock, TAILLE_ENTETE)
(taille,) = struct.unpack("!I", entete)
if taille > TAILLE_MAX:
raise ValueError(f"Message trop grand : {taille} octets")
charge = recevoir_exactement(sock, taille)
return json.loads(charge.decode("utf-8"))Ce qui coûte des points critère « Framing », 3 pts : un recv qui ne boucle pas, un b"" non testé, l’oubli du ! dans struct.pack, ou l’absence de garde-fou sur la taille annoncée.
serveur_stats.py
import socket, threading, sys
from protocole import envoyer_message, recevoir_message
def traiter(requete):
t = requete.get("type")
if t == "PING":
return {"type": "PONG"}
if t == "STATS":
nombres = requete.get("nombres")
if not isinstance(nombres, list) or not nombres:
return {"type": "ERREUR",
"message": "liste absente ou vide"}
try:
valeurs = [float(x) for x in nombres]
except (TypeError, ValueError):
return {"type": "ERREUR",
"message": "valeurs non numeriques"}
return {"type": "OK",
"n": len(valeurs),
"min": min(valeurs),
"max": max(valeurs),
"moyenne": sum(valeurs) / len(valeurs)}
return {"type": "ERREUR", "message": f"type inconnu : {t}"}
def servir(connexion, adresse):
connexion.settimeout(30.0)
try:
while True:
requete = recevoir_message(connexion)
if requete.get("type") == "QUIT":
break
envoyer_message(connexion, traiter(requete))
except (ConnectionError, ValueError, OSError):
pass
finally:
connexion.close()
if __name__ == "__main__":
port = int(sys.argv[1]) if len(sys.argv) > 1 else 50007
srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
srv.bind(("127.0.0.1", port))
srv.listen(64)
print(f"Serveur stats sur le port {port}")
while True:
c, a = srv.accept()
threading.Thread(target=servir, args=(c, a),
daemon=True).start()client_stats.py
import socket, random
from concurrent.futures import ThreadPoolExecutor
from protocole import envoyer_message, recevoir_message
NOMBRES = [random.randint(1, 1000) for _ in range(100)]
def interroger(entree):
machine, port = entree.split(":")
try:
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.settimeout(3.0)
s.connect((machine, int(port)))
envoyer_message(s, {"type": "STATS", "nombres": NOMBRES})
reponse = recevoir_message(s)
envoyer_message(s, {"type": "QUIT"})
s.close()
if reponse.get("type") == "OK":
return (entree, reponse)
except (OSError, ValueError, ConnectionError):
pass
return (entree, None)
serveurs = [l.strip() for l in open("serveurs.txt") if l.strip()]
with ThreadPoolExecutor(max_workers=20) as pool:
resultats = list(pool.map(interroger, serveurs))
ok = 0
print(f"{'serveur':30s} {'n':>5s} {'min':>8s} "
f"{'max':>8s} {'moyenne':>10s}")
for entree, r in resultats:
if r is None:
print(f"{entree:30s} {'INJOIGNABLE':>33s}")
else:
ok += 1
print(f"{entree:30s} {r['n']:5d} {r['min']:8.1f} "
f"{r['max']:8.1f} {r['moyenne']:10.2f}")
print(f"\n{ok}/{len(serveurs)} serveurs ont repondu")
4.2 Comment rendre votre copie
Lisez cette page avant de commencer, et réservez-vous 15 minutes à la fin pour le rendu. Les copies sont dépouillées automatiquement : ce qui n’est pas au bon endroit n’est pas corrigé.
1. Un seul fichier texte, et rien d’autre
Vous rendez un unique fichier texte, nommé exactement :
NOM: votre nom de famille en majuscules, sans accent, sans espace, sans tiret (SEGUINEAU, pasSéguineau de Préval).Prenom: votre prénom, initiale en majuscule, sans accent (Andre, pasAndré).BGD701_EVAL1_DUPONT_Marie.txt.py, ni vos dossiers de travail.2. Comment travailler, puis rendre
.pyséparés, que vous exécutez et testez. C’est la seule façon de vérifier que votre code fonctionne.Ne repoussez pas cette recopie à la dernière minute. Dès qu’un fichier fonctionne, collez-le tout de suite dans le fichier de rendu : vous ne risquez plus de le perdre, et vous n’aurez qu’à le remplacer si vous l’améliorez ensuite.
3. Le squelette à recopier
Créez le fichier et recopiez-y exactement ce squelette, puis écrivez vos réponses sous chaque balise.
Les balises
FICHIERattendues pour cette évaluation. Une balise par fichier, avec le nom de fichier exact, dans cet ordre :#=== FICHIER protocole.py ===#=== FICHIER serveur_stats.py ===#=== FICHIER client_stats.py ===4. Règles sur les balises
#, sous la forme#=== NOM ===(un espace de chaque côté).#=== FIN ===doit être la dernière ligne du fichier.Votre code ne doit contenir aucune ligne commençant par
#===: elle serait prise pour une balise et couperait votre fichier en deux. Un commentaire Python ordinaire (# ceci est un commentaire) ne pose évidemment aucun problème.Chaque bloc
FICHIERdoit contenir le fichier en entier, avec sesimportet son indentation d’origine. Il sera extrait tel quel et exécuté : un fichier tronqué ou ré-indenté ne fonctionnera pas. Copiez-collez, ne retapez pas.5. Conditions matérielles
L’évaluation se déroule sur les machines Linux de l’école, en salle de TP. Tous les raccourcis indiqués ici sont donc ceux de Linux.
Le réseau est coupé pendant l’évaluation. Vous ne pouvez pas utiliser
ssh,scp, ni joindre une autre machine. Tout doit fonctionner sur votre seule machine, entre processus, en utilisant l’adresse127.0.0.1et plusieurs ports différents.Les sujets sont conçus pour cela : à aucun moment vous n’avez besoin d’une autre machine ni d’Internet.
6. Votre responsabilité
Ctrl+S). Des copies ont déjà été perdues à quelques minutes de la fin.BGD701_EVAL1_NOM_Prenom.txt, sans accent ni espace.#=== ETAT ===ce qui marche et ce qui ne marche pas : c’est pris en compte.Consacrez environ 1 heure à la partie 1 et 2 heures à la partie 2. Un programme qui tourne à moitié rapporte davantage qu’un programme parfait mais inachevé : faites d’abord marcher un cas simple, puis enrichissez.