WebSocket

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.

Sommaire de l'étude

WebSocket (WS) : un nouveau protocole différent de HTTP

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.
Problèmes avec le protocole HTTP

HTTP est le protocole standard utilisé pour le Web, il est très efficace pour certains cas d’utilisation mais il dispose néanmoins de quelques inconvénients dans le cas d’applications Web interactive :

  1. half-duplex Communication bidirectionnelle mais pas en même temps : basé sur le principe requête/réponse, le client envoi une requête puis le serveur réalise un traitement avant de renvoyer une réponse, le client est donc contraint d’attendre une réponse du serveur.
  2. verbose Informations supplémentaires : beaucoup d’informations sont présentes avec les headers HTTP associés au message, aussi bien dans la requête HTTP que dans la réponse HTTP.
  3. Non connecté : Son avantage mais également son gros défaut, c'est que ce protocole HTTP est la plupart du temps en mode non connecté. Lorsque la réponse a été obtenu à l'issue de la requête proposé par le client, la connexion est définitivement interrompue jusqu'à ce qu'une nouvelle requête soit proposée. Si vous désirez que le serveur envoie de lui-même des informations, il est nécessaire d’utiliser des méthodes de contournement polling, long polling, Comet/Ajax car il n’existe pas de standard.
Ce protocole n’est donc pas optimisé pour des applications qui ont d’important besoins de communication temps réel bi-directionnelle.
Changement avec le protocole WebSocket

C’est pourquoi le nouveau protocole WebSocket propose des fonctionnalités plus évoluées que HTTP, puisqu’il est :

  1. Basé sur une unique connexion TCP entre deux pairs en HTTP chaque requête/réponse> necessite une nouvelle connexion TCP.
  2. bi-directionnel : le client peut envoyer un message au serveur et le serveur peut envoyer un message au client.
  3. full-duplex : le client peut envoyer plusieurs messages vers le serveur et le serveur vers le client sans attendre de réponse l’un de l’autre.
Le terme client est utilisé uniquement pour définir celui qui va initialiser la connexion. Dès lors que la connexion est établie, le client et le serveur deviennent tous les deux des pairs, avec les mêmes pouvoirs l’un par rapport à l’autre.

Grâce à ce principe, le protocole Websocket propose une implémentation native et unifiée dans les navigateurs et serveurs web d'un canal bidirectionnel permettant en temps réel :

Le protocole WebSocket plus en détails

Dans le cadre du protocole WebSocket pour la communication bidirectionnelle, l'application serveur et l'application cliente doivent connaître les détails du protocole. Une interaction WebSocket commence par un établissement de liaison dans lequel les deux parties le client et le serveur confirment mutuellement leur intention de communiquer via une connexion permanente. Ensuite, un groupe de paquets de messages est envoyé via TCP dans les deux sens.

protocole WebSocket

Outre les détails figurant dans la figure ci-dessus, notez que lorsque la connexion est fermée, les deux points de terminaison échangent une trame de fermeture afin de fermer proprement la connexion. La demande d'établissement de liaison initiale est composée d'une requête HTTP ordinaire envoyée par le client au serveur Web.
Deux phases interviennent dans ce processus
  1. Handshake Poignée de main : Cette phase correspond à un unique échange requête/réponse HTTP entre l’initiateur de la connexion pair client et le pair serveur. Cet échange HTTP est spécifique car il utilise la notion de mise à niveau, définie dans la spécification HTTP. Le principe est simple : l’Upgrade HTTP permet au client de communiquer avec le serveur pour lui demander de changer de protocole de communication et ainsi faire en sorte que le client et le serveur utilisent un protocole autre que HTTP pour discuter.
    La requête est un HTTP GET configuré comme une demande de mise à niveau
    GET /chat HTTP/1.1
    Host: server.example.com
    Upgrade: websocket
    Connection: Upgrade
    Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
    Origin: http://example.com

    Avec HTTP, une requête du client avec l'en-tête Upgrade indique l'intention du client de demander à ce que le serveur passe à un autre protocole. Avec le protocole WebSocket, la demande de mise à niveau effectuée au serveur contient une clé unique que le serveur retournera altérée comme preuve qu'il a accepté la demande de mise à niveau. Il s'agit d'une démonstration pratique permettant de prouver que le serveur comprend le protocole WebSocket.

    Voici un exemple de réponse à une demande d'établissement de liaison
    HTTP/1.1 101 WebSocket Protocol Handshake
    Upgrade: websocket
    Connection: Upgrade
    Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=

    Le code de statut de réussite est toujours 101. Tout autre code de statut sera interprété comme un refus de mettre à niveau vers le protocole WebSocket. Le serveur concatène la clé reçue avec une chaîne GUID fixe et il calcule le hachage à partir de la chaîne obtenue. La valeur de hachage est ensuite codée en Base64 et retournée au client via l'en-tête Sec-WebSocket-Accept.

    Lorsque la demande de mise à niveau du protocole HTTP vers le protocole Web Socket a été validée par le serveur endpoint, il n’y a plus de communication possible en HTTP, tous les échanges sont réalisés via le protocole WebSocket.
  2. Data transfer : Une fois que l'initialisation de la connexion a été faite entre le client et le serveur, ceux-ci sont connectés et appairés ; les deux peuvent envoyer, recevoir des messages et fermer la connexion. Dans le détail, une fois que le handshake est acceptée, la mise en place du protocole WebSocket est donc acquise. Une connexion côté pair serveur est ouverte ainsi que côté pair client, une gestion de callback est activée pour initier la communication. La phase de Data transfer peut alors entrer en jeu, c’est-à-dire que les deux pairs peuvent désormais s'échanger des messages dans une communication bi-directionnelle et full-duplex. À la suite de l'établissement de liaison, le client et le serveur peuvent envoyer des messages librement via le protocole WebSocket. Notez que les messages WebSocket sont asynchrones et qu'une demande d'envoi ne recevra donc pas nécessairement une réponse immédiate, comme avec HTTP. Avec le protocole WebSocket, il est conseillé de réfléchir en termes de messages généraux allant du client vers le serveur, ou inversement, et d'oublier le modèle classique requête/réponse de HTTP.
Conclusion et intérêt du protocole WebSocket

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.

Intérêt WebSocket

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.

