IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)

 

Développons en Java   2.30  
Copyright (C) 1999-2022 Jean-Michel DOUDOUX    (date de publication : 15/06/2022)

[ Précédent ] [ Sommaire ] [ Suivant ] [Télécharger ]      [Accueil ]

 

76. L'API WebSocket

 

chapitre    7 6

 

Niveau : niveau 4 Supérieur 

 

Comme fréquemment avec une nouvelle technologie, la communauté Java propose différentes solutions commerciales ou open source (notamment avec le framework Atmosphere), chacune avec une API différente. Une fois que la technologie est suffisamment mature, une spécification standard est développée par le JCP.

Les Websockets n'échappent pas à cette situation : la JSR 356 est une spécification pour la mise en oeuvre des Websockets dans la plate-forme Java aussi bien côté serveur que côté client.

Cette spécification propose les caractéristiques principales suivantes :

  • développer des WebSocket Endpoint qui sont des composants Java capable d'utiliser le protocole WebSocket
  • utilisation d'annotations ou de l'API pour développer les endpoints
  • envoi et consommation de messages au format texte ou binaire
  • utilisation de messages entiers ou d'un ensemble de morceaux
  • envoi de messages de manière synchrone ou asynchrone
  • utilisation d'encodeurs ou décodeurs pour mapper des objets en messages en vice versa

L'API WebSocket est de type event-driven : selon les différents événements qui surviennent durant le cycle de vie de la websocket, des callbacks à implémenter sont invoqués par le conteneur.

Un client Java peut utiliser une implémentation de la JSR 356 pour communiquer par Websockets avec un serveur.

La mise en oeuvre des WebSockets côté client et serveur peut se faire de deux manières :

  • utilisation d'annotations sur des POJO
  • utilisation d'une API

La JSR 356 est incluse dans les spécifications de la plate-forme Java EE 7 : chaque serveur d'applications doit fournir une implémentation de cette spécification. D'autres implémentations peuvent être utilisées en dehors d'un contexte Java EE, par exemple Tomcat 8 propose un support de la JSR 356.

L'implémentation de référence est le projet Tyrus dont la page officielle est à l'url : https://tyrus.java.net/

Ce chapitre contient plusieurs sections :

 

76.1. Les principales classes et interfaces

La communication entre un client et un serveur se fait au moyen d'endpoints : un endpoint côté client et un endpoint côté serveur.

Avec une WebSocket, l'endpoint client est celui qui initialise la connexion. Une fois la connexion établie, les deux endpoints possèdent les mêmes fonctionnalités.

Un endpoint est géré par un conteneur encapsulé dans une instance de type WebSocketContainer. Ce conteneur encapsule plusieurs paramètres relatifs à la communication (timeout par défaut, buffer, ...) et permet la gestion des extensions.

L'interface ServerContainer hérite de l'interface WebSocketContainer. Elle ajoute deux surcharges de la méthode add() qui permettent d'enregistrer des endpoints dans le conteneur.

Il ne doit y avoir qu'une seule instance de type ServerContainer pour une même application.

Une implémentation de la JSR 356 qui puisse être exécutée en dehors d'un conteneur web doit fournir sa propre solution pour obtenir une instance de type ServerContainer.

L'ouverture d'un channel pour une socket permet d'obtenir une instance de type javax.websocket.Session qui permet de gérer la communication : enregistrement de handler pour traiter les messages reçus, fermer un channel, obtenir une instance de type RemoteEndpoint pour envoyer des messages, obtenir et modifier des paramètres de configuration, obtenir des informations sur l'état, ...

La conception de l'API assure une séparation des rôles entre le Endpoint qui encapsule le cycle de vie du endpoint (open, close, error) et la gestion des messages reçus qui est assurée par un objet de type MessageHandler.

Les principales classes du package javax.net.websocket sont :

Classe Rôle

MessageHandler

Gestion des messages entrant d'un endpoint

RemoteEndpoint

Envoie de messages à l'autre endpoint

Session

Encapsule la conversation

Endpoint

Encapsule un endpoint


Les différences entre l'API client et l'API serveur sont minimes : l'API client est un sous-ensemble de l'API serveur.

 

76.1.1. L'interface javax.websocket.Session

L'interface javax.websocket.Session définit les fonctionnalités relatives à une conversation entre un endpoint et son équivalent distant.

Une instance de type Session est valide tant que la connexion n'est pas fermée : si la session est fermée, une invocation d'une de ses méthodes lève une exception de IllegalStateException

Dès que la connexion est créée lors du handshake, l'implémentation associe au endpoint une instance de type Session. Plusieurs solutions sont utilisables pour obtenir cette instance selon le mode de développement du endpoint :

  • en utilisant les annotations : les méthodes annotées @OnOpen, @OnMessage, @OnClose et @OnError peuvent avoir un paramètre de type Session
  • en utilisant l'API : les méthodes onOpen(), onClose() et OnError() possèdent un paramètre de type Session

Dans ces cas, l'implémentation se charge de passer l'instance de la Session en paramètre lors de l'invocation de ces méthodes.

La classe Session propose plusieurs méthodes permettant d'obtenir des informations sur la connexion :

Méthode

Rôle

void addMessageHandler(MessageHandler handler)

Enregistrer un objet qui va gérer les messages entrants

void close()

Fermer la conversation avec un code de status normal et sans description de la raison

void close(CloseReason closeReason)

Fermer la conversation avec un code de status normal en précisant la description de la raison

RemoteEndpoint.Async getAsyncRemote()

Obtenir une référence sur un objet qui encapsule l'autre partie de la conversation et permet de lui envoyer des messages de manière asynchrone

RemoteEndpoint.Basic getBasicRemote()

Obtenir une référence sur un objet qui encapsule l'autre partie de la conversation et permet de lui envoyer des messages

WebSocketContainer getContainer()

Obtenir le conteneur qui gère la session

String getId()

Obtenir l'identifiant unique de la session

int getMaxBinaryMessageBufferSize()

Obtenir la taille maximale du buffer d'un message binaire gérable par la session

long getMaxIdleTimeout()

Obtenir le timeout en millisecondes avant que la conversation puisse être fermée par le conteneur si la session est inactive

int getMaxTextMessageBufferSize()

Obtenir la taille maximale du buffer d'un message texte gérable par la session

Set<MessageHandler> getMessageHandlers()

Obtenir une copie immuable de l'ensemble des objets qui gèrent les messages entrants

List<Extension> getNegotiatedExtensions()

Obtenir une liste des extensions utilisées pour la conversation

String getNegotiatedSubprotocol()

Obtenir le sous protocole demandé lors du handshake de la connexion

Set<Session> getOpenSessions()

Obtenir une copie de l'ensemble des sessions ouvertes sur le même endpoint que la session

Map<String,String> getPathParameters()

Obtenir une collection des paramètres (nom/valeur) utilisés dans la requête pour ouvrir la session

String getProtocolVersion()

Obtenir la version du protocole WebSocket utilisée par la conversation

String getQueryString()

Obtenir la requête utilisée pour ouvrir la session

Map<String,List<String>> getRequestParameterMap()

Obtenir une collection des paramètres utilisés dans la requête pour ouvrir la session

URI getRequestURI()

Obtenir l'URI et ses paramètres utilisés pour ouvrir la session

Principal getUserPrincipal()

Obtenir l'utilisateur identifié pour cette session s'il est défini sinon renvoie null

Map<String,Object> getUserProperties()

Obtenir une collection des propriétés spécifiques à la conversation

boolean isOpen()

Renvoyer un booléen qui précise si la socket sous-jacente est ouverte ou non

boolean isSecure()

Renvoyer un booléen qui précise si la socket sous-jacente utilise un protocole sécurisé

void removeMessageHandler(MessageHandler handler)

Retirer l'objet qui gère les messages entrants de ceux associés à la conversation

void setMaxBinaryMessageBufferSize(int length)

Définir la taille maximale du buffer d'un message binaire gérable par la session

void setMaxIdleTimeout(long milliseconds)

Définir le timeout en millisecondes avant que la conversation puisse être fermée par le conteneur si la session est inactive. La valeur fournie en paramètre doit être supérieure à zéro.

void setMaxTextMessageBufferSize(int length)

Définir la taille maximale du buffer d'un message binaire gérable par la session


La collection de type Map<String, Object> retournée par la méthode getUserProperties() permet de stocker des informations spécifiques à la session et à l'application qui pourront ainsi être partagées par les différents échanges de la conversation.

La classe CloseReason encapsule la raison de la fermeture de la websocket. Elle ne possède qu'un seul constructeur qui attend en paramètre une valeur de type CloseReason.CloseCodes et une chaîne de caractères qui décrit la raison de la fermeture.

L'énumération CloseReason.CloseCodes contient les codes de fermeture définit par la spécification.

Exemple ( code Java 7 ) :
   
session.close(new CloseReason(CloseCodes.NORMAL_CLOSURE, "Fin de la conversation"));

Il est important de s'assurer de la fermeture d'une session lorsque celle-ci n'est plus utilisée pour permettre de libérer les ressources consommées par la WebSocket.

 

76.1.2. Les interfaces RemoteEndpoint

L'interface javax.websocket.RemoteEndpoint définit les fonctionnalités utilisables sur l'endpoint distant de la conversation notamment l'envoi de messages.

Il existe deux types de RemoteEndpoint :

  • RemoteEndpoint.Basic pour l'envoi synchrone d'un message
  • RemoteEndpoint.Async pour l'envoi asynchrone d'un message

Pour obtenir une instance de type RemoteEndpoint, il faut invoquer la méthode getBasicRemote() ou la méthode getAsyncRemote() de la classe Session.

Il n'y a pas de garantie sur la livraison du message au endpoint.

L'interface RemoteEndpoint définit plusieurs méthodes :

Méthode

Rôle

void flushBatch()

Indiquer à l'implémentation que tous les messages peuvent être envoyés au endpoint

boolean getBatchingAllowed()

Renvoyer un booléen qui précise si l'implémentation peut utiliser le mode batching

void sendPing(ByteBuffer data)

Envoyer un message de type ping contenant les données fournies en paramètres

void sendPong(ByteBuffer data)

Envoyer un message de type pong contenant les données fournies en paramètres

void setBatchingAllowed(boolean allowed)

Préciser à l'implémentation si elle peut utiliser le mode batching pour envoyer un message


Le mode batching permet à l'implémentation de traiter par lots les messages à envoyer. Toutes les implémentations ne proposent pas un support du mode batch qui est désactivé par défaut.

