Pour faire suite à l’article sur la théorie, voici l’article sur la pratique en C.
Le principe est simple : tout d’abord nous allons mettre toutes les fonctions en C dont ont besoin un client ou un serveur pour gérer les sockets réseau dans un couple de fichiers sockets.h et sockets.c que voici :
sockets.h :
/*
* sockets.h
* ---------
* Interface des fonctions réseau TCP/UDP, compatible IPv4 et IPv6.
*/
#ifndef SOCKETS_H
#define SOCKETS_H
#include <stddef.h> /* pour size_t */
/*
* Crée une socket TCP en écoute sur le port donné (ex: "56000").
* Retourne un descripteur prêt pour accepter_client(), ou -1 en cas d'erreur.
*/
int etablir_socket_ecoute_tcp(const char *port);
/*
* Crée une socket UDP liée au port donné.
* Retourne un descripteur, ou -1 en cas d'erreur.
*/
int etablir_socket_ecoute_udp(const char *port);
/*
* Se connecte en TCP à hote:port (IPv4 ou IPv6, automatique).
* Retourne un descripteur connecté, ou -1.
*/
int contacter_socket_tcp(const char *hote, const char *port);
/*
* Se connecte en UDP à hote:port.
* Retourne un descripteur, ou -1.
*/
int contacter_socket_udp(const char *hote, const char *port);
/*
* Accepte une connexion entrante sur la socket d'écoute fd_ecoute.
* - fd_ecoute : descripteur renvoyé par etablir_socket_ecoute_tcp()
* - adresse_client : buffer où sera écrite l'adresse IP du client
* (chaîne lisible, ex: "127.0.0.1" ou "::1")
* - taille_adresse : taille du buffer ci-dessus
*
* Retourne le descripteur de la connexion client, ou -1.
* Aucun struct sockaddr n'est exposé à l'appelant.
*/
int accepter_client(int fd_ecoute, char *adresse_client, size_t taille_adresse);
#endif /* SOCKETS_H */
Comme on peut le voir, on a à notre disposition 4 fonctions : 2 fonctions destinées à être utilisées par les programmes serveurs (etablir_socket_ecoute_tcp et etablir_socket_ecoute_tcp) et 2 fonctions destinées à être utilisées par les programmes clients (contacter_socket_tcp et contacter_socket_udp).
Le code de ces fonctions est prévu pour tenter des connexions IPv6 si disponible, et de basculer automatiquement sur IPv4 si IPv6 n’est pas disponible pour la connexion.
L’implémentation de ces fonctions est dans sockets.c dont voici le contenu :
/*
* sockets.c
* ---------
* Gestion de sockets TCP/UDP IPv4 + IPv6, totalement transparente
* pour l'appelant.
*
* Stratégie IPv4/IPv6 :
* - Client : getaddrinfo(AF_UNSPEC) renvoie la liste (IPv6 d'abord en
* général), on essaie chaque adresse jusqu'à ce que la
* connexion réussisse. Bascule automatique vers IPv4.
* - Serveur : on ouvre une socket IPv6 "dual-stack" (IPV6_V6ONLY = 0)
* qui accepte IPv6 natif ET IPv4-mapped. Si IPv6 n'est pas
* disponible, on retombe sur une socket IPv4.
*
* Toute la manipulation d'adresses (sockaddr, inet_ntop, IPv4-mapped...)
* reste confinée ici. L'appelant ne voit que des fd et des chaînes.
*/
#include "sockets.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <netdb.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
/*
* configure_socket_ecoute()
* -------------------------
* Options communes d'une socket d'écoute :
* - SO_REUSEADDR : redémarrage rapide sans attendre TIME_WAIT
* - IPV6_V6ONLY = 0 : socket IPv6 acceptant aussi les clients IPv4
*/
static void configure_socket_ecoute(int fd, int famille)
{
int on = 1;
setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));
if (famille == AF_INET6) {
int off = 0;
setsockopt(fd, IPPROTO_IPV6, IPV6_V6ONLY, &off, sizeof(off));
}
}
/*
* sockaddr_vers_chaine()
* ----------------------
* Convertit un struct sockaddr_storage (IPv4 ou IPv6, y compris
* IPv4-mapped) en chaîne lisible. Fonction interne, jamais exposée.
*/
static void sockaddr_vers_chaine(const struct sockaddr_storage *ss,
char *sortie, size_t taille)
{
if (ss->ss_family == AF_INET) {
const struct sockaddr_in *a = (const struct sockaddr_in *)ss;
inet_ntop(AF_INET, &a->sin_addr, sortie, taille);
} else if (ss->ss_family == AF_INET6) {
const struct sockaddr_in6 *a = (const struct sockaddr_in6 *)ss;
if (IN6_IS_ADDR_V4MAPPED(&a->sin6_addr)) {
/* Adresse IPv4 encapsulée : on n'affiche que la partie IPv4. */
struct in_addr v4;
memcpy(&v4, &a->sin6_addr.s6_addr[12], sizeof(v4));
inet_ntop(AF_INET, &v4, sortie, taille);
} else {
inet_ntop(AF_INET6, &a->sin6_addr, sortie, taille);
}
} else {
snprintf(sortie, taille, "inconnu");
}
}
/* ------------------------------------------------------------------ */
/* Sockets d'écoute */
/* ------------------------------------------------------------------ */
int etablir_socket_ecoute_tcp(const char *port)
{
struct addrinfo hints, *res, *p;
int fd = -1;
memset(&hints, 0, sizeof(hints));
hints.ai_family = AF_UNSPEC;
hints.ai_socktype = SOCK_STREAM;
hints.ai_flags = AI_PASSIVE;
if (getaddrinfo(NULL, port, &hints, &res) != 0) {
perror("getaddrinfo (tcp/ecoute)");
return -1;
}
for (p = res; p != NULL; p = p->ai_next) {
fd = socket(p->ai_family, p->ai_socktype, p->ai_protocol);
if (fd < 0)
continue;
configure_socket_ecoute(fd, p->ai_family);
if (bind(fd, p->ai_addr, p->ai_addrlen) == 0)
break;
close(fd);
fd = -1;
}
freeaddrinfo(res);
if (fd < 0) {
fprintf(stderr, "Aucune socket TCP n'a pu être liée au port %s\n", port);
return -1;
}
if (listen(fd, SOMAXCONN) < 0) {
perror("listen (tcp)");
close(fd);
return -1;
}
return fd;
}
int etablir_socket_ecoute_udp(const char *port)
{
struct addrinfo hints, *res, *p;
int fd = -1;
memset(&hints, 0, sizeof(hints));
hints.ai_family = AF_UNSPEC;
hints.ai_socktype = SOCK_DGRAM;
hints.ai_flags = AI_PASSIVE;
if (getaddrinfo(NULL, port, &hints, &res) != 0) {
perror("getaddrinfo (udp/ecoute)");
return -1;
}
for (p = res; p != NULL; p = p->ai_next) {
fd = socket(p->ai_family, p->ai_socktype, p->ai_protocol);
if (fd < 0)
continue;
configure_socket_ecoute(fd, p->ai_family);
if (bind(fd, p->ai_addr, p->ai_addrlen) == 0)
break;
close(fd);
fd = -1;
}
freeaddrinfo(res);
if (fd < 0)
fprintf(stderr, "Aucune socket UDP n'a pu être liée au port %s\n", port);
return fd;
}
/* ------------------------------------------------------------------ */
/* Clients */
/* ------------------------------------------------------------------ */
int contacter_socket_tcp(const char *hote, const char *port)
{
struct addrinfo hints, *res, *p;
int fd = -1;
memset(&hints, 0, sizeof(hints));
hints.ai_family = AF_UNSPEC;
hints.ai_socktype = SOCK_STREAM;
if (getaddrinfo(hote, port, &hints, &res) != 0) {
perror("getaddrinfo (tcp/client)");
return -1;
}
for (p = res; p != NULL; p = p->ai_next) {
fd = socket(p->ai_family, p->ai_socktype, p->ai_protocol);
if (fd < 0)
continue;
if (connect(fd, p->ai_addr, p->ai_addrlen) == 0)
break;
close(fd);
fd = -1;
}
freeaddrinfo(res);
if (fd < 0)
fprintf(stderr, "Impossible de se connecter à %s:%s (TCP)\n", hote, port);
return fd;
}
int contacter_socket_udp(const char *hote, const char *port)
{
struct addrinfo hints, *res, *p;
int fd = -1;
memset(&hints, 0, sizeof(hints));
hints.ai_family = AF_UNSPEC;
hints.ai_socktype = SOCK_DGRAM;
if (getaddrinfo(hote, port, &hints, &res) != 0) {
perror("getaddrinfo (udp/client)");
return -1;
}
for (p = res; p != NULL; p = p->ai_next) {
fd = socket(p->ai_family, p->ai_socktype, p->ai_protocol);
if (fd < 0)
continue;
if (connect(fd, p->ai_addr, p->ai_addrlen) == 0)
break;
close(fd);
fd = -1;
}
freeaddrinfo(res);
if (fd < 0)
fprintf(stderr, "Impossible de contacter %s:%s (UDP)\n", hote, port);
return fd;
}
/* ------------------------------------------------------------------ */
/* Acceptation d'un client */
/* ------------------------------------------------------------------ */
int accepter_client(int fd_ecoute, char *adresse_client, size_t taille_adresse)
{
struct sockaddr_storage ss;
socklen_t longueur = sizeof(ss);
int fd_client = accept(fd_ecoute, (struct sockaddr *)&ss, &longueur);
if (fd_client < 0)
return -1;
if (adresse_client != NULL && taille_adresse > 0)
sockaddr_vers_chaine(&ss, adresse_client, taille_adresse);
return fd_client;
}
A ce stade, nous disposons dans sockets.h et sockets.c de toutes les fonctions nécessaires pour permettre à un serveur ou à un client de faire du réseau TCP ou UDP sans avoir à se soucier de la complexité des fonctions réseau.
Dans notre exercice pratique, nous allons écrire un serveur simple, utilisant les fonctions disponibles dans sockets.h et sockets.c, qui va se mettre en écoute TCP sur le port 56000. Il sera en mesure de traiter plusieurs clients simultanément. Lorsqu’un client va le contacter, celui enverra une chaine de caractère que le serveur affichera à l’écran pour montrer qu’il a bien reçu. Il répondra en retour une autre chaine de caractères vers le client et fermera la connexion réseau.
Nous allons également écrire le code source du client qui va avec. Ce client commencera par contacter le serveur sur le port 56000 TCP en utilisant lui aussi les fonctions présentes dans sockets.h et sockets.c. Une fois la connexion établie, il enverra une chaine de caractères puis attendra en retour une chaine de caractères réponse qu’il afficher à l’écran pour prouver qu’il l’a bien reçue. Puis il mourra.
Dans notre exercice, le client et le serveur sont adaptés l’un à l’autre, ce qui est parfait. Cet exercice simple s’appelle PING PONG puisque le client enverra PING et le serveur répondra PONG.
Voici le code source du serveur, serveur.c :
/*
* serveur.c
* ---------
* Serveur TCP multi-clients sur le port 56000.
*
* Compilation : gcc -Wall -Wextra -O2 serveur.c sockets.c -o serveur
*/
#include "sockets.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/wait.h>
#define PORT_SERVEUR "56000"
#define TAILLE_TAMPON 1024
#define TAILLE_ADRESSE 64
/*
* traiter_client()
* ----------------
* Exécutée dans le processus fils. Lit le message du client, l'affiche,
* renvoie "PONG", puis ferme la connexion.
*/
static void traiter_client(int fd_client, const char *adresse_client)
{
char tampon[TAILLE_TAMPON];
ssize_t n = recv(fd_client, tampon, sizeof(tampon) - 1, 0);
if (n <= 0) {
if (n < 0) perror("recv");
close(fd_client);
return;
}
tampon[n] = '\0';
tampon[strcspn(tampon, "\r\n")] = '\0';
printf("[client %s] reçu : \"%s\"\n", adresse_client, tampon);
fflush(stdout);
const char *reponse = "PONG";
if (send(fd_client, reponse, strlen(reponse), 0) < 0)
perror("send");
close(fd_client);
}
/* Évite les processus zombies. */
static void gestionnaire_sigchld(int sig)
{
(void)sig;
while (waitpid(-1, NULL, WNOHANG) > 0)
;
}
int main(void)
{
/* Évite que send() sur une connexion fermée tue le serveur. */
signal(SIGPIPE, SIG_IGN);
struct sigaction sa;
memset(&sa, 0, sizeof(sa));
sa.sa_handler = gestionnaire_sigchld;
sa.sa_flags = SA_RESTART | SA_NOCLDSTOP;
sigaction(SIGCHLD, &sa, NULL);
int fd_ecoute = etablir_socket_ecoute_tcp(PORT_SERVEUR);
if (fd_ecoute < 0)
return EXIT_FAILURE;
printf("Serveur en écoute sur le port %s (TCP, IPv4/IPv6)\n", PORT_SERVEUR);
fflush(stdout);
while (1) {
char adresse_client[TAILLE_ADRESSE];
int fd_client = accepter_client(fd_ecoute,
adresse_client,
sizeof(adresse_client));
if (fd_client < 0) {
if (errno == EINTR) continue;
perror("accept");
continue;
}
pid_t pid = fork();
if (pid < 0) {
perror("fork");
close(fd_client);
continue;
}
if (pid == 0) {
/* Fils : ne sert plus à écouter. */
close(fd_ecoute);
traiter_client(fd_client, adresse_client);
exit(EXIT_SUCCESS);
}
/* Père : n'utilise pas cette connexion. */
close(fd_client);
}
close(fd_ecoute);
return EXIT_SUCCESS;
}
Et voici maintenant le code source de client.c :
/*
* client.c
* --------
* Client TCP minimaliste :
* 1. se connecte au serveur sur le port 56000
* 2. envoie "PING"
* 3. attend la réponse, l'affiche
* 4. se termine
*
* Usage : ./client [hote] (hote par défaut : "localhost")
*
* Compilation : gcc -Wall -Wextra -O2 client.c sockets.c -o client
*/
#include "sockets.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#define PORT_SERVEUR "56000"
#define TAILLE_TAMPON 1024
int main(int argc, char *argv[])
{
const char *hote = (argc > 1) ? argv[1] : "localhost";
int fd = contacter_socket_tcp(hote, PORT_SERVEUR);
if (fd < 0) {
fprintf(stderr, "Échec de connexion au serveur %s:%s\n",
hote, PORT_SERVEUR);
return EXIT_FAILURE;
}
printf("Connecté au serveur %s:%s\n", hote, PORT_SERVEUR);
const char *message = "PING";
if (send(fd, message, strlen(message), 0) < 0) {
perror("send");
close(fd);
return EXIT_FAILURE;
}
printf("Envoyé : \"%s\"\n", message);
char tampon[TAILLE_TAMPON];
ssize_t n = recv(fd, tampon, sizeof(tampon) - 1, 0);
if (n <= 0) {
if (n < 0) perror("recv");
else fprintf(stderr, "Le serveur a fermé la connexion sans répondre.\n");
close(fd);
return EXIT_FAILURE;
}
tampon[n] = '\0';
printf("Reçu du serveur : \"%s\"\n", tampon);
close(fd);
return EXIT_SUCCESS;
}
Comme on peut le voir, ni serveur.c ni client.c ne font apparaitre la complexité des fonctions en C gérant le réseau. sockets.h et sockets.c forment une librairie simple, réutilisable dans n’importe quel projet client/serveur codé en C.
Voyons maintenant que cela fonctionne nickel côté exécution.
Côté serveur :

Une fois compilé et lancé, le serveur se met en écoute sur le port 56000 TCP.
Côté client :

Le client ouvre la connexion avec le serveur, lui envoie PING et affiche ce qu’il reçoit en retour. Le client découvre alors que la réponse reçue est PONG.
Si l’on revient voir le serveur, on voit que le client l’a contacté et a envoyé PING :

Et bien sûr, si l’on lance 3 fois un client, le serveur est contacté 3 fois, mais ne meurt pas puisqu’il doit continuer à servir les futurs clients.

Voilà, c’est ainsi que sont progammés les serveurs écrits en C (Apache, Nginx et bien d’autres) et les clients écrits en C.
Bien entendu, comme déjà mentionné, les fichiers sockets.h et sockets.c sont à garder précieusement pour de nouveaux codes 😎
