Partie 6 - Sockets¶
Support présentationIntroduction¶
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.
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).
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.
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.
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
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’appelacceptcrée une nouvelle socket connectée dédiée à ce client.Le client crée aussi une socket (
socket) puis tente de se connecter au serveur viaconnecten précisant l’adresse IP et le port du serveur.Une fois la connexion établie, les deux côtés peuvent échanger des données avec
send/recv(ouwrite/read).Pour terminer, la connexion TCP est fermée proprement avec
shutdownetclose, ce qui libère les ressources.
À retenir
listenmet simplement la socket serveur en attente, mais ne crée pas la connexion.C’est
acceptqui génère une nouvelle socket connectée pour dialoguer avec un client, tandis que la socket d’écoute continue à attendre d’autres connexions.
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.
Le serveur crée une socket (
socket) de typeSOCK_DGRAMpuis l’associe à une adresse IP et un port avecbind. Il utilise ensuiterecvfrompour attendre la réception d’un datagramme, et peut répondre avecsendto.Le client crée aussi une socket (
socket), puis envoie un datagramme au serveur avecsendtoen indiquant explicitement l’adresse IP et le port de destination. Pour recevoir la réponse, il utiliserecvfrom.Chaque envoi ou réception est indépendant : il n’y a pas de notion de session ou de « connexion » comme en TCP.
Les sockets UDP se ferment simplement avec
close.
À retenir
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.
Création d’un socket¶
Prototype 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: souvent0(choix par défaut selontype)
Retourne -1 si erreur (errno contient le code d’erreur).
Exemple :
// 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).
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
sockfd: descripteur de socketaddr: pointeur vers la structure d’adresse (cast enstruct 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 <arpa/inet.h>.
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-<netinet/in.h>)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 :
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-<netinet/in.h>)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 :
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-<sys/un.h>)struct sockaddr_un { sa_family_t sun_family; // AF_UNIX char sun_path[108]; // chemin du fichier-socket };
Exemple :
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 :
int listen(int sockfd, int backlog);
sockfd: socket créée et bindéebacklog: 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 :
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.
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
sockfd: descripteur de la socket d’écouteaddr: 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 :
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 :
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
sockfd: descripteur de la socket du clientaddr: adresse du serveuraddrlen: taille de la structure
Retourne 0 si succès, -1 si erreur (errno contient le code d’erreur).
Exemple (IPv4 localhost:8080) :
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 :
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éebuf: tampon de donnéeslen: taille du tamponflags: options (souvent 0)
Retourne le nombre d’octets envoyés/reçus ou -1 si erreur.
Exemple (écho simple) :
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) :
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 UDPbuf: tampon de donnéeslen: taille du tamponflags: options (souvent 0)dest_addr/src_addr: adresse de destination/sourceaddrlen: taille de la structure d’adresse
Retourne le nombre d’octets envoyés/reçus ou -1 si erreur.
Exemple (ping UDP localhost:8080) :
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 plusshutdown(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) :
Terminer les envois :
shutdown(fd, SHUT_WR)Lire jusqu’à EOF (
recvqui retourne 0) pour consommer les données restantes.Appeler
close(fd)pour libérer la ressource.
Exemple : fermeture TCP propre¶
// 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 :
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.
1#include <sys/socket.h>
2#include <sys/un.h>
3#include <string.h>
4
5int fd = socket(AF_UNIX, SOCK_STREAM, 0);
6struct sockaddr_un addr = {0};
7addr.sun_family = AF_UNIX;
8strncpy(addr.sun_path, "/tmp/mysock", sizeof addr.sun_path - 1);
9bind(fd, (struct sockaddr*)&addr, sizeof addr);
Avec AF_UNIX, il faut fermer puis supprimer le fichier-socket (côté serveur) :
close(fd_unix); // ferme le descripteur
unlink("/tmp/mysocket"); // supprime le chemin de la socket (sinon fichier persiste)