Cours sur les sockets et pipes (communication entre processus et pc)

Développement système natif en c/c++ avec win32 ...

Moderator: Rick

Post Reply
Hydraxx
Site Admin
Posts: 46
Joined: Mon Jan 12, 2026 4:04 pm
Location: France
Contact:

Cours sur les sockets et pipes (communication entre processus et pc)

Post by Hydraxx »

Salut, :D

après le filesystem et les I/O, on attaque un autre bloc fondamental du système Windows : la communication. Autrement dit, comment deux entités distinctes s’échangent des données, que ce soit sur la meme machine ou à travers le réseau.

Ce sujet est important, parce qu’en pratique énormément de logiciels reposent dessus. Services Windows, outils client/serveur, programmes qui se parlent entre eux, consoles reliées à un processus enfant, applications réseau, agents locaux, watchers, outils d’admin, architecture distribuée… tout cela repose, d’une manière ou d’une autre, sur des mécanismes de communication.

Sous Windows, les deux grandes familles à connaitre sont les suivantes :
  • les pipes, utilisés surtout pour l’IPC local
  • les sockets, utilisés pour le réseau, mais aussi parfois en local
Pourquoi pipes et sockets sont importants

Comprendre ces mécanismes permet de mieux voir :
  • comment deux processus s’échangent des données
  • comment un service peut dialoguer avec un client
  • comment un parent redirige l’entrée ou la sortie d’un enfant
  • comment se construit un serveur TCP
  • comment transitent les données sur un système Windows
  • pourquoi certaines communications bloquent, se coupent, ou deviennent instables
Le but ici n’est pas seulement de montrer deux ou trois fonctions, mais de donner une vision plus propre du modèle, avec les API et structures qu’il faut réellement connaitre.

Pipes et sockets : la différence de fond

Un pipe est avant tout un mécanisme de communication entre composants locaux. Historiquement, il sert surtout à l’IPC, c’est-à-dire à la communication entre processus sur la meme machine. Selon le type de pipe, on peut aussi s’en servir dans des scénarios de parent/enfant, ou dans un petit modèle client/serveur local.

Un socket, lui, appartient davantage au monde réseau. Il permet de communiquer entre processus sur la meme machine ou sur des machines différentes. Son domaine naturel est le transport de données via une pile réseau.

On peut donc résumer les choses ainsi :
  • pipe = IPC local avant tout
  • socket = communication réseau, avec possibilité de communication locale aussi
Mais il faut faire attention à ne pas simplifier à l’excès. La vraie différence ne tient pas seulement à “local contre réseau”. Elle tient aussi au modèle d’API, au protocole sous-jacent, au type d’adressage, au niveau d’abstraction, et au contexte d’utilisation.

Le modèle général des pipes sous Windows

Les pipes sous Windows sont des objets noyau manipulés via des HANDLE. Cela signifie qu’ils s’intègrent très naturellement au modèle Win32 déjà vu avec les fichiers et les I/O.

On retrouve donc des fonctions familières comme :
  • CreatePipe
  • CreateNamedPipeW
  • ConnectNamedPipe
  • DisconnectNamedPipe
  • ReadFile
  • WriteFile
  • PeekNamedPipe
  • WaitNamedPipeW
  • SetNamedPipeHandleState
  • TransactNamedPipe
  • CreateFileW côté client
  • CloseHandle
On voit immédiatement une chose importante : les pipes Win32 sont très proches du modèle I/O générique. On lit et on écrit souvent dedans comme dans un fichier.

Anonymous pipes

Les anonymous pipes sont la forme la plus simple. Ils sont surtout utilisés dans des scénarios de communication entre un processus parent et un processus enfant. Par exemple, lorsqu’un processus crée un enfant et redirige son entrée standard ou sa sortie standard.

Ils ont plusieurs caractéristiques :
  • ils sont en général unidirectionnels
  • ils ne portent pas de nom global
  • ils reposent sur des handles héritables ou transmis
  • ils sont surtout adaptés à des communications locales simples
Création :

Code: Select all

HANDLE hRead = NULL;
HANDLE hWrite = NULL;

if (!CreatePipe(&hRead, &hWrite, NULL, 0))
    return 1;
