← Tous les cours / TCP & Sockets — La plomberie des serveurs Cours
00 — VUE D'ENSEMBLE

TCP & Sockets :
la plomberie des serveurs

HTTP, Redis, PostgreSQL, SSH — ils ont tous une chose en commun : au fond, ils s'appuient sur des sockets TCP. Construire un serveur TCP de zéro, c'est comprendre ce que tous ces outils font en dessous.

Un serveur web, un serveur de base de données, un cache Redis : à première vue, des bêtes très différentes. Mais descends d'un niveau — au niveau du système d'exploitation — et tu trouveras exactement la même danse : créer un socket, le lier à un port, écouter les connexions, en accepter une, lire des octets, écrire des octets, fermer. La chorégraphie est la même pour tous.

Ce cours démonte cette mécanique pièce par pièce. Tu commences par comprendre ce qu'est vraiment TCP (un tuyau d'octets fiable, pas un protocole applicatif), tu découvres l'API Berkeley Sockets qui sert d'interface entre ton code et le noyau, et tu termines en construisant toi-même un serveur et un client en Python — une vingtaine de lignes qui font exactement ce que fait Nginx quand il accepte ta connexion.

01 — LE SERVEUR TCP

Un serveur, c'est juste
un processus qui attend

Pas de hardware mystérieux. Juste un programme comme les autres — qui écoute sur un port et échange des octets avec qui se connecte.

Quand on parle de « serveur » ici, on ne parle pas de machines physiques. Un serveur, c'est simplement un processus lancé par le système d'exploitation : il attend des clients, reçoit des données via une connexion réseau, applique sa logique, et renvoie une réponse.

Ce qui le rend « TCP » c'est le transport : au lieu de lire depuis l'entrée standard ou un fichier, il reçoit des octets depuis une connexion TCP. Et à la place d'écrire dans un fichier ou sur la sortie standard, il renvoie des octets sur cette même connexion.

Le mot-clé : octets. TCP ne sait pas si tu envoies une requête HTTP, une commande Redis, une requête PostgreSQL ou une ligne de texte. Pour lui, tout ça c'est la même chose : un flux d'octets ordonné entre deux programmes. C'est la couche applicative qui décide de leur signification.

Tous les serveurs font la même danse

Un serveur HTTP, un serveur de base de données, un cache : malgré leurs différences énormes au niveau applicatif, ils partagent tous le même rituel au niveau des sockets :

  1. Créer un socket
  2. Le lier (bind) à une adresse et un port
  3. Passer en mode écoute (listen)
  4. Accepter une connexion cliente (accept)
  5. Lire des octets (recv) et en écrire (send)
  6. Fermer la connexion (close)

Les détails du protocole applicatif changent du tout au tout — mais la plomberie en dessous reste la même. C'est cette plomberie que tu vas apprendre ici.

Quelques protocoles bâtis sur TCP

Navigateur
HTTP/1.1
TCP
Serveur web
Nginx
Application
Redis CLI
RESP / TCP
Cache
Redis
Terminal
ssh client
SSH / TCP
Machine distante
sshd

Dans tous les cas, TCP transporte un flux d'octets — c'est le protocole au-dessus (HTTP, RESP, SSH) qui leur donne un sens.

Lien avec le cours PHP : quand Nginx transmet une requête à PHP-FPM, il utilise un socket Unix (AF_UNIX) ou TCP (AF_INET) — la même mécanique, avec un protocole applicatif différent : FastCGI. Voir le cours PHP, fiche 05.
02 — LES SOCKETS

Une socket :
une prise dans le noyau

Le système d'exploitation expose une abstraction unifiée pour que deux processus puissent s'échanger des données — localement ou via le réseau. Cette abstraction, c'est la socket.

Socket — abstraction fournie par le système d'exploitation pour la communication inter-processus (IPC). Une socket peut être ouverte, lue, écrite, et fermée. Elle fonctionne comme un fichier spécial : dans les langages bas niveau comme C, socket() retourne un descripteur de fichier (un entier). En Python, c'est un objet socket qui encapsule ce descripteur.

