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
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
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
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
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
Code: Select all
HANDLE hRead = NULL;
HANDLE hWrite = NULL;
if (!CreatePipe(&hRead, &hWrite, NULL, 0))
return 1;
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;
Code: Select all
char buffer[64] = { 0 };
DWORD read = 0;
if (!ReadFile(hRead, buffer, sizeof(buffer) - 1, &read, NULL))
return 1;
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;
Pour contrôler plus finement l’héritage, il faut aussi connaitre :
- SetHandleInformation
Code: Select all
if (!SetHandleInformation(hRead, HANDLE_FLAG_INHERIT, 0))
return 1;
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
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;
}
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
- 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
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;
Code: Select all
BOOL ok = ConnectNamedPipe(hPipe, NULL);
if (!ok)
{
DWORD err = GetLastError();
if (err != ERROR_PIPE_CONNECTED)
return 1;
}
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 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
Pour modifier certains comportements d’un handle de pipe, on peut utiliser :
- SetNamedPipeHandleState
Code: Select all
DWORD mode = PIPE_READMODE_MESSAGE;
if (!SetNamedPipeHandleState(hClient, &mode, NULL, NULL))
return 1;
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
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
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
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
Avant d’utiliser Winsock, il faut initialiser la bibliothèque :
Code: Select all
WSADATA wsa;
if (WSAStartup(MAKEWORD(2, 2), &wsa) != 0)
return 1;
Code: Select all
WSACleanup();
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
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;
- la famille d’adresses : AF_INET, AF_INET6
- le type : SOCK_STREAM, SOCK_DGRAM
- le protocole : IPPROTO_TCP, IPPROTO_UDP
Serveur TCP minimal
Un serveur TCP de base suit généralement cette logique :
- socket
- bind
- listen
- accept
- send / recv
- closesocket
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;
Code: Select all
if (listen(s, SOMAXCONN) == SOCKET_ERROR)
return 1;
Code: Select all
SOCKET client = accept(s, NULL, NULL);
if (client == INVALID_SOCKET)
return 1;
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;
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;
Code: Select all
char buffer[256] = { 0 };
int ret = recv(client, buffer, sizeof(buffer), 0);
if (ret == SOCKET_ERROR)
return 1;
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
- 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
- sendto
- recvfrom
Code: Select all
int ret = sendto(
s,
data,
dataLen,
0,
(sockaddr*)&addr,
sizeof(addr)
);
Pour du code moderne, il faut connaitre :
- getaddrinfo
- freeaddrinfo
- getnameinfo
- inet_pton
- inet_ntop
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;
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
Code: Select all
u_long mode = 1;
if (ioctlsocket(s, FIONBIO, &mode) == SOCKET_ERROR)
return 1;
Pour attendre plusieurs sockets ou surveiller leur état, plusieurs modèles existent :
- select
- WSAEventSelect
- WSAWaitForMultipleEvents
- WSAPoll
- I/O overlapped
- IOCP
select reste une API classique pour surveiller plusieurs sockets.
Les structures importantes sont :
- fd_set
- timeval
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);
WSAEventSelect et événements réseau
Windows propose aussi un modèle fondé sur des événements avec :
- WSAEventSelect
- WSACreateEvent
- WSAWaitForMultipleEvents
- WSAEnumNetworkEvents
- WSAResetEvent
- WSACloseEvent
I/O overlapped et WSABUF
Pour du réseau plus avancé, il faut connaitre les fonctions overlapped de Winsock :
- WSASend
- WSARecv
- AcceptEx
- ConnectEx
- DisconnectEx
- WSABUF
Code: Select all
char buffer[512];
WSABUF buf;
buf.buf = buffer;
buf.len = sizeof(buffer);
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
- CreateIoCompletionPort
- GetQueuedCompletionStatus
- PostQueuedCompletionStatus
Les options de socket
Autour des sockets, il faut aussi connaitre :
- setsockopt
- getsockopt
- shutdown
- closesocket
- SO_REUSEADDR
- SO_KEEPALIVE
- TCP_NODELAY
- SO_RCVBUF
- SO_SNDBUF
- SO_LINGER
Nom local, nom distant et informations de connexion
API utiles à connaitre :
- getsockname
- getpeername
Gérer les erreurs Winsock
Avec Winsock, on ne récupère pas les erreurs avec GetLastError, mais avec :
- WSAGetLastError
Quelques erreurs classiques :
- WSAEWOULDBLOCK
- WSAECONNRESET
- WSAETIMEDOUT
- WSAEADDRINUSE
- WSAENETDOWN
- WSAENOTSOCK
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
- 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
- IPC local pur = named pipe souvent très bon choix
- réseau = socket
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
- 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
Pour les pipes :
- SECURITY_ATTRIBUTES
- OVERLAPPED
- STARTUPINFOW
- PROCESS_INFORMATION
- WSADATA
- sockaddr
- sockaddr_in
- sockaddr_in6
- addrinfo
- fd_set
- timeval
- WSABUF
- WSAPOLLFD
Pour les anonymous pipes :
- CreatePipe
- ReadFile
- WriteFile
- SetHandleInformation
- CloseHandle
- CreateNamedPipeW
- ConnectNamedPipe
- DisconnectNamedPipe
- WaitNamedPipeW
- PeekNamedPipe
- GetNamedPipeInfo
- GetNamedPipeHandleState
- SetNamedPipeHandleState
- TransactNamedPipe
- CallNamedPipeW
- ImpersonateNamedPipeClient
- CreateFileW
- CreateProcessW
- SetStdHandle
- 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
- WSAEventSelect
- WSAWaitForMultipleEvents
- WSAEnumNetworkEvents
- WSASend
- WSARecv
- CreateIoCompletionPort
- GetQueuedCompletionStatus
- PostQueuedCompletionStatus
- AcceptEx
- ConnectEx
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
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