Création du service à l'aide des annotations

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.

Java API pour WebSocket

JEE7 intègre maintenant l'API Java pour les WebSocket qui propose un certaine nombre de fonctionnalités inhérentes :

Mise en place du service : WebSocket Endpoint

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 :

Code minimum pour un service WebSocket
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;
    }
}
Mise en place du service grâce à l'annotation @ServerEndPoint
À l'image du Service Web REST, il est très facile d'implémenter un Service WebSocket grâce à ce principe d'annotations. Vous devez précicer sur l'annotation @ServerEndPoint l'URI d'accès, ici /echo ou plus simplement echo. Cette Endpoint sera créée et déployée au démarrage du serveur Web.
Comment recevoir les messages du client
Au minimum, pour que le service serve à quelque chose, vous devez également ajouter une méthode dont le nom est à votre libre choix annotée par @OnMessage qui sera automatiquement appelée à chaque fois qu'un client envoie un message. C'est cette méthode qui réceptionne tous les messages de tous les clients à noter qu'un objet de cette classe sera créé pour chacun des clients connectés.
Principe d'utilisation
Ici, la méthode renvoie instantanément au client le message reçu. Ce type de communication est appelée synchrone, puisque la réponse obtenue suit immédiatement la requête. Nous sommes pour l'instant dans la même situation que dans le Service Web REST sauf que le client reste toujours connecté et qu'il peut à tout moment envoyer un nouveau message. Enfin, dans cet exemple, le type de message est pour l'instant textuel.
Côté client

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.

Site dédié pour tester le protocole WebSocket

ws://localhost:8080/ws/echo
Pour communiquer avec le serveur, vous devez donc préciser la bonne URI en employant le protocole WebSocket. La première chose à faire consiste à définir l'emplacement du serveur avec son numéro de service si ce n'est pas le port 80 localhost:8080, le nom de l'application Web ws suivi du service EndPoint echo.
Connexion au service
Ensuite, vous faites une demande de connexion au serveur. Si la demande est acceptée, vous restez connectés tout le temps que vous désirez.
Envoi et réception des messages
Vous pouvez maintenant envoyer autant de messages que vous souhaitez. Vu l'écriture actuelle de notre service rudimentaire, comme son nom l'indique, il renvoi le même message précédé du texte Merci pour ton message : .
Deconnexion du service
Dès que vous ne souhaitez plus rester connecté, il faut en faire une demande explicite au serveur.
L’API met à disposition plusieurs types d’annotations
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
handshake
Le endpoint participe à l'ouverture et l'initialisation de la connexion. Il va alors envoyer et recevoir une variété de messages WebSocket. Le cycle de vie se termine une fois que la connexion est fermée.
Clôture de la connexion
Si une connexion active sur un endpoint WebSocket est sur le point d’être fermée pour quelques raisons que cela soit, sur un événement de fermeture WebSocket du pair, ou parce que l’implémentation l'a décidée, l'implémentation du WebSocket doit appeler la méthode annotée @OnClose du endpoint.
Cycle de vie
Après établissement d'une connexion entre deux point d'accès, nous pouvons gérer le cycle de vie de la communication, au travers des méthodes de rappel associées aux annotations @OnOpen, @OnClose et @OnError :
@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.
Tous les types de message

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){}

Cycle de vie et communication asynchrone

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.

Le serveur envoi un message dès l'établissement de la connexion

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.

Communication à la connexion
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().

URI paramétrée

À 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.

Login de connexion
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.

Communiquer avec tous les clients

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.

Avertissement à chaque connexion et déconnexion
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);
    }
}
Attribut session
De même que j'avais créé un attribut correspondant au login du client, il peut être intéressant de prévoir un attribut permettant de conserver la session actuelle. Du coup les autres méthodes de rappel n'auront plus besoin de prendre en compte un paramètre de type Session, comme c'est le cas avec la nouvelle méthode annotée @OnClose.
Lister l'ensemble des clients actuellement connectés
Comme pour toutes les classes, rien n'empêche de créer ses propres méthodes, indépendemment des méthodes de rappel. Ainsi, la méthode privée va me permettre d'envoyer un message à tous les clients à chaque nouvelle connexion et à chaque nouvelle deconnexion. Pour cela, je me sert encore de l'objet de type Session qui possède la méthode getOpenSessions() et qui renvoie l'ensemble des sessions représentant les différentes connexions avec les clients actuellement connectés.
Envoi ciblé
Remarquez au passage que j'envoie les messages uniquement aux autres clients. Effectivement, je contrôle l'ensemble des sessions avec la session actuellement mémorisé dans l'attribut de la classe.

Placer des informations dans la session d'un client

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.

Annonce de tous les clients connectés en temps réel
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.

Communiquer avec un autre client en particulier

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.

Mise en oeuvre d'un chat
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;
    }
}
Lecture d'une structure JSON
Depuis la version 7 de JEE l'API concernant JSON est maintenant intégré et disponible. Deux classes sont particulièrement adaptées : JsonObject qui permet de récupérer un ensemble d'attributs, comme c'est le cas ici, JsonArray qui prend en compte une collection d'éléments. Vous avez ensuite plusieurs classes qui prennent s'intéressent plus particulièrement aux valeurs proprement dites suivant leur type : JsonValue, JsonString et JsonNumber.
Création d'une structure JSON
Pour la création d'une structure JSON vous devez utiliser d'autres classes respectivement JsonObjectBuilder qui permet de générer un ensemble d'attributs distincts dans une même entité et JsonArrayBuilder qui permet de générer une collection d'éléments.
Gestion des flux d'entrée (ou de sortie)
Toutes les classes que nous venons de voir permettent de récupérer ou de générer une structure au format JSON. Cependant, cette structure vient généralement d'un flux envoyé dans le réseau généralement sous forme de texte. Encode une fois, il existe des classes spécialisées suivant les différentes situations. Nous avons la classe JsonWriter qui permet de générer un flux sous forme de texte au format JSON à partir d'un objet présent en mémoire centrale. La classe JsonReader, et c'est elle qui nous intéresse, permet de faire l'inverse, c'est-à-dire qu'elle retrouve la structure de l'objet à partir d'un flux de texte formaté en JSON.
Localiser le bon flux
Je rappelle que pour récupérer le flot d'octets, nous devons préciser à partir de quelle structure physique nous l'associons : disque dur, réseau, simple texte, etc. Par rapport au sujet qui nous préoccupe, deux classes sortent du lot, InputStream qui permet de récupérer un flot d'octets, et StringReader qui génère un flot de caractères à partir d'une simple chaîne de caractères.
Récupérer chacun des attributs de l'objet
Pour terminer, cette API propose une classe utilitaire Json qui possède un certain nombre de méthodes statiques, comme createReader() qui permet de créer une structure JSON à partir d'un flux de caractères, getString() qui retourne la valeur de l'attribut spécifié en argument sous forme d'une chaîne de caractères, getInt() qui retourne la valeur de l'attribut spécifié en argument sous forme de valeur numérique, etc.