L'analogie avec les prises électriques n'est pas accidentelle : le concept a été emprunté au monde réel. Une prise électrique définit une interface standardisée — même forme, même tension — qui permet à n'importe quel appareil de se connecter à n'importe quel mur. Les sockets Berkeley font pareil : une interface unifiée pour connecter des processus, quelle que soit leur nature.

Familles et types de sockets

Une socket se décrit par deux dimensions : sa famille (qui détermine l'espace d'adressage) et son type (qui détermine le style de communication).

FamilleSignificationUsage typique
AF_INETIPv4Communication réseau via IPv4
AF_INET6IPv6Communication réseau via IPv6
AF_UNIXLocal (même machine)IPC entre processus sur la même machine
TypeSignificationProtocole courant
SOCK_STREAMFlux d'octets fiable et ordonnéTCP (avec AF_INET/AF_INET6)
SOCK_DGRAMDatagrammes (sans connexion)UDP (avec AF_INET/AF_INET6)
Piège fréquent : AF_UNIX + SOCK_STREAM n'est pas TCP. C'est un flux local géré directement par le noyau, sans en-têtes IP, sans ports, sans routage. PHP-FPM et Nginx s'y connectent souvent via AF_UNIX pour éviter l'overhead réseau — mais la mécanique socket reste la même.

Dans ce cours, on se concentre sur la combinaison la plus courante : AF_INET + SOCK_STREAM — soit des sockets TCP sur IPv4.

Le descripteur de fichier

En C (et dans le noyau Linux), une socket est représentée par un descripteur de fichier : un entier renvoyé par l'appel socket(). C'est exactement comme l'entier que renvoie open() pour un fichier ordinaire. En Python, la classe socket.socket encapsule ce descripteur — tu peux l'obtenir avec sock.fileno() si tu en as besoin.

Les appels recv() et send() fonctionnent comme read() et write() sur un fichier, mais acceptent en plus un paramètre flags pour des comportements particuliers (comme MSG_PEEK pour lire sans consommer les données).

03 — TCP : FIABILITÉ & PAQUETS

Des octets fragmentés,
puis réassemblés

Le réseau ne transporte pas des messages entiers. Il transporte de petits paquets numérotés — et c'est TCP qui se charge de les remettre dans l'ordre, même s'ils arrivent en désordre.

La fragmentation

Quand ton programme appelle send(), les données ne traversent pas le réseau en un seul bloc. La carte réseau les découpe en paquets (la taille maximale dépend du MTU — Maximum Transmission Unit — typiquement 1500 octets sur Ethernet). Chaque paquet embarque :

La couche IP s'occupe du routage : chaque paquet peut emprunter un chemin différent et arriver dans le désordre. C'est TCP qui reprend la main ensuite.

Comment TCP garantit l'ordre et l'intégrité

TCP ajoute au-dessus d'IP deux mécanismes complémentaires :

Depuis le point de vue de ton programme, tout ça est invisible : recv() te retourne les octets dans l'ordre correct, toujours. TCP a géré la plomberie à ta place.

TCP vs UDP : UDP (SOCK_DGRAM) abandonne la fiabilité et l'ordre en échange de la vitesse. Pas de numéros de séquence, pas d'accusé de réception. C'est le choix de DNS, des jeux en temps réel, ou de QUIC (qui réimplémente sa propre fiabilité par-dessus). TCP est le bon choix dès que tu as besoin d'exactitude : commandes SQL, pages HTTP, données de configuration.

Simulateur — fragmentation et réassemblage

Clique sur Envoyer pour voir un message découpé en paquets numérotés, voyager dans le désordre, puis être réassemblé correctement par TCP.

Message original
"Hello, serveur ! Ceci est un message de test TCP."
Appuie sur "Envoyer le message" pour lancer la simulation.
04 — CONNEXIONS & PORTS