Lorsque le mode batching est utilisé, il est nécessaire d'invoquer explicitement la méthode flushBatch() pour s'assurer que tous les messages ont été envoyés.

L'interface RemoteEndpoint.async définit les fonctionnalités pour l'envoi de messages de manière asynchrone. Elle définit plusieurs méthodes :

Méthode

Rôle

long getSendTimeout()

Retourner le nombre de millisecondes durant laquelle l'implémentation peut attendre pour envoyer le message

Future<Void> sendBinary(ByteBuffer data)

Envoyer des données binaires de manière asynchrone

Future<Void> sendBinary(ByteBuffer data, SendHandler handler)

Envoyer des données binaires de manière asynchrone

Future<Void> sendObject(Object data)

Envoyer un objet de manière asynchrone

Future<Void> sendObject(Object data, SendHandler handler)

Envoyer un objet de manière asynchrone

Future<Void> sendText(String data)

Envoyer des données binaires de manière asynchrone

Future<Void> sendText(String data, SendHandler handler)

Envoyer des données binaires de manière asynchrone

void setSendTimeout(long timeout)

Définir le nombre de millisecondes que peut attendre l'implémentation pour envoyer un message


Exemple ( code Java 7 ) :
public void envoyerMessagePartiel(Session session, String message, Boolean isLast) throws
  IOException {
  session.getBasicRemote().sendText(message, isLast);
}

L'interface RemoteEndpoint.Basic définit les fonctionnalités pour l'envoi de messages de manière synchrone. Elle définit plusieurs méthodes :

Méthode

Rôle

OutputStream getSendStream()

Obtenir un flux pour envoyer des données binaires

Writer getSendWriter()

Obtenir un flux pour envoyer des données textuelles

void sendBinary(ByteBuffer data)

Envoyer des données binaires

void sendBinary(ByteBuffer partialData, boolean isLast)

Envoyer un morceau des données binaires. Le booléen indique si c'est le dernier morceau

void sendBinary(Object data)

Envoyer un objet

void sendText(String data)

Envoyer des données textuelles

void sendText(String partialData, boolean isLast)

Envoyer un morceau des données textuelles. Le booléen indique si c'est le dernier morceau


L'envoi de message est bloquant jusqu'à ce que tout le message ait été envoyé à la connexion sous-jacente.

Une exception de type IllegalStateException peut être levée si deux messages sont envoyés en même temps sur la même connexion.

 

76.1.3. Les interfaces MessageHandler

L'interface MessageHandler définit les fonctionnalités relatives à la réception d'un message dans une conversation.

L'interface MessageHandler est utilisée :

  • directement par le développeur pour traiter les messages reçus par un endpoint développé avec l'API
  • indirectement par l'implémentation pour les méthodes des endpoints annotés avec @OnMessage

Les spécifications du protocole WebSocket précise qu'un message peut être envoyé dans son intégralité ou de manière partielle. L'interface MessageHandler possède deux interfaces imbriquées pour supporter ces fonctionnalités :

  • MessageHandler.Partial : utilisée par l'implémentation pour traiter un message partiel
  • MessageHandler.Whole : utilisée par l'implémentation pour traiter un message complet

L'interface MessageHandler.Partial<T> est un handler qui sera invoqué par l'implémentation lors de la réception d'une partie d'un message.

Le type T peut être :

  • String dans le cas d'un message de type texte
  • ByteBuffer ou Byte[] dans le cas d'un message de type binaire

Il ne faut pas utiliser l'instance de type ByteBuffer après l'invocation de la méthode onMessage() car l'implémentation peut recycler cette instance.

Elle ne définit qu'une seule méthode :

Méthode

Rôle

void onMessage(T partialMessage, boolean last)

Traiter un message partiel qui a été reçu par l'implémentation


L'interface MessageHandler.Whole<T> est un handler qui sera invoqué par l'implémentation lors de la réception d'un message.

Le type T peut être :

  • String, Reader ou un objet pour lequel un Decoder.Text ou Decoder.TextStream est enregistré dans le cas d'un message de type texte
  • ByteBuffer, Byte[], InputStream ou un objet pour lequel un Decoder.Binary ou Decoder.BinaryStream est enregistré dans le cas d'un message de type binaire
  • PongMessage pour les messages de type pong

Il ne faut pas utiliser l'instance de type ByteBuffer, Reader et InputStream après l'invocation de la méthode onMessage() car l'implémentation peut recycler cette instance.

Elle ne définit qu'une seule méthode

Méthode

Rôle

void onMessage(T message)

Traiter un message qui a été reçu par l'implémentation


Il est nécessaire d'enregistrer un MessageHandler à la Session.

Exemple ( code Java 7 ) :
public class MonEndpoint extends Endpoint {
      
    @Override
    public void onOpen(Session session, EndpointConfig EndpointConfig) {
        session.addMessageHandler(new MessageHandler.Whole<String>() {
            @Override
            public void onMessage(String message) {
                System.out.println("Message recu: "+message);
            }
        });
    }
}

Une instance de type MessageHandler ne sera invoquée que par un seul thread d'une session à la fois. Si une même instance de type MessageHandler est associée à plusieurs sessions, alors la gestion des accès concurrents doit être prise en charge dans l'implémentation du MessageHandler.

Il n'est possible de n'enregistrer qu'un seul MessageHandler complet ou partiel pour un même type de données sur une même session. Par exemple, il n'est possible d'enregistrer un MessageHandler.Whole de type texte et un MessageHandler.Partial de type texte sur la même session.

Exemple ( code Java 7 ) :
    @Override
    public void onOpen(Session session, EndpointConfig config) {
        final RemoteEndpoint.Basic remote = session.getBasicRemote();
        session.addMessageHandler(new MessageHandler.Whole<String>() {
            @Override
            public void onMessage(String text) {
                try {
                    remote.sendText(text);
                } catch (IOException ioe) {
                   LOGGER.log(Level.SEVERE, "Erreur durant l'envoi",ioe);
                }
            }
        });
        session.addMessageHandler(new MessageHandler.Partial<String>() {
            @Override
            public void onMessage(String text, boolean last) {
                try {
                    remote.sendText(text);
                } catch (IOException ioe) {
                    LOGGER.log(Level.SEVERE, "Erreur durant l'envoi",ioe);
                }
            }
        });
    }
Résultat :
janv. 18, 2014 6:00:37 PM
fr.jmdoudoux.dej.websockets.server.MonEchoEndpoint onError
Grave: onError java.lang.IllegalStateException: Text
MessageHandler already registered.

Il n'est donc possible d'enregistrer qu'un seul MessageHandler pour des données de texte, un seul pour des données binaires et un seul pour des messages de type pong.

L'implémentation peut proposer de gérer elle-même les messages partiels en les stockant jusqu'à la réception du dernier morceau et invoquer le MessageHandler pour message complet avec l'intégralité du message. Inversement, si seulement un MessageHandler pour message partiel est enregistré et qu'un message complet est envoyé par l'endpoint distant alors l'implémentation peut invoquer le handler en lui passant le message.

 

76.2. Le développement d'un endpoint

La JSR 356 définit une API qui permet d'utiliser les WebSockets dans une application aussi bien côté serveur que côté client.

Les WebSockets fonctionnent en mode client/serveur. Le client est toujours responsable de l'initialisation de la communication en demandant l'ouverture de la connexion. Côté serveur, l'endpoint est publié pour attendre une demande de connexion d'un client. Une fois la connexion établie, le client et le serveur peuvent agir de manière symétrique : les API pour la partie cliente et serveur sont similaires.

Lors de l'utilisation de WebSockets, plusieurs événements peuvent survenir :

  • un client initialise une connexion HTTP de type handshake vers le serveur
  • le serveur répond pour établir la connexion en changeant le protocole de HTTP à WebSocket
  • le client et le serveur peuvent alors émettre et recevoir des messages de manière symétrique
  • la connexion peut être fermée par le client ou le serveur

Dans un endpoint, plusieurs méthodes sont définies comme des callbacks qui seront invoquées selon les événements de la communication : onOpen, onMessage, onError et onClose.

Pour répondre à ses événements, la JSR 356 propose deux modèles de programmation pour définir des méthodes qui seront les callbacks sur les événements liés aux conversations avec la WebSocket :

  • utilisation d'annotations sur des POJO et les méthodes de ses POJO
  • utilisation de l'API en implémentant l'interface Endpoint

 

76.2.1. Le développement d'un endpoint avec les annotations

La manière la plus simple de définir un endpoint est d'utiliser les annotations.

La JSR 356 définit plusieurs annotations :

Annotation

Application

Utilité

@javax.websocket.server.ServerEndpoint

Classe

Définir un POJO comme étant un endpoint côté serveur

@javax.websocket.OnOpen

Méthode

Déclarer une méthode comme étant un callback lors d'un événement de type Open

@javax.websocket.OnClose

Méthode

Déclarer une méthode comme étant un callback lors d'un événement de type Close

@javax.websocket.OnMessage

Méthode

Déclarer une méthode comme étant un callback lors d'un événement de type Message

@javax.websocket.server.PathParam

Paramètre d'une méthode

Permet de mapper un paramètre d'une méthode avec un marqueur défini dans le template de l'uri associé du endpoint. A la réception d'une requête HTTP qui satisfasse le template, le conteneur va extraire la valeur est la passé comme valeur du paramètre de la méthode annotée

@javax.websocket.OnError

Méthode

Déclarer une méthode comme étant un callback lors d'une erreur

 

76.2.1.1. L'annotation @ServerEndpoint

Le développement d'un endpoint serveur peut se faire en utilisant un simple POJO annoté avec l'annotation @javax.websocket.server.ServerEndpoint : ceci permet de préciser au conteneur que la classe est un endpoint pour WebSocket côté serveur.

L'annotation @ServerEndpoint possède plusieurs attributs :

Nom

Rôle

value

URI relative ou template pour l'URI relative à l'url du contexte de la webapp. Obligatoire

decoders

Enregistrer des décodeurs

encoders

Enregistrer des encodeurs

subprotocols

Définir la liste des noms des sous-protocoles qui sont supportés

Exemple : soap, wamp (websocket application message processing), ...

Le premier nom de la liste qui correspond à celui fourni par l'endpoint client sera utilisé

configurator

Fournir le type d'une classe de type ServerEndpointConfig.Configurator qui permettra de configurer les connexions au endpoint.


Exemple ( code Java 7 ) :
@ServerEndpoint(
    value = "/monendpoint",
    decoders = MonDecoder.class,
    encoders = MonEncoder.class,
    subprotocols = {"sousprotocole1", "sousprotocole2"},
    configurator = MonConfigurator.class)