Les EJB et la persistance

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.

Messagerie asynchrone

À 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.

Mise en oeuvre du singleton
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;
    }
}
Plus d'attributs personnels associés à chacun des clients
Vu que nous ne disposons plus que d'un seul objet dans le service, contrairement au comportement par défaut, nous ne pouvons plus créer des attributs représentant le login du client ainsi que sa session. Tout enregistrement personnel doit se faire ou travers de la session, grâce à la méthode prévue pour cela, getUserProperties().
Chaque session représente la connection d'un client
Heureusement, nous pouvons communiquer malgré tout à un client en particulier grâce à la session le représentant. Vous remarquez d'ailleurs, que maintenant toutes les méthodes de rappel disposent d'un paramètre supplémentaire de type Session. Nous sommes obligés de rajouter ces paramètres puisqu'il n'est plus possible de mémoriser la session en cours dans un attribut commun. Cela n'aurait pas de sens.
Enregistrement des messages différés
L'attribut messages permet de sauvegarder momentanément les messages dédiées à un client non connecté actuellement. C'est la méthode envoyerMessage() qui enregistre cette information en précisant la cible et le contenu du message. Lorsque un client se connecte, dès l'ouverture, une vérification est faite au niveau de cet attribut messages pour savoir si un message personnel n'est pas actuellement en attente.
L'ensemble des sessions comme attribut du singleton

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.

Capacités nouvelles pour le singleton
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;
    }
}
Injection de bean session

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.

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.
WebSocket
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.
La persistance

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é.

Entité Message
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;
   }
}
WebSocket ServiceEcho
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();
   }
}
Vues des clients

Les encodeurs et les décodeurs

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.

Mise en place d'un décodeur et d'un encodeur

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.

Entité
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.
Décodeur
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() {   }
   
}
Interfaces utilisées
Pour créer un décodeur, vous devez mettre en place une nouvelle classe qui implémente l'une des quatre interfaces suivantes : Decoder.Binary<T>, Decoder.BinaryStream<T>, Decoder.Text<T> ou Decoder.TextStream<T> en rapport avec le type de flux, où T représente la classe qui va stocker l'ensemble des informations issues du message envoyé par le client.
Méthodes à implémenter
Comme d'habitude, vous êtes obligés de redéfinir les méthodes intégrées à l'interface. Elles sont au nombre de quatre. Les méthodes init() et destroy() sont rarement utiles ici, leurs contenues sont vierges. La méthode willDecode() permet d'évaluer si le message peut être décodé ici, nous regardons si le message est bien au format JSON . La méthode qui réalise le décodage s'appelle tout simplement decode(). Comme prévu, elle prend comme paramètre un texte de type String qui contient le message envoyé par le client, et retourne le résultat de son décodage en un objet correspondant au type spécifié dans le template dans notre projet c'est l'entité, donc Message.
Décodage
Dans cette méthode decode(), nous retrouvons finalement l'écriture que nous avons déjà mis en oeuvre dans le projet précédent en décomposant la structure du format JSON et nous renseignons les attributs nécessaires pour l'entité, notamment le destinataire et le contenu du message.
Encodeur
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() {  }
}
Interfaces utilisées
À l'image du décodeur, pour créer un encodeur, vous devez créer une nouvelle classe qui implémente l'une des quatre interfaces prévues pour cette phase là. Cette fois-ci, il s'agit de : Encoder.Binary<T>, Encoder.BinaryStream<T>, Encoder.Text<T> ou Encoder.TextStream<T>. T représente la classe qui contient l'ensemble des informations issues du WebSocket qui sera ensuite transformé en un message envoyé pour le client concerné.
Méthodes à implémenter
Ces interfaces possèdent également des méthodes, au nombre de trois. Nous retrouvons les méthodes init() et destroy(). La dernière méthode s'appelle, comme on l'imagine encode(). Elle prend comme paramètre un objet du type spécifié dans le template dans notre projet c'est l'entité, donc Message et doit renvoyer un texte formaté ou pas à l'aide des éléments constituant l'objet récupéré.
Encodage
Dans cette méthode encode(), nous utilisons tout simplement les compétences de l'entité, nous faisons juste appel à la méthode toString(). Vu la simplicité du traitement et le peu de lignes de code, nous aurions pu éviter d'utiliser un encodeur. La plupart du temps, nous profitons de cette phase d'encodage pour élaborer un texte formaté, au format JSON par exemple, ce qui justifie l'utilisation d'un encodeur.
Serveur EndPoint
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();
   }
}
Annotation @ServerEndPoint
La classe du EndPoint doit être au courant du décodeur et de l'encodeur que nous venons de créer. Cela se fait au niveau de l'annotation @ServerEndPoint qui possède des attributs spécifiques decoders et encoders. Remarquez au passage qui sont au pluriels. Effectivement, nous pouvons mettre en oeuvre autant de décodeurs et d'encodeurs que nous le désirons. Remarquez également que l'attribut relatif au chemin de l'URL se nomme value.
Phase de décodage
Remarquez que le paramètre de la méthode de rappel annotée @OnMessage a changé. Il s'agit maintenant d'un objet de type Message qui comporte toutes les informations issues de la phase de décodage. Nous connaissons donc le destinataire et le message qui lui est dédié. Nous rajoutons l'expéditeur qui est connue uniquement par le WebSocket, qui correspond au login fourni durant la phase d'ouverture. Cette méthode se retrouve bien allégé puisque le décodage du message reçu ne s'y trouve plus.
Phase d'encodage
Pour envoyer des informations aux clients, vous ne devez plus vous soucier de la forme du texte à générer puisque toutes les informations sont stockées dans l'entité. C'est l'encodeur qui s'en occupe. Il suffit juste de faire appel à la méthode sendObject() avec, en argument, l'entité que vous avez renseigné. Rien n'empêche malgré tout d'envoyer le texte que vous voulez sans passer par cette phase d'encodage en utilisant tout simplement les méthodes sendText() comme précédemment.
Prévoir plusieurs encodeurs

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.

