CSC 4103 – Programmation système

Portail informatique
L'objectif de cette séance est de réviser toutes les notions vues au cours du module. Il s'agit également de préparer le contrôle final qui se basera sur le travail fait pendant cette séance.

Le but de cet exercice est de mettre œuvre deux programmes (un client et un serveur) permettant à plusieurs utilisateurs de discuter en mode texte. Il s'agit d'un exemple typique de client/serveur IRC.

Pour commencer, téléchargez l'archive TP10_sujet.tgz et extrayez la.

Cette archive contient le code d'un client et d'un serveur communicant en utilisant un protocole proche du protocole IRC. Les fichiers de cette archive sont:

  • server.c : code l'application serveur
  • server_code_etudiant.c : code à compléter pour faire fonctionner le serveur
  • server_code_etudiant.h : entête du fichier server_code_etudiant.c
  • client.c : code de l'application client
  • client_code_etudiant.c : code à compléter pour faire fonctionner le client
  • client_code_etudiant.h : entête du fichier client_code_etudiant.c
  • default_names.txt : liste de noms à assigner aux utilisateurs (crédit: Les Fatals Picards)

Les fichiers server.c et client.c ne doivent pas être modifiés.

Pour faciliter la compilation du projet, il manque un fichier Makefile.

Créez un fichier Makefile permettant de créer :

  • l'exécutable server à partir des fichiers server.c et server_code_etudiant.c
  • l'exécutable client à partir des fichiers client.c et client_code_etudiant.c

Ajoutez également une règle clean permettant de faire le ménage (supprimer les deux exécutables et les fichiers .o).

Pour commencer, on souhaite mettre en œuvre la commande /me. Cette commande permet d'indiquer qu'un utilisateur effectue une action. Par exemple:

[Anonymous_11] Ca roule ? [Anonymous_12] Ca va. Et toi ? * Anonymous_11 galère avec l'exercice de révision :'(

Ici, l'utilisateur Anonymous_11 a saisi la commande: /me galère avec l'exercice de révision :'(

La syntaxe de la commande /me est la suivante:

/me [message]

Dans le fichier server_code_etudiant.c, complétez la fonction handle_incoming_cmd pour qu'elle appelle la fonction void process_cmd_me(client_t* client, char* param); lorsque le client envoie la commande /me.

Écrivez également la fonction process_cmd_me.

void process_cmd_me(client_t* client, char* param) { char buff_out[1024]; sprintf(buff_out, " *%s %s\n", client->name, param); send_message_all(buff_out); } [...] if(!strcmp(command, "/quit")){ return; } else if(!strcmp(command, "/ping")) { process_cmd_ping(cli, cmd_line); } else if(!strcmp(command, "/me")) { process_cmd_me(cli, cmd_line); } [...]

La gestion des clients connectés au serveur se fait, pour l'instant, avec un tableau de client_t*. Pour envoyer un message à tous les clients, il est alors nécessaire de parcourir l'ensemble du tableau. Ensuite, pour chaque case, il fautvérifier si la case contient un client.

Éditez le fichier server_code_etudiant.c pour utiliser une liste chaînée à la place du tableau.

server_code_etudiant_list.c

Lorsqu'un client se connecte, le serveur lui assigne un nom. Par défaut, le client est nommé anonymous_X (où X est le numéro de client).

Modifiez la fonction assign_default_name pour que le nom soit choisi de manière aléatoire parmi les lignes du fichier default_names.txt. On vous rappelle que la fonction int rand(void) permet de tirer le prochain nombre aléatoire alors que l'expression srand(time(0)); permet de choisir de façon aléatoire l'élément initial de la suite aléatoire.

Pensez à vérifier que le nom choisi n'est pas déjà assigné à un autre utilisateur. void assign_default_name(client_t* cli) { // sprintf(cli->name, "Anonymous_%d", cli->uid); FILE*f = fopen("default_names.txt", "r"); if(!f) { perror("fopen failed"); abort(); } /* compute the number of lines in the file */ fseek(f, 0, SEEK_END); int index_end = ftell(f); int nb_lines=index_end/MAX_NAME_LENGTH; int selected_line; char new_name[MAX_NAME_LENGTH]; do { /* read one line randomly */ selected_line = rand() % nb_lines; fseek(f, selected_line*MAX_NAME_LENGTH*sizeof(char), SEEK_SET); if(fgets(new_name, MAX_NAME_LENGTH, f) == NULL) { perror("fgets failed"); abort(); } strip_newline(new_name); /* check if the name is already assigned to a client */ } while(get_client_from_name(new_name)); /* assign the name to the client */ strncpy(cli->name, new_name, MAX_NAME_LENGTH); printf("new name: %s (num %d)\n", cli->name, selected_line); fclose(f); }

On souhaite maintenant définir la fonction /names qui demande au serveur de retourner à un client la liste des utilisateurs connectés. Par exemple, l'utilisateur Anonymous_11 peut obtenir l'affichage suivant:

(chatroom) $ /names * There are 3 clients * client 19877 | Anonymous_11 * client 19863 | Yvan_le_hareng * client 19848 | Théophile_la_drosophile (chatroom) $

La syntaxe de la commande /names est la suivante:

/names

Dans le fichier server_code_etudiant.c, complétez la fonction handle_incoming_cmd pour qu'elle appelle la fonction void process_cmd_names(client_t* client, char* param); lorsque le client envoie la commande /names.

Écrivez aussi la fonction process_cmd_names.

/* Send list of active clients */ void send_active_clients(client_t *client){ int i; for(i=0;i<MAX_CLIENTS;i++){ if(clients[i]){ sprintf(s, " * client %d | %s\n", clients[i]->uid, clients[i]->name); send_message(s, client); } } } void process_cmd_names(client_t* client) { char buff_out[1024]; sprintf(buff_out, "* There are %d clients\n", cli_count); send_message(buff_out, client); send_active_clients(client); } [...] } else if(!strcmp(command, "/me")) { process_cmd_me(cli, cmd_line); } else if(!strcmp(command, "/names")) { process_cmd_names(cli); } else { /* /help or unknown command */ process_cmd_help(cli); } [...]

Si un grand nombre d'utilisateurs est connecté au serveur, l'envoi de la liste des utilisateurs risque de prendre beaucoup de temps. Le serveur ne pourra alors pas répondre rapidement aux requêtes envoyées par les autres clients.

Fort heureusement, les machines modernes sont équipées de plusieurs coeurs et peuvent donc faire plusieurs choses en parallèle. Modifiez la fonction process_cmd_names pour qu'elle crée un nouveau processus qui se chargera du traitement de la requête. Lorsque le traitement de la requête sera terminé, pensez à terminer le processus fils (avec la fonction exit(EXIT_SUCCESS)).

void process_cmd_names(client_t* client) { if(! fork()) { char buff_out[1024]; sprintf(buff_out, "* There are %d clients\n", cli_count); send_message(buff_out, client); send_active_clients(client); exit(EXIT_SUCCESS); } }

On souhaite maintenant que les clients soient prévenus lorsque le serveur se termine.

Modifiez le serveur pour qu'il appelle la fonction server_finalize lorsqu'il reçoit le signal SIGINT ou SIGTERM.

Écrivez ensuite la fonction server_finalize. Cette fonction doit envoyer à tous les clients le message "* The server is about to stop", et terminer le processus avec le code de retour EXIT_SUCCESS.

void server_init() { [...] struct sigaction s; s.sa_handler = server_finalize; s.sa_flags = 0; s.sa_mask = 0; sigaction(SIGINT, &s, NULL); sigaction(SIGTERM, &s, NULL); [...] } void server_finalize(int signo) { char buff_out[1024]; sprintf(buff_out, "* The server is about to stop.\n"); send_message_all(buff_out); exit(EXIT_SUCCESS); }