public class MonServeurEndpoint {
}

L'annotation @ServerEndpoint attend une valeur d'attribut obligatoire qui précise l'URI associée au endpoint. La valeur de l'URI doit obligatoirement commencer par un caractère slash et peut se terminer ou pas par un caractère slash.

Exemple ( code Java 7 ) :
@ServerEndpoint("/echo") 
public class EchoEndpoint { }

Le chemin précisé comme URI peut contenir des paramètres dont les valeurs correspondantes seront extraites à l'exécution de l'URL utilisée. L'obtention de la valeur se fait en utilisant l'annotation @PathParam.

Exemple ( code Java 7 ) :
@ServerEndpoint("/personnes/{pers-id}") 
public class PersonneEndpoint { 
  @OnMessage
  public void traiter(@PathParam("pers-id")String id) {
  }
}

L'URL complète pour utiliser la WebSocket sera composée de plusieurs éléments :

  • le hostname et le port du conteneur
  • l'uri de la webapp
  • l'uri de la WebSocket
Exemple :
ws://localhost:8080/MaWebApp/echo

 

76.2.1.2. L'annotation @OnMessage

L'annotation @javax.websocket.OnMessage permet de définir une méthode qui sera invoquée chaque fois qu'un message est reçu pour le endpoint.

Le message peut être de plusieurs types :

  • String
  • byte[]
  • ByteBuffer
  • toute classe pour lequel il existe un décodeur

Lorsque l'endpoint reçoit un message, la méthode annotée avec l'annotation @OnMessage est invoquée. Cette méthode peut avoir en paramètre :

  • le contenu du message
  • un objet de type javax.websocket.Session qui encapsule la session courante
  • zéro ou plusieurs paramètres de type String annotés avec @PathParam qui contiendront les valeurs des paramètres utilisés dans la requête

Une seule méthode d'une classe annotée avec @ServerEndpoint ou @ClientEndpoint peut être annotée avec @OnMessage

Exemple ( code Java 7 ) :
@OnMessage
public void traiterOnMessage(String message) {
   System.out.println("Message recu par WebSocket : "+message);
} 

La méthode annotée peut avoir comme type de retour :

  • void
  • String, byte[], ByteBuffer, toute classe pour lequel il existe un encodeur

Si la méthode annotée avec @OnMessage retourne une valeur alors l'implémentation enverra un message contenant cette valeur au endpoint client.

Exemple ( code Java 7 ) :
  @OnMessage
  public String traiterOnMessage(String message) {
    return message.toUpperCase();
  } 

Il est aussi possible d'envoyer un message en utilisant un objet de type RemoteEndPoint.Basic obtenu en invoquant la méthode getBasicRemote() de la session courante.

Exemple ( code Java 7 ) :
RemoteEndpoint.Basic remoteEndpoint = session.getBasicRemote();
remoteEndpoint.sendText ("contenu du message");

Pour obtenir une instance de la session courante, il faut ajouter un paramètre de type javax.websocket.Session à la méthode. L'implémentation fournira alors en paramètre l'instance de la Session.

 

76.2.1.3. L'annotation @OnOpen

L'annotation @javax.websocket.OnOpen permet de définir une méthode qui sera invoquée lorsque la connexion de la WebSocket est ouverte. Chaque connexion est associée à une session. La méthode annotée ne sera invoquée qu'une seule fois pour une même connexion d'une WebSocket.

Lorsque la connexion de la WebSocket est établie, une instance de type Session est créée et la méthode annotée avec @OnOpen est invoquée. Cette méthode peut avoir optionnellement en paramètre :

  • un objet de type javax.websocket.EndpointConfig qui encapsule les informations utiles lors du handshake
  • un objet de type javax.websocket.Session qui encapsule la session courante
  • zéro ou plusieurs paramètres de type String annoté avec @PathParam qui contiendront les valeurs des paramètres utilisées dans la requête

Une seule méthode d'une classe annotée avec @ServerEndpoint ou @ClientEndpoint peut être annotée avec @OnOpen.

Exemple ( code Java 7 ) :
  private Map<String, Object> userProperties;

  @OnOpen
  public void traiterOnOpen (Session session, EndpointConfig config) {
    System.out.println ("WebSocket ouverte : "+session.getId());
    properties = config.getUserProperties();
  }

 

76.2.1.4. L'annotation @OnClose

L'annotation @javax.websocket.OnClose permet de définir une méthode qui sera invoquée lorsque la connexion de la WebSocket est fermée.

Lorsque la connexion est fermée, la méthode annotée avec @OnClose est invoquée. Cette méthode peut avoir optionnellement en paramètre :

  • un objet de type javax.websocket.Session qui encapsule la session courante. Celle-ci ne pourra plus être utilisée à la fin de l'invocation de la méthode
  • un objet de type javax.websocket.CloseReason qui précise la raison de la fermeture de la WebSocket
  • zéro ou plusieurs paramètres de type String annotés avec @PathParam qui contiendront les valeurs des paramètres utilisés dans la requête

Une seule méthode d'une classe annotée avec @ServerEndpoint ou @ClientEndpoint peut être annotée avec @OnClose.

Exemple ( code Java 7 ) :
  @OnClose
  public void traiterOnClose (CloseReason reason) {
    System.out.println("Fermeture de la WebSocket a cause de : "+reason.getReasonPhrase());
  }

 

76.2.1.5. L'annotation @OnError

Lorsqu'une erreur survient durant la conversation la méthode annotée avec @javax.websocket.OnError est invoquée. Cette méthode doit avoir un objet de type Throwable qui encapsule l'exception en paramètre et peut avoir optionnellement en paramètre :

  • un objet de type javax.websocket.Session qui encapsule la session courante
  • zéro ou plusieurs paramètres de type String annotés avec @PathParam qui contiendront les valeurs des paramètres utilisés dans la requête

Une seule méthode d'une classe annotée avec @ServerEndpoint ou @ClientEndpoint peut être annotée avec @OnError.

Exemple ( code Java 7 ) :
  @OnError
  public void onError(Session session, Throwable t) {
    t.printStackTrace();
  }

 

76.2.1.6. L'annotation @ClientEndpoint

Pour créer un endpoint côté client, il faut créer une classe qui soit annotée avec l'annotation @javax.websocket.ClientEndpoint.

L'annotation @ClientEndpoint possède plusieurs attributs :

Nom

Rôle

decoders

Enregistrer des décodeurs qui permettent de transformer un message texte ou binaire en un objet. La valeur est un tableau de noms de classes qui implémentent l'interface Decoder

encoders

Enregistrer des encodeurs qui permettent de transformer un objet en un message texte ou binaire. La valeur est un tableau de noms de classes qui implémentent l'interface Encoder

subprotocols

Tableau des noms des sous-protocoles supportés par le client

Exemple : soap, wamp (websocket application message processing), ...

Configurator

Préciser la classe de type ClientEndpointConfig.Configurator qui sera utilisée


Exemple ( code Java 7 ) :
@ClientEndpoint (
    decoders = MonDecoder.class,
    encoders = MonEncoder.class,
    subprotocols = {"sousprotocole1", "sousprotocole2"},
    configurator = MonConfigurator.class)
public class MonClientEndpoint {}

Un endpoint client ne peut pas recevoir de requêtes pour créer une connexion.

Il est possible de définir une classe fille de la classe ClientEndpointConfiguration.Configurator qui permet de modifier certaines parties de la requête réponse lors du traitement d'un handshake.

La méthode beforeRequest() permet de modifier les éléments du header avant que la requête ne soit envoyée.

La méthode afterResponse() permet de modifier les traitements de la réponse du handshake.

Exemple ( code Java 7 ) :
public class MonConfigurator {
  public void beforeRequest(Map<String, List<String>> headers) {
  }
    
  public void afterResponse(HandshakeResponse hr) {
    // traitements de la réponse du handshake

  }
}

 

76.2.2. Le développement d'un endpoint sans annotations

Il est possible de développer un endpoint en utilisant l'API WebSocket.

Il faut alors écrire une classe qui héritent de la classe javax.websocket.Endpoint et redéfinir selon les besoins les méthodes onOpen(), onClose() et onError().

La gestion des messages reçus se fait en enregistrant un handler de messages, qui est une instance de type MessageHandler, à la session dans les traitements de la méthode onOpen() : cette enregistrement se fait en invoquant la méthode addMessageHandler() de la session.

L'émission d'un message se fait en utilisant une instance de type RemoteEndpoint.

 

76.2.2.1. La développement d'un endpoint serveur

La classe abstraite javax.websocket.EndPoint encapsule un endpoint d'une WebSocket. Pour développer un endpoint en utilisant l'API, il faut créer une classe fille qui hérite de la classe EndPoint et redéfinir les méthodes utiles pour traiter les événements des conversations (open, error et close).

La classe Endpoint définit possède plusieurs méthodes :

Méthode

Rôle

void onClose(Session session, CloseReason closeReason)

Callback lors de la fermeture de la WebSocket

void onError(Session session, Throwable t)

Callback lorsqu'une erreur survient

abstract void onOpen(Session session, EndpointConfig config)

Callback lorsqu'une nouvelle conversation débute


Pour permettre au endpoint de traiter les messages reçus, il faut ajouter une implémentation de type MessageHandler à la session courante. Cette opération doit être faite dans la redéfinition de la méthode onOpen() du endpoint.

Exemple ( code Java 7 ) :
package fr.jmdoudoux.dej.websockets.server;

import java.io.IOException;
import java.util.Date;
import java.util.logging.Logger;
import javax.websocket.CloseReason;
import javax.websocket.EndpointConfig;
import javax.websocket.MessageHandler;
import javax.websocket.RemoteEndpoint;
import javax.websocket.Session;

public class MonEchoEndpoint extends javax.websocket.Endpoint  {

    private static final Logger LOGGER = Logger.getLogger(MonEchoEndpoint.class.getName());
    
    public MonEchoEndpoint() {
        super();
        LOGGER.info("invocation constructeur MonEchoEndPoint");
    }
    
    @Override
    public void onOpen(Session session, EndpointConfig config) {
        final RemoteEndpoint.Basic remote = session.getBasicRemote();
        session.addMessageHandler(new MessageHandler.Whole<String>() {
            @Override
            public void onMessage(String text) {
                try {
                    remote.sendText(ThreadSafeFormatter.getDateFormatter().format(new Date())
+ " (MonEchoEndPoint) " + text);
                } catch (IOException ioe) {
                    LOGGER.severe("Could not send the message", ioe);
                }
            }
        });
    }