MessagePersonnel
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() {  }
}
Flux de texte
L'objectif pour cet encodeur est de créer un texte au format JSON. Cette fois-ci, nous devons mettre en place un flux de texte qui servira ensuite à être envoyé au client concerné. Pour cela, nous utilisons la classe StringWriter. Du coup, la génération du texte formaté se fera à l'aide de la classe JsonWriter.
Génération d'un objet prenant en compte les attributs à envoyer
La classe JsonObjectBuilder permet de générer un objet qui va représenter la structure du document JSON. À l'aide de la méthode add(), vous rajouter les différents attributs qui vont constitués la structure.
Formatage
Une fois que la structure est complète, vous générer le texte à l'aide de l'objet de type JsonWriter. N'oublier surtout pas de clôturer le flux de texte. Le contenu du texte se trouve maintenant en mémoire dans l'objet de type StringWriter.
ListeConnectés
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() {   }
}
Choix du type pour l'encodage
Nous pouvons prendre n'importe quelle classe qui sert de base à l'encodage. Ici, l'objectif est de récupérer la liste des clients connectés. Nous avons tout à fait le droit d'utiliser de prendre List<String>. Après tout c'est une classe comme une autre. Nous retrouvons ce choix comme paramètre de la méthode encode().
Donner la liste des logins
Comme pour le premier encodeur, nous passons par la classe JsonObjectBuilder, sauf que cette fois-ci, il sera composé d'un seul élément nommé connectés. La valeur de cet élément correspond à un ensemble représenté par une structure de tableau que nous implémentons au moyen de la classe JsonArrayBuilder. Le contenu de ce tableau correspond à la liste des clients actuellement connectés.
ServiceChat
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();
   }
}
Anntotation du WebSocket
Dans le même principe, nous devons préciser tous les encodeurs que nous utilisons. Il suffit de donner leurs noms en les entourants cette fois-ci d'accolades.
Appel des bonnes méthodes
Pour choisir le bon encodeur, il suffit de prendre l'objet adéquat lorsque nous appelons la méthode sendObject(), soit avec un argument de type Message, soit avec un argument de type List<String>.
Communication cohérente
Vous remarquez encore une fois qu'il est possible de mélanger les méthodes sendText() et sendObject(). Toutefois, pour le client qui va recevoir les différents messages, il faut qu'il soit capable de les différencier pour permettre leurs interprétations. Nous le verrons lorsque nous construirons notre client, il sera impératif d'avoir systématiquement le même type de format. Si nous commençons avec un format JSON, tous les autres messages devront être dans ce format là.
Résultats

Client WebSocket

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.

Application cliente - Chat

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.

Fichier de projet
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.
Principal.h
#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.
Principal.cpp
#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");
}
Méthodes adaptées
La classe QWebSocket possède un certain nombre de méthodes en relation directe avec toute la procédure nécessaire pour communiquer avec un WebSocket. Pour établir la connexion, vous disposez de la méthode open() qui prend pour argument  l'URL respectant le protocole ws ws://192.168.1.30:8080/ws/chat/manu pa exemple. Pour envoyer un message sous forme de texte, il suffit d'utiliser la méthode sendTextMessage(). A contrario, lorsque vous recevez un message textuel venant cette fois-ci du serveur, vous  êtes alertés grâce au signal nommé textMessageReceived(). Enfin, si vous désirez arrêter la communication avec le service, il suffit d'appeler tout simplement la méthode close(). Vous remarquez que le nom des méthodes est assez évocateur et similaires aux annotations ou aux méthodes utilisées par le service.
Gestion événementielle
La librairie Qt utilise, comme les autres librairies graphique, la gestion événementielle. Dans le cas de Qt, l'approche est assez spéciale, elle fonctionne suivant le principe de signaux et de slots. Même si elle est spéciale, cette façon de faire est géniale dans le sens où il est possible de faire communiquer deux objets qui n'ont absolument rien à voir entre eux pas de relation d'héritage, d'agrégation ou d'association.
Écoute du réseau
Dans le cas qui nous préoccupe, la gestion événementielle paraît tout à fait adapté. Lorsque nous demandons par exemple une connexion au service à l'aide de la méthode open() vue ci-dessus, le signal connected() associé à l'objet webSocket est alors activé si la connexion a pu s'établir avec le serveur. Il en est de même avec la déconnexion au moyen du signal deconnected() et de la réception d'un nouveau message venant du service avec le signal textMessageReceived().
Les slots
À chacun de ces signaux est associée une méthode de rappel slot qui sera automatiquement lancée dès que le signal concerné est activé. C'est dans le constructeur que sont établies ces différentes associations. Respectivement, nous disposons  des slots : connexion(), deconnexion() et receptionMessage().
Décriptage de l'ensemble des messages JSON

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 :

Objets constituant l'IHM

