Ici, nous construirons un client de messagerie instantanée.
Le dépôt ChatClient
que nous dupliquerons contient déjà deux classes :
Chat
, sera le moteur de l'application ;ChatWindow
, servira d'interface graphique.Ces deux classes devront être complétées pour parvenir à un client fonctionnel.
La classe centrale du client, Chat
, devra être en mesure de dialoguer avec l'interface graphique qui sera bâtie autour d'elle.
La communication se fera à travers le mécanisme désormais bien connu des signaux et des slots.
Dans ce premier exercice, nous mettrons en place deux signaux, connected
et disconnected
,
qui seront émis lorsque le socket intégré à la classe sera respectivement connecté ou déconnecté.
Ce socket, de la classe QTcpSocket, nous permettra de communiquer avec le monde extérieur.
Chat
de façon à retransmettre les deux signaux.Le protocole de communication adopté par le serveur repose sur trois règles simples :
/
;#
.Opération | Chaîne envoyée au serveur | Réponse du serveur |
---|---|---|
Choisir un pseudonyme | Première chaîne envoyée au serveur après ouverture de la connexion. | En cas de réussite (pseudonyme valide et libre) :
|
Envoyer un message | Chaîne ne commençant pas par / |
Diffusion du message à tous les utilisateurs connectés (y compris l'émetteur initial) |
Envoyer un message privé | /private destinataire message |
En cas de réussite :
|
Obtenir la liste des utilisateurs connectés | /list |
#list pseudo1 pseudo2 … |
Changer de pseudonyme | /alias nouveau-pseudo |
En cas de réussite (pseudonyme valide et libre) :
|
Se déconnecter | /quit |
|
Se tromper de commande | /grrrr |
#error invalid_command |
Ce protocole est également disponible ici.
Une méthode de traitement dédiée à un type de commande précis est appelée "processeur".
La boucle de lecture mise en place dans le constructeur Chat
est chargée d'analyser les réponses du serveur.
Si le premier mot d'une réponse correspond à une commande connue, le reste du message sera traité par le processeur adéquat.
Sinon, la réponse sera traitée comme un message normal.
La recherche du processeur adéquat s'effectuera au sein du tableau associatif Chat::PROCESSORS
.
N'hésitez pas à consulter la documentation associée au patron de classe std::map.
L'invocation d'une méthode à partir d'un objet et d'un pointeur sur membre est évoquée ici et là.
Chat
.La classe ChatWindow
est également incomplète.
Dans un premier temps, nous nous occuperons de la zone de saisie des messages.
Nous l'intégrerons dans la mise en page de l'interface graphique puis nous permettrons l'envoi de message. Ceux-ci seront émis dès que l'utilisateur appuiera sur la touche "entrée". Cet événement, matérialisé par le signal returnPressed, déclenchera l'envoi du texte présent dans la zone puis son effacement.
ChatWindow
, insérez la zone de saisie avec QDockWidget.Dans un second temps, nous connecterons les signaux connected
et disconnected
émis par Chat
.
En particulier, à chaque connexion, l'utilisateur devra saisir le pseudonyme qui servira à l'identifier auprès des autres utilisateurs. Cette saisie s'effectuera via QInputDialog::getText. Si le pseudonyme choisi est disponible, le serveur lui retournera un message validant ce choix ainsi que la liste des utilisateurs connectés. Sinon, un message d'erreur précédera une déconnexion pure et simple.
connected
, la saisie et l'envoi du pseudonyme.disconnected
.Nous ajouterons ensuite à la classe Chat
les processeurs et signaux manquants.
Commande | Nom du signal |
---|---|
#alias |
alias |
#connected |
user_connected |
#disconnected |
user_disconnected |
#renamed |
user_renamed |
#list |
user_list |
#private |
user_private |
Le processeur de la commande serveur #error
nous servira d'exemple pour les autres commandes.
Ensuite, dans ChatWindow
, chaque signal sera connecté à une lambda-expression
chargée de traiter les informations entrantes.
Pour l'instant, ces informations seront simplement retranscrites à l'écran sous forme textuelle.
ChatWindow
, gérez chaque type de signal émis par Chat
.Contrairement au client, le serveur n'utilise pas les bibliothèques Qt.
Il s'agit ici de code C++ standard, auquel est associé la biliothèque open source Asio
.
Cette bibliothèque est dite header only car elle n'a pas besoin d'être compilée ; il suffit d'inclure ses en-têtes dans son code pour pouvoir exploiter ses fonctionnalités.
La version 1.24.0 est disponible localement : /home/bib/aassif/asio-1.24.0/
.
Le projet ChatServer
ne dispose pas de fichier .pro
car il repose sur un Makefile
.
Il se compile en ligne de commande en lançant make
et génère l'exécutable server
.
Cet exercice repose également sur std::shared_ptr.
Comme nous faisons appel aux fonctions asynchrones de la bibliothèque Asio
,
nous devons veiller à ne jamais détruire un objet que nous avons associé à une lambda-expression
sous peine de voir notre serveur interrompre brutalement son exécution.
En C++, la façon la plus simple de pallier ce problème est d'avoir recours à des pointeurs intelligents.
Ces pointeurs peuvent être de différentes natures : uniques, partagés ou faibles.
En l'occurrence, nous utilisons des pointeurs partagés dont l'allocation passe par une fonction dédiée, std::make_shared, et dont la destruction n'est déclenchée qu'une fois le dernier objet y faisant référence détruit.
Pour en revenir au serveur, celui-ci est modélisé par une classe Server
déclarée et définie dans un même fichier server.hpp
.
Cette classe abrite une classe interne Client
qui représente un client vu du serveur.
Pour la raison évoquée précédemment, les clients seront manipulés via des pointeurs intelligents.
La classe Server
repose sur trois attributs d'instances privés :
m_context
,
de type asio::io_context,
le contexte d'exécution asynchrone d'entrée/sortie ;m_acceptor
,
de type asio::ip::tcp::acceptor,
chargé d'attendre des connexions entrantes ;m_clients
,
de type std::list<ClientPtr>
,
la liste des clients.Elle incorpore aussi un attribut de classe, Server::PROCESSORS
, similaire à Chat::PROCESSORS.
Son code, largement incomplet, requiert de nombreuses modifications :
find
est vide alors qu'elle devrait chercher et retourner
le client correspondant à l'alias passé en paramètre ;broadcast
, chargée de diffuser un message à tous les utilisateurs, l'est également ;process
, il manque le code chargé de trouver le processeur adéquat
parmi ceux présents dans le tableau associatif Server::PROCESSORS
;/quit
est gérée, tous les autres processeurs sont absents !La classe Client
déclare, elle, cinq champs privés :
m_server
, pointeur sur le serveur parent ;m_socket
,
de type asio::ip::tcp::socket ;m_buffer
, de type asio::streambuf,
le tampon de lecture local ;m_alias
,
de type std::string,
le pseudo associé au client ;m_active
, booléen indiquant si le serveur doit attendre des commandes de ce client.Son code est quasiment complet : seule la méthode start
doit être revue.
Le fonctionnement de cette méthode est linéaire mais néanmoins chargé. Elle doit :
#alias
,#list
),#connected
),Les méthodes find
, broadcast
et process_list
seront utilisées dans la méthode start
du client.
Server::find
.Server::broadcast
.Server::process
.Server::process_list
.Server::Client::start
.