`SOCK_STREAM` n’est pas TCP : comprendre les types de sockets et les protocoles

SOCK_STREAM n’est pas TCP : comprendre les types de sockets et les protocoles

Table des matières

Si vous avez écrit du code réseau en C/C++ ou en Python, vous avez probablement intégré une règle qui semble universelle :
SOCK_STREAM est généralement associé à TCP et SOCK_DGRAM à UDP.

C’est parce que la plupart des exemples, des tutoriels, et même la documentation officielle (pages de manuel TCP et UDP) créent des sockets TCP et UDP de cette manière :

1// socket TCP
2socket(AF_INET, SOCK_STREAM, 0);
3
4// socket UDP
5socket(AF_INET, SOCK_DGRAM, 0);

La seule différence entre les deux appels est le second paramètre, ce qui rend naturel l’association entre SOCK_STREAM et TCP, et SOCK_DGRAM et UDP.

Cependant, cette association est un raccourci pratique, pas une vérité fondamentale.

Si on explicite cela, la relation devient plus claire :

1// socket TCP
2socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
3
4// socket UDP
5socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);

Lorsque vous appelez socket(domain, type, protocol), vous combinez :

  • Un domain (ou famille d’adresses) qui définit où la communication a lieu.
  • Un type qui définit son comportement.
  • Un protocol qui définit son implémentation réelle.

Le protocole est passé dans le troisième paramètre.

SOCK_STREAM et SOCK_DGRAM représentent le type de socket.

Ce que définissent réellement les types de sockets

Le type de socket ne définit pas la manière dont les données circulent sur le réseau. Il définit la manière dont votre programme interagit avec ces données.

Une socket de type flux (SOCK_STREAM) fournit une interface orientée connexion, où : les octets arrivent dans l’ordre et il n’existe pas de frontières entre messages (un flux continu d’octets). Vous lisez et écrivez comme si vous manipuliez un flux continu.

Cependant, la fiabilité dépend du protocole de transport sous-jacent.

Cette animation montre comment les données sont envoyées et reçues au niveau de l’API socket. Chaque appel “send” est représenté comme un message distinct côté émetteur.

Cette visualisation se situe au niveau de l’API socket et ne représente pas le comportement au niveau des paquets réseau.

Voici ce que cela donne en code :

 1// ÉMETTEUR
 2send(sender_socket, "Hello ", 6, 0);
 3send(sender_socket, "world!", 6, 0);
 4
 5// RÉCEPTEUR
 6// Le récepteur obtient "Hello world!" comme
 7// un flux continu, pas deux messages séparés.
 8char buffer[13] = {0};
 9recv(receiver_socket, buffer, 12, 0);
10printf("%s\n", buffer); // Affiche : "Hello world!"

Le code de réception lirait toujours les 12 octets envoyés, même si recv() est appelé plusieurs fois avec des tailles plus petites :

1// RÉCEPTEUR
2char buffer[13] = {0};
3recv(receiver_socket, buffer, 9, 0);
4printf("%s\n", buffer); // Affiche : "Hello wor"
5recv(receiver_socket, buffer, 6, 0);
6printf("%s\n", buffer); // Affiche : "ld!"

Si recv() demande plus de données que ce qui est disponible, il ne bloque pas en attendant la quantité demandée. Il ne bloque que lorsqu’aucune donnée n’est disponible ; sinon, il retourne immédiatement les octets déjà prêts. Les sockets de type flux ne préservent pas les frontières des messages — les lectures partielles sont la norme, pas l’exception.

Note : Le deuxième recv() dans l’exemple précédent retourne 3 octets parce que c’est tout ce qui était disponible à ce moment-là. En pratique, cela dépend du timing : il peut retourner moins d’octets que demandé, ou davantage si des données supplémentaires sont déjà arrivées dans le tampon du socket. recv() retourne en fonction de la disponibilité des données, et non de la taille demandée.

Rien de tout cela n’implique TCP (même si c’est le cas le plus courant). Cela décrit uniquement un comportement.

De même, une socket de datagramme (SOCK_DGRAM) fournit une interface orientée message, où les messages peuvent : arriver dans le désordre, être perdus, et conserver des frontières claires.

Vous lisez et écrivez par blocs distincts.

Remarquez comment la même séquence d’envois est reçue différemment ici, par rapport à un socket de type flux.

 1// ÉMETTEUR
 2send(sender_socket, "Hello ", 6, 0);
 3send(sender_socket, "world!", 6, 0);
 4
 5// RÉCEPTEUR
 6// Le récepteur obtient deux messages séparés : "Hello " et "world!".
 7char buffer[13] = {0};
 8recv(receiver_socket, buffer, 12, 0);
 9printf("%s\n", buffer); // Affiche : "Hello "