A partir de la, un coté sert à lire, l’autre à écrire. Les opérations se font avec ReadFile et WriteFile, comme pour un fichier.

Exemple d’écriture :

Code: Select all

DWORD written = 0;
const char* msg = "hello pipe";

if (!WriteFile(hWrite, msg, (DWORD)strlen(msg), &written, NULL))
    return 1;
Exemple de lecture :

Code: Select all

char buffer[64] = { 0 };
DWORD read = 0;

if (!ReadFile(hRead, buffer, sizeof(buffer) - 1, &read, NULL))
    return 1;
Ce mécanisme est simple, robuste, et très pratique pour la redirection standard.

SECURITY_ATTRIBUTES et héritage de handles

Quand on travaille avec des anonymous pipes dans un scénario parent/enfant, il faut connaitre la structure SECURITY_ATTRIBUTES, car elle permet notamment de rendre les handles héritables.

Exemple :

Code: Select all

SECURITY_ATTRIBUTES sa = { 0 };
sa.nLength = sizeof(sa);
sa.lpSecurityDescriptor = NULL;
sa.bInheritHandle = TRUE;

if (!CreatePipe(&hRead, &hWrite, &sa, 0))
    return 1;
Cette structure est importante dans beaucoup d’API Win32, pas seulement pour les pipes. Ici, elle sert surtout à exprimer que les handles peuvent etre hérités par un processus enfant.

Pour contrôler plus finement l’héritage, il faut aussi connaitre :
  • SetHandleInformation
Exemple :

Code: Select all

if (!SetHandleInformation(hRead, HANDLE_FLAG_INHERIT, 0))
    return 1;
Cela permet par exemple de rendre un seul coté du pipe non héritable.

Créer un processus enfant avec redirection

Les anonymous pipes prennent tout leur sens quand on les relie à un enfant créé avec CreateProcessW.

Les structures importantes ici sont :
  • STARTUPINFOW
  • PROCESS_INFORMATION
Exemple schématique :

Code: Select all

STARTUPINFOW si = { 0 };
PROCESS_INFORMATION pi = { 0 };

si.cb = sizeof(si);
si.dwFlags = STARTF_USESTDHANDLES;
si.hStdInput = hRead;
si.hStdOutput = hWrite;
si.hStdError = hWrite;

if (!CreateProcessW(
    NULL,
    commandLine,
    NULL,
    NULL,
    TRUE,
    0,
    NULL,
    NULL,
    &si,
    &pi))
{
    return 1;
}
Ce genre de code est très utilisé pour lancer un processus enfant et dialoguer avec lui via stdin/stdout/stderr.

Named pipes

Les named pipes sont beaucoup plus riches et plus puissants. Contrairement aux anonymous pipes, ils sont identifiés par un nom, et peuvent servir de base à un vrai modèle client/serveur local.

Le nom ressemble typiquement à ceci :

Code: Select all

\\.\pipe\DemoPipe
Caractéristiques principales :
  • ils peuvent etre bidirectionnels
  • ils possèdent un nom global dans l’espace des pipes
  • plusieurs clients peuvent se connecter
  • ils reposent sur des objets noyau
  • ils peuvent fonctionner en mode byte ou message
  • ils peuvent aussi fonctionner en mode bloquant ou overlapped
Création coté serveur :

Code: Select all

HANDLE hPipe = CreateNamedPipeW(
    L"\\\\.\\pipe\\DemoPipe",
    PIPE_ACCESS_DUPLEX,
    PIPE_TYPE_BYTE | PIPE_READMODE_BYTE | PIPE_WAIT,
    PIPE_UNLIMITED_INSTANCES,
    4096,
    4096,
    0,
    NULL
);

if (hPipe == INVALID_HANDLE_VALUE)
    return 1;
Le serveur attend ensuite un client :

Code: Select all

BOOL ok = ConnectNamedPipe(hPipe, NULL);
if (!ok)
{
    DWORD err = GetLastError();
    if (err != ERROR_PIPE_CONNECTED)
        return 1;
}
Une fois connecté, il peut lire et écrire avec ReadFile et WriteFile.

Coté client, on ouvre le pipe avec CreateFileW :

Code: Select all

