Introduction à la programmation réseau avec sockets
Après avoir appris les bases de la programmation socket, j'ai souhaité créer une application de discussion simple fonctionnant sous Windows. Cette implémentation utilise le mécanisme d'I/O multiplexing, notamment la fonction select() pour surveiller l'état des sockets. Bien que cette technique soit considérée comme obsolète dans la programmation serveur moderne, elle offre une bonne compréhension des concepts fondamentaux. L'objectif final est d'explorer la technologie epoll sous Linux.
Fonctionnement du système
Le serveur joue le rôle d'un relais : il surveille les connexions entrantes et转发 chaque message reçu vers tous les autres clients connectés (broadcast). Chaque client doit pouvoir envoyer et recevoir des messages simultanément. Le serveur doit également supporter les protocoles IPv4 et IPv6.
Implémentation du serveur
// ServeurDiscussion.cpp
#if defined(_WIN32)
#ifndef _WIN32_WINNT
#define _WIN32_WINNT 0x0600
#endif
#include <WinSock2.h>
#include <WS2tcpip.h>
#pragma comment(lib,"ws2_32.lib")
#endif
#include <cstdio>
#include <string.h>
#include <iostream>
using namespace std;
int main()
{
// Initialisation de Winsock
WSAData donnees;
if (WSAStartup(MAKEWORD(2, 2), &donnees))
{
cout << "Echec de l'initialisation" << endl;
return -1;
}
// Configuration des parametres d'adresse
struct addrinfo indices;
memset(&indices, 0, sizeof(indices));
indices.ai_family = AF_UNSPEC;
indices.ai_socktype = SOCK_STREAM;
indices.ai_flags = AI_PASSIVE;
struct addrinfo* adresse_liaison;
getaddrinfo(NULL, "8080", &indices, &adresse_liaison);
// Creation du socket d'ecoute
SOCKET socket_ecoute;
socket_ecoute = socket(adresse_liaison->ai_family,
adresse_liaison->ai_socktype,
adresse_liaison->ai_protocol);
if (socket_ecoute == INVALID_SOCKET)
{
cout << "Echec de socket()" << endl;
return -1;
}
// Configuration pour accepter IPv4 et IPv6
setsockopt(socket_ecoute, IPPROTO_IPV6, IPV6_V6ONLY, NULL, 0);
// Liaison du socket
int resultat = bind(socket_ecoute, adresse_liaison->ai_addr,
adresse_liaison->ai_addrlen);
if (resultat)
{
cout << "Echec de bind(): " << WSAGetLastError() << endl;
return -1;
}
freeaddrinfo(adresse_liaison);
if (listen(socket_ecoute, 10) < 0)
{
cout << "Echec de listen(): " << WSAGetLastError();
return -1;
}
// Gestion des connexions multiples avec select()
fd_set ensemble_principal;
FD_ZERO(&ensemble_principal);
FD_SET(socket_ecoute, &ensemble_principal);
SOCKET socket_max = socket_ecoute;
while (1)
{
fd_set ensemble_lecture;
ensemble_lecture = ensemble_principal;
if (select(socket_max + 1, &ensemble_lecture, NULL, NULL, NULL) < 0)
{
cout << "Echec de select(): " << WSAGetLastError();
return -1;
}
// Parcours de tous les sockets
SOCKET desc;
for (desc = 1; desc <= socket_max; ++desc)
{
if (FD_ISSET(desc, &ensemble_lecture))
{
// Nouveau client ?
if (desc == socket_ecoute)
{
struct sockaddr_storage adresse_client;
socklen_t taille_client = sizeof(adresse_client);
SOCKET socket_client = accept(socket_ecoute,
(struct sockaddr*)&adresse_client, &taille_client);
if (socket_client == INVALID_SOCKET)
{
cout << "Echec de accept(): " << WSAGetLastError();
return -1;
}
FD_SET(socket_client, &ensemble_principal);
if (socket_client > socket_max)
socket_max = socket_client;
char tampon_adresse[100];
getnameinfo((struct sockaddr*)&adresse_client, taille_client,
tampon_adresse, sizeof(tampon_adresse),
NULL, 0, NI_NUMERICHOST);
cout << "Nouvelle connexion: " << tampon_adresse << endl;
}
else
{
// Reception des donnees du client
char tampon_lecture[1024];
int octets_recus = recv(desc, tampon_lecture,
sizeof(tampon_lecture), 0);
if (octets_recus <= 0)
{
// Deconnexion ou erreur
if (octets_recus == 0)
{
cout << "Client " << dec << " deconnecte." << endl;
}
else
{
cout << "Erreur recv() pour client " << desc
<< ". Code: " << WSAGetLastError() << endl;
}
// Liberation des ressources
closesocket(desc);
FD_CLR(desc, &ensemble_principal);
// Mise a jour du socket maximum
if (desc == socket_max)
{
socket_max--;
for (SOCKET s = socket_max; s >= 1; s--)
{
if (FD_ISSET(s, &ensemble_principal))
{
socket_max = s;
break;
}
}
}
}
else
{
// Donnees recues avec succes
tampon_lecture[octets_recus] = '\0';
// Extraction des informations client
struct sockaddr_storage info_client;
socklen_t taille_info = sizeof(info_client);
char ip_client[INET6_ADDRSTRLEN];
int port_client = 0;
if (getpeername(desc, (struct sockaddr*)&info_client,
&taille_info) == 0)
{
if (info_client.ss_family == AF_INET)
{
struct sockaddr_in* ipv4 =
(struct sockaddr_in*)&info_client;
inet_ntop(AF_INET, &ipv4->sin_addr,
ip_client, sizeof(ip_client));
port_client = ntohs(ipv4->sin_port);
}
else if (info_client.ss_family == AF_INET6)
{
struct sockaddr_in6* ipv6 =
(struct sockaddr_in6*)&info_client;
inet_ntop(AF_INET6, &ipv6->sin6_addr,
ip_client, sizeof(ip_client));
port_client = ntohs(ipv6->sin6_port);
}
}
else
{
strcpy(ip_client, "Inconnu");
}
cout << "Recu de " << ip_client << ":"
<< port_client << ": " << tampon_lecture << endl;
// Diffusion vers tous les autres clients
for (SOCKET cible = 1; cible <= socket_max; cible++)
{
if (FD_ISSET(cible, &ensemble_principal) &&
cible != desc && cible != socket_ecoute)
{
char tampon_diffusion[1200];
if (strcmp(ip_client, "Inconnu") == 0)
{
snprintf(tampon_diffusion,
sizeof(tampon_diffusion),
"[Inconnu]: %s", tampon_lecture);
}
else
{
struct sockaddr_storage test_addr;
socklen_t test_len = sizeof(test_addr);
if (getpeername(desc,
(struct sockaddr*)&test_addr,
&test_len) == 0)
{
if (test_addr.ss_family == AF_INET)
{
snprintf(tampon_diffusion,
sizeof(tampon_diffusion),
"[%s:%d]: %s",
ip_client, port_client,
tampon_lecture);
}
else
{
snprintf(tampon_diffusion,
sizeof(tampon_diffusion),
"[%s]:%d: %s",
ip_client, port_client,
tampon_lecture);
}
}
else
{
snprintf(tampon_diffusion,
sizeof(tampon_diffusion),
"[%s]: %s", ip_client,
tampon_lecture);
}
}
send(cible, tampon_diffusion,
strlen(tampon_diffusion), 0);
}
}
}
}
}
}
}
closesocket(socket_ecoute);
WSACleanup();
return 0;
}
Configuration du support IPv4/IPv6
struct addrinfo hints;
memset(&hints, 0, sizeof(hints));
hints.ai_family = AF_UNSPEC; // Accepte IPv4 et IPv6
hints.ai_socktype = SOCK_STREAM;
hints.ai_flags = AI_PASSIVE;
La structure addrinfo permet de configurer le type d'adresse souhaité. En utilisant AF_UNSPEC, le serveur accepte les connexions IPv4 et IPv6. L'option IPV6_V6ONLY est désactivée pour permettre cette compatibilité.
Gestion du multiplexage
fd_set master;
FD_ZERO(&master);
FD_SET(socket_listen, &master);
SOCKET max_socket = socket_listen;
La structure fd_set contenir les descripteurs de fichiers à surveiller. On y ajoute le socket d'écoute, et max_socket facilite l'itération ultérieure.
Implémentation du client
// ClientDiscussion.cpp
#if defined(_WIN32)
#ifndef _WIN32_WINNT
#define _WIN32_WINNT 0x0600
#endif
#include <WinSock2.h>
#include <WS2tcpip.h>
#include <conio.h>
#pragma comment(lib,"ws2_32.lib")
#endif
#include <cstdio>
#include <string.h>
#include <iostream>
using namespace std;
int main(int argc, char* argv[])
{
#if defined(_WIN32)
WSADATA donnees;
if (WSAStartup(MAKEWORD(2, 2), &donnees))
{
fprintf(stderr, "Echec d'initialisation.\n");
return 1;
}
#endif
// Resolution d'adresse du serveur
printf("Configuration de l'adresse distante...\n");
struct addrinfo hints, * adresse_distant;
memset(&hints, 0, sizeof(hints));
hints.ai_socktype = SOCK_STREAM;
if (getaddrinfo("127.0.0.1", "8080", &hints, &adresse_distant))
{
fprintf(stderr, "Echec de getaddrinfo().\n");
return 1;
}
// Affichage de l'adresse de connexion
char buffer_adresse[100], buffer_service[100];
getnameinfo(adresse_distant->ai_addr, adresse_distant->ai_addrlen,
buffer_adresse, sizeof(buffer_adresse),
buffer_service, sizeof(buffer_service),
NI_NUMERICHOST);
printf("Connexion a: %s:%s\n", buffer_adresse, buffer_service);
// Creation du socket client
SOCKET socket_distant = socket(adresse_distant->ai_family,
adresse_distant->ai_socktype,
adresse_distant->ai_protocol);
if (socket_distant == INVALID_SOCKET)
{
fprintf(stderr, "Echec de socket().\n");
freeaddrinfo(adresse_distant);
return 1;
}
// Etablissement de la connexion
printf("Connexion en cours...\n");
if (connect(socket_distant, adresse_distant->ai_addr,
adresse_distant->ai_addrlen))
{
fprintf(stderr, "Echec de connect().\n");
freeaddrinfo(adresse_distant);
closesocket(socket_distant);
return 1;
}
freeaddrinfo(adresse_distant);
printf("Connexion etablie!\n");
printf("Tapez vos messages et appuyez sur Entree. Tapez 'quit' pour quitter.\n\n");
// Boucle principale
while (true)
{
fd_set lectures;
FD_ZERO(&lectures);
FD_SET(socket_distant, &lectures);
struct timeval delai = { 0, 100000 };
if (select(socket_distant + 1, &lectures, NULL, NULL, &delai) < 0)
{
fprintf(stderr, "Echec de select().\n");
break;
}
// Reception des messages du serveur
if (FD_ISSET(socket_distant, &lectures))
{
char tampon[4096];
int octets_recus = recv(socket_distant, tampon,
sizeof(tampon) - 1, 0);
if (octets_recus <= 0)
{
printf("Serveur deconnecte.\n");
break;
}
tampon[octets_recus] = '\0';
printf("Recu: %s", tampon);
}
// Detection de la saisie utilisateur
#if defined(_WIN32)
if (_kbhit())
{
char saisie[4096];
if (fgets(saisie, sizeof(saisie), stdin))
{
if (strncmp(saisie, "quit", 4) == 0)
{
printf("Deconnexion...\n");
break;
}
int octets_envoyes = send(socket_distant, saisie,
strlen(saisie), 0);
if (octets_envoyes <= 0)
{
printf("Echec d'envoi. Connexion perdue.\n");
break;
}
}
}
#endif
}
// Nettoyage
printf("Fermeture de la connexion...\n");
closesocket(socket_distant);
#if defined(_WIN32)
WSACleanup();
#endif
printf("Client termine.\n");
return 0;
}
Limitations de l'approche select()
Le mécanisme select() présente plusieurs inconvénients :
- Limitation du nombre de descripteurs : La constante
FD_SETSIZE(habituellement 1024) limite le nombre de connexions simulatnées. - Performence dégradée : À chaque itération, tous les descripteurs sont parcourus, impliquant des copies de données importantes vers le noyau.
- Fonctionnalités limitées : Difficile à utiliser pour des applications à forte concurrence avec un contrôle granulaire.
Améliorations posibles
Plusieurs évolutions peuvent être envisagées :
- Implémenter une messagerie privée entre clients plutôt que le broadcast actuel
- Unifier l'affichage des adresses IPv4 et IPv6 pour plus de cohérence
- Ajouter un système de journalisation des messages côté serveur
- Intégrer un mécanisme de chiffrement pour les communications