Principal.cpp
#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();
}
Objets spécifiques pour le formatage JSON
À l'image de ce que nous avons découvert dans JEE, la librairie Qt dispose d'un certain nombre de classes capables de décoder ou de générer un texte au format JSON. Il existe trois classes fondamentales, QJsonDocument qui permet de récupérer ou de générer un texte ou un flux binaire au format JSON, QJsonObject qui permet de récupérer ou de générer un ensemble d'attributs dans une même entité et QJsonArray> qui permet de récupérer ou de générer une collection d'éléments.
Récupération d'un document JSON
La classe QJsonDocument nous permet de récupérer un texte au format JSON. Pour cela, vous créez un objet de type QJsonDocument et vous faites appel à la méthode statique fromJson() en passant en argument de la méthode le texte reçu par le serveur.
Récupération d'un objet de la structure JSON
Pour récupérer une entité formée d'un ensemble d'attributs, vous utilisez maintenant la classe QJsonObject. Vous créez un objet de cette classe en vous servant de la méthode object() de la classe QJsonDocument. Une fois que l'objet créé, vous allez récupérer successivement chaque attribut de l'entité en utilisant des crochets et en spécifiant le nom de l'attribut sous forme de chaîne de caractères à l'intérieur de ces crochets. Vous devez utiliser ensuite une méthode qui retourne la valeur de l'attribut suivant le type souhaité, ici nous utilisons la méthode toString().
Récupération d'une collection d'éléments
Lorsque vous devez récupérer un ensemble d'entités, vous passez cette fois-ci par la classe QJsonArray. Cette classe dispose d'une méthode count() qui nous permet de connaître le nombre d'élément constituant la collection. Grâce à cela, nous pouvons ainsi parcourir la collection au travers d'une itérative et d'utiliser encore une fois les crochets pour récupérer chaque élément de la collection.
Générer un document JSON
Cette fois-ci, la manoeuvre est inversée. Vous constituez d'abord votre structure soit avec un objet de type QJsonObject ou un objet de type QJsonArray, vous créez ensuite un document de type QJsonDocument en passant l'objet précédent en argument du constructeur et le trou est joué. Il suffit juste de faire appel à la structure JSON en appelant la méthode toJson() de la classe QJsonDocument.

Envoi de photos

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.

Tous les types de message

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){}
Côté serveur

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.

Entité Message
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.
Décodeur DécodeurMessage
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.
Encodeur MessagePersonnel
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.
Encodeur ListeConnectés
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.
WebSocket ServicePhotos
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();
   }
}
Nouvel attribut dans la classe
Vous remarquez la précense d'un nouvel attribut nommé message qui correspond à l'entité Message. Il va nous servir à mémoriser temporairement la prise en compte d'une nouvelle photo. En effet, lorsqu'un client désire envoyer une photo à un autre client, le transfert se déroule en deux phases. D'une part, le client envoi un message précisant le destinataire de la photo, et ensuite il envoi le flux d'octets correspondant à l'image à transmettre. Il est nécessaire de mémoriser le destinataire au travers de cet attribut message pour savoir à qui envoyer la photo.
Deux types de message
Vous remarquez dans le code que nous disposons de deux méthodes annotées @OnMessage. La première nommée alerte() va nous servir à connaître le destinataire de la photo. Cette méthode doit être appelée en premier pour enregistrer le nom du destinataire au travers de l'attribut message. La deuxième nommée réceptionPhoto(), comme son nom l'indique, permet de récolter l'ensemble de la photo grâce au paramètre de type ByteBuffer. Cette deuxième méthode appelle à son tour la méthode envoyerPhoto() à la place de envoyerMessage() qui a changé son type d'argument.
La méthode envoyerPhoto()
Cette méthode vérifie, grâce à l'attribut message, si le destinataire est déjà connecté. Si c'est le cas, le message est d'abord envoyer au client en passant par la phase d'encodage afin de connaître l'expéditeur de la photo, ceci au moyen de la méthode connue sendObject(). Ensuite, c'est au tour de la photo, grâce au paramètre de type ByteBuffer, et en utilisant la nouvelle méthode sendBinary().Si le destinataire n'est pas encore présent, nous devons enregistrer la photo dans un fichier temporaire dont le nom correspond à l'identifiant de l'entité. Nous enregistrons ensuite l'entité dans la base de données comment avant afin de pouvoir envoyer ultérieurement la photo en attente lorsque le destinataire se connectera enfin.
La méthode photosEnAttente()
Dans cette méthode, nous faisons la procédure inverse. Nous vérifions si le client qui est en train de se connecté ne dispose pas de photos en attente. Si c'est le cas, comme au préalable, nous envoyons d'abord le message, au moyen de la méthode sendObject(), pour indiquer quel est l'expéditeur et nous envoyons ensuite la photo au moyen de la méthode sendBinary(). Durant cette phase, il faut penser, d'une part à supprimer l'enregistrement dans la base de données, mais également à supprimer le fichier photo, pour que ces informations temporaires ne soit plus présentes.
Côté client

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.

IHM Client

Image.h
#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.
Image.cpp
#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());
    }
}
Principal.h
#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().
Principal.cpp
#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();
}
Phase de construction
Nous devons rajouter une nouvelle connexion. En effet, le signal binaryMessageReceived() apparaîtra lorsqu'une photo sera envoyée depuis le serveur. Elle est rattachée au slot receptionPhoto() que nous venons de déclarer.
La méthode envoiMessage()
Cette méthode a subit une modification par rapport au projet précédent. Comme prévu dans le service, elle se déroule en deux phases. Il est nécessaire d'envoyer maintenant uniquement le destinataire de la photo au format JSON. Tout de suite après, nous envoyons la suite d'octets correspondant à l'image.
La méthode receptionMessage()
Cette méthode subit une petite modification, juste pour signaler qu'une photo va être envoyée avec le nom de l'expéditeur.
La méthode receptionPhoto()
Comme son nom l'indique, c'est cette méthode qui récupère le flux d'octets correspondant à la photo envoyée par un autre client par l'intermédiaire du service. Cette méthode envoie tout simplement ce flux d'octets à l'objet photo issu de la classe Image que nous avons créée préalablement.

Mise en place d'un service à l'aide de la librairie Qt

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.

Retour sur le service d'écho codé en Java

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.

Login de connexion
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);
    }
}
Même service codé en C++ à l'aide de la librairie Qt

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.

Utilisation du service avec deux clients

Fichier de projet
#-------------------------------------------------
#
# 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.
IHM (Formulaire)