HANDLE hClient = CreateFileW(
    L"\\\\.\\pipe\\DemoPipe",
    GENERIC_READ | GENERIC_WRITE,
    0,
    NULL,
    OPEN_EXISTING,
    0,
    NULL
);

if (hClient == INVALID_HANDLE_VALUE)
    return 1;
Les modes des named pipes

Les named pipes possèdent plusieurs dimensions de configuration. C’est important à comprendre, car beaucoup de bugs viennent d’un mode mal choisi.

Il faut distinguer notamment :
  • le mode d’accès : PIPE_ACCESS_INBOUND, PIPE_ACCESS_OUTBOUND, PIPE_ACCESS_DUPLEX
  • le type : PIPE_TYPE_BYTE ou PIPE_TYPE_MESSAGE
  • le mode de lecture : PIPE_READMODE_BYTE ou PIPE_READMODE_MESSAGE
  • le mode d’attente : PIPE_WAIT ou PIPE_NOWAIT
Le mode byte est un flux d’octets sans notion native de message. Le mode message, lui, préserve des frontières de messages. Cela change la façon de raisonner sur la réception.

Pour modifier certains comportements d’un handle de pipe, on peut utiliser :
  • SetNamedPipeHandleState
Exemple :

Code: Select all

DWORD mode = PIPE_READMODE_MESSAGE;

if (!SetNamedPipeHandleState(hClient, &mode, NULL, NULL))
    return 1;
API utiles autour des named pipes

En plus des fonctions de base, plusieurs API sont très utiles :
  • DisconnectNamedPipe, pour déconnecter le client courant
  • WaitNamedPipeW, pour attendre qu’un pipe nommé soit disponible
  • PeekNamedPipe, pour regarder les données présentes sans les consommer
  • GetNamedPipeInfo, pour récupérer des infos générales
  • GetNamedPipeHandleState, pour interroger l’état ou le mode
  • CallNamedPipeW, pour un échange simple de type requete/réponse
  • TransactNamedPipe, pour envoyer une requete et attendre la réponse
  • ImpersonateNamedPipeClient, dans des scénarios de sécurité ou de service
Ce dernier point est particulièrement important dans certains services Windows, où le serveur peut temporairement adopter le contexte de sécurité du client.

OVERLAPPED et named pipes asynchrones

Comme pour les fichiers, les named pipes peuvent fonctionner en mode overlapped. Cela permet de ne pas bloquer un thread sur ConnectNamedPipe, ReadFile ou WriteFile.

Les structures à connaitre sont alors les memes que pour les I/O asynchrones classiques :
  • OVERLAPPED
  • event dans ov.hEvent
On peut ainsi construire des serveurs locaux plus robustes, capables de traiter plusieurs clients proprement.

Quand choisir un named pipe

Un named pipe est un excellent choix quand :
  • la communication est locale
  • on veut un modèle client/serveur intégré à Windows
  • on veut bénéficier des ACL et du modèle de sécurité Win32
  • on veut rester dans le modèle HANDLE + ReadFile/WriteFile
  • on construit un service Windows ou un agent local
C’est une technologie très utilisée dans l’écosystème Windows.

Le modèle général des sockets sous Windows

Les sockets utilisent l’API Winsock. Ici, on quitte en partie le monde des HANDLE Win32 classiques pour entrer dans une API réseau plus spécialisée.

Le type central est :
  • SOCKET
Un SOCKET n’est pas exactement un HANDLE classique, meme si conceptuellement il représente lui aussi une ressource à fermer.

Avant d’utiliser Winsock, il faut initialiser la bibliothèque :

Code: Select all

WSADATA wsa;

if (WSAStartup(MAKEWORD(2, 2), &wsa) != 0)
    return 1;
Et la nettoyer à la fin :

Code: Select all

WSACleanup();
C’est une étape obligatoire. Oublier WSAStartup fait partie des erreurs de base les plus fréquentes.

Structures fondamentales de Winsock

Pour travailler proprement avec les sockets, il faut reconnaitre au minimum plusieurs structures :
  • WSADATA
  • sockaddr
  • sockaddr_in
  • sockaddr_in6
  • addrinfo
  • fd_set
  • timeval
  • WSAPOLLFD
  • WSABUF
