Un chat en Pharo : le client

GNU/Linux Magazine n° 189 | janvier 2016 | Stéphane Ducasse
Creative Commons
  • Actuellement 0 sur 5 étoiles
0
Merci d'avoir participé !
Vous avez déjà noté cette page, vous ne pouvez la noter qu'une fois !
Votre note a été changée, merci de votre participation !
Poursuivons la découverte de Pharo et de quelques-uns de ses principaux frameworks. Vous avez déjà fait connaissance avec Teapot permettant de concevoir des services. Dans cet article, vous allez étudier la construction de requêtes HTTP à l'aide de Zinc [1] et construire une interface graphique à l'aide de Spec.

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

Tags : Chat, Client, Pharo