Principal.h
#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 map qui 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à.
Principal.cpp
#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);
}
Le code source C++ est plus conséquent que l'équivalent en code Java
Lorsque vous regardez le source Java correspondant et l'implémentation de cette classe en C++, vous vous dites que la différence est énorme. Comme précisée en introduction, le problème vient du fait que nous n'avons pas de serveur d'applications. Vous devez donc vous préoccuper de gérer l'ensemble des sessions de tous les clients, la gestion événementielle pour la prise en compte des méthodes de rappel, etc. Ceci dit, lorsque nous proposerons des sujets plus conséquents, nous n'aurons pas beaucoup plus de code à rajouter. C'est finalement l'ossature minimale pour un bon fonctionnement.
Construction de lobjet service
Durant la phase de création d'un objet de type QWebSocketServer vous devez lui associer un nom logique qui correspond au service qui sera utilisé lors du processus handshake du 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://.
Gestion des nouvelles connexions
Dans Qt, les méthodes de rappel lancées automatiquement ne se font pas à l'aide d'annotations, comme c'est le cas avec JEE. Je le rappelle, dans le monde de Qt, la gestion des différents événements venant du réseau ouverture de connexion, fermeture, envoi de messages, etc. se fait avec le principe des 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.
Démarrer ou arrêter le service
L'IHM comporte un seul bouton qui change d'intitulé pour démarrer ou pour arrêter le service. Contrairement aux serveurs d'applications, le service n'est pas actif dès le départ. Il faut le demander explicitement grâce à la méthode listen(). Vous devez alors préciser le mode d'écoute IPv4, IPv6, Localhost, etc. ainsi que le numéro de port. Dans notre cas, nous prenons toutes les requêtes et le numéro de service est le 8080. Pour arrêter le service, nous retrouvons la méthode classique close().
Point de connexion représentant le client - méthode nouvelleConnexion()
Dès que le service est démarré et dès qu'un nouveau client se connecte, vous devez établir une connexion permanente avec lui. Comme nous l'avons déjà vu, cela se fait au moyen d'un objet de la classe QWebSocket. Pour récupérer ce point de connexion particulier, vous devez appeler la méthode nextPendingConnexion() de la classe QWebSocketServer. Grâce à ce point de connexion, et donc grâce à cette classe QWebSocket, vous êtes à même de récupérer un certain nombre d'informations, notamment la partie path de l'URL bien entendu, ici aussi nous n'avons pas d'annotations spécifiques. Avec ce point de connexion, nous pouvons tout de suite envoyer un message de bienvenue au client, grâce à la méthode sendTextMessage() que nous avons déjà utilisé.
Sauvegarde du point de connexion et gestion des événements associés à chaque client - méthode nouvelleConnexion()
Si vous désirez recevoir des messages du client, il faut à tout prix conserver son point de connexion, ce que nous faisons à l'aide de notre map en 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().
Réception des messages
À chaque fois qu'un nouveau message arrive au service, c'est donc la méthode receptionMessage() qui est sollicité. Toutes les classes de la librairie Qt possèdent la méthode sender() issue de la classe de base QObject qui renvoie un pointeur sur l'objet qui a envoyé le signal événement. Dans notre cas, c'est l'objet de type QWebSocket représentant le client qui a provoqué l'appel de la méthode receptionMessage(). Il suffit de récupérer le nom du login du client correspondant et de répondre immédiatement au client avec la méthode sendTextMessage().
Déconnexion du client
La méthode deconnexion() gère l'événement qui prend en compte la fin de connexion du client. Il faut alors retirer le point de connexion correspondant à ce client de la map et détruire l'objet QWebObjet qui ne sera alors plus utile, ce que fait la méthode deleteLater().
Communiquer avec tous les clients en donnant la liste des connectés

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.

Principal.h
#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().
Principal.cpp
#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.
Communication entre clients connectés (chat)

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.

Principal.cpp
#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.
Messagerie asynchrone

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.

Principal.h
#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 map qui 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 map sera enregistrée le nom du destinataire et le contenu du message.
Principal.cpp
#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);
}
Destinataire pas encore connecté
Si le destinataire du message n'est pas encore présent, nous conservons son message lors de l'exécution de la méthode receptionMessage().
Nouvelle ouverture de session
A chaque fois qu'un nouveau client se connecte, il faut vérifier si un message personnel n'est pas en attente. Si c'est le cas, le message est alors tout de suite envoyé au client. Il ne faut pas oublier de le supprimer de la liste des messages conservés.
Désactiver les événements des clients qui se déconnectent
Vous remarquez que j'ai rajouté deux lignes supplémentaires dans la méthode deconnexion(). À chaque fois qu'un nouveau client se connecte, nous avons prévu une gestion événementielle adaptée. Il est souhaitable de désactiver cette gestion personnalisée dès que le client se déconnecte et ne fait donc plus parti du service.
Échanges de photos

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.

Principal.h
#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
Nouvelle structure
Un certain nombre de nouveautés apparaissent dans la partie déclaration, notamment une nouvelle structure, nommée Photo qui va nous permettre d'enregistrer, pour une même entité, un certain nombre d'élément. Comme nous nous en doutons, pour envoyer une photo il est important de connaître son expéditeur, son destinataire et également le nom du fichier de sauvegarde si le destinataire n'est pas encode connecté.
Nouvelle map
Vous remarquez également que la map a changée de nom, nous avons maintenant l'attribut photos à la place de messages. La structure de la map n'est plus la même puisque cette fois-ci à chaque destinataire nous enregistrons les informations issues de la structure Photo.
Enregistrement temporaire
Vous savez que l'envoi d'un photo se déroule en deux phases, d'abord le nom du destinataire est envoyé et ensuite le contenu de la photo elle-même. C'est pour cela que nous avons besoin de l'attribut photo.
Identifier le fichier temporaire
Dans le cas où le destinataire n'est pas encore connecté, nous devons enregistrer la photo dans un fichier. Le nom du fichier doit être un identifiant unique, ici un simple numéro qui sera automatiquement incrémenté, représenté par l'attribut id.
Nouvelle méthode
Pour recevoir la photo, nous passons par la méthode receptionPhoto() qui permet de récupérer le flux d'octets.
Principal.cpp
#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());
}
Gestion événementielle
Nous devons maintenant prendre en compte un nouveau type de message entrant pour chaque client connecté. Puisque des flux binaires peuvent être transmis par le réseau, il faut pouvoir activer et désactiver par la suite la méthode de rappel receptionPhoto().
Réception de message textuel
La méthode receptionMessage() n'a plus la même fonction. L'objectif maintenant est de récupéré le nom du destinataire et de le sauvegarder temporairement, en même temps d'ailleurs que l'expéditeur. Comme le texte envoyé est format JSON, il est nécessaire de le décripter.
Gestion de la photo reçue
La nouvelle méthode receptionPhoto() permet de récupérer le flux d'octets venant du client. Nous devons contrôler si le destinataire de la photo est connecté. Si c'est le cas, nous envoyons instantanément un message stipulant le nom de l'expéditeur de la photo et tout de suite après, nous envoyons le flux d'octets ainsi récupéré. En même temps, nous envoyons également un message à l'expéditeur confirmant l'envoi de la photo. Si le destinataire n'est pas encore présent sur le réseau, nous devons enregistrer la photo dans un fichier avec un identifiant unique. Nous enregistrons toutes ces informations expéditeur, destinaire et nom du fichier dans la map photos dans l'attente de la connexion du destinataire.
Le destinataire se connecte enfin
Dès qu'un nouveau client se connecte, il faut vérifier s'il ne dispose d'une photo en attente. Si c'est le cas, dans la méthode nouvelleConnexion(), nous parcourons la map photos à 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 map photos.
Revoir la liste des connectés
Pour conclure, pour être en bonne relation avec le client, vous devez formaliser la liste des connectés au format JSON.