sockaddr est la forme générique. sockaddr_in est la version IPv4. sockaddr_in6 est la version IPv6. addrinfo est la structure moderne utilisée avec getaddrinfo pour résoudre proprement adresses et services.

Créer un socket

Exemple TCP IPv4 classique :

Code: Select all

SOCKET s = socket(
    AF_INET,
    SOCK_STREAM,
    IPPROTO_TCP
);

if (s == INVALID_SOCKET)
    return 1;
Les paramètres principaux sont :
  • la famille d’adresses : AF_INET, AF_INET6
  • le type : SOCK_STREAM, SOCK_DGRAM
  • le protocole : IPPROTO_TCP, IPPROTO_UDP
Pour un code plus flexible, on préfère souvent utiliser getaddrinfo puis créer le socket en fonction des résultats.

Serveur TCP minimal

Un serveur TCP de base suit généralement cette logique :
  • socket
  • bind
  • listen
  • accept
  • send / recv
  • closesocket
Exemple pour bind :

Code: Select all

sockaddr_in addr = { 0 };
addr.sin_family = AF_INET;
addr.sin_port = htons(4444);
addr.sin_addr.s_addr = INADDR_ANY;

if (bind(s, (sockaddr*)&addr, sizeof(addr)) == SOCKET_ERROR)
    return 1;
Puis :

Code: Select all

if (listen(s, SOMAXCONN) == SOCKET_ERROR)
    return 1;
Ensuite :

Code: Select all

SOCKET client = accept(s, NULL, NULL);
if (client == INVALID_SOCKET)
    return 1;
Une fois le client accepté, le dialogue peut commencer.

Client TCP minimal

Coté client :

Code: Select all

sockaddr_in addr = { 0 };
addr.sin_family = AF_INET;
addr.sin_port = htons(4444);
addr.sin_addr.s_addr = inet_addr("127.0.0.1");

if (connect(s, (sockaddr*)&addr, sizeof(addr)) == SOCKET_ERROR)
    return 1;
Cela fonctionne, mais inet_addr est aujourd’hui plutot ancien. Pour du code plus propre, on préfère généralement inet_pton ou mieux encore getaddrinfo.

Envoyer et recevoir

Les équivalents réseau de WriteFile et ReadFile sont send et recv.

Exemple :

Code: Select all

if (send(client, "Hello", 5, 0) == SOCKET_ERROR)
    return 1;
Exemple de réception :

Code: Select all

char buffer[256] = { 0 };
int ret = recv(client, buffer, sizeof(buffer), 0);

if (ret == SOCKET_ERROR)
    return 1;
Ici aussi, il faut raisonner sur la quantité réellement transmise. En TCP, il n’existe pas de garantie qu’un recv renverra exactement un “message applicatif” complet. TCP transporte un flux, pas des messages délimités par nature.

C’est un point extrêmement important.

TCP contre UDP

TCP et UDP ne servent pas au meme usage.

TCP :
  • est orienté connexion
  • fournit un flux fiable et ordonné
  • ne préserve pas de frontières de messages applicatifs
UDP :
  • est sans connexion au sens classique
  • transporte des datagrammes
  • préserve la notion de paquet
  • n’offre pas la meme garantie de livraison ou d’ordre que TCP
Pour UDP, on utilise souvent :
  • sendto
  • recvfrom
Exemple :

Code: Select all

int ret = sendto(
    s,
    data,
    dataLen,
    0,
    (sockaddr*)&addr,
    sizeof(addr)
);
Résolution de noms et adresses : getaddrinfo

Pour du code moderne, il faut connaitre :
  • getaddrinfo
  • freeaddrinfo
  • getnameinfo
  • inet_pton
  • inet_ntop
getaddrinfo permet de résoudre proprement un nom d’hote ou un service, tout en restant compatible IPv4 / IPv6.

La structure associée est addrinfo.

Exemple schématique :

Code: Select all

addrinfo hints = { 0 };
addrinfo* result = NULL;

hints.ai_family = AF_UNSPEC;
hints.ai_socktype = SOCK_STREAM;
hints.ai_protocol = IPPROTO_TCP;

if (getaddrinfo("127.0.0.1", "4444", &hints, &result) != 0)
    return 1;
AF_UNSPEC permet justement de ne pas se limiter a priori à IPv4 ou IPv6.