    @Override
    public void onClose(Session session, CloseReason closeReason) {
        LOGGER.info("onClose : "+closeReason);
    }

    @Override
    public void onError(Session session, Throwable throwable) {
        LOGGER.severe("onError", throwable);
    }    
}

Par défaut, le Configurator associé au ServerEndPointConfig assure qu'une instance de type EndPoint ne sera invoquée que par un seul thread pour une même connexion.

 

76.2.2.2. Le développement d'un endpoint client

Pour qu'un client se connecte à un serveur, il faut obtenir une instance de type javax.websocket.WebSocketContainer en invoquant la méthode getWebSocketContainer() de la classe javax.websocket.ContainerProvider.

Il faut invoquer la méthode connectToServer() de l'instance de type WebSocketContainer qui attend en paramètre :

  • la classe du endpoint client
  • l'URI de la websocket
Exemple ( code Java 7 ) :
package fr.jmdoudoux.dej.websocket.client;

import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.websocket.ClientEndpoint;
import javax.websocket.DeploymentException;
import javax.websocket.OnMessage;
import javax.websocket.Session;

@ClientEndpoint
public class TestClientWebSocket {
    private static final Logger LOGGER = Logger.getLogger(TestClientWebSocket.class.getName());
    
    @OnMessage
    public void onMessage(String message, Session session) {
       LOGGER.log(Level.INFO, message);
    }
    
    public static void main(String[] args) {
        LOGGER.log(Level.INFO, "Lancement client");
        javax.websocket.WebSocketContainer container =
            javax.websocket.ContainerProvider.getWebSocketContainer();
        try {
            container.connectToServer(TestClientWebSocket.class,
                new URI("ws://localhost:8080/MaWebApp/valeurs"));
        } catch (DeploymentException | IOException | URISyntaxException ex) {
            LOGGER.log(Level.SEVERE, null, ex);
        }
        
       while(true) {}
    }
}
Résultat :
nov. 15, 2013 9:43:43 PM
fr.jmdoudoux.dej.websocket.client.TestClientWebSocket main
Infos: Lancement client
nov. 15, 2013 9:43:45 PM
fr.jmdoudoux.dej.websocket.client.TestClientWebSocket onMessage
Infos: 103,90
nov. 15, 2013 9:43:46 PM fr.jmdoudoux.dej.websocket.client.TestClientWebSocket
onMessage
Infos: 104,21
nov. 15, 2013 9:43:47 PM
fr.jmdoudoux.dej.websocket.client.TestClientWebSocket onMessage
Infos: 104,56
nov. 15, 2013 9:43:48 PM
fr.jmdoudoux.dej.websocket.client.TestClientWebSocket onMessage
Infos: 104,62

Pour exécuter le code ci-dessus, il faut ajouter plusieurs bibliothèques au classpath notamment l'API Java EE 7 et les bibliothèques requises par l'implémentation WebSocket utilisée.

L'interface javax.websocket.WebSocketContainer définit les fonctionnalités d'une classe qui permet un accès au conteneur qui exécute la websocket.

Elle permet notamment de configurer certains paramètres des endpoints et de se connecter à un serveur de websockets.

Méthode

Rôle

Session connectToServer(Class<?> annotatedEndpointClass, URI path)

Connecter à la websocket définie par l'URI fournie en paramètre la classe annotée qui encapsule l'endpoint

Session connectToServer(Class<? extends Endpoint> endpointClass, ClientEndpointConfig cec, URI path)

Connecter à la websocket définie par l'URI fournie en paramètre la classe qui encapsule l'endpoint en utilisant la configuration fournie

Session connectToServer(Endpoint endpointInstance, ClientEndpointConfig cec, URI path)

Connecter à la websocket définie par l'URI fournie en paramètre la classe qui encapsule l'endpoint en utilisant la configuration fournie

Session connectToServer(Object annotatedEndpointInstance, URI path)

Connecter à la websocket définie par l'URI fournie en paramètre la classe annotée qui encapsule l'endpoint

long getDefaultAsyncSendTimeout()

Définir le nombre de millisecondes que l'implémentation peut attendre lors de l'envoi d'un message pour tous les RemoteEndpoints associés au container

int getDefaultMaxBinaryMessageBufferSize()

Obtenir la taille maximale du buffer que le conteneur peut utiliser pour stocker un message binaire

long getDefaultMaxSessionIdleTimeout()

Obtenir le nombre de millisecondes après lequel une session inactive sera fermée

Int getDefaultMaxTextMessageBufferSize()

Définir la taille maximale du buffer que le conteneur peut utiliser pour stocker un message texte

Set<Extension> getInstalledExtensions()

Renvoyer une collection contenant les extensions installée dans le conteneur

setAsyncSendTimeout(long timeoutmillis)

Définir le nombre de millisecondes que l'implémentation peut attendre lors de l'envoi d'un message pour tous les RemoteEndpoints associés au container

void setDefaultMaxBinaryMessageBufferSize(int max)

Définir la taille maximale du buffer que le conteneur peut utiliser pour stocker un message binaire

void setDefaultMaxSessionIdleTimeout(long timeout)

Définir le nombre de millisecondes après lequel une session inactive sera fermée

void setDefaultMaxTextMessageBufferSize(int max)

Définir la taille maximale du buffer que le conteneur peut utiliser pour stocker un message texte


La classe ContainerProvider permet d'obtenir une instance de type WebSocketContainer en utilisant le ServiceLoader : le type de l'implémentation est précisé dans le fichier META-INF/services/javax.websocket.ContainerProvider contenu dans le jar de l'implémentation de la JSR 356.

Méthode

Rôle

protected abstract WebSocketContainer getContainer()

Charger l'implémentation du conteneur

static WebSocketContainer getWebSocketContainer()

Obtenir une instance de type WebsocketContainer


Exemple :
package fr.jmdoudoux.dej.websocket.client;

import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.websocket.DeploymentException;

public class TestClientWebSocketMain {

    private static final Logger LOGGER = 
        Logger.getLogger(TestClientWebSocketMain.class.getName());

    public static void main(String[] args) {
        LOGGER.log(Level.INFO, "Lancement client");
        try {
            long compteur = 0L;
            final ValeursClientEndpoint clientEndPoint = new ValeursClientEndpoint(
                   new URI("ws://localhost:8080/MaWebApp/monbean"));
            while (true) {
                try {
                    Thread.sleep(5000);
                } catch (InterruptedException ex) {
                }
                compteur++;
                clientEndPoint.sendMessage(""+compteur);
            }
        } catch (DeploymentException | IOException | URISyntaxException ex) {
            LOGGER.log(Level.SEVERE, null, ex);
        }
    }
}
Exemple :
package fr.jmdoudoux.dej.websocket.client;

import java.io.IOException;
import java.io.StringReader;
import java.net.URI;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.json.Json;
import javax.json.JsonObject;
import javax.websocket.ClientEndpoint;
import javax.websocket.CloseReason;
import javax.websocket.ContainerProvider;
import javax.websocket.DeploymentException;
import javax.websocket.OnClose;
import javax.websocket.OnError;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.WebSocketContainer;

@ClientEndpoint
public class ValeursClientEndpoint {

    private static final Logger LOGGER = 
        Logger.getLogger(ValeursClientEndpoint.class.getName());

    Session session = null;

    public ValeursClientEndpoint(URI endpointURI) throws DeploymentException, IOException {
         WebSocketContainer container = ContainerProvider
               .getWebSocketContainer();
        container.connectToServer(this, endpointURI);
    }

    @OnOpen
    public void onOpen(Session session) {
        LOGGER.log(Level.INFO, "Client endpoint open");
        this.session = session;
    }

    @OnClose
    public void onClose(Session session, CloseReason reason) {
        LOGGER.log(Level.INFO, "Client endpoint close");
        this.session = null;
    }
    
    @OnError
    public void onError(Throwable t) {
        LOGGER.log(Level.SEVERE, "Client endpoint error ", t);
    }

    @OnMessage
    public void onMessage(String message, Session session) {
        LOGGER.log(Level.INFO, "message recu="+message);
        JsonObject jsonObject = Json.createReader(new StringReader(message)).readObject();
        String nom = jsonObject.getString("nom");
        String valeur = jsonObject.getString("valeur");
        LOGGER.log(Level.INFO, "nom="+nom+" valeur="+valeur);
    }

    public void sendMessage(String message) {
        if (session != null) {
            this.session.getAsyncRemote().sendText(message);
        }
    }
}
Résultat :
janv. 30, 2014 9:49:27 PM
fr.jmdoudoux.dej.websocket.client.TestClientWebSocketMain main
Infos: Lancement client
janv. 30, 2014 9:49:28 PM
fr.jmdoudoux.dej.websocket.client.ValeursClientEndpoint onOpen
Infos: Client endpoint open
janv. 30, 2014 9:49:33 PM
fr.jmdoudoux.dej.websocket.client.ValeursClientEndpoint onMessage
Infos: message
recu={"nom":"nom1","valeur":"valeur1"}
janv. 30, 2014 9:49:33 PM
fr.jmdoudoux.dej.websocket.client.ValeursClientEndpoint onMessage
Infos: nom=nom1 valeur=valeur1
janv. 30, 2014 9:49:38 PM
fr.jmdoudoux.dej.websocket.client.ValeursClientEndpoint onMessage
Infos: message
recu={"nom":"nom2","valeur":"valeur2"}
janv. 30, 2014 9:49:38 PM
fr.jmdoudoux.dej.websocket.client.ValeursClientEndpoint onMessage
Infos: nom=nom2 valeur=valeur2
janv. 30, 2014 9:49:43 PM
fr.jmdoudoux.dej.websocket.client.ValeursClientEndpoint onMessage
Infos: message
recu={"nom":"nom3","valeur":"valeur3"}
janv. 30, 2014 9:49:43 PM
fr.jmdoudoux.dej.websocket.client.ValeursClientEndpoint onMessage
Infos: nom=nom3 valeur=valeur3

 

76.2.3. La configuration des endpoints sans annotations

Lorsque des endpoints sont développés en utilisant l'API, il est nécessaire d'écrire du code pour permettre de définir leur configuration qui sera utilisée lors de leur enregistrement.

Cette configuration peut impliquer l'utilisation de différentes classes et interfaces selon les besoins notamment des objets de type EndpointConfig.

 

76.2.3.1. L'interface EndpointConfig

L'interface javax.websocket.EndpointConfig définit les fonctionnalités de base pour la configuration d'un endpoint client ou serveur.

