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 :
Créer un socket TCP (
AF_INET,SOCK_STREAM).Activer l’option
SO_REUSEADDR(voir le cours) pour pouvoir relancer le serveur tout de suite sans l’erreurAddress already in use.Lier (
bind) le socket à toutes les interfaces (INADDR_ANY) sur le port8080.Mettre le socket en écoute avec un backlog de 5.
Accepter une seule connexion client.
Lire les données reçues du client (buffer de 1024 octets) ;
recvn’ajoute pas de'\0'(voir le cours).Renvoyer au client ce qu’il a envoyé (écho).
Fermer proprement la connexion en suivant le « patron de fermeture conseillé » du cours (
shutdown, lecture jusqu’à EOF, puisclose).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 :
Créer un socket TCP.
Se connecter au serveur sur
127.0.0.1:8080.Envoyer le message
"Bonjour serveur !"au serveur.Recevoir l’écho du serveur (le même message) et l’afficher.
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é.
Aide : squelette des deux fichiers
// p6e1_serveur.c (avec l'en-tête du début de la page)
int send_all(int fd, const char *buf, size_t len) {
// copiée du cours
}
int main(void) {
// 1. socket -> fd (socket d'écoute)
// 2. setsockopt : SO_REUSEADDR
// 3. struct sockaddr_in server_addr (INADDR_ANY, PORT), puis bind
// 4. listen
// 5. accept -> client_fd ; afficher l'adresse et le port du client
// 6. recv sur client_fd, ajouter le '\0', afficher le message
// 7. send_all sur client_fd : renvoyer les octets reçus
// 8. shutdown(client_fd, SHUT_WR), recv jusqu'à 0, close(client_fd)
// 9. close(fd)
return 0;
}
// p6e1_client.c (avec l'en-tête du début de la page)
int send_all(int fd, const char *buf, size_t len) {
// copiée du cours
}
int main(void) {
// 1. socket -> fd
// 2. struct sockaddr_in server_addr (PORT, inet_pton de ADDRESS)
// 3. connect
// 4. send_all : "Bonjour serveur !"
// 5. recv, ajouter le '\0', afficher la réponse
// 6. shutdown(fd, SHUT_WR), recv jusqu'à 0, close(fd)
return 0;
}
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 envoietextpuis"\n"avecsend_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 avecfgets(buffer, BUF_SIZE, in):NULLsignifie que le pair a fermé la connexion (ou une erreur, à distinguer avecferror(in)). En fin de programme,fclose(in)ferme aussi le socket. Un seulFILE *par socket : on lit avecin, on écrit avecsend_linesur 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).
Pourquoi BUF_SIZE - 1 ?
La ligne lue fait alors au plus BUF_SIZE - 2 caractères et, avec le '\n' ajouté par send_line, elle tient dans le fgets(buffer, BUF_SIZE, in) du serveur.
Avec BUF_SIZE, une saisie de BUF_SIZE - 1 caractères ou plus arriverait en deux lectures côté serveur, la seconde ne contenant que le '\n' : un message vide en trop, et des réponses décalées d’un cran.
Une saisie trop longue part simplement en plusieurs messages.
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é.
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: renvoyerpong;/time: renvoyer l’heure courante. Utilisertime_t now = time(NULL);puischar *time_str = ctime(&now);.ctimerenvoie 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 appliquertoupper, de<ctype.h>) ;/lower <texte>: transformer le texte en minuscules (tolowersur chaque caractère) ;/quit: répondreBye, 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 avecstrncmp(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
/upperou/lowern’a pas de texte ("/upper"seul ou"/upper "suivi d’un espace), répondreUsage : /upper <texte>(ou/lower) : ne jamais envoyer de réponse vide. Tester ces deux cas avecstrcmpavant lestrncmp;toupperettolowerattendent une valeur représentable enunsigned char(ouEOF) : écriretoupper((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é.
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 :
Un thread par client : reprenez le serveur de p6e3, mettez
acceptdans une boucle, et déplacez le dialogue avec le client dans une fonctionvoid *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 deuxnc 127.0.0.1 8080) connectés en même temps.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).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à-1dans 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, envoyerServeur 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_createen 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 avecpthread_join: le détacher avecpthread_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 - 1caractères (il faut garder la place du'\0') ;sans espace (utiliser
strchr(pseudo, ' '), qui retourneNULLsi 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 ; retourne1si le pseudo est accepté,0sinon. 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 queset_pseudoaccepte un pseudo ; retourne1si un pseudo a été accepté,0si le client s’est déconnecté avant ;
ctimerenvoie un buffer statique partagé par tous les threads : dans un programme multi-threadé, utiliserctime_r(&now, time_str)avec un bufferchar time_str[26];à la déconnexion du client, remettre sa case à
fd = -1(sous le mutex) avant de fermer le socket : dès leclose, le noyau peut redonner ce numéro de descripteur au prochainaccept(il attribue toujours le plus petit numéro libre), et deux cases du tableau contiendraient alors le mêmefd.
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é
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_linedoit envoyer le texte et son'\n'en un seulsend_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 avecsnprintf, 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_allpeut faire plusieurssend(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,
broadcastpeut lui envoyer des messages. Si la ligneBienvenue ...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 ligneBienvenue ...sous le mutex, juste après avoir écrit le pseudo. Remarque :broadcastenvoie lui aussi sous le mutex. Unsendpeut 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.
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 *inque le thread principal (passé en argument depthread_create) : ne créez pas un secondfdopen, les lignes déjà lues en avance dans le buffer du premierFILE *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épasserBUF_SIZE. Avecfgets, 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 (getlineretourne-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 appelleshutdown(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 avecpthread_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) avecprintf("\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).
Aide : structure du client
atomic_int server_disconnected = 0; // mis à 1 par le thread de réception
void *receive_messages(void *arg) {
FILE *in = arg; // le même FILE * que le thread principal
// boucle getline sur in : afficher chaque ligne reçue
// getline retourne -1 : le serveur a fermé la connexion
// free(line), puis atomic_store(&server_disconnected, 1)
return NULL;
}
int main(void) {
// connexion et FILE *in = fdopen(sock_fd, "r"), comme en p6e4
// message de bienvenue et choix du pseudo, comme en p6e4
// pthread_create(&recv_thread, NULL, receive_messages, in)
// boucle : fgets sur stdin, send_line
// sortir sur Ctrl+D, après /quit, ou si server_disconnected
// shutdown(sock_fd, SHUT_WR) : le serveur ferme à son tour,
// getline retourne -1 et le thread de réception se termine
// pthread_join(recv_thread, NULL), puis fclose(in)
}
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) :
/list: renvoyer la liste des pseudos connectés, un par ligne./msg <pseudo> <message>: envoyer un message privé à un utilisateur, précédé de[MP de <expéditeur>]. L’expéditeur reçoitMessage privé envoyé à <pseudo>, ouUtilisateur '<pseudo>' non trouvési personne n’a ce pseudo. Sans pseudo ou sans message (/msg,/msg bob), répondreUsage : /msg <pseudo> <message>. Pour séparer le pseudo du message, copier les arguments (ce qui suit"/msg ") dans un buffer, chercher le premier espace avecstrchret le remplacer par'\0': le buffer contient alors le pseudo, et le message commence juste après l’espace./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çoitPseudo changé de <ancien> en <nouveau>, ou le message d’erreur de la connexion si le pseudo est invalide. Sans pseudo (/nickseul ou/nicksuivi d’un espace), répondreUsage : /nick <nouveau_pseudo>. Les autres clients sont prévenus :[Serveur] <ancien> a changé de pseudo en <nouveau>.Historique : enregistrer dans un fichier
chat.logles 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).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.
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). Utiliserlocaltime_r(version thread-safe delocaltime) pour découper l’heure dans unestruct tm(champstm_hour,tm_min,tm_sec, etc.), puisstrftimepour 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);
Messages d’accueil : compléter le message de bienvenue (voir la sortie ci-dessous) et la ligne
[Serveur] Commandes disponiblesaffichée au démarrage du serveur avec/list,/msget/nick. Quand le pseudo est accepté, répondreBienvenue <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.
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