Non-bloquant et multiplexage

Par défaut, les sockets sont bloquants. Cela signifie que :
  • connect peut bloquer
  • recv peut bloquer
  • send peut bloquer
  • accept peut bloquer
Pour passer en non-bloquant :

Code: Select all

u_long mode = 1;

if (ioctlsocket(s, FIONBIO, &mode) == SOCKET_ERROR)
    return 1;
Ensuite, certaines opérations peuvent renvoyer WSAEWOULDBLOCK au lieu d’attendre.

Pour attendre plusieurs sockets ou surveiller leur état, plusieurs modèles existent :
  • select
  • WSAEventSelect
  • WSAWaitForMultipleEvents
  • WSAPoll
  • I/O overlapped
  • IOCP
select, fd_set et timeval

select reste une API classique pour surveiller plusieurs sockets.

Les structures importantes sont :
  • fd_set
  • timeval
Exemple schématique :

Code: Select all

fd_set readSet;
FD_ZERO(&readSet);
FD_SET(s, &readSet);

timeval tv;
tv.tv_sec = 1;
tv.tv_usec = 0;

int ret = select(0, &readSet, NULL, NULL, &tv);
Ce n’est pas le modèle le plus scalable, mais il reste important à connaitre.

WSAEventSelect et événements réseau

Windows propose aussi un modèle fondé sur des événements avec :
  • WSAEventSelect
  • WSACreateEvent
  • WSAWaitForMultipleEvents
  • WSAEnumNetworkEvents
  • WSAResetEvent
  • WSACloseEvent
Cela permet d’intégrer les sockets dans une logique d’attente par événements, plus proche du reste du modèle Win32.

I/O overlapped et WSABUF

Pour du réseau plus avancé, il faut connaitre les fonctions overlapped de Winsock :
  • WSASend
  • WSARecv
  • AcceptEx
  • ConnectEx
  • DisconnectEx
La structure centrale pour les buffers est :
  • WSABUF
Exemple de WSABUF :

Code: Select all

char buffer[512];
WSABUF buf;
buf.buf = buffer;
buf.len = sizeof(buffer);
Ces API sont particulièrement importantes dès qu’on s’approche d’un vrai modèle serveur performant sous Windows.

IOCP : le modèle serveur haute performance

Si on veut aller vers du serveur scalable, il faut au moins savoir que Windows possède un modèle très important :
  • I/O Completion Ports
Les API fondamentales à connaitre de nom sont notamment :
  • CreateIoCompletionPort
  • GetQueuedCompletionStatus
  • PostQueuedCompletionStatus
Ce n’est pas encore le sujet ici en détail, mais il faut retenir qu’IOCP est l’un des grands modèles de concurrence et d’I/O de Windows pour les serveurs performants.

Les options de socket

Autour des sockets, il faut aussi connaitre :
  • setsockopt
  • getsockopt
  • shutdown
  • closesocket
Quelques options fréquentes :
  • SO_REUSEADDR
  • SO_KEEPALIVE
  • TCP_NODELAY
  • SO_RCVBUF
  • SO_SNDBUF
  • SO_LINGER
shutdown est utile pour fermer proprement un demi-canal, par exemple en émission seulement.

Nom local, nom distant et informations de connexion

API utiles à connaitre :
  • getsockname
  • getpeername
Elles permettent de connaitre l’adresse locale réellement associée à un socket, ou bien l’adresse distante connectée.

Gérer les erreurs Winsock

Avec Winsock, on ne récupère pas les erreurs avec GetLastError, mais avec :
  • WSAGetLastError
C’est un point de base à retenir.

Quelques erreurs classiques :
  • WSAEWOULDBLOCK
  • WSAECONNRESET
  • WSAETIMEDOUT
  • WSAEADDRINUSE
  • WSAENETDOWN
  • WSAENOTSOCK
Sans une bonne gestion d’erreur, un programme réseau devient très vite pénible à déboguer.

Pipes ou sockets : comment choisir

Le choix dépend surtout du contexte.

Les pipes sont souvent préférables quand :
  • la communication est strictement locale
  • on veut un modèle Win32 intégré
  • on veut profiter facilement du modèle de sécurité Windows
  • on construit une architecture locale client/serveur