Cette interface définit plusieurs méthodes :

Méthode

Rôle

List<Class<? extends Decoder>> getDecoders()

Obtenir la liste des décodeurs associés au endpoint. L'implémentation va créer une instance de chacun des décodeurs lors de l'enregistrement du endpoint

List<Class<? extends Encoder>> getEncoders()

Obtenir la liste des encodeurs associés au endpoint. L'implémentation va créer une instance de chacun des encodeurs lors de l'enregistrement du endpoint

Map<String,Object> getUserProperties()

Obtenir une collection de type Map qui permet de gérer des données relatives au endpoint. Il est préférable que les clés et valeurs contenues dans cette collection soient sérialisables


Elle possède deux interfaces filles :

  • ClientEndpointConfig pour la configuration d'un endpoint client
  • ServerEndpointConfig pour la configuration d'un endpoint serveur

 

76.2.3.2. La configuration des endpoints serveur

Lors de l'écriture de endpoints serveur avec l'API, il est nécessaire d'écrire une classe qui implémente l'interface ServerApplicationConfig. Elle va permettre de fournir les informations de configuration pour chaque endpoint à enregistrer dans le serveur.

 

76.2.3.2.1. L'interface ServerEndpointConfig

L'interface javax.websocket.server.ServerEndpointConfig définit les méthodes d'un objet qui encapsule la configuration d'un endpoint pour son déploiement dans un serveur.

Elle définit plusieurs méthodes :

Méthode

Rôle

ServerEndpointConfig.Configurator getConfigurator()

Renvoyer l'instance de type ServerEndpointConfig.Configurator utilisée par cette configuration

Class<?> getEndpointClass()

Renvoyer le type de classe du endpoint

List<Extension> getExtensions()

Renvoyer la liste des extensions

String getPath()

Renvoyer l'uri associée au endpoint

List<String> getSubprotocols()

Renvoyer la liste des sous-protocoles


Pour faciliter la création d'une instance de type ServerEndpointConfig, il faut utiliser la classe ServerEndpointConfig.Builder.

Pour permettre de personnaliser certaines opérations notamment le handshake, il est possible d'utiliser une instance de type ServerEndpointConfig.Configurator.

 

76.2.3.2.2. La classe ServerEndpointConfig.Builder

La classe javax.websocket.server.ServerEndpointConfig.Builder permet de créer une instance de type ServerEndpointConfig en utilisant le motif de conception builder.

Elle propose donc plusieurs méthodes qui permettent de fournir les différents éléments qui seront encapsulés dans l'instance, une méthode pour renvoyer l'instance du Builder et une pour obtenir l'instance construite :

Méthode

Rôle

ServerEndpointConfig build()

Créer et renvoyer l'instance de type ServerEndpointConfigBuilds encapsulant les attributs fournis au builder

ServerEndpointConfig.builder configurator(ServerEndpointConfig.Configurator serverEndpointConfigurator)

Fournir à la configuration le Configurator qui sera utilisé

static ServerEndpointConfig.builder create(Class<?> endpointClass, String path)

