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.
Pour ce cours, nous utiliserons le mode connecté avec TCP.
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 un socket (
socket), l’associe à une adresse IP et un numéro de port (bind), puis le place en écoute (listen). Quand un client se présente, l’appelacceptcrée un nouveau socket connecté dédié à ce client.Le client crée aussi un 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 le socket serveur en attente, mais ne crée pas la connexion.C’est
acceptqui génère un nouveau socket connecté pour dialoguer avec un client, tandis que le 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 un 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 un 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 :
// 1. connexion TCP en mode connecté
int fd = socket(AF_INET, SOCK_STREAM, 0);
if (fd == -1) {
perror("socket");
exit(EXIT_FAILURE);
}
// Option SO_REUSEADDR pour éviter "Address already in use", uniquement côté serveur
int opt = 1;
if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof opt) == -1) {
perror("setsockopt");
close(fd);
exit(EXIT_FAILURE);
}
Le côté client, comme le côté serveur, doit créer le socket qui retourne un File Descriptor.
Définition des interfaces avec bind¶
bind permet d’associer un socket à une adresse locale (IP + port ou chemin UNIX).
Le serveur doit l’appeler pour réserver l’adresse sur laquelle il écoute.
Le client n’a pas besoin de bind, car le noyau lui attribue automatiquement une adresse locale lors de connect ou sendto, toutefois, un client peut appeler bind lorsqu’il veut imposer une adresse ou un port source spécifique.
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
Exemple complet avec le bind et sockaddr_in :
#define PORT 8080
// 2. Configuration de l'adresse (écoute sur toutes les interfaces, port 8080)
struct sockaddr_in server_addr = {0};
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = htonl(INADDR_ANY); // 0.0.0.0
server_addr.sin_port = htons(PORT);
// 3. Bind
if (bind(server_fd, (struct sockaddr *)&server_addr, sizeof server_addr) == -1) {
perror("bind");
close(server_fd);
exit(EXIT_FAILURE);
}
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 :
#define BACKLOG 16
// 5. Listen
if (listen(fd, BACKLOG) == -1) {
perror("listen");
close(fd);
exit(EXIT_FAILURE);
}
Le serveur est maintenant prêt à accepter des clients.
Accepter des connexions avec accept¶
Un serveur TCP appelle accept pour accepter une connexion ce qui crée un nouveau socket connecté pour le client.
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
sockfd: descripteur du 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 du socket d’écoute si succès, -1 si erreur (errno contient le code d’erreur).
Exemple :
// 6. Acceptation d'un client (à faire dans une boucle pour en accepter plusieurs)
struct sockaddr_in client_addr;
socklen_t client_len = sizeof client_addr;
int client_fd = accept(fd, (struct sockaddr *)&client_addr, &client_len);
if (client_fd == -1) {
perror("accept");
}
// Gérer le client (dans un vrai serveur, utiliser fork/thread/epoll)
handle_client(client_fd, &client_addr);
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) :
// 2. Configuration de l'adresse du serveur
struct sockaddr_in server_addr = {0};
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(PORT);
// Conversion de l'adresse IP texte en binaire
if (inet_pton(AF_INET, SERVER_IP, &server_addr.sin_addr) <= 0) {
perror("inet_pton");
close(fd);
exit(EXIT_FAILURE);
}
// 3. Connexion au serveur
printf("[Client TCP] Connexion à %s:%d...\n", SERVER_IP, PORT);
if (connect(fd, (struct sockaddr *)&server_addr, sizeof server_addr) == -1) {
perror("connect");
close(fd);
exit(EXIT_FAILURE);
}
printf("[Client TCP] Connecté !\n");
Envoyer et recevoir des données avec send/recv¶
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).
send et recv peuvent envoyer/recevoir moins que la taille demandée, il est plus sûr de les utiliser dans une boucle pour garantir d’obtenir le message complet.
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) :
// envoyer
const char *msg = "Hello\n";
if (send(sock, msg, strlen(msg), 0) == -1) {
perror("send");
}
// recevoir
char buf[1024];
ssize_t n = recv(sock, buf, sizeof buf - 1, 0);
if (n > 0) {
buf[n] = 0;
printf("%s", buf);
}
Fermeture d’un socket¶
En fin de communication, il faut libérer proprement le 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¶
printf("\n[Client TCP] Fermeture de la connexion...\n");
shutdown(fd, SHUT_WR); // On n'envoie plus
// Consommer les données restantes jusqu'à EOF
ssize_t n;
while ((n = recv(fd, buffer, sizeof buffer, 0)) > 0) {
// On ignore les données restantes
}
close(fd);
printf("[Client TCP] Déconnecté.\n");
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); // ferme le descripteur
unlink("/tmp/mysocket"); // supprime le chemin de la socket (sinon fichier persiste)