Partie 6 - Sockets

L’objectif de cette partie est de réaliser un serveur de chat avec sockets TCP étape par étape. Pour chaque exercice vous devrez modifier votre serveur et client petit à petit. Faites deux fichiers (client et serveur) par exercice, faites un copier coller pour l’exercice suivant pour garder l’évolution du projet.

Voici une base de header pour vos fichiers :

#define _GNU_SOURCE
#include <arpa/inet.h>
#include <errno.h>
#include <netinet/in.h>
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <time.h>
#include <unistd.h>

#define PORT 8080 // deux côtés
#define BUF_SIZE 1024 // pour la taille des buffers, deux côtés
#define ADDRESS "127.0.0.1" // côté client
#define BACKLOG 5 // côté serveur
#define MAX_CLIENTS 10 // côté serveur

p6e1 - Client/Serveur écho simple

Créer un serveur TCP écho qui accepte une seule connexion, lit un message et le renvoie à un seul client. Vous pouvez suivre le cours et copier coller les exemples de code du cours pour construire le fichier morceau par morceau.

Pour le serveur - p6e1_serveur.c :

  1. Crée un socket TCP (AF_INET, SOCK_STREAM)

  2. Configure l’option SO_REUSEADDR pour éviter "Address already in use"

  3. Bind le socket sur toutes les interfaces (INADDR_ANY) sur le port 8080

  4. Met le socket en écoute avec un backlog de 5

  5. Accepte une seule connexion client

  6. Lit les données reçues du client (buffer de 1024 octets)

  7. Renvoie au client ce qu’il a envoyé (écho)

  8. Ferme proprement la connexion (shutdown puis close)

  9. Supprime le socket d’écoute

Résultat attendu (côté serveur) :

$ gcc -std=c2x -Wall -Wextra -pedantic -g p6/p6e1_serveur.c -o serveur && ./serveur
[Serveur] Démarrage sur le port 8080...
[Serveur] En attente de connexion...
[Serveur] Client connecté depuis 127.0.0.1:38292
[Serveur] Message reçu (17 octets) : Bonjour serveur !
[Serveur] Écho renvoyé au client
[Serveur] Connexion fermée

Pour le client - p6e1_client.c :

  1. Crée un socket TCP

  2. Se connecte au serveur sur 127.0.0.1:8080

  3. Envoie un message "Bonjour serveur !" au serveur

  4. Reçoit l’écho du serveur (le même message) et l’affiche

  5. Ferme proprement la connexion

Résultat attendu (côté client, dans un 2ème terminal) :

$ gcc -std=c2x -Wall -Wextra -pedantic -g p6/p6e1_client.c -o client && ./client
[Client] Connexion à 127.0.0.1:8080...
[Client] Connecté !
[Client] Envoi du message : Bonjour serveur !
[Client] Réponse reçue (17 octets) : Bonjour serveur !
[Client] Déconnecté.

p6e2 - Messages interactifs en boucle

Permettre au client d’envoyer plusieurs messages entrés dans le terminal (utiliser fgets()) et au serveur de répondre en boucle jusqu’à ce que le client se déconnecte.

Côté serveur, après avoir accepté le client, le serveur envoie un message de bienvenue, puis dans une boucle infinie, il attend de recevoir un message, puis le renvoie. Si recv retourne <0 → erreur, 0 → le client c’est déconnecté (Ctrl+D côté client), autre → un message est reçu.

Côté client, après le connect , reçoit et affiche le message de bienvenue du serveur, puis, dans une boucle infinie, au lieu d’envoyer un message préparé, lit une entrée utilisateur (fgets), retire le \n puis l’envoie au serveur (send) et attend de recevoir un message (recv). Si recv retourne <0 → erreur, 0 → le serveur c’est déconnecté (Ctrl+C côté serveur), autre → un message est reçu.

Si le client ne saisi rien (la longueur de la chaîne est vide après avoir enlevé \n), dans ce cas, retourner au début de la boucle sans envoyer au serveur (avec un continue).

$ gcc -std=c2x -Wall -Wextra -pedantic -g p6/p6e2_serveur.c -o serveur && ./serveur
[Serveur] Démarrage sur le port 8080...
[Serveur] En attente de connexion...
[Serveur] Client connecté depuis 127.0.0.1:49380
[Serveur] Message reçu (7 octets) : Bonjour
[Serveur] Écho renvoyé
[Serveur] Message reçu (5 octets) : Hello
[Serveur] Écho renvoyé
[Serveur] Message reçu (3 octets) : Bye
[Serveur] Écho renvoyé
[Serveur] Client déconnecté
[Serveur] Arrêt.
$ gcc -std=c2x -Wall -Wextra -pedantic -g p6/p6e2_client.c -o client && ./client
[Client] Connexion à 127.0.0.1:8080...
[Client] Connecté !
[Client] Entrez vos messages (Ctrl+D pour quitter) :
[Serveur] Bienvenue sur notre serveur !
> Bonjour
[Client] Écho : Bonjour
> Hello
[Client] Écho : Hello
>       // <- [Entrée] sans texte
>       // <- [Entrée] sans texte
> Bye
[Client] Écho : Bye
>       // <- [Ctrl+D]
[Client] Fin de saisie.
[Client] Déconnecté.

