L'interactivité croissante des applications web, consécutive à l'amélioration des performances des navigateurs, a rapidement rendu nécessaire le développement de techniques de communications bidirectionnelles entre l'application web et les processus serveur. Le protocole WebSocket vise à développer un canal de communication bidirectionnel et full-duplex sur un socket TCP pour les navigateurs et les serveurs web.
Le protocole HTTP ne permet pas d'atteindre une connectivité sans temps mort entre les clients Web et les serveurs. Par contre, le protocole WebSocket permet une communication bidirectionnelle entre les applications Web et les serveurs Web via un seul socket TCP. En d'autres termes, ce protocole rend possible la connexion permanente d'une application Web hébergée dans un navigateur avec un point de terminaison Web. Le résultat final est que les données et les notifications peuvent passer d'un navigateur et d'un serveur Web à l'autre sans aucun retard et sans aucune nécessité d'organiser des demandes supplémentaires.
Ce que les développeurs souhaitaient vraiment, c’était une connexion entre le client et le serveur qui reste ouverte indéfiniment, permettant aux deux parties d’envoyer des données dans les deux sens si nécessaire.Le client et le serveur peuvent envoyer librement des données à l’autre par ce protocole. Il partage le port HTTP 80 avec le
contenu existant, traversant facilement les différents pare-feu, routeurs, proxy, etc… Cette technologie est couramment appelée méthode
Push
. Le serveur vient pousser push les données vers le client et n’est pas obligé d’attendre que ce dernier ne
demande l’information.
Le protocole des WebSockets change la manière dont un serveur web réagit aux requêtes d'un client : au lieu de fermer la connexion, il envoie un message avec le code HTTP 101 et laisse la connexion ouverte. Les deux parties s'attendent ensuite à recevoir des données et à en écrire sur le flux. Contrairement à HTTP, le protocole supporte une communication bilatérale complète de telle manière que le client et le serveur peuvent s'échanger des messages en même temps.
Après avoir pris connaissance avec le principe du protocole WebSocket, nous allons maintenant le mettre en oeuvre avec la technologie du
Java Entreprise Edition version 7 qui l'intègre dorénavant. Comme pour les Web Services, la création d'un service EndPoint
qui
utilise ce protocole se fait très simplement au moyen d'annotations adaptées. Durant ce chapitre, nous allons découvrir l'ensemble de ces
annotations qui seront détaillées par la suite.
JEE7 intègre maintenant l'API Java pour les WebSocket qui propose un certaine nombre de fonctionnalités inhérentes :
L'API WebSocket est simple d'utilisation. Le principe est de créer un serveur WebSocket par la définition d'une Endpoint, de gérer le cycle de vie de l'application à travers les méthodes annotées @OnOpen, @OnMessage et @OnClose. L'API Java WebSocket propose deux méthodes pour définir et utiliser les webSocket ; par programmation Java ou par annotation qui est plus simple à utiliser. Ainsi, la transformation d’une classe basique dite POJO vers un WebSocket Endpoint de type serveur c’est-à-dire capable de gérer des requêtes de plusieurs clients sur une même URI est extrêment simple, puisqu’il suffit d’annoter la classe avec @ServerEndpoint et de mettre en place une méthode du POJO avec @OnMessage :
import javax.websocket.server.ServerEndpoint; import
javax.websocket.
OnMessage; @ServerEndpoint("/echo") public class ServiceEcho { @OnMessage public String écho(String message){ return "Merci pour ton message : " + message; } }
Le client, généralement écrit en Javascript nous développerons lors de cette étude des clients en C++ à l'aide de la librairie Qt et exécuté sur un navigateur implémentant le protocole WebSocket, se connecte à ce même Endpoint à travers une URI respectant le protocole WebSocket dont la signature est la suivante :
ws-URI = "ws:" "//" host [ ":" port ] path [ "?" query ]
wss-URI = "wss:" "//" host [ ":" port ] path [ "?" query ]
wss est utilisé dans le cas d'une connexion WebSocket sécurisé à travers le protocole TLS, comme HTTPS.
Annotation | Rôle |
---|---|
@ServerEndpoint | Déclare un Server Endpoint |
@ClientEndpoint | Déclare un Client Endpoint |
@OnOpen | Défini la méthode appelée pour gérer l'évenement d’ouverture de la connexion |
@OnMessage | Défini la méthode appelée pour gérer l'évenement de réception d’un message |
@OnError | Défini la méthode appelée pour gérer l'évenement lors d’une erreur |
@OnClose | Défini la méthode appelée pour gérer l'évenement de clôture de la connexion |
@ServerEndpoint("/echo")
public class ServiceEcho {
@OnOpen
public void ouverture(Session session, EndpointConfig config){}
@OnClose
public void clôture(Session session, CloseReason closeReason){}
@OnError
public void erreur(Session session, Throwable thr){}
}
javax.websocket.Session est une interface représentant une connexion entre un client et un serveur. Un objet de type
session est créé à la connexion d'un nouveau client (peer) et peut être récupéré à partir la méthode de rappel associée à @OnOpen
de la classe Endpoint en question. Ce paramètre de type Session est optionnel et nous pouvons le retrouver sur toutes les
méthodes de rappel.Les messages reçus pouvent être de différentes natures. Nous avons vu qu'il est possible de recevoir et de renvoyer du texte String ou Reader, mais nous pouvons également envoyer et recevoir des messages au format binaire, sous forme d'une suite d'octets ByteBuffer, byte[], InputStream. Un troisième type de message permet de contrôler si la connexion avec le serveur est toujours d'actualité PongMessage.
Pour chacun de ces cas, nous utilisons l'annotation @OnMessage. Il est tout à fait possible d'avoir au plus 3 méthodes avec cette annotation une méthode pour chaque type de message, texte, binaire et pong.
@OnMessage
public void onTexteMessage(Session session, String message){}
@OnMessage
public void onBinaireMessage(Session session, ByteBuffer message){}
@OnMessage
public void onPongMessage(Session session, PongMessage message){}
Nous allons maintenant rentrer dans le vif du sujet en nous servant des méthodes de rappel que nous venons de découvrir. Nous en profiterons pour voir comment le serveur peut envoyer ses propres messages à tout moment indépendamment du client, cette fois-ci donc de façon asynchrone. Enfin nous verrons comment le serveur peut diffuser une information à l'ensemble des clients déjà connectés.
Nous allons compléter le service précédent en prenant en compte la méthode de rappel annotée @OnOpen. Cette méthode est automatiquement appelée lorsqu'un nouveau client tente de se connecter au service.
import java.io.IOException;
import javax.websocket.*;
import javax.websocket.server.ServerEndpoint;
@ServerEndpoint("/echo")
public class ServiceEcho {
@OnMessage
public String écho(String message){
return "Merci pour ton message : " + message;
}
@OnOpen
public void ouverture(Session session) throws IOException {
session.getBasicRemote().sendText("Vous êtes connecté");
}
}
Nous verrons tout au long de cette étude que le paramètre de type Session est fondamental, comme c'est le cas ici. Je
rappelle qu'il représente la connexion entre le serveur et le client actuel. Grâce à lui, nous pouvons être en contact avec le client
distant au moyen de la méthode getBasicRemote() qui renvoie un objet représentant le client. À partir de là, au moyen de cet
objet, il suffit de faire appel à la méthode correspondant au type de message à envoyer, ici sendText().
À l'image du Service Web REST, il est possible d'utiliser l'annotation @PathParam qui permet d'extraire un élément de la requête ws spécifiée dans l'URI et de passer la valeur identifiant quelconque en paramètre de la méthode. Nous pouvons ainsi gérer plus facilement les différents clients associés au service.
import java.io.IOException;
import javax.websocket.*;
import javax.websocket.server.*;
@ServerEndpoint("/echo/{login}")
public class ServiceEcho {
private String login;
@OnMessage
public String écho(String message){
return login + " : " + message;
}
@OnOpen
public void ouverture(Session session, @PathParam("login") String login) throws IOException {
this.login = login;
session.getBasicRemote().sendText("Bonjour "+login);
}
}
Pour mettre en oeuvre une URI paramétrée, vous devez spécifier votre variable entre accollades {} qui doit correspondre à
l'attribut de l'annotation @PathParam, comme nous l'avons déjà implémenté dans les Service Web REST. Remarquez au passage que
puisque notre service est une simple classe, rien empêche d'utiliser autant d'attributs que vous voulez pour mémoriser les actions faites
par le client durant toute la session.
Le serveur peut connaître l'ensemble des clients actuellement connectés. Comme il peut communiquer quand il le désire, il peut également le faire à l'ensemble des clients. En reprenant le projet précédent, nous allons faire en sorte que tous les clients soient au courant lorsqu'un nouveau client se connecte ou se déconnecte.
import java.io.IOException;
import javax.websocket.*;
import javax.websocket.server.*;
@ServerEndpoint("/echo/{login}")
public class ServiceEcho {
private String login;
private Session session;
@OnMessage
public String écho(String message){
return login + " : " + message;
}
@OnOpen
public void ouverture(Session session, @PathParam("login") String login) throws IOException {
this.login = login;
this.session = session;
session.getBasicRemote().sendText("Bonjour "+login);
envoyerATous(login + " vient de se connecter");
}
@OnClose
public void fermeture() throws IOException {
envoyerATous(login + " vient de se déconnecter");
}
private void envoyerATous(String message) throws IOException {
for (Session chacun : session.getOpenSessions())
if (chacun.isOpen() && chacun!=session) chacun.getBasicRemote().sendText(message);
}
}
Il est souvent intéressant de conserver des informations dans la session du client pour qu'elles soient consultables par les autres. Je rappelle que chaque client possède sa propre session avec, comme nous l'avons déjà vu, un certain nombre de méthodes adaptées. Dans ce registre, il existe une méthode qui permet d'enregistrer ou de lire des informations personnelles au moyen de propriétés sauvegardées en mémoire centrale. Il faut noter que cet enregistrement n'est conservé que le temps d'activation de la session. Ainsi, lorsque le client se déconnecte, toutes ses données personnelles sont perdues. Par exemple, en reprenant le même projet, il serait judicieux que lorsqu'un nouveau client se connecte, il soit au courant de tous les autres clients déjà présents dans le service.
import java.io.IOException;
import java.util.*;
import javax.websocket.*;
import javax.websocket.server.*;
@ServerEndpoint("/echo/{login}")
public class ServiceEcho {
private String login;
private Session session;
@OnMessage
public String écho(String message){
return login + " : " + message;
}
@OnOpen
public void ouverture(Session session, @PathParam("login") String login) throws IOException {
this.login = login;
this.session = session;
envoyerATous(login + " vient de se connecter");
session.getUserProperties().put("login", login);
listeDesConnectés();
}
@OnClose
public void fermeture() throws IOException {
envoyerATous(login + " vient de se déconnecter");
listeDesConnectés();
}
private void envoyerATous(String message) throws IOException {
for (Session chacun : session.getOpenSessions())
if (chacun.isOpen()) chacun.getBasicRemote().sendText(message);
}
private void listeDesConnectés() throws IOException {
List<string> liste = new ArrayList<>();
for (Session chacun : session.getOpenSessions())
liste.add((String) chacun.getUserProperties().get("login"));
envoyerATous("Connectés : "+liste.toString());
}
}
L'enregistrement ou la récupération de données utilisateur sous forme de carte clé, valeur se fait au travers de la
méthode getUserProperties() intégrée dans l'objet de type Session. Comme pour toutes les cartes, il suffit ensuite
d'utiliser la méthode put() ou la méthode get() en spécifiant la clé requise pour respectivement enregistrer et
lire l'information souhaitée. Ici, c'est le login qui est enregistré pour chacune des sessions utilisateur.
Jusqu'à présent, nous n'avons fait aucune distinction entre les clients. La communication se faisait systématiquement pour tous les clients en même temps. À l'image d'un chat, il serait intéressant de pouvoir dialoguer directement avec un des clients présent dans le service. Attention, dans ce cas là, il faut que notre message soit attitré à un autre client en particulier, ce qui sous-entend que lorsque nous envoyons un message, il y ait le contenu même du message, mais également le nom du destinataire. Notre message comporte donc une structure composée. Le format JSON paraît adapté à ce genre de situation puisque, tout en restant sous forme de texte, nous pouvons associer un certain nombre de valeurs à un ensemble d'attributs.
import java.io.*;
import java.util.*;
import javax.json.*;
import javax.websocket.*;
import javax.websocket.server.*;
@ServerEndpoint("/chat/{login}")
public class ServiceChat {
private String login;
private Session session;
@OnMessage
public String chat(String requête) throws IOException{
JsonObject json = Json.createReader(new StringReader(requête)).readObject();
String cible = json.getString("cible");
String message = json.getString("message");
return envoyerMessage(cible, message) ? "Message envoyé" : "Cible inconnue";
}
@OnOpen
public void ouverture(Session session, @PathParam("login") String login) throws IOException {
this.login = login;
this.session = session;
envoyerATous(login + " vient de se connecter");
session.getUserProperties().put("login", login);
listeDesConnectés();
}
@OnClose
public void fermeture() throws IOException {
envoyerATous(login + " vient de se déconnecter");
listeDesConnectés();
}
private void envoyerATous(String message) throws IOException {
for (Session chacun : session.getOpenSessions())
if (chacun.isOpen()) chacun.getBasicRemote().sendText(message);
}
private void listeDesConnectés() throws IOException {
List<String> liste = new ArrayList<>();
for (Session chacun : session.getOpenSessions())
liste.add((String) chacun.getUserProperties().get("login"));
envoyerATous("Connectés : "+liste.toString());
}
private boolean envoyerMessage(String destinataire, String message) throws IOException {
for (Session chacun : session.getOpenSessions())
if (destinataire.equals(chacun.getUserProperties().get("login"))) {
chacun.getBasicRemote().sendText(login + " > " + message);
return true;
}
return false;
}
}
Dans le chapitre précédent, nous avons pris connaissance avec les principales propriétés du protocole WebSocket : communication bidirectionnelle permanente, communication en temps réel avec d'autres clients, possibilité qu'a le serveur d'envoyer des messages à tout instant pour un ou plusieurs clients connectés au service. Je voudrais repréciser qu'à chaque fois qu'un nouveau client se connecte, un objet du EndPoint est généré automatiquement à l'image d'un bean session de type Stateful. L'intérêt, c'est qu'il est alors possible d'avoir des attributs propres à chaque client comme nous l'avons fait dans le projet de chat, avec login et session. Par contre, beaucoup de ressources sont utilisées : un objet pour chaque client connecté. Dans ce chapitre, nous allons voir comment utiliser un seul objet EndPoint pour l'ensemble des clients connectés au service. A cette occasion, nous en profiterons pour utiliser les beans Session ainsi que les entités.
À l'image des beans Session, il est possible d'ajouter les annotations @Stateless, @Stateful et @Singleton directement sur la classe du EndPoint. L'importation pour utiliser ces beans sessions, je le rappelle, est javax.ejb, mais dans le cadre d'un WebSocket, nous pouvons aussi utiliser l'importation javax.inject. Grâce à ces annotations, nous pouvons directement dans le WebSocket réaliser les mêmes types de développement qu'avec les beans Session sans passer par une classe supplémentaire.
Quel type d'annotation allons-nous choisir ? @Stateful à priori n'apporte pas beaucoup d'intérêt puisque, comme nous l'avons dit en introduction, WebSocket génère déjà un objet pour chacun des clients connectés il sera quand même très souvent utilisé, voir plus loin. @Stateless n'est pas non plus très adapté à notre situation puisque nous ne pouvons pas maîtriser le nombre d'objets créés automatiquement très utile par contre en temps qu'objet séparé pour gérer les entités. Par contre @Singleton peut être utile dans le cas où nous désirons avoir un seul objet généré qui va permettre d'avoir des attributs qui sont communs à l'ensemble des clients, connectés ou pas.
Ainsi, par exemple, nous allons reprendre le projet précédent en le complétant de telle sorte que si un client envoie un message pour un autre client qui n'est pas encore connecté, le message est conservé en mémoire jusqu'à ce que ce dernier se connecte à son tour et reçoive alors le message qui lui était dédié. Dans ce cas de figure, nous avons à la fois une messagerie instantanée couplée avec une messagerie asynchrone classique, comme le courrier électronique.
import java.io.*;
import java.util.*;
import javax.inject.Singleton; // ou import javax.ejb.Singleton;
import javax.json.*;
import javax.websocket.*;
import javax.websocket.server.*;
@ServerEndpoint("/chat/{login}")
@Singleton
public class ServiceChat {
private Map<String, String> messages = new HashMap<>();
@OnMessage
public String chat(Session session, String requête) throws IOException{
JsonObject json = Json.createReader(new StringReader(requête)).readObject();
String cible = json.getString("cible");
String message = json.getString("message");
return envoyerMessage(session, cible, message) ? "Message envoyé" : "Destinataire pas encore connecté";
}
@OnOpen
public void ouverture(Session session, @PathParam("login") String login) throws IOException {
envoyerATous(session, login + " vient de se connecter");
session.getUserProperties().put("login", login);
listeDesConnectés(session);
if (messages.containsKey(login)) {
session.getBasicRemote().sendText(messages.get(login));
messages.remove(login);
}
}
@OnClose
public void fermeture(Session session) throws IOException {
envoyerATous(session, session.getUserProperties().get("login") + " vient de se déconnecter");
listeDesConnectés(session);
}
private void envoyerATous(Session session, String message) throws IOException {
for (Session chacun : session.getOpenSessions())
if (chacun.isOpen()) chacun.getBasicRemote().sendText(message);
}
private void listeDesConnectés(Session session) throws IOException {
List liste = new ArrayList<>();
for (Session chacun : session.getOpenSessions())
liste.add((String) chacun.getUserProperties().get("login"));
envoyerATous(session, "Connectés : "+liste.toString());
}
private boolean envoyerMessage(Session session, String destinataire, String message) throws IOException {
String login = (String) session.getUserProperties().get("login");
for (Session chacun : session.getOpenSessions())
if (destinataire.equals(chacun.getUserProperties().get("login"))) {
chacun.getBasicRemote().sendText(login + " > " + message);
return true;
}
messages.put(destinataire, login+" > "+message);
return false;
}
}
Vous remarquez que le code précédent est un petit peu plus complexe puisque maintenant l'ensemble des méthodes comportent un paramètre de type Session. Nous allons simplifier ce code en faisant en sorte que l'ensemble des sessions soient enregistrées dans un attribut spécifique de la classe avec leurs logins spécifiques. Ainsi, nous ne seront plus obligés d'enregistrer systématiquement chaque nouveau login dans la session.
import java.io.*;
import java.util.*;
import javax.inject.Singleton;
import javax.json.*;
import javax.websocket.*;
import javax.websocket.server.*;
@ServerEndpoint("/chat/{login}")
@Singleton
public class ServiceChat {
private Map<String, String> messages = new HashMap<>();
private Map<Session, String> sessions = new HashMap<>();
@OnOpen
public void ouverture(Session session, @PathParam("login") String login) throws IOException {
envoyerATous(login + " vient de se connecter");
sessions.put(session, login);
if (messages.containsKey(login)) {
session.getBasicRemote().sendText(messages.get(login));
messages.remove(login);
}
envoyerATous("Connectés : "+sessions.values().toString());
}
@OnClose
public void fermeture(Session session) throws IOException {
String login = sessions.get(session);
sessions.remove(session);
envoyerATous(login + " vient de se déconnecter");
envoyerATous("Connectés : "+sessions.values().toString());
}
@OnMessage
public String chat(Session session, String requête) throws IOException{
JsonObject json = Json.createReader(new StringReader(requête)).readObject();
String cible = json.getString("cible");
String message = json.getString("message");
return envoyerMessage(sessions.get(session), cible, message) ? "Message envoyé" : "Destinataire pas encore connecté";
}
private void envoyerATous(String message) throws IOException {
for (Session chacun : sessions.keySet())
chacun.getBasicRemote().sendText(message);
}
private boolean envoyerMessage(String login, String destinataire, String message) throws IOException {
for (Session chacun : sessions.keySet())
if (destinataire.equals(sessions.get(chacun))) {
chacun.getBasicRemote().sendText(login + " > " + message);
return true;
}
messages.put(destinataire, login+" > "+message);
return false;
}
}
Nous pouvons appliquer encore une autre démarche. Lorsque nous n'avons pas de singleton, l'intérêt c'est qu'il est possible d'avoir des attributs spécifiques à chacun des clients, puisqu'un objet est automatiquement généré pour chaque nouveau client. Ainsi, par exemple, en enregistrant la session dans un attribut dédié, nous n'avons plus besoin d'avoir systématiquement un paramètre de type Session dans les méthodes de rappel. Le singleton est également important lorsque vous désirez factoriser des données communes pour l'ensemble des clients. Je vous propose maintenant de conserver ces deux approches avec deux classes séparées, le singleton d'une part qui permet de stocker les messages en attente, et le WebSocket lui-même cette fois-ci, non singleton qui va utiliser les compétences du singleton.
import java.util.*;
import javax.ejb.*;
@Singleton
@Startup
public class Messages {
private Map<String, String> messages = new HashMap<>();
public void stocker(String login, String message) {
messages.put(login, message);
}
public boolean rechercher(String login) {
return messages.containsKey(login);
}
public String restituer(String login) {
String message = messages.get(login);
messages.remove(login);
return message;
}
}
Il s'agit d'un singleton classique javax.ejb.Singleton. Attention, il faut penser à le démarrer dès le déploiement. C'est
l'annotation @Startup qui réalise cette opération. import java.io.*;
import java.util.*;
import javax.ejb.*;
import javax.inject.*;
import javax.json.*;
import javax.websocket.*;
import javax.websocket.server.*;
@ServerEndpoint("/chat/{login}")
@Stateful
public class ServiceChat {
private String login;
private Session session;
@Inject // ou @EJB
private Messages messages;
@OnOpen
public void ouverture(Session session, @PathParam("login") String login) throws IOException {
this.login = login;
this.session = session;
envoyerATous(login + " vient de se connecter");
session.getUserProperties().put("login", login);
if (messages.rechercher(login))
session.getBasicRemote().sendText(messages.restituer(login));
listeDesConnectés();
}
@OnClose
public void fermeture() throws IOException {
envoyerATous(login + " vient de se déconnecter");
listeDesConnectés();
}
@OnMessage
public String chat(String requête) throws IOException{
JsonObject json = Json.createReader(new StringReader(requête)).readObject();
String cible = json.getString("cible");
String message = json.getString("message");
return envoyerMessage(login, cible, message) ? "Message envoyé" : "Destinataire pas encore connecté";
}
private void envoyerATous(String message) throws IOException {
for (Session chacun : session.getOpenSessions())
if (chacun.isOpen()) chacun.getBasicRemote().sendText(message);
}
private boolean envoyerMessage(String login, String destinataire, String message) throws IOException {
for (Session chacun : session.getOpenSessions())
if (destinataire.equals(chacun.getUserProperties().get("login"))) {
chacun.getBasicRemote().sendText(login + " > " + message);
return true;
}
messages.stocker(destinataire, login+" > "+message);
return false;
}
private void listeDesConnectés() throws IOException {
List<String> liste = new ArrayList<>();
for (Session chacun : session.getOpenSessions())
liste.add((String) chacun.getUserProperties().get("login"));
envoyerATous("Connectés : "+liste.toString());
}
}
Nous retrouvons pratiquement le même codage que lorsque nous n'avions pas de singleton. Attention, actuellement la seule solution
qui permet d'atteindre un EJB est que la classe qui l'appelle soit elle-même un EJB, d'où l'annotation Stateful. C'est bien
entendu ce type d'EJB qu'il faut prendre quisqu'il propose les mêmes fonctionnalités de base qu'un WebSocket. L'exemple précédent fonctionne très bien, mais uniquement si un seul message est conservé pour un destinatiare donné. Si plusieurs expéditeurs désirent envoyer un message à un destinataire non encore connecté, seul le dernier message est envoyé lors de sa connexion, puisque l'archivage des messages se fait au moyen d'une Map Ce qui est loin d'être adapté à ce contexte. L'idéal ici, est d'utiliser une base de données et donc de passer par les entités. Je rappelle que seuls les EJB sont compétents pour la gestion de ces entités, ce qui ne pose pas de problème puisque, comme nous venons de le voir, les WebSocket peuvent être en même temps des beans Session. Nous pouvons aussi, grâce à l'injection, avoir un autre bean Session différent du WebSocket qui s'occupe des entités. Si nous prenons cette dernière version, le bean Session de type Stateless me paraît le plus adapté.
package ws;
import java.io.Serializable;
import java.text.DateFormat;
import java.util.Date;
import javax.persistence.*;
@Entity
@NamedQuery(name="recherche", query="SELECT m FROM Message m WHERE m.destinataire = :destinataire")
public class Message implements Serializable {
@Id
private long id;
private String expéditeur;
private String destinataire;
private String message;
@Temporal(TemporalType.TIMESTAMP)
private Date dateEnvoi;
public long getId() { return id; }
public String getExpéditeur() { return expéditeur; }
public void setExpéditeur(String expéditeur) { this.expéditeur = expéditeur; }
public String getDestinataire() { return destinataire; }
public void setDestinataire(String destinataire) { this.destinataire = destinataire; }
public String getMessage() { return message; }
public void setMessage(String message) { this.message = message; }
public Date getDateEnvoi() { return dateEnvoi; }
public Message() {
id = System.currentTimeMillis();
dateEnvoi = new Date(id);
}
public Message(String expéditeur, String destinataire, String message) {
this.expéditeur = expéditeur;
this.destinataire = destinataire;
this.message = message;
id = System.currentTimeMillis();
dateEnvoi = new Date(id);
}
@Override
public String toString() {
String date = DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT).format(dateEnvoi);
return "["+date+"] "+expéditeur+" > "+message;
}
}
package ws;
import java.io.*;
import java.util.*;
import javax.ejb.*;
import javax.json.*;
import javax.persistence.*;
import javax.websocket.*;
import javax.websocket.server.*;
@ServerEndpoint("/chat/{login}")
@Stateful
public class ServiceChat {
private String login;
private Session session;
@PersistenceContext
private EntityManager bd;
@OnOpen
public void ouverture(Session session, @PathParam("login") String login) throws IOException {
this.login = login;
this.session = session;
envoyerATous(login + " vient de se connecter");
session.getUserProperties().put("login", login);
messagesEnAttente();
listeDesConnectés();
}
@OnClose
public void fermeture() throws IOException {
envoyerATous(login + " vient de se déconnecter");
listeDesConnectés();
}
@OnMessage
public String chat(String requête) throws IOException{
JsonObject json = Json.createReader(new StringReader(requête)).readObject();
String cible = json.getString("cible");
String message = json.getString("message");
return envoyerMessage(cible, message) ? "Message envoyé" : "Destinataire pas encore connecté";
}
private void envoyerATous(String message) throws IOException {
for (Session chacun : session.getOpenSessions())
if (chacun.isOpen()) chacun.getBasicRemote().sendText(message);
}
private void listeDesConnectés() throws IOException {
List<String> liste = new ArrayList<>();
for (Session chacun : session.getOpenSessions())
liste.add((String) chacun.getUserProperties().get("login"));
envoyerATous("Connectés : "+liste.toString());
}
private boolean envoyerMessage(String destinataire, String message) throws IOException {
for (Session chacun : session.getOpenSessions())
if (destinataire.equals(chacun.getUserProperties().get("login"))) {
chacun.getBasicRemote().sendText(login + " > " + message);
return true;
}
bd.persist(new Message(login, destinataire, message));
return false;
}
private void messagesEnAttente() throws IOException {
List<Message> messages = listeDesMessages();
if (!messages.isEmpty())
for (Message message : messages) {
session.getBasicRemote().sendText(message.toString());
bd.remove(message);
}
}
private List<Message> listeDesMessages() {
Query requête = bd.createNamedQuery("recherche");
requête.setParameter("destinataire", login);
return requête.getResultList();
}
}
Les informations, les messages qui arrivent ou qui repartent du WebSocket peuvent être de simples textes que nous pouvons exploiter tout de suite, mais peuvent être aussi des textes formatés avec une structure complexe. Dans ce dernier cas de figure, les formats utilisés sont souvent des documents XML ou plus récemment des documents au format JSON. Dans les projets que nous venons de mettre en oeuvre, c'est d'ailleurs sous ce dernier format que les clients nous communiquent leurs messages. Jusqu'à présent, nous avons décodé le texte à l'intérieur de la méthode de rappel associée à l'annotation @OnMessage. Il serait plus judicieux de séparer cette partie de décodage avec le traitement relatif à la gestion du message lui-même. Les décodeurs et les encodeurs des WebSocket sont là pour résoudre cette séparation et d'avoir ainsi un code plus propre et cohérent même si nous avons plus de classes au final.
La mise en place d'un décodage consiste à transformer, soit un message texte, soit un ensemble d'octets, en une classe qui sera utilisée par la suite directement par le WebSocket les attributs sont remplis par le décodeur. L'encodage est l'opération inverse, c'est-à-dire qu'à partir d'une classe préfabriquée, un message est formaté sous forme d'un texte au format JSON par exemple ou d'un flux binaire adapté. Nous allons reprendre le projet précédent en intégrant un décodeur et un encodeur.
package ws;
import java.io.Serializable;
import java.text.DateFormat;
import java.util.Date;
import javax.persistence.*;
@Entity
@NamedQuery(name="recherche", query="SELECT m FROM Message m WHERE m.destinataire = :destinataire")
public class Message implements Serializable {
@Id
private long id;
private String expéditeur;
private String destinataire;
private String message;
@Temporal(TemporalType.TIMESTAMP)
private Date dateEnvoi;
public long getId() { return id; }
public String getExpéditeur() { return expéditeur; }
public void setExpéditeur(String expéditeur) { this.expéditeur = expéditeur; }
public String getDestinataire() { return destinataire; }
public void setDestinataire(String destinataire) { this.destinataire = destinataire; }
public String getMessage() { return message; }
public void setMessage(String message) { this.message = message; }
public Date getDateEnvoi() { return dateEnvoi; }
public Message() {
id = System.currentTimeMillis();
dateEnvoi = new Date(id);
}
public Message(String expéditeur, String destinataire, String message) {
this.expéditeur = expéditeur;
this.destinataire = destinataire;
this.message = message;
id = System.currentTimeMillis();
dateEnvoi = new Date(id);
}
@Override
public String toString() {
String date = DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT).format(dateEnvoi);
return "["+date+"] "+expéditeur+" > "+message;
}
}
Nous avons besoin d'une classe spécifique pour la phase de décodage. Autant utiliser l'entité Message que nous avons mis
en oeuvre dans le projet précédent puisque nous avons besoin d'enregistrer les informations relatives à l'expéditeur, le destinataire et
le contenu du message à transmettre. package ws;
import java.io.StringReader;
import javax.json.*;
import javax.websocket.*;
public class DécodeurMessage implements Decoder.Text<Message> {
@Override
public Message decode(String envoi) throws DecodeException {
JsonObject json = Json.createReader(new StringReader(envoi)).readObject();
Message message = new Message();
message.setDestinataire(json.getString("cible"));
message.setMessage(json.getString("message"));
return message;
}
@Override
public boolean willDecode(String envoi) {
try{
Json.createReader(new StringReader(envoi)).read(); return true;
}
catch(JsonException e){ return false; }
}
@Override
public void init(EndpointConfig config) { }
@Override
public void destroy() { }
}
package ws;
import javax.websocket.*;
public class EncodeurMessage implements Encoder.Text<Message> {
@Override
public String encode(Message message) throws EncodeException {
return message.toString();
}
@Override
public void init(EndpointConfig config) { }
@Override
public void destroy() { }
}
package ws;
import java.io.*;
import java.util.*;
import javax.ejb.*;
import javax.persistence.*;
import javax.websocket.*;
import javax.websocket.server.*;
@ServerEndpoint(value="/chat/{login}", encoders=EncodeurMessage.class, decoders=DécodeurMessage.class)
@Stateful
public class ServiceChat {
private String login;
private Session session;
@PersistenceContext
private EntityManager bd;
@OnOpen
public void ouverture(Session session, @PathParam("login") String login) throws IOException, EncodeException {
this.login = login;
this.session = session;
envoyerATous(login + " vient de se connecter");
session.getUserProperties().put("login", login);
messagesEnAttente();
listeDesConnectés();
}
@OnClose
public void fermeture() throws IOException {
envoyerATous(login + " vient de se déconnecter");
listeDesConnectés();
}
@OnMessage
public String chat(Message envoi) throws IOException, EncodeException {
envoi.setExpéditeur(login);
return envoyerMessage(envoi) ? "Message envoyé" : "Destinataire pas encore connecté";
}
private void envoyerATous(String message) throws IOException {
for (Session chacun : session.getOpenSessions())
if (chacun.isOpen()) chacun.getBasicRemote().sendText(message);
}
private void listeDesConnectés() throws IOException {
List<String> liste = new ArrayList<>();
for (Session chacun : session.getOpenSessions())
liste.add((String) chacun.getUserProperties().get("login"));
envoyerATous("Connectés : "+liste.toString());
}
private boolean envoyerMessage(Message message) throws IOException, EncodeException {
for (Session chacun : session.getOpenSessions())
if (message.getDestinataire().equals(chacun.getUserProperties().get("login"))) {
chacun.getBasicRemote().sendObject(message);
return true;
}
bd.persist(message);
return false;
}
private void messagesEnAttente() throws IOException, EncodeException {
List<Message> messages = listeDesMessages();
if (!messages.isEmpty())
for (Message message : messages) {
session.getBasicRemote().sendObject(message);
bd.remove(message);
}
}
private List<Message> listeDesMessages() {
Query requête = bd.createNamedQuery("recherche");
requête.setParameter("destinataire", login);
return requête.getResultList();
}
}
Nous allons modifier notre projet en faisant en sorte que la phase d'encodage permette la création de textes au format JSON. Nous allons même réaliser deux encodeurs. Le premier enverra au destinataire un document avec le nom de l'expéditeur, le contenu du message, ainsi que la date de création du message. Le deuxième encodeur enverra la liste des connectés au format JSON.
package ws;
import java.io.StringWriter;
import javax.json.*;
import javax.websocket.*;
public class MessagePersonnel implements Encoder.Text<Message> {
@Override
public String encode(Message message) throws EncodeException {
StringWriter texte = new StringWriter();
JsonWriter json = Json.createWriter(texte);
JsonObjectBuilder réponse = Json.createObjectBuilder();
réponse.add("expéditeur", message.getExpéditeur());
réponse.add("message", message.getMessage());
réponse.add("date", message.getId());
json.writeObject(réponse.build());
json.close();
return texte.toString();
}
@Override
public void init(EndpointConfig config) { }
@Override
public void destroy() { }
}
package ws;
import java.io.StringWriter;
import java.util.List;
import javax.json.*;
import javax.websocket.*;
public class ListeConnectés implements Encoder.Text<List<String>> {
@Override
public String encode(List<String> logins) throws EncodeException {
StringWriter texte = new StringWriter();
JsonWriter json = Json.createWriter(texte);
JsonObjectBuilder connectés = Json.createObjectBuilder();
JsonArrayBuilder liste = Json.createArrayBuilder();
for (String login : logins) liste.add(login);
connectés.add("connectés", liste);
json.writeObject(connectés.build());
json.close();
return texte.toString();
}
@Override
public void init(EndpointConfig config) { }
@Override
public void destroy() { }
}
package ws;
import java.io.*;
import java.util.*;
import javax.ejb.*;
import javax.persistence.*;
import javax.websocket.*;
import javax.websocket.server.*;
@ServerEndpoint(value="/chat/{login}",
encoders = {MessagePersonnel.class, ListeConnectés.class},
decoders = DécodeurMessage.class)
@Stateful
public class ServiceChat {
private String login;
private Session session;
@PersistenceContext
private EntityManager bd;
@OnOpen
public void ouverture(Session session, @PathParam("login") String login) throws IOException, EncodeException {
this.login = login;
this.session = session;
envoyerATous(login + " vient de se connecter");
session.getUserProperties().put("login", login);
messagesEnAttente();
listeDesConnectés();
}
@OnClose
public void fermeture() throws IOException, EncodeException {
envoyerATous(login + " vient de se déconnecter");
listeDesConnectés();
}
@OnMessage
public String chat(Message envoi) throws IOException, EncodeException {
envoi.setExpéditeur(login);
return envoyerMessage(envoi) ? "Message envoyé" : "Destinataire pas encore connecté";
}
private void envoyerATous(String message) throws IOException {
for (Session chacun : session.getOpenSessions())
if (chacun.isOpen()) chacun.getBasicRemote().sendText(message);
}
private void listeDesConnectés() throws IOException, EncodeException {
List<String> liste = new ArrayList<>();
for (Session chacun : session.getOpenSessions())
liste.add((String) chacun.getUserProperties().get("login"));
for (Session chacun : session.getOpenSessions())
if (chacun.isOpen()) chacun.getBasicRemote().sendObject(liste);
}
private boolean envoyerMessage(Message message) throws IOException, EncodeException {
for (Session chacun : session.getOpenSessions())
if (message.getDestinataire().equals(chacun.getUserProperties().get("login"))) {
chacun.getBasicRemote().sendObject(message);
return true;
}
bd.persist(message);
return false;
}
private void messagesEnAttente() throws IOException, EncodeException {
List<Message> messages = listeDesMessages();
if (!messages.isEmpty())
for (Message message : messages) {
session.getBasicRemote().sendObject(message);
bd.remove(message);
}
}
private List<Message> listeDesMessages() {
Query requête = bd.createNamedQuery("recherche");
requête.setParameter("destinataire", login);
return requête.getResultList();
}
}
Nous avons passé pas mal de temps sur la partie service. Dans ce chapitre, nous allons voir comment élaborer un client capable de travailler directement avec le protocole WebSocket et de pouvoir ainsi être en relation avec le service que nous venons de mettre en oeuvre. Pour cela, nous allons utiliser la librairie Qt qui, à partir de la version 5.3, intègre tous les sources correspondants. Notamment, la classe QWebSocket implémente et encapsule tout le comportement inhérent au protocole WebSocket. Il est désormais très facile de créer une application cliente en relation avec notre service WebSocket, ceci avec très peu de lignes de code supplémentaires.
L'objectif ici et de prévoir une application cliente réalisée à l'aide de la librairie Qt avec pour l'instant la même utilisation que l'application Web que nous avons utilisé jusqu'à présent. Dans cette première réalisation nous n'allons pas analyser les messages envoyés par le serveur, nous les afficherons tout simplement sans aucun décriptage. Par contre l'interface permettra de localiser l'emplacement du service et de préciser le nom de l'expéditeur. Vous avez ci-dessous une illustration avec deux clients en communication directes.
QT += core gui network websockets
greaterThan(QT_MAJOR_VERSION, 4): QT += widgets
TARGET = WebSocketClient
TEMPLATE = app
CONFIG += c++11
SOURCES += main.cpp principal.cpp
HEADERS += principal.h
FORMS += principal.ui
Dans ce fichier de projet, il est nécessaire d'intégrer le module concernant le réseau et aussi le module concernant les WebSockets.
#ifndef PRINCIPAL_H
#define PRINCIPAL_H
#include <QMainWindow>
#include "ui_principal.h"
#include <QtWebSockets/QWebSocket>
class Principal : public QMainWindow, public Ui::Principal
{
Q_OBJECT
public:
explicit Principal(QWidget *parent = 0);
private:
QWebSocket webSocket;
bool estConnecte;
private slots:
void commuter();
void envoiMessage();
void receptionMessage(QString texte);
void connexion();
void deconnexion();
private:
void seConnecter();
};
#endif // PRINCIPAL_H
Comme nous l'avons spécifié lors de l'introduction, la classe QWebSocket implémente toutes les fonctionnalités afin de
permettre à une application cliente de se connecter et de communiquer avec un WebSocket. Dans la classe principale de
l'application fenêtrée, nous intégrons un attribut qui est un objet de la classe QWebSocket.#include "principal.h"
Principal::Principal(QWidget *parent) : QMainWindow(parent)
{
setupUi(this);
estConnecte = false;
connect(&webSocket, SIGNAL(textMessageReceived(QString)), this, SLOT(receptionMessage(QString)));
connect(&webSocket, SIGNAL(connected()), this, SLOT(connexion()));
connect(&webSocket, SIGNAL(disconnected()), this, SLOT(deconnexion()));
// connect(&webSocket, SIGNAL(error(QAbstractSocket::SocketError)), this, SLOT(deconnexion()));
}
void Principal::commuter()
{
estConnecte = !estConnecte;
if (estConnecte) seConnecter();
else webSocket.close();
}
void Principal::seConnecter()
{
QString url = "ws://";
url+=adresse->text();
url+=":8080/ws/chat/";
url+=login->text();
webSocket.open(QUrl(url));
}
void Principal::envoiMessage()
{
webSocket.sendTextMessage(message->text());
}
void Principal::receptionMessage(QString texte)
{
reception->append(texte);
}
void Principal::connexion()
{
reception->append("Connecté au service");
}
void Principal::deconnexion()
{
reception->append("Déconnecté du service");
}
Nous allons faire évoluer le projet précédent de telle sorte que maintenant nous nous ne préoccupions plus de formater les messages lorsque nous les envoyons, et que ceux qui arrivent du serveur soient automatiquement mis en forme pour récupérer les clients connectés dans une ComboBox, et que le message reçu d'un autre client soit mis en clair avec connaissance de celui qui l'a envoyé. Voici ci-dessous la nouvelle apparence :
#include "principal.h"
#include <QJsonObject>
#include <QJsonArray>
#include <QJsonDocument>
Principal::Principal(QWidget *parent) : QMainWindow(parent)
{
setupUi(this);
estConnecte = false;
connect(&webSocket, SIGNAL(textMessageReceived(QString)), this, SLOT(receptionMessage(QString)));
connect(&webSocket, SIGNAL(connected()), this, SLOT(connexion()));
connect(&webSocket, SIGNAL(disconnected()), this, SLOT(deconnexion()));
connect(&webSocket, SIGNAL(error(QAbstractSocket::SocketError)), this, SLOT(deconnexion()));
}
void Principal::commuter()
{
estConnecte = !estConnecte;
if (estConnecte) seConnecter();
else webSocket.close();
}
void Principal::seConnecter()
{
QString url = "ws://";
url+=adresse->text();
url+=":8080/ws/chat/";
url+=login->text();
webSocket.open(QUrl(url));
}
void Principal::envoiMessage()
{
if (destinataire->text().isEmpty()) reception->append("ATTENTION, il faut un destinataire");
else {
QJsonObject json;
json["cible"] = destinataire->text();
json["message"] = message->text();
QJsonDocument document(json);
webSocket.sendTextMessage(document.toJson());
}
}
void Principal::receptionMessage(QString texte)
{
QJsonDocument document = QJsonDocument::fromJson(texte.toUtf8());
if (!document.isObject()) reception->append(texte);
else {
QJsonObject json = document.object();
if (json.contains("expéditeur"))
{
QString expediteur = json["expéditeur"].toString();
QString message = json["message"].toString();
reception->append(expediteur+" > "+message);
}
if (json.contains("connectés"))
{
QJsonArray connectes = json["connectés"].toArray();
logins->clear();
for (int i=0; i<connectes.count(); i++)
logins->addItem(connectes[i].toString());
}
}
}
void Principal::connexion()
{
reception->append("Connecté au service");
}
void Principal::deconnexion()
{
reception->append("Déconnecté du service");
logins->clear();
}
Pour l'instant nous nous sommes contentés d'envoyer et de recevoir des messages uniquement sous forme de texte. Durant ce chapitre, nous allons découvrir comment envoyer un ensemble d'octets qui ne représentent plus spécialement un texte, mais n'importe quoi d'autre. Nous allons nous servir de cette possibilité pour envoyer des photos sur le réseau d'un client à l'autre en nous servant du service WebSocket. Avant d'aborder ce projet, nous allons revoir les différents types de message que nous pouvons recevoir et par là même envoyer.
Les messages reçus pouvent être de différentes natures. Nous avons vu qu'il est possible de recevoir et de renvoyer du texte String ou Reader, mais nous pouvons également envoyer et recevoir des messages au format binaire, sous forme d'une suite d'octets ByteBuffer, byte[], InputStream. Un troisième type de message permet de contrôler si la connexion avec le serveur est toujours d'actualité PongMessage.
Pour chacun de ces cas, nous utilisons l'annotation @OnMessage. Il est ainsi tout à fait possible d'avoir au plus 3 méthodes avec cette annotation une méthode pour chaque type de message, texte, binaire et pong.
@OnMessage
public void onTexteMessage(Session session, String message){}
@OnMessage
public void onBinaireMessage(Session session, ByteBuffer message){}
@OnMessage
public void onPongMessage(Session session, PongMessage message){}
Nous allons nous servir de l'ossature du projet précédent en faisant quelques modifications. Notamment, nous ne faisons plus transiter des messages textuels mais uniquement des photos. Malgré cela, il est quant même nécessaire d'envoyer des informations textuelles supplémentaires afin de connaître au moins le destinataire de la photo.
import java.io.Serializable;
import javax.persistence.*;
@Entity
@NamedQuery(name="recherche", query="SELECT m FROM Message m WHERE m.destinataire = :destinataire")
public class Message implements Serializable {
@Id
private long id;
private String expéditeur;
private String destinataire;
public long getId() { return id; }
public String getExpéditeur() { return expéditeur; }
public void setExpéditeur(String expéditeur) { this.expéditeur = expéditeur; }
public String getDestinataire() { return destinataire; }
public void setDestinataire(String destinataire) { this.destinataire = destinataire; }
public Message() { id = System.currentTimeMillis(); }
public Message(String expéditeur, String destinataire, String nomFichier) {
this.expéditeur = expéditeur;
this.destinataire = destinataire;
id = System.currentTimeMillis();
}
}
L'entité Message possède maintenant moins de propriétés, notamment la propriété message, mais également tout ce
qui concerne la date. L'objectif ici est de garder uniquement la relation entre l'expéditeur et le destinataire. import java.io.StringReader;
import javax.json.*;
import javax.websocket.*;
public class DécodeurMessage implements Decoder.Text<Message> {
@Override
public Message decode(String envoi) throws DecodeException {
JsonObject json = Json.createReader(new StringReader(envoi)).readObject();
Message message = new Message();
message.setDestinataire(json.getString("cible"));
return message;
}
@Override
public boolean willDecode(String envoi) {
try{
Json.createReader(new StringReader(envoi)).read(); return true;
}
catch(JsonException e){ return false; }
}
@Override
public void init(EndpointConfig config) { }
@Override
public void destroy() { }
}
Nous gardons le décodeur, mais son seul objetcif maintenant est de connaître le destinataire qui sera utile lorsque la photo sera
envoyée. En effet, le protocole utilisé ici, c'est d'abord d'envoyer un message textuel au format JSON
qui nous indique le destinataire de la photo. Juste après, dans un deuxième temps, le flux d'octets correspondant à l'image est lui aussi
envoyé pour réaliser le transfert vers son destinataire. import java.io.StringWriter;
import javax.json.*;
import javax.websocket.*;
public class MessagePersonnel implements Encoder.Text<Message> {
@Override
public String encode(Message message) throws EncodeException {
StringWriter texte = new StringWriter();
JsonWriter json = Json.createWriter(texte);
JsonObjectBuilder réponse = Json.createObjectBuilder();
réponse.add("expéditeur", message.getExpéditeur());
json.writeObject(réponse.build());
json.close();
return texte.toString();
}
@Override
public void init(EndpointConfig config) { }
@Override
public void destroy() { }
}
L'encodeur MessagePersonnel a lui aussi subit une petite cure de jouvence. Le service envoi un petit message textuel au
format JSON juste pour savoir qui envoie la photo. La photo sera transmise tout de suite
après l'envoi de ce message, à l'image de la réception. import java.io.StringWriter;
import java.util.List;
import javax.json.*;
import javax.websocket.*;
public class ListeConnectés implements Encoder.Text<List<String>> {
@Override
public String encode(List<String> logins) throws EncodeException {
StringWriter texte = new StringWriter();
JsonWriter json = Json.createWriter(texte);
JsonObjectBuilder connectés = Json.createObjectBuilder();
JsonArrayBuilder liste = Json.createArrayBuilder();
for (String login : logins) liste.add(login);
connectés.add("connectés", liste);
json.writeObject(connectés.build());
json.close();
return texte.toString();
}
@Override
public void init(EndpointConfig config) { }
@Override
public void destroy() { }
}
Cet encodeur n'a subit aucune modification. Il nous renseigne toujours sur la liste des clients actuellement connectés. import java.io.*;
import java.nio.ByteBuffer;
import java.util.*;
import javax.ejb.*;
import javax.persistence.*;
import javax.websocket.*;
import javax.websocket.server.*;
@ServerEndpoint(value="/photos/{login}",
encoders = {ListeConnectés.class, MessagePersonnel.class},
decoders = DécodeurMessage.class)
@Stateful
public class ServicePhotos {
private String login;
private Session session;
private Message message;
private final String répertoire = "/home/manu/Images/";
@PersistenceContext
private EntityManager bd;
@OnOpen
public void ouverture(Session session, @PathParam("login") String login) throws IOException, EncodeException {
this.login = login;
this.session = session;
envoyerATous(login + " vient de se connecter");
session.getUserProperties().put("login", login);
photosEnAttente();
listeDesConnectés();
}
@OnClose
public void fermeture() throws IOException, EncodeException {
envoyerATous(login + " vient de se déconnecter");
listeDesConnectés();
}
@OnMessage
public void alerte(Message envoi) {
envoi.setExpéditeur(login);
message = envoi;
}
@OnMessage
public String réceptionPhoto(ByteBuffer photo) throws IOException, EncodeException {
return envoyerPhoto(photo) ? "Photo envoyée" : "Destinataire pas encore connecté";
}
private void envoyerATous(String message) throws IOException, EncodeException {
for (Session chacun : session.getOpenSessions())
if (chacun.isOpen()) chacun.getBasicRemote().sendText(message);
}
private void listeDesConnectés() throws IOException, EncodeException {
List<String> liste = new ArrayList<>();
for (Session chacun : session.getOpenSessions())
liste.add((String) chacun.getUserProperties().get("login"));
for (Session chacun : session.getOpenSessions())
if (chacun.isOpen()) chacun.getBasicRemote().sendObject(liste);
}
private boolean envoyerPhoto(ByteBuffer photo) throws IOException, EncodeException {
for (Session chacun : session.getOpenSessions())
if (message.getDestinataire().equals(chacun.getUserProperties().get("login"))) {
chacun.getBasicRemote().sendObject(message);
chacun.getBasicRemote().sendBinary(photo);
return true;
}
FileOutputStream fichier = new FileOutputStream(répertoire+message.getId());
fichier.write(photo.array());
fichier.close();
bd.persist(message);
return false;
}
private void photosEnAttente() throws IOException, EncodeException {
List<Message> messages = listeDesMessages();
if (!messages.isEmpty())
for (Message message : messages) {
session.getBasicRemote().sendObject(message);
bd.remove(message);
File fichier = new File(répertoire+message.getId());
byte[] octets = new byte[(int)fichier.length()];
FileInputStream flux = new FileInputStream(fichier);
flux.read(octets);
flux.close();
fichier.delete();
ByteBuffer photo = ByteBuffer.wrap(octets);
session.getBasicRemote().sendBinary(photo);
}
}
private List<Message> listeDesMessages() {
Query requête = bd.createNamedQuery("recherche");
requête.setParameter("destinataire", login);
return requête.getResultList();
}
}
Là aussi, nous utilisons l'ossature du projet précédent en ne prenant plus en compte cette fois-ci la saisie des messages, mais nous récupérons une photo présente sur le disque local du client pour l'envoyer ensuite à un destinataire via le service ServicePhotos.
#ifndef IMAGE_H
#define IMAGE_H
#include <QWidget>
class Image : public QWidget
{
Q_OBJECT
public:
explicit Image(QWidget *parent = 0) : QWidget(parent) {}
private slots:
void chargerPhoto();
void sauverPhoto();
protected:
void paintEvent(QPaintEvent *) override;
public:
void chargerPhoto(const QByteArray &octets);
QByteArray getOctets() { return octets; }
private:
QImage photo;
QByteArray octets;
};
#endif // IMAGE_H
Cette classe apporte très peu de commentaire. C'est une classe générique pour permettre l'affichage automatique suivant la surface
du contrôle. Les méthodes chargerPhoto() et sauverPhoto() permettent respectivement de récupérer une photo ou de
la sauvegarder à partir du disque dur du client. En revanche les méthodes chargerPhoto(QByteArray) et getOctets()
permettent de faire la même chose mais depuis le serveur. Ce sont ces méthodes qui vont être utilisées pour transférer des photos pour un
autre client. #include "image.h"
#include <QFileDialog>
#include <QPainter>
#include <QRect>
#include <QFile>
void Image::chargerPhoto()
{
QString nom = QFileDialog::getOpenFileName(this, "Choisissez votre photo", "", "Images (*.jpeg *.jpg)");
if (!nom.isEmpty())
{
QFile fichier(nom);
fichier.open(QIODevice::ReadOnly);
octets = fichier.readAll();
photo.loadFromData(octets);
update();
}
}
void Image::chargerPhoto(const QByteArray &octets)
{
photo.loadFromData(this->octets = octets);
update();
}
void Image::sauverPhoto()
{
if (!photo.isNull())
{
QString nom = QFileDialog::getSaveFileName(this, "Sauvegardez la photo");
if (!nom.isEmpty())
{
QFile fichier(nom);
fichier.open(QIODevice::WriteOnly);
fichier.write(octets);
}
}
}
void Image::paintEvent(QPaintEvent *)
{
if (!photo.isNull())
{
QPainter dessin(this);
double ratio = (double) photo.width() / photo.height();
int largeur = width();
int hauteur = width() / ratio;
QRect cadrage(0, 0, largeur, hauteur);
dessin.drawImage(cadrage, photo, photo.rect());
}
}
#ifndef PRINCIPAL_H
#define PRINCIPAL_H
#include <QMainWindow>
#include "ui_principal.h"
#include <QtWebSockets/QWebSocket>
class Principal : public QMainWindow, public Ui::Principal
{
Q_OBJECT
public:
explicit Principal(QWidget *parent = 0);
private:
QWebSocket webSocket;
bool estConnecte;
private slots:
void commuter();
void envoiMessage();
void receptionMessage(const QString &texte);
void receptionPhoto(const QByteArray &octets);
void connexion();
void deconnexion();
private:
void seConnecter();
};
#endif // PRINCIPAL_H
Nous retrouvons pratiquement le même code que précédemment, si ce n'est le rajout du slot receptionPhoto().
#include "principal.h"
#include <QJsonObject>
#include <QJsonArray>
#include <QJsonDocument>
Principal::Principal(QWidget *parent) : QMainWindow(parent)
{
setupUi(this);
estConnecte = false;
connect(&webSocket, SIGNAL(textMessageReceived(QString)), this, SLOT(receptionMessage(QString)));
connect(&webSocket, SIGNAL(binaryMessageReceived(QByteArray)), this, SLOT(receptionPhoto(QByteArray)));
connect(&webSocket, SIGNAL(connected()), this, SLOT(connexion()));
connect(&webSocket, SIGNAL(disconnected()), this, SLOT(deconnexion()));
connect(&webSocket, SIGNAL(error(QAbstractSocket::SocketError)), this, SLOT(deconnexion()));
}
void Principal::commuter()
{
estConnecte = !estConnecte;
if (estConnecte) seConnecter();
else webSocket.close();
}
void Principal::seConnecter()
{
QString url = "ws://";
url+=adresse->text();
url+=":8080/ws/photos/";
url+=login->text();
webSocket.open(QUrl(url));
}
void Principal::envoiMessage()
{
if (destinataire->text().isEmpty()) reception->append("ATTENTION, il faut un destinataire");
else {
QJsonObject json;
json["cible"] = destinataire->text();
QJsonDocument document(json);
webSocket.sendTextMessage(document.toJson());
webSocket.sendBinaryMessage(photo->getOctets());
}
}
void Principal::receptionMessage(const QString &texte)
{
QJsonDocument document = QJsonDocument::fromJson(texte.toUtf8());
if (!document.isObject()) reception->append(texte);
else {
QJsonObject json = document.object();
if (json.contains("expéditeur"))
{
QString expediteur = json["expéditeur"].toString();
reception->append(expediteur+" envoie une photo");
}
if (json.contains("connectés"))
{
QJsonArray connectes = json["connectés"].toArray();
logins->clear();
for (int i=0; i<connectes.count(); i++)
logins->addItem(connectes[i].toString());
}
}
}
void Principal::receptionPhoto(const QByteArray &octets)
{
photo->chargerPhoto(octets);
}
void Principal::connexion()
{
reception->append("Connecté au service");
}
void Principal::deconnexion()
{
reception->append("Déconnecté du service");
logins->clear();
}
Jusqu'à présent, nous avons mis en oeuvre, d'une part le service utilisant le protocole WebSocket avec du développement
Java, et par ailleurs toute la partie cliente, cette fois-ci codé en C++ à l'aide de la librairie Qt
. Cette dernière permet
également de mettre en place un service WebSocket et c'est l'objet de ce chapitre. L'avantage de proposer du code Java sur la
partie service est que les sources sont très simples à mettre en oeuvre. Tout se fait au moyen des annotations, ce qui permet d'avoir un
code extrêmement allégé. Toute la partie multi-tâches, multi-utilisateurs et multi-sessions se fait automatiquement dans le serveur
d'applications. Dans le code, nous n'avons pas à nous en préoccuper. Avec la librairie Qt
, il en va tout autrement. Vous devrez
gérer tous ces détails, ce qui fait que le code sera plus conséquent. Par contre, vous n'aurait pas besoin de serveur d'applications.
Le code ci-dessous est vraiment très simple et très court. La classe ServiceEcho représente le service. Chaque objet de cette classe représente la connexion avec un client en particulier. C'est le serveur d'application qui les crée et qui les détruit également automatiquement à chaque nouvelle connexion. Chaque client dispose ainsi de sa propre session. La gestion événementielle est assurée par le mécanisme des annotations. Ainsi, à chaque nouvelle connexion, c'est la méthode ouverture() qui apppelée automatiquement grâce à l'annotation @OnOpen. De la même façon, à chaque fois que le client envoie un message, c'est la méthode écho() qui est appelée, grâce à l'annotation @OnMessage. Le fait que chaque client dispose de sa propre session donc de son propre objet, il est possible de proposer des attributs comme c'est le cas avec login.
import java.io.IOException;
import javax.websocket.*;
import javax.websocket.server.*;
@ServerEndpoint("/echo/{login}")
public class ServiceEcho {
private String login;
@OnMessage
public String écho(String message){
return login + " : " + message;
}
@OnOpen
public void ouverture(Session session, @PathParam("login") String login) throws IOException {
this.login = login;
session.getBasicRemote().sendText("Bonjour "+login);
}
}
Reprenons le même projet en utilisant cette fois-ci la librairie Qt. Nous profitons de l'occasion pour mettre en place une IHM qui permettra de lancer ou d'arrêter le service à notre convenance. À titre de vérification des fonctionnalités, je prévois également une zone de texte qui nous permet de suivre les différents événements.
#-------------------------------------------------
#
# Project created by QtCreator 2014-06-08T09:40:14
#
#-------------------------------------------------
QT += core gui websockets
CONFIG += c++11
greaterThan(QT_MAJOR_VERSION, 4): QT += widgets
TARGET = WebSocketServeur
TEMPLATE = app
SOURCES += main.cpp principal.cpp
HEADERS += principal.h
FORMS += principal.ui
Comme pour l'application cliente, pour devez spécifier la prise en compte du module qui gère le protocole WebSocket.
#ifndef PRINCIPAL_H
#define PRINCIPAL_H
#include <QMainWindow>
#include <QWebSocketServer>
#include <QWebSocket>
#include <map>
#include "ui_principal.h"
using namespace std;
class Principal : public QMainWindow, public Ui::Principal
{
Q_OBJECT
public:
explicit Principal(QWidget *parent = 0);
private:
QWebSocketServer service;
map<QWebSocket *, QString> connectes;
public slots:
void demarrer(bool etat);
void nouvelleConnexion();
void receptionMessage(const QString &message);
void deconnexion();
};
#endif // PRINCIPAL_H
La classe représentant le service se nomme QWebSocketServer. Avec la librairie Qt, vous n'avez pas de serveur
d'applications. Vous êtes dans l'obligation de gérer l'ensemble des clients connectés, ce que nous faisons au travers d'une mapqui prend en compte la connexion elle-même et le login du client. Remarquez au passage que la connexion avec le client se fait à l'aide de la classe QWebSocket que nous connaissons déjà.
#include "principal.h"
#include <QUrl>
Principal::Principal(QWidget *parent) : QMainWindow(parent), service("echo", QWebSocketServer::NonSecureMode, this)
{
setupUi(this);
connect(&service, SIGNAL(newConnection()), this, SLOT(nouvelleConnexion()));
}
void Principal::demarrer(bool etat)
{
if (etat) {
if (service.listen(QHostAddress::Any, 8080))
log->append("Lancement du service");
else log->append("Problème de connexion");
bouton->setText("Arrêter service");
}
else {
service.close();
log->append("Arrêt du service");
bouton->setText("Démarrer service");
}
}
void Principal::nouvelleConnexion()
{
QWebSocket *client = service.nextPendingConnection();
QString nom = client->requestUrl().path();
nom.remove('/');
client->sendTextMessage("Bonjour "+nom);
log->append(nom+" vient de se connecter");
connectes[client] = nom;
connect(client, SIGNAL(textMessageReceived(QString)), this, SLOT(receptionMessage(QString)));
connect(client, SIGNAL(disconnected()), this, SLOT(deconnexion()));
}
void Principal::receptionMessage(const QString &message)
{
QWebSocket *client = (QWebSocket *) sender();
QString repondre = connectes[client] + " > "+message;
client->sendTextMessage(repondre);
log->append(repondre);
}
void Principal::deconnexion()
{
QWebSocket *client = (QWebSocket *) sender();
client->deleteLater();
log->append(connectes[client] + " vient de se déconnecter");
connectes.erase(client);
}
handshakedu protocole HTTP. C'est aussi durant cette phase que vous devez préciser si l'échange des messages se fait en mode sécurisé ou pas, soit donc avec le protocole ws:// ou le protocole wss://.
signaux en relation avec des slots. Cela se traduit par l'utilisation de la méthode connect(). Toujours durant cette phase de construction, nous prévoyons le lancement automatique de la méthode nouvelleConnexion() à chaque fois qu'un nouveau client se connecte à ce service.
mapen lui associant son nom de login. Ensuite, il faut mettre en place la gestion événementielle qui nous permettra de récupérer chacun des messages que ce client particulier enverra. De la même façon, il faut se préoccuper de la déconnexion de ce client pour libérer la ressource associée. C'est pour ces raisons que nous utilisons de nouveau deux fois la méthode connect() pour chaque client, les méthodes de rappel sont respectivement receptionMessage() et deconnexion().
mapet détruire l'objet QWebObjet qui ne sera alors plus utile, ce que fait la méthode deleteLater().
Nous allons reprendre le même projet qui consiste à avertir tous les clients qu'un nouveau client se connecte ou se déconnecte. Par ailleurs, à chaque fois qu'un client apparaît ou disparaît, la liste des clients est mise à jour et est envoyé à ceux qui restent.
#ifndef PRINCIPAL_H
#define PRINCIPAL_H
#include <QMainWindow>
#include <QWebSocketServer>
#include <QWebSocket>
#include <map>
#include "ui_principal.h"
using namespace std;
class Principal : public QMainWindow, public Ui::Principal
{
Q_OBJECT
public:
explicit Principal(QWidget *parent = 0);
private:
QWebSocketServer service;
map<QWebSocket *, QString> connectes;
public slots:
void demarrer(bool etat);
void nouvelleConnexion();
void receptionMessage(const QString &message);
void deconnexion();
private:
void envoyerATous(const QString &message);
void listeDesConnectes();
};
#endif // PRINCIPAL_H
Nous rajoutons deux nouvelles méthodes au projet précédent, savoir envoyerATous() et listeDesConnectes().
#include "principal.h"
#include <QUrl>
Principal::Principal(QWidget *parent) : QMainWindow(parent), service("echo", QWebSocketServer::NonSecureMode, this)
{
setupUi(this);
connect(&service, SIGNAL(newConnection()), this, SLOT(nouvelleConnexion()));
}
void Principal::demarrer(bool etat)
{
if (etat) {
if (service.listen(QHostAddress::Any, 8080))
log->append("Lancement du service");
else log->append("Problème de connexion");
bouton->setText("Arrêter service");
}
else {
service.close();
log->append("Arrêt du service");
bouton->setText("Démarrer service");
}
}
void Principal::nouvelleConnexion()
{
QWebSocket *client = service.nextPendingConnection();
QString nom = client->requestUrl().path();
nom.remove('/');
client->sendTextMessage("Bonjour "+nom);
log->append(nom+" vient de se connecter");
envoyerATous(nom+" vient de se connecter");
connectes[client] = nom;
listeDesConnectes();
connect(client, SIGNAL(textMessageReceived(QString)), this, SLOT(receptionMessage(QString)));
connect(client, SIGNAL(disconnected()), this, SLOT(deconnexion()));
}
void Principal::receptionMessage(const QString &message)
{
QWebSocket *client = (QWebSocket *) sender();
QString repondre = connectes[client] + " > "+message;
client->sendTextMessage(repondre);
log->append(repondre);
}
void Principal::deconnexion()
{
QWebSocket *client = (QWebSocket *) sender();
client->deleteLater();
QString nom = connectes[client];
log->append(nom + " vient de se déconnecter");
connectes.erase(client);
envoyerATous(nom + " vient de se déconnecter");
listeDesConnectes();
}
void Principal::envoyerATous(const QString &message)
{
for (auto &client : connectes) client.first->sendTextMessage(message);
}
void Principal::listeDesConnectes()
{
QString liste = "[";
for (auto &client : connectes) liste.append(" "+client.second);
liste.append(" ]");
envoyerATous("Connectés : "+liste);
}
Mise à part le code de ces deux méthodes supplémentaires, nous n'avons pas besoin de rajouter une structure complexe. Tout ce que
nous avons fait jusqu'à présent est suffisant puisque dans la structure de base, nous sommes obligé de gérer, dans sa globalité, le point
de connexion de chaque client. Vous remarquez ainsi, que le projet soit simple ou compliqué, il est nécessaire d'avoir une ossature de
base minimale qui permet de gérer finalement toutes les situations. Nous allons reprendre le projet qui permet à un client de communiquer avec un autre client à l'image d'un chat. Je rapelle que nous devons utiliser le format JSON afin de pouvoir identifier le destinataire et le contenu du message lui-même.
#include "principal.h"
#include <QUrl>
#include <QJsonObject>
#include <QJsonDocument>
Principal::Principal(QWidget *parent) : QMainWindow(parent), service("chat", QWebSocketServer::NonSecureMode, this)
{
setupUi(this);
connect(&service, SIGNAL(newConnection()), this, SLOT(nouvelleConnexion()));
}
void Principal::demarrer(bool etat)
{
if (etat) {
if (service.listen(QHostAddress::Any, 8080))
log->append("Lancement du service");
else log->append("Problème de connexion");
bouton->setText("Arrêter service");
}
else {
service.close();
log->append("Arrêt du service");
bouton->setText("Démarrer service");
}
}
void Principal::nouvelleConnexion()
{
QWebSocket *client = service.nextPendingConnection();
QString nom = client->requestUrl().path();
nom.remove('/');
client->sendTextMessage("Bonjour "+nom);
log->append(nom+" vient de se connecter");
envoyerATous(nom+" vient de se connecter");
connectes[client] = nom;
listeDesConnectes();
connect(client, SIGNAL(textMessageReceived(QString)), this, SLOT(receptionMessage(QString)));
connect(client, SIGNAL(disconnected()), this, SLOT(deconnexion()));
}
void Principal::receptionMessage(const QString &texte)
{
QWebSocket *client = (QWebSocket *) sender();
QString expediteur = connectes[client];
QJsonDocument document = QJsonDocument::fromJson(texte.toUtf8());
QJsonObject json = document.object();
QString destinataire = json["cible"].toString();
QString message = json["message"].toString();
bool trouve = false;
for (auto &cible : connectes)
if (cible.second == destinataire) {
cible.first->sendTextMessage(expediteur+" > "+message);
client->sendTextMessage("Message envoyé");
trouve = true;
}
if (!trouve) client->sendTextMessage("Cible inconnue");
}
void Principal::deconnexion()
{
QWebSocket *client = (QWebSocket *) sender();
client->deleteLater();
QString nom = connectes[client];
log->append(nom + " vient de se déconnecter");
connectes.erase(client);
envoyerATous(nom + " vient de se déconnecter");
listeDesConnectes();
}
void Principal::envoyerATous(const QString &message)
{
for (auto &client : connectes) client.first->sendTextMessage(message);
}
void Principal::listeDesConnectes()
{
QString liste = "[";
for (auto &client : connectes) liste.append(" "+client.second);
liste.append(" ]");
envoyerATous("Connectés : "+liste);
}
Dans tout le source seule la méthode receptionMessage() subit un changement. Cette fois-ci nous devons décoder le
texte reçu au format JSON. L'objectif est de retrouver le destinataire avec le contenu du
message. Nous connaissons déjà les classes relatives au décodage du texte, notamment QJsonDocument et QJsonObject.
Nous allons reprendre le projet précédent en le complétant de telle sorte que si un client envoie un message pour un autre client qui n'est pas encore connecté, le message est conservé en mémoire jusqu'à ce que ce dernier se connecte à son tour et reçoive alors le message qui lui était dédié. Dans ce cas de figure, nous avons à la fois une messagerie instantanée couplée avec une messagerie asynchrone classique, comme le courrier électronique.
#ifndef PRINCIPAL_H
#define PRINCIPAL_H
#include <QMainWindow>
#include <QWebSocketServer>
#include <QWebSocket>
#include <map>
#include "ui_principal.h"
using namespace std;
class Principal : public QMainWindow, public Ui::Principal
{
Q_OBJECT
public:
explicit Principal(QWidget *parent = 0);
private:
QWebSocketServer service;
map<QWebSocket *, QString> connectes;
map<QString, QString> messages;
public slots:
void demarrer(bool etat);
void nouvelleConnexion();
void receptionMessage(const QString &texte);
void deconnexion();
private:
void envoyerATous(const QString &message);
void listeDesConnectes();
};
#endif // PRINCIPAL_H
Dans la déclaration de la classe, je rajoute une nouvelle mapqui permet de conserver temporairement le message lorsque le destinataire n'est pas encore connecté. Dans l'absolu, il aurait été préférable de prendre plutôt un
multimap. Dans cette
mapsera enregistrée le nom du destinataire et le contenu du message.
#include "principal.h"
#include <QUrl>
#include <QJsonObject>
#include <QJsonArray>
#include <QJsonDocument>
Principal::Principal(QWidget *parent) : QMainWindow(parent), service("chat", QWebSocketServer::NonSecureMode, this)
{
setupUi(this);
connect(&service, SIGNAL(newConnection()), this, SLOT(nouvelleConnexion()));
}
void Principal::demarrer(bool etat)
{
if (etat) {
if (service.listen(QHostAddress::Any, 8080))
log->append("Lancement du service");
else log->append("Problème de connexion");
bouton->setText("Arrêter service");
}
else {
service.close();
log->append("Arrêt du service");
bouton->setText("Démarrer service");
}
}
void Principal::nouvelleConnexion()
{
QWebSocket *client = service.nextPendingConnection();
QString nom = client->requestUrl().path();
nom.remove('/');
client->sendTextMessage("Bonjour "+nom);
log->append(nom+" vient de se connecter");
envoyerATous(nom+" vient de se connecter");
connectes[client] = nom;
listeDesConnectes();
auto recherche = messages.find(nom);
if (recherche!=messages.end()) {
client->sendTextMessage(recherche->second);
messages.erase(nom);
}
connect(client, SIGNAL(textMessageReceived(QString)), this, SLOT(receptionMessage(QString)));
connect(client, SIGNAL(disconnected()), this, SLOT(deconnexion()));
}
void Principal::receptionMessage(const QString &texte)
{
QWebSocket *client = (QWebSocket *) sender();
QString expediteur = connectes[client];
QJsonDocument document = QJsonDocument::fromJson(texte.toUtf8());
QJsonObject json = document.object();
QString destinataire = json["cible"].toString();
QString message = json["message"].toString();
bool trouve = false;
for (auto &cible : connectes)
if (cible.second == destinataire) {
cible.first->sendTextMessage(expediteur+" > "+message);
client->sendTextMessage("Message envoyé");
trouve = true;
}
if (!trouve) {
client->sendTextMessage("Destinataire non encore connecté");
messages[destinataire] = expediteur+" > "+message;
}
}
void Principal::deconnexion()
{
QWebSocket *client = (QWebSocket *) sender();
client->deleteLater();
QString nom = connectes[client];
log->append(nom + " vient de se déconnecter");
connectes.erase(client);
envoyerATous(nom + " vient de se déconnecter");
listeDesConnectes();
disconnect(client, SIGNAL(textMessageReceived(QString)), this, SLOT(receptionMessage(QString)));
disconnect(client, SIGNAL(disconnected()), this, SLOT(deconnexion()));
}
void Principal::envoyerATous(const QString &message)
{
for (auto &client : connectes) client.first->sendTextMessage(message);
}
void Principal::listeDesConnectes()
{
QString liste = "[";
for (auto &client : connectes) liste.append(" "+client.second);
liste.append(" ]");
envoyerATous("Connectés : "+liste);
}
Nous allons conclure ce chapitre en mettant en oeuvre un service d'envoi de photo entre deux clients connectés ou pas. Nous avons déjà abordé cette étude lors du chapitre précédent. Nous disposons du code côté client. Il ne nous reste plus qu'à établir l'architecture du service pour réaliser ces fonctionnalités. Vous avez ci-dessous deux vues de l'application cliente.
#ifndef PRINCIPAL_H
#define PRINCIPAL_H
#include <QMainWindow>
#include <QWebSocketServer>
#include <QWebSocket>
#include <map>
#include "ui_principal.h"
using namespace std;
struct Photo
{
QString expediteur;
QString destinataire;
QString fichier;
};
class Principal : public QMainWindow, public Ui::Principal
{
Q_OBJECT
public:
explicit Principal(QWidget *parent = 0);
private:
QWebSocketServer service;
map<QWebSocket *, QString> connectes;
map<QString, Photo> photos;
Photo photo;
int id = 1;
public slots:
void demarrer(bool etat);
void nouvelleConnexion();
void receptionMessage(const QString &texte);
void receptionPhoto(const QByteArray &octets);
void deconnexion();
private:
void envoyerATous(const QString &message);
void listeDesConnectes();
};
#endif // PRINCIPAL_H
mapa changée de nom, nous avons maintenant l'attribut photos à la place de messages. La structure de la
mapn'est plus la même puisque cette fois-ci à chaque destinataire nous enregistrons les informations issues de la structure Photo.
#include "principal.h"
#include <QUrl>
#include <QJsonObject>
#include <QJsonArray>
#include <QJsonDocument>
#include <QFile>
Principal::Principal(QWidget *parent) : QMainWindow(parent), service("chat", QWebSocketServer::NonSecureMode, this)
{
setupUi(this);
connect(&service, SIGNAL(newConnection()), this, SLOT(nouvelleConnexion()));
}
void Principal::demarrer(bool etat)
{
if (etat) {
if (service.listen(QHostAddress::Any, 8080))
log->append("Lancement du service");
else log->append("Problème de connexion");
bouton->setText("Arrêter service");
}
else {
service.close();
log->append("Arrêt du service");
bouton->setText("Démarrer service");
}
}
void Principal::nouvelleConnexion()
{
QWebSocket *client = service.nextPendingConnection();
QString nom = client->requestUrl().path();
nom.remove('/');
client->sendTextMessage("Bonjour "+nom);
log->append(nom+" vient de se connecter");
envoyerATous(nom+" vient de se connecter");
connectes[client] = nom;
listeDesConnectes();
auto recherche = photos.find(nom);
if (recherche!=photos.end()) {
client->sendTextMessage("Photo de "+recherche->second.expediteur);
QFile fichier(recherche->second.fichier);
fichier.open(QIODevice::ReadOnly);
QByteArray octets = fichier.readAll();
client->sendBinaryMessage(octets);
photos.erase(nom);
fichier.remove();
}
connect(client, SIGNAL(textMessageReceived(QString)), this, SLOT(receptionMessage(QString)));
connect(client, SIGNAL(binaryMessageReceived(QByteArray)), this, SLOT(receptionPhoto(QByteArray)));
connect(client, SIGNAL(disconnected()), this, SLOT(deconnexion()));
}
void Principal::receptionMessage(const QString &texte)
{
QWebSocket *client = (QWebSocket *) sender();
photo.expediteur = connectes[client];
QJsonDocument document = QJsonDocument::fromJson(texte.toUtf8());
QJsonObject json = document.object();
photo.destinataire = json["cible"].toString();
}
void Principal::receptionPhoto(const QByteArray &octets)
{
QWebSocket *client = (QWebSocket *) sender();
bool trouve = false;
for (auto &cible : connectes)
if (cible.second == photo.destinataire) {
cible.first->sendTextMessage("Photo de "+photo.expediteur);
cible.first->sendBinaryMessage(octets);
client->sendTextMessage("Photo envoyée");
trouve = true;
}
if (!trouve) {
client->sendTextMessage("Destinataire pas encore connecté");
photo.fichier = QString("/home/manu/Images/%1").arg(id++);
photos[photo.destinataire] = photo;
QFile fichier(photo.fichier);
fichier.open(QIODevice::WriteOnly);
fichier.write(octets);
}
}
void Principal::deconnexion()
{
QWebSocket *client = (QWebSocket *) sender();
client->deleteLater();
QString nom = connectes[client];
log->append(nom + " vient de se déconnecter");
connectes.erase(client);
envoyerATous(nom+" vient de se déconnecter");
listeDesConnectes();
disconnect(client, SIGNAL(textMessageReceived(QString)), this, SLOT(receptionMessage(QString)));
disconnect(client, SIGNAL(binaryMessageReceived(QByteArray)), this, SLOT(receptionPhoto(QByteArray)));
disconnect(client, SIGNAL(disconnected()), this, SLOT(deconnexion()));
}
void Principal::envoyerATous(const QString &message)
{
for (auto &client : connectes) client.first->sendTextMessage(message);
}
void Principal::listeDesConnectes()
{
QJsonArray liste;
for (auto &client : connectes) liste.append(client.second);
QJsonObject json;
json["connectés"] = liste;
QJsonDocument document(json);
for (auto &client : connectes) client.first->sendTextMessage(document.toJson());
}
mapphotos dans l'attente de la connexion du destinataire.
mapphotos à la recherche de l'expéditeur. Ce dernier sera alors averti sous forme de texte. Il faut ensuite récupérer les octets du fichier photo sauvegardé temporairement et les envoyés directement au destinataire. Il faut penser enfin à supprimer le fichier photo ainsi que les informations relatives dans la
mapphotos.
Dans ce dernier chapitre, nous allons reprendre à la fois le projet du service chat avec son application cliente correspondante dans
l'environnement de Qt. Nous allons rajouter des fonctionnalités qui vont nous permettre de travailler avec les Icon Tray Ajout
d'une notification dans la barre des tâches du système d'exploitation. Nous verrons également comment utiliser un fichier de ressources
et comment prévoir un fichier de configuration nous évitant d'écrire systématiquement les mêmes paramètres au départ, l'application cliente
pourra ainsi se connecter directement au service.
Jusqu'à présent, nous avons étudié l'accès aux données dans des fichiers externes, mais avec Qt, il est également possible
d'intégrer du texte ou des données binaires dans l'exécutable de l'application. Pour ce faire, il convient d'utiliser le système de ressources
de Qt.
*.qrcqui répertorie les fichiers à intégrer dans l'exécutable. Les ressources sont ensuite converties en code C++ par l'utilitaire standard intégré rcc le compilateur de ressources de Qt. Depuis l'application, les ressources sont identifiées par le préfixe de chemin, ici /icones.
Les systèmes d'exploitation
, quels qu'ils soient, disposent tous d'un système de notifications, représentées par les icônes des
applications, dans la barre des tâches, juste à côté de l'horloge. Ces différentes applications ont la particularité d'être actives dès le
démarrage du système d'exploitation
. Il suffit alors de cliquer sur l'icône souhaitée pour faire apparaître le menu correspondant et
de lancer ainsi l'action désirée.
Par ailleurs, suivant le déroulement de l'application, des petites bulles d'aide peuvent apparaître pour notifier que certains événements ont bien été prise en compte. Dans la librairie Qt, la classe qui représente une notification s'appelle QSystemIconTray.
Dans votre application, si vous désirez mettre en œuvre une notification, vous devez donc créer un objet relatif à la classe QSystemIconTray. Durant la phase de création, vous devez indiquer quel est l'élément qui lance cette notification, généralement vous spécifiez le pointeur this. Vous pouvez également proposer à ce moment là, l'icône de notification. Vous devez ensuite proposer le menu contextuel qui est à construire au préalable au moyen de la méthode setContextMenu(). Il faut bien entendu rendre visible votre notification à l'aide de la méthode show(). Enfin, suivant des événements particuliers, vous pouvez visualiser des messages contextuels au moyen de la méthode showMessage().
Un objet de la classe QFile représente un fichier spécifique du système de fichier local quelque soit la plate-forme utilisée. Vous spécifiez le nom du fichier désiré au moment de la construction. Par la suite, pour exploiter le contenu du fichier, vous devez l'ouvrir en spécifiant le mode d'ouverture requis, lecture ou écriture, tout ceci au moyen de la méthode open().
/quelque soit le système de fichier, même pour Windows. ATTENTION, le symbole
\n'est pas du tout supporté.
enum Permission {ReadOwner,
WriteOwner, ExeOwner, ReadUser, WriteUser, ExeUser, ReadGroup, WriteGroup, ExeGroup, ReadOther, WriteOther, ExeOther};
La façon la plus simple de charger et d'enregistrer des données binaires avec Qt consiste à prendre un objet de type QFile, à ouvrir le fichier et à y accéder par le biais d'un objet QDataStream. Ce dernier fournit un format de stockage indépendant de la plate-forme, qui supporte les types C++ de base tels que les int et double, de nombreux types de données Qt, dont QByteArray, QFont, QImage, QPixmap et QString ainsi que des classes conteneur telles que QList<T>. Un QByteArray est un simple tableau d'octets représenté sous la forme d'un décompte d'octets, suivi des octets eux-mêmes.
Si le fichier s'ouvre avec succès, nous créons un flux QDataStream dans lequel nous plaçons successivement l'ensemble des informations de natures totalement différentes à l'aide du simple opérateur que nous connaissons bien <<. Ces informations sont bien enregistrées sous forme de suite d'octets et finalement stockées dans le fichier correspondant.Les formats de fichiers binaires sont généralement plus compacts que ceux basés sur le texte, mais ils ne sont pas lisibles ou modifiables par l'homme. Si vous désirez pouvoir consulter les données avec un simple éditeur, il est possible d'utiliser à la place les formats texte. Qt fournit la classe QTextStream pour lire et écrire des fichiers de texte brut, mais également d'autres formats de texte comme le HTML ou le XML ainsi que du code source.
QTextStream se charge automatiquement de la conversion entre Unicode et le codage local prévu par le système de fichier en cours, et gère de façon transparente les conventions de fin de ligne utilisées par les différents systèmes d'exploitation \r\n sur Windows et \n sur Unix et Mac OS. En plus des caractères et des chaînes, QTextStream prend en charge les types numériques de base du C++, qu'il convertit alors automatiquement en chaînes. L'écriture du texte est très facile, mais sa lecture peut représenter un véritable défit, car les données textuelles, contrairement aux données binaires écrites au moyen de QDataStream, sont fondamentalement ambiguës. Afin d'éviter ce genre d'inconvénient, il serait peut-être souhaitable d'enregistrer le texte ligne par ligne au moyen du terminateur endl et de faire de même lors de la lecture au moyen de la méthode readLine() de QTextStream qui récupère à chaque occurrence une seule ligne du texte enregistré. Il est également possible de traiter le texte en entier. Nous pouvons ainsi lire le fichier complet en une seule fois à l'aide de la méthode readAll() de QTextStream, si nous ne nous préoccupons pas bien sûr de l'utilisation de la mémoire, ou si nous savons que le fichier est petit.Nous allons reprendre le service chat
que nous avons déjà constitué. Nous en profiterons pour mettre en place le système de
notification. Par ailleurs, l'interface sera épuré avec toutefois la possibilité de choisir le numéro de port.
#include "principal.h"
#include <QApplication>
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
Principal w;
// w.show();
return a.exec();
}
Qt Creator génère automatiquement ce fichier et généralement nous pouvons le laisser tel quel. Ici toutefois, nous
devons empêcher que la fenêtre s'affiche dès le départ. C'est pour cette raison que j'ai placé en commentaire w.show()
.
#ifndef PRINCIPAL_H
#define PRINCIPAL_H
#include <QMainWindow>
#include <QWebSocketServer>
#include <QWebSocket>
#include <map>
#include <QSystemTrayIcon>
#include "ui_principal.h"
using namespace std;
class Principal : public QMainWindow, public Ui::Principal
{
Q_OBJECT
public:
explicit Principal(QWidget *parent = 0);
private:
QWebSocketServer service;
map<QWebSocket *, QString> connectes;
map<QString, QString> messages;
bool quitter = false;
QSystemTrayIcon *notification;
public slots:
void demarrer(bool valide);
void nouvelleConnexion();
void receptionMessage(const QString &texte);
void deconnexion();
void arreter();
protected:
void closeEvent(QCloseEvent *);
private:
void envoyerATous(const QString &message);
void listeDesConnectes();
void setNotification();
};
#endif // PRINCIPAL_H
#include "principal.h"
#include <QUrl>
#include <QJsonObject>
#include <QJsonArray>
#include <QJsonDocument>
#include <QMenu>
#include <QCloseEvent>
Principal::Principal(QWidget *parent) : QMainWindow(parent), service("chat", QWebSocketServer::NonSecureMode, this)
{
setupUi(this);
connect(&service, SIGNAL(newConnection()), this, SLOT(nouvelleConnexion()));
setNotification();
}
void Principal::demarrer(bool valide)
{
if (valide) {
if (service.listen(QHostAddress::Any, port->value()))
{
etat->showMessage("Lancement du service");
notification->showMessage("Action", "Lancement du service", QSystemTrayIcon::Information);
}
else etat->showMessage("Problème de connexion");
actionService->setText("Arrêter le service");
port->setReadOnly(true);
}
else {
service.close();
etat->showMessage("Arrêt du service");
notification->showMessage("Action", "Arrêt du service", QSystemTrayIcon::Information);
actionService->setText("Démarrer le service");
port->setReadOnly(false);
}
}
void Principal::nouvelleConnexion()
{
QWebSocket *client = service.nextPendingConnection();
QString nom = client->requestUrl().path();
nom.remove('/');
client->sendTextMessage("Bonjour "+nom);
etat->showMessage(nom+" vient de se connecter");
notification->showMessage("Evénement", nom+" vient de se connecter", QSystemTrayIcon::Information);
envoyerATous(nom+" vient de se connecter");
connectes[client] = nom;
listeDesConnectes();
auto recherche = messages.find(nom);
if (recherche!=messages.end()) {
client->sendTextMessage(recherche->second);
messages.erase(nom);
}
connect(client, SIGNAL(textMessageReceived(QString)), this, SLOT(receptionMessage(QString)));
connect(client, SIGNAL(disconnected()), this, SLOT(deconnexion()));
}
void Principal::receptionMessage(const QString &texte)
{
QWebSocket *client = (QWebSocket *) sender();
QString expediteur = connectes[client];
QJsonDocument document = QJsonDocument::fromJson(texte.toUtf8());
QJsonObject json = document.object();
QString destinataire = json["cible"].toString();
QString message = json["message"].toString();
bool trouve = false;
for (auto &cible : connectes)
if (cible.second == destinataire) {
cible.first->sendTextMessage(expediteur+" > "+message);
client->sendTextMessage("Message envoyé");
trouve = true;
}
if (!trouve) {
client->sendTextMessage("Destinataire non encore connecté");
messages[destinataire] = expediteur+" > "+message;
}
}
void Principal::deconnexion()
{
QWebSocket *client = (QWebSocket *) sender();
client->deleteLater();
QString nom = connectes[client];
etat->showMessage(nom + " vient de se déconnecter");
notification->showMessage("Evénement", nom+" vient de se déconnecter", QSystemTrayIcon::Information);
connectes.erase(client);
envoyerATous(nom + " vient de se déconnecter");
listeDesConnectes();
disconnect(client, SIGNAL(textMessageReceived(QString)), this, SLOT(receptionMessage(QString)));
disconnect(client, SIGNAL(disconnected()), this, SLOT(deconnexion()));
}
void Principal::envoyerATous(const QString &message)
{
for (auto &client : connectes) client.first->sendTextMessage(message);
}
void Principal::listeDesConnectes()
{
QJsonArray liste;
for (auto &client : connectes) liste.append(client.second);
QJsonObject json;
json["connectés"] = liste;
QJsonDocument document(json);
for (auto &client : connectes) client.first->sendTextMessage(document.toJson());
}
void Principal::setNotification()
{
QMenu *menu = new QMenu(this);
menu->addAction(actionService);
menu->addAction(actionAfficher);
menu->addSeparator();
menu->addAction(actionQuitter);
notification = new QSystemTrayIcon(QIcon(":/icones/ressources/chat.png"), this);
notification->setContextMenu(menu);
notification->show();
notification->showMessage("Bonjour", "Service chat", QSystemTrayIcon::Information);
}
void Principal::closeEvent(QCloseEvent *evt)
{
if (!quitter) {
hide();
evt->ignore();
}
}
void Principal::arreter()
{
quitter = true;
close();
}
enum {NoIcon, Information, Warning, Critical};
Nous allons également modifier l'application cliente concernant le service de chat
. Cette fois-ci, la fenêtre principale de
l'application dispose de deux onglets. Le premier sert à déterminer tous les paramètres de communication. Une fois que l'application est
déployée sur un poste particulier, les paramètres sont automatiquement mis en place grâce à un fichier de configuration nommé config
et placé dans le répertoire ressources. Le deuxième onglet concerne tout simplement l'envoi et la réception des messages.
{
"adresse": "192.168.1.75",
"automatique": true,
"login": "bruno",
"port": "8080"
}
Si vous remarquez bien, le texte de ce fichier est au format JSON. Lors des
différents chapitres, nous avons découvert les objets permettant de lire ou d'écrire vers ce format là. C'est simple et rapide à faire.
#include "principal.h"
#include <QApplication>
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
Principal w;
// w.show();
return a.exec();
}
Qt Creator génère automatiquement ce fichier et généralement nous pouvons le laisser tel quel. Ici toutefois, nous
devons empêcher que la fenêtre s'affiche dès le départ. C'est pour cette raison que j'ai placé en commentaire w.show()
.
#ifndef PRINCIPAL_H
#define PRINCIPAL_H
#include <QMainWindow>
#include <QtWebSockets/QWebSocket>
#include <QSystemTrayIcon>
#include "ui_principal.h"
class Principal : public QMainWindow, public Ui::Principal
{
Q_OBJECT
public:
explicit Principal(QWidget *parent = 0);
private:
QWebSocket webSocket;
bool estConnecte = false, quitter = false;
QSystemTrayIcon *notification;
private slots:
void commuter();
void envoiMessage();
void receptionMessage(const QString &texte);
void connexion();
void deconnexion();
void enregistrerConfig();
void arreter();
protected:
void closeEvent(QCloseEvent *);
private:
void seConnecter();
void lireConfig();
void setNotification();
};
#endif // PRINCIPAL_H
Nous remarquons les mêmes attributs et méthodes que pour le serveur, savoir le système de notification et de clôture de
l'application. Nous avons en plus deux méthodes lireConfig() et ecrireConfig() qui permettent respectivement de
récupérer les paramètres du fichier config ou d'en proposer de nouveaux. #include "principal.h"
#include <QJsonObject>
#include <QJsonArray>
#include <QJsonDocument>
#include <QFile>
#include <QMenu>
#include <QCloseEvent>
Principal::Principal(QWidget *parent) : QMainWindow(parent)
{
setupUi(this);
setNotification();
lireConfig();
connect(&webSocket, SIGNAL(textMessageReceived(QString)), this, SLOT(receptionMessage(QString)));
connect(&webSocket, SIGNAL(connected()), this, SLOT(connexion()));
connect(&webSocket, SIGNAL(disconnected()), this, SLOT(deconnexion()));
connect(&webSocket, SIGNAL(error(QAbstractSocket::SocketError)), this, SLOT(deconnexion()));
}
void Principal::commuter()
{
estConnecte = !estConnecte;
if (estConnecte && !login->text().isEmpty()) seConnecter();
else webSocket.close();
}
void Principal::seConnecter()
{
QString url = "ws://";
url+=adresse->text()+":";
url+=port->text()+"/";
url+=login->text();
webSocket.open(QUrl(url));
onglets->setCurrentIndex(1);
}
void Principal::envoiMessage()
{
if (destinataire->text().isEmpty()) reception->append("ATTENTION, il faut un destinataire");
else {
QJsonObject json;
json["cible"] = destinataire->text();
json["message"] = message->text();
QJsonDocument document(json);
webSocket.sendTextMessage(document.toJson());
}
}
void Principal::receptionMessage(const QString &texte)
{
QJsonDocument document = QJsonDocument::fromJson(texte.toUtf8());
if (!document.isObject()) {
reception->append(texte);
notification->showMessage("Notification", texte, QSystemTrayIcon::Information);
}
else {
QJsonObject json = document.object();
if (json.contains("expéditeur"))
{
QString expediteur = json["expéditeur"].toString();
QString message = json["message"].toString();
reception->append(expediteur+" > "+message);
notification->showMessage("Nouveau message", expediteur+" > "+message, QSystemTrayIcon::Information);
}
if (json.contains("connectés"))
{
QJsonArray connectes = json["connectés"].toArray();
logins->clear();
for (int i=0; i<connectes.count(); i++)
if (connectes[i].toString() != login->text())
logins->addItem(connectes[i].toString());
}
}
}
void Principal::connexion()
{
reception->append("Connecté au service");
}
void Principal::deconnexion()
{
reception->append("Déconnecté du service");
logins->clear();
}
void Principal::lireConfig()
{
QFile fichier("ressources/config");
if (fichier.open(QIODevice::ReadOnly))
{
QTextStream lire(&fichier);
QJsonDocument document = QJsonDocument::fromJson(lire.readAll().toUtf8());
QJsonObject json = document.object();
adresse->setText(json["adresse"].toString());
port->setText(json["port"].toString());
login->setText(json["login"].toString());
bool lancer = json["automatique"].toBool();
automatique->setChecked(lancer);
if (lancer) boutonConnexion->click();
}
}
void Principal::enregistrerConfig()
{
QFile fichier("ressources/config");
if (fichier.open(QIODevice::WriteOnly))
{
QTextStream ecrire(&fichier);
QJsonObject json;
json["adresse"] = adresse->text();
json["port"] = port->text();
json["login"] = login->text();
json["automatique"] = automatique->isChecked();
QJsonDocument document(json);
ecrire << document.toJson();
}
}
void Principal::setNotification()
{
QMenu *menu = new QMenu(this);
menu->addAction(actionConnecter);
menu->addAction(actionAfficher);
menu->addSeparator();
menu->addAction(actionQuitter);
notification = new QSystemTrayIcon(QIcon(":/icones/ressources/chat_contacts.png"), this);
notification->setContextMenu(menu);
notification->show();
notification->showMessage("Bonjour", "Application chat", QSystemTrayIcon::Information);
}
void Principal::closeEvent(QCloseEvent *evt)
{
if (!quitter) {
hide();
evt->ignore();
}
}
void Principal::arreter()
{
quitter = true;
close();
}
Pour terminer sur ce sujet, je vous propose de reprendre le service chat
que nous venons de développer dans différentes situations
afin qu'il soit adapté à un système embarqué. Dans ce cas de figure, vous n'avez plus besoin, côté service, de proposer une IHM.
Le service sera alors exécuté en ligne de commande avec d'éventuelles options pour choisir, par exemple, le numéro de service souhaité.
Lorsque le programme sera déployé sur le système embarqué, et pour que vous ayez le choix du numéro de service, il serait judicieux de placer le numéro de port en option de la ligne de commande lorsque vous lancer le service. Puisqu'il n'y a plus d'IHM, nous avons un problème pour arrêter le service. Une des solutions consiste à le faire depuis l'application cliente. Nous pourrons, par exemple, envoyer le message stop sans l'encapsuler dans un format JSON.
QT += core websockets
QT -= gui
CONFIG += c++11 console
TARGET = ServeurWS
TEMPLATE = app
SOURCES += principal.cpp service.cpp
HEADERS += service.h
Le module websockets utilise intrinsèquement le module networks. Pour que ce dernier puisse fonctionner
correctement, vous êtes obligé de prendre le module core. En effet, ce dernier permet de mettre en oeuvre la gestion
événementielle qui est indispensable pour la communication réseau. #include <QtCore/QCoreApplication>
#include "service.h"
int main(int nombre, char *options[])
{
if (nombre==2)
{
QCoreApplication appli(nombre, options);
Service websocket(options[1]);
QObject::connect(&websocket, SIGNAL(arreter()), &appli, SLOT(quit()));
return appli.exec();
}
else return 0;
}
#ifndef SERVICE_H
#define SERVICE_H
#include <QObject>
#include <QWebSocketServer>
#include <QWebSocket>
#include <map>
using namespace std;
class Service : public QObject
{
Q_OBJECT
public:
explicit Service(QString port, QObject *parent = 0);
signals:
void arreter();
private:
QWebSocketServer service;
map<QWebSocket *, QString> connectes;
map<QString, QString> messages;
public slots:
void nouvelleConnexion();
void receptionMessage(const QString &texte);
void deconnexion();
private:
void envoyerATous(const QString &message);
void listeDesConnectes();
};
#endif // SERVICE_H
Nous retrouvons pratiquement la même classe que précédemment avec toutefois deux changement. Le premier concerne la mise en place
du signal arreter(), nécessaire pour permettre la communication avec la classe principale de l'application et d'arrêter ainsi
le programme lorsque un utilisateur le désirera. Maintenant, nous n'avons plus besoin du slot demarrer() puisque nous n'avons
plus de bouton du même nom. #include "service.h"
#include <QUrl>
#include <QJsonObject>
#include <QJsonArray>
#include <QJsonDocument>
Service::Service(QString port, QObject *parent) : QObject(parent), service("chat", QWebSocketServer::NonSecureMode, this)
{
if (service.listen(QHostAddress::Any, port.toInt()))
connect(&service, SIGNAL(newConnection()), this, SLOT(nouvelleConnexion()));
else arreter();
}
void Service::nouvelleConnexion()
{
QWebSocket *client = service.nextPendingConnection();
QString nom = client->requestUrl().path();
nom.remove('/');
client->sendTextMessage("Bonjour "+nom);
envoyerATous(nom+" vient de se connecter");
connectes[client] = nom;
listeDesConnectes();
auto recherche = messages.find(nom);
if (recherche!=messages.end()) {
client->sendTextMessage(recherche->second);
messages.erase(nom);
}
connect(client, SIGNAL(textMessageReceived(QString)), this, SLOT(receptionMessage(QString)));
connect(client, SIGNAL(disconnected()), this, SLOT(deconnexion()));
}
void Service::receptionMessage(const QString &texte)
{
if (texte=="stop") { arreter(); return; }
QWebSocket *client = (QWebSocket *) sender();
QString expediteur = connectes[client];
QJsonDocument document = QJsonDocument::fromJson(texte.toUtf8());
QJsonObject json = document.object();
QString destinataire = json["cible"].toString();
QString message = json["message"].toString();
bool trouve = false;
for (auto &cible : connectes)
if (cible.second == destinataire) {
cible.first->sendTextMessage(expediteur+" > "+message);
client->sendTextMessage("Message envoyé");
trouve = true;
}
if (!trouve) {
client->sendTextMessage("Destinataire non encore connecté");
messages[destinataire] = expediteur+" > "+message;
}
}
void Service::deconnexion()
{
QWebSocket *client = (QWebSocket *) sender();
client->deleteLater();
QString nom = connectes[client];
connectes.erase(client);
envoyerATous(nom + " vient de se déconnecter");
listeDesConnectes();
disconnect(client, SIGNAL(textMessageReceived(QString)), this, SLOT(receptionMessage(QString)));
disconnect(client, SIGNAL(disconnected()), this, SLOT(deconnexion()));
}
void Service::envoyerATous(const QString &message)
{
for (auto &client : connectes) client.first->sendTextMessage(message);
}
void Service::listeDesConnectes()
{
QString liste = "[";
for (auto &client : connectes) liste.append(" "+client.second);
liste.append(" ]");
envoyerATous("Connectés : "+liste);
}
Le code source de la classe Service est plus simple. Deux petit changement apparaissent, dans le constructeur et dans la
méthode receptionMessage(). Dans les deux cas nous prévoyons une demande d'arrêt du programme si les conditions sont requises.
Nous terminons cette étude en nous intéressant à la sécurisation des échanges entre des clients et un service qui utilise le protocole websocket
.
Les transactions sécurisées se feront par SSL. SSL sert
à la fois à crypter les données transmises et à vérifier l’identité des acteurs du dialogue (clients et serveur). Il existe une version
libre de SSL qui s'appelle OpenSSL
, celle que nous utiliserons ici. Pour la mise en oeuvre de ces transactions sécurisés, nous nous
servirons des projets de chat précédents.
Le SSL (Secure Socket Layer) / TLS (Transport Layer Security) est le protocole de sécurité le plus répandu qui créé un canal sécurisé entre deux machines communiquant sur Internet ou dans un réseau interne. Dans notre société centrée sur un Internet vulnérable, le SSL est généralement utilisé lorsqu'un navigateur doit se connecter de manière sécurisée à un serveur web.
Techniquement parlant, le SSL est un protocole transparent qui nécessite peu d'interaction de la part de l'utilisateur final. Dans
le cas des navigateurs, par exemple, les utilisateurs sont avertis de la présence de la sécurité SSL grâce à l'affichage d'un
cadenas et du protocole https
dans l'url. La clé du succès du SSL est donc son incroyable simplicité pour l'utilisateur
final.
On trouve principalement deux grandes familles de cryptographie : la cryptographie symétrique ou dite à clé secrète et la cryptographie asymétrique dite aussi à clé publique.
Nous parlons de cryptographie symétrique lorsqu'un texte, document, etc. est chiffré et déchiffré avec la même clé, la clé secrète. Ce procédé est à la fois simple et sûr. Principal inconvénient : étant donné que nous possédons qu'une clé, si vous la donnez à quelqu'un pour qu'il puisse vous envoyer des messages chiffrés avec celle-ci, il pourra aussi bien déchiffrer tous les autres documents que vous avez chiffrés avec cette dernière.
Contrairement à la cryptographie symétrique, ici avec l'asymétrique, nous avons besoin de deux clés. Tout d'abord nous avons la clé publique. Celle-ci, tout le monde peut la posséder, il n'y a aucun risque, vous pouvez la transmettre à n'importe qui. Elle sert à chiffrer le message. Mais il existe aussi la clé privée que seul le récepteur possède, en l'occurrence vous. Elle servira à déchiffrer le message chiffré avec la clé publique.
RSA fait partie des algorithmes de cryptographie asymétrique, celui utilisé par openssl
. RSA utilise
un algorithme très sophistiqué donc plus sûr qui prend en compte des nombres premiers de plus de 100 chiffres chacun que
nous ne détaillerons pas ici.
SSL utilise le principe de la cryptographie asymétrique. Il repose sur le principe d'une clé publique et d'une clé
privée par acteur. La clé publique sert à crypter et la clé privée sert à décrypter. La clé privée doit
absolument rester PRIVÉE, c'est pour cela qu'elle est généralement stockée dans un fichier crypté par un mot de passe appelée passphrase
.
Ainsi le serveur possède la clé publique du client1 et du client2, le client1 et le client2 possèdent celle du serveur.
Lorsque le client1 dialogue avec le serveur (exemple : Salut tu vas bien ?), il crypte le message avec la clé publique du serveur. Il n'y a que le serveur qui est capable de décrypter le message car il est le seul à posséder la clé privée.L'échange du serveur vers le client se déroule avec le même processus : Comment peut-on être sûr que l'on utilise la bonne clé publique pour crypter ? En effet un
méchant piratepeut s'interposer entre le client et le serveur en injectant sa clé publique au client. Il sera donc capable de déchiffrer tout ce qui vient du client.
Pour résoudre ce problème de sécurité, il existe des organismes qui s'appellent des autorités de certification
qui signent les clés
publiques. Cette signature permet de certifier l'origine de la clé publique. Une clé publique signée
s'appelle un certificat
.
Une autorité de certification AC ou CA pour certificate authority est un tiers de confiance qui a un rôle central dans le mécanisme de sécurisation. Pour initier une transaction sécurisée par SSL, un serveur doit fournir un certificat et une clé publique. Ce certificat doit être signé par un CA afin de garantir son authenticité.
Il existe des CA publiques par exemple, Verisign. Ce sont des fournisseurs qui facturent la génération du certificat. Afin de garantir leur identité, les CA signent le certificat donné avec leur clé privée. L'avantage des CA publiques est que la plupart des systèmes d'exploitations possèdent la clé publique de ces CA. Le désavantage est que chaque certificat émis est payant et assez cher.
Il peut être très intéressant de créer son propre CA pour son réseau interne. Des économies sont réalisées, et nous ne dépendons plus d'une entreprise externe pour la vérification des certificats. Le problème est que les clients externes ne feront pas confiance à nos certificats, à cause de l'absence de la clé publique dans le système d'exploitation. Ce n'est pas un problème bloquant dans le cadre d'un réseau interne, il suffira juste de transmettre la clé publique du CA à nos clients ce qui convient très bien à des applications classiques sans utilisation de navigateur.La génération des clés privées et publiques se feront au travers de OpenSSL
. Il faut qu'il soit installer sur votre système, ce qui
est le cas par défaut des systèmes d'exploitaion à base de Dedian. Sinon tapez la commande suivante : sudo apt-get
install openssl
.
Il faut saisir une commande du type :
openssl commande [paramètres de la commande]
On génère la clef privée à l'aide de la commande suivante en définissant un nom de fichier évocateur :
openssl genrsa -out serveur.key
Cette commande crée la clé privée avec l'algorithme RSA 2048 bits. L'option -out spécifie le fichier de sortie génération du fichier.
À partir de notre clé privée, nous allons maintenant générer un fichier de demande de signature de certificat
en anglais CSR
: Certificate Signing Request. Un CSR est un message qui, une fois soumis au CA Autorité
de certification, permet de générer un certificat. On génère la demande de certificat avec l'une des
commandes suivantes :
openssl req -new -key serveur.key > serveur.csr
openssl req -new -key serveur.key -out serveur.csr
L'option req est l'utilitaire de génération de certificats ou de demandes de certificat. L'option -new correspond à une nouvelle demande de certificat. L'option -key spécifie la clé privée qui permet de générer la demande de certificat. Vous allez devoir répondre à un certain nombre de questions qui permet d'identifier normalement l'entreprise. Vous pouvez ne rien remplir, sauf le premier champ Country Name. Vous pouvez mettre le nom du serveur tel qu'il est appelé de l'extérieur dans le champ Common Name par exemple : www.example.com ou mettre à la place votre nom. Ce n'est pas la peine de saisir les autres extra attributes...
Ceci a pour effet de créer le formulaire de demande de certificat fichier serveur.csr à partir de notre clé privée préalablement créée. Ce fichier contient la clé publique à certifier. Les clés publiques sont donc construites à partir des clés privées et elles doivent obligatoirement être signées puisque ce sont elles qui sont échangées et se déplacent sur le réseau alors que les clés privées restent sur place pour le décryptage. Maintenant, deux choix s'offrent à vous, soit envoyer le fichier serveur.csr à un organisme le tiers de confiance ou l'autorité de certification CA et ainsi obtenir le certificat dûment signé par la clé privée de l'organisme après avoir payé, ou bien signer vous-même le certificat. C'est ce dernier choix qui nous intéresse ici puisque dans le projet nous avons une communication entre plusieurs applications, et nous ne passons pas par un navigateur.Nous procédons de la même façon pour générer la clef privée ainsi que la demande signature de certificat, cette fois-ci pour le client, avec les mêmes commandes que précédemment, seuls les noms des fichiers générés changent :
openssl genrsa -out client.key
openssl req -new -key client.key > client.csr
Pour signer un certificat, vous devez devenir votre propre autorité de certification
, cela implique encore une fois de réaliser une clé avec cette fois-ci un certificat auto-signé. La création de la clé privée de l'autorité de certification se fait comme précédemment :
openssl genrsa -des3 > ca.key
openssl
genrsa -des3 -out ca.key
Ce qui a pour effet de créer la clé privée de l'autorité de certification
. Dans ce cas, il vaut mieux rajouter l'option des3 qui introduit l'usage d'une passphrase mot de passe long qui peut même contenir du blanc car c'est cette clé privée qui signera tous les certificats que nous emettrons ; cette passphrase sera donc demandée à chaque utilisation de la clé.
Ensuite, à partir de la clé privée, nous créons un certificat x509 pour une durée de validité de 10 ans par exemple auto-signé :
openssl req -new -x509 -days 3650 -key ca.key > ca.crt
openssl req -new -x509 -days 3650 -key ca.key -out ca.crt
Annotation | Rôle |
---|---|
req | Utilitaire de génération de certificats ou de demande de certificat. |
-new | Nouvelle de mande de certificat. |
-x509 |
Cette option génère un certificat auto-signé à la place d'une demande de certificat. Elle est utilisée typiquement pour générer des certificats de test ou le certificat auto-signé d'une CA. |
-days 3650 |
Durée de validité du certificat en nombre de jours. |
-out | Spécifie le nom du fichier certificat de sortie |
-ca | CA minimale utilisée pour signer des demandes de certificats. |
-CAkey | Spécifie la clé privée de l'autorité de certification du CA. |
-CAserial | Fichier texte qui comporte le numéro de série du certificat. |
Il faut saisir la passphrase puisque nous utilisons ca.key que nous avons protégé antérieurement. Nous donnons les renseignements concernant cette fois-ci l'autorité de certification
c'est à dire nous-même. Attention : le nom de Common Name doit être différent de celui qui a été donné pour la clé.
Maintenant que nous possédons le certificat du CA, nous pouvons signés toutes les demandes de certificat, celui du serveur et celui du client. La commande qui signe la demande de certificat est la suivante :
Les commandes précédentes peuvent être raccourcies. En effet, il n'est pas nécessaire de spécifier le nom du fichier associé au numéro de série du certificat. Par défaut, sans spécification, c'est toujours ca.srl qui est généré. Ce numéro de série est indispensable, sinon le certificat n'est pas valide. Par contre une fois qu'il a été créé, nous pouvons le réutiliser pour les autres certificats. En effet, si nous ne demandons pas de création de numéro de série,openssl x509 -days 3650 -req -in serveur.csr -out serveur.crt -CA ca.crt -CAkey ca.key -CAcreateserial -CAserial ca.srl
openssl x509 -days 3650 -req -in client.csr -out client.crt -CA ca.crt -CAkey ca.key -CAcreateserial -CAserial ca.srl
opensslva lire automatiquement le fichier ca.srl s'il existe, sinon une erreur se produit. Voici les commandes que nous aurions pu proposer à la place des précédentes :
Nos certificats sont maintenant générés. Ces clés publiques pourront être déployées sur les différentes machines concernées avec leurs clés privées. Beaucoup de fichiers ont été créés mais seuls quatre sont maintenant important pour le déploiement. Pour le serveur, sa clé privée serveur.key et sa clé publique certifiée pour dix ans serveur.crt. Pour le client, sa clé privée client.key et sa clé publique certifiée pour dix ans client.crt.openssl x509 -days 3650 -req -in serveur.csr -out serveur.crt -CA ca.crt -CAkey ca.key -CAcreateserial
openssl x509 -days 3650 -req -in client.csr -out client.crt -CA ca.crt -CAkey ca.key
Nous allons reprendre le service chat
précédent et le modifier pour qu'il puisse prendre en compte ces clés que nous venons générées afin que nos différents échanges soient parfaitement sécurisés. Avant de réaliser les modifications nécessaires, nous devons au préalable placer la clé privée et la clé publique certifiée du serveur dans le répertoire ressources, là où se trouvent déjà les différentes icônes. Vous devez alors les prendre en compte au moyen du fichier de gestion des ressources ressources.qrc.
QT += core gui websockets
CONFIG += c++11
greaterThan(QT_MAJOR_VERSION, 4): QT += widgets
TARGET = WebSocketServeur
TEMPLATE = app
SOURCES += main.cpp principal.cpp
HEADERS += principal.h
FORMS += principal.ui
RESOURCES += ressources.qrc
#include "principal.h"
#include <QApplication>
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
Principal w;
// w.show();
return a.exec();
}
Qt Creator génère automatiquement ce fichier et généralement nous pouvons le laisser tel quel. Ici toutefois, nous
devons empêcher que la fenêtre s'affiche dès le départ. C'est pour cette raison que j'ai placé en commentaire w.show()
.
#ifndef PRINCIPAL_H
#define PRINCIPAL_H
#include <QMainWindow>
#include <QWebSocketServer>
#include <QWebSocket>
#include <map>
#include <QSystemTrayIcon>
#include "ui_principal.h"
using namespace std;
class Principal : public QMainWindow, public Ui::Principal
{
Q_OBJECT
public:
explicit Principal(QWidget *parent = 0);
private:
QWebSocketServer service;
map<QWebSocket *, QString> connectes;
map<QString, QString> messages;
bool quitter = false;
QSystemTrayIcon *notification;
public slots:
void demarrer(bool valide);
void nouvelleConnexion();
void receptionMessage(const QString &texte);
void deconnexion();
void arreter();
protected:
void closeEvent(QCloseEvent *);
private:
void envoyerATous(const QString &message);
void listeDesConnectes();
void setNotification();
void configurationSSL();
};
#endif // PRINCIPAL_H
#include "principal.h" #include <QUrl> #include <QJsonObject> #include <QJsonArray> #include <QJsonDocument> #include <QMenu> #include <QCloseEvent> #include <QSslKey> Principal::Principal(QWidget *parent) : QMainWindow(parent), service("chat", QWebSocketServer::SecureMode, this) { setupUi(this); connect(&service, SIGNAL(newConnection()), this, SLOT(nouvelleConnexion())); setNotification(); configurationSSL(); } void Principal::configurationSSL() { QFile cert(":/ssl/ressources/serveur.crt"); QFile cle(":/ssl/ressources/serveur.key"); cert.open(QIODevice::ReadOnly); cle.open(QIODevice::ReadOnly); QSslCertificate certificat(&cert); QSslKey cleSSL(&cle, QSsl::Rsa); QSslConfiguration ssl; ssl.setPeerVerifyMode(QSslSocket::VerifyNone); ssl.setLocalCertificate(certificat); ssl.setPrivateKey(cleSSL); service.setSslConfiguration(ssl); }
void Principal::demarrer(bool valide) { if (valide) { if (service.listen(QHostAddress::Any, port->value())) { etat->showMessage("Lancement du service"); notification->showMessage("Action", "Lancement du service", QSystemTrayIcon::Information); } else etat->showMessage("Problème de connexion"); actionService->setText("Arrêter le service"); port->setReadOnly(true); } else { service.close(); etat->showMessage("Arrêt du service"); notification->showMessage("Action", "Arrêt du service", QSystemTrayIcon::Information); actionService->setText("Démarrer le service"); port->setReadOnly(false); } } void Principal::nouvelleConnexion() { QWebSocket *client = service.nextPendingConnection(); QString nom = client->requestUrl().path(); nom.remove('/'); client->sendTextMessage("Bonjour "+nom); etat->showMessage(nom+" vient de se connecter"); notification->showMessage("Evénement", nom+" vient de se connecter", QSystemTrayIcon::Information); envoyerATous(nom+" vient de se connecter"); connectes[client] = nom; listeDesConnectes(); auto recherche = messages.find(nom); if (recherche!=messages.end()) { client->sendTextMessage(recherche->second); messages.erase(nom); } connect(client, SIGNAL(textMessageReceived(QString)), this, SLOT(receptionMessage(QString))); connect(client, SIGNAL(disconnected()), this, SLOT(deconnexion())); } void Principal::receptionMessage(const QString &texte) { QWebSocket *client = (QWebSocket *) sender(); QString expediteur = connectes[client]; QJsonDocument document = QJsonDocument::fromJson(texte.toUtf8()); QJsonObject json = document.object(); QString destinataire = json["cible"].toString(); QString message = json["message"].toString(); bool trouve = false; for (auto &cible : connectes) if (cible.second == destinataire) { cible.first->sendTextMessage(expediteur+" > "+message); client->sendTextMessage("Message envoyé"); trouve = true; } if (!trouve) { client->sendTextMessage("Destinataire non encore connecté"); messages[destinataire] = expediteur+" > "+message; } } void Principal::deconnexion() { QWebSocket *client = (QWebSocket *) sender(); client->deleteLater(); QString nom = connectes[client]; etat->showMessage(nom + " vient de se déconnecter"); notification->showMessage("Evénement", nom+" vient de se déconnecter", QSystemTrayIcon::Information); connectes.erase(client); envoyerATous(nom + " vient de se déconnecter"); listeDesConnectes(); disconnect(client, SIGNAL(textMessageReceived(QString)), this, SLOT(receptionMessage(QString))); disconnect(client, SIGNAL(disconnected()), this, SLOT(deconnexion())); } void Principal::envoyerATous(const QString &message) { for (auto &client : connectes) client.first->sendTextMessage(message); } void Principal::listeDesConnectes() { QJsonArray liste; for (auto &client : connectes) liste.append(client.second); QJsonObject json; json["connectés"] = liste; QJsonDocument document(json); for (auto &client : connectes) client.first->sendTextMessage(document.toJson()); } void Principal::setNotification() { QMenu *menu = new QMenu(this); menu->addAction(actionService); menu->addAction(actionAfficher); menu->addSeparator(); menu->addAction(actionQuitter); notification = new QSystemTrayIcon(QIcon(":/icones/ressources/chat.png"), this); notification->setContextMenu(menu); notification->show(); notification->showMessage("Bonjour", "Service chat", QSystemTrayIcon::Information); } void Principal::closeEvent(QCloseEvent *evt) { if (!quitter) { hide(); evt->ignore(); } } void Principal::arreter() { quitter = true; close(); }
SSLavec la prise en compte des différentes clés que nous venons de générer.
opensslutilise le cryptage RSA 2048 bits.
Bien entendu, là aussi nous reprenons le projet précédent auquel nous allons rajouter les clés associées au client. À l'image du serveur, nous plaçons ces clés également dans le répertoire ressources. Nous modifions le fichier de gestion des ressources ressources.qrc en conséquence.
{
"adresse": "192.168.1.75",
"automatique": true,
"login": "bruno",
"port": "8080"
}
Si vous remarquez bien, le texte de ce fichier est au format JSON. Lors des
différents chapitres, nous avons découvert les objets permettant de lire ou d'écrire vers ce format là. C'est simple et rapide à faire.
#include "principal.h"
#include <QApplication>
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
Principal w;
// w.show();
return a.exec();
}
Qt Creator génère automatiquement ce fichier et généralement nous pouvons le laisser tel quel. Ici toutefois, nous
devons empêcher que la fenêtre s'affiche dès le départ. C'est pour cette raison que j'ai placé en commentaire w.show()
.
#ifndef PRINCIPAL_H
#define PRINCIPAL_H
#include <QMainWindow>
#include <QtWebSockets/QWebSocket>
#include <QSystemTrayIcon>
#include "ui_principal.h"
class Principal : public QMainWindow, public Ui::Principal
{
Q_OBJECT
public:
explicit Principal(QWidget *parent = 0);
private:
QWebSocket webSocket;
bool estConnecte = false, quitter = false;
QSystemTrayIcon *notification;
private slots:
void commuter();
void envoiMessage();
void receptionMessage(const QString &texte);
void connexion();
void deconnexion();
void enregistrerConfig();
void arreter();
protected:
void closeEvent(QCloseEvent *);
private:
void seConnecter();
void lireConfig();
void setNotification();
void configurationSSL();
};
#endif // PRINCIPAL_H
Là aussi, nous avons besoin de la méthode qui va permettre de configurer le protocole de sécurité SSL. #include "principal.h"
#include <QJsonObject>
#include <QJsonArray>
#include <QJsonDocument>
#include <QFile>
#include <QMenu>
#include <QCloseEvent>
#include <QSslKey>
Principal::Principal(QWidget *parent) : QMainWindow(parent)
{
setupUi(this);
setNotification();
lireConfig();
configurationSSL();
connect(&webSocket, SIGNAL(textMessageReceived(QString)), this, SLOT(receptionMessage(QString)));
connect(&webSocket, SIGNAL(connected()), this, SLOT(connexion()));
connect(&webSocket, SIGNAL(disconnected()), this, SLOT(deconnexion()));
connect(&webSocket, SIGNAL(error(QAbstractSocket::SocketError)), this, SLOT(deconnexion()));
}
void Principal::configurationSSL()
{
QFile cert(":/ssl/ressources/client.crt");
QFile cle(":/ssl/ressources/client.key");
cert.open(QIODevice::ReadOnly);
cle.open(QIODevice::ReadOnly);
QSslCertificate certificat(&cert);
QSslKey cleSSL(&cle, QSsl::Rsa);
QSslConfiguration ssl;
ssl.setPeerVerifyMode(QSslSocket::VerifyNone);
ssl.setLocalCertificate(certificat);
ssl.setPrivateKey(cleSSL);
webSocket.setSslConfiguration(ssl);
}
void Principal::commuter()
{
estConnecte = !estConnecte;
if (estConnecte && !login->text().isEmpty()) seConnecter();
else webSocket.close();
}
void Principal::seConnecter()
{
QString url = "wss://";
url+=adresse->text()+":";
url+=port->text()+"/";
url+=login->text();
webSocket.open(QUrl(url));
onglets->setCurrentIndex(1);
}
void Principal::envoiMessage()
{
if (destinataire->text().isEmpty()) reception->append("ATTENTION, il faut un destinataire");
else {
QJsonObject json;
json["cible"] = destinataire->text();
json["message"] = message->text();
QJsonDocument document(json);
webSocket.sendTextMessage(document.toJson());
}
}
void Principal::receptionMessage(const QString &texte)
{
QJsonDocument document = QJsonDocument::fromJson(texte.toUtf8());
if (!document.isObject()) {
reception->append(texte);
notification->showMessage("Notification", texte, QSystemTrayIcon::Information);
}
else {
QJsonObject json = document.object();
if (json.contains("expéditeur"))
{
QString expediteur = json["expéditeur"].toString();
QString message = json["message"].toString();
reception->append(expediteur+" > "+message);
notification->showMessage("Nouveau message", expediteur+" > "+message, QSystemTrayIcon::Information);
}
if (json.contains("connectés"))
{
QJsonArray connectes = json["connectés"].toArray();
logins->clear();
for (int i=0; i<connectes.count(); i++)
if (connectes[i].toString() != login->text())
logins->addItem(connectes[i].toString());
}
}
}
void Principal::connexion()
{
reception->append("Connecté au service");
}
void Principal::deconnexion()
{
reception->append("Déconnecté du service");
logins->clear();
}
void Principal::lireConfig()
{
QFile fichier("ressources/config");
if (fichier.open(QIODevice::ReadOnly))
{
QTextStream lire(&fichier);
QJsonDocument document = QJsonDocument::fromJson(lire.readAll().toUtf8());
QJsonObject json = document.object();
adresse->setText(json["adresse"].toString());
port->setText(json["port"].toString());
login->setText(json["login"].toString());
bool lancer = json["automatique"].toBool();
automatique->setChecked(lancer);
if (lancer) boutonConnexion->click();
}
}
void Principal::enregistrerConfig()
{
QFile fichier("ressources/config");
if (fichier.open(QIODevice::WriteOnly))
{
QTextStream ecrire(&fichier);
QJsonObject json;
json["adresse"] = adresse->text();
json["port"] = port->text();
json["login"] = login->text();
json["automatique"] = automatique->isChecked();
QJsonDocument document(json);
ecrire << document.toJson();
}
}
void Principal::setNotification()
{
QMenu *menu = new QMenu(this);
menu->addAction(actionConnecter);
menu->addAction(actionAfficher);
menu->addSeparator();
menu->addAction(actionQuitter);
notification = new QSystemTrayIcon(QIcon(":/icones/ressources/chat_contacts.png"), this);
notification->setContextMenu(menu);
notification->show();
notification->showMessage("Bonjour", "Application chat", QSystemTrayIcon::Information);
}
void Principal::closeEvent(QCloseEvent *evt)
{
if (!quitter) {
hide();
evt->ignore();
}
}
void Principal::arreter()
{
quitter = true;
close();
}
Nous avons désactivé la vérification de l'identité de l'autorité de certification ssl.setPeerVerifyMode(QSslSocket::VerifyNone) vu que c'est nous qui sommes l'autorité de certification et nous qui avons signés les deux certificats. Finalement, il est tout à fait possible de nous passer d'être autorité de certification en créant directement les certificats du client et du serveur uniquement à partir de leurs clés privées par une auto-signature comme nous l'avons fait pour le certificat de l'autorité de certification. Du coup, seuls quatre fichiers sont suffisants et les options de la commande openssl deviennent plus facile à retenir. Par contre, dans ce cas de figure, il est souhaitable de mettre un mot de passe passphrase sur vos deux clés le mot de passe était présent sur la clé du CA, afin d'améliorer la protection.
openssl genrsa -des3 > serveur.key // génération de la clé privée du serveur avec mot de passe
openssl req -new -x509 -days 3650 -key serveur.key > serveur.crt// clé publique du serveur (certificat auto-signé) openssl genrsa -des3 > client.key
// génération de la clé privée du client avec mot de passe
openssl req -new -x509 -days 3650 -key client.key > client.crt // clé publique du client (certificat auto-signé)
Principal::Principal(QWidget *parent) : QMainWindow(parent), service("chat", QWebSocketServer::SecureMode, this)
{
setupUi(this);
connect(&service, SIGNAL(newConnection()), this, SLOT(nouvelleConnexion()));
setNotification();
configurationSSL();
}
void Principal::configurationSSL()
{
QFile cert(":/ssl/ressources/serveur.crt");
QFile cle(":/ssl/ressources/serveur.key");
cert.open(QIODevice::ReadOnly);
cle.open(QIODevice::ReadOnly);
QSslCertificate certificat(&cert);
QSslKey cleSSL(&cle, QSsl::Rsa, QSsl::Pem, QSsl::PrivateKey, "Votre mot de passe");
QSslConfiguration ssl;
ssl.setPeerVerifyMode(QSslSocket::VerifyNone);
ssl.setLocalCertificate(certificat);
ssl.setPrivateKey(cleSSL);
service.setSslConfiguration(ssl);
}
Principal::Principal(QWidget *parent) : QMainWindow(parent)
{
setupUi(this);
setNotification();
lireConfig();
configurationSSL();
connect(&webSocket, SIGNAL(textMessageReceived(QString)), this, SLOT(receptionMessage(QString)));
connect(&webSocket, SIGNAL(connected()), this, SLOT(connexion()));
connect(&webSocket, SIGNAL(disconnected()), this, SLOT(deconnexion()));
connect(&webSocket, SIGNAL(error(QAbstractSocket::SocketError)), this, SLOT(deconnexion()));
}
void Principal::configurationSSL()
{
QFile cert(":/ssl/ressources/client.crt");
QFile cle(":/ssl/ressources/client.key");
cert.open(QIODevice::ReadOnly);
cle.open(QIODevice::ReadOnly);
QSslCertificate certificat(&cert);
QSslKey cleSSL(&cle, QSsl::Rsa, QSsl::Pem, QSsl::PrivateKey, "Votre mot de passe");
QSslConfiguration ssl;
ssl.setPeerVerifyMode(QSslSocket::VerifyNone);
ssl.setLocalCertificate(certificat);
ssl.setPrivateKey(cleSSL);
webSocket.setSslConfiguration(ssl);
}
void Principal::seConnecter()
{
QString url = "wss://";
url+=adresse->text()+":";
url+=port->text()+"/";
url+=login->text();
webSocket.open(QUrl(url));
onglets->setCurrentIndex(1);
}
Vu que la clé privée reste sur place seules les clés publiques sont transmises sur le réseau, le mot de passe peut effectivement paraître inutile, surtout dans le cas où les données ne sont pas relatives à des données bancaires. À vous de voir. Sans mot de passe sur la clé privée, cela devient encore plus facile et plus concis :
openssl genrsa -out serveur.key // génération de la clé privée du serveur sans mot de passe
openssl req -new -x509 -days 3650 -key serveur.key > serveur.crt// clé publique du serveur (certificat auto-signé) openssl genrsa -out client.key
// génération de la clé privée du client sans mot de passe
openssl req -new -x509 -days 3650 -key client.key > client.crt // clé publique du client (certificat auto-signé)
Principal::Principal(QWidget *parent) : QMainWindow(parent), service("chat", QWebSocketServer::SecureMode, this)
{
setupUi(this);
connect(&service, SIGNAL(newConnection()), this, SLOT(nouvelleConnexion()));
setNotification();
configurationSSL();
}
void Principal::configurationSSL()
{
QFile certificat(":/ssl/ressources/serveur.crt");
QFile clePrivee(":/ssl/ressources/serveur.key");
certificat.open(QIODevice::ReadOnly);
clePrivee.open(QIODevice::ReadOnly);
QSslConfiguration ssl;
ssl.setPeerVerifyMode(QSslSocket::VerifyNone);
ssl.setLocalCertificate(QSslCertificate(&certificat));
ssl.setPrivateKey(QSslKey(&clePrivee, QSsl::Rsa));
service.setSslConfiguration(ssl);
}
Principal::Principal(QWidget *parent) : QMainWindow(parent)
{
setupUi(this);
setNotification();
lireConfig();
configurationSSL();
connect(&webSocket, SIGNAL(textMessageReceived(QString)), this, SLOT(receptionMessage(QString)));
connect(&webSocket, SIGNAL(connected()), this, SLOT(connexion()));
connect(&webSocket, SIGNAL(disconnected()), this, SLOT(deconnexion()));
connect(&webSocket, SIGNAL(error(QAbstractSocket::SocketError)), this, SLOT(deconnexion()));
}
void Principal::configurationSSL()
{
QFile certificat(":/ssl/ressources/client.crt");
QFile clePrivee(":/ssl/ressources/client.key");
certificat.open(QIODevice::ReadOnly);
clePrivee.open(QIODevice::ReadOnly);
QSslConfiguration ssl;
ssl.setPeerVerifyMode(QSslSocket::VerifyNone);
ssl.setLocalCertificate(QSslCertificate(&certificat));
ssl.setPrivateKey(QSslKey(&clePrivee, QSsl::Rsa));
webSocket.setSslConfiguration(ssl);
}
void Principal::seConnecter()
{
QString url = "wss://";
url+=adresse->text()+":";
url+=port->text()+"/";
url+=login->text();
webSocket.open(QUrl(url));
onglets->setCurrentIndex(1);
}
Avec ces toutes dernières lignes de code, vous remarquez que la mise en oeuvre d'une communications sécurisées prend très peu de temps et la méthode configurationSSL()
devient très concise. Cela ne vaut pas le coup de s'en passer.