10recv(receiver_socket, buffer, 12, 0);
11printf("%s\n", buffer); // Affiche : "world!"

Même si la taille du buffer est identique, le comportement est complètement différent de SOCK_STREAM.

Si vous appelez recv() avec une taille inférieure à celle du message envoyé, vous ne recevrez qu’une partie du message.

Contrairement aux sockets de type flux, où les données non lues restent disponibles, les octets restants de ce datagramme sont perdus.

Chaque appel à recv() renvoie au plus un message.

Si le buffer est plus grand que le datagramme, recv() ne retourne malgré tout qu’un seul message. L’espace supplémentaire du buffer est simplement inutilisé. Les frontières des datagrammes sont conservées, et les messages ne sont jamais fusionnés en une seule lecture.

S’il n’y a aucun datagramme disponible, recv() bloque jusqu’à l’arrivée d’un paquet (ou retourne immédiatement une erreur en mode non bloquant).

Note : Le mode bloquant / non bloquant est un mode d’E/S (ou I/O) du socket et s’applique à tous les types de sockets. Il détermine si recv() peut attendre des données, mais n’affecte pas la sémantique de livraison sous-jacente du protocole.

Il existe d’autres types de sockets, comme SOCK_RAW, qui contournent entièrement les protocoles de transport pour donner un accès direct aux paquets réseau de bas niveau (généralement des paquets IP). Mais dans la plupart des cas, le choix dépend de la manière dont vous souhaitez gérer le flux de données.

Quand SOCK_STREAM n’est pas TCP

En pratique, la plupart du code se ressemble:

1socket(AF_INET, SOCK_STREAM, 0);

Le paramètre protocol est défini à 0 pour sélectionner le protocole par défaut selon la famille et le type.

Pour la combinaison AF_INET et SOCK_STREAM, le protocole par défaut est TCP. Pour AF_INET et SOCK_DGRAM, le protocole par défaut est UDP.

Le noyau effectue ce choix automatiquement, et avec le temps, cette distinction disparaît de la vue.

Même type de socket, domaine différent

1socket(AF_UNIX, SOCK_STREAM, 0);

Vu de l’extérieur, cela se comporte comme une socket de flux classique. Vous obtenez une communication fiable, ordonnée et orientée connexion. Votre code lit et écrit de la même manière.

Mais en dessous, tout change.

Il n’y a ni TCP, ni IP, ni notion de ports. La communication ne quitte jamais la machine.

Vous utilisez une socket de domaine Unix, qui repose sur une couche de transport totalement différente.

Comme la communication est locale, le noyau contourne une grande partie de la pile réseau IP. Il n’y a pas besoin d’en-têtes IP, de routage ou de mécanismes TCP comme le contrôle de congestion ou la gestion des fenêtres, ce qui rend la communication plus simple et plus rapide.

L’abstraction reste identique, mais l’implémentation est une copie mémoire à mémoire.

Une connexion TCP implique retransmissions, contrôle de congestion et routage réseau. Une socket Unix est une communication locale inter-processus, souvent plus rapide et plus simple.

Protocole différent, même abstraction de socket

SCTP (Stream Control Transmission Protocol) est un protocole de transport fiable et orienté connexion. Contrairement à TCP, il est fondamentalement orienté message et prend en charge plusieurs flux indépendants dans une même association (équivalent SCTP d’une connexion entre deux points de terminaison).

Dans l’API socket, SCTP est généralement utilisé avec SOCK_SEQPACKET, qui conserve les frontières de messages :

1socket(AF_INET, SOCK_SEQPACKET, IPPROTO_SCTP);

Cependant, RFC 6458 définit un mode one-to-one où SCTP peut, sur certains systèmes, être exposé via une interface SOCK_STREAM, même s’il reste un protocole orienté message en interne :

1socket(AF_INET, SOCK_STREAM, IPPROTO_SCTP);

Dans ce cas, l’application observe une interface de type flux où les frontières de messages ne sont pas visibles. Cela ne change pas la sémantique interne de SCTP ; cela modifie uniquement la manière dont le système d’exploitation expose les données via l’API socket.

Cela met en évidence une distinction importante : le type de socket définit l’interface côté application, tandis que le protocole définit le comportement de transport sous-jacent.

En spécifiant explicitement IPPROTO_SCTP, SCTP est utilisé comme protocole de transport, tandis que l’application interagit via une interface de type flux.