Le handshake :
établir la connexion

Avant d'envoyer le premier octet de données, client et serveur s'accordent sur un état partagé. C'est le handshake en 3 temps de TCP.

Une connexion = un accord sur les numéros de séquence

Pour que les deux extrémités puissent numéroter leurs octets et détecter les pertes, elles doivent d'abord se mettre d'accord sur leurs numéros de séquence initiaux. C'est l'objet du handshake TCP en 3 voies (three-way handshake) :

  1. SYN — le client envoie un paquet de synchronisation avec son numéro de séquence initial.
  2. SYN-ACK — le serveur accuse réception (ACK) du numéro du client et envoie le sien.
  3. ACK — le client accuse réception du numéro du serveur. La connexion est établie des deux côtés.

Ce n'est qu'après ce handshake que les données applicatives peuvent circuler.

Les ports : différencier les connexions

Sur une même machine, plusieurs processus peuvent écouter en même temps (un serveur web sur le port 80, une base de données sur le 5432, SSH sur le 22…). Et un même serveur peut avoir des milliers de clients connectés simultanément. Comment distinguer tout ça ?

TCP identifie chaque connexion par un tuple à 4 éléments — ce tuple doit être unique dans le monde entier au moment où la connexion existe :

(IP source, port source, IP destination, port destination)

Exemple : (203.0.113.42, 51234, 93.184.216.34, 80)
Un client depuis son IP, sur son port éphémère, vers un serveur web.

Le port destination est choisi par l'application serveur (80 pour HTTP, 443 pour HTTPS, 5432 pour PostgreSQL…). Le port source du client, lui, est choisi automatiquement par le système d'exploitation : c'est un port éphémère, un numéro temporaire dans la plage 49152–65535 sur Linux.

Simulateur — le handshake TCP en 3 voies

Clique sur Étape suivante pour avancer pas à pas dans l'établissement d'une connexion TCP.

Client
FERMÉ
Réseau
paquets TCP
Serveur
EN ÉCOUTE
SYN (seq=1000)connect() côté client
SYN-ACK (seq=5000, ack=1001)accept() débloqué côté serveur
ACK (ack=5001)connect() revient côté client
DATA : "Hello, serveur !"sendall() côté client
DATA : "Hello, serveur !"sendall() echo côté serveur
FINshutdown() + close() côté client
État initial : le serveur écoute, le client n'est pas encore connecté.
05 — L'API BERKELEY SOCKETS

Le contrat entre
ton code et le noyau

L'API Berkeley Sockets, née en 1983 avec BSD Unix, définit les appels système pour créer des connexions réseau. Elle est aujourd'hui présente dans tous les langages : C, Python, Go, Java, Node.js… avec des noms légèrement différents, mais la même logique.

Les appels du côté serveur

Appel (Python)Ce qu'il faitRemarque
socket(AF_INET, SOCK_STREAM) Crée un socket TCP/IPv4. Retourne un objet socket (qui encapsule un descripteur de fichier). Le troisième paramètre proto=0 laisse le noyau choisir le protocole adapté au type (TCP pour SOCK_STREAM).
bind(('0.0.0.0', 6543)) Réserve le port 6543 sur toutes les interfaces réseau (0.0.0.0 = toutes). '127.0.0.1' limite l'écoute à la boucle locale (localhost uniquement).
listen(backlog) Bascule le socket en mode écoute. Le paramètre backlog fixe la taille de la file d'attente des connexions entrantes que le noyau maintient avant qu'accept() les traite. Une valeur de 10 signifie que le noyau peut maintenir jusqu'à 10 connexions en attente sans les perdre.
accept() Bloque jusqu'à l'arrivée d'un client. Retourne un nouveau socket pour cette connexion, plus l'adresse du client. Le socket d'écoute originel continue d'écouter. C'est le nouveau socket qui sert à dialoguer avec ce client.
recv(taille) Lit jusqu'à taille octets depuis la connexion. Retourne b'' (bytes vide) quand le client a fermé sa connexion. Un appel unique à recv() peut retourner moins d'octets que demandé — il faut souvent boucler.
sendall(data) Envoie tous les octets de data. Réessaie automatiquement si l'envoi est partiel. send() peut envoyer moins d'octets que demandé sans signaler d'erreur — sendall() est plus sûr.
close() Ferme la connexion et libère les ressources associées. À appeler sur le socket client après chaque échange, et sur le socket d'écoute à l'arrêt du serveur.

