Séance 1

Pourquoi distribuer ? Premiers pas en TCP

Rémi Sharrock

BGD701 — Télécom Paris

Objectifs

  • Comprendre par le calcul pourquoi une machine ne suffit plus
  • Le vocabulaire minimal : IP, port, client, serveur, TCP
  • Écrire votre premier serveur et votre premier client
  • Exposer une information réelle : la charge CPU d’une machine

Pourquoi ce cours existe

Un calcul simple

Un SSD lit à environ 500 Mo/s. Vous comptez les mots d’un fichier de 1 To.

1000000 Mo500 Mo/s=2000 s≈33 minutes \frac{1\,000\,000 \text{ Mo}}{500 \text{ Mo/s}} = 2\,000 \text{ s} \approx 33 \text{ minutes}

33 minutes juste pour lire. Avec un traitement, le double ou le triple.

Et pour 1 Po ?

Mille téraoctets. L’ordre de grandeur de Common Crawl, l’archive du Web qui sert à entraîner les IA.

1000×33 minutes≈23 jours 1\,000 \times 33 \text{ minutes} \approx 23 \text{ jours}

Vingt-trois jours de lecture continue. Sans panne.

À retenir

Le Big Data ne commence pas quand les données sont grosses. Il commence quand une machine ne suffit plus dans un temps acceptable.

Deux stratégies

Grossir

scale up — vertical

Une machine plus puissante.

+ simple, code inchangé

− le prix explose

− plafond physique

Multiplier

scale out — horizontal

Cent machines ordinaires.

+ moins cher

+ pas de plafond

− tout devient plus dur

Ce dont on hérite

  1. Comment les machines se parlent ? → séances 1 à 3
  2. Comment répartir données et travail ? → séances 5 et 6
  3. Que faire quand une machine tombe ? → séances 5 et 7

Avec cent machines, la panne n’est plus une hypothèse. C’est une certitude statistique.

Tout le cours en une phrase

Distribuer, c’est échanger un problème de puissance

contre un problème de coordination.

Hadoop, Spark, Kafka sont des réponses industrielles à ce problème. Nous construirons les nôtres, en plus simple.

Le minimum vital sur le réseau

Adresse IP et port

L’adresse IP

Identifie la machine.

137.194.2.15

L’adresse de l’immeuble.

Le port

Identifie le programme.

De 1 à 65535.

Le numéro d’appartement.

Une machine fait tourner cent programmes réseau : un port chacun.

Quelques ports réservés

Port Usage
22 SSH
80 HTTP
443 HTTPS
5432 PostgreSQL
9092 Kafka

Sous 1024 : droits administrateur. Ici, on prend des ports hauts.

Attention aux collisions

Vous n’êtes pas seul sur les machines de l’école

Deux personnes sur le port 50000, et l’une des deux obtient :

Address already in use

Prenez un port personnel : 40000+40000\ + les trois derniers chiffres de votre numéro d’étudiant.

Deux adresses particulières

127.0.0.1

Alias localhost. Votre machine.

Joignable depuis elle seule.

Parfait pour tester.

0.0.0.0

Toutes les interfaces.

Joignable depuis le réseau.

Quand d’autres doivent vous contacter.

Client et serveur

La relation est asymétrique.

  • Le serveur démarre en premier, s’installe sur un port, et attend. Il ne sait ni qui l’appellera, ni quand.
  • Le client démarre quand il veut, connaît l’adresse et le port, et initie la connexion.

TCP : quatre garanties

  1. Connexion — un lien établi avant tout échange
  2. Fiabilité — un paquet perdu est renvoyé automatiquement
  3. Ordre — les octets arrivent dans l’ordre d’envoi
  4. Contrôle de flux — TCP ralentit si le destinataire est lent

TCP ou UDP ?

UDP n’offre aucune de ces garanties, mais il est plus léger.

On l’utilise pour la voix, la vidéo, les jeux : là où une donnée en retard est pire qu’une donnée perdue.

Dans ce cours, ce sera toujours TCP.

Le piège de la séance 3

Retenez la formulation exacte

TCP garantit que les octets arrivent, dans l’ordre.