Créer et retourner une instance de type Builder en lui passant en paramètres les informations obligatoires (la classe du endpoint et l'url relative associée au endpoint)

ServerEndpointConfig.builder decoders(List<Class<? extends Decoder>> decoders)

Fournir à la configuration les décodeurs qui seront utilisés

ServerEndpointConfig.builder encoders(List<Class<? extends Encoder>> encoders)

Fournir à la configuration les encodeurs qui seront utilisés

ServerEndpointConfig.builder extensions(List<Extension> extensions)

Fournir à la configuration les extensions qui seront utilisées

ServerEndpointConfig.Builder subprotocols(List<String> subprotocols)

Fournir à la configuration les sous-protocoles qui seront utilisés


Exemple ( code Java 7 ) :
ServerEndpointConfig config =
ServerEndpointConfig.Builder.create(MonEndpoint.class,"/monendpoint").
    decoders(Arrays.<Class<? extends Decoder>>asList(MonDecoder.class)).
    encoders(Arrays.<Class< extends Encoder>>asList(MonEncoder.class)).build();

 

76.2.3.2.3. La classe ServerEndpointConfig.Configurator

La classe javax.websocket.server.ServerEndpointConfig.Configurator permet de personnaliser certaines opérations lors de demandes de connexion par des clients.

Elle possède plusieurs méthodes :

Méthode

Rôle

boolean checkOrigin(String originHeaderValue)

Vérifier la valeur de l'attribut Origin Header fournie par le client lors d'une demande de connexion. La valeur booléenne renvoyée indique le succès de la vérification

<T> T getEndpointInstance(Class<T> endpointClass)

Méthode invoquée par le conteneur pour obtenir une instance du endpoint. L'implémentation par défaut renvoie une nouvelle instance à chaque invocation.

List<Extension> getNegotiatedExtensions(List<Extension> installed, List<Extension> requested)

Renvoyer une collection des extensions supportées par le serveur par rapport à celles fournies en paramètres. La liste doit être vide si aucune des extensions n'est supportée

String getNegotiatedSubprotocol(List<String> supported, List<String> requested)

Renvoyer le sous-protocole sélectionné par le serveur parmi ceux fournis en paramètres : ils correspondent à ceux supportés par un client qui se connecte. Renvoi une chaîne vide si aucun n'est supporté

void modifyHandshake(ServerEndpointConfig sec, HandshakeRequest request, HandshakeResponse response)

Callback invoqué par le serveur permettant de personnaliser la réponse du handshake

 

76.2.3.2.4. L'interface ServerApplicationConfig

L'interface javax.websocket.server.ServerApplicationConfig permet d'encapsuler la configuration des endpoints à enregistrer dans un conteneur.

Elle définit plusieurs méthodes :

Méthode

Rôle

Set<ServerEndpointConfig> getEndpointConfigs(Set<Class<? extends Endpoint>> endpointClasses)

Renvoyer une collection de type ServerEndpointConfig contenant la configuration des endpoints développés avec l'API

Set<Class<?>> getAnnotatedEndpointClasses(Set<Class<?>> scanned)

Renvoyer une collection des endpoints utilisant des annotations devant être enregistrés dans le serveur


Lors de développement de endpoints sans utiliser les annotations, il faut écrire une classe qui implémente l'interface ServerApplicationConfig pour définir la configuration des endpoints.

L'exemple ci-dessous configure un seul endpoint, défini programmatiquement, associé à l'uri /monecho.

Exemple ( code Java 7 ) :
package fr.jmdoudoux.dej.websockets.server;

import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import java.util.logging.Logger;
import javax.websocket.Endpoint;
import javax.websocket.server.ServerApplicationConfig;
import javax.websocket.server.ServerEndpointConfig;

public class MonServerApplicationConfig implements ServerApplicationConfig {

  private static final Logger LOGGER = 
    Logger.getLogger(MonServerApplicationConfig.class.getName());

  @Override
  public Set<ServerEndpointConfig> getEndpointConfigs(
    Set<Class<? extends Endpoint>> endpointClasses) {        
    return new HashSet<ServerEndpointConfig>(Arrays.asList(
      ServerEndpointConfig.Builder.create(MonEchoEndpoint.class, "/monecho").build()));
  }

  @Override
  public Set<Class<?>> getAnnotatedEndpointClasses(Set<Class<?>> scanned) {
    return Collections.emptySet();
  }
}

La manière dont cette classe sera passée au serveur est dépendante de l'implémentation du serveur car cette fonctionnalité n'est pas spécifiée par l'API.

 

76.2.3.3. La configuration d'un endpoint client

L'interface javax.websocket.ClientEndpointConfig définit les fonctionnalités pour encapsuler la configuration d'un endpoint client. Elle hérite de l'interface Endpoint

Une instance de type ClientEndpointConfig est utilisée par un WebSocketContainer pour enregistrer un endpoint client.

Elle définit plusieurs méthodes :

Méthode

Rôle

ClientEndpointConfig.Configurator getConfigurator()

Renvoyer l'instance de type ClientEndpointConfig.Configurator utilisée par cette configuration

List<Extension> getExtensions()

Renvoyer la liste des extensions

List<String> getSubprotocols()

Renvoyer la liste des sous-protocoles


Pour faciliter la création d'une instance de type ClientEndpointConfig, il faut utiliser la classe ClientEndpointConfig.Builder.

Pour permettre de personnaliser certaines opérations notamment le handshake, il est possible d'utiliser une instance de type ClientEndpointConfig.Configurator.

 

76.2.3.3.1. La classe ClientEndpointConfig.Builder

La classe javax.websocket.ClientEndpointConfig.Builder permet de créer une instance de type ClientEndpointConfig en utilisant le motif de conception builder.

Elle propose donc plusieurs méthodes qui permettent de fournir les différents éléments qui seront encapsulés dans l'instance, une méthode pour renvoyer l'instance du builder et une pour obtenir l'instance construite :

Méthode

Rôle

ClientEndpointConfig build()

Créer et renvoyer l'instance de type ClientEndpointConfigBuilds encapsulant les attributs fournis au builder

ClientEndpointConfig.builder configurator(ClientEndpointConfig.Configurator clientEndpointConfigurator)

Fournir à la configuration le Configurator qui sera utilisé

static ClientEndpointConfig.builder create()

Créer et retourner une instance de type Builder

ClientEndpointConfig.builder decoders(List<Class<? extends Decoder>> decoders)

Fournir à la configuration les décodeurs qui seront utilisés

ClientEndpointConfig.builder encoders(List<Class<? extends Encoder>> encoders)

Fournir à la configuration les encodeurs qui seront utilisés

ClientEndpointConfig.builder extensions(List<Extension> extensions)

Fournir à la configuration les extensions qui seront utilisées

ServerEndpointConfig.Builder preferredSubprotocols(List<String> preferedSubprotocols)

Fournir à la configuration les sous-protocoles préférés qui seront utilisés


Exemple ( code Java 7 ) :
  ClientEndpointConfig maConfig = ClientEndpointConfig.Builder.create()
    .encoders(Arrays.<Class<? extends Encoder>>asList(MonBeanEncoder.class))
    .decoders(Arrays.<Class<? extends Decoder>>asList(MonBeanDecoder.class))
    .preferredSubprotocols(Arrays.asList("sub1", "sub2")).build();

 

76.3. Les encodeurs et les décodeurs

Les messages échangés entre deux endpoints peuvent être du texte ou des données binaires. Les Encoders et les Decoders permettent respectivement de transformer un objet Java en un format sérialisé utilisable dans un message et vice versa.

Un objet Java quelconque peut être encodé sous une forme textuelle ou binaire pour être envoyé au endpoint à l'extrémité de la conversation. Dans ce cas, l'endpoint qui reçoit le message doit posséder un décodeur capable à partir du message de recréer l'objet correspondant. Généralement se sont des formats standards comme XML ou Json qui sont utilisés, ce qui évite d'avoir à réinventer son propre format.

L'API permet d'utiliser trois types de messages :

  • texte
  • binaire
  • pong qui est utilisé par le protocole pour gérer la connexion

Lors du développement d'un endpoint en utilisant les annotations, une méthode permettant de gérer un message pour chacun des trois types peut être annotée avec @OnMessage.

Chaque type est associé à plusieurs types Java correspondant selon les besoins.

Pour les messages de type texte :

  • String : pour recevoir l'intégralité du message
  • String et un booléen : pour recevoir des morceaux de texte
  • un type primitif ou son wrapper correspondant pour recevoir le résultat de la conversion du message dans ce type
  • Reader : pour pouvoir traiter le contenu du message sous la forme d'un flux
  • une classe quelconque pour laquelle un Decoder pour données textuelles (Decoder.Text ou Decoder.TextStream) est enregistré

Pour les messages de type binaire :

  • byte[] ou ByteBuffer : pour recevoir l'intégralité du message
  • byte[] ou ByteBuffer et un booléen : pour recevoir des morceaux de message
  • InputStream : pour pouvoir traiter le contenu du message sous la forme d'un flux
  • une classe quelconque pour laquelle un Decoder pour des données binaires (Decoder.Binary ou Decoder.BinaryStream) est enregistré

Pour les messages de type Pong :

  • PongMessage

Lors du développement d'un endpoint en utilisant l'API, un seul MessageHandler peut être enregistré pour chacun des types.

 

76.3.1. Les encodeurs

Un encodeur permet de transformer un objet Java dans un format texte ou binaire qui pourra être envoyé comme réponse dans un message.

Un encodeur doit implémenter l'interface javax.websocket.Encoder qui définit plusieurs méthodes.

Méthode

Rôle

void destroy()

Cette méthode est invoquée lorsque l'encodeur va être retiré : elle doit permettre de libérer des ressources et de terminer l'encodeur de manière propre

void init(EndpointConfig config)

Cette méthode est invoquée lorsque l'encodeur est associé au endpoint : elle doit permettre d'initialiser l'encodeur


L'interface Encoder possède plusieurs sous-interfaces :

  • Encoder.Text pour convertir des objets Java en texte
  • Encoder.TextStream pour convertir des objets Java en flux de texte
  • Encoder.Binary pour convertir des objets Java en binaire
  • Encoder.BinaryStream pour convertir des objets Java en flux binaire

Ces interfaces définissent chacune une méthode encode() avec différentes valeurs de retour et paramètres.

L'exemple ci-dessous va écrire un encodeur pour un bean.

Exemple ( code Java 7 ) :
package fr.jmdoudoux.dej.websockets;

public class MonBean {

    private String nom;
    private String valeur;

    public MonBean() {
    }

    public MonBean(String nom, String valeur) {
        this.nom = nom;
        this.valeur = valeur;
    }

    public String getNom() {
        return nom;
    }

    public void setNom(String nom) {
        this.nom = nom;
    }

    public String getValeur() {
        return valeur;
    }

    public void setValeur(String valeur) {
        this.valeur = valeur;
    }    
}

Les traitements sont réalisés en redéfinissant la méthode encode().

Exemple ( code Java 7 ) :
package fr.jmdoudoux.dej.websockets;

import java.io.StringWriter;
import javax.json.Json;
import javax.json.stream.JsonGenerator;
import javax.websocket.EncodeException;
import javax.websocket.Encoder;
import javax.websocket.EndpointConfig;

public class MonBeanEncoder implements Encoder.Text<MonBean> {
    @Override
    public String encode(MonBean monBean) throws EncodeException {
        StringWriter writer = new StringWriter();
        JsonGenerator generator = Json.createGenerator(writer);
        generator.writeStartObject()
            .write("nom", monBean.getNom())
            .write("valeur", monBean.getValeur())
            .writeEnd();
        generator.close();
        return writer.toString();
    }

    @Override public void init(EndpointConfig config) {
    }

    @Override public void destroy() {
    }
}

Dans l'exemple ci-dessus, l'objet est sérialisé en un message de type texte utilisant le format JSON.

 

76.3.2. Les décodeurs

Un décodeur permet de transformer le contenu texte ou binaire d'un message en un objet ou un graphe d'objets.

Un décodeur est une classe qui doit implémenter l'interface javax.websocket.Decoder qui définit plusieurs méthodes :

Méthode

Rôle

void destroy()

Cette méthode est invoquée lorsque le décodeur va être retiré : elle doit permettre de libérer des ressources et de terminer le décodeur de manière propre

void init(EndpointConfig config)

Cette méthode est invoquée lorsque le décodeur est associé au endpoint : elle doit permettre d'initialiser le décodeur


L'interface Decoder possède plusieurs sous interfaces :

  • Decoder.Text pour convertir du texte en objets Java
  • Decoder.TextStream pour convertir un flux de texte en objets Java
  • Decoder.Binary pour convertir des données binaires en objets Java
  • Decoder.BinaryStream pour convertir un flux binaire en objets Java

Ces interfaces définissent chacune une méthode decode() avec différentes valeurs de retour et paramètres.

L'exemple ci-dessous va écrire un décodeur pour un bean.

Exemple ( code Java 7 ) :
package fr.jmdoudoux.dej.websockets;

import java.io.StringReader;
import javax.json.Json;
import javax.json.JsonObject;
import javax.websocket.DecodeException;
import javax.websocket.Decoder;
import javax.websocket.EndpointConfig;

public class MonBeanDecoder implements Decoder.Text<MonBean> {

    @Override
    public MonBean decode(String message) throws DecodeException {
        MonBean resultat = null;
        JsonObject jsonObject = Json.createReader(new StringReader(message)).readObject();
        String nom = jsonObject.getJsonString("nom").getString();
        String valeur = jsonObject.getJsonString("valeur").getString();
        resultat = new MonBean(nom, valeur);
        return resultat;
    }

    @Override
    public boolean willDecode(String message) {
        return message.startsWith("{");
    }

    @Override
    public void init(EndpointConfig config) {
    }

    @Override
    public void destroy() {
    }
}

 

76.3.3. L'enregistrement des encodeurs et des décodeurs

Un endpoint doit connaître l'ensemble des encodeurs/décodeurs qu'il peut utiliser.

En utilisant les annotations, il faut utiliser les attributs encoders et decoders des annotations @ClientEndpoint et @ServerEndpoint.

Exemple ( code Java 7 ) :
@ServerEndpoint(value="/monendpoint", encoders
  = MonMessageEncoder.class, decoders= MonMessageDecoder.class)
public class MonEndpoint {
  // ...

}

En utilisant l'API, les encodeurs et décodeurs sont obtenus en invoquant les méthodes getEncoders() et getDecoders() de l'interface EndpointConfig.

Les méthodes encoders() et decoders() des classes ClientEndpointConfig.Builder et ServerEndpointConfig.Builder permettent de fournir les encodeurs et décodeurs à l'instance de type EndpointConfig qui sera créée.

 

76.4. Le débogage des WebSockets

Pour visualiser les requêtes échangées avec le protocole WebSocket, il est possible d'utiliser l'application WireShark.

A partir de la version 20 de Chrome, l'outil Chrome Dev Tools permet de visualiser l'activité d'une WebSocket. Pour l'activer, il suffit d'utiliser l'option « Outils de développement » du menu « Outils »

Il faut sélectionner la requête qui correspond à la WebSocket.

L'onglet « Header » permet de voir la requête et la réponse du handshake

L'onglet « Frames » permet de visualiser les messages échangés.

L'ouverture de l'url « chrome://net-internals/ » dans un nouvel onglet permet d'obtenir des informations de bas niveau sur les échanges réseaux.

Netbeans, à partir de sa version 7.4, propose la fonctionnalité Network Monitor qui utilise un plug-in Chrome pour afficher les échanges des conversations (handshake HTTP dans l'onglet Headers et messages échangés dans l'onglet Frames).

Cette fonctionnalité requiert l'utilisation de Chrome comme navigateur et l'installation du plugin correspondant.

 

76.5. Des exemples d'utilisation

Cette section va proposer plusieurs exemples de mise en oeuvre des WebSockets.

 

76.5.1. Un premier cas simple

Ce premier cas n'exploite pas toutes les possibilités des Websockets mais il permet de mettre en place un exemple simple. Il va simplement renvoyer la chaîne de caractères reçue en la faisant précéder de la date/heure.

La partie serveur est une webapp qui contient un POJO annoté

Exemple ( code Java 7 ) :
package fr.jmdoudoux.dej.websockets;

import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import javax.websocket.OnMessage;
import javax.websocket.server.ServerEndpoint;

@ServerEndpoint("/echo")
public class EchoEndPoint {
    @OnMessage
    public String echo(String message) {
        return ThreadSafeFormatter.getDateFormatter().format(new Date()) + " "
            +  message;
    }
}

class ThreadSafeFormatter {
    private static final ThreadLocal<SimpleDateFormat> formatter = 
        new ThreadLocal<SimpleDateFormat>() {
        @Override
        protected SimpleDateFormat initialValue() {
            return new SimpleDateFormat("dd-MM-yyyy HH:mm:ss");
        }
    };

    public static DateFormat getDateFormatter() {
        return formatter.get();
    }
}

La partie cliente est une page HTML 5 qui utilise l'API Javascript Websocket.

Exemple :
<!DOCTYPE html>
<html>
    <head>
        <title>Test WebSockets</title>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width">
        <script language="javascript" type="text/javascript">
            var wsUri = getRootUri() + "/EchoWebApp/echo";
            function getRootUri() {
                return "ws://" + (document.location.hostname == "" ?
                    "localhost" : document.location.hostname) + ":" +
                    (document.location.port == "" ? "8080" : document.location.port);
            }

            function init() {
                messageDiv = document.getElementById("messageDivId");
                websocket = new WebSocket(wsUri);
                websocket.onopen = function(evt) {
                    onOpen(evt)
                };
                websocket.onmessage = function(evt) {
                    onMessage(evt)
                };
                websocket.onerror = function(evt) {
                    onError(evt)
                };
            }

            function onOpen(evt) {
                afficher("CONNECTE");
            }

            function onMessage(evt) {
                afficher("RECU : " + evt.data);
            }

            function onError(evt) {
                afficher('<span style="color: red;">ERREUR:</span> ' + evt.data);
            }

            function envoyer() {
                var message = textId.value;
                afficher("ENVOYE : " + message);
                websocket.send(message);
            }

            function afficher(message) {
                var ligne = document.createElement("p");
                ligne.innerHTML = message;
                messageDiv.appendChild(ligne);
            }
            
            window.addEventListener("load", init, false);
</script>
    </head>
    <body>
        <h2 style="text-align: center;">Client WebSocket Echo</h2>
        <div style="text-align: center;">
            <form action="">
                <input id="textId" name="message" value="" type="text">&nbsp;
                <input onclick="envoyer()" value="Envoyer" type="button">
            </form>
        </div>
        <div id="messageDivId"></div>
    </body>
</html>

 

76.5.2. La mise à jour périodique d'un graphique

Un POJO est défini comme étant l'endpoint d'une WebSocket. Elle conserve une collection des sessions ouvertes sur l'endpoint. La classe contient une méthode statique send() qui envoie la valeur reçue en paramètre à tous les clients connectés à la WebSocket.

Exemple ( code Java 7 ) :
package fr.jmdoudoux.dej.websockets;

import java.io.IOException;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.websocket.OnClose;
import javax.websocket.OnError;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.ServerEndpoint;

@ServerEndpoint("/valeurs")
public class ValeursEndPoint {
    private static final Logger logger = Logger.getLogger(ValeursEndPoint.class.getName());
    static Queue<Session> queue = new ConcurrentLinkedQueue<>();

    public static void send(double valeur) {
        String message = String.format("%.2f", valeur);
        try {
            for (Session session : queue) {
                session.getBasicRemote().sendText(message);
                logger.log(Level.INFO, "Send: {0} ", message + " to " + session.getId());
            }
        } catch (IOException e) {
            logger.log(Level.WARNING, e.toString());
        }
    }

    @OnOpen
    public void open(Session session) {
        queue.add(session);
    }

    @OnClose
    public void close(Session session) {
        queue.remove(session);
    }

    @OnError
    public void error(Session session, Throwable t) {
        queue.remove(session);
    }
}

Un EJB Timer est utilisé pour déterminer une nouvelle valeur aléatoirement toutes les secondes et les envoyer par WebSocket aux différents clients connectés.

Exemple ( code Java 7 ) :
package fr.jmdoudoux.dej.websockets;

import java.util.Random;
import java.util.logging.Logger;
import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import javax.ejb.LocalBean;
import javax.ejb.Schedule;
import javax.ejb.Singleton;
import javax.ejb.Startup;
import javax.ejb.Stateless;
import javax.ejb.TimerConfig;
import javax.ejb.TimerService;

@Singleton
@Startup
@LocalBean
public class ValeursBean {
    private static final Logger logger = Logger.getLogger(ValeursBean.class.getName());
    private Random random;
    private volatile double valeur = 100.0;

    @PostConstruct
    public void init() {
        random = new Random();
    }
    
    @Schedule(second="*/1", minute="*", hour="*", persistent = false)
    public void timeoutx() {
        valeur += 1.0 * (random.nextInt(100) -50) / 100.0;
        logger.info("nouvelle valeur "+valeur);
        ValeursEndPoint.send(valeur);
    }
}

La page web utilise la bibliothèque Javascript Smoothie pour afficher les données reçues sous la forme d'un graphique. Les données sont reçues par la WebSocket.

Exemple :
<!DOCTYPE html>
<html>
    <head>
        <title>Test Websockets pour alimenter un graphique</title>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width">
        <script type="text/javascript" src="smoothie.js"></script>
        <script language="javascript" type="text/javascript">
            var wsUri = getRootUri() + "/MaWebApp/valeurs";
            var line1 = new TimeSeries();
            function getRootUri() {
                return "ws://" + (document.location.hostname == "" ?
                    "localhost" : document.location.hostname) + ":" +
                    (document.location.port == "" ? "8080" : document.location.port);
            }

            function init() {
                messageDiv = document.getElementById("messageDivId");
                websocket = new WebSocket(wsUri);
                websocket.onopen = function(evt) {
                    onOpen(evt)
                };
               
                websocket.onmessage = function(evt) {
                    onMessage(evt)
                };
               
                websocket.onerror = function(evt) {
                    onError(evt)
                };
                var smoothie = new SmoothieChart();
                smoothie.streamTo(document.getElementById("graphCanvas"));
                smoothie.addTimeSeries(line1);
            }
            function onOpen(evt) {
                afficher("CONNECTE");
            }
            function onMessage(evt) {
                afficher("RECU : " + evt.data);
                line1.append(new Date().getTime(), parseFloat(evt.data.replace(',', '.'))); 
            }
            function onError(evt) {
                afficher('<span style="color: red;">ERREUR:</span> ' + evt.data);
            }
            function afficher(message) {
                var ligne = document.createElement("p");
                ligne.innerHTML = message;
                messageDiv.innerHTML = ligne.innerHTML ;
            }
            window.addEventListener("load", init, false);
        </script>
    </head>
    <body>
        <div>Evolution de la valeur</div>
        <canvas id="graphCanvas" width="400" height="100"></canvas>
        <div id="messageDivId"></div>
    </body>
</html>

 

76.6. L'utilisation d'implémentations

Une JSR ne définit que des spécifications : il est nécessaire d'utiliser une implémentation de ces spécifications. Cette implémentation peut être :

  • l'implémentation de référence
  • l'implémentation fournie par l'environnement (un serveur Java EE par exemple)
  • une implémentation proposée par un tiers

 

76.6.1. L'utilisation de Tyrus

Tyrus est l'implémentation de référence de la JSR 356. Elle propose une API dédiée qui permet de lancer un serveur HTTP avec un conteneur pour les websockets qui peut s'exécuter dans une application standalone Java SE.

 

76.6.1.1. L'utilisation de Tyrus côté client

Le développement d'un endpoint côté client avec Tyrus se fait en utilisant l'API de la JSR 356.

Exemple ( code Java 7 ) :
package fr.jmdoudoux.dej.websockets.client;

import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.websocket.ClientEndpoint;
import javax.websocket.DeploymentException;
import javax.websocket.OnMessage;
import javax.websocket.Session;

@ClientEndpoint
public class TestClientWebSocketTyrus {
  private static final Logger LOGGER = 
    Logger.getLogger(TestClientWebSocketTyrus.class.getName());
    
  @OnMessage
  public void onMessage(String message, Session session) {
    LOGGER.log(Level.INFO, message);
  }
    
  public static void main(String[] args) {
    LOGGER.log(Level.INFO, "Lancement client ba");
    javax.websocket.WebSocketContainer container
       = javax.websocket.ContainerProvider.getWebSocketContainer();
    try {
      Session session = container.connectToServer(TestClientWebSocketTyrus.class,
        URI.create("ws://localhost:8098/websockets/monecho"));
      while (true) {
        try {
          Thread.sleep(5000);
        } catch (InterruptedException ex) {
          LOGGER.log(Level.SEVERE, null, ex);
        }
        session.getBasicRemote().sendText("hello");
      }
    } catch (DeploymentException | IOException ex) {
      LOGGER.log(Level.SEVERE, "Impossible de se connecter au serveur", ex);
    }
  }
}

Dans l'exemple ci-dessus, la classe principale est aussi l'endpoint client. Elle se connecte à un endpoint serveur et lui envoie toutes les cinq secondes le message « hello ». L' endpoint client qu'elle implémente affiche simplement la réponse du serveur.

Pour exécuter un client WebSocket, il faut ajouter au classpath plusieurs bibliothèques : tyrus-client.jar, tyrus-server.jar, tyrus-core.jar, tyrus-websocket-core.jar, tyrus-spi.jar, tyrus-container-servlet.jar et tyrus-container-grizzly.jar

Ceci peut être fait en ajoutant plusieurs dépendances si l'application est un projet Maven : javax.websocket:javax.websocket-api:1.0, org.glassfish.tyrus:tyrus-server:1.1 (l'implémentation de la JSR 356), org.glassfish.tyrus:tyrus-client:1.1 (l'implémentation de la partie cliente) et org.glassfish.tyrus:tyrus-container-grizzly:1.1 (l'implémentation standalone du conteneur)