Les appels du côté client

Appel (Python)Ce qu'il fait
socket(AF_INET, SOCK_STREAM) Identique au serveur — crée un socket TCP/IPv4.
connect(('ip', port)) Initie la connexion vers le serveur. Déclenche le handshake TCP en 3 voies. Bloque jusqu'à ce que la connexion soit établie.
sendall(data) Envoie les données vers le serveur.
shutdown(SHUT_WR) Signale au serveur que le client a fini d'envoyer des données (envoie un FIN TCP). La connexion reste ouverte en lecture pour recevoir la réponse.
recv(taille) Lit la réponse du serveur.
close() Ferme la connexion.
💡 Pourquoi accept() crée un nouveau socket ? Le socket d'écoute est une "oreille" permanente — il reste ouvert pour accueillir d'autres clients. Chaque connexion cliente nécessite son propre socket dédié pour que les échanges de données ne se mélangent pas. Après accept(), tu te retrouves avec deux sockets côté serveur : l'écouteur, et le socket de la connexion en cours.
06 — CONSTRUIRE LE SERVEUR

Le serveur TCP
en Python

Vingt lignes. C'est tout ce qu'il faut pour un serveur TCP fonctionnel qui écoute les connexions, reçoit des données et les renvoie à l'identique (un "echo server").

server.py — code complet

import socket

# Crée un socket TCP sur IPv4.
# proto=0 : le noyau choisit TCP automatiquement pour SOCK_STREAM.
serv_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM, proto=0)

# Lie le socket à toutes les interfaces (0.0.0.0) sur le port 6543.
serv_sock.bind(('0.0.0.0', 6543))

# Passe en mode écoute. Le backlog (10) = taille de la file d'attente noyau.
serv_sock.listen(10)

while True:
    # Bloque jusqu'à l'arrivée d'un client.
    # Retourne un NOUVEAU socket pour cette connexion + l'adresse du client.
    client_sock, client_addr = serv_sock.accept()
    print('Nouvelle connexion depuis', client_addr)

    chunks = []
    while True:
        # Lit jusqu'à 2048 octets. Retourne b'' quand le client a fini d'envoyer.
        data = client_sock.recv(2048)
        if not data:
            break
        chunks.append(data)

    # Renvoie tout ce qui a été reçu (echo).
    client_sock.sendall(b''.join(chunks))
    client_sock.close()

Ce que fait chaque partie

socket.socket(AF_INET, SOCK_STREAM, proto=0)

Crée le socket d'écoute. AF_INET = IPv4, SOCK_STREAM = flux TCP fiable. Le proto=0 est optionnel : quand il vaut 0, le noyau déduit TCP d'après SOCK_STREAM.

bind(('0.0.0.0', 6543))

'0.0.0.0' signifie "toutes les interfaces réseau de la machine" : loopback, Ethernet, Wi-Fi. Pour restreindre à localhost uniquement, on utiliserait '127.0.0.1'. Le port 6543 est arbitraire — les ports en dessous de 1024 nécessitent des privilèges root sur Linux.

listen(10)

Le paramètre backlog contrôle la file d'attente que le noyau maintient pour les connexions arrivées mais pas encore traitées par accept(). Si 10 clients se connectent pendant qu'on traite le premier, leurs connexions sont mises en attente. Au-delà, le noyau refuse les connexions suivantes.

serv_sock.accept()