IconTray et fichier de configuration avec QT

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.

Intégration des ressources dans un projet Qt

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.

Une ressource est un fichier XML, dont l'extension est *.qrc qui 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.
ressources.qrc

Avantages
L'intégration de données dans l'exécutable présente plusieurs avantages : les données ne peuvent être perdues et cette opération permet la création d'exécutables véritablement autonomes.
Inconvénients
Les inconvénients sont les suivants : si les données intégrées doivent être changées, il est impératif de remplacer l'exécutable entier, et la taille de ce dernier sera plus importante car il doit s'adapter aux données intégrées.
Système de notification en barre des tâches du système d'exploitation

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.

Exemple

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().
La classe QFile

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().

Méthodes utiles
QFile(nom du fichier)
Construit un objet représentant le fichier spécifié en argument. Si vous spécifier un fichier au travers d'un répertoire, vous utilisez le séparateur quelque soit le système de fichier, même pour Windows. ATTENTION, le symbole \ n'est pas du tout supporté.
copy(nom du fichier source, nom du fichier destination)
Copie un fichier et en crée un autre. Si l'opération s'est bien déroulée, la fonction retourne true, sinon false dans le cas contraire.
exists()
Permet de savoir si le fichier représenté par QFile existe vraiment.
fileName() - setFileName(nom du fichier)
Permet de connaître le nom du fichier représenté par QFile. Il est également possible de représenter un autre fichier au moyen du même objet QFile.
open(mode d'ouverture)
Ouvre réellement le fichier représenté par QFile. Nous pouvons ouvrir le fichier en lecture seule QIODevice::ReadOnly, en écriture seule QIODevice::WriteOnly, en lecture et écriture QIODevice::ReadWrite. Vous pouvez rajoutez des spécifications supplémentaires pour indiquer par exemple que le fichier doit être un fichier texte au moyen de la constante suivante QIODevice::Text.
permissions() – setPermissions(permissions)
Il est possible de connaître ou de régler les permissions accordées pour chaque fichier. Ces permissions sont de la même nature que celles que vous rencontrez sous les systèmes Unix. La classe QFile possède en interne une énumération dénommée Permission qui propose l'ensemble des constantes correspondant à ce système de fichier. Nous retrouvons les quatre types d'utilisateurs, respectivement : Propriétaire, Utilisateur, Groupe et Autre. enum Permission {ReadOwner, WriteOwner, ExeOwner, ReadUser, WriteUser, ExeUser, ReadGroup, WriteGroup, ExeGroup, ReadOther, WriteOther, ExeOther};
remove()
Supprime définitivement le fichier en cours et renvoie true si l'opération s'est déroulée correctement.
rename(nouveau nom)
Permet de changer le nom du fichier.
close()
Clôture le fichier si ce dernier est ouvert. Permet ainsi d'éviter de perdre des données. Ceci-dit, cette méthode est automatiquement appelée lorsque l'objet QFile est détruit, notamment lorsque nous sortons de la portée de la déclaration de l'objet.
size()
Retourne la taille du fichier en octets.
Lire et écrire des données binaires

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. Si nous souhaitons lire ou écrire dans un fichier en une seule fois, nous pouvons éviter l'utilisation de QDataStream et recourir à la place aux méthodes write() et readAll() de QIODevice et donc de QFile.
Lire et écrire du texte

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.
Service chat

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.

ressources.qrc

Interface Home Machine

Lorsque vous mettez en place un menu contextuel pour le système de notification, vous avez besoin de créer autant d'action associé à chacun des éléments du menu.

Au départ, au lancement du programme, la fenêtre principale de l'application ne doit pas être visible. C'est au moyen de l'action actionAfficher que la fenêtre s'affiche, cette action étant lancée au travers du menu contextuel Afficher la fenêtre. La clôture du programme ne peut se faire qu'avec le menu contextuel Quitter l'application et non pas avec le bouton système de clôture x . Ce dernier sert uniquement à cacher la fenêtre temporairement jusqu'à une nouvelle demande de réaffichage.
main.cpp
#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().
Principal.h
#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
Arrêt du programme
La clôture du programme ne peut se faire qu'avec le menu contextuel Quitter l'application et non pas avec le bouton système de clôture x . Dès que nous cliquons sur cette option de menu, la méthode arreter est alors automatiquement lancée. Cette méthode active l'attribut booléen quitter false par défaut qui sert maintenant à déterminer si nous désirons réellement quitter l'application. Nous devons également redéfinir la méthode closeEvent() afin d'empêcher la clôture automatique avec le bouton système x .
Système de notification
Pour mettre en oeuvre le système de notification, vous devez utiliser la classe QSystemIconTray. L'initialisation de l'attribut notification et de la mise en place du menu contextuel sont décrits dans la méthode setNotification().
Principal.cpp
#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();
}
Clôture du programme
Dans la méthode closeEvent() nous vérifions bien que l'utilisateur désire quitter le programme. Si c'est le cas, nous ne faisons rien de particulier, ce qui donnera l'arrêt normal et définitf. Dans le cas contraire, nous demandons à cacher la fenêtre et d'ignorer la demande de clôture. La méthode arreter() valide la demande de clôture.
Système de notification
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().
La méthode showMessage(titre, message, icône, durée)
Message contextuel avec un titre qui apparaît pendant 10 secondes par défaut. Si vous désirez changer le temps d'affichage, vous devez spécifier le nombre de millisecondes sur l'argument durée. Dans ce message contextuel apparaît une petite icône qui indique le type de message, soit une simple information, soit un message d'avertissement ou enfin un message d'erreur. Prenez l'une des constantes suivantes pour choisir l'icône qui convient :
enum {NoIcon, Information, Warning, Critical};
L'application cliente

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.

