🔝 Retour au Sommaire
Toute communication réseau commence par un seul appel système :
#include <sys/socket.h>
int socket(int domain, int type, int protocol);Cet appel demande au noyau d'allouer les ressources internes nécessaires (buffers, structures d'état, entrée dans la table des file descriptors) et retourne un file descriptor — un entier positif qui sera votre point d'accès à ce socket pour toutes les opérations suivantes. En cas d'échec, la fonction retourne -1 et positionne errno.
Les trois paramètres déterminent la nature exacte du socket créé.
Le domaine spécifie la famille d'adresses que le socket utilisera. Comme vu en section 22.1, les trois valeurs courantes sont :
| Constante | Famille | Usage |
|---|---|---|
AF_INET |
IPv4 | Adresses 32 bits (192.168.1.1) |
AF_INET6 |
IPv6 | Adresses 128 bits (::1, 2001:db8::1) |
AF_UNIX |
Local | Communication inter-processus via le filesystem |
Pour du code réseau moderne destiné à fonctionner en production, privilégiez AF_INET6 avec le mode dual-stack activé (c'est le comportement par défaut sur Linux). Un socket IPv6 dual-stack peut accepter des connexions IPv4 et IPv6, ce qui simplifie considérablement le code.
Si vous utilisez getaddrinfo pour résoudre les adresses (ce qui est recommandé), le domaine sera déterminé automatiquement par le résultat de la résolution — vous n'aurez même pas à le choisir explicitement.
Le type détermine la sémantique de communication du socket :
| Constante | Protocole typique | Comportement |
|---|---|---|
SOCK_STREAM |
TCP | Flux d'octets fiable, ordonné, orienté connexion |
SOCK_DGRAM |
UDP | Datagrammes indépendants, sans connexion, non fiable |
SOCK_RAW |
IP brut | Accès direct au protocole réseau (requiert CAP_NET_RAW) |
SOCK_RAW est réservé aux cas très spécifiques : implémentation de protocoles personnalisés, outils de diagnostic comme ping ou traceroute, ou analyse de paquets. Vous n'en aurez pas besoin dans 99 % des situations.
Depuis Linux 2.6.27, le paramètre type peut être combiné par OR binaire avec deux flags utiles :
SOCK_NONBLOCK — Crée le socket en mode non-bloquant dès sa création. Sans ce flag, le socket est bloquant par défaut : un appel à recv() sur un socket sans données disponibles bloquera le thread appelant indéfiniment. En mode non-bloquant, le même appel retourne immédiatement avec errno positionné à EAGAIN ou EWOULDBLOCK.
// Équivalent à socket() suivi de fcntl(fd, F_SETFL, O_NONBLOCK)
int fd = socket(AF_INET6, SOCK_STREAM | SOCK_NONBLOCK, 0);SOCK_CLOEXEC — Positionne le flag close-on-exec sur le file descriptor. Cela garantit que le socket sera automatiquement fermé si le processus appelle exec() pour lancer un autre programme. Sans ce flag, un processus enfant créé par fork() + exec() hériterait du socket ouvert — une fuite de file descriptor silencieuse et un risque de sécurité.
// Bonne pratique : toujours utiliser SOCK_CLOEXEC
int fd = socket(AF_INET6, SOCK_STREAM | SOCK_CLOEXEC, 0);En C++ moderne, utilisez systématiquement SOCK_CLOEXEC. La combinaison des deux flags est courante pour les sockets non-bloquants :
int fd = socket(AF_INET6, SOCK_STREAM | SOCK_NONBLOCK | SOCK_CLOEXEC, 0);💡 Ces flags existent pour résoudre des race conditions entre
socket()etfcntl()/ioctl(). Dans un programme multi-threadé, un autre thread pourrait appelerfork()entre la création du socket et le positionnement du flag — le socket fuiterait dans le processus enfant. Les flags atomiques éliminent ce problème.
Le troisième paramètre spécifie le protocole exact à utiliser. En pratique, la combinaison domaine + type détermine presque toujours le protocole de manière univoque :
AF_INET+SOCK_STREAM→ TCP (protocole 6)AF_INET+SOCK_DGRAM→ UDP (protocole 17)
Passer 0 demande au système de choisir le protocole par défaut pour la combinaison domaine/type donnée — c'est le bon choix dans la quasi-totalité des cas. Les constantes IPPROTO_TCP et IPPROTO_UDP existent si vous avez besoin d'être explicite, mais 0 est parfaitement idiomatique.
Voici les combinaisons les plus courantes :
#include <sys/socket.h>
// TCP IPv4
int tcp4 = socket(AF_INET, SOCK_STREAM | SOCK_CLOEXEC, 0);
// TCP IPv6 (dual-stack : accepte aussi IPv4)
int tcp6 = socket(AF_INET6, SOCK_STREAM | SOCK_CLOEXEC, 0);
// UDP IPv4
int udp4 = socket(AF_INET, SOCK_DGRAM | SOCK_CLOEXEC, 0);
// UDP IPv6
int udp6 = socket(AF_INET6, SOCK_DGRAM | SOCK_CLOEXEC, 0);
// TCP IPv6 non-bloquant
int tcp6_nb = socket(AF_INET6, SOCK_STREAM | SOCK_NONBLOCK | SOCK_CLOEXEC, 0);socket() peut échouer. Les causes les plus fréquentes sont :
errno |
Signification | Cause typique |
|---|---|---|
EACCES |
Permission refusée | Tentative de créer un SOCK_RAW sans privilèges |
EMFILE |
Trop de fichiers ouverts (processus) | Limite par processus atteinte (ulimit -n) |
ENFILE |
Trop de fichiers ouverts (système) | Limite système atteinte (/proc/sys/fs/file-max) |
ENOBUFS / ENOMEM |
Mémoire insuffisante | Ressources noyau épuisées |
EPROTONOSUPPORT |
Protocole non supporté | Combinaison domaine/type/protocole invalide |
La vérification est obligatoire :
int sockfd = socket(AF_INET6, SOCK_STREAM | SOCK_CLOEXEC, 0);
if (sockfd == -1) {
// En C++ moderne, on préfère lancer une exception ou retourner std::expected
throw std::system_error(errno, std::system_category(), "socket()");
}L'utilisation de std::system_error est idiomatique en C++ pour les erreurs système : elle encapsule le code d'erreur (errno), la catégorie (système) et un message descriptif dans une seule exception. Nous reviendrons sur les stratégies alternatives (comme std::expected) dans la section sur la gestion d'erreurs réseau.
Un socket fraîchement créé utilise les options par défaut du système. Avant de l'utiliser, vous aurez souvent besoin de modifier certaines options avec setsockopt() :
#include <sys/socket.h>
int setsockopt(int sockfd, int level, int optname,
const void* optval, socklen_t optlen);Le paramètre level indique à quelle couche protocolaire l'option s'applique (SOL_SOCKET pour les options génériques, IPPROTO_TCP pour les options TCP, etc.), et optname identifie l'option spécifique.
Quand un serveur TCP se ferme, le port qu'il utilisait entre dans un état TIME_WAIT pendant quelques minutes (typiquement 60 secondes sur Linux). Si vous relancez immédiatement le serveur, bind() échouera avec EADDRINUSE — « Address already in use ». C'est un comportement normal de TCP, conçu pour éviter que des paquets retardataires de l'ancienne connexion soient confondus avec ceux d'une nouvelle.
SO_REUSEADDR permet de réutiliser l'adresse malgré cet état :
int opt = 1;
if (setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)) == -1) {
throw std::system_error(errno, std::system_category(), "setsockopt(SO_REUSEADDR)");
}Activez SO_REUSEADDR sur tous vos sockets serveur, sans exception. Ne pas le faire est une source de frustration récurrente en développement (le serveur refuse de redémarrer après un crash ou un arrêt propre) et un problème en production (redémarrage de service bloqué pendant le TIME_WAIT).
Disponible depuis Linux 3.9, SO_REUSEPORT permet à plusieurs sockets (dans des processus ou threads différents) de se bind sur le même couple adresse:port. Le noyau distribue alors les connexions entrantes entre les sockets de manière équitable.
int opt = 1;
setsockopt(sockfd, SOL_SOCKET, SO_REUSEPORT, &opt, sizeof(opt)); C'est le mécanisme utilisé par Nginx en mode multi-worker pour distribuer les connexions sans goulot d'étranglement sur un seul processus accept(). Cela permet d'éviter le « thundering herd problem » où tous les workers se réveillent pour une seule connexion entrante.
Par défaut, TCP utilise l'algorithme de Nagle qui agrège les petits paquets pour réduire l'overhead réseau. C'est bénéfique pour les transferts de gros volumes, mais catastrophique pour les protocoles interactifs ou le RPC où la latence de chaque message compte.
int opt = 1;
setsockopt(sockfd, IPPROTO_TCP, TCP_NODELAY, &opt, sizeof(opt)); Activez TCP_NODELAY quand vous avez besoin de latence minimale : communication requête/réponse, protocoles interactifs, streaming de données en temps réel. Laissez Nagle activé pour les transferts bulk où le throughput prime.
TCP ne détecte pas naturellement une connexion morte (par exemple, un câble débranché ou un processus distant tué par kill -9). Sans mécanisme supplémentaire, un recv() bloquant attendrait indéfiniment.
SO_KEEPALIVE demande au noyau d'envoyer périodiquement des « probes » sur les connexions inactives pour vérifier qu'elles sont toujours vivantes :
int opt = 1;
setsockopt(sockfd, SOL_SOCKET, SO_KEEPALIVE, &opt, sizeof(opt));
// Paramètres fins (spécifiques Linux)
int idle = 60; // Délai avant le premier probe (secondes)
int interval = 10; // Intervalle entre les probes
int count = 5; // Nombre de probes avant abandon
setsockopt(sockfd, IPPROTO_TCP, TCP_KEEPIDLE, &idle, sizeof(idle));
setsockopt(sockfd, IPPROTO_TCP, TCP_KEEPINTVL, &interval, sizeof(interval));
setsockopt(sockfd, IPPROTO_TCP, TCP_KEEPCNT, &count, sizeof(count)); Avec ces paramètres, le noyau enverra un probe après 60 secondes d'inactivité, puis un probe toutes les 10 secondes. Si 5 probes consécutifs restent sans réponse, la connexion est déclarée morte et le prochain recv() retournera une erreur.
Par défaut sur Linux, un socket AF_INET6 accepte à la fois les connexions IPv4 et IPv6 (mode dual-stack). Pour restreindre un socket à IPv6 uniquement :
int opt = 1;
setsockopt(sockfd, IPPROTO_IPV6, IPV6_V6ONLY, &opt, sizeof(opt)); À l'inverse, pour s'assurer explicitement que le dual-stack est actif (utile pour la portabilité, car le défaut varie selon les OS) :
int opt = 0;
setsockopt(sockfd, IPPROTO_IPV6, IPV6_V6ONLY, &opt, sizeof(opt)); Option Niveau Quand l'utiliser
────────────── ────────── ────────────────────────────────────────
SO_REUSEADDR SOL_SOCKET Toujours, sur les sockets serveur
SO_REUSEPORT SOL_SOCKET Multi-worker (Nginx-style)
TCP_NODELAY IPPROTO_TCP Protocoles interactifs, faible latence
SO_KEEPALIVE SOL_SOCKET Connexions longue durée
IPV6_V6ONLY IPPROTO_IPV6 Contrôle dual-stack explicite
SO_RCVBUF SOL_SOCKET Ajuster la taille du buffer de réception
SO_SNDBUF SOL_SOCKET Ajuster la taille du buffer d'envoi
SO_LINGER SOL_SOCKET Contrôler le comportement de close()
Pour interroger la valeur actuelle d'une option :
int opt;
socklen_t opt_len = sizeof(opt);
if (getsockopt(sockfd, SOL_SOCKET, SO_RCVBUF, &opt, &opt_len) == 0) {
// Note : Linux retourne le double de la valeur réelle (overhead noyau inclus)
std::println("Buffer de réception : {} octets", opt);
}C'est utile pour le diagnostic et le monitoring, ou pour vérifier que le système a bien appliqué une option que vous avez demandée (le noyau peut ajuster les valeurs de buffers aux limites système).
Un socket est une ressource système : il occupe un file descriptor et des buffers noyau. L'oublier, c'est créer une fuite de file descriptors — un problème qui ne se manifeste qu'en charge, quand le processus atteint sa limite (EMFILE), et qui est notoirement difficile à diagnostiquer après coup.
La solution en C++ est le pattern RAII : le constructeur acquiert la ressource, le destructeur la libère. Voici un wrapper minimal mais fonctionnel :
#include <sys/socket.h>
#include <unistd.h>
#include <utility> // std::exchange
#include <cerrno>
#include <system_error>
class Socket {
public:
// Constructeur : crée le socket
Socket(int domain, int type, int protocol = 0)
: fd_{socket(domain, type | SOCK_CLOEXEC, protocol)}
{
if (fd_ == -1) {
throw std::system_error(errno, std::system_category(), "socket()");
}
}
// Constructeur depuis un fd existant (retour de accept() par exemple)
explicit Socket(int fd) noexcept : fd_{fd} {}
// Destructeur : ferme le socket
~Socket() {
if (fd_ != -1) {
close(fd_); // close() ne lance pas d'exception
}
}
// Non copiable — un socket ne peut pas être dupliqué implicitement
Socket(const Socket&) = delete;
Socket& operator=(const Socket&) = delete;
// Déplaçable — transfert de propriété
Socket(Socket&& other) noexcept
: fd_{std::exchange(other.fd_, -1)} {}
Socket& operator=(Socket&& other) noexcept {
if (this != &other) {
if (fd_ != -1) {
close(fd_);
}
fd_ = std::exchange(other.fd_, -1);
}
return *this;
}
// Accès au file descriptor brut
[[nodiscard]] int fd() const noexcept { return fd_; }
// Conversion implicite pour les appels POSIX
[[nodiscard]] int get() const noexcept { return fd_; }
// Vérification de validité
[[nodiscard]] bool valid() const noexcept { return fd_ != -1; }
// Libération du fd sans fermeture (pour transfert à une autre API)
int release() noexcept { return std::exchange(fd_, -1); }
// Helpers pour setsockopt
void set_option(int level, int optname, int value) {
if (setsockopt(fd_, level, optname, &value, sizeof(value)) == -1) {
throw std::system_error(errno, std::system_category(), "setsockopt()");
}
}
void enable_reuse_addr() {
set_option(SOL_SOCKET, SO_REUSEADDR, 1);
}
void enable_tcp_nodelay() {
set_option(IPPROTO_TCP, TCP_NODELAY, 1);
}
private:
int fd_ = -1;
};Non copiable, déplaçable — Un socket est une ressource unique, exactement comme std::unique_ptr ou std::fstream. Copier un file descriptor sans appeler dup() créerait deux objets tentant de fermer le même fd — un double-free classique. Le constructeur de copie et l'opérateur de copie sont donc supprimés. En revanche, le déplacement transfère la propriété proprement, en utilisant std::exchange pour invalider le fd source.
SOCK_CLOEXEC automatique — Le constructeur ajoute SOCK_CLOEXEC au type demandé. C'est une décision de sécurité : par défaut, le socket ne fuira pas vers les processus enfants.
[[nodiscard]] — Les accesseurs sont marqués nodiscard car ignorer la valeur de retour de fd() ou valid() est presque certainement un bug.
release() — Parfois, vous devez transférer la propriété du fd à une API tierce (Asio, par exemple). release() retourne le fd et invalide le wrapper, exactement comme std::unique_ptr::release().
#include <netinet/in.h>
#include <netinet/tcp.h>
// Création d'un socket serveur TCP IPv6
Socket server_sock{AF_INET6, SOCK_STREAM};
server_sock.enable_reuse_addr();
server_sock.enable_tcp_nodelay();
// Le socket sera automatiquement fermé quand server_sock sort du scope,
// même en cas d'exception entre la création et la fin du bloc.
// Avec un socket non-bloquant :
Socket async_sock{AF_INET6, SOCK_STREAM | SOCK_NONBLOCK};Un usage courant est d'encapsuler le résultat d'accept() dans un Socket :
// accept() retourne un nouveau fd pour le client
int client_fd = accept4(server_sock.fd(),
reinterpret_cast<sockaddr*>(&client_addr),
&addr_len,
SOCK_CLOEXEC);
if (client_fd == -1) {
// Gérer l'erreur...
}
// Encapsuler immédiatement dans le wrapper RAII
Socket client_sock{client_fd};
// À partir d'ici, la fermeture est automatique💡 Notez l'utilisation de
accept4()au lieu deaccept(). Disponible depuis Linux 2.6.28,accept4accepte un paramètre de flags (SOCK_CLOEXEC,SOCK_NONBLOCK) pour configurer le nouveau socket de manière atomique — exactement comme les flags desocket().
Tant qu'on parle de RAII, voici comment encapsuler proprement le résultat de getaddrinfo que nous avons vu en section 22.1 :
#include <netdb.h>
#include <memory>
struct AddrInfoDeleter {
void operator()(addrinfo* p) const noexcept {
if (p) freeaddrinfo(p);
}
};
using AddrInfoPtr = std::unique_ptr<addrinfo, AddrInfoDeleter>;
// Fonction helper qui retourne un unique_ptr RAII
AddrInfoPtr resolve(const char* host, const char* service,
int family = AF_UNSPEC, int socktype = SOCK_STREAM)
{
addrinfo hints{};
hints.ai_family = family;
hints.ai_socktype = socktype;
hints.ai_flags = AI_PASSIVE; // Adapté pour bind() si host == nullptr
addrinfo* result = nullptr;
int status = getaddrinfo(host, service, &hints, &result);
if (status != 0) {
throw std::runtime_error(
std::string("getaddrinfo: ") + gai_strerror(status)
);
}
return AddrInfoPtr{result};
}Utilisation :
// Résolution pour un serveur écoutant sur le port 8080
auto addr = resolve(nullptr, "8080", AF_INET6);
// Création du socket avec les paramètres résolus
Socket sock{addr->ai_family, addr->ai_socktype, addr->ai_protocol};
// addr->ai_addr et addr->ai_addrlen sont prêts pour bind()
// La mémoire sera libérée automatiquement quand addr sort du scopeCe pattern élimine toute possibilité d'oublier freeaddrinfo(). Le unique_ptr avec deleter personnalisé est la manière idiomatique d'encapsuler des ressources C en RAII sans écrire une classe complète.
En production, un serveur peut ouvrir des milliers voire des dizaines de milliers de sockets simultanés. Il est important de connaître les limites et de les ajuster :
Limite par processus — Par défaut sur Ubuntu, un processus peut ouvrir 1024 file descriptors. C'est insuffisant pour un serveur réseau. Vérifiez et ajustez avec :
# Afficher la limite actuelle
ulimit -n
# Augmenter pour la session courante
ulimit -n 65535
# Rendre permanent via /etc/security/limits.conf
# myapp soft nofile 65535
# myapp hard nofile 65535Limite système — Le nombre total de file descriptors ouverts sur le système :
# Afficher la limite
cat /proc/sys/fs/file-max
# Modifier (temporaire)
sudo sysctl -w fs.file-max=2097152
# Modifier (permanent via /etc/sysctl.conf)
# fs.file-max = 2097152Ports éphémères — Quand un client se connecte sans spécifier de port source, le noyau en attribue un dans la plage éphémère. La plage par défaut sur Linux est 32768-60999, soit environ 28 000 ports. Pour des clients qui ouvrent beaucoup de connexions sortantes (load balancers, proxies), cette plage peut devenir limitante :
# Afficher la plage actuelle
cat /proc/sys/net/ipv4/ip_local_port_range
# Élargir
sudo sysctl -w net.ipv4.ip_local_port_range="1024 65535"Créer un socket en C++ moderne sur Linux, c'est combiner trois éléments : l'appel système socket() avec les bons paramètres et flags, la configuration via setsockopt() des options adaptées à votre cas d'usage, et une encapsulation RAII qui garantit la fermeture automatique quelles que soient les circonstances.
Les points à retenir :
- Utilisez toujours
SOCK_CLOEXECpour éviter les fuites de fd vers les processus enfants. - Activez
SO_REUSEADDRsur tous les sockets serveur. - Activez
TCP_NODELAYpour les protocoles interactifs et le RPC. - Encapsulez les file descriptors dans un wrapper RAII dès leur création — jamais de
close()manuel dans du code de haut niveau. - Préférez
AF_INET6en dual-stack pour supporter IPv4 et IPv6 avec un seul socket.
Prochaine étape → Section 22.1.2 : bind, listen, accept, connect — les opérations qui transforment un socket brut en un point de communication actif, côté serveur comme côté client.