Depuis la version 8 de Java, JavaFX devient le
standard officiel pour le développement des interfaces des
applications Java au détriment de Swing. La
grande nouveauté de JavaFX est de pouvoir
structurer nos applications à partir d'un modèle MVC.
Je rappelle que ce modèle permet de découpler la Vue
grâce à l'outil SceneBuilder et à FXML
du Contrôleur qui s'occupe de prendre en compte
les événements et de réaliser les traitements de fond au moyen
du langage Java. Le Modèle quant à lui ne gère que
la persistance des données. Afin de bien réaliser et de bien
maîtriser ces différents TPs, je vous invite à bien consulter
les différents cours préliminaires.
Premier contact - Hello World !
Pour ce premier projet, nous allons le réaliser uniquement
avec du code Java. L'objectif ici est de prendre contact
avec les différents composants graphiques qui existent dans
la librairie JavaFX. Vous remarquerez que les
noms des composants proposés sont les mêmes noms que les
composants originaux issus de la bibliothèque AWT
le préfixe J de Swing a été supprimé.
Mise à part cette histoire de noms, nous avons exactement le
même style de programmation que nous avons déjà développé
avec Swing.
Hello World ! avec le modèle MVC
Nous allons reprende le même projet, mais cette fois-ci
avec l'architecture MVC.
Nous en profiterons pour utiliser le logiciel SceneBuilder
qui nous permettra de concevoir l'interface graphique
uniquement à l'aide de la souris, avec bien entendu le
réglage des différentes propriétés. Cette fois-ci
l'arcitecture est un petit peu plus complexe puisque nous
devons mettre en oeuvre trois fichiers. Le premier est un
fichier au format XML dont l'extension est *.fxml
qui représente la Vue de l'application qui peut
être entièrement générée par SceneBuilder ou
directement en écrivant les balises. Le Contrôleur
est assurée par une classe Java qui doit implémenter
l'interface Initializable qui est en relation
directe grâce à l'annotation @FXML
avec les composants définis dans la Vue. Ce Contrôleur
assure le traitement à réaliser suivant les événements
répertoriés. Enfin, nous retrouvons le fichier correspondant
à l'application principale dont le seul but maintenant est
de prendre en compte la Vue qui se
trouve dans un autre fichier qui elle même se
mettra en relation avec le Contrôleur attribut
fx:controller de l'élément racine du fichier XML.
Conversion entre les €uros et les Francs
Pour ce projet, nous n'allons conforter nos nouvelles
connaissances sans rajouter de nouveaux concepts mais
simplement pour nous habituer à ce modèle de conception.
Nous réalisons un simple convertisseur monnétaire entre les
€uros et les Francs. L'intérêt ici est de prendre contact
avec un composant très fréquemment utilisé dans les IHM qui
permet de réaliser des saisies. Dans le cas de JavaFX, la
classe utilisée est un TextField.
Utilisation des Shape - JavaFX 2D
Durant cette étude, nous allons nous intéresser à
l'utilisation de formes toutes faites proposées par JavaFX
2D. Nous disposons d'un ensemble de composants qui
sont des formes géométriques, comme des cercles, des
rectangles, des ellipses, des polygones, etc. Toutes ces
formes sont implémentées au travers de classes spécifiques,
comme Circle, Rectangle, Ellipse, Polygon, etc.
Toutes ces classes héritent de la classe de base des formes
nommée Shape. Cette super-classe hérite elle-même de
la classe Node ce qui nous permet d'intégrer
n'importe quelle forme dans un panneau quelconque au même
titre que les autres composants, comme les boutons, les
libellés, etc. en utilisant la même démarche que nous
venons d'apprendre.
JavaFX 2D avec du tracé personnalisé (calque)
Dans cette nouvelle étude, nous allons toujours travailler
sur la technologie JavaFX 2D, toutefois, au lieu d'utiliser
des composants des noeuds déjà tout fait,
comme les clases Circle, Rectangle, etc., nous
allons tracer les formes qui nous intéressent sur un dessin,
au travers des classes spécifiques, comme Canvas qui
représente la feuille de dessin ou le calque suivant notre
point de vue. Le canevas peut ensuite être placé dans le
panneau au même titre que les autres composants. Le canevas
propose un contexte graphique GraphicsContext
qui permet de réaliser, à l'aide de méthodes spécifiques,
des tracés d'ellipse, de rectangle, de courbes de béziers,
etc.
Création d'un nouveau composant - les propriétés
Nous allons reprendre le projet précédent, mais cette
fois-ci notre démarche sera différente. Notre objectif sera
de créer un nouveau composant qui sera directement
exploitable par SceneBuilder avec de nouvelles
propriétés tout en conservant ce qui existe. Tous les
composants que nous plaçons dans notre fenêtre, pour qu'ils
soient visibles et exploitables, doivent tous hériter directement
ou indirectement de la classe Control.
Notre étude ici consiste à créer un composant capable de
réaliser des saisies de valeurs monétaires quelque
soit la monnaie avec un formatage adapté à
l'écriture française. Puisqu'il s'agit d'une saisie, nous
fabriquerons une nouvelle classe Monnaie qui hérite
directement de la classe TextField.
Afficher des images - Image et ImageView
Dans le projet qui suit, nous nous intéressons à
l'affichage automatique des images. Pour que cela puisse se
faire correctement, nous utilisons deux classes spécifiques
qui jouent chacune leur rôle. La première, Image,
permet de récupérer des photos à partir des fichiers
enregistrés quelque soit leurs formats et de
les traiter par la suite à l'aide de méthodes adaptées. La
deuxième, ImageView, s'occupe de l'affichage de
cette photo. Elle hérite de la classe Control et
réagit comme les autres contrôles présents dans l'IHM. Nous
exploitons l'objet Image généralememnt dans le contrôleur
ou dans le modèle alors que ImageView
sera mis en oeuvre au niveau de la vue. Nous
profitons de l'occasion pour voir comment utiliser les
boîtes de dialogues prédéfinies dans JavaFX.
Création d'une nouvelle interface Dessin
Cette étude va nous permettre de travailler avec la notion
d'interface que Java est capable d'implémenter. Nous allons
nous servir du projet précédent sur le tracé des différentes
formes auquel nous rajoutons le tracé de texte. Dans une
interface, nous déclarons l'ensemble des méthodes qui
devront être impérativement redéfinies. Toutes les classes
qui implémentrons cette interface devront respecter ce
contrat. Ainsi, puisqu'il s'agit d'une déclaration, toutes
les méthodes de l'interface sont systématiquement abstraite
et publique. Les interfaces ne possèdent pas d'attributs.
Seules quelques constantes peuvent être intégrées. L'intérêt
des interfaces est de mettre en relation des hiérarchies de
classes qui à priori n'ont rien en commun si ce n'est les
méthodes redéfinies. Lorsque nous passons par des
interfaces, comme leurs noms l'indiques, nous ne sommes plus
en contact direct avec les classes, nous n'avons pas d'accès
aux autres méthodes publiques, ce qui permet de rajouter une
protection supplémentaire.
Enregistrer des dessins
Par rapport au projet précédent, nous allons permettre
d'enregistrer l'ensemble du tracé réalisé, de telle sorte
qu'ultérieurement il soit possible de récupérer l'ensemble
de ces tracés afin de les réafficher sur notre fenêtre. Ce
projet nous permet d'utiliser les flux d'objets en Java.
Cela nous permet également de voir comment contruire une
barre de boutons sous forme d'icônes. Nous profitons de
l'occasion pour permettre le changement de couleur de
l'ensemble des dessins.
Éditeur HTML
Après avoir pris connaissance des flux en Java, nous allons
maintenant les utiliser afin de pouvoir communiquer au
travers du réseau. Notre objectif ici est d'ouvrir une
socket à l'aide de la classe prévue à cet effet qui se nomme
justement Socket. Avec un éditeur simple, nous
allons afficher le document HTML de votre choix. Il suffit
alors de proposer une URL correspondant au site que vous
désirez atteindre. Si l'URL n'est pas correcte, une boîte
d'alerte est alors proposée.
Service monétaire
Dans la section précédente, nous avons vu comment dialoguer au travers du réseau et comment créer une applications cliente capable
de communiquer avec un service Web, au travers du protocole HTTP. Dans cette
nouvelle rubrique, nous allons créer notre propre service personnalisé qui pemettra de réaliser des conversions monétaires. La mise
en oeuvre d'un service se fait très simplement à l'aide d'une classe de haut niveau ServerSocket qui encapsule toutes les
difficultés liées à la création d'un service au niveau du système d'exploitation. Avec une telle classe, la création d'un service
devient trivial. Côté application serveur, chaque client connecté possèdera sa propre Socket qui permettra d'identifier le
client. Comme pour l'application cliente, il faut penser à créer tous les flux nécessaires pour l'échange correcte des données.
Service Chat
Le service précédent permet de dialoguer entre un seul client et le service. La durée de communication est très courte, juste le
temps de renvoyer la réponse associée à la requête proposée. Du coup, nous n'avons pas eu besoin de mettre en place un système
multi-tâche. Plusieurs clients peuvent se connecter, la réponse leurs sera donnée instantanément à chaque requête soumise. Il
existe des situations toutefois où nous devons maintenir la connexion entre les intervenants, c'est le cas notamment avec le
service chat qui met en relation plusieurs clients simultanéments. Dans ce cas de figure, nous sommes obligé de prévoir au niveau
du service un ensemble de threads pour chacun des clients connectés qui assureront le multi-service. C'est ce système que nous
allons développer dans ce chapitre.
Service Web REST - Service monétaire
Si vous devez utiliser vos applications dans le réseau local, l'approche du chapitre précédent convient tout à fait. Vous pouvez
atteindre votre service n'importe où au sein du réseau. Puisque nous utilisons des sockets, il est également possible de maintenir
votre connexion ouverte le temps que vous voulez. Par contre, si vous désirez que votre service puisse être utilisé ailleurs que
dans votre réseau local en passant par le réseau Internet, cette technique devient problématique. Pour des raisons de sécurité, le
numéro de service que vous avez choisi 3377 va être bloqué par le parefeu il ne faut surtout pas ouvrir ce port
particulier vers l'extérieur. L'une des solutions qui vient alors à l'esprit est de mettre en oeuvre un service web REST qui a l'avantage de prendre les ports classiques 80 ou 8080
qui sont généralement ouverts par le parefeu. Ensuite, la communication se fait au travers du protocole HTTP
qui est très fiable. Par contre, cela dépend des situations ici cela ne pose pas de problèmes, mais l'inconvénient
c'est que la connexion entre le client et le serveur reste ouverte juste le temps de la réponse en association avec la requête
proposée impossibilité d'avoir deux clients qui communiquent entre eux comme pour le service Chat.
Service Web REST - Archivage de photos
Maintenant que nous maîtriser bien le principe de fonctionnement de ce type de service, nous allons élargir nos compétences en
utilisant les principales méthodes de gestion du protocole HTTP qui permettent
: la création, la modification, la récupération et la suppression des ressources, avec respectivement, les méthodes POST,
PUT, GET et DELETE. Pour cela, nous allons élaborer un nouveau projet qui permet d'archiver des photos sur le
serveur, afin de les retrouver plutard lorsque nous en aurons besoin, le tout en passant par Internet.
Service et protocole WebSocket - Service Chat
Les services Web REST est une technologie vraiment intéressante puisqu'elle permet de dialoguer par Internet. Elle possède
toutefois une limite de taille, dès que le serveur donne sa réponse, le client se trouve déconnecté du serveur. C'est le principe
même du protocole HTTP. Dans ce cadre là, il n'est pas possible de prévoir un échange entre clients, puisque le serveur ne
peut pas interpeler un client de sa propre initiative. L'idéal serait de pouvoir utiliser l'architecture des sockets avec
une connexion permanente, pas seulement en réseau local, mais également au travers d'Internet. C'est là qu'interviennent
les services WebSocket qui permettent de fusionner les deux technologies que nous venons d'expérimenter socket au
travers du Web. Le protocole websocket commence par établir une connexion en HTTP, sur le même port que
le service Web classique le pare-feu est franchi, et demande ensuite un upgrade vers le protocole websocket
proprement dit. Une fois que le changement est effectué, la connexion reste établie, et les échanges peuvent se faire comme pour
une communication par sockets. Dans ce cadre là, le serveur peut, quand il le désire, envoyer des informations à n'importe
lequel de ses clients.
Élaboration du service
La mise en oeuvre d'un Service WebSocket reprend le même principe qu'un Service Web REST, mais de façon plus
simple et plus concise. Encore une fois, vous avez besoin du serveur GlassFish. Vous devez créer un projet de type Application
Web, mais cette fois-ci, vous n'avez plus besoin du descripteur de déploiement web.xml, une seule classe annotée
suffit, avec en son sein des méthodes annotées, dont voici l'architecture :
L’API met à disposition plusieurs types d’annotations
Annotation
Rôle
@ServerEndpoint
Déclare un Service WebSocket
@ClientEndpoint
Déclare un Client qui communique avec un service WebSocket
@OnOpen
Défini la méthode de rappel qui gère l'évenement d’ouverture de la connexion
@OnMessage
Défini la méthode de rappel qui gère l'évenement de réception d’un message
@OnError
Défini la méthode de rappel qui gère l'évenement lors d’une erreur
@OnClose
Défini la méthode de rappel qui gère l'évenement de clôture de la connexion
L'ossature générale du service ressemble à celui que nous avions conçu lors du projet sur les sockets. La
différence tient essentiellement aux méthodes de rappel liées à l'ouverture, la fermeture et la transmission des messages. Lors de
l'établissement de la connexion, vous devez préciser le login à la fin de l'URL spécifiant le protocole websocket. Le
multi-tâche est géré automatiquement du fait qu'un objet est créé à chaque établissement de connexion. Chaque objet représente et
suit un client en particulier. L'objet est ensuite automatiquement détruit lorsque le client se déconnecte à son tour. Le point de
communication avec le client se fait au travers d'un objet de type Session. Grâce à la Map attribut
statique, chaque client connaît l'ensemble des clients déjà connectés. Il est donc facile de transmettre un message à un
destinataire répertorié.Il existe un site qui permet de tester simplement et rapidement notre Service Chat dont
voici l'adresse : https://www.websocket.org/echo.html.
Développement de la partie cliente en JavaFX
Maintenant que nous avons validé le fonctionnement du service, nous allons réaliser une application cliente qui nous permettra
d'échanger nos messages tranquillement depuis Internet. Pour cela, nous reprenons l'application que nous avons déjà mis en oeuvre
lors de la communication par Socket auquel nous allons effectuer quelques petits changements, notamment sur le contrôleur.
Comme pour l'élaboration du service, nous allons utiliser des annotations qui vont nous permettre de simplifier et d'alléger le
code de façon assez significative. Par contre, pour cela, vous devez rajouter une librairie supplémentaire tyrus-standalone-client
qui comporte tout ce qu'il faut pour communiquer par le protocole websocket.
Dès que l'application démarre, grâce au fichier de configuration précédent, elle doit se connecter automatiquement au service.
L'application reçoit alors automatiquement l'ensemble des autres clients déjà connectés. Nous pouvons alors soumettre nos
différents messages aux destinaires choisis.
ChatFXML.java
package client;
import javafx.application.Application;
import javafx.fxml.*;
import javafx.scene.*;
import javafx.stage.Stage;
public class ChatFXML extends Application {
@Override
public void start(Stage fenêtre) throws Exception {
Parent root = FXMLLoader.load(getClass().getResource("Chat.fxml"));
Scene scene = new Scene(root);
fenêtre.setScene(scene);
fenêtre.setTitle("Chat");
fenêtre.show();
}
}
Le code ci-dessus redevient normal. Nous n'avons pas besoin de nous préoccuper de la déconnexion, cela se fait
automatiquement.Chat.fxml
Nous retrouvons les mêmes annotations que pour le service, @OnOpen et @OnMessage, la première nous
permet de connaître la session et donc de pouvoir être en permanence en contact avec le service, la deuxième nous permet de
récupérer le message venant du serveur et de l'afficher en conséquence. Vous remarquez également la présence de l'annotation @ClientEndpoint
qui précise quelle est la classe qui permet de gérer la communication par le protocole websocket. Grâce à cette
annotation, notre classe est maintenant capable de gérer le multi-tâche et de travailler de façon événementielle à l'image de ce
que nous faisons avec la librairie QT. La connexion au service se fait dès la création du contrôleur au moyen de la classe WebSocketContainer.
Développement de la partie cliente en C++ avec la librairie QT
Nous reprenons l'ossature générale du projet défini avec les sockets. Cette fois-ci nous utilisons une classe spécialisée QWebSocket
qui, comme son nom l'indique, encapsule toutes les fonctionnalités propre à ce protocole. Dans ce cadre là, nous n'avons plus à
nous préoccuper de la gestion des flux, tout se fait automatiquement. Du coup, le code correspondant est très réduit.
Ici, nous recevons toujours le même type de message, nous n'avons pas à discriminer la requête en préalable. Du coup, le
codage de la réception est très rudimentaire.IHM et Événements
La classe QWebSocket dispose de méthodes adaptées aux différentes situations suivant l'ordre de communication avec
le service.
open()
L'établissement de la connexion se fait au travers de cette méthode avec laquelle vous devez spécifier l'URL correspondant
au protocole avec l'identifiant du client.
sendTextMessage()
Lorsque vous désirez soumettre un nouveau message au service, vous appelez cette méthode spécifique.
textMessageReceived()
Il s'agit d'un signal qui est soumis lorsque le service envoi lui-même un message au client. Vous devez alors proposer un
SLOT adapté qui permettra de récupérer le texte du message et de réagir en conséquence.
Élaboration du service en C++ avec la librairie QT
Dans ce que nous venons de construire, le code du service et le code de la partie cliente sont extrêmement réduits et très
simples à mettre en oeuvre. Le service est très concis puisque c'est le serveur d'applications qui gère la création des
différents objets associés à l'ensemble des clients système multi-tâche. Par contre, il est nécessaire d'avoir ce
serveur d'applications. Si vous souhaitez réaliser ce type de service sur une informatique embarquée, cela peut poser des
problèmes d'installation. Dans ce cas de figure, il est possible de réaliser un service de cette nature grâce à la librairie QT.
La réalisation d'un service WebSocket en C++ se fait au moyen de la classe QWebSocketServer dans un programme
classique utilisant les compétences de Qt. Vu que vous n'avez pas de serveur d'applications, vous êtes obligé de gérer le
muti-tâche plutôt multi-utilisateur, qui en Qt se fait au moyen de la gestion événementielle. À chaque nouveau
client connecté, il faut prévoir un événement pour la réception d'un nouveau message et un événement pour la déconnexion. Du coup,
lors de cette déconnexion, il faut supprimer ces événements. Cette gestion alourdi un peu le code, mais elle nous permet de mieux
comprendre les différentes interactions ce mode est caché dans le cadre d'un serveur d'applications.ServiceChat.pro
Voici quelques explications pour mieux comprendre le fonctionnement de ce service
Pas d'IHM
Vu que ce service est prévu pour une informatique embarquée, nous n'avons pas besoin de créer une IHM. Nous disposons
toutefois d'un programme principal qui doit permettre la gestion des événements grâce aux modules intégrés de Qt. L'application
principale tourne constamment en tâche de fond et attends qu'un arrêt soit proposé toujours sous forme d'événement.
Quitter l'application proprement
Lorsque nous lançons un service sur une informatique embarquée sans environnement graphique, la difficulté est toujours de
pouvoir arrêter le service. Ici, nous avons choisi de permettre cet arrêt lorsqu'un client tape le mot stop dans un
message.
Un seul objet représentant le service est créé
Puisque nous avons un seul objet représentant l'ensemble du service contrairement au serveur d'applications,
il est absolument nécessaire de savoir à tout moment quel client envoi le message ou quel client se déconnecte, ou sous une
autre façon de voir, qui a soumis un événement. La classe ServiceChat hérite de la classe QObject qui dispose
d'une méthode sender() qui nous renseigne sur la source du signal donc sur l'expéditeur.
Un port par service WebSocket
Malheureusement, la conception d'un service WebSocket avec la librairie Qt ne permet de faire qu'un seul service sur
le port désigné. Si vous souhaitez avoir plusieurs services, vous êtes obligé de prendre plusieurs numéros de service, ou
alors, le service 8080 doit gérer l'ensemble des services.
URL différente
Dans ce contexte, il n'est pas nécessaire de préciser le type de service comme pour le serveur d'applications où vous devez
spécifier l'application web associée. L'URL est donc plus réduite : ws://192.168.1.36:8080/manu par exemple.
Élaboration d'un service autonome en Java sans serveur d'applications
Pour conclure sur ce sujet, vous remarquez que le code que nous venons d'écrire avec la librairie Qt est plus conséquent que
celui que nous avions développé en Java. Par contre, l'avantage de la version C++, c'est qu'il n'est pas nécessaire d'avoir un
serveur d'applications. En java, cette possibilité existe également. Vous pouvez développer effectivement un service WebSocket
dans une application java standard autonome sans avoir besoin du serveur d'applications. Dans ce cas de figure, il sera
nécessaire d'intégrer dans votre projet un ensemble de librairies qui gèrent le protocole websocket qui constituent
normalement la version entreprise édition de java.
service.Chat.java
package service;
import java.io.IOException;
import java.util.*;
import javax.websocket.*;
import javax.websocket.server.*;
import org.glassfish.tyrus.server.Server;
@ServerEndpoint("/{login}")
public class Chat {
private String login;
private static HashMap<String, Session> clients = new HashMap<>();
public static void main(String[] args) throws DeploymentException {
Server service = new Server("localhost", 8080, "/chat", null, Chat.class);
service.start();
Scanner clavier = new Scanner(System.in);
clavier.nextLine();
}
@OnOpen
public void ouverture(Session client, @PathParam("login") String login) throws IOException {
this.login = login;
clients.put(login, client);
envoyerATous(login + " vient de se connecter");
envoyerATous("Connectés : "+clients.keySet().toString());
}
@OnClose
public void fermeture() throws IOException {
clients.remove(login);
envoyerATous(login + " vient de se déconnecter");
envoyerATous("Connectés : "+clients.keySet().toString());
}
@OnMessage
public String chat(String réception) throws IOException{
Scanner requête = new Scanner(réception);
String destinataire = requête.next();
String message = requête.nextLine();
return envoyerMessage(destinataire, message) ? "Message envoyé" : "Cible inconnue";
}
private void envoyerATous(String message) throws IOException {
for (Session client : clients.values())
if (client.isOpen()) client.getBasicRemote().sendText(message);
}
private boolean envoyerMessage(String destinataire, String message) throws IOException {
if (clients.containsKey(destinataire)) {
clients.get(destinataire).getBasicRemote().sendText(login + " > " + message);
return true;
}
return false;
}
}
Il s'agit d'une application java classique en mode console qui possède la classe principale d'application Chat
avec sa méthode principale main() à l'intérieure de laquelle nous lançons le service WebSocket grâce à la
classe Server avec toutes les informations nécessaires au bon fonctionnement : la localisation, le numéro de port, le path
de l'URL, et la classe qui implémente le service proprement dit. Mise à part cette particularité, nous retrouvons exactement la
même écriture, avec toutes les annotations requises, que nous avions élaboré lors de la conception avec le serveur d'applications.Il est possible ensuite de lancer votre service en ligne de commande sur n'importe quel système qui possède une machine
virtuelle Java. Remarquez la présence des archives dans le répertoire lib.
Application cliente sous Android
Android étant codé en Java, il est tout à fait envisageable de prévoir une application client qui utilise le protocole websocket
comme nous l'avons fait avec une application cliente en JavaFX. La seule et même condition, c'est d'intégrer l'archive tyrus-standalone-client
dans votre projet APK. Pour ce projet, nous utilisons Android Studio qui permet de mettre en place des applications Android
de façon simple.
La première démarche à suivre en premier est de placer l'archive tyrus-standalone-client dans le répertoire
spécifique LIBS qui se trouve systématiquement dans toute application Android. Ensuite, il est nécessaire que cette
archive soit compilée et donc intégrée automatiquement dans l'exécutable final Fichier APK , déployé dans votre
système Android. Pour cela, vous devez mettre en place une dépendance supplémentaire à votre projet, soit avec un outil
adapté bouton , soit directement dans le fichier de gestion des dépendances.
La suite du développement est tout-à-fait classique dans le cadre d'un projet Android, si ce n'est la particularité
d'utiliser des annotations spécifiques similaires à ce que nous avons mis en oeuvre dans l'application cliente en JavaFX.
Grâce aux annotations, ce code est très concis. Nous retrouvons globalement la même structure de ce que nous avons
déjà développé dans le Contrôleur de l'application cliente en JavaFX. La petite différence, c'est que
nous rajoutons un thread spécifique pour la gestion des messages reçus à l'aide de la méthode runOnUiThread(). Cela
permet d'avoir une meilleure fluidité sans bloquer l'IHM du système Android. Dans un projet Android, depuis la version 3,
il est également nécessaire, en plus du manifeste, de donner les droits d'accès au réseau. Enfin, il est plus judicieux de placer
la description de l'URI dans un fichier de ressource plutôt que directement dans le code source. Sous Android, cela fait
dans le document strings.xml
Amélioration du code afin d'avoir une application plus conviviale
En reprenant notre base de travail, je vous propose de rajouter des fonctionnalités supplémentaires sur le projet précédent afin
d'obtenir plus de convivialité et que l'utilisation de cette application soit plus intuitive. Par exemple, le choix du
destinataire se fera avec une liste déroulante qui se met à jour automatiquement suivant l'ensemble des destinataires présents
actuellement sur le réseau. Ensuite, nous allons prévoir une couleur en fonction des messages envoyés et des messages reçus en
suivant le même canevas que les applications d'envoi et de réception de SMS.
Le grand changement concerne le rajout de composant de type TextView à la volée. À chaque message envoyé ou
reçu, un nouveau TextView est créé et rajouté dans l'interface il est rajouté dans les gestionnaire LinearLayout
qui est intégré dans le ScrollView. Deux fonds différents ont été créés pour que visuellement nous fassions la
différence entre l'expéditeur et le destinataire. Le deuxième changement concerne l'utilisation d'un Spinner à la place
d'un EditText pour choisir son destinataire.
Dans la méthode de rappel onCreate(), nous rajoutons le mise en oeuvre d'un adaptateur de liste afin de gérer
correctement le Spinner. Ensuite, dans la méthode reception(), nous évaluons le message reçu afin de prendre
en compte la liste des connectés pour le Spinner à chaque fois qu'elle se présente. Nous bloquons également maintenant le
message Message envoyé. Une fois que tout ces critères sont pris en compte, nous créons un nouveau TextView avec
le fond spécifique reponse avec comme contenu le message reçu. Pour la méthode soumettre(), là aussi nous
créons un nouveau TextView avec comme contenu le message envoyé et le fond spécifique envoi.
WebSocket, JSON et criptage - Service Chat
Nous allons compléter l'étude précédente afin de permettre la mise en oeuvre d'une application cliente en JavaFX plus
performante en intégrant des échanges sur le réseau avec des messages formatés en JSON,
avec un criptage de l'information. Pour cela nous nous servirons du service déjà mis en oeuvre avec la librairie Qt dans
l'étude portant sur le codage du polymorphisme avec le langage C++.
Format JSON
JSON est un format léger pour l'échange de données structurées complexes. Il est
à l'image des documents XML en moins verbeux. Il est très utile lorsque vous devez
transférer toutes les informations relatives à une structure ou à un objet persistant par exemple. Voici ci-dessous un exemple de
document JSON représentant un objet de type Personne qui peut disposer
de plusieurs numéros de téléphones :
L'ossature du document ressemble à une structure dont le début et la fin sont désignés par des accolades. Chaque élément du
document possède une clé et une valeur associée. L'ensemble des éléments sont séparés par des virgules. Pour la
définition de la clé et de la valeur, vous devez l'écrire entre guillemets, sauf éventuellement pour les valeurs
numériques. Enfin, si une clé possède plusieurs valeurs, vous devez les spécifier entre des crochets séparées par des virgules et
toujours écrites entre guillemets.La librairie QT dispose de classes toutes prètes pour générer ou
lire des documents au format JSON. Vous avez la classe QJsonDocument qui
permet de prendre en compte l'ensemble du document pour sa génération ou sa lecture, la classe QJsonObject
qui permettra de créer ou lire la structure globale de l'entité et la classe QJsonArray qui sera capable de générer ou de
lire un ensemble de valeurs pour une même clé.La librairie QT stocke l'ensemble des
éléments de l'objet de type QJsonObject dans une collection de type map, ce qui correspond bien au principe de
l'association clé/valeur. Grâce à ce type de collection, il est très facile de retrouver chacun des éléments en utilisant
les crochets. Les valeurs retournées ou prises en compte des éléments sont de type QVariant. Cette classe peut ensuite
convertir directement vos valeurs vers des types correspond au langage C++ ou propres à la librairie QT,
grâce à des méthodes associées comme toString(), toInt(), toDouble(), toDate(), etc.Java dispose
également de classes toutes prètes pour générer ou lire des documents au format JSON.
Vous avez la classe JsonWriter qui permet de prendre en compte l'ensemble du document pour sa génération,
les classes JsonObjectBuilder et JsonObject qui permettent respectivement de créer ou de lire la structure globale
de l'entité et les classes JsonArrayBuilder ainsi que JsonArray qui sont capables de générer ou de lire un ensemble
de valeurs pour une même clé.
Service en C++ embarquée dans une raspberry
Pour ce projet, nous n'avons pas besoin de constituer une base de données côté serveur. Il suffit juste de mémoriser l'ensemble
des clients actuellement connectés, ceci en temps réel. Chaque client reçoit une notification sur le dernier client qui se
connecte ou se déconnecte avec en plus la liste de tous les présents. Le criptage est présenté dans l'étude du
polymorphisme.
Développement de la partie cliente en JavaFX
Reprenons l'application que nous avons déjà mis en oeuvre lors de l'étude précédente avec laquelle nous allons effectuer
quelques petits changements, notamment sur le contrôleur. Nous devons effectivement rajouter la partie criptage ainsi
que le prise en compte du format JSON. La vue change également
puisque nous prévoyons cette fois-ci une ComboBox qui nous donnera en temps réel tous les clients connectés. Comme
précédemment, nous utilisons des annotations qui vont nous permettre de simplifier et d'alléger le code de façon assez
significative. Par contre, pour cela, vous devez rajouter la librairie supplémentaire tyrus-standalone-client qui
comporte tout ce qu'il faut pour communiquer par le protocole websocket. Une autre librairie javax.json-1.0.4.jar
doit également être rajouter afin d'intégrer dans votre projet l'ensemble des classes nécessaires pour la prise en compte du
format JSON.
Dès que l'application démarre, grâce au fichier de configuration précédent, elle doit se connecter automatiquement au service.
L'application reçoit alors automatiquement l'ensemble des autres clients déjà connectés. Nous pouvons alors soumettre nos
différents messages aux destinaires choisis.
ChatFXML.java
package client;
import javafx.application.Application;
import javafx.fxml.*;
import javafx.scene.*;
import javafx.stage.Stage;
public class ChatFXML extends Application {
@Override
public void start(Stage fenêtre) throws Exception {
Parent root = FXMLLoader.load(getClass().getResource("Chat.fxml"));
Scene scene = new Scene(root);
fenêtre.setScene(scene);
fenêtre.setTitle("Messagerie instantanée");
fenêtre.show();
}
}
package client;
import java.io.*;
import java.net.*;
import java.nio.*;
import java.nio.charset.*;
import java.util.*;
import javafx.application.Platform;
import javafx.fxml.*;
import javafx.scene.control.*;
import javax.json.*;
import javax.websocket.*;
import org.glassfish.tyrus.client.ClientManager;
@ClientEndpoint
public class ChatController implements Initializable {
@FXML private TextField message;
@FXML private TextArea zone;
@FXML private ComboBox<String> connectés;
private String expéditeur;
private Session session;
@Override
public void initialize(URL url, ResourceBundle rb) {
try {
ResourceBundle config = ResourceBundle.getBundle("config/url");
String adresse = config.getString("adresse");
String port = config.getString("port");
expéditeur = config.getString("login");
ClientManager client = ClientManager.createClient();
URI uri = URI.create("ws://"+adresse+":"+port+"/"+expéditeur);
client.connectToServer(this, uri);
}
catch (IOException | DeploymentException ex) {
Alert problème = new Alert(Alert.AlertType.WARNING);
problème.setHeaderText("Impossible de se connecter");
problème.showAndWait();
}
}
@OnOpen
public void ouverture(Session session) {
this.session = session;
}
@FXML
private void soumettre() throws IOException, EncodeException {
zone.appendText("moi > "+message.getText()+ '\n');
StringWriter texte = new StringWriter();
JsonWriter json = Json.createWriter(texte);
JsonObjectBuilder soumettre = Json.createObjectBuilder();
soumettre.add("commande", "message");
soumettre.add("expéditeur", expéditeur);
soumettre.add("destinataire", connectés.getValue());
soumettre.add("message", message.getText());
json.writeObject(soumettre.build());
json.close();
String formatUTF8 = new String(texte.toString().getBytes(), Charset.forName("UTF-8"));
session.getBasicRemote().sendBinary(cripter(formatUTF8.getBytes()));
message.selectAll();
}
@OnMessage
public void réception(ByteBuffer réception) {
Platform.runLater(() -> {
String décriptage = décripter(réception);
JsonObject json = Json.createReader(new StringReader(décriptage)).readObject();
String commande = json.getString("commande");
if (commande.equals("message"))
zone.appendText(json.getString("expéditeur")+" > "+json.getString("message") + '\n');
else {
JsonArray logins = json.getJsonArray("connectés");
zone.appendText("connectés : "+logins.toString()+ '\n');
connectés.getItems().clear();
for (int i=0; i<logins.size(); i++) connectés.getItems().add(logins.getString(i));
connectés.getSelectionModel().select(0);
}
});
}
private String décripter(ByteBuffer texte) {
// Remettre en place les lettres mélangées (clé de criptage suivi du message cripté)
byte[] mélange = texte.array();
byte[] message = new byte[mélange.length];
int nombre = mélange.length;
byte[] ordre;
int taille = nombre%3 == 0 ? nombre+1 : nombre;
ordre = Arrays.copyOf(mélange, taille);
for (int i=0, indice=2; i<nombre; i++, indice+=3) message[indice%ordre.length] = ordre[i];
// Phase de décriptage
int clef = 0;
byte lettre = message[clef++]; // lettre de référence pour la clef
int chiffre = message[clef++] - lettre; // chiffre de référence pour le décriptage
nombre = message[clef++] - lettre; // nombre d'éléments dans la clef
int[] décalages = new int[nombre];
for (int i=0; i<nombre; i++) décalages[i] = message[clef++] - lettre;
byte[] décodage = new byte[message.length - clef];
for (int i=clef, j=0; i<message.length; i++, j++) décodage[j] = (byte)(message[i] + chiffre + décalages[(i-clef)%nombre]);
return new String(décodage, Charset.forName("UTF-8")); // Codage proposé automatiquement par le C++ (serveur)
}
private ByteBuffer cripter(byte[] message) {
byte lettre = (byte) (' ' + random()); // lettre de référence pour la clef
byte chiffre = (byte) random(); // chiffre référence pour le criptage
int nombre = random(); // nombre d'éléments supplémentaires dans la clef
byte[] encodage = new byte[message.length+3+nombre];
encodage[0] = lettre;
encodage[1] = (byte) (lettre + chiffre);
encodage[2] = (byte) (lettre + nombre);
int pos = 3;
int[] décalages = new int[nombre]; // suite de nombre servant au calcul de chaque lettre du message
for (int i=0; i<nombre; i++) {
décalages[i] = random();
encodage[pos++] = (byte) (lettre + décalages[i]);
}
for (int i=0; i<message.length; i++) encodage[pos++] = (byte) (message[i] - chiffre - décalages[i%nombre]);
// Brassage des lettres afin de mélanger la clé de criptage avec son message cripté
nombre = encodage.length;
byte[] mélange = new byte[nombre];
int taille = nombre%3 == 0 ? nombre+1 : nombre;
for (int i=0, indice=2, j=0; i<nombre; i++, indice+=3, j++) mélange[j] = encodage[indice%taille];
return ByteBuffer.wrap(mélange);
}
private int random() {
return (int)(Math.random()*9)+1;
}
}
Les annotations
Nous retrouvons les mêmes annotations que pour le service, @OnOpen et @OnMessage, la première nous
permet de connaître la session et donc de pouvoir être en permanence en contact avec le service, la deuxième nous permet de
récupérer le message venant du serveur et de l'afficher en conséquence. Vous remarquez également la présence de l'annotation @ClientEndpoint
qui précise quelle est la classe qui permet de gérer la communication par le protocole websocket. Grâce à cette
annotation, notre classe est maintenant capable de gérer le multi-tâche et de travailler de façon événementielle à l'image de
ce que nous faisons avec la librairie QT. La connexion au service se fait dès la création du contrôleur au moyen de la classe WebSocketContainer.
Criptage et décriptage
Ce code source est relativement conséquent, notamment à cause des deux méthodes spécifiques décripter() et cripter().
Elles doivent suivre scrupuleusement ce qui a été prévu lors de la conception en C++. Justement, le formatage des
chaînes de caractères est différent suivant les langages de programmation, surtout entre le C++ et Java.
Le C++ manipule des chaînes au format UTF-8. Du coup, dans notre code source en JavaFX, il faut
générer ou prendre en compte ce format là et traiter les chaînes plutôt sous forme de suites d'octets.
Méthode de rappel lors de la réception de nouveau message
La méthode réception() est automatiquement sollicité dès qu'un nouveau message venant du serveur apparaît. Cette
méthode est annotée @OnMessage. À cause de cette annotation, comme pour la version Android, que nous avons traité
dans le chapitre précédent, il est difficile d'interagir avec la vue puisque nous ne sommes plus sur le thread courant
d'une application JavaFX standard. Il suffit d'appliquer le même type de remède en invoquant le thread du contôleur
au moyen de la classe Platform avec sa méthode statique runLater().
Application cliente sous Android
Pour conclure, je vous propose de fusionner l'application que nous avions déjà faite sous Android avec le code précédent afin
d'intégrer également le format JSON ainsi que le criptage automatique des
messages envoyés et reçus. Là aussi, nous devons intégrer les archives tyrus-standalone-client et javax.json-1.0.4.jar
dans votre projet APK.
Comme précédemment, la première démarche à suivre en premier est de placer la deuxième archive javax.json-1.0.4.jar
dans le répertoire spécifique LIBS qui se trouve systématiquement dans toute application Android. Ensuite, il est
nécessaire que cette archive soit compilée et donc intégrée automatiquement dans l'exécutable final Fichier APK ,
déployé dans votre système Android. Pour cela, vous devez mettre en place une dépendance supplémentaire à votre projet,
soit avec un outil adapté bouton , soit directement dans le fichier de gestion des dépendances.
build.gradle
package manu.chat;
import android.os.StrictMode;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.*;
import android.widget.*;
import org.glassfish.tyrus.client.ClientManager;
import java.io.*;
import java.nio.*;
import java.nio.charset.*;
import java.net.URI;
import java.util.*;
import javax.websocket.*;
import javax.json.*;
@ClientEndpoint
public class Chat extends AppCompatActivity {
private Spinner destinataire;
private EditText message;
private LinearLayout zone;
private ScrollView ascenseur;
private Session session;
private String expediteur;
private ArrayAdapter<String> liste;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.chat);
destinataire = (Spinner) findViewById(R.id.destinataire);
message = (EditText) findViewById(R.id.message);
zone = (LinearLayout) findViewById(R.id.zone);
ascenseur = (ScrollView) findViewById(R.id.ascenseur);
liste = new ArrayAdapter<String>(this, android.R.layout.simple_spinner_item, new ArrayList<String>());
destinataire.setAdapter(liste);
// Autoriser l'utilisation d'internet
StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder().permitAll().build());
// lancer la connexion au service WebSocket
ClientManager client = ClientManager.createClient();
try {
String adresse = getString(R.string.adresse);
String port = getString(R.string.port);
expediteur = getString(R.string.expediteur);
client.connectToServer(this, URI.create("ws://"+adresse+":"+port+"/"+expediteur));
}
catch (Exception e) { e.getStackTrace(); }
}
@OnOpen
public void ouverture(Session session) {
this.session = session;
}
@OnMessage
public void reception(final ByteBuffer message) {
runOnUiThread(new Runnable() {
@Override
public void run() {
String decriptage = decripter(message);
JsonObject json = Json.createReader(new StringReader(decriptage)).readObject();
String commande = json.getString("commande");
if (commande.equals("message")) {
TextView reponse = new TextView(Chat.this);
reponse.setBackground(getDrawable(R.drawable.reponse));
reponse.setText(json.getString("expéditeur") + " > " + json.getString("message"));
zone.addView(reponse);
ascenseur.fullScroll(ScrollView.FOCUS_DOWN);
}
else {
JsonArray logins = json.getJsonArray("connectés");
liste.clear();
for (int i=0; i<logins.size(); i++) liste.add(logins.getString(i));
}
}
});
}
public void soumettre(View envoi) throws IOException {
StringWriter texte = new StringWriter();
JsonWriter json = Json.createWriter(texte);
JsonObjectBuilder soumettre = Json.createObjectBuilder();
soumettre.add("commande", "message");
soumettre.add("expéditeur", expediteur);
soumettre.add("destinataire", (String) destinataire.getSelectedItem());
soumettre.add("message", message.getText().toString());
json.writeObject(soumettre.build());
json.close();
String formatUTF8 = new String(texte.toString().getBytes(), Charset.forName("UTF-8"));
session.getBasicRemote().sendBinary(cripter(formatUTF8.getBytes()));
session.getBasicRemote().sendText(destinataire.getSelectedItem().toString() + " " + message.getText());
TextView moi = new TextView(Chat.this);
moi.setBackground(getDrawable(R.drawable.envoi));
moi.setText("moi > " + message.getText());
zone.addView(moi);
ascenseur.fullScroll(ScrollView.FOCUS_DOWN);
message.selectAll();
}
@Override
protected void onStop() {
super.onStop();
try {
session.close();
}
catch (IOException e) { e.printStackTrace(); }
}
private String decripter(ByteBuffer texte) {
// Remettre en place les lettres mélangées (clé de criptage suivi du message cripté)
byte[] mélange = texte.array();
byte[] message = new byte[mélange.length];
int nombre = mélange.length;
byte[] ordre;
int taille = nombre%3 == 0 ? nombre+1 : nombre;
ordre = Arrays.copyOf(mélange, taille);
for (int i=0, indice=2; i<nombre; i++, indice+=3) message[indice%ordre.length] = ordre[i];
// Phase de decriptage
int clef = 0;
byte lettre = message[clef++]; // lettre de référence pour la clef
int chiffre = message[clef++] - lettre; // chiffre de référence pour le decriptage
nombre = message[clef++] - lettre; // nombre d'éléments dans la clef
int[] décalages = new int[nombre];
for (int i=0; i<nombre; i++) décalages[i] = message[clef++] - lettre;
byte[] décodage = new byte[message.length - clef];
for (int i=clef, j=0; i<message.length; i++, j++) décodage[j] = (byte)(message[i] + chiffre + décalages[(i-clef)%nombre]);
return new String(décodage, Charset.forName("UTF-8")); // Codage proposé automatiquement par le C++ (serveur)
}
private ByteBuffer cripter(byte[] message) {
byte lettre = (byte) (' ' + random()); // lettre de référence pour la clef
byte chiffre = (byte) random(); // chiffre référence pour le criptage
int nombre = random(); // nombre d'éléments supplémentaires dans la clef
byte[] encodage = new byte[message.length+3+nombre];
encodage[0] = lettre;
encodage[1] = (byte) (lettre + chiffre);
encodage[2] = (byte) (lettre + nombre);
int pos = 3;
int[] décalages = new int[nombre]; // suite de nombre servant au calcul de chaque lettre du message
for (int i=0; i<nombre; i++) {
décalages[i] = random();
encodage[pos++] = (byte) (lettre + décalages[i]);
}
for (int i=0; i<message.length; i++) encodage[pos++] = (byte) (message[i] - chiffre - décalages[i%nombre]);
// Brassage des lettres afin de mélanger la clé de criptage avec son message cripté
nombre = encodage.length;
byte[] mélange = new byte[nombre];
int taille = nombre%3 == 0 ? nombre+1 : nombre;
for (int i=0, indice=2, j=0; i<nombre; i++, indice+=3, j++) mélange[j] = encodage[indice%taille];
return ByteBuffer.wrap(mélange);
}
private int random() {
return (int)(Math.random()*9)+1;
}
}
Dans la méthode de rappel onCreate(), nous rajoutons le mise en oeuvre d'un adaptateur de liste afin de
gérer correctement le Spinner. Ensuite, dans la méthode reception(), nous évaluons le message reçu afin de
prendre en compte la liste des connectés pour le Spinner à chaque fois qu'elle se présente. Nous bloquons également
maintenant le message Message envoyé. Une fois que tout ces critères sont pris en compte, nous créons un nouveau TextView
avec le fond spécifique reponse avec comme contenu le message reçu. Pour la méthode soumettre(), là aussi
nous créons un nouveau TextView avec comme contenu le message envoyé et le fond spécifique envoi.
Graphisme 2D - Shapes, Canvas, Charts
Pour clôturer l'ensemble de ces études, nous allons utiliser des composants de hauts niveaux qui permettent de réaliser des tracés
de courbes avec des objets de type Chart qui permettent de créer différentes représentations graphiques de données. Nous
profiterons de cette étude pour fabriquer une application qui contôle les données biométriques de capteurs fixés sur un patient en
perte d'autonomie. Quatre types de capteurs sont visualisés, la température corporelle, le pouls, le taux d'oxygène dans le sang et
l'électrocardiogramme.