Appel bloquant : le programme s'arrête ici jusqu'à ce qu'un client se connecte. Quand c'est le cas, accept() retourne deux valeurs : client_sock (un nouveau socket dédié à cette connexion) et client_addr (le tuple (ip, port) du client). Le serv_sock continue d'écouter d'autres connexions.

La boucle recv(2048)

Un seul appel à recv() peut retourner moins d'octets que la taille demandée : TCP garantit l'ordre, pas la cohérence des lots. Il faut donc boucler jusqu'à ce que recv() retourne b'' (bytes vide) — ce qui indique que le client a fermé sa connexion ou appelé shutdown(SHUT_WR).

sendall()

Contrairement à send() (qui peut envoyer moins d'octets que demandé et retourner le nombre réellement envoyé), sendall() réessaie en boucle jusqu'à ce que tous les octets soient transmis — ou lève une exception en cas d'erreur réseau.

Serveur séquentiel : ce serveur traite un client à la fois. Pendant qu'il reçoit les données d'un client, tous les autres attendent dans la file du backlog. Pour gérer plusieurs clients en parallèle, il faudrait utiliser des threads, des processus (comme PHP-FPM), ou de l'I/O asynchrone.
07 — CONSTRUIRE LE CLIENT

Le client TCP :
plus simple que le serveur

Le client n'a pas besoin de bind(), de listen(), ni d'accept(). Il crée un socket, se connecte, envoie ses données, signale qu'il a fini, et lit la réponse.

client.py — code complet

import socket

# Crée un socket TCP/IPv4. Même appel que pour le serveur.
client_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

# Se connecte au serveur. Déclenche le handshake TCP en 3 voies.
# Remplace l'IP par celle de ta machine serveur.
client_sock.connect(('127.0.0.1', 6543))

# Envoie les données.
client_sock.sendall(b'Hello, serveur !')

# Signale qu'on a fini d'envoyer (FIN TCP côté écriture).
# La connexion reste ouverte en lecture pour recevoir la réponse.
client_sock.shutdown(socket.SHUT_WR)

# Lit la réponse. Même prudence que côté serveur : boucler jusqu'à b''.
chunks = []
while True:
    data = client_sock.recv(2048)
    if not data:
        break
    chunks.append(data)

print('Reçu :', repr(b''.join(chunks)))

# Ferme la connexion.
client_sock.close()

Pourquoi le client est plus simple

Le serveur doit maintenir un socket d'écoute permanent et créer un nouveau socket à chaque connexion. Le client n'a qu'une seule connexion à gérer : la sienne. Pas de bind() (le noyau assigne automatiquement un port éphémère), pas de listen(), pas d'accept().

Le rôle de shutdown(SHUT_WR)

Quand le serveur boucle sur recv(), comment sait-il que le client a fini d'envoyer ? Il attend que recv() retourne b''. Cela se produit quand le client ferme sa connexion en écriture — ce que shutdown(SHUT_WR) fait : il envoie un paquet FIN TCP, signalant "j'ai fini d'écrire", tout en gardant la connexion ouverte en lecture.

Si le client appelait directement close() sans passer par shutdown(), la connexion se fermerait complètement et il ne pourrait plus lire la réponse du serveur.

Ordre des opérations côté client :
  1. connect() — établit la connexion
  2. sendall() — envoie les données
  3. shutdown(SHUT_WR) — signale la fin d'envoi
  4. recv() en boucle — lit la réponse
  5. close() — ferme définitivement
🔗 Remarque sur l'IP dans l'exemple : on utilise '127.0.0.1' (loopback) pour un test local où client et serveur tournent sur la même machine. Dans un environnement distribué, tu remplacerais par l'IP réelle du serveur.
08 — TOUT ASSEMBLER

Le cycle de vie
d'une connexion TCP

Client et serveur, côte à côte. Clique sur "Étape suivante" pour avancer dans le cycle de vie complet, appel système par appel système.

SERVEUR

socket()Crée le socket d'écoute
bind(0.0.0.0:6543)Réserve le port 6543
listen(10)Passe en mode écoute
accept() ⏳Bloque en attendant un client
accept() ✓Retourne conn_sock (fd=4)
recv(2048)Lit "Hello, serveur !"
sendall(data)Renvoie les données (echo)
recv() → b''Le client a fermé l'écriture
close(conn_sock)Ferme cette connexion
Descripteurs de fichier actifs

CLIENT

socket()Crée le socket client
connect(127.0.0.1:6543)Déclenche le handshake TCP
sendall(b'Hello…')Envoie les données
shutdown(SHUT_WR)Signale la fin d'envoi (FIN)
recv() → dataReçoit l'echo du serveur
close()Ferme la connexion
Note
Le client n'appelle jamais bind(), listen() ou accept().
Le noyau lui assigne un port éphémère automatiquement.
Appuie sur "Étape suivante" pour démarrer la simulation.

Les limites du serveur séquentiel

Le serveur qu'on a écrit traite les clients un par un. Pendant qu'il lit les données d'un client, les autres attendent dans la file du backlog. Si un client est lent (connexion mauvaise, gros upload), tous les autres attendent.

Dans la vraie vie, on résout ça de trois façons :

Lien avec le cours PHP : PHP-FPM est un gestionnaire de processus qui pré-fork un pool de workers. Chacun écoute sur un socket Unix ou TCP, accepte une connexion FastCGI d'Nginx, exécute un script PHP, et se remet en attente. La même mécanique socket — multipliée. Voir le cours PHP, fiche 06.
09 — SYNTHÈSE

Ce qu'il faut
retenir

Un concept, une phrase. La référence rapide à relire dans un an.

ConceptL'essentiel
Serveur TCPUn processus qui attend des connexions sur un port et échange des octets. La logique applicative (HTTP, SQL…) est au-dessus de TCP.
SocketAbstraction OS pour la communication inter-processus. En C : un descripteur de fichier. En Python : un objet socket.
AF_INET + SOCK_STREAMIPv4 + TCP — le combo utilisé par la quasi-totalité des serveurs réseau.
AF_UNIX + SOCK_STREAMFlux local géré par le noyau — pas TCP, pas de ports, pas de réseau. Utilisé par PHP-FPM ↔ Nginx.
Paquets TCPLes données sont fragmentées en paquets numérotés. TCP les réassemble dans l'ordre correct, invisible depuis le code.
Handshake 3 voiesSYN → SYN-ACK → ACK. S'accordent sur les numéros de séquence avant tout échange de données.
Tuple à 4 éléments(IP src, port src, IP dst, port dst) — identifie chaque connexion de façon unique dans le monde entier.
bind()Réserve un port sur une interface. Serveur uniquement.
listen(backlog)Active l'écoute. Le backlog = taille de la file d'attente noyau.
accept()Bloque jusqu'à un client, retourne un nouveau socket pour cette connexion. Le socket d'écoute reste ouvert.
recv()Retourne b'' quand le client a fermé sa connexion en écriture. Boucler jusqu'à ce cas.
sendall() vs send()sendall() garantit que tout est envoyé. send() peut être partiel sans lever d'exception.
shutdown(SHUT_WR)Envoie un FIN TCP (fin d'écriture) tout en gardant la lecture ouverte. Permet de recevoir la réponse après avoir fini d'envoyer.

Guide de choix rapide

Tu veux communiquer entre deux processus sur la même machine ?
→ Utilise AF_UNIX + SOCK_STREAM. Moins d'overhead, pas besoin de ports.
Tu veux communiquer à travers le réseau (ou entre conteneurs) ?
→ Utilise AF_INET + SOCK_STREAM (TCP). Fiable, ordonné, universel.
Tu veux de la vitesse et tu peux tolérer des pertes (streaming, jeux, DNS) ?
→ Considère AF_INET + SOCK_DGRAM (UDP). Mais tu gères toi-même l'ordre et les pertes.

Pour aller plus loin