Pharo permet la définition en quelques dizaines de lignes un server REST grâce au package Teapot qui étend Zinc, le superbe client/server HTTP de Pharo développé par la société BetaNine et offert gracieusement à la communauté. L'objectif de ce chapitre est de vous faire développer en cinq classes, une application de chat client/server avec un client graphique. Cette petite aventure vous permettra de vous familiariser avec Pharo et de voir l'aisance avec laquelle un server REST peut être défini. Développée en quelques heures, TinyChat a été conçu comme une application pédagogique. Dans cet objectif, à la fin de l'article nous proposons une liste d'améliorations possibles.
Nous allons donc construire un serveur de discussion (chat) et un client permettant de s'y connecter (voir figure 46.1).
La communication entre le client et le serveur sera basée sur HTTP et REST.
En plus des classes TCServer
et TinyChat
(le client), nous définirons trois autres classes : la classe TCMessage
qui représente les messages échangés (dans le future vous pourrez étendre TinyChat pour échanger des éléments plus structurés comme JSON ou STON le format textuel Pharo.), la classe TCMessageQueue
qui stocke les messages et TCConsole
l'interface graphique.
Vous pouvez charger Teapot en utilisant le Configuration Browser qui se trouve dans le menu Tools du menu principal. Sélectionnez Teapot et utilisez "Install Stable". Une autre solution consiste à utiliser le script suivant :
Un message est un objet très simple avec un texte et un identifiant pour l'émetteur.
Nous définissons la classe TCMessage
dans le package TinyChat
.
Les variables d'instances sont les suivantes:
sender
: le login de l'expéditeur,text
: le texte du message,separator
: un caractère séparateur pour l'affichage.Nous créons les accesseurs suivants:
La méthode initialize
définit la valeur du caractère séparateur.
La méthode de classe TCMessage class>>from:text:
permet d'instancier un message :
Le message yourself
rend le receveur du message : c'est une manière de s'assurer que le nouvel objet créé sera
bien retourné par le message from:text:
et non le résultat du message text:
.
Nous ajoutons une méthode printOn:
pour transformer le message en une chaîne de caractères.
Le modèle de la chaîne est sender-separator-text-crlf. Exemple: 'john>hello !!!'.
La méthode printOn:
est invoquée par la méthode printString
. Il est important de comprendre que la méthode printOn:
est invoquée par les outils tels que le débogueur ou l'inspecteur d'objets.
Nous devons également définir deux méthodes pour créer un message à partir d'une chaine, ayant la forme: 'olivier>tinychat est cool'
.
Tout d'abord, créons une méthode de classe qui sera invoquée de la manière suivante: TCMessage fromString: 'olivier>tinychat est cool'
,
puis la méthode d'instance remplissant les variables de l'objet préalablement créé.
Maintenant nous sommes prêts pour définir le serveur.
Pour le serveur, nous allons définir une classe pour gérer une queue de messages. Ce n'est pas vraiment nécessaire mais cela permet de bien identifier les responsabilités.
Créez la classe TCMessageQueue
dans le package TinyChat-Server.
La variable d'instance messages
est une collection ordonnée donc le contenu est composé d'instances de TCMessage
.
Une OrderedCollection est une collection qui s'agrandit dynamiquement lors d'ajouts.
On doit pouvoir ajouter un message add:
, effacer la liste avec reset
et connaître le nombre de messages avec size
.
Lorsqu'un client demande au serveur la liste des derniers messages échangés, il indique au serveur l'index du dernier message qu'il connaît. Le serveur répond alors la liste des messages reçus depuis cet index.
La classe TCMessageQueue
doit pouvoir formater une liste de messages (à partir d'un index) en une chaîne de caractères que le serveur pourra transmettre au client.
On ajoute ensuite une méthode à la classe TCMessageQueue
pour construire une seule chaine de caractères à partir de chaque chaîne de caractères produite par chaque message :
Le coeur du serveur est basé sur le framework REST Teapot, permettant l'envoi et la réception des messages. Il maintient en plus une liste de messages qu'il communique aux clients.
La variable d'instance messagesQueue
référence la liste des messages reçus et envoyés par le serveur.
La variable d'instance teapotServer
référence l'instance du serveur TeaPot que l'on créée à l'aide de la méthode initializePort:
Le routage HTTP est défini dans la méthode registerRoutes
. Trois opérations sont définies :
messages/count
: retourne au client le nombre de messages reçus par le serveur,messages/<id:IsInteger>
: le serveur retourne les messages à partir de l'index indiqué dans la requête HTTP,/message/add
: le client envoie un message au serveur.
Nous exprimons ici que le chemin message/count va donner lieu à l'execution du message messageCount
sur le serveur lui-même.
Le pattern <id:IsInteger>
indique que l'argument doit être exprimé sous forme de nombre et qu'il sera converti en un entier.
La gestion des erreurs est construite dans la méthode registerErrorHandlers
. Ici on voit comment on construit une instance de la classe TeaResponse
.
Le démarrage du serveur est confié à la méthode TCServer class>>startOn:
qui reçoit le numéro de port TCP en paramètre.
Il faut également gérer l'arrêt du serveur. La méthode stop
met fin à l'exécution du serveur TeaPot et vide la liste des message.
Comme il est probable que vous executiez plusieurs fois l'expression TCServer startOn:
, nous définissons la méthode de classe stopAll
qui stoppe tous les serveurs
en récuperant toutes les instance de la classe. La méthode TCServer class>>stopAll
demande l'arrêt de chaque instance du serveur.
La méthode addMessage
extrait de la requête du client le message posté. Elle ajoute à la liste des messages une nouvelle instance de TCMessage
.
La méthode messageCount
retourne le nombre de messages reçus.
La méthode messageFrom:
retourne la liste des messages reçus par le serveur depuis l'index indiqué par le client. Les messages sont retournés au client
sous la forme d'une chaine de caractères. Ce point sera définitivement à améliorer.
Nous en avons fini avec le server. Nous pouvons maintenant le tester un peu. Commençons par le lancer :
Maintenant nous pouvons soit vérifier avec un navigateur web (figure 46.2), soit à l'aide du client/serveur Zinc disponible par défaut dans Pharo.
Les amateurs du shell peuvent également utiliser la commande curl
Nous pouvons aussi ajouter un message de la manière suivante :
Maintenant, nous pouvons nous concentrer sur la partie client de TinyChat. Le client se compose de deux classes:
TinyChat
est la classe contenant la logique métier (connexion, envoi et réception des messages),TCConsole
est une classe définissant l'interface graphique.La logique du client est la suivante:
De plus, lorsque le client transmet un message au serveur, il en profite pour également lire les messages échangés depuis sa dernière connexion.
Nous créons la classe TinyChat
dans le package TinyChat-client
.
Cette classe définit les variables suivantes:
Nous initialisons les variables qui le nécessitent dans la méthode initialize
suivante.
Nous définissons les méthodes pour communiquer avec le serveur. Elles respectent le protocole HTTP.
Deux méthodes permettent de formater la requête. L'une n'a pas d'argument et
permet de construire les requêtes /messages/add
et /messages/count
. L'autre a un
argument qui est utilisé pour la lecture des messages à partir d'une position.
Il suffit ensuite de définir les trois commandes HTTP du client:
Nous avons besoin d'émettre ces commandes et de pouvoir récupérer des informations à partir du serveur.
Pour cela, nous définissons deux méthodes. La méthode readLastMessageID
retourne l'index du dernier
message reçu par le serveur.
La méthode readMissingMessages
ajoute les derniers messages reçus par le serveur à la
liste des messages connus par le client. Cette méthode retourne le nombre de messages récupérés.
Nous sommes prêt à définir le comportement de rafraichissement du client avec la méthode refreshMessages
.
Elle utilise un processus léger pour lire à intervalle régulier les messages reçus par le serveur. Le délai est fixé à deux secondes.
Le message fork
envoyé à un bloc (une fermeture lexical en Pharo) exécute ce bloc dans un processus léger. La logique est
de boucler tant que le client ne spécifie pas que veut s'arrêter via la variable exit
.
L'expression (Delay forSeconds: 2) wait
suspend l'exécution du processus léger dans lequel elle se trouve pendant un certain
nombre de secondes.
La méthode sendNewMessage:
poste le message de l'utilisateur au serveur.
Cette méthode est utilisée par la méthode send:
qui reçoit en paramètre le texte saisi par l'utilisateur.
La chaine de caractères est alors convertie en une instance de TCMessage
. Le message est envoyé.
Le client met à jour l'index du dernier message connu et déclenche l'affichage du message dans l'interface graphique.
La déconnexion du client est gérée par la méthode disconnect
qui envoie un message au serveur pour signaler le
départ de l'utilisateur et met fin à la boucle de récupération périodique des messages.
Pour initialiser les paramètres de connexion, on définit une méthode de class TinyChat class>>connect:port:login:
.
Cette méthode permet de se connecter de la manière suivante : TinyChat connect: 'localhost' port: 8080 login: 'username'
Le code appelle la méthode host:port:login:
. Cette méthode met à jour la variable d'instance url
en construisant l'URL et
en affectant le nom de l'utilisateur à la variable d'instance login
.
La méthode start
envoie un message au serveur pour présenter l'utilisateur, récupèrer l'index du dernier message reçu par le serveur
et mettre à jour la liste des messages connus par le client.
C'est également cette méthode qui initialise l'interface graphique de l'utilisateur. Une évolution pourrait être de décoreller le modèle de son interface graphique en utilisant
une conception basée sur des évenements.
L'interface graphique est composée d'une fenêtre contenant une liste et un champ de saisie comme montré dans la figure 46.1.
La variable d'instance chat
est une référence à une instance de la classe TinyChat
et nécessite uniquement un accesseur en écriture.
Les variables d'instance list
et input
dispose chacune d'un accesseur en lecture. Ceci est imposé par Spec le constructeur d'interface.
L'interface graphique a un titre pour la fenêtre. Pour le définir, il faut écrire une méthode title
.
La méthode de classe TCConsole class>>attach:
reçoit en argument l'instance du client de chat avec lequel l'interface graphique
va être utilisée. Cette méthode déclenche l'ouverture de la fenêtre et met en place l'événement gérant la fermeture de celle ci et donc,
provoquant la déconnexion du client.
La méthode TCConsole class>>defaultSpec
définit la mise en page des composants contenus dans la fenêtre. Ici nous avons une colonne avec une liste
et un champ de saisie placé juste en dessous.
La méthode initializeWidgets
spécifie la nature et le comportement des composants graphiques. Ainsi le acceptBlock:
permet de définir l'action à exécuter lorsque le texte
est entré dans le champ de saisie. Ici nous l'envoyons à client et nous le vidons.
La méthode print
affiche les messages reçus par le client en les affectant au contenu de la liste.
Notez que cette méthode est invoquée par la méthode refreshMessages
et que changer tous les éléments de la liste
à chaque ajout d'un nouveau messages est peu élégant mais l'exemple se veut volontairement simple.
Voilà vous pouvez maintenant chatter avec votre serveur.
Nous avons montré que la création d'un serveur REST est extrêmement simple avec Teapot. La définition de TinyChat donne un cadre ludique à l'exploration de la programmation en Pharo et nous esperons que vous avez apprécié cette ballade. TinyChat est une petite application que nous avons développé de manière très simple afin de vous permettre de l'étendre et d'expérimenter. Voici une liste d'améliorations : gestion parcimonieuse des ajouts d'éléments dans la liste graphique, gestion d'accès concurrents dans la collection sur le serveur (en effet, si le serveur pouvait recevoir des requêtes concurrentes la structure de donnée utilisée n'est pas adéquate), gestion des erreurs de connexion, rendre les clients robustes à la fermeture du serveur, obtenir la liste des personnes connectées, pouvoir définir le délai de récupération des messages, utiliser JSON pour le transport des messages, afficher le nom de la personne connectée dans la fenêtre. Le projet est disponible à l'adresse http://www.smalltalkhub.com/#!/~olivierauverlot/TinyChat. A vous de jouer!