Fichier de configuration config
{
    "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.
ressources.qrc

Interface Homme Machine

main.cpp
#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().
Principal.h
#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.
Principal.cpp
#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();
}

Service Websocket sur système embarqué

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é.

Mise en oeuvre du service

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.

Fichier de projet
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.
principal.cpp
#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;
}
Fonction principale
Ce code source possède la fonction principale de l'application. Comme pour tout environnement Qt, nous passons par la classe QCoreApplication.
Intérêt de la classe QCoreApplication
Cette classe possède un certain nombre de fonctionnalités importantes pour la gestion d'une application. La première concerne la gestion événementielle grâce à la méthode connect(). La deuxième qui va avec et qui est fondamentale est de permettre le fonctionnement continuel, grâce à la méthode exec() qui tourne en tâche de fond et qui attend jusqu'à ce que l'utilisateur désire s'arrêter.
Lancement du service et arrêt du programme
Le service est représenté par la classe du même nom Service. Nous créons un objet de cette classe avec en argument du constructeur le numéro du service désiré, proposé en option de la ligne de commande. L'arrêt de l'application et donc du service se fait lorsque le signal arreter() de la classe Service est sollicité. La méthode quit() de la classe QCoreApplication est alors exécutée se qui clôture définitivement l'application, la méthode exec() se termine.
service.h
#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.
service.cpp
#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.

Sécurisation des échanges par WebSocket - OpenSSL

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.

Qu'est-ce que le SSL ?

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.

Cryptage

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.

La cryptographie symétrique

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.

La cryptographie asymétrique

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.

Cryptographie asymétrique dans le détail

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

La clef qui est choisie privée n'est jamais transmise à personne alors que la clef qui est choisie publique est transmissible sans restrictions. Ainsi, à l'établissement de la connexion, les différents acteurs s'échangent leurs clés publiques, on parle de transaction :

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 pirate peut 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.

Autorité de certification

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.
Marche à suivre pour la génération des clés et des certificats avec OpenSSL

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.

Boîte à outils d'OpenSSL

Il faut saisir une commande du type :

openssl commande [paramètres de la commande]
Génération de la clé privée du serveur

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.

Demande de signature du certificat (pour la clé publique)

À 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.
Génération de la clé privée du client avec sa demande de signature de certificat

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

Nous sommes l'autorité de certification

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
Paramètres courants de openssl
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é.

C'est notre certificat d'autorité de certification qui va nous permettre de signer tous les certificats créés pour le serveur et pour les clients.
Certification de nos clés publiques serveur et client pour diffusion

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 :

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

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 va 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 :
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
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.

Service chat

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.

ressources.qrc

Fichier de projet
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
Interface Home Machine

Lorsque vous mettez en place un menu contextuel pour le système de notification, vous avez besoin de créer autant d'action associé à chacun des éléments du menu.

Au départ, au lancement du programme, la fenêtre principale de l'application ne doit pas être visible. C'est au moyen de l'action actionAfficher que la fenêtre s'affiche, cette action étant lancée au travers du menu contextuel Afficher la fenêtre. La clôture du programme ne peut se faire qu'avec le menu contextuel Quitter l'application et non pas avec le bouton système de clôture x . Ce dernier sert uniquement à cacher la fenêtre temporairement jusqu'à une nouvelle demande de réaffichage.
main.cpp
#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().
Principal.h
#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
Modification du code précédent
La seule méthode rajouté se nomme configurationSSL(). Cette méthode sera automatiquement appelée par le constructeur et c'est à ce moment là que les clés que nous avons générées seront prisent en compte. Toute la gestion de la sécurisation par SSL se trouve dans cette unique méthode.
Principal.cpp
#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();
}
QWebSocketServer::SecureMode
Demande de sécurisation des transactions. Cette classe permet d'utiliser le protocole suivant deux modes, avec ou sans sécurité. Dans ce dernier cas, l'URL de connexion est la suivante wss://adresse:port/action à la place de ws://adresse:port/action.
QSslConfiguration
Heureusement, il existe une classe spécialisée pour la configuration du protocole de sécurité SSL avec la prise en compte des différentes clés que nous venons de générer.
QSslCertificate
Comme son nom l'indique, cette classe représente la clé publique certifiée à partir du fichier serveur.crt que nous venons de produire.
QSslKey
Comme son nom l'indique, cette classe représente la clé privée à partir du fichier serveur.key que nous venons de produire. Vous devez préciser le type d'algorithme de cryptage. Nous avons vu que pour générer cette clé privée, openssl utilise le cryptage RSA 2048 bits.
ssl.setPeerVerifyMode(QSslSocket::VerifyNone);
Lorsque vous utilisez un navigateur et que vous allez sur un site en mode HTTPS, il est généralement souhaitable de connaître l'identité de l'organisme qui a signé le certificat. Pour les applications classiques fenêtrées cette vérification ne possède peu d'intérêt puisque la communication en mode SSL se fait en coulisse sans que l'utilisateur s'en rende compte.
L'application cliente

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.

Fichier de configuration config
{
    "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.
ressources.qrc

Interface Homme Machine

main.cpp
#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().
Principal.h
#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.
Principal.cpp
#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();
}
configurationSSL();
Nous avons besoin de la même méthode. Tout le contenu est strictement similaire au serveur. Seul les noms des fichiers relatifs aux clés sont différents.
QString url = "wss://";
ATTENTION : il est impératif d'établir la connexion en mode sécurisé en proposant la bonne URL.
Auto-signer ses propres certificats sans passer par l'autorité de certification

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é)

Modification du code afin de pouvoir introduire les mots de passes des clés privées
ServeurWebSocket/Principal.cpp
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);
}
ClientWebSocket/Principal.cpp
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);
}
Ne pas mettre de mot de passe sur la clé privée

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é)
ServeurWebSocket/Principal.cpp
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);
}
ClientWebSocket/Principal.cpp
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.