Nous allons nous concentrer sur la partie client de TinyChat. Pour la construire, nous allons simplement ajouter deux classes au projet :
- 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 :
- au lancement du client, celui-ci demande au serveur l'index du dernier message reçu ;
- toutes les deux secondes, le client se connecte au serveur pour lire les messages échangés depuis sa dernière connexion. Pour cela, il transmet au serveur l'index du dernier message dont il a eu connaissance.
De plus, lorsque le client transmet un message au serveur, il en profite également pour lire les messages échangés depuis sa dernière connexion.
1. La classe TinyChat
Nous créons la classe TinyChat dans le package TinyChat-client :
Object subclass: #TinyChat
instanceVariableNames: 'url login exit messages console lastMessageIndex'
classVariableNames: ''
category: 'TinyChat-client'
Cette classe définit les variables suivantes :
- url contient l'url HTTP permettant au client de se connecter au serveur ;
- login est une chaîne de caractères identifiant le client ;
- messages est une variable d'instance contenant les messages lus par le client ;
- lastMessageIndex est le numéro du dernier message lu par le client ;
- exit est une valeur booléenne. Tant que cette valeur est vraie, le client se connecte à intervalle régulier au serveur pour lire les messages échangés depuis sa dernière connexion ;
- console est une instance de TCConsole : une console graphique permettant à l'utilisateur de saisir et de consulter les messages.
Nous initialisons les variables qui le nécessitent dans la méthode initialize :
TinyChat >> initialize
super initialize.
exit := false.
LastMessageIndex := 0.
messages := OrderedCollection new.
2. Définir les commandes HTTP
Nous devons maintenant définir les méthodes permettant au client HTTP de communiquer avec le serveur.
Deux méthodes permettent d'assembler les chemins d'accès. L'une n'a pas d'argument et permet de construire les requêtes /messages/add et /messages/count. L'autre méthode a un argument qui est utilisé pour la lecture des messages à partir d'une position.
TinyChat >> command: aPath
^'{1}{2}' format: { url . APath }
TinyChat >> command: aPath argument: anArgument
^'{1}{2}/{3}' format: { url . aPath . anArgument asString }
Il suffit ensuite de définir les trois commandes HTTP du client :
TinyChat >> cmdLastMessageID
^ self command: '/messages/count'
TinyChat >> cmdNewMessage
^self command: '/messages/add'
TinyChat >> cmdMessagesFromLastIndexToEnd
"Returns the server messages from my current last index to the last one on the server."
^ self command: '/messages' argument: lastMessageIndex
3. Gérer les opérations 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 :
TinyChat >> readLastMessageID
| id |
id := (ZnClient new url: self cmdLastMessageID; get) asInteger.
id = 0 ifTrue: [ id := 1 ].
^ id
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 :
TinyChat >> readMissingMessages
"Gets the new messages that have been posted since the last request."
| response receivedMessages |
response := (ZnClient new url: self cmdMessagesFromLastIndexToEnd; get).
^ response
ifNil: [ 0 ]
ifNotNil: [
receivedMessages := response subStrings: (String crlf).
receivedMessages do: [ :msg | messages add: (TCMessage fromString: msg)].
receivedMessages size.
]
Fig. 1 : Lecture de l'historique des messages reçus par le serveur.
Nous sommes prêts à définir le comportement de rafraîchissement 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 lexicale en Pharo), exécute ce bloc dans un processus léger. La logique est de boucler tant que le client ne spécifie pas qu'il 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 :
TinyChat >> refreshMessages
[
[ exit ] whileFalse: [
(Delay forSeconds: 2) wait.
lastMessageIndex := lastMessageIndex + (self readMissingMessages).
console print: messages.
]] fork
La méthode sendNewMessage: poste le message de l'utilisateur au serveur :
TinyChat >> sendNewMessage: aMessage
^ ZnClient new
url: self cmdNewMessage;
formAt: 'sender' put: (aMessage sender);
formAt: 'text' put: (aMessage text);
post
Cette méthode est utilisée par la méthode du client send: qui reçoit en paramètre le texte saisi par l'utilisateur. La chaîne de caractères est alors convertie en une instance de TCMessage. Le message est ensuite envoyé. Le client met à jour l'index du dernier message connu et déclenche l'affichage du contenu du message dans l'interface graphique :
TinyChat >> send: aString
| msg |
msg := TCMessage from: login text: aString.
self sendNewMessage: msg.
lastMessageIndex := lastMessageIndex + (self readMissingMessages).
console print: messages.
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. Elle met fin à la boucle de récupération périodique des messages.
TinyChat >> disconnect
self sendNewMessage: (TCMessage from: login text: 'I exited from the chat room.').
exit := true
3.1 Fixer les paramètres du client
Pour initialiser les paramètres de connexion, on définit une méthode de classe 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 de cette méthode est donc :
TinyChat class >> connect: aHost port: aPort login: aLogin
^ self new host: aHost port: aPort login: aLogin; start
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 :
TinyChat >> host: aHost port: aPort login: aLogin
url := 'http://' , aHost , ':' , aPort asString.
login := aLogin
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 consister à séparer le modèle de son interface graphique en utilisant une conception basée sur des événements.
TinyChat >> start
console := TCConsole attach: self.
self sendNewMessage: (TCMessage from: login text: 'I joined the chat room').
lastMessageIndex := self readLastMessageID.
self refreshMessages.
4. Création de l'interface graphique
L'interface graphique est composée d'une fenêtre contenant une liste et un champ de saisie :
ComposableModel subclass: #TCConsole
instanceVariableNames: 'chat list input'
classVariableNames: ''
category: 'TinyChat-client'
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 disposent chacune d'un accesseur en lecture. Ceci est imposé par le constructeur d'interface Spec :
TCConsole >> input
^ input
TCConsole >> list
^ list
TCConsole >> chat: anObject
chat := anObject
L'interface graphique possède un titre pour la fenêtre. Pour le définir, il faut écrire une méthode title :
TCConsole >> title
^ 'TinyChat'
La méthode de classe TCConsole class>>attach: reçoit en argument l'instance du client de chat avec laquelle 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 ainsi que la déconnexion du client :
TCConsole class >> attach: aTinyChat
| window |
window := self new chat: aTinyChat.
window openWithSpec whenClosedDo: [ aTinyChat disconnect ].
^ window
La méthode TCConsole class>>defaultSpec définit la mise en page des composants contenus dans la fenêtre. Nous avons une colonne avec une liste et un champ de saisie placé juste en dessous :
TCConsole class >> defaultSpec
<spec: #default>
^ SpecLayout composed newColumn: [ :c |
c add: #list; add: #input height: 30 ]; yourself
Fig. 2 : L'interface graphique de TinyChat.
La méthode initializeWidgets spécifie la nature et le comportement des composants graphiques. Ainsi, le code passé à acceptBlock: permet de définir l'action à exécuter lorsque le texte est entré dans le champ de saisie. Nous l'envoyons à chat et nous effaçons son contenu lorsque l'utilisateur appuie sur la touche <Entrée>.
TCConsole >> initializeWidgets
list := ListModel new.
input := TextInputFieldModel new
ghostText: 'Type your message here...';
enabled: true;
acceptBlock: [ :string |
chat send: string.
input text: '' ].
self focusOrder add: input.
La méthode print: affiche les messages reçus par le client en les affectant au contenu de la liste.
TCConsole >> print: aCollectionOfMessages
list items: (aCollectionOfMessages collect: [ :m | m printString ])
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 message est peu élégant mais l'exemple se veut volontairement simple.
Conclusion
Et voilà ! Quelques classes regroupant quelques dizaines de lignes de code vous ont permis de construire un sympathique petit outil de chat. La définition de TinyChat donne un cadre ludique à l'exploration de la programmation en Pharo et nous espérons que vous avez apprécié cette ballade. TinyChat est une petite application que nous avons développée de manière très simple afin de vous permettre de l'étendre et d'expérimenter. Il y a de nombreuses améliorations possibles : 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ées utilisée n'est pas adéquat), 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 ou XML pour le transport des messages, afficher le nom de la personne connectée dans la fenêtre. Le projet est disponible sur le site de dépôt Smalltalkhub [3]. À vous de jouer!
Références
[1] Site officiel du projet Zinc: http://zn.stfx.eu/zn/index.html
[2] Site officiel du projet Pharo : http://www.pharo.org
[3] Télécharger le projet TinyChat : http://www.smalltalkhub.com/#!/~olivierauverlot/TinyChat
Pour aller plus loin
L'ouvrage collectif « Pharo par l'exemple », Square Bracket Associates, 2011
L'ouvrage collectif « Deep inside Pharo », Square Bracket Associates, 2013