Développons en Java 2.30 | |
Copyright (C) 1999-2022 Jean-Michel DOUDOUX | (date de publication : 15/06/2022) |
|
Niveau : | 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 :
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 :
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 :
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.
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 :
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.
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 :
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.
L'interface MessageHandler définit les fonctionnalités relatives à la réception d'un message dans une conversation.
L'interface MessageHandler est utilisée :
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 :
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 :
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 :
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.
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 :
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 :
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 |
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 :
Exemple : |
ws://localhost:8080/MaWebApp/echo |
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 :
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 :
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 :
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.
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 :
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();
} |
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 :
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());
} |
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 :
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();
} |
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
}
} |
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.
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.
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 :
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 |
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.
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 :
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.
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.
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(); |
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 |
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.
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.
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();
|
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 :
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 :
Pour les messages de type binaire :
Pour les messages de type Pong :
Lors du développement d'un endpoint en utilisant l'API, un seul MessageHandler peut être enregistré pour chacun des types.
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 :
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.
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 :
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() {
}
} |
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.
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.
Cette section va proposer plusieurs exemples de mise en oeuvre des WebSockets.
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">
<input onclick="envoyer()" value="Envoyer" type="button">
</form>
</div>
<div id="messageDivId"></div>
</body>
</html> |
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> |
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 :
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.
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 |
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. |
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
|
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.
|