Partie 6 - Sockets

L’objectif de cette partie est de réaliser un serveur de chat avec des sockets TCP, étape par étape. Pour chaque exercice, vous devrez modifier votre serveur et votre client petit à petit. Faites deux fichiers (client et serveur) par exercice, puis copiez-les pour l’exercice suivant afin de garder une trace de l’évolution du projet.

Voici une base d’en-tête pour vos fichiers :

#define _GNU_SOURCE
#include <arpa/inet.h>
#include <ctype.h>
#include <errno.h>
#include <netinet/in.h>
#include <pthread.h>
#include <stdatomic.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 PSEUDO_SIZE 32 // 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 3 // côté serveur

p6e1 - Client/Serveur écho simple

Créer un serveur d’écho TCP qui accepte une seule connexion, lit un message et le renvoie au client. Vous pouvez suivre le cours et copier-coller les exemples de code du cours pour construire le fichier morceau par morceau. Pour les envois, utilisez la fonction send_all du cours (envois partiels et SIGPIPE). Le schéma de la connexion pas à pas montre l’enchaînement des appels des deux côtés. Après chaque appel, testez la valeur de retour comme dans le cours (perror, close, exit).

Pour le serveur - p6e1_serveur.c :

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

  2. Activer l’option SO_REUSEADDR (voir le cours) pour pouvoir relancer le serveur tout de suite sans l’erreur Address already in use.

  3. Lier (bind) le socket à toutes les interfaces (INADDR_ANY) sur le port 8080.

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

  5. Accepter une seule connexion client.

  6. Lire les données reçues du client (buffer de 1024 octets) ; recv n’ajoute pas de '\0' (voir le cours).

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

  8. Fermer proprement la connexion en suivant le « patron de fermeture conseillé » du cours (shutdown, lecture jusqu’à EOF, puis close).

  9. Fermer 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

Avant d’écrire le client, vous pouvez tester le serveur avec nc 127.0.0.1 8080 dans un second terminal : tapez un message puis Entrée, le serveur doit le renvoyer, puis Ctrl+D pour terminer : après l’écho, le serveur attend la fin de connexion (voir Tester avec nc et ss). Le message affiché par le serveur contient le '\n' tapé.

Pour le client - p6e1_client.c :

  1. Créer un socket TCP.

  2. Se connecter au serveur sur 127.0.0.1:8080.

  3. Envoyer le message "Bonjour serveur !" au serveur.

  4. Recevoir l’écho du serveur (le même message) et l’afficher.

  5. Fermer proprement la connexion (même patron de fermeture que le serveur).