Côté client, vous pouvez effacer de la console les lignes précédentes avec le code suivant :

printf("\033[1A"); // remonte d’une ligne
printf("\033[2K"); // efface la ligne

Ce qui retire la ligne de saisie du texte :

$ gcc -std=c2x -Wall -Wextra -pedantic -g p6/p6e2_client.c -o client && ./client
[Client] Connexion à 127.0.0.1:8080...
[Client] Connecté !
[Client] Entrez vos messages (Ctrl+D pour quitter) :
[Serveur] Bienvenue sur notre serveur !
[Client] Écho : Bonjour
[Client] Écho : Hello
>       // <- [Entrée] sans texte
>       // <- [Entrée] sans texte
[Client] Écho : Bye
>       // <- [Ctrl+D]
[Client] Fin de saisie.
[Client] Déconnecté.

p6e3 - Protocole simple : commandes

Implémenter un interpréteur de commandes simple côté serveur. Le client envoie une commande, le serveur lui répond le résultat :

  • /ping : renvoyer pong

  • /time : renvoyer l’heure courante. Utiliser time_t now = time(NULL) puis ctime(&now) qui retourne la date en char *

  • /upper <texte> : transformer le texte en majuscules (parcourir chaque caractère, toupper())

  • /lower <texte> : transformer en minuscules (tolower())

  • /quit : fermer la connexion client

Si le message n’est pas une commande, le serveur renvoie l’écho.

Il est utile de vider les buffers à chaque tour de boucle pour ne pas garder le texte des appels précédents :

// memset met tous les bits qui commencent à l'adresse de buffer à 0 pour BUF_SIZE octets
memset(buffer, 0, BUF_SIZE);

Résultat :

$ gcc -std=c2x -Wall -Wextra -pedantic -g p6/p6e3_serveur.c -o serveur && ./serveur
[Serveur] Démarrage sur le port 8080...
[Serveur] Commandes disponibles : /ping, /time, /upper <texte>, /lower <texte>, /quit
[Serveur] En attente de connexion...
[Serveur] Client connecté depuis 127.0.0.1:42980
[Serveur] Message reçu : /ping
[Serveur] Message reçu : /time
[Serveur] Message reçu : /upper coucou
[Serveur] Message reçu : /lower COUCOU
[Serveur] Message reçu : coucou
[Serveur] Message reçu : /quit
[Serveur] Commande /quit reçue, fermeture de la connexion
[Serveur] Arrêt.
$ gcc -std=c2x -Wall -Wextra -pedantic -g p6/p6e3_client.c -o client && ./client
[Client] Connexion à 127.0.0.1:8080...
[Client] Connecté !
[Client] Commandes disponibles :
- /ping
- /time
- /upper <texte>
- /lower <texte>
- /quit

> /ping
PONG
> /time
Fri Nov 28 10:26:22 2025

> /upper coucou
COUCOU
> /lower COUCOU
coucou
> coucou
coucou
> /quit
BYE
[Client] Déconnecté.

p6e4 - Plusieurs clients indépendants (threads)

Il faut maintenant gérer plusieurs clients. Chaque client aura un thread dédié afin que le serveur accepte plusieurs clients simultanément. À la connexion, le serveur demandera au client son pseudo. Chaque client a une session indépendante avec laquelle il peut communiquer avec le serveur.

Le serveur devra donc garder un tableau de clients de taille MAX_CLIENTS dont il pourra enregistrer les informations avec l’aide d’une structure :

typedef struct client_s {
    int fd;
    struct sockaddr_in addr;
    char pseudo[32];
} client_t;

p6e5 - Serveur de chat multi-clients

Implémenter un serveur de chat broadcast : chaque message d’un client est envoyé à tous les autres.

Implémenter une fonction broadcast(const char *msg, int except_fd) qui envoie msg à tous les clients sauf except_fd.

À la connexion : envoyer [Serveur] <pseudo> rejoint le chat à tous.

À la déconnexion : envoyer [Serveur] <pseudo> a quitté.

Format des messages : [pseudo] Message

p6e6 - Serveur avec fonctionnalités avancées

Ajoutez les fonctionnalités suivantes :

  1. /list : liste tous les pseudos connectés

  2. /msg <pseudo> <message> : message privé à un utilisateur

  3. /nick <nouveau_pseudo> : changer de pseudo

  4. Historique : sauvegarder tous les messages dans un fichier chat.log

5. Couleurs : utiliser les codes ANSI pour colorer les messages dans le terminal, la connexion et déconnexion et pour chaque utilisateur 7. Timestamp : ajouter l’heure devant chaque message