Tip

SOCK_SEQPACKET est un autre type de socket.

C’est un hybride : il est orienté connexion et fiable (comme SOCK_STREAM), tout en préservant les frontières des messages (comme SOCK_DGRAM).

Il est couramment utilisé avec SCTP et constitue également un outil standard pour la communication inter-processus locale via AF_UNIX.

Quand SOCK_DGRAM n’est pas UDP

Le même concept s’applique aux datagrammes. Si vous écrivez :

1socket(AF_UNIX, SOCK_DGRAM, 0);

Vous n’utilisez plus UDP. Vous utilisez une socket de datagramme du domaine Unix.

Comme il s’agit de SOCK_DGRAM, les frontières de messages sont conservées : si vous envoyez 100 octets, le récepteur reçoit exactement 100 octets.

Cependant, contrairement à UDP sur Internet, un datagramme Unix est fiable. Comme le transfert est géré localement, les paquets ne peuvent pas être perdus par un routeur ou arriver dans le désordre, même si une perte peut encore se produire si les buffers locaux sont saturés. Si le buffer du récepteur est plein, l’émetteur bloque ou reçoit EAGAIN, ce qui évite toute perte silencieuse.

C’est une interface orientée messages avec une livraison locale fiable (au niveau du noyau), mais sans les garanties de livraison de bout en bout de TCP.

Le type de socket définit l’interface (frontières de messages), tandis que le domaine définit la réalité (communication locale fiable). Autrement dit : le type de socket définit le comportement, le protocole définit les mécanismes, et le domaine définit le périmètre.

Si vous avez déjà constaté comment des hypothèses sur les familles d’adresses et le comportement des sockets peuvent casser des systèmes réels, j’ai détaillé un cas concret ici : Ça marchait avant : comment une mise à jour de l’OS a cassé mes sockets Rust.

Pourquoi cette abstraction est importante

Il est facile de penser que l’API socket est trop verbeuse. Pourquoi demander un domaine, un type et un protocole si dans 99 % des cas on veut simplement TCP sur IP ?

La réponse est la séparation des responsabilités et la pérennité du modèle.

Les ingénieurs qui ont conçu les sockets de Berkeley dans les années 1980 savaient que les réseaux allaient évoluer.

En découplant le besoin de l’application (par exemple “avoir un flux fiable d’octets”) de l’implémentation réseau (TCP, SCTP ou IPC local), ils ont créé une API qui dure depuis plus de quarante ans.

SOCK_STREAM ne signifie pas TCP. C’est un contrat entre votre application et le système d’exploitation. Le protocole détermine comment ce contrat est respecté.

Tant que le système respecte ce contrat, les détails internes de transport des octets ne concernent pas l’application.

Cette séparation permet à l’API socket de rester indépendante de tout protocole de transport particulier.

Points clés

  • SOCK_STREAM ≠ TCP
  • SOCK_DGRAM ≠ UDP
  • Type de socket = sémantique d’interface (comment les données sont exposées)
  • Protocole = sémantique de transport (comment les données sont transmises)
  • Domaine = périmètre de communication (où la communication a lieu)
  • Le type de socket (SOCK_STREAM, SOCK_DGRAM, SOCK_SEQPACKET, etc.) est une abstraction locale de l’API et n’est pas négocié entre les pairs ; le client et le serveur n’ont pas besoin d’utiliser le même type de socket, seulement des types compatibles avec le même protocole sous-jacent.

Articles Connexes

Au-delà de 127.0.0.1 : vous possédez 16 millions d'adresses loopback

Au-delà de 127.0.0.1 : vous possédez 16 millions d’adresses loopback

De nombreux développeurs utilisent localhost et 127.0.0.1 de manière interchangeable, en pensant que le loopback se limite à une seule adresse. …

Lire la suite
*Ça marchait avant* : comment une mise à jour de l’OS a cassé mes sockets Rust

Ça marchait avant : comment une mise à jour de l’OS a cassé mes sockets Rust

Je n’ai changé aucune ligne de code. J’ai simplement mis à jour mon système d’exploitation (OS), et soudain mon outil Rust a cessé de fonctionner. …

Lire la suite
Sensibilité à la casse : 70 ans d’évolution de Fortran à Mojo

Sensibilité à la casse : 70 ans d’évolution de Fortran à Mojo

Une analyse basée sur les données couvrant 70 ans d’histoire de la programmation, de Pascal à Go et Nim, pour comprendre pourquoi certains langages …

Lire la suite