Exemple :
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>fr.jmdoudoux.dej.websockets</groupId>
  <artifactId>TestClientTyrus</artifactId>
  <version>1.0-SNAPSHOT</version>
  <packaging>jar</packaging>
  <name>TestClientTyrus</name>
  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  </properties>
  <dependencies>
    <dependency>
      <groupId>javax.websocket</groupId>
      <artifactId>javax.websocket-api</artifactId>
      <version>1.0</version>
    </dependency>
    <dependency>
      <groupId>org.glassfish.tyrus</groupId>
      <artifactId>tyrus-server</artifactId>
      <version>1.1</version>
    </dependency>
    <dependency>
      <groupId>org.glassfish.tyrus</groupId>
      <artifactId>tyrus-client</artifactId>
      <version>1.1</version>
    </dependency>
    <dependency>
      <groupId>org.glassfish.tyrus</groupId>
      <artifactId>tyrus-container-grizzly</artifactId>
      <version>1.1</version>
    </dependency>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>3.8.1</version>
    <scope>test</scope>
    </dependency>
  </dependencies>
  <build>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <version>3.1</version>
        <configuration>
          <compilerVersion>1.7</compilerVersion>
          <source>1.7</source>
          <target>1.7</target>
        </configuration>
      </plugin>
    </plugins>
  </build>  
</project>
Résultat :
janv. 06, 2014 09:46:52 PM
fr.jmdoudoux.dej.websockets.client.TestClientWebSocketTyrus main
Infos: Lancement client
janv. 06, 2014 09:46:58 PM
fr.jmdoudoux.dej.websockets.client.TestClientWebSocketTyrus onMessage
Infos: 06-01-2014 09:46:58 (MonEchoEndPoint) hello
janv. 06, 2014 09:47:03 PM
fr.jmdoudoux.dej.websockets.client.TestClientWebSocketTyrus onMessage
Infos: 06-01-2014 09:47:03 (MonEchoEndPoint) hello
janv. 06, 2014 09:47:08 PM
fr.jmdoudoux.dej.websockets.client.TestClientWebSocketTyrus onMessage
Infos: 06-01-2014 09:47:08 (MonEchoEndPoint) hello

 

76.6.1.2. L'utilisation de Tyrus côté serveur

Tyrus propose une API qui permet de démarrer un conteneur qui sera la partie serveur dans une application standalone.