Résultat attendu (côté client, dans un second 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é.
Échanges entre le client et le serveur écho de p6e1.

p6e2 - Messages interactifs en boucle

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

Protocole. TCP ne conserve pas les frontières des messages (voir le cours) : à partir de cet exercice, chaque message se termine par '\n', dans les deux sens.

  • pour envoyer, utiliser la fonction send_line, qui envoie text puis "\n" avec send_all :

    // Envoie text suivi de '\n'. Retourne 0 si tout a été envoyé, -1 en cas d'erreur.
    int send_line(int fd, const char *text) {
        if (send_all(fd, text, strlen(text)) == -1) {
            return -1;
        }
        return send_all(fd, "\n", 1);
    }
    
  • pour recevoir, créer un flux FILE *in = fdopen(fd, "r") sur le socket et lire une ligne avec fgets(buffer, BUF_SIZE, in) : NULL signifie que le pair a fermé la connexion (ou une erreur, à distinguer avec ferror(in)). En fin de programme, fclose(in) ferme aussi le socket. Un seul FILE * par socket : on lit avec in, on écrit avec send_line sur le descripteur.

Ctrl+D (en début de ligne) signale la fin de l’entrée standard : fgets(..., stdin) retourne alors NULL, ce qui permet de quitter le client proprement.

Côté serveur, après avoir accepté le client, le serveur envoie un message de bienvenue (une ligne), puis, dans une boucle infinie, attend une ligne et la renvoie avec send_line (après avoir retiré le '\n', que send_line rajoute). Quand fgets retourne NULL, le client s’est déconnecté (Ctrl+D côté client) : le serveur sort de la boucle, ferme la connexion et s’arrête (un seul client, comme en p6e1).

Côté client, après le connect, le client reçoit et affiche le message de bienvenue du serveur, puis affiche [Client] Entrez vos messages (Ctrl+D pour quitter) :. Ensuite, dans une boucle infinie, il lit une saisie de l’utilisateur (fgets sur stdin), retire le '\n', l’envoie au serveur avec send_line, puis attend l’écho (fgets sur le socket). Si fgets sur le socket retourne NULL, le serveur s’est déconnecté (Ctrl+C côté serveur).

Pour la saisie, utiliser fgets(input, BUF_SIZE - 1, stdin) (et non BUF_SIZE).

Si la saisie est vide (chaîne de longueur 0 après retrait du '\n'), revenir au début de la boucle sans rien envoyer (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é !
[Serveur] Bienvenue sur notre serveur !
[Client] Entrez vos messages (Ctrl+D pour quitter) :
> 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é.

(Facultatif) Côté client, vous pouvez effacer la ligne de saisie avant l’envoi avec les codes d’échappement ANSI suivants. Ce sont des séquences spéciales (le caractère ESC, \033, suivi de [ et d’une commande) que le terminal interprète au lieu de les afficher :

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

Placées après le test de saisie vide (juste avant l’envoi), ces lignes effacent de l’écran chaque saisie envoyée (les saisies vides restent affichées) :

$ gcc -std=c2x -Wall -Wextra -pedantic -g p6/p6e2_client.c -o client && ./client
[Client] Connexion à 127.0.0.1:8080...
[Client] Connecté !
[Serveur] Bienvenue sur notre serveur !
[Client] Entrez vos messages (Ctrl+D pour quitter) :
[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é.
Boucle d'échanges entre le client et le serveur de p6e2.

p6e3 - Protocole simple : commandes

Implémenter un interpréteur de commandes simple côté serveur.

À la connexion, le serveur envoie un message de bienvenue sur plusieurs lignes qui liste les commandes disponibles (voir la sortie ci-dessous). Ce message se termine par une ligne vide : le client lit et affiche les lignes jusqu’à recevoir la ligne "\n".

Ensuite, le client envoie une ligne ; si elle commence par '/', c’est une commande, et le serveur répond par une ligne contenant le résultat. Côté serveur, écrire une fonction void process_command(const char *command, char *response) qui écrit dans response (un buffer de BUF_SIZE octets) la réponse à la commande command (sans le '\n') :

  • /ping : renvoyer pong ;

  • /time : renvoyer l’heure courante. Utiliser time_t now = time(NULL); puis char *time_str = ctime(&now);. ctime renvoie une chaîne qui se termine par '\n' : retirer ce '\n' avant l’envoi ;

  • /upper <texte> : transformer le texte en majuscules (parcourir chaque caractère et appliquer toupper, de <ctype.h>) ;

  • /lower <texte> : transformer le texte en minuscules (tolower sur chaque caractère) ;

  • /quit : répondre Bye, puis fermer la connexion avec le client.

Côté client, après avoir envoyé /quit et affiché la réponse, quitter la boucle et fermer proprement la connexion.

Précisions :

  • pour reconnaître /upper <texte>, comparer seulement le début de la chaîne avec strncmp(command, "/upper ", 7) == 0 (compare au plus les 7 premiers caractères) ; le texte commence alors à command + 7, l’adresse du 8e caractère (voir l’arithmétique des pointeurs dans le cours) ;

  • si /upper ou /lower n’a pas de texte ("/upper" seul ou "/upper " suivi d’un espace), répondre Usage : /upper <texte> (ou /lower) : ne jamais envoyer de réponse vide. Tester ces deux cas avec strcmp avant le strncmp ;

  • toupper et tolower attendent une valeur représentable en unsigned char (ou EOF) : écrire toupper((unsigned char)text[i]). Les caractères accentués (codés sur plusieurs octets en UTF-8) ne sont pas convertis : /upper été renvoie éTé ;

  • pour une commande inconnue, répondre Commande inconnue ;

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

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:53490
[Serveur] Message reçu (5 octets) : /ping
[Serveur] Message reçu (5 octets) : /time
[Serveur] Message reçu (14 octets) : /upper bonjour
[Serveur] Message reçu (6 octets) : /upper
[Serveur] Message reçu (14 octets) : /lower BONJOUR
[Serveur] Message reçu (4 octets) : /xyz
[Serveur] Message reçu (5 octets) : salut
[Serveur] Message reçu (5 octets) : /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é !
[Serveur] Bienvenue sur notre serveur !
[Serveur] Commandes disponibles :
  - /ping
  - /time
  - /upper <texte>
  - /lower <texte>
  - /quit

> /ping
pong
> /time
Sat Nov 29 16:48:18 2025
> /upper bonjour
BONJOUR
> /upper
Usage : /upper <texte>
> /lower BONJOUR
bonjour
> /xyz
Commande inconnue
> salut
salut
> /quit
Bye
[Client] Déconnecté.
Échanges de commandes entre le client et le serveur de p6e3.

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 (voir Servir plusieurs clients dans le cours). Chaque client dispose d’une session indépendante avec le serveur. Dans ce thread dédié, le serveur dialogue avec le client dans une boucle infinie jusqu’à sa déconnexion.

Procédez par étapes :

  1. Un thread par client : reprenez le serveur de p6e3, mettez accept dans une boucle, et déplacez le dialogue avec le client dans une fonction void *handle_client(void *arg) exécutée par un thread détaché. Pas encore de tableau ni de pseudo : le client de p6e3 fonctionne tel quel. Testez avec deux clients (ou deux nc 127.0.0.1 8080) connectés en même temps.

  2. Tableau des clients : ajoutez le tableau clients, son mutex, la recherche d’une case libre et le refus quand le serveur est plein (consignes ci-dessous).

  3. Pseudo : ajoutez le choix du pseudo, côté serveur puis côté client.

Le serveur devra donc garder un tableau de clients de taille MAX_CLIENTS, dans lequel il enregistrera les informations de chaque client à l’aide de la structure :

typedef struct {
    int fd;                   // -1 si la case est libre
    struct sockaddr_in addr;
    char pseudo[PSEUDO_SIZE]; // chaîne vide tant que le pseudo n'est pas choisi
} client_t;

Consignes pour le serveur :

  • au démarrage, mettre fd à -1 dans toutes les cases du tableau ;

  • le tableau est partagé entre les threads : protéger chaque accès par un mutex (voir le chapitre sur les threads) ;

  • après accept, chercher une case libre (fd == -1). S’il n’y en a pas, envoyer Serveur plein, réessayez plus tard., fermer la connexion et afficher [Serveur] Connexion refusée : serveur plein ;

  • sinon, remplir la case et créer un thread avec pthread_create en lui passant l’adresse de la case (&clients[slot]) : elle appartient au tableau global et reste donc valide pendant toute la vie du thread. Personne n’attendra ce thread avec pthread_join : le détacher avec pthread_detach ;

  • le thread envoie le message de bienvenue de p6e3 (terminé par une ligne vide), puis lit le pseudo proposé par le client ;

  • un pseudo est valide s’il est :

    • non vide ;

    • d’au plus PSEUDO_SIZE - 1 caractères (il faut garder la place du '\0') ;

    • sans espace (utiliser strchr(pseudo, ' '), qui retourne NULL si la chaîne ne contient pas d’espace) ;

    • pas déjà utilisé par un autre client ;

  • si le pseudo est valide, le serveur répond Bienvenue <pseudo> ! Commandes : /ping, /time, /upper, /lower, /quit ; sinon, il répond par un message d’erreur et attend une autre proposition ;

  • découpage conseillé :

    • int set_pseudo(client_t *client, const char *pseudo) : vérifie que le pseudo est valide et, s’il l’est, l’écrit dans la case du client ; retourne 1 si le pseudo est accepté, 0 sinon. La vérification « pas déjà utilisé » et l’écriture se font sous le même verrou : sinon, deux clients pourraient vérifier en même temps le même pseudo, le trouver libre, et le prendre tous les deux ;

    • int ask_pseudo(client_t *client, FILE *in) : lit les propositions du client et répond par le message d’erreur jusqu’à ce que set_pseudo accepte un pseudo ; retourne 1 si un pseudo a été accepté, 0 si le client s’est déconnecté avant ;

  • ctime renvoie un buffer statique partagé par tous les threads : dans un programme multi-threadé, utiliser ctime_r(&now, time_str) avec un buffer char time_str[26] ;

  • à la déconnexion du client, remettre sa case à fd = -1 (sous le mutex) avant de fermer le socket : dès le close, le noyau peut redonner ce numéro de descripteur au prochain accept (il attribue toujours le plus petit numéro libre), et deux cases du tableau contiendraient alors le même fd.

Consignes pour le client : après le message de bienvenue, afficher Entrez votre pseudo :, lire le pseudo et l’envoyer, puis lire la réponse du serveur. Recommencer tant que la réponse ne commence pas par Bienvenue (strncmp(buffer, "Bienvenue", 9) != 0). Si le serveur est plein, il ferme la connexion : le client le détecte (fgets retourne NULL) et s’arrête.

Ne pas oublier d’ajouter -pthread à la compilation.

Résultat (dans différents terminaux) :

$ gcc -std=c2x -Wall -Wextra -pedantic -g -pthread p6/p6e4_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 connexions (max 3 clients)...
[Serveur] alice connecté depuis 127.0.0.1:51168
[Serveur] bob connecté depuis 127.0.0.1:51172
[Serveur] charlie connecté depuis 127.0.0.1:51180
[Serveur] Connexion refusée : serveur plein
[Serveur] Message reçu de alice (5 octets) : /time
[Serveur] Message reçu de bob (14 octets) : /upper bonjour
[Serveur] Message reçu de charlie (14 octets) : /lower BONJOUR
[Serveur] Message reçu de charlie (5 octets) : /quit
[Serveur] Commande /quit reçue, fermeture de la connexion
[Serveur] bob déconnecté
[Serveur] alice déconnecté
^C
$ gcc -std=c2x -Wall -Wextra -pedantic -g -pthread p6/p6e4_client.c -o client && ./client
[Client] Connexion à 127.0.0.1:8080...
[Client] Connecté !
[Serveur] Bienvenue sur notre serveur !
[Serveur] Commandes disponibles :
  - /ping
  - /time
  - /upper <texte>
  - /lower <texte>
  - /quit

Entrez votre pseudo : alice
Bienvenue alice ! Commandes : /ping, /time, /upper, /lower, /quit
> /time
Sat Nov 29 17:24:24 2025
>       // <- [Ctrl+D]
[Client] Fin de saisie.
[Client] Déconnecté.
$ ./client
[Client] Connexion à 127.0.0.1:8080...
[Client] Connecté !
[Serveur] Bienvenue sur notre serveur !
[Serveur] Commandes disponibles :
  - /ping
  - /time
  - /upper <texte>
  - /lower <texte>
  - /quit

Entrez votre pseudo : alice
Pseudo invalide (vide, avec espace, trop long ou déjà utilisé)
Entrez votre pseudo : bob le bricoleur
Pseudo invalide (vide, avec espace, trop long ou déjà utilisé)
Entrez votre pseudo : bob
Bienvenue bob ! Commandes : /ping, /time, /upper, /lower, /quit
> /upper bonjour
BONJOUR
>       // <- [Ctrl+D]
[Client] Fin de saisie.
[Client] Déconnecté.
$ ./client
[Client] Connexion à 127.0.0.1:8080...
[Client] Connecté !
[Serveur] Bienvenue sur notre serveur !
[Serveur] Commandes disponibles :
  - /ping
  - /time
  - /upper <texte>
  - /lower <texte>
  - /quit

Entrez votre pseudo : charlie
Bienvenue charlie ! Commandes : /ping, /time, /upper, /lower, /quit
> /lower BONJOUR
bonjour
> /quit
Bye
[Client] Déconnecté.
$ ./client
[Client] Connexion à 127.0.0.1:8080...
[Client] Connecté !
Serveur plein, réessayez plus tard.
[Client] Serveur déconnecté
Serveur multi-threadé de p6e4 : un thread par client.

p6e5 - Serveur de chat multi-clients

Implémenter un serveur de chat avec diffusion (broadcast) : chaque message d’un client est envoyé à tous les autres. Lorsqu’un utilisateur envoie une commande, lui seul reçoit le résultat de la commande.

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

À la connexion (une fois le pseudo accepté) : envoyer [Serveur] <pseudo> a rejoint le chat à tous les autres clients.

À la déconnexion : envoyer [Serveur] <pseudo> a quitté le chat à tous les autres clients.

Format des messages diffusés : [pseudo] Message.

Plusieurs threads peuvent maintenant écrire sur le socket d’un même client : son propre thread (réponse à une commande) et ceux des autres clients (broadcast). Deux précautions côté serveur :

  • send_line doit envoyer le texte et son '\n' en un seul send_all, sinon une ligne diffusée par un autre thread peut s’intercaler entre le texte et son '\n'. Construire la ligne complète dans un buffer avec snprintf, qui retourne le nombre de caractères écrits (ou qui auraient été écrits si le buffer était trop petit) :

    char line[BUF_SIZE * 2];
    int n = snprintf(line, sizeof(line), "%s\n", text);
    if (n < 0 || (size_t)n >= sizeof(line)) {
        return -1; // ligne trop longue pour le buffer
    }
    return send_all(fd, line, (size_t)n);
    

    Remarque : même un seul send_all peut faire plusieurs send (envoi partiel) ; une garantie stricte demanderait un mutex d’écriture par client, ce qui n’est pas demandé ici ;

  • dès que le pseudo d’un client est écrit dans le tableau, broadcast peut lui envoyer des messages. Si la ligne Bienvenue ... part après le relâchement du mutex, un message diffusé peut arriver avant elle : le client le prend pour la réponse à son pseudo, croit ce dernier refusé et redemande un pseudo. Envoyer la ligne Bienvenue ... sous le mutex, juste après avoir écrit le pseudo. Remarque : broadcast envoie lui aussi sous le mutex. Un send peut bloquer si un client ne lit plus ses messages, et tout le serveur attendrait alors ce client : c’est une simplification assumée, suffisante pour ce TP.

Serveur de chat : un thread par client, tableau des clients protégé par un mutex ; un message est diffusé à tous sauf à son émetteur, la réponse à une commande n'est envoyée qu'à l'émetteur.

Avant d’écrire le client, testez le serveur avec deux ou trois nc 127.0.0.1 8080 : tapez un pseudo, puis des messages ; les diffusions des autres s’affichent directement dans nc.

Côté client, le programme doit à la fois attendre la saisie de l’utilisateur (fgets sur stdin, bloquant) et afficher les messages des autres dès qu’ils arrivent (lecture d’une ligne sur le socket, bloquante aussi). Un seul thread ne peut pas attendre les deux en même temps : chaque client utilisera donc un thread qui exécute une fonction void *receive_messages(void *arg), chargée d’afficher les messages reçus dans le terminal, pendant que le thread principal gère la saisie.

  • le thread de réception lit avec le même FILE *in que le thread principal (passé en argument de pthread_create) : ne créez pas un second fdopen, les lignes déjà lues en avance dans le buffer du premier FILE * seraient perdues ;

  • pour lire les lignes reçues, le thread de réception utilise getline (vue dans les exercices de la partie 2), qui agrandit son buffer selon la longueur de la ligne : une ligne diffusée (pseudo + message) peut dépasser BUF_SIZE. Avec fgets, une telle ligne arriverait en deux lectures, et l’affichage facultatif ci-dessous (\r\033[2K) effacerait à l’écran le premier morceau en affichant le second :

    char *line = NULL; // alloué et agrandi par getline
    size_t size = 0;
    while (getline(&line, &size, in) != -1) {
        printf("%s", line); // line contient le '\n'
    }
    free(line);
    
  • une variable globale atomique (atomic_int, voir le chapitre sur les threads) permet au thread de réception d’indiquer au thread principal que le serveur a fermé la connexion (getline retourne -1). (Facultatif) Une seconde variable atomique peut indiquer au thread de réception que l’utilisateur quitte (Ctrl+D ou /quit), pour qu’il ne réaffiche pas l’invite > ;

  • pour quitter (Ctrl+D ou /quit), le thread principal appelle shutdown(sock_fd, SHUT_WR) : le serveur voit la fin de connexion et ferme à son tour, ce qui termine le thread de réception. Il suffit ensuite de l’attendre avec pthread_join ;

  • (Facultatif) pour afficher un message reçu sans le mélanger avec l’invite >, le thread de réception peut revenir en début de ligne (\r) et l’effacer (\033[2K) avec printf("\r\033[2K%s", message), puis réafficher l’invite. Cela efface aussi à l’écran une saisie en cours : les caractères déjà tapés ne sont plus visibles mais restent dans le buffer du terminal, et partiront avec la prochaine Entrée (limite acceptée ici).

Client de chat : un thread principal pour la saisie et l'envoi, un thread de réception pour l'affichage, et des variables atomiques entre les deux.

Vous pouvez tester la communication avec votre voisin : le serveur écoute déjà sur toutes ses interfaces grâce à INADDR_ANY, il suffit de remplacer ADDRESS côté client par l’adresse IP de la machine du serveur. Pour trouver cette adresse, sur la machine du serveur (ip addr liste les interfaces réseau et leurs adresses) :

$ ip -4 addr show | grep "inet"

Prendre l’adresse qui n’est pas 127.0.0.1. Normalement, elle est de la forme 192.168.X.X (réseau privé domestique/entreprise), 10.X.X.X (réseau privé) ou 172.16.X.X à 172.31.X.X (réseau privé). Attention, un pare-feu peut bloquer le port 8080.

Résultat :

$ gcc -std=c2x -Wall -Wextra -pedantic -g -pthread p6/p6e5_serveur.c -o serveur && ./serveur
[Serveur] Démarrage sur le port 8080...
[Serveur] Commandes disponibles : /ping, /time, /upper <texte>, /lower <texte>, /quit
[Serveur] Serveur de chat - En attente de connexions (max 3 clients)...
[Serveur] azerty connecté depuis 127.0.0.1:40082
[Serveur] qwerty connecté depuis 127.0.0.1:40090
[Serveur] bepo connecté depuis 127.0.0.1:40104
[Serveur] Message reçu de azerty (7 octets) : salut !
[Serveur] Message reçu de qwerty (9 octets) : bonjour !
[Serveur] Message reçu de bepo (6 octets) : hola !
[Serveur] Message reçu de azerty (4 octets) : bye!
[Serveur] azerty déconnecté
[Serveur] Message reçu de qwerty (2 octets) : a+
[Serveur] Message reçu de qwerty (5 octets) : /quit
[Serveur] Commande /quit reçue, fermeture de la connexion
[Serveur] bepo déconnecté
^C
$ gcc -std=c2x -Wall -Wextra -pedantic -g -pthread p6/p6e5_client.c -o client && ./client
[Client] Connexion à 127.0.0.1:8080...
[Client] Connecté !
[Serveur] Bienvenue sur notre serveur !
[Serveur] Commandes disponibles :
  - /ping
  - /time
  - /upper <texte>
  - /lower <texte>
  - /quit

Entrez votre pseudo : azerty
Bienvenue azerty ! Commandes : /ping, /time, /upper, /lower, /quit
[Serveur] qwerty a rejoint le chat
[Serveur] bepo a rejoint le chat
> salut !
[qwerty] bonjour !
[bepo] hola !
> bye!
>       // <- [Ctrl+D]
[Client] Fin de saisie.
[Client] Déconnecté.
$ ./client
[Client] Connexion à 127.0.0.1:8080...
[Client] Connecté !
[Serveur] Bienvenue sur notre serveur !
[Serveur] Commandes disponibles :
  - /ping
  - /time
  - /upper <texte>
  - /lower <texte>
  - /quit

Entrez votre pseudo : qwerty
Bienvenue qwerty ! Commandes : /ping, /time, /upper, /lower, /quit
[Serveur] bepo a rejoint le chat
[azerty] salut !
> bonjour !
[bepo] hola !
[azerty] bye!
[Serveur] azerty a quitté le chat
> a+
> /quit
Bye
[Client] Déconnecté.
$ ./client
[Client] Connexion à 127.0.0.1:8080...
[Client] Connecté !
[Serveur] Bienvenue sur notre serveur !
[Serveur] Commandes disponibles :
  - /ping
  - /time
  - /upper <texte>
  - /lower <texte>
  - /quit

Entrez votre pseudo : bepo
Bienvenue bepo ! Commandes : /ping, /time, /upper, /lower, /quit
[azerty] salut !
[qwerty] bonjour !
> hola !
[azerty] bye!
[Serveur] azerty a quitté le chat
[qwerty] a+
[Serveur] qwerty a quitté le chat
>       // <- [Ctrl+D]
[Client] Fin de saisie.
[Client] Déconnecté.

Si le serveur s’arrête (Ctrl+C), le thread de réception l’affiche ; le thread principal, bloqué dans fgets, s’arrête à la saisie suivante :

[Client] Serveur déconnecté (Entrée pour quitter)
        // <- [Entrée]
[Client] Déconnecté.

p6e6 - Serveur avec fonctionnalités avancées

Cet exercice est facultatif : les fonctionnalités sont indépendantes, choisissez celles que vous voulez. De la plus simple à la plus difficile : /list, historique, horodatage, couleurs, /nick, /msg.

Ajouter les fonctionnalités suivantes au serveur (le client de p6e5 peut être réutilisé tel quel) :

  1. /list : renvoyer la liste des pseudos connectés, un par ligne.

  2. /msg <pseudo> <message> : envoyer un message privé à un utilisateur, précédé de [MP de <expéditeur>]. L’expéditeur reçoit Message privé envoyé à <pseudo>, ou Utilisateur '<pseudo>' non trouvé si personne n’a ce pseudo. Sans pseudo ou sans message (/msg, /msg bob), répondre Usage : /msg <pseudo> <message>. Pour séparer le pseudo du message, copier les arguments (ce qui suit "/msg ") dans un buffer, chercher le premier espace avec strchr et le remplacer par '\0' : le buffer contient alors le pseudo, et le message commence juste après l’espace.

  3. /nick <nouveau_pseudo> : changer de pseudo, avec les mêmes règles de validité qu’à la connexion (en particulier, un pseudo déjà utilisé est refusé). L’émetteur reçoit Pseudo changé de <ancien> en <nouveau>, ou le message d’erreur de la connexion si le pseudo est invalide. Sans pseudo (/nick seul ou /nick suivi d’un espace), répondre Usage : /nick <nouveau_pseudo>. Les autres clients sont prévenus : [Serveur] <ancien> a changé de pseudo en <nouveau>.

  4. Historique : enregistrer dans un fichier chat.log les connexions, déconnexions, changements de pseudo, messages et messages privés, chaque ligne précédée de l’heure (plusieurs threads écrivent dans ce fichier : le protéger par un mutex).

  5. Couleurs : le serveur ajoute des codes ANSI dans les messages qu’il envoie pour colorer (a) les messages de connexion et de déconnexion, (b) le pseudo de chaque utilisateur, avec une couleur différente par utilisateur.

  6. Horodatage : ajouter l’heure (HH:MM:SS) devant chaque message que le serveur transmet à un autre client (messages diffusés, connexions, déconnexions, changements de pseudo, messages privés). Utiliser localtime_r (version thread-safe de localtime) pour découper l’heure dans une struct tm (champs tm_hour, tm_min, tm_sec, etc.), puis strftime pour la mettre en forme :

    time_t now = time(NULL);
    struct tm tm;
    localtime_r(&now, &tm); // struct tm *localtime_r(const time_t *timep, struct tm *result);
    char timestamp[16];
    strftime(timestamp, sizeof(timestamp), "%H:%M:%S", &tm);
    
  7. Messages d’accueil : compléter le message de bienvenue (voir la sortie ci-dessous) et la ligne [Serveur] Commandes disponibles affichée au démarrage du serveur avec /list, /msg et /nick. Quand le pseudo est accepté, répondre Bienvenue <pseudo> ! Tapez vos messages ou utilisez les commandes.

Avertissement

Le client repère l’acceptation du pseudo avec strncmp(buffer, "Bienvenue", 9), et la fin du message de bienvenue avec la ligne "\n". La ligne de réponse au pseudo doit donc commencer par Bienvenue (code couleur éventuel après ce mot, pas devant), et la dernière ligne du message de bienvenue doit être exactement "\n", sans code couleur : sinon, le client ne reconnaît plus ces lignes et se bloque.

Commandes /list, /msg et /nick traitées par le thread serveur du client, avec accès au tableau des clients et à chat.log sous mutex.

Exemple (sans les couleurs) :

$ gcc -std=c2x -Wall -Wextra -pedantic -g -pthread p6/p6e6_serveur.c -o serveur && ./serveur
[Serveur] Démarrage sur le port 8080...
[Serveur] Commandes disponibles : /ping, /time, /upper <texte>, /lower <texte>, /list, /msg <pseudo> <message>, /nick <nouveau_pseudo>, /quit
[Serveur] Serveur de chat avancé - En attente de connexions (max 3 clients)...
[Serveur] Historique sauvegardé dans chat.log
[Serveur] alice connecté depuis 127.0.0.1:54204
[Serveur] bob connecté depuis 127.0.0.1:54208
[Serveur] Message reçu de alice (5 octets) : /list
[Serveur] Message reçu de alice (15 octets) : /msg bob coucou
[Serveur] Message reçu de alice (12 octets) : /msg zoe hey
[Serveur] Message reçu de alice (9 octets) : /nick bob
[Serveur] Message reçu de alice (12 octets) : /nick alicia
[Serveur] Message reçu de alicia (5 octets) : salut
[Serveur] alicia déconnecté
[Serveur] bob déconnecté
^C
$ gcc -std=c2x -Wall -Wextra -pedantic -g -pthread p6/p6e6_client.c -o client && ./client
[Client] Connexion à 127.0.0.1:8080...
[Client] Connecté !
[Serveur] Bienvenue sur notre serveur de chat !
[Serveur] Commandes disponibles :
  - /ping
  - /time
  - /upper <texte>
  - /lower <texte>
  - /list (liste des utilisateurs)
  - /msg <pseudo> <message> (message privé)
  - /nick <nouveau_pseudo> (changer de pseudo)
  - /quit

Entrez votre pseudo : alice
Bienvenue alice ! Tapez vos messages ou utilisez les commandes.
[15:34:46] [Serveur] bob a rejoint le chat
> /list
Utilisateurs connectés :
  - alice
  - bob
> /msg bob coucou
Message privé envoyé à bob
> /msg zoe hey
Utilisateur 'zoe' non trouvé
> /nick bob
Pseudo invalide (vide, avec espace, trop long ou déjà utilisé)
> /nick alicia
Pseudo changé de alice en alicia
> salut
>       // <- [Ctrl+D]
[Client] Fin de saisie.
[Client] Déconnecté.
$ ./client
[...]
Entrez votre pseudo : bob
Bienvenue bob ! Tapez vos messages ou utilisez les commandes.
[15:34:52] [MP de alice] coucou
[15:35:03] [Serveur] alice a changé de pseudo en alicia
[15:35:08] [alicia] salut
[15:35:12] [Serveur] alicia a quitté le chat
>

Contenu de chat.log :

[15:34:40] === Serveur démarré ===
[15:34:45] alice a rejoint le chat
[15:34:46] bob a rejoint le chat
[15:34:52] [MP] alice -> bob: coucou
[15:35:03] alice a changé de pseudo en alicia
[15:35:08] alicia: salut
[15:35:12] alicia a quitté le chat
[15:35:20] bob a quitté le chat