.. _sockets:
Partie 6 - Sockets
==================
.. raw:: html
Support présentation
Introduction
------------
Un **socket** est un point de communication bidirectionnel entre deux processus.
Il peut servir à communiquer **localement** (``AF_UNIX``) ou via le **réseau**
(``AF_INET`` pour IPv4, ``AF_INET6`` pour IPv6).
Les sockets sont manipulées comme des **descripteurs de fichiers** (entiers), utilisables avec ``read``/``write`` ou ``send``/``recv``.
.. figure:: _images/socket_a_b.excalidraw.png
:alt: Deux processus qui utilisent des sockets pour communiquer via le réseau IP.
:align: center
Modes de transport TCP/UDP
~~~~~~~~~~~~~~~~~~~~~~~~~~
- **TCP** (*Transmission Control Protocol*) : ``SOCK_STREAM``, mode *connecté*, fiable, ordonné (handshake, accusés, retransmissions, contrôle de flux et de congestion). Idéal pour HTTP/HTTPS, SSH, SMTP/IMAP/POP3, bases de données.
- **UDP** (*User Datagram Protocol*) : ``SOCK_DGRAM``, mode *non connecté*, non fiable (best-effort), non ordonné, sans retransmission intégrée. Idéal lorsque la **latence** prime (streaming temps réel, VoIP, jeux).
.. figure:: _images/TCP_UDP.jpg
:alt: Présentation de TCP avec les handshake et de UDP avec des envois de données directs.
:align: center
`Source de l'image `__.
TCP fournit une **connexion** et une **fiabilité**, UDP fournit une **boîte aux lettres** rapide sans garantie. Analogie avec boire de l'eau, avec TCP on est sûr de boire l'eau, avec UDP, on "espère" boire l'eau.
.. figure:: https://i.redd.it/duv11av99nm11.png
:align: center
:alt: Image montrant d'un côté "TCP" avec une personne buvant correctement à une bouteille d'eau, de l'autre "UDP" avec une personne versant le contenu d'une bouteille d'eau sur le visage.
`Source de l'image `__
Modèle OSI et pile TCP/IP
-------------------------
Les sockets se situent au-dessus des protocoles de transport (TCP/UDP) et exposent une API de programmation.
.. figure:: _images/OSI.png
:alt: Modèle OSI avec les 7 couches.
:align: center
`Source de l'image `__.
Principe d'un échange d'une communication en mode connecté (TCP)
----------------------------------------------------------------
Une communication TCP met en jeu deux applications distinctes :
- le **serveur** : attend et accepte les connexions entrantes
- le **client** : initie la connexion vers le serveur
1. Le **serveur** doit être lancé avant le client.
Il crée une socket (``socket``), l'associe à une adresse IP et un numéro de port (``bind``), puis la place en écoute (``listen``).
Quand un client se présente, l'appel ``accept`` crée une **nouvelle socket connectée** dédiée à ce client.
2. Le **client** crée aussi une socket (``socket``) puis tente de se connecter au serveur via ``connect`` en précisant l'adresse IP et le port du serveur.
3. Une fois la connexion établie, les deux côtés peuvent échanger des données avec ``send``/``recv`` (ou ``write``/``read``).
4. Pour terminer, la connexion TCP est fermée proprement avec ``shutdown`` et ``close``, ce qui libère les ressources.
.. admonition:: À retenir
:class: note
- ``listen`` met simplement la socket serveur en attente, mais **ne crée pas la connexion**.
- C'est ``accept`` qui génère une **nouvelle socket connectée** pour dialoguer avec un client, tandis que la socket d'écoute continue à attendre d'autres connexions.
.. figure:: _images/TCP.jpg
:alt: Présentation de TCP.
:align: center
`Source de l'image `__.
Principe d'un échange d'une communication en mode non connecté (UDP)
--------------------------------------------------------------------
Une communication UDP implique également deux applications :
- le **serveur** : attend les datagrammes sur un port donné
- le **client** : envoie des datagrammes vers l'adresse et le port du serveur
Contrairement à TCP, **aucune connexion persistante n'est établie** : chaque envoi est indépendant et peut être perdu ou reçu dans un ordre différent.
1. Le **serveur** crée une socket (``socket``) de type ``SOCK_DGRAM`` puis l'associe à une adresse IP et un port avec ``bind``.
Il utilise ensuite ``recvfrom`` pour attendre la réception d'un datagramme, et peut répondre avec ``sendto``.
2. Le **client** crée aussi une socket (``socket``), puis envoie un datagramme au serveur avec ``sendto`` en indiquant explicitement l'adresse IP et le port de destination.
Pour recevoir la réponse, il utilise ``recvfrom``.
3. Chaque envoi ou réception est indépendant : il n'y a pas de notion de session ou de « connexion » comme en TCP.
4. Les sockets UDP se ferment simplement avec ``close``.
.. admonition:: À retenir
:class: note
- UDP ne garantit **ni la livraison**, **ni l'ordre**, **ni l'absence de doublons**.
- C'est le programme qui doit gérer ces aspects si nécessaire (accusés de réception,
numéros de séquence, retransmission, etc.).
- Ce manque de fiabilité en contrepartie d'une faible latence en fait un choix adapté
au streaming, à la voix sur IP (VoIP) ou aux jeux en ligne.
.. figure:: _images/UDP.jpg
:alt: Présentation de UDP.
:align: center
`Source de l'image `__.
Création d'un socket
--------------------
Prototype C :
.. code-block:: c
int socket(int domain, int type, int protocol);
- ``domain`` : ``AF_INET`` (IPv4), ``AF_INET6`` (IPv6), ``AF_UNIX`` (local)
- ``type`` : ``SOCK_STREAM`` (TCP), ``SOCK_DGRAM`` (UDP)
- ``protocol`` : souvent ``0`` (choix par défaut selon ``type``)
Retourne ``-1`` si erreur (``errno`` contient le code d'erreur).
Exemple :
.. code-block:: c
// connexion TCP en mode connecté
int fd_tcp = socket(AF_INET, SOCK_STREAM, 0);
if (fd_tcp == -1) {
perror("socket TCP");
exit(1);
}
// connexion UDP en mode non connecté
int fd_udp = socket(AF_INET, SOCK_DGRAM, 0);
if (fd_udp == -1) {
perror("socket UDP");
exit(1);
}
Définition des interfaces avec ``bind``
---------------------------------------
``bind`` associe un socket à une adresse locale (IP + port pour ``AF_INET``/``AF_INET6`` ou chemin pour ``AF_UNIX``).
.. code-block:: c
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
- ``sockfd`` : descripteur de socket
- ``addr`` : pointeur vers la structure d'adresse (cast en ``struct sockaddr*``)
- ``addrlen`` : taille de la structure
Retourne ``0`` si succès, ``-1`` si erreur (``errno`` contient le code d'erreur).
.. note::
En pratique, on **n'utilise pas directement** ``struct sockaddr``.
On remplit une structure spécialisée (``sockaddr_in``, ``sockaddr_in6``, ``sockaddr_un``),
puis on la **cast** en ``struct sockaddr*`` pour l'appel système.
.. note::
La fonction ``htons`` (*Host TO Network Short*, ``htonl`` pour *Long*) convertit un entier 16 bits de l'ordre d'octets de la machine (**host order**) vers l'ordre d'octets du réseau (**network order**, big-endian). Elle est définie dans ````.
Utilisation typique : convertir un **numéro de port** avant de l'inscrire
dans une structure d'adresse ``sockaddr_in``.
Sans cette conversion, le programme pourrait fonctionner par hasard
sur une machine **little-endian** (x86) mais échouer sur une architecture
**big-endian** (ordre mémoire différent).
- Adresse IPv4 (``AF_INET`` - ````)
.. code-block:: c
struct sockaddr_in {
sa_family_t sin_family; // AF_INET
in_port_t sin_port; // port en ordre réseau (htons)
struct in_addr sin_addr; // adresse IPv4 (htonl ou inet_pton)
unsigned char sin_zero[8]; // padding pour compatibilité
};
struct in_addr {
uint32_t s_addr; // adresse IPv4 en ordre réseau
};
Exemple :
.. code-block:: c
struct sockaddr_in addr = {0}; // met tous les champs de addr à 0
addr.sin_family = AF_INET;
addr.sin_port = htons(8080); // port 8080 → ordre réseau
addr.sin_addr.s_addr = htonl(INADDR_ANY); // 0.0.0.0 → écouter sur toutes les interfaces IPv4
- Adresse IPv6 (``AF_INET6`` - ````)
.. code-block:: c
struct sockaddr_in6 {
sa_family_t sin6_family; // AF_INET6
in_port_t sin6_port; // port en ordre réseau (htons)
uint32_t sin6_flowinfo; // infos QoS/flux
struct in6_addr sin6_addr; // adresse IPv6 (inet_pton)
uint32_t sin6_scope_id; // identifiant de zone (interfaces locales)
};
struct in6_addr {
unsigned char s6_addr[16]; // adresse IPv6 en binaire
};
Exemple :
.. code-block:: c
struct sockaddr_in6 addr6 = {0};
addr6.sin6_family = AF_INET6;
addr6.sin6_port = htons(8080); // port 8080 → ordre réseau
addr6.sin6_addr = in6addr_any; // :: → écouter sur toutes les interfaces IPv6
- Adresse locale (``AF_UNIX`` - ````)
.. code-block:: c
struct sockaddr_un {
sa_family_t sun_family; // AF_UNIX
char sun_path[108]; // chemin du fichier-socket
};
Exemple :
.. code-block:: c
struct sockaddr_un addr = {0};
addr.sun_family = AF_UNIX;
strcpy(addr.sun_path, "/tmp/mysocket"); // chemin du fichier-socket
Attendre des connexions avec ``listen``
---------------------------------------
Pour les sockets TCP, le serveur utilise ``listen`` pour indiquer qu'il accepte des connexions entrantes :
.. code-block:: c
int listen(int sockfd, int backlog);
- ``sockfd`` : socket créée et bindée
- ``backlog`` : taille maximale de la file d'attente des connexions
Retourne ``0`` si succès, ``-1`` si erreur (``errno`` contient le code d'erreur).
Si plusieurs demandes de connexion ont lieu en même temps, elles sont stockées dans une file d'attente.
Exemple :
.. code-block:: c
int s = socket(AF_INET, SOCK_STREAM, 0);
// ... bind(s, ...)
if (listen(s, 16) == -1) {
perror("listen");
exit(1);
}
// la socket s est maintenant en écoute
Accepter des connexions avec ``accept``
---------------------------------------
Un serveur TCP appelle ``accept`` pour accepter une connexion ce qui crée une **nouvelle socket** connectée pour le client.
.. code-block:: c
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
- ``sockfd`` : descripteur de la socket d'écoute
- ``addr`` : pointeur vers une structure qui contiendra l'adresse du client connecté
- ``addrlen`` : pointeur vers la taille de cette structure
Retourne un nouveau descripteur, distinct de la socket d'écoute si succès, ``-1`` si erreur (``errno`` contient le code d'erreur).
Exemple :
.. code-block:: c
struct sockaddr_in cli;
socklen_t len = sizeof cli;
int c = accept(s, (struct sockaddr*)&cli, &len);
if (c == -1) {
perror("accept");
// gérer l'erreur
} else {
// c est la socket connectée au client
}
Connexion à un socket avec ``connect``
--------------------------------------
Le client appelle ``connect`` pour établir la connexion :
.. code-block:: c
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
- ``sockfd`` : descripteur de la socket du client
- ``addr`` : adresse du serveur
- ``addrlen`` : taille de la structure
Retourne ``0`` si succès, ``-1`` si erreur (``errno`` contient le code d'erreur).
Exemple (IPv4 localhost:8080) :
.. code-block:: c
int sock = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in serv = {0};
serv.sin_family = AF_INET;
serv.sin_port = htons(8080);
inet_pton(AF_INET, "127.0.0.1", &serv.sin_addr);
if (connect(sock, (struct sockaddr*)&serv, sizeof serv) == -1) {
perror("connect");
exit(1);
}
Envoyer et recevoir des données avec ``send``/``recv`` ou ``sendto``/``recvfrom``
---------------------------------------------------------------------------------
Ces fonctions permettent d'échanger des données.
En TCP, la socket est **connectée** et l'adresse du pair est implicite.
En UDP, on utilise généralement ``sendto``/``recvfrom`` pour préciser l'adresse à chaque envoi/réception (sauf si la socket UDP a été ``connect``-ée pour figer le pair).
En TCP :
.. code-block:: c
int send(int sockfd, const void *buf, size_t len, int flags);
int recv(int sockfd, void *buf, size_t len, int flags);
- ``sockfd`` : descripteur de socket connectée
- ``buf`` : tampon de données
- ``len`` : taille du tampon
- ``flags`` : options (souvent 0)
Retourne le nombre d'octets envoyés/reçus ou ``-1`` si erreur.
Exemple (écho simple) :
.. code-block:: c
const char *msg = "Hello\n";
if (send(sock, msg, strlen(msg), 0) == -1) perror("send");
char buf[1024];
ssize_t n = recv(sock, buf, sizeof buf - 1, 0);
if (n > 0) {
buf[n] = 0;
printf("%s", buf);
}
En UDP (avec adresse) :
.. code-block:: c
int sendto(int sockfd, const void *buf, size_t len, int flags,
const struct sockaddr *dest_addr, socklen_t addrlen);
int recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);
- ``sockfd`` : descripteur de socket UDP
- ``buf`` : tampon de données
- ``len`` : taille du tampon
- ``flags`` : options (souvent 0)
- ``dest_addr`` / ``src_addr`` : adresse de destination/source
- ``addrlen`` : taille de la structure d'adresse
Retourne le nombre d'octets envoyés/reçus ou ``-1`` si erreur.
Exemple (ping UDP localhost:8080) :
.. code-block:: c
int u = socket(AF_INET, SOCK_DGRAM, 0);
struct sockaddr_in dst = {0};
dst.sin_family = AF_INET;
dst.sin_port = htons(8080);
inet_pton(AF_INET, "127.0.0.1", &dst.sin_addr);
const char *p = "Ping";
if (sendto(u, p, strlen(p), 0, (struct sockaddr*)&dst, sizeof dst) == -1) perror("sendto");
// côté serveur UDP : recvfrom(...), puis sendto(...) pour répondre
Fermeture d'une socket
----------------------
En fin de communication, il faut **libérer proprement** la socket pour éviter les fuites de ressources et signaler correctement la fin d'échange au pair.
TCP : ``shutdown`` puis ``close``
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
En TCP, la fermeture peut être **demi-duplex** (on arrête l'envoi mais on continue à recevoir) ou **complète**.
L'API propose :
- ``shutdown(fd, SHUT_WR)`` // on n'envoie plus, on peut encore recevoir (half-close)
- ``shutdown(fd, SHUT_RD)`` // on ne reçoit plus
- ``shutdown(fd, SHUT_RDWR)`` // on n'envoie ni ne reçoit (fermeture logique des deux sens)
- ``close(fd)`` // libère le descripteur au niveau du processus
**Patron de fermeture conseillé (côté client ou serveur) :**
1. Terminer les envois : ``shutdown(fd, SHUT_WR)``
2. Lire jusqu'à **EOF** (``recv`` qui retourne 0) pour consommer les données restantes.
3. Appeler ``close(fd)`` pour libérer la ressource.
Exemple : fermeture TCP propre
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. code-block:: c
// fin des envois vers le pair
shutdown(fd, SHUT_WR); // signal FIN envoyé
// lire jusqu'à EOF pour s'assurer de recevoir la fin distante
char buf[4096];
for (;;) {
ssize_t n = recv(fd, buf, sizeof buf, 0);
if (n > 0) {
// traiter éventuellement les derniers octets
continue;
}
if (n == 0) {
// EOF : le pair a fermé sa moitié d'écriture
break;
}
if (errno == EINTR) continue; // interruption signal → reprendre
perror("recv");
break;
}
// libère le descripteur local
close(fd);
UDP : ``close`` suffit
~~~~~~~~~~~~~~~~~~~~~~
UDP n'a pas de connexion persistante ni de handshake de fermeture. Il suffit de fermer le descripteur :
.. code-block:: c
close(fd_udp); // libère la socket UDP
Sockets locales (AF_UNIX)
-------------------------
Pour la communication **locale** entre processus, ``AF_UNIX`` est très performant (pas de pile IP). Au lieu d'une IP/port, on utilise un **chemin de fichier**.
.. code-block:: c
:linenos:
#include
#include
#include
int fd = socket(AF_UNIX, SOCK_STREAM, 0);
struct sockaddr_un addr = {0};
addr.sun_family = AF_UNIX;
strncpy(addr.sun_path, "/tmp/mysock", sizeof addr.sun_path - 1);
bind(fd, (struct sockaddr*)&addr, sizeof addr);
Avec ``AF_UNIX``, il faut **fermer** puis **supprimer le fichier-socket** (côté serveur) :
.. code-block:: c
close(fd_unix); // ferme le descripteur
unlink("/tmp/mysocket"); // supprime le chemin de la socket (sinon fichier persiste)