La classe org.glassfish.tyrus.server.Server est une implémentation d'un serveur de WebSockets.

Plusieurs surcharges du constructeur permettent de préciser un ou plusieurs endpoints à enregistrer dans le serveur. Une première surcharge attend en paramètre la classe d'un endpoint défini en utilisant l'annotation @ServerEndpoint.

Exemple ( code Java 7 ) :
package fr.jmdoudoux.dej.websockets.server;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.HashSet;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.websocket.DeploymentException;
import org.glassfish.tyrus.server.Server;

public class TestServerWebSocketTyrus {
    private static final Logger LOGGER =
    Logger.getLogger(TestServerWebSocketTyrus.class.getName());

    public static void main(String[] args) {
        Server server = new Server("localhost", 8098, "/websockets", EchoEndpoint.class);
        try {
            LOGGER.log(Level.INFO, "Lancement du serveur");
            server.start();
            BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
            System.out.print("Appuyer sur Entree pour arreter le serveur.");
            reader.readLine();
        } catch (IOException | DeploymentException e) { 
            throw new RuntimeException(e);
        } finally {
            LOGGER.log(Level.INFO, "Arret du serveur");
            server.stop();
        }
    }
}

Il est possible de passer en troisième paramètres, une classe de type ServerApplicationConfig

Exemple ( code Java 7 ) :
    Server server = new Server("localhost", 8098, "/websockets",
      MonServerApplicationConfig.class); 

La troisième surcharge permet de fournir une collection de type Set contenant les endpoints définis avec l'annotation @ServerEndpoint à enregistrer dans le serveur.

Pour compiler et exécuter cette application, plusieurs dépendances sont requises :

Exemple :
<projectxmlns="http://maven.apache.org/POM/4.0.0"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
  http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>fr.jmdoudoux.dej.websockets</groupId>
  <artifactId>TestServerWebSocketTyrus</artifactId>
  <version>1.0-SNAPSHOT</version>
  <packaging>jar</packaging>
  <name>TestServerWebSocketTyrus</name>
  <url>http://maven.apache.org</url>
    <properties>
      <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>
    <dependencies>
      <dependency>
        <groupId>javax.websocket</groupId>
        <artifactId>javax.websocket-api</artifactId>
        <version>1.0</version>
      </dependency>
      <dependency>
        <groupId>org.glassfish.tyrus</groupId>
        <artifactId>tyrus-server</artifactId>
        <version>1.1</version>
      </dependency>
      <dependency>
        <groupId>org.glassfish.tyrus</groupId>
        <artifactId>tyrus-client</artifactId>
        <version>1.1</version>
      </dependency>
      <dependency>
        <groupId>org.glassfish.tyrus</groupId>
        <artifactId>tyrus-container-grizzly</artifactId>
        <version>1.1</version>
      </dependency>
      <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>3.8.1</version>
        <scope>test</scope>
      </dependency>
    </dependencies>
  
    <build>
      <plugins>
        <plugin>
          <groupId>org.apache.maven.plugins</groupId>
          <artifactId>maven-compiler-plugin</artifactId>
            <version>3.1</version>
            <configuration>
              <compilerVersion>1.7</compilerVersion>
              <source>1.7</source>
              <target>1.7</target>
            </configuration>
          </plugin>
        </plugins>
    </build>
</project>

L'exécution lance un serveur dédié directement dans l'application standalone.

Résultat :
janv. 06, 2014 09:40:38 PM fr.jmdoudoux.dej.websockets.server.TestServerWebSocketTyrus
main
Infos: Lancement du serveur
janv. 06, 2014 09:40:38 PM
org.glassfish.tyrus.server.ServerContainerFactory create
Infos: Provider class loaded:
org.glassfish.tyrus.container.grizzly.GrizzlyEngine
janv. 06, 2014 09:40:39 PM
org.glassfish.grizzly.http.server.NetworkListener start
Infos: Started listener bound to [0.0.0.0:8098]
janv. 06, 2014 09:40:39 PM
org.glassfish.grizzly.http.server.HttpServer start
Infos: [HttpServer] Started.
janv. 06, 2014 09:40:39 PM org.glassfish.tyrus.server.Server start
Infos: WebSocket Registered apps: URLs all start with ws://localhost:8098
Appuyer sur Entree pour arreter le serveur.
janv. 06, 2014 09:40:39 PM org.glassfish.tyrus.server.Server start
Infos: WebSocket server started.
janv. 06, 2014 09:42:31 PM
fr.jmdoudoux.dej.websockets.server.TestServerWebSocketTyrus main
Infos: Arret du serveur
janv. 06, 2014 09:42:32 PM
org.glassfish.grizzly.http.server.NetworkListener stop
Infos: Stopped listener bound to [0.0.0.0:8098]
janv. 06, 2014 09:42:32 PM org.glassfish.tyrus.server.Server stop
Infos: Websocket Server stopped.

 

76.6.2. L'utilisation de Javascript côté client

HTML 5 propose une API Javascript pour permettre d'utiliser les Websockets dans les pages Web.

La majorité des navigateurs récents supportent le protocole WebSocket : il est possible de consulter l'url https://caniuse.com/#feat=websockets ou https://caniuse.com/websockets pour s'assurer du support pour une version donnée d'un navigateur.

La classe Javascript WebSocket est l'élément principal pour utiliser les websockets dans une page Web.

Il faut créer un objet de type WebSocket

var socket = new WebSocket(url, [sub-protocol]);

Il attend en paramètre l'URL de la websocket : le protocole de l'URL doit être ws:// ou wss:///

Le second paramètre, optionnel, permet de préciser le ou les sous-protocoles utilisables pour la communication avec le serveur. Lors de la connexion, le serveur sélectionnera un de ceux-ci s'il le supporte. Dès que la connexion est établie, la propriété protocol de la classe WebSocket permet de connaître le protocol choisi par le serveur.

Résultat :
var socket = new WebSocket("ws://localhost:8080/websockets/monbean", 
  ["procotole1","protocole2"]);

La classe WebSocket possède plusieurs attributs :

Attribut

Rôle

readyState

Fournir l'état de la connexion

  • 0 : la connexion n'est pas encore établie
  • 1 : la connexion est établie
  • 2 : la connexion est en cours de fermeture
  • 3 : la connexion est fermée

bufferedAmount

Nombre d'octets


La classe WebSocket possède plusieurs événements liés au cycle de vie de la websocket :

Evénement

Rôle

onopen

La connexion est établie

onmessage

Un message est reçu. Les données sont stockées dans la propriété data du paramètre

onerror

Une erreur est survenue

onclose

La connexion est fermée


Avant de se connecter au endpoint, il faut associer des gestionnaires sur les événements utiles liés au cycle de vie de la websocket.

La classe WebSocket possède deux méthodes :

Méthode

Rôle

send()

Envoyer un message par la websocket

close()

Fermer la connexion


Exemple :
<!DOCTYPE html>
<html>
    <head>
        <title>Test Websockets pour obtenir des données</title>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width">
        <script language="javascript" type="text/javascript">
            var wsUri = getRootUri() + "/MaWebApp/monbean";
            var websocket;
            var id = 0;
            
            function getRootUri() {
                return "ws://" + (document.location.hostname == "" ?
                        "localhost" : document.location.hostname) + ":" +
                        (document.location.port == "" ? "8080" : document.location.port);
            }
            function init() { 
                messageDiv = document.getElementById("messageDivId");
                websocket = new WebSocket(wsUri);
                websocket.onopen = function(evt) {
                    onOpen(evt)
                };
                websocket.onmessage = function(evt) {
                    onMessage(evt)
                };
                websocket.onerror = function(evt) {
                    onError(evt)
                };
                setInterval(sendMessage,5000);
            }
            function close() {
                websocket.close();
            }
            function onOpen(evt) {
                afficher("CONNECTE");
            }
            function onMessage(evt) {
                afficher("RECU : " + evt.data);
                afficherDonnees(evt.data)
            }
            function onError(evt) {
                afficher('<span style="color: red;">ERREUR:</span> ' + evt.data);
            }
            function afficherDonnees(message) {
                donnees = JSON.parse(message);
                var nom = document.getElementById("nom");
                var valeur = document.getElementById("valeur");
                nom.value = donnees.nom;
                valeur.value = donnees.valeur;
            }
            function afficher(message) {
                var ligne = document.createElement("p");
                ligne.innerHTML = message;
                messageDiv.innerHTML = ligne.innerHTML ;
            }
            
           function sendMessage() {
              id = id + 1;
              websocket.send(id);    
            }
            
            window.addEventListener("load", init, false);
            window.addEventListener("unload", close, false);
        </script>
    </head>
    <body>
      <div id="donnees">
        <input id="nom" class="text-field" type="text" placeholder="Nom" readonly />
        <input id="valeur" class="text-field" type="text" placeholder="Valeur" readonly />
        <input class="button" type="submit" value="Send" onclick="sendMessage();" />
      </div>
      <div id="messageDivId"></div>
    </body>
</html>

Il est important de gérer correctement les événements du cycle de vie de la websocket. La connexion est généralement bien gérée car sinon aucun messages n'est émis ou reçus. Par contre, la déconnexion doit être correctement gérée notamment pour permettre de libérer les ressources côtés serveur : ce traitement peut par exemple se faire dans l'événement onunload() du tag <body> de la page.

Il est possible de tester si le navigateur est capable de mettre en oeuvre les websockets.

Résultat :
if(window.WebSocket) {
  // utilisation de la websocket
} else {
  alert('Le navigateur ne supporte pas les websockets');
}

Le protocole WebSocket est développé pour être utilisé par différents types de clients mais tous n'ont pas un support équivalent des fonctionnalités. Ainsi, il n'est pas possible d'échanger des données binaires avec un client utilisant Javascript.

 

 


[ Précédent ] [ Sommaire ] [ Suivant ] [Télécharger ]      [Accueil ]

78 commentaires Donner une note à l´article (5)

 

Copyright (C) 1999-2022 Jean-Michel DOUDOUX. Vous pouvez copier, redistribuer et/ou modifier ce document selon les termes de la Licence de Documentation Libre GNU, Version 1.1 ou toute autre version ultérieure publiée par la Free Software Foundation; les Sections Invariantes étant constitués du chapitre Préambule, aucun Texte de Première de Couverture, et aucun Texte de Quatrième de Couverture. Une copie de la licence est incluse dans la section GNU FreeDocumentation Licence. La version la plus récente de cette licence est disponible à l'adresse : GNU Free Documentation Licence.