🔝 Retour au Sommaire
Un socket fraîchement créé n'est qu'une coquille vide — une structure noyau avec des buffers et un protocole, mais sans adresse, sans port, sans rôle défini. C'est la séquence d'appels système qui suit la création qui donne au socket sa fonction : serveur qui écoute et accepte des connexions, ou client qui initie une connexion vers un serveur distant.
Cette section détaille les quatre opérations qui orchestrent l'établissement de la communication TCP, puis couvre les spécificités UDP. Chaque appel système est présenté avec sa signature, son rôle dans le cycle de vie, ses pièges courants et les bonnes pratiques C++ modernes associées.
Rappelons le schéma du cycle de vie TCP vu en section 22.1, en soulignant cette fois quel appel est exécuté par quel rôle :
SERVEUR CLIENT
┌──────────────────┐ ┌──────────────────┐
│ socket() │ │ socket() │
│ bind() ◄─────┤ Serveur seulement │ │
│ listen() ◄─────┤ │ │
│ accept() ◄─────┤ │ connect() ──────┤ Client seulement
│ recv/send │◄───────────────────►│ recv/send │
│ close() │ │ close() │
└──────────────────┘ └──────────────────┘
Le serveur exécute bind → listen → accept (dans cet ordre, obligatoirement). Le client n'a besoin que de connect. Les deux côtés utilisent ensuite les mêmes fonctions d'envoi/réception (section 22.1.3).
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr* addr, socklen_t addrlen);bind() associe le socket à une adresse locale : une combinaison d'adresse IP et de port. C'est ce qui dit au noyau « quand un paquet arrive sur cette adresse et ce port, livre-le à ce socket ».
Sans bind(), le socket n'a pas d'adresse locale. Le noyau ne sait pas à quel trafic le rattacher.
Le port est le paramètre critique. C'est lui qui identifie votre service. Un serveur HTTP binde sur le port 80 ou 443, un serveur gRPC sur le port 50051, votre application personnalisée sur le port de votre choix.
L'adresse IP contrôle sur quelles interfaces réseau le socket écoute :
| Adresse | Signification |
|---|---|
INADDR_ANY / in6addr_any |
Toutes les interfaces — c'est le choix par défaut pour les serveurs |
127.0.0.1 / ::1 |
Loopback uniquement — accessible seulement depuis la machine locale |
192.168.1.42 |
Une interface spécifique — le socket n'accepte le trafic que sur cette IP |
Plutôt que de remplir manuellement les structures sockaddr_in ou sockaddr_in6, utilisez le résultat de getaddrinfo (voir section 22.1.1) — c'est plus propre, portable et compatible IPv4/IPv6 :
// Résolution : nullptr comme host = INADDR_ANY / in6addr_any
auto addr = resolve(nullptr, "8080", AF_INET6, SOCK_STREAM);
Socket server{addr->ai_family, addr->ai_socktype, addr->ai_protocol};
server.enable_reuse_addr();
if (bind(server.fd(), addr->ai_addr, addr->ai_addrlen) == -1) {
throw std::system_error(errno, std::system_category(), "bind()");
}Pour les cas où vous avez besoin de contrôle fin ou pour comprendre ce qui se passe sous le capot :
sockaddr_in addr{};
addr.sin_family = AF_INET;
addr.sin_port = htons(8080); // N'oubliez JAMAIS htons()
addr.sin_addr.s_addr = INADDR_ANY; // Toutes les interfaces
if (bind(sockfd, reinterpret_cast<sockaddr*>(&addr), sizeof(addr)) == -1) {
throw std::system_error(errno, std::system_category(), "bind()");
}sockaddr_in6 addr{};
addr.sin6_family = AF_INET6;
addr.sin6_port = htons(8080);
addr.sin6_addr = in6addr_any; // Équivalent de INADDR_ANY pour IPv6
// Désactiver V6ONLY pour accepter aussi les connexions IPv4
int opt = 0;
setsockopt(sockfd, IPPROTO_IPV6, IPV6_V6ONLY, &opt, sizeof(opt));
if (bind(sockfd, reinterpret_cast<sockaddr*>(&addr), sizeof(addr)) == -1) {
throw std::system_error(errno, std::system_category(), "bind()");
}errno |
Signification | Cause et solution |
|---|---|---|
EADDRINUSE |
Adresse déjà utilisée | Un autre processus écoute sur ce port, ou le port est en TIME_WAIT. Solution : SO_REUSEADDR (voir section 22.1.1), ou choisir un autre port. |
EACCES |
Permission refusée | Tentative de bind sur un port < 1024 sans privilèges root (ou CAP_NET_BIND_SERVICE). |
EADDRNOTAVAIL |
Adresse non disponible | L'adresse IP spécifiée n'existe sur aucune interface de la machine. |
⚠️ Le piège des ports privilégiés — Les ports 1 à 1023 sont historiquement réservés et nécessitent les privilèges root. En production, ne lancez jamais votre serveur en root. Utilisez plutôt un port non-privilégié (≥ 1024) combiné à un reverse proxy (Nginx, HAProxy), ou attribuez la capabilityCAP_NET_BIND_SERVICEau binaire avecsetcap.
Pour un serveur UDP, bind() fonctionne exactement de la même manière. La différence est qu'il n'y aura pas de listen() ni accept() ensuite — le socket est immédiatement prêt à recevoir des datagrammes avec recvfrom().
Non, en général. Quand un client appelle connect() (TCP) ou sendto() (UDP) sans avoir fait de bind() préalable, le noyau attribue automatiquement un port éphémère et l'adresse de l'interface de sortie. C'est le comportement normal et souhaité.
Un client n'appelle bind() que dans des cas très spécifiques : forcer l'utilisation d'une interface réseau particulière sur une machine multi-homed, ou utiliser un port source fixe (rare, parfois nécessaire pour traverser certains firewalls).
#include <sys/socket.h>
int listen(int sockfd, int backlog);listen() transforme un socket bindé en un socket d'écoute (listening socket). C'est une transition d'état irréversible : après listen(), le socket ne pourra plus être utilisé pour envoyer ou recevoir des données lui-même. Son unique rôle sera d'attendre des connexions entrantes, que vous récupérerez avec accept().
Concrètement, listen() demande au noyau de commencer à accepter les demandes de connexion TCP (SYN) sur l'adresse et le port bindés, et de les placer dans une file d'attente en attendant que l'application appelle accept().
if (listen(server.fd(), SOMAXCONN) == -1) {
throw std::system_error(errno, std::system_category(), "listen()");
}
// Le socket est maintenant en écoute — les clients peuvent se connecterLe paramètre backlog contrôle la taille de la file d'attente des connexions en attente. Pour comprendre son rôle, il faut connaître le mécanisme interne du noyau.
Quand un client initie une connexion TCP, le three-way handshake se déroule entièrement dans le noyau, sans intervention de l'application :
Client Noyau serveur Application
────── ───────────── ───────────
── SYN ──────────────► File SYN (semi-ouvertes)
Le noyau répond SYN-ACK
◄── SYN-ACK ──────────
── ACK ──────────────► File ACCEPT (établies) ────────► accept()
Connexion prête
Le noyau Linux maintient en réalité deux files par socket d'écoute :
La SYN queue (file des connexions semi-ouvertes) — Contient les connexions pour lesquelles le noyau a reçu le SYN et envoyé le SYN-ACK, mais n'a pas encore reçu l'ACK final. La taille de cette file est contrôlée par tcp_max_syn_backlog (par défaut 128 ou 1024 selon les distributions).
L'accept queue (file des connexions établies) — Contient les connexions dont le handshake est terminé (ACK reçu), mais que l'application n'a pas encore récupérées avec accept(). C'est cette file dont la taille est contrôlée par le paramètre backlog de listen().
Quand la file accept est pleine, le comportement dépend de la configuration du noyau (tcp_abort_on_overflow). Par défaut, le noyau ignore silencieusement les ACK supplémentaires — le client pense que la connexion est établie, mais le serveur ne la voit pas. Le client finira par retransmettre et la connexion aboutira si de la place se libère, ou échouera par timeout.
SOMAXCONN — C'est la valeur maximale autorisée par le système. Sur les noyaux Linux récents, elle vaut 4096 (définie dans /proc/sys/net/core/somaxconn). C'est le choix recommandé pour les serveurs de production.
listen(server.fd(), SOMAXCONN); // RecommandéSi vous passez une valeur supérieure à SOMAXCONN, le noyau la tronque silencieusement à SOMAXCONN. Si vous passez une valeur trop faible, vous risquez de perdre des connexions en cas de pic de trafic.
En développement, une valeur plus petite (16, 32) peut suffire. Mais il n'y a aucun avantage à utiliser une petite valeur — SOMAXCONN ne consomme pas significativement plus de mémoire.
Pour un serveur haute performance, augmentez somaxconn :
# Valeur actuelle
cat /proc/sys/net/core/somaxconn
# Augmenter (temporaire)
sudo sysctl -w net.core.somaxconn=65535
# Augmenter la SYN queue aussi
sudo sysctl -w net.ipv4.tcp_max_syn_backlog=65535UDP n'a pas de notion de connexion, donc pas de listen(). Un socket UDP est prêt à recevoir des datagrammes dès qu'il est bindé.
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr* addr, socklen_t* addrlen);
// Version Linux avec flags (préférée)
int accept4(int sockfd, struct sockaddr* addr, socklen_t* addrlen, int flags);accept() extrait la première connexion de la file d'attente du socket d'écoute et crée un nouveau socket dédié à cette connexion. C'est un point fondamental : le socket d'écoute n'est jamais utilisé pour communiquer avec un client. Chaque appel à accept() retourne un nouveau file descriptor qui représente la connexion bidirectionnelle avec un client spécifique.
Socket d'écoute (fd=3) Sockets de connexion
port 8080 (un par client)
┌─────────────┐
│ listen() │──── accept() ────► fd=4 ◄──► Client A (192.168.1.10:49152)
│ │──── accept() ────► fd=5 ◄──► Client B (10.0.0.5:52301)
│ │──── accept() ────► fd=6 ◄──► Client C (172.16.0.8:38777)
│ port 8080 │
└─────────────┘
Le socket d'écoute (fd=3) reste ouvert et continue d'accepter de nouvelles connexions. Chaque socket client (fd=4, 5, 6) est indépendant et sera utilisé pour send() / recv() avec son client respectif.
Mode bloquant (défaut) — Si la file d'attente est vide (aucun client en attente), accept() bloque le thread appelant jusqu'à ce qu'une connexion arrive. C'est le comportement naturel pour un serveur simple mono-thread.
Mode non-bloquant — Si le socket d'écoute a été créé avec SOCK_NONBLOCK ou configuré avec fcntl(), accept() retourne immédiatement -1 avec errno = EAGAIN quand il n'y a aucune connexion en attente. C'est le mode utilisé avec les mécanismes de multiplexage (epoll, section 22.3).
sockaddr_storage client_addr{};
socklen_t addr_len = sizeof(client_addr);
int client_fd = accept4(
server.fd(),
reinterpret_cast<sockaddr*>(&client_addr),
&addr_len,
SOCK_CLOEXEC // Atomique : pas de race condition
);
if (client_fd == -1) {
if (errno == EAGAIN || errno == EWOULDBLOCK) {
// Socket non-bloquant, pas de connexion en attente — normal
} else {
throw std::system_error(errno, std::system_category(), "accept4()");
}
}
// Encapsuler immédiatement en RAII
Socket client{client_fd};accept() remplit la structure sockaddr avec l'adresse du client qui s'est connecté. Pour l'exploiter (logging, contrôle d'accès, diagnostics) :
#include <arpa/inet.h>
void log_client_info(const sockaddr_storage& addr) {
char ip_str[INET6_ADDRSTRLEN];
uint16_t port;
if (addr.ss_family == AF_INET) {
auto* v4 = reinterpret_cast<const sockaddr_in*>(&addr);
inet_ntop(AF_INET, &v4->sin_addr, ip_str, sizeof(ip_str));
port = ntohs(v4->sin_port);
} else { // AF_INET6
auto* v6 = reinterpret_cast<const sockaddr_in6*>(&addr);
inet_ntop(AF_INET6, &v6->sin6_addr, ip_str, sizeof(ip_str));
port = ntohs(v6->sin6_port);
}
std::println("Connexion depuis {}:{}", ip_str, port);
}inet_ntop convertit une adresse binaire en chaîne lisible (le p signifie « presentation », le n signifie « network »). C'est la fonction inverse d'inet_pton. Utilisez INET6_ADDRSTRLEN (46 octets) comme taille de buffer — c'est suffisant pour les deux familles.
Un serveur ne fait pas qu'un seul accept(). Il tourne en boucle, acceptant les connexions une par une (ou en les dispatchant à des threads/tâches) :
while (running) {
sockaddr_storage client_addr{};
socklen_t addr_len = sizeof(client_addr);
int client_fd = accept4(
server.fd(),
reinterpret_cast<sockaddr*>(&client_addr),
&addr_len,
SOCK_CLOEXEC
);
if (client_fd == -1) {
if (errno == EINTR) {
continue; // Interrompu par un signal — réessayer
}
throw std::system_error(errno, std::system_category(), "accept4()");
}
Socket client{client_fd};
log_client_info(client_addr);
// Traiter la connexion :
// - Directement (serveur séquentiel, simple mais lent)
// - Dans un nouveau thread (serveur multi-threadé)
// - Via epoll (serveur événementiel, section 22.3)
handle_client(std::move(client));
}💡
EINTR— Quand un signal est délivré au processus pendant un appel bloquant, l'appel système peut être interrompu et retourner-1avecerrno = EINTR. Ce n'est pas une erreur : il suffit de relancer l'appel. Vous verrez ce pattern dans tout code réseau POSIX.
errno |
Signification | Notes |
|---|---|---|
EAGAIN / EWOULDBLOCK |
Pas de connexion en attente | Normal en mode non-bloquant |
EINTR |
Interrompu par un signal | Relancer l'appel |
EMFILE |
Limite de fd du processus atteinte | Augmenter ulimit -n |
ENFILE |
Limite de fd système atteinte | Augmenter fs.file-max |
ECONNABORTED |
Connexion avortée par le client | Le client a fermé avant la fin du handshake — ignorer et continuer |
Comme listen(), accept() n'a pas de sens en UDP. Un socket UDP n'a pas de notion de connexion individuelle — tous les datagrammes arrivent sur le même socket.
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr* addr, socklen_t addrlen);connect() est l'opération symétrique d'accept() — c'est le client qui l'appelle pour initier une connexion vers un serveur. Pour TCP, connect() déclenche le three-way handshake. L'appel bloque (par défaut) jusqu'à ce que la connexion soit établie ou qu'une erreur survienne.
La manière idiomatique de se connecter est d'itérer sur les résultats de getaddrinfo jusqu'à trouver une adresse qui fonctionne. C'est important pour la robustesse : un nom DNS peut résoudre vers plusieurs adresses (IPv4 et IPv6, plusieurs serveurs), et certaines peuvent être injoignables :
addrinfo hints{};
hints.ai_family = AF_UNSPEC; // IPv4 ou IPv6
hints.ai_socktype = SOCK_STREAM; // TCP
addrinfo* result = nullptr;
int status = getaddrinfo("api.example.com", "443", &hints, &result);
if (status != 0) {
throw std::runtime_error(
std::string("getaddrinfo: ") + gai_strerror(status)
);
}
AddrInfoPtr addr_list{result}; // RAII (voir section 22.1.1)
// Parcourir les résultats et tenter chaque adresse
int sockfd = -1;
for (auto* rp = addr_list.get(); rp != nullptr; rp = rp->ai_next) {
sockfd = socket(rp->ai_family,
rp->ai_socktype | SOCK_CLOEXEC,
rp->ai_protocol);
if (sockfd == -1) {
continue; // Cette combinaison n'est pas supportée — essayer la suivante
}
if (connect(sockfd, rp->ai_addr, rp->ai_addrlen) == 0) {
break; // Connexion réussie
}
// Échec de connect — fermer et essayer l'adresse suivante
close(sockfd);
sockfd = -1;
}
if (sockfd == -1) {
throw std::runtime_error("Impossible de se connecter à api.example.com:443");
}
Socket connection{sockfd}; // Encapsuler en RAII
// La connexion est établie — prêt pour send()/recv()Ce pattern d'itération est fondamental. Le RFC 6555 (« Happy Eyeballs ») va même plus loin en recommandant de tenter les connexions IPv6 et IPv4 en parallèle pour minimiser la latence perçue. Les librairies comme Asio implémentent ce mécanisme automatiquement.
En mode non-bloquant, connect() retourne immédiatement -1 avec errno = EINPROGRESS. La connexion se poursuit en arrière-plan, et vous devez utiliser poll() ou epoll pour détecter quand elle aboutit :
// Socket non-bloquant
Socket sock{AF_INET6, SOCK_STREAM | SOCK_NONBLOCK};
int ret = connect(sock.fd(), addr->ai_addr, addr->ai_addrlen);
if (ret == -1 && errno != EINPROGRESS) {
throw std::system_error(errno, std::system_category(), "connect()");
}
if (ret == -1) {
// Connexion en cours — attendre avec poll()
pollfd pfd{};
pfd.fd = sock.fd();
pfd.events = POLLOUT; // Écriture possible = connexion établie
int poll_ret = poll(&pfd, 1, 5000); // Timeout 5 secondes
if (poll_ret == 0) {
throw std::runtime_error("connect() timeout");
}
if (poll_ret == -1) {
throw std::system_error(errno, std::system_category(), "poll()");
}
// Vérifier le résultat réel de la connexion
int error = 0;
socklen_t len = sizeof(error);
getsockopt(sock.fd(), SOL_SOCKET, SO_ERROR, &error, &len);
if (error != 0) {
throw std::system_error(error, std::system_category(), "connect()");
}
}
// Connexion établieL'étape getsockopt(SO_ERROR) est cruciale et souvent oubliée. Le fait que poll() signale le socket comme prêt en écriture ne signifie pas que la connexion a réussi — cela signifie seulement que le résultat est disponible. L'erreur réelle (refus de connexion, hôte injoignable, etc.) est stockée dans SO_ERROR.
En UDP, connect() a un sens différent. Il n'établit pas de connexion (il n'y a pas de handshake), mais il associe une adresse de destination au socket. Les conséquences sont :
- Vous pouvez utiliser
send()etrecv()au lieu desendto()etrecvfrom()— plus simple. - Le noyau filtre les datagrammes entrants : seuls ceux provenant de l'adresse connectée sont délivrés.
- Les erreurs ICMP (destination injoignable, etc.) sont reportées à l'application — ce qui n'est pas le cas sans
connect().
// UDP "connecté" — pratique pour un client qui parle à un seul serveur
Socket udp_client{AF_INET6, SOCK_DGRAM};
connect(udp_client.fd(), server_addr->ai_addr, server_addr->ai_addrlen);
// Désormais, send/recv au lieu de sendto/recvfrom
send(udp_client.fd(), data, len, 0);
recv(udp_client.fd(), buffer, sizeof(buffer), 0); errno |
Signification | Diagnostic |
|---|---|---|
ECONNREFUSED |
Connexion refusée | Le serveur n'écoute pas sur ce port. Vérifier que le serveur tourne et le port est correct. |
ETIMEDOUT |
Timeout | Le serveur est injoignable (firewall, réseau coupé). Vérifier la connectivité réseau. |
ENETUNREACH |
Réseau injoignable | Pas de route vers le réseau de destination. Vérifier la table de routage. |
EHOSTUNREACH |
Hôte injoignable | L'hôte spécifique ne répond pas. |
EINPROGRESS |
Connexion en cours | Socket non-bloquant — la connexion est en cours, pas une erreur. |
EALREADY |
Connexion déjà en cours | Un connect() non-bloquant précédent n'est pas encore terminé. |
EISCONN |
Socket déjà connecté | Le socket est déjà connecté — vous ne pouvez pas appeler connect() deux fois sur un socket TCP. |
Le timeout par défaut de connect() est contrôlé par le noyau et peut atteindre 2 minutes (plusieurs retransmissions du SYN avec backoff exponentiel). C'est beaucoup trop long pour la plupart des applications.
Deux approches pour imposer un timeout plus court :
Approche 1 : Socket non-bloquant + poll — C'est la méthode montrée ci-dessus. Vous contrôlez le timeout via le paramètre de poll().
Approche 2 : SO_SNDTIMEO — Positionner un timeout d'envoi sur le socket avant connect(). Moins propre que l'approche non-bloquante, mais plus simple :
timeval tv{};
tv.tv_sec = 5; // 5 secondes
tv.tv_usec = 0;
setsockopt(sockfd, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv));
// connect() retournera EINPROGRESS ou ETIMEDOUT après 5 secondesVoici comment les quatre opérations s'enchaînent dans un serveur TCP complet et minimal. Ce code utilise le wrapper RAII Socket et la fonction resolve() de la section 22.1.1 :
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
#include <poll.h>
#include <unistd.h>
#include <print>
// (Socket, AddrInfoPtr et resolve() définis en section 22.1.1)
void handle_client(Socket client) {
// Sera détaillé en section 22.1.3
char buffer[4096];
ssize_t n = recv(client.fd(), buffer, sizeof(buffer), 0);
if (n > 0) {
send(client.fd(), buffer, n, 0); // Echo
}
// client est fermé automatiquement (RAII)
}
int main() {
// 1. Résoudre l'adresse
auto addr = resolve(nullptr, "8080", AF_INET6, SOCK_STREAM);
// 2. Créer le socket
Socket server{addr->ai_family, addr->ai_socktype, addr->ai_protocol};
// 3. Configurer les options
server.enable_reuse_addr();
// 4. Binder sur le port
if (bind(server.fd(), addr->ai_addr, addr->ai_addrlen) == -1) {
throw std::system_error(errno, std::system_category(), "bind()");
}
// 5. Passer en mode écoute
if (listen(server.fd(), SOMAXCONN) == -1) {
throw std::system_error(errno, std::system_category(), "listen()");
}
std::println("Serveur en écoute sur le port 8080...");
// 6. Boucle d'accept
while (true) {
sockaddr_storage client_addr{};
socklen_t addr_len = sizeof(client_addr);
int client_fd = accept4(
server.fd(),
reinterpret_cast<sockaddr*>(&client_addr),
&addr_len,
SOCK_CLOEXEC
);
if (client_fd == -1) {
if (errno == EINTR) continue;
throw std::system_error(errno, std::system_category(), "accept4()");
}
// 7. Traiter le client (ici de manière séquentielle)
handle_client(Socket{client_fd});
}
}Et le client correspondant :
#include <sys/socket.h>
#include <unistd.h>
#include <cstring>
#include <print>
int main() {
// 1. Résoudre l'adresse du serveur
auto addr = resolve("localhost", "8080", AF_UNSPEC, SOCK_STREAM);
// 2. Itérer sur les résultats et se connecter
int sockfd = -1;
for (auto* rp = addr.get(); rp != nullptr; rp = rp->ai_next) {
sockfd = socket(rp->ai_family,
rp->ai_socktype | SOCK_CLOEXEC,
rp->ai_protocol);
if (sockfd == -1) continue;
if (connect(sockfd, rp->ai_addr, rp->ai_addrlen) == 0) {
break;
}
close(sockfd);
sockfd = -1;
}
if (sockfd == -1) {
std::println(stderr, "Impossible de se connecter");
return 1;
}
Socket connection{sockfd};
// 3. Envoyer et recevoir
const char* msg = "Hello, server!";
send(connection.fd(), msg, strlen(msg), 0);
char buffer[4096];
ssize_t n = recv(connection.fd(), buffer, sizeof(buffer) - 1, 0);
if (n > 0) {
buffer[n] = '\0';
std::println("Réponse du serveur : {}", buffer);
}
// connection est fermé automatiquement (RAII)
}
⚠️ Ce serveur est séquentiel : il ne traite qu'un client à la fois. Pendant qu'il gère un client, tous les autres attendent dans la file du backlog. C'est acceptable pour un prototype ou un outil interne, mais inadapté pour la production. La section 22.2 construit un serveur plus réaliste, et la section 22.3 introduitepollpour gérer des milliers de connexions simultanées.
La fermeture d'un socket se fait avec le même close() que pour un fichier :
close(sockfd);Avec le wrapper RAII, vous n'appelez jamais close() manuellement — le destructeur s'en charge. Mais il est utile de comprendre ce qui se passe en interne.
Pour un socket TCP, close() déclenche la séquence de terminaison TCP (FIN → ACK). Si des données restent dans le buffer d'envoi, le comportement dépend de l'option SO_LINGER :
- Par défaut (
SO_LINGERdésactivé) —close()retourne immédiatement. Le noyau continue d'envoyer les données restantes en arrière-plan. SO_LINGERactivé avec timeout > 0 —close()bloque jusqu'à ce que toutes les données soient envoyées, ou que le timeout expire.SO_LINGERactivé avec timeout = 0 — Le noyau envoie un RST (reset) immédiat. Toutes les données non envoyées sont perdues. Utile pour les fermetures brutales volontaires.
shutdown() offre un contrôle plus fin que close() en permettant de fermer un seul sens de la communication :
#include <sys/socket.h>
int shutdown(int sockfd, int how);how |
Effet |
|---|---|
SHUT_RD |
Ferme la lecture — les données entrantes suivantes seront ignorées |
SHUT_WR |
Ferme l'écriture — envoie un FIN au pair. Le pair verra EOF sur son recv() |
SHUT_RDWR |
Ferme les deux sens — équivalent à close() pour la connexion, mais ne libère pas le fd |
Le cas d'usage le plus courant est SHUT_WR : « j'ai fini d'envoyer, mais je veux encore recevoir la réponse du serveur ». C'est le mécanisme propre pour signaler la fin d'un flux de données au pair :
// Client : j'ai fini d'envoyer
shutdown(sock.fd(), SHUT_WR);
// Je peux encore recevoir la réponse
char buffer[4096];
ssize_t n = recv(sock.fd(), buffer, sizeof(buffer), 0); La différence clé entre shutdown(SHUT_RDWR) et close() : shutdown agit sur la connexion (partagée entre descripteurs dupliqués), tandis que close agit sur le file descriptor (une seule référence). Si un socket a été dup()é ou transmis via fork(), close() d'un côté ne ferme pas la connexion tant que l'autre fd reste ouvert. shutdown() ferme la connexion pour tous les descripteurs qui la partagent.
Les quatre opérations bind, listen, accept et connect forment le protocole d'établissement de toute communication TCP sous Linux. Voici les points essentiels à retenir :
bind()associe une adresse locale au socket. Utilisezgetaddrinfoplutôt qu'un remplissage manuel. Le client n'a généralement pas besoin de binder.listen()active le mode écoute et crée les files d'attente du noyau. PassezSOMAXCONNcomme backlog en production, et ajustezsomaxconnau niveau système si nécessaire.accept()/accept4()extrait une connexion établie et retourne un nouveau socket dédié. Utilisezaccept4avecSOCK_CLOEXECpour éviter les race conditions. GérezEINTRdans la boucle d'accept.connect()initie la connexion côté client. Itérez sur les résultats degetaddrinfopour la robustesse. En mode non-bloquant, vérifiezSO_ERRORaprès quepollsignale le socket prêt.shutdown()offre un contrôle fin sur la fermeture partielle, complémentaire àclose().
Prochaine étape → Section 22.1.3 : send, recv, sendto, recvfrom — l'échange de données proprement dit, avec la gestion des envois partiels, des signaux et des subtilités TCP vs UDP.