Les sockets sont préférables quand :
  • la communication doit pouvoir sortir de la machine
  • on veut un protocole réseau standard
  • on vise une architecture serveur classique
  • on a besoin de TCP ou UDP
Une règle simple reste valable dans beaucoup de cas :
  • IPC local pur = named pipe souvent très bon choix
  • réseau = socket
Erreurs classiques

Coté pipes :
  • oublier de fermer les handles
  • oublier ConnectNamedPipe coté serveur
  • mal choisir byte mode ou message mode
  • bloquer inutilement sur ReadFile ou WriteFile
  • mal gérer l’héritage de handles avec CreateProcess
Coté sockets :
  • oublier WSAStartup
  • oublier WSACleanup
  • oublier closesocket
  • utiliser GetLastError au lieu de WSAGetLastError
  • bloquer le thread principal avec accept ou recv
  • croire que TCP renvoie des messages complets
  • mal gérer le non-bloquant et WSAEWOULDBLOCK
Les structures à connaitre absolument

Pour les pipes :
  • SECURITY_ATTRIBUTES
  • OVERLAPPED
  • STARTUPINFOW
  • PROCESS_INFORMATION
Pour Winsock :
  • WSADATA
  • sockaddr
  • sockaddr_in
  • sockaddr_in6
  • addrinfo
  • fd_set
  • timeval
  • WSABUF
  • WSAPOLLFD
Les API à connaitre absolument

Pour les anonymous pipes :
  • CreatePipe
  • ReadFile
  • WriteFile
  • SetHandleInformation
  • CloseHandle
Pour les named pipes :
  • CreateNamedPipeW
  • ConnectNamedPipe
  • DisconnectNamedPipe
  • WaitNamedPipeW
  • PeekNamedPipe
  • GetNamedPipeInfo
  • GetNamedPipeHandleState
  • SetNamedPipeHandleState
  • TransactNamedPipe
  • CallNamedPipeW
  • ImpersonateNamedPipeClient
  • CreateFileW
Pour les processus enfants et redirections :
  • CreateProcessW
  • SetStdHandle
Pour Winsock :
  • WSAStartup
  • WSACleanup
  • socket
  • bind
  • listen
  • accept
  • connect
  • send
  • recv
  • sendto
  • recvfrom
  • shutdown
  • closesocket
  • ioctlsocket
  • select
  • setsockopt
  • getsockopt
  • getaddrinfo
  • freeaddrinfo
  • getnameinfo
  • inet_pton
  • inet_ntop
  • WSAGetLastError
Pour les modèles plus avancés :
  • WSAEventSelect
  • WSAWaitForMultipleEvents
  • WSAEnumNetworkEvents
  • WSASend
  • WSARecv
  • CreateIoCompletionPort
  • GetQueuedCompletionStatus
  • PostQueuedCompletionStatus
  • AcceptEx
  • ConnectEx
Conclusion

Pipes et sockets remplissent finalement le meme grand rôle : transporter des données d’un point à un autre. Ce qui change, ce n’est pas l’idée générale, mais le contexte d’utilisation, le modèle d’API, le type d’adressage, et le périmètre de communication.

Les pipes s’intègrent naturellement au modèle Win32 fondé sur les handles et les I/O génériques. Les sockets, eux, ouvrent la porte au réseau, à Winsock, à TCP, à UDP, à la résolution de noms, et aux modèles serveurs plus avancés.

Si tu maitrises déjà correctement :
  • CreatePipe et CreateNamedPipeW
  • ReadFile / WriteFile sur pipes
  • CreateProcessW avec redirection
  • WSAStartup
  • socket / bind / listen / accept / connect
  • send / recv
  • getaddrinfo
  • ioctlsocket
  • select ou WSAEventSelect
  • les structures OVERLAPPED, WSADATA, sockaddr_in et addrinfo
alors tu possèdes déjà une base solide pour comprendre comment les programmes se parlent sous Windows.

Et surtout, tu commences à voir que la communication système n’est pas un “bonus” du développement Windows, mais une partie centrale de l’architecture logicielle, que ce soit en local avec des pipes ou sur le réseau avec des sockets.

A bientot pour le prochain cours :D

Who is online

Users browsing this forum: No registered users and 0 guests