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 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 TCP écho 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).

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).

  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

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, écrire une fonction int send_line(int fd, const char *text) qui envoie text puis "\n" avec send_all.

  • 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.

Côté serveur, après avoir accepté le client, le serveur envoie un message de bienvenue (une ligne), puis, dans une boucle infinie, il attend de recevoir une ligne, puis la renvoie telle quelle (sans 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.

Côté client, après le connect, le client reçoit et affiche le message de bienvenue du serveur. Puis, 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).

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é !
[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é.

(Facultatif) Côté client, vous pouvez effacer la ligne de saisie après 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 font disparaître les lignes de saisie envoyées de l’affichage (les saisies vides restent) :

$ 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é.
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 :

  • /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' : la retirer avant de l’envoyer ;

  • /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.

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) ;

  • si le texte de /upper ou /lower est vide, répondre Usage : /upper <texte> (ou /lower) : ne jamais envoyer de réponse vide ;

  • toupper et tolower attendent une valeur de unsigned char : é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 (7 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. 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.

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, de moins de PSEUDO_SIZE caractères, sans espace (utiliser strchr(pseudo, ' '), qui retourne NULL si la chaîne ne contient pas d’espace) et pas déjà utilisé par un autre client. S’il 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 ;

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

  • à la déconnexion du client, remettre sa case à fd = -1 (sous le mutex) avant de fermer le socket.

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> rejoint le chat à tous les autres clients.

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

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

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 (fgets sur le socket, bloquant aussi). Un seul fil d’exécution 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.

  • 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 (fgets retourne NULL).

  • 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.

  • 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.

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 -4 addr show | grep "inet"

Prendre l’adresse qui n’est pas 127.0.0.1. Normalement, elle commence par 192.168.X.X (réseau privé domestique/entreprise), 10.X.X.X (réseau privé) ou est comprise entre 172.16.X.X et 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:34884
[Serveur] Client 127.0.0.1:34890 déconnecté avant de donner son pseudo
[Serveur] azerty déconnecté
[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 rejoint le chat
[Serveur] bepo 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 rejoint le chat
[azerty] salut !
> bonjour !
[bepo] hola !
[azerty] bye!
[Serveur] azerty a quitté
> 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é
[qwerty] a+
[Serveur] qwerty a quitté
>       // <- [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)
[Client] Déconnecté.

p6e6 - Serveur avec fonctionnalités avancées

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

  1. /list : répondre 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. Pour séparer le pseudo du message, chercher le premier espace après /msg `` avec ``strchr.

  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é). 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 diffusé (messages, connexions, déconnexions, changements de pseudo, messages privés). Utiliser localtime_r (version thread-safe de localtime) pour découper l’heure, puis strftime(buffer, taille, "%H:%M:%S", &tm) pour la mettre en forme.

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] 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
[...]
Entrez votre pseudo : alice
Bienvenue alice ! Tapez vos messages ou utilisez les commandes.
[15:34:46] [Serveur] bob 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:47] [MP de alice] coucou
[15:34:48] [Serveur] alice a changé de pseudo en alicia
[15:34:48] [alicia] salut
[15:34:48] [Serveur] alicia a quitté
>

Contenu de chat.log :

[15:34:45] === Serveur démarré ===
[15:34:45] alice a rejoint le chat
[15:34:46] bob a rejoint le chat
[15:34:47] [MP] alice -> bob: coucou
[15:34:48] alice a changé de pseudo en alicia
[15:34:48] alicia: salut
[15:34:48] alicia a quitté le chat
[15:34:49] bob a quitté le chat