Il ne garantit pas que vos messages arrivent séparés.

Cette distinction semble anodine. Elle sera le sujet central de la séance 3, et une source classique de bugs.

Le socket

L’analogie du téléphone

Téléphone Python Signification
Acheter un téléphone socket() Créer le point d’accès
Obtenir un numéro bind() S’attribuer adresse et port
Brancher le téléphone listen() Se déclarer prêt
Décrocher accept() Accepter une connexion
Composer un numéro connect() Se connecter au serveur
Parler sendall() Envoyer des octets
Écouter recv() Recevoir des octets
Raccrocher close() Fermer

Qui fait quoi

Serveur seul

socket()

bind()

listen()

accept()

Client seul

connect()

Les deux

sendall()

recv()

close()

On envoie des octets

Un socket ne transporte ni texte, ni nombres, ni objets. Il transporte des octets — le type bytes.

texte  = "Bonjour"          # str   : du texte
octets = texte.encode()     # bytes : b'Bonjour'
retour = octets.decode()    # str   : "Bonjour"

Octets littéraux : b"Bonjour". Par défaut, l’encodage est UTF-8.

L’erreur numéro un

Oublier .encode()

TypeError: a bytes-like object is required, not 'str'

Et sans .decode() à l’affichage, vous verrez b'Bonjour' au lieu de Bonjour.

Votre premier serveur

Un serveur echo

import socket

HOTE = "127.0.0.1"
PORT = 50007

serveur = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

# Autoriser la reutilisation immediate du port
serveur.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)

# S'attribuer l'adresse et le port
serveur.bind((HOTE, PORT))

# Se declarer pret (5 = taille de la file d'attente)
serveur.listen(5)
print(f"Serveur en ecoute sur {HOTE}:{PORT}")

while True:
    # Bloque ici jusqu'a ce qu'un client se connecte
    connexion, adresse = serveur.accept()
    print(f"Connexion de {adresse}")

    # Lire au plus 1024 octets
    donnees = connexion.recv(1024)
    print(f"Recu : {donnees.decode()}")

    # Renvoyer les memes octets
    connexion.sendall(donnees)

    # Fermer cette connexion, pas le serveur
    connexion.close()

Trois lignes à commenter

socket(AF_INET, SOCK_STREAM) — IPv4 et TCP. Toujours les mêmes ici.

setsockopt(… SO_REUSEADDR …) — sans elle, relancer le serveur donne Address already in use. Mettez-la toujours.

bind((HOTE, PORT)) — double parenthèse : bind attend un argument, un tuple.

accept() bloque

Le programme s’arrête là tant que personne ne se connecte.

Il renvoie deux choses : un nouveau socket dédié au client, et l’adresse du client.

Deux sockets, deux rôles

  • serveur : socket d’écoute. Sert à accepter, rien d’autre.
  • connexion : socket de dialogue avec ce client. C’est là qu’on fait recv et sendall.

Deux verbes à ne pas confondre

recv(1024) lit au plus 1024 octets. « Au plus » : on y reviendra en séance 3.

sendall(donnees) envoie tous les octets.

send() existe aussi, mais il peut n’envoyer qu’une partie des données. Utilisez toujours sendall().

Votre premier client

import socket

HOTE = "127.0.0.1"
PORT = 50007

client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client.connect((HOTE, PORT))

client.sendall("Bonjour le reseau".encode())

reponse = client.recv(1024)
print("Reponse du serveur :", reponse.decode())

client.close()

Pas de bind, pas de listen, pas de accept. Il se connecte, parle, écoute, raccroche.

Deux terminaux

Terminal 1

$ python3 serveur_echo.py
Serveur en ecoute sur 127.0.0.1:50007

Le terminal semble figé. C’est normal : il est bloqué dans accept().

Terminal 2

$ python3 client_echo.py
Reponse du serveur : Bonjour le reseau

Et dans le terminal 1 :

Connexion de ('127.0.0.1', 54321)
Recu : Bonjour le reseau

L’ordre compte

Le serveur d’abord, le client ensuite

Le client lancé seul donne :

ConnectionRefusedError: [Errno 61] Connection refused

Traduction : personne n’écoute sur ce port.

Pour arrêter le serveur : Ctrl+C.

Un serveur utile

La charge de la machine

$ uptime
 14:32:07 up 12 days,  3:41,  2 users,  load average: 0.42, 0.55, 0.61

Le load average : charge moyenne sur 1, 5 et 15 minutes.

Une charge de 1,0 ≈ un cœur occupé en permanence. Sur 8 cœurs : 8 = pleine charge, 0,4 = presque libre.

serveur_charge.py

import socket
import os
import sys

# Le port peut etre passe en argument, pour lancer plusieurs serveurs
# sur la meme machine.
PORT = int(sys.argv[1]) if len(sys.argv) > 1 else 50007

serveur = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
serveur.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
serveur.bind(("0.0.0.0", PORT))   # accessible depuis le reseau
serveur.listen(5)
print(f"Serveur de charge en ecoute sur le port {PORT}")

while True:
    connexion, adresse = serveur.accept()
    charge1, charge5, charge15 = os.getloadavg()
    message = f"{charge1:.2f} {charge5:.2f} {charge15:.2f}\n"
    connexion.sendall(message.encode())
    connexion.close()

Cette fois, 0.0.0.0 : on veut être joignable depuis le réseau.

Si vous travaillez sous Windows

os.getloadavg() n’existe pas sous Windows

C’est une fonction Unix : elle marche à l’école et sur macOS, mais lève AttributeError sous Windows.

Le cours donne une solution de repli, en bibliothèque standard uniquement.

Deux règles : aucune bibliothèque externe (le réseau est coupé en examen, rien ne s’installe), et vérifiez votre code sur une machine de l’école au moins une fois.

client_charge.py

import socket
import sys

HOTE = sys.argv[1] if len(sys.argv) > 1 else "127.0.0.1"
PORT = 50007

client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client.connect((HOTE, PORT))

reponse = client.recv(1024).decode().strip()
client.close()

c1, c5, c15 = [float(x) for x in reponse.split()]
print(f"{HOTE} -> 1min={c1}  5min={c5}  15min={c15}")
$ python3 client_charge.py tp-1a201-05.enst.fr
tp-1a201-05.enst.fr -> 1min=0.42  5min=0.55  15min=0.61

Le germe de tout le cours

Pourquoi c’est important

  • Séance 2 : interroger cent machines en parallèle
  • Séance 7 : ces mêmes machines exécuteront notre MapReduce

Un système distribué commence toujours là : savoir qui est vivant, et dans quel état.

Vous venez d’écrire un protocole

Sans le savoir

Un accord sur la forme des messages :

trois nombres décimaux, séparés par des espaces, terminés par un retour à la ligne

Le client ne décode que parce qu’il connaît cette convention.

Passez aux virgules, et le client plante. Nous formaliserons cela en séance 3.

À vous

Les exercices

  • 1.1 — Serveur horloge. Envoyer la date et l’heure à chaque connexion.
  • 1.2 — Le serveur qui compte. Renvoyer le nombre de caractères, et afficher le total de clients servis.
  • 1.3 — Interroger plusieurs machines. Trois serveurs, une moyenne, et surtout : ne pas planter si l’un ne répond pas.
  • 1.4 — Serveur calculatrice. "12 + 30" → "42".
  • 1.5 — Observer le blocage. Deux clients en même temps.

Un piège dans l’exercice 1.4

N’utilisez jamais eval()

Faille de sécurité majeure. Un client malveillant enverrait :

"__import__('os').system('rm -rf /')"

Découpez avec split() et traitez l’opérateur avec des if.

Le même réflexe reviendra en séance 3, avec pickle.

Ce qu’il faut retenir

  • Le Big Data commence quand une machine ne suffit plus en temps
  • Distribuer = échanger de la puissance contre de la coordination
  • IP = la machine, port = le programme
  • Serveur : bind + listen + accept. Client : connect
  • On envoie des octets : .encode() / .decode()
  • recv(n) lit au plus nn octets ; toujours sendall()
  • TCP garantit l’ordre des octets, pas la séparation des messages