Streaming HTTP : mise en oeuvre avec le langage Java

3c4219fa12e875f02a81d5957876de8e?s=47 Mickael BARON
February 19, 2019

Streaming HTTP : mise en oeuvre avec le langage Java

Ce support de cours présente les bibliothèques Tyrus (JSR 356) et JAX-RS pour développer en Java la partie serveur et cliente de WebSocket et de Server-Sent Event. Pour les deux technologies un plan similaire sera suivi : généralités, développement serveur, développement de la couche cliente et problématiques de déploiement.

3c4219fa12e875f02a81d5957876de8e?s=128

Mickael BARON

February 19, 2019
Tweet

Transcript

  1. SOA – Streaming Mickaël BARON – Février 2019 mailto:baron.mickael@gmail.com ou

    mailto:baron@ensma.fr @mickaelbaron mickael-baron.fr Mise en œuvre
  2. - M. Baron - Page mickael-baron.fr @mickaelbaron 2 Creative Commons

    Contrat Paternité Partage des Conditions Initiales à l'Identique 2.0 France http://creativecommons.org/licenses/by-sa/2.0/fr Licence Streaming - Outils
  3. - M. Baron - Page mickael-baron.fr @mickaelbaron 3 À propos

    de l’auteur … † Mickaël BARON † Ingénieur de Recherche au LIAS † https://www.lias-lab.fr † Equipe : Ingénierie des Données et des Modèles † Responsable des plateformes logicielles, « coach » technique † Responsable Rubriques Java de Developpez.com † Communauté Francophone dédiée au développement informatique † https://java.developpez.com † 4 millions de visiteurs uniques et 12 millions de pages vues par mois † 750 00 membres, 2 000 forums et jusqu'à 5 000 messages par jour Streaming - Outils @mickaelbaron mickael-baron.fr
  4. - M. Baron - Page mickael-baron.fr @mickaelbaron 4 Plan du

    cours † Technologies Push avec le langage Java † WebSocket (Tyrus) † SSE (JAX-RS) † Technologies Publish/Subscribe avec le langage Java † RabbitMQ Streaming - Outils
  5. - M. Baron - Page mickael-baron.fr @mickaelbaron 5 Déroulement du

    cours † Pédagogie du cours † Des bulles d’aide tout au long du cours † Survol des principaux concepts en évitant une présentation exhaustive † Logiciels utilisés † Navigateur Web † Pré-requis † JAX-RS † Exemples † WebSocket : https://github.com/mickaelbaron/ws-examples † SSE : https://github.com/mickaelbaron/sse-examples Streaming - Outils
  6. - M. Baron - Page mickael-baron.fr @mickaelbaron 6 Ressources :

    liens sur le Web † WebSocket † stackoverflow.com/questions/33262947/what-are-the-available-java-ee- websockets-implementations † github.com/eugenp/tutorials/tree/master/java-websocket † github.com/javaee-samples/javaee7-samples/tree/master/websocket † www.baeldung.com/java-websockets † fr.slideshare.net/arungupta1/nuts-and-bolts-of-websocket-devoxx-2014 † fr.slideshare.net/ouadielahdioui/realtime-applications-avec-la-spcification-java- jsr-356-et-le-protocole-websocket-rfc-6455 † github.com/arun-gupta/nuts-and-bolts-of-websocket † Server-Sent Event † www.baeldung.com/java-ee-jax-rs-sse † jersey.github.io/documentation/latest/sse.html Streaming - Outils
  7. - M. Baron - Page mickael-baron.fr @mickaelbaron Version Java supportée

    par ce support de cours 7 q Pas de gestion des modules dans les exemples (utilisation du classpath) Streaming - Outils
  8. SOA – Streaming Mickaël BARON – Février 2019 mailto:baron.mickael@gmail.com ou

    mailto:baron@ensma.fr @mickaelbaron mickael-baron.fr WebSocket avec Java (Tyrus)
  9. - M. Baron - Page mickael-baron.fr @mickaelbaron Plan du cours

    : WebSocket avec Java (Tyrus) 9 WebSocket † Généralités WebSocket avec Java † Premier WebSocket avec Java et Tyrus † Développement de WebSocket (vue serveur) † Cycle de vie † Envoyer et recevoir des messages † Personnaliser les messages † Développement de WebSocket (vue client) † Déploiement
  10. - M. Baron - Page mickael-baron.fr @mickaelbaron Généralités WebSocket avec

    Java 10 † Ce cours s’intéresse au développement de WebSocket en différenciant † Côté serveur : publie un WebSocket qui est en attente de connexions † Côté client : initialise une connexion vers un WebSocket † La majorité des langages de programmation orientés web supportent le développement de WebSocket † Java, JavaScript, PHP, C#, Python… † Ce cours se limite au langage Java † Différents frameworks de développement de services web † Ceux qui respectent la spécification JSR 356 (détailler après) † Autres … WebSocket
  11. - M. Baron - Page mickael-baron.fr @mickaelbaron Généralités WebSocket avec

    Java : la spécification 11 WebSocket † La spécification WebSocket est décrire par la JSR 356 † Version courante de la spécification est la 1.1 (2014) † Code source API : github.com/eclipse-ee4j/websocket-api † Cette spécification décrit la mise en œuvre de WebSocket côté client et serveur † Le développement de WebSocket repose sur l’utilisation de classes Java † Soit en utilisant des annotations † Soit en utilisant une approche API (~ héritage de classes)
  12. - M. Baron - Page mickael-baron.fr @mickaelbaron Généralités WebSocket avec

    Java : l’implémentation 12 † Différentes implémentations de la spécification JSR 356 sont disponibles † Tyrus : implémentation de référence fournie par Glassfish † Site projet : tyrus-project.github.io † Tomcat : serveur web de la fondation Apache † Site projet : tomcat.apache.org † Doc : tomcat.apache.org/tomcat-9.0-doc/web-socket-howto.html † Jetty : serveur web de la fondation Eclipse † Site projet : www.eclipse.org/jetty † Doc : www.eclipse.org/jetty/documentation/9.4.x/websocket-intro.html † Undertow : serveur web derrière Wildfly (Redhat) † Site projet : undertow.io † Doc : undertow.io/undertow-docs/undertow-docs-2.0.0/index.html #websockets WebSocket
  13. - M. Baron - Page mickael-baron.fr @mickaelbaron Généralités WebSocket avec

    Java : l’implémentation 13 WebSocket † Dans la suite de ce support de cours nous présenterons l’implémentation de référence Tyrus † Version actuelle 1.15 respectant la spécification JSR 356 1.1 † Implémentation intégrée dans le serveur d’application Glassfish † Fournit des connecteurs pour s’intégrer avec d’autres serveurs d’application † Site projet : tyrus-project.github.io † Code source : github.com/eclipse-ee4j/tyrus
  14. - M. Baron - Page mickael-baron.fr @mickaelbaron Généralités WebSocket avec

    Tyrus : Maven 14 † Dépendance pour l’API WebSocket † Dépendance pour les besoins API serveur † Dépendance pour les besoins API client <dependency> <groupId>javax.websocket</groupId> <artifactId>javax.websocket-api</artifactId> <version>1.1</version> <scope>provided</scope> </dependency> Nécessaire pour l’accès à l’API (pour la compilation) <dependency> <groupId>org.glassfish.tyrus</groupId> <artifactId>tyrus-server</artifactId> <version>${tyrus.version}</version> </dependency> WebSocket <dependency> <groupId>org.glassfish.tyrus</groupId> <artifactId>tyrus-client</artifactId> <version>${tyrus.version}</version> </dependency> Différentes implémentations existes (voir section Cliente et Déploiement)
  15. - M. Baron - Page mickael-baron.fr @mickaelbaron Généralités WebSocket avec

    Tyrus : développement 15 † Le développement de WebSocket avec JSR 356 est basé sur l’utilisation de classes Java † En utilisant des annotations spécifiques † En héritant de la classe Endpoint † Quand utiliser Annotation ou héritage ? † Annotation pour séparer le code spécifique pour le WebSocket † Héritage pour s’intégrer dans un code existant † Pas de description requise dans des fichiers de configuration † Pas de configuration spécifique pour le déploiement (dernière partie du cours) † Un WebSocket est déployé dans une application web WebSocket
  16. - M. Baron - Page mickael-baron.fr @mickaelbaron Plan du cours

    : WebSocket avec Java (Tyrus) 16 WebSocket † Généralités WebSocket avec Java † Premier WebSocket avec Java et Tyrus † Développement de WebSocket (vue serveur) † Cycle de vie † Envoyer et recevoir des messages † Personnaliser les messages † Développement de WebSocket (vue client) † Déploiement
  17. - M. Baron - Page mickael-baron.fr @mickaelbaron Premier WebSocket avec

    Java et Tyrus 17 † Exemple : reçoit un message et diffuse à tous les clients @ServerEndpoint(value = "/hello") public class HelloworldEndpoint { @OnMessage public void onMessage(String message, Session session) { for (Session currentSession : session.getOpenSessions()) { currentSession.getBasicRemote().sendText( System.currentTimeMillis() + ":" + message); } } } HelloWorldEndpoint.java du projet ws-helloworldwebsocket Méthode invoquée à chaque fois qu’un message est envoyé à ce WebSocket serveur Envoi un message à tous les clients WebSocket WebSocket
  18. - M. Baron - Page mickael-baron.fr @mickaelbaron Premier WebSocket avec

    Java et Tyrus 18 † Exemple (suite) : reçoit un message et diffuse à tous les clients var ws; function connect() { var host = document.location.host; var pathname = document.location.pathname; ws = new WebSocket(document.getElementById("wsURI").value); ws.onmessage = function (evt) { console.log(evt); writeLog(evt.data); }; ... } function send() { ws.send(document.getElementById("wsMessage").value); } websocket.js du projet ws-helloworldwebsocket Fonction invoquée à chaque fois qu’un message est publié par le WebSocket serveur WebSocket
  19. - M. Baron - Page mickael-baron.fr @mickaelbaron Exemple file rouge

    : le classique « Chat » 19 † Utilisation d’un exemple représentatif pour la présentation des subtilités techniques de l’API JSR 356 † Un utilisateur précise un pseudo et nom de salon † Un utilisateur ne peut pas se connecter plusieurs fois (refus de connexion) † Le WebSocket (côté serveur) publie le message d’un utilisateur à tous les utilisateurs d’un salon † Les messages peuvent être textuels (texte), binaires (des images) ou personnalisés (ChatMessage) † Le format du message String peuvent être « sans » ou JSON WebSocket
  20. - M. Baron - Page mickael-baron.fr @mickaelbaron WebSocket (serveur) :

    généralités 20 † Une classe Java doit être annotée avec @ServerEndpoint pour être considérée comme un WebSocket (serveur) † Chaque connexion vers un WebSocket crée une instance unique de la classe annotée @ServerEndpoint (autant d’instance que de connexion) † Cette instance existe pendant toute la durée de vie de la connexion du WebSocket † En d’autres termes : s’il existe n connexions (soit n client) alors n instances d’une classe @ServerEndPoint devront être créées WebSocket
  21. - M. Baron - Page mickael-baron.fr @mickaelbaron WebSocket (serveur) :

    définir l’URI 21 † L’annotation @ServerEndpoint permet de renseigner l’URI d’accès au WebSocket sur le serveur (paramètre value) † La valeur donnée à value correspond à une expression URI relative au contexte de l’application web † Possibilité également d’utiliser des paramètres de requêtes /hello?param1=value1&param2=value2 WebSocket @ServerEndpoint(value = "/hello") public class HelloworldEndpoint { ... } ws://localhost:8025/helloworldwebsocket/hello Protocole pour WebSocket ws ou wss Port Contexte de l'application WEB URI du WebSocket Adresse du serveur HelloworldEndpoint.java du projet ws-helloworldwebsocket
  22. - M. Baron - Page mickael-baron.fr @mickaelbaron WebSocket (serveur) :

    définir l’URI 22 † La valeur définie de value dans @ServerEndpoint ne se limite pas seulement aux expressions constantes † Possibilité de définir des expressions complexes appelées URI-template (= Template Parameters de JAX-RS) † Pour distinguer une expression complexe dans value son contenu est délimité par { … } † Possibilité également de mixer dans la valeur de value des expressions constantes et des expressions complexes † Exemple : @ServerEndpoint(value = "/chat/{chatroom}/{username}") public class ChatEndpoint { ... } ChatEndpoint.java du projet ws-chatwebsocket WebSocket ws://localhost:8025/chatwebsocket/chat/java/mbaron
  23. - M. Baron - Page mickael-baron.fr @mickaelbaron Plan du cours

    : WebSocket avec Java (Tyrus) 23 WebSocket † Généralités WebSocket avec Java † Premier WebSocket avec Java et Tyrus † Développement de WebSocket (vue serveur) † Cycle de vie † Envoyer et recevoir des messages † Personnaliser les messages † Développement de WebSocket (vue client) † Déploiement
  24. - M. Baron - Page mickael-baron.fr @mickaelbaron WebSocket (serveur) :

    cycle de vie 24 † L’API JSR 356 fournit quatre annotations pour gérer le cycle de vie d’un WebSocket † @OnOpen : à la création de la connexion † @OnMessage : lors de la réception de message † @OnError : si une erreur se produit † @OnClose : à la fermeture de la connexion † Ces annotations sont rattachées à des méthodes Java WebSocket
  25. - M. Baron - Page mickael-baron.fr @mickaelbaron WebSocket (serveur) :

    cycle de vie 25 † En fonction des annotations utilisées, différents paramètres sont disponibles (pas forcément les mêmes) † Pour récupérer les valeurs des URI-template (définis au niveau de @ServerEndpoint) utilisation de 0 à n paramètres de type String annotés avec @PathParam @ServerEndpoint(value = "/chat/{chatroom}/{username}") public class ChatEndpoint { @OnOpen public void onOpen(Session session, @PathParam("chatroom") String chatRoom, @PathParam("username") String userName) { ... } ChatEndpoint.java du projet ws-chatwebsocket Récupération des valeurs de chatroom et username via l’annotation @PathParam WebSocket
  26. - M. Baron - Page mickael-baron.fr @mickaelbaron WebSocket (serveur) :

    cycle de vie - @OnOpen 26 † La méthode annotée @OnOpen est invoquée à chaque fois qu’une connexion à un WebSocket est réalisée † Paramètres autorisés (facultatif) † Session : décrit la conversation entre le WebSocket client et serveur (sera détaillée plus tard) † EndpointConfig : les informations définies lors de la définition du WebSocket serveur (URI, encodeur, décodeur…) † Comment paramétrer une connexion ? † Pas de possibilité de transmettre des messages lors de la connexion † Via l’utilisation des paramètres de chemin (@PathParam) † Via l’utilisation des paramètres de requête WebSocket
  27. - M. Baron - Page mickael-baron.fr @mickaelbaron WebSocket (serveur) :

    cycle de vie - @OnOpen 27 † Exemple : création de la connexion @ServerEndpoint(value = "/chat/{chatroom}/{username}") public class ChatEndpoint { // Key = username private static Map<String, Session> allSessions = new HashMap<>(); // Key = session id private static Map<String, String> allUsers = new HashMap<>(); // Key = session id private static Map<String, String> allChatRooms = new HashMap<>(); @OnOpen public void onOpen(Session session, @PathParam("chatroom") String chatRoom, @PathParam("username") String userName) throws IOException { if (allSessions.containsKey(userName)) { session.close(); } else { allChatRooms.put(session.getId(), chatRoom); allUsers.put(session.getId(), userName); allSessions.put(userName, session); this.broadcast(userName + " connected!", session, chatRoom); } } } ChatEndpoint.java du projet ws-chatwebsocket Récupération des valeurs des URI-templates Toutes les autres connexions du même salon recevront un message indiquant qu’un nouvel utilisateur s’est connecté WebSocket
  28. - M. Baron - Page mickael-baron.fr @mickaelbaron WebSocket (serveur) :

    cycle de vie - @OnMessage 28 † La méthode annotée @OnMessage est invoquée à chaque fois que le WebSocket serveur reçoit des messages † Selon le type du message (String, Byte[], classe Java) les paramètres de la méthode annotée peuvent être différents † Paramètres autorisés † Session : décrit la conversation entre le WebSocket client et serveur † String ou Byte[] ou classe Java : le contenu du message reçu WebSocket
  29. - M. Baron - Page mickael-baron.fr @mickaelbaron WebSocket (serveur) :

    cycle de vie - @OnMessage 29 † La méthode annotée @OnMessage peut avoir un type de retour non void correspondant au type du message † String, Byte[], classe Java… † Dans quel cas utiliser une méthode avec un type non void ? † non void : pour retourner à un seul WebSocket client et avec une obligation de retour de message † void : pour retourner à plusieurs WebSocket clients et sans obligation de retour (voir section « Envoyer des messages ») @ServerEndpoint(value = "/hellosinglereturn") public class HelloworldSingleEndpoint { ... @OnMessage public String onMessage(String message, Session session) { return System.currentTimeMillis() + ":" + message; } } HelloworldSingleReturnEndpoint.java du projet ws-helloworldwebsocket WebSocket
  30. - M. Baron - Page mickael-baron.fr @mickaelbaron WebSocket (serveur) :

    cycle de vie - @OnMessage 30 † Exemple : traitement des messages @ServerEndpoint(value = "/chat/{chatroom}/{username}") public class ChatEndpoint { // Key = session id private static Map<String, String> allUsers = new HashMap<>(); // Key = session id private static Map<String, String> allChatRooms = new HashMap<>(); ... @OnMessage public void onMessage(String message, Session session) { this.broadcast(message + " (from: " + allUsers.get(session.getId()) + ")", null, allChatRooms.get(session.getId())); } } ChatEndpoint.java du projet ws-chatwebsocket La récupération des valeurs des URI-templates n’est pas nécessaire Toutes les autres connexions du même salon recevront le message envoyé WebSocket
  31. - M. Baron - Page mickael-baron.fr @mickaelbaron WebSocket (serveur) :

    cycle de vie - @OnClose 31 † La méthode annotée @OnClose est invoquée à chaque fois qu’une connexion WebSocket se termine † Aucune information ne peut être transmise vers le WebSocket † Paramètres autorisés (facultatif) † Session : décrit la conversation entre le WebSocket client et serveur (sera détaillée) † CloseReason : la raison pour laquelle la connexion s’est terminée † Quoi faire lors après une déconnexion ? † Libérer d’éventuelles ressources ou avertir les autres connexions WebSocket
  32. - M. Baron - Page mickael-baron.fr @mickaelbaron WebSocket (serveur) :

    cycle de vie - @OnClose 32 † Exemple : fermeture de la connexion @ServerEndpoint(value = "/chat/{chatroom}/{username}") public class ChatEndpoint { // Key = session id private static Map<String, String> allUsers = new HashMap<>(); // Key = session id private static Map<String, String> allChatRooms = new HashMap<>(); ... @OnClose public void onClose(Session session) { String currentUsername = allUsers.get(session.getId()); allSessions.remove(currentUsername); allUsers.remove(session.getId()); this.broadcast(currentUsername + " disconnected!", session, allChatRooms.get(session.getId())); } } ChatEndpoint.java du projet ws-chatwebsocket La récupération des valeurs des URI-templates n’est pas nécessaire 32 Tous les autres connexions du même salon recevront un message indiquant qu’un nouvel utilisateur s’est déconnecté WebSocket
  33. - M. Baron - Page mickael-baron.fr @mickaelbaron WebSocket (serveur) :

    par API 33 † Pour s’intégrer dans un code existant possibilité d’utiliser une approche API au lieu de passer par des annotations † Nécessite d’hériter de la classe Endpoint qui fournit † abstract void onOpen(Session, EndpointConfig) † void onClose(Session, CloseReason) † void onError(Session, Throwable) † La méthode onMessage n’existe pas et doit être traitée dans le corps de la méthode onOpen WebSocket
  34. - M. Baron - Page mickael-baron.fr @mickaelbaron WebSocket (serveur) :

    par API 34 † Exemple : WebSocket du « Chat » via API public class ChatEndpointUsingAPI extends Endpoint { ... @Override public void onOpen(Session session, EndpointConfig config) { String chatRoom = session.getPathParameters().get("chatroom"); String userName = session.getPathParameters().get("userName"); if (allSessions.containsKey(userName)) { try { session.close(); } catch (IOException e) { e.printStackTrace(); } } else { allChatRooms.put(session.getId(), chatRoom); allUsers.put(session.getId(), userName); allSessions.put(userName, session); session.addMessageHandler(String.class, new MessageHandler.Whole<String>() { public void onMessage(String message) { ChatEndpointUsingAPI.this.broadcast(message + ...); } }); this.broadcast(userName + " connected!", session, chatRoom); } } } ChatEndpointUsingAPI.java du projet ws-chatwebsocket Récupération des valeurs de paramètres de chemin Le code est très semblable au code réalisé avec les annotations WebSocket
  35. - M. Baron - Page mickael-baron.fr @mickaelbaron WebSocket (serveur) :

    par API 35 † Exemple (suite) : WebSocket du « Chat » via API public class ChatEndpointUsingAPI extends Endpoint { ... @Override public void onClose(Session session, CloseReason closeReason) { String currentUsername = allUsers.get(session.getId()); allSessions.remove(currentUsername); allUsers.remove(session.getId()); this.broadcast(currentUsername + " disconnected!", session, allChatRooms.get(session.getId())); } } ChatEndpointUsingAPI.java du projet ws-chatwebsocket WebSocket
  36. - M. Baron - Page mickael-baron.fr @mickaelbaron Accès aux informations

    de la connexion (Session) 36 † L’objet Session permet de récupérer des informations sur la connexion du WebSocket † Map<String, String> getPathParameters() : paramètres de chemin † String getQueryString() : les paramètres de requêtes † Map<String, List<String>> getRequestParameterMap() : tous les paramètres confondus † Session getOpenSessions() : toutes les sessions ouvertes † close() : pour fermer explicitement la session † Basic getBasicRemote() : pour envoyer un message synchrone † Async getAsyncRemote() : pour envoyer un message en mode asynchrone WebSocket
  37. - M. Baron - Page mickael-baron.fr @mickaelbaron Accès aux informations

    de la connexion (Session) 37 † Exemple : utilité de l’objet Session @ServerEndpoint(value = "/chat/{chatroom}/{username}") public class ChatEndpoint { ... @OnOpen public void onOpen(Session session, @PathParam("chatroom") String chatRoom, @PathParam("username") String userName) throws IOException { System.out.println("PathParameters:"); session.getPathParameters().forEach((c, v) -> { System.out.println(c + " " + v); }); System.out.println("QueryParameters:"); System.out.println(session.getQueryString()); System.out.println("Path and Query Parameters:"); session.getRequestParameterMap().forEach((k,v) -> { System.out.println(k + " " + v); }); System.out.println("Connection number:" + session.getOpenSessions().size()); if (allSessions.containsKey(userName)) { session.close(); } ... } } ChatEndpoint.java du projet ws-chatwebsocket Affichage des paramètres de chemin Affichage des paramètres de requêtes Affichage de tous les paramètres (chemin et requête) Fermeture explicite de la session courante ws://localhost:8025/chatwebsocket/chat/java/mbaron?chatroomqp=javaqp&usernameqp=mickaelbaronqp WebSocket
  38. - M. Baron - Page mickael-baron.fr @mickaelbaron Plan du cours

    : WebSocket avec Java (Tyrus) 38 WebSocket † Généralités WebSocket avec Java † Premier WebSocket avec Java et Tyrus † Développement de WebSocket (vue serveur) † Cycle de vie † Envoyer et recevoir des messages † Personnaliser les messages † Développement de WebSocket (vue client) † Déploiement
  39. - M. Baron - Page mickael-baron.fr @mickaelbaron Recevoir et envoyer

    des messages 39 † Les messages reçus et envoyés peuvent être de format † Texte = String ou Reader (comme vu précédemment) † Binaire = byte[] ou ByteBuffer (transmettre une image par exemple) † La réception d’un message peut être complète ou partielle † Si partielle, l’envoi du message est morcelé † Appel plusieurs fois la méthode annotée par @OnMessage † Possibilité d’utiliser des encodeurs et décodeurs afin de manipuler des types personnalisés † Recevoir et envoyer un objet de type Message WebSocket
  40. - M. Baron - Page mickael-baron.fr @mickaelbaron Recevoir des messages

    40 WebSocket † Pour recevoir un message † annoter une méthode par @OnMessage † choisir un format texte (String ou Reader) ou binaire (byte[] ou ByteBuffer) pour le paramètre désignant le message † Le format du message est dépendant du format utilisé par le WebSocket client † Possibilité d’utiliser au moins deux fois l’annotation @OnMessage pour gérer des messages textes et binaires † Pas de possibilité de surcharger @OnMessage sur des types de format équivalent (byte[] <=> ByteBuffer)
  41. - M. Baron - Page mickael-baron.fr @mickaelbaron Recevoir des messages

    41 † Exemple : envoyer une image pendant un discussion @ServerEndpoint(value = "/chat/{chatroom}/{username}") public class ChatBinaryEndpoint { ... @OnMessage public void onMessage(byte[] message, Session session) { this.broadcastBinaryMessage(ByteBuffer.wrap(message), null, allChatRooms.get(session.getId())); } @OnMessage public void onMessage(String message, Session session) { this.broadcastStringMessage(message + " (from: " + allUsers.get(session.getId()) + ")", null, allChatRooms.get(session.getId())); } } ChatBinaryEndpoint.java du projet ws-chatbinarywebsocket Invoquée pour l’envoi de l’image Invoquée pour l’envoi du texte WebSocket
  42. - M. Baron - Page mickael-baron.fr @mickaelbaron Recevoir des messages

    42 † Si le message à recevoir est trop important possibilité de le recevoir par paquet (taille du buffer) † Partielle : segmentation du message † Pour la réception « Partielle » un paramètre booléen dans la méthode annotée permet de connaître l’état de réception † Complète : réception complète du message † L’attribut maxMessageSize permet de fixer la taille du buffer (byte) † Par défaut maxMessageSize = -1 et peut recevoir un fichier de la taille supporté par la JVM † Si maxMessageSize != -1 le message ne peut dépasser la taille sous peine de fermeture de session avec CloseReason.TOO_BIG WebSocket
  43. - M. Baron - Page mickael-baron.fr @mickaelbaron Recevoir des messages

    43 † Exemple : segmentation d’un message de taille importante @ServerEndpoint(value = "/hellopartial") public class HelloworldPartialEndpoint { ... @OnMessage public void onTextMessage(String message, Session session, boolean part) throws IOException { System.out.println("Text length " + message.length()); if (part) { System.out.println("Whole message received"); } else { System.out.println("Partial message received"); } session.getBasicRemote().sendText(System.currentTimeMillis() + ":" + message.length()); } } HelloworldPartialEndpoint.java du projet ws-helloworldpartialwebsocket Tant que l’envoi du message n’est pas terminé Une fois l’envoi du message terminé WebSocket Génère un message de taille 1 Mo
  44. - M. Baron - Page mickael-baron.fr @mickaelbaron Recevoir des messages

    44 † Exemple : segmentation d’un message de taille importante @ServerEndpoint(value = "/hellomessagemax") public class HelloworldMessageMaxEndpoint { ... @OnMessage(maxMessageSize = 500) public void onTextMessage(String message, Session session) throws IOException { ... } @OnClose public void onClose(Session session, CloseReason reason) { System.out.println(reason.getReasonPhrase()); } @OnError public void onError(Session session, Throwable exception) { System.out.println(exception.getMessage()); } } HelloworldMessageMaxEndpoint.java du projet ws-helloworldpartialwebsocket Déclare la limite (en byte) du message La connexion est fermée car la taille du message est trop importante Traitement de l’erreur WebSocket Génère un message de taille 1 Mo
  45. - M. Baron - Page mickael-baron.fr @mickaelbaron Envoyer des messages

    45 † Pour envoyer des messages depuis le WebSocket serveur † Retour de la méthode annotée @OnMessage (à 1 seule connexion) † Utilisation de l’objet Session (à 1..n connexions) † Via objet Session, l’envoi peut se faire de façon synchrone ou asynchrone † Dans les deux cas, possibilité de retourner des messages textuels ou binaires † Exemples de méthodes † Synchrone (#Session.getBasicRemote()) † void sendText(String) † void sendBinary(ByteBuffer buf) † Asynchrone (#Session.getAsyncRemote()) † void sendBinary(ByteBuffer, SendHandler) † Future<Void> sendText(String) WebSocket
  46. - M. Baron - Page mickael-baron.fr @mickaelbaron Envoyer des messages

    46 WebSocket @ServerEndpoint(value = "/hellosinglereturn") public class HelloworldSingleReturnEndpoint { ... @OnMessage public String onMessage(String message, Session session) { return System.currentTimeMillis() + ":" + message; } } HelloworldSingleReturnEndpoint.java du projet ws-helloworldwebsocket † Exemple : deux façons d’envoyer des messages @ServerEndpoint(value = "/hellosinglesession") public class HelloworldSingleSessionEndpoint { ... @OnMessage public void onMessage(String message, Session session) { session.getBasicRemote().sendText(System.currentTimeMillis() + ":" + message); } } HelloworldSingleSessionEndpoint.java du projet ws-helloworldwebsocket Le résultat est identique
  47. - M. Baron - Page mickael-baron.fr @mickaelbaron 47 Envoyer des

    messages † Exemple : envoyer des messages à plusieurs connexions @ServerEndpoint(value = "/chat/{chatroom}/{username}") public class ChatBinaryEndpoint { @OnMessage public void onMessage(String message, Session session) { this.broadcastStringMessage(message + " (from: " + allUsers.get(session.getId()) + ")", null, allChatRooms.get(session.getId())); } private void broadcastStringMessage(String message, Session exclude, String currentChatRoom) { allSessions.forEach((username, session) -> { try { if (!(exclude != null && session.getId().equals(exclude.getId()))) { if (allChatRooms.get(session.getId()).equals(currentChatRoom)) { session.getBasicRemote().sendObject(message); } } } catch (IOException | EncodeException e) { e.printStackTrace(); } }); } } ChatBinaryEndpoint.java du projet ws-chatbinarywebsocket WebSocket
  48. - M. Baron - Page mickael-baron.fr @mickaelbaron Plan du cours

    : WebSocket avec Java (Tyrus) 48 WebSocket † Généralités WebSocket avec Java † Premier WebSocket avec Java et Tyrus † Développement de WebSocket (vue serveur) † Cycle de vie † Envoyer et recevoir des messages † Personnaliser les messages † Développement de WebSocket (vue client) † Déploiement
  49. - M. Baron - Page mickael-baron.fr @mickaelbaron Personnaliser les messages

    49 † Pour l’instant les messages reçus et envoyés étaient de type String et ByteBuffer † Possibilité de définir des types personnalisés afin de structurer les messages † Exemple pour le « chat » de discussion => ChatMessage (Message textuel, Date d’envoi et Navigateur utilisé) † Le format de représentation (XML, JSON…) est à la charge du développeur † L’API JSR 356 fournit la possibilité de définir † Decoder : décode le message vers un type personnalisé † Encoder : encode le message depuis type personnalisé † Pour préciser le Decoder et Encoder utilisation des attributs decoders et encoders de l’annotation @ServerEndpoint WebSocket
  50. - M. Baron - Page mickael-baron.fr @mickaelbaron Personnaliser les messages

    : Decoder 50 † Decoder une interface générique permettant de coder le passage d’un message vers un type personnalisé † Comment implémenter sa classe Decoder ? † Préciser le type générique (ChatMessage) † Préciser le format du message sur lequel s’applique ce décodage (textuel ou binaire) † Implémenter deux méthodes † T decode(String ou ByteBuffer) : décrit la transformation de String ou ByteBuffer vers le type générique † boolean willDecode(String ou ByteBuffer) : autorise ou pas la transformation WebSocket
  51. - M. Baron - Page mickael-baron.fr @mickaelbaron Personnaliser les messages

    : Decoder 51 WebSocket † Exemple : décoder un message String vers ChatMessage public class ChatMessageDecoder implements Decoder.Text<ChatMessage> { public void init(EndpointConfig config) {} public void destroy() {} public ChatMessage decode(String s) throws DecodeException { ObjectMapper mapper = new ObjectMapper(); try { ChatMessage readValue = mapper.readValue(s, ChatMessage.class); if (readValue.getCreated() == null) { readValue.setCreated(new Date()); } return readValue; } catch (IOException e) { e.printStackTrace(); return null; } } public boolean willDecode(String s) { return (s != null); } } Type générique = ChatMessage Format de message = textuel Utilisation de l’API Jackson pour désérialiser JSON => ChatMessage Si le message n’est pas null alors le décodage est possible ChatMessageDecoder.java du projet ws-chatjsonwebsocket
  52. - M. Baron - Page mickael-baron.fr @mickaelbaron Personnaliser les messages

    : Decoder 52 † Exemple (suite) : décoder un message String vers … public class ChatMessage { private String content; private Date created; private String browser; public ChatMessage() { } public String getContent() { return content; } public void setContent(String content) { this.content = content; } public Date getCreated() { return created; } public void setCreated(Date created) { this.created = created; } public String getBrowser() { return browser; } public void setBrowser(String browser) { this.browser = browser; } } ChatMessage.java du projet ws-chatjsonwebsocket Simple classe de type POJO WebSocket
  53. - M. Baron - Page mickael-baron.fr @mickaelbaron Personnaliser les messages

    : Encoder 53 † Encoder une interface générique permettant d’encoder un message depuis un type personnalisé † Comment implémenter sa classe Encoder ? † Préciser le type générique (ChatMessage) † Préciser le format du message sur lequel s’applique cet encodage (textuel ou binaire) † Implémenter une méthode † String ou ByteBuffer encode (T) : décrit la transformation d’un type générique vers String ou ByteBuffer WebSocket
  54. - M. Baron - Page mickael-baron.fr @mickaelbaron Personnaliser les messages

    : Encoder 54 † Exemple : encoder un message String depuis ChatMessage public class ChatMessageEncoder implements Encoder.Text<ChatMessage> { public void init(EndpointConfig config) { } public void destroy() { } public String encode(ChatMessage object) throws EncodeException { ObjectMapper mapper = new ObjectMapper(); try { return mapper.writeValueAsString(object); } catch (JsonProcessingException e) { e.printStackTrace(); return "ERROR"; } } } Type générique = ChatMessage Format de message = textuel ChatMessageEncoder.java du projet ws-chatjsonwebsocket Utilisation de l’API Jackson pour sérialiser ChatMessage => JSON WebSocket
  55. - M. Baron - Page mickael-baron.fr @mickaelbaron Personnaliser les messages

    55 † Exemple : déclarer les Encoder et Decoder @ServerEndpoint( value = "/chat/{chatroom}/{username}", decoders = ChatMessageDecoder.class, encoders = ChatMessageEncoder.class) public class ChatJSONEndpoint { @OnMessage public void onMessage(Session session, ChatMessage message) { this.broadcastObjectMessage(message, allUsers.get(session.getId()), null, allChatRooms.get(session.getId())); } private void broadcastObjectMessage(ChatMessage message, String user, Session exclude, String currentChatRoom) { allSessions.forEach((username, session) -> { try { if (!(exclude != null && session.getId().equals(exclude.getId()))) { if (allChatRooms.get(session.getId()).equals(currentChatRoom)) { session.getBasicRemote().sendObject(message); } } } catch (IOException | EncodeException e) { e.printStackTrace(); } }); } } ChatJSONEndpoint.java du projet ws-chatjsonwebsocket Déclaration des objets Decoder et Encoder L’utilisation du type personnalisé permet de choisir quelle méthode annotée @OnMessage à invoquer WebSocket Le message est envoyé comme un objet ChatMessage. C’est au Encoder de faire le travail.
  56. - M. Baron - Page mickael-baron.fr @mickaelbaron Plan du cours

    : WebSocket avec Java (Tyrus) 56 WebSocket † Généralités WebSocket avec Java † Premier WebSocket avec Java et Tyrus † Développement de WebSocket (vue serveur) † Cycle de vie † Envoyer et recevoir des messages † Personnaliser les messages † Développement de WebSocket (vue client) † Déploiement
  57. - M. Baron - Page mickael-baron.fr @mickaelbaron Développement Client 57

    WebSocket † Le développement de la partie WebSocket cliente est très similaire à la partie WebSocket serveur † Différences † Annotation @ClientEndpoint au lieu de @ServerEndpoint † On ne renseigne pas d’URI (attribut value) † Un WebSocket client n’est connecté qu’à un seul WebSocket serveur † Possibilité d’utiliser des classes annotées ou l’API Endpoint † Pour préciser l’adresse du WebSocket serveur utilisation d’un objet ClientManager † Dans la suite, nous donnerons des exemples pour † Client Java via l’annotation et l’API (Endpoint) † Client JavaScript
  58. - M. Baron - Page mickael-baron.fr @mickaelbaron Développement Client 58

    † Dépendance pour l’accès à l’API cliente de Tyrus <dependency> <groupId>org.glassfish.tyrus</groupId> <artifactId>tyrus-client</artifactId> <version>${tyrus.version}</version> </dependency> † Dépendance pour l’accès à l’implémentation cliente Grizzly † Il existe d’autres implémentations : InMemory, JDK-Client et Servlet <dependency> <groupId>org.glassfish.tyrus</groupId> <artifactId>tyrus-container-grizzly-client</artifactId> <version>${tyrus.version}</version> </dependency> WebSocket
  59. - M. Baron - Page mickael-baron.fr @mickaelbaron Développement Client 59

    WebSocket † Exemple : WebSocket client via @ClientEndpoint @ClientEndpoint public class ChatClientEndpoint { private CountDownLatch countDownLatch; public ChatClientEndpoint(CountDownLatch pCountDownLatch) { this.countDownLatch = pCountDownLatch; } @OnOpen public void onOpen(Session session) throws IOException { } @OnMessage public void onMessage(String message) { System.out.println("Received message: " + message); countDownLatch.countDown(); } } public class ChatClientWebSocketLauncher { private static CountDownLatch messageLatch = new CountDownLatch(1); public static void main(String[] args) { try { ClientManager client = ClientManager.createClient(); Session currentSession = client.connectToServer(new ChatClientEndpoint(messageLatch), new URI("ws://localhost:8026/chatwebsocket/chat/java/mickaelbaron")); currentSession.getBasicRemote().sendText("My First Message"); messageLatch.await(2, TimeUnit.SECONDS); } catch (Exception e) { e.printStackTrace(); } } } ChatClientEndpoint.java du projet ws-chatclientwebsocket ChatClientWebSocketLauncher.java du projet ws-chatclientwebsocket Connexion au WebSocket serveur
  60. - M. Baron - Page mickael-baron.fr @mickaelbaron Développement Client 60

    † Exemple : WebSocket client via l’API Endpoint public class ChatClientWebSocketUsingAPILauncher { private static CountDownLatch messageLatch; public static void main(String[] args) { try { messageLatch = new CountDownLatch(1); final ClientEndpointConfig cec = ClientEndpointConfig.Builder.create().build(); ClientManager client = ClientManager.createClient(); Session currentSession = client.connectToServer(new Endpoint() { @Override public void onOpen(Session session, EndpointConfig config) { session.addMessageHandler(new MessageHandler.Whole<String>() { @Override public void onMessage(String message) { System.out.println("Received message: " + message); messageLatch.countDown(); } }); } }, cec, new URI("ws://localhost:8026/chatwebsocket/chat/java/mickaelbaron")); currentSession.getBasicRemote().sendText("My First Message"); messageLatch.await(100, TimeUnit.SECONDS); } catch (Exception e) { e.printStackTrace(); } } } ChatClientWebSocketUsingAPILauncher.java du projet ws-chatclientwebsocket WebSocket
  61. - M. Baron - Page mickael-baron.fr @mickaelbaron Développement Client 61

    † Exemple : WebSocket client via l’API Endpoint avec OpenJFX public class ChatJavaFXClientWebSocketApplication extends Application { private void connect(String wsUri) { final ClientEndpointConfig cec = ClientEndpointConfig.Builder.create().build(); ClientManager client = ClientManager.createClient(); try { currentSession = client.connectToServer(new Endpoint() { @Override public void onOpen(Session session, EndpointConfig config) { writeMessage("Connect to WSEndpoint."); session.addMessageHandler(new MessageHandler.Whole<String>() { @Override public void onMessage(String message) { writeMessage(message); } }); } }, cec, new URI(wsUri)); } catch (DeploymentException | IOException | URISyntaxException e) { e.printStackTrace(); } } } $ java -cp 'target/dependency/*:target/classes' --module-path target/dependency --add-modules=javafx.fxml,,javafx.controls fr.mickaelbaron.chatjavafxclientwebsocket.ChatJavaFXClientWebSocketApplication ChatJavaFXClientWebSocketApplication.java du projet ws-chatjavafxclientwebsocket WebSocket
  62. - M. Baron - Page mickael-baron.fr @mickaelbaron Développement Client 62

    † Exemple : WebSocket client avec JavaScript var ws; function connect() { var host = document.location.host; var pathname = document.location.pathname; ws = new WebSocket(document.getElementById("wsURI").value); ws.onopen = function (evt) { console.log(evt); writeMessage("Connect to WSEndpoint."); }; ws.onmessage = function (evt) { console.log(evt); writeMessage(evt.data); }; ws.onerror = function (evt) { console.log(evt); }; ws.onclose = function (evt) { writeMessage("Disconnect from WSEndpoint."); } } function disConnect() { ws.close(); } function send() { ws.send(document.getElementById("wsMessage").value); } websocket.js du projet ws-chatwebsocket WebSocket
  63. - M. Baron - Page mickael-baron.fr @mickaelbaron Plan du cours

    : WebSocket avec Java (Tyrus) 63 WebSocket † Généralités WebSocket avec Java † Premier WebSocket avec Java et Tyrus † Développement de WebSocket (vue serveur) † Cycle de vie † Envoyer et recevoir des messages † Personnaliser les messages † Développement de WebSocket (vue client) † Déploiement
  64. - M. Baron - Page mickael-baron.fr @mickaelbaron Déploiement pour le

    WebSocket serveur 64 † Deux formes de déploiement pour exécuter un WebSocket † Déploiement sur un serveur d’application Java † Nécessite l’installation d’un serveur compatible Java EE ou autres (Jetty 9+, Tomcat, WildFly, Glassfish…) † Déploiement comme une application Java classique † Populaire depuis l’arrivée des microservices † Serveur d’application est intégré (embedded) WebSocket
  65. - M. Baron - Page mickael-baron.fr @mickaelbaron Déploiement : serveur

    d’application Java 65 † Les serveurs d’application implémentant JSR 356 † compatibles Java EE (Glassfish, Wildfly…) † les autres (Jetty 9+, Tomcat 7 et 8) † Configuration † Pas besoin de paramétrer le fichier web.xml † Quelles classes sont scannées pour identifier les WebSockets ? † celles qui implémentent ServerApplicationConfig † les classes qui sont annotées par @ServerEndpoint † les classes qui héritent de Endpoint † Dépendances nécessaires ? WebSocket <dependency> <groupId>javax.websocket</groupId> <artifactId>javax.websocket-api</artifactId> <version>1.1</version> <scope>provided</scope> </dependency>
  66. - M. Baron - Page mickael-baron.fr @mickaelbaron Déploiement : serveur

    d’application Java 66 WebSocket † Exemple : déployer un WebSocket (packagé war) dans une instance Tomcat via Docker $ mvn clean package # Compile et build le projet ws-chatwarwebsocket => Fichier chatwarwebsocket.war disponible dans le répertoire target/ $ docker pull tomcat:9-jre11-slim # Télécharge la version 9 de Tomcat avec une JRE 11 => Image Docker disponible $ docker run --rm --name chatwarwebsocket-tomcat -v $(pwd)/target/chatwarwebsocket.war:/usr/ local/tomcat/webapps/chatwarwebsocket.war -it -p 8080:8080 tomcat:9-jre11-slim => WebSocket disponible à l’adresse ws://localhost:8080/chatwarwebsocket/chat Projet ws-chatwarwebsocket
  67. - M. Baron - Page mickael-baron.fr @mickaelbaron Déploiement : application

    Java classique 67 † Un WebSocket peut être déployé comme une application Java (JAR) sans avoir à fournir une application web (WAR) † Usages † Pour les tests fonctionnels, fournir des bouchons de WebSocket † Déployer son application comme un microservice (voir cours) † Dépendances Maven † Dépendance pour l’accès à l’API serveur de Tyrus † Dépendance pour l’accès à l’implémentation serveur Grizzly <dependency> <groupId>org.glassfish.tyrus</groupId> <artifactId>tyrus-server</artifactId> <version>${tyrus.version}</version> </dependency> <dependency> <groupId>org.glassfish.tyrus</groupId> <artifactId>tyrus-container-grizzly-server</artifactId> <version>${tyrus.version}</version> </dependency> WebSocket
  68. - M. Baron - Page mickael-baron.fr @mickaelbaron Déploiement : application

    Java classique 68 † Exemple : déployer un WebSocket serveur dans une application Java classique avec Grizzly public class ChatWebSocketLauncher { public static void main(String[] args) { final Map<String, Object> serverProperties = new HashMap<>(); serverProperties.put(Server.STATIC_CONTENT_ROOT, "./static"); Server server = new Server("localhost", 8028, "/chatjsonwebsocket", serverProperties, ChatJSONEndpoint.class); try { server.start(); System.out.println("Tyrus app started available at ws://localhost:8028/chatjsonwebsocket" + "\nHit enter to stop it..."); System.in.read(); } catch (Exception e) { e.printStackTrace(); } finally { server.stop(); } } } WebSocket ChatWebSocketLauncher.java du projet ws-chatjsonwebsocket Possibilité de déployer des ressources statiques (html, JS…)
  69. SOA – Streaming Mickaël BARON – Février 2019 mailto:baron.mickael@gmail.com ou

    mailto:baron@ensma.fr @mickaelbaron mickael-baron.fr JAX-RS et Jersey (SSE)
  70. - M. Baron - Page mickael-baron.fr @mickaelbaron Plan du cours

    : Server Sent Events avec Java (JAX-RS) 70 SSE † Généralités SSE avec Java † Premier SSE avec Java et JAX-RS † Développement de SSE † Diffusion unique (simple) † Diffusion multiple (broadcast) † Client † Déploiement
  71. - M. Baron - Page mickael-baron.fr @mickaelbaron Généralités SSE avec

    Java 71 SSE † Server-Sent Events technologie uni-directionnelle pour l’envoi de données serveur vers des clients † Pour développer un serveur SSE utilisation de l’API JAX-RS † Tout ce que nous avons vu avec JAX-RS reste valide † https://speakerdeck.com/mickaelbaron/jax-rs-developper-des-services- web-rest-avec-java † Il existe deux implémentations † Avant < JAX-RS 2.1 : spécifique à l’implémentation Jersey † Depuis => JAX-RS 2.1 : décrit dans la spécification JAX-RS et supportée dans les implémentations (notamment Jersey) † Par conséquent Jersey fournit deux implémentations de SSE † Dans la suite nous utiliserons l’implémentation SSE supportée par JAX-RS 2.1
  72. - M. Baron - Page mickael-baron.fr @mickaelbaron Généralités SSE avec

    Java : bibliothèques alternatives 72 SSE † jEaSSE : implémentation pour Servlet 3 et Vert.x † Site projet : github.com/mariomac/jeasse † Akka : bibliothèque pour Java et Scala † Site projet : akka.io † Spring WebFlux : implémentation fournie par Spring † Site projet : spring.io
  73. - M. Baron - Page mickael-baron.fr @mickaelbaron Généralités SSE avec

    Java : dépendance Maven 73 SSE † Pour prendre en compte l’implémentation SSE supportée par JAX-RS avec Jersey † Rappel : pour ajouter le support JSON lors de l’envoi d’un événement SSE <dependency> <groupId>org.glassfish.jersey.media</groupId> <artifactId>jersey-media-sse</artifactId> </dependency> <dependency> <groupId>org.glassfish.jersey.media</groupId> <artifactId> jersey-media-json-jackson</artifactId> </dependency>
  74. - M. Baron - Page mickael-baron.fr @mickaelbaron Généralités SSE :

    événement SSE (rappel) 74 † Un événement SSE est un bloc de texte composé de † event : le type d’événement (ex : add, remove…) † data : le contenu du message envoyé par le serveur (ex : « Hello ») † id : un identifiant (utile en cas de reprise déconnexion pour savoir quel était le dernier événement envoyé) † retry : temps en millisecondes pour une reconnexion du client vers le serveur (en cas de panne) † ‘:’ : commentaire † Chaque événement SSE est séparé par un double \n\n : This is a new HelloWorld message and continue the communication. event: add-message id: 123 retry: 1000 data: HelloWorld : This is a new HelloWorld message and continue the communication. event: add-message id: 124 ... SSE
  75. - M. Baron - Page mickael-baron.fr @mickaelbaron Premier SSE avec

    Java et JAX-RS 75 SSE † Exemple : envoyer un événement et mise en attente @Path("sse") public class HelloworldSseEndpoint { @GET @Produces(MediaType.SERVER_SENT_EVENTS) public void getHelloWorldWithSimpleSSE( @Context SseEventSink eventSink, @Context Sse sse) { OutboundSseEvent event = sse.newEventBuilder() .name("add-message") .data("HelloWorld") .comment("This is a new HelloWorld message.") .id("123") .reconnectDelay(1000) .build(); eventSink.send(event); } } HelloWorldSseEndpoint.java du projet sse-helloworld Doit produire un message de type text/event-stream Construction d’un événement de type SseEvent Transmission d’un seul événement au client
  76. - M. Baron - Page mickael-baron.fr @mickaelbaron Premier SSE avec

    Java et JAX-RS 76 † Exemple (suite) : envoyer un événement et mise en attente $ curl http://localhost:9992/api/sse : This is a new HelloWorld message and continue the communication. event: add-message id: 123 retry: 1000 data: HelloWorld Connexion au serveur via cURL comme un simple appel de service web REST Réception de l’événement SSE Mise en attente de réception de nouveaux événements SSE ou de la fermeture de la connexion SSE Délais avant que le client se reconnecte si une connexion se termine avec le serveur
  77. - M. Baron - Page mickael-baron.fr @mickaelbaron Plan du cours

    : Server Sent Events avec Java (JAX-RS) 77 SSE † Généralités SSE avec Java † Premier SSE avec Java et JAX-RS † Développement de SSE † Diffusion unique (simple) † Diffusion multiple (broadcast) † Client † Déploiement
  78. - M. Baron - Page mickael-baron.fr @mickaelbaron Développement SSE avec

    JAX-RS : déclarer ? 78 SSE † Une ressource SSE est une méthode annotée avec JAX-RS † Doit produire le type MIME : text/event-stream † Doit injecter un objet SseEventSink pour communiquer avec le client † Peut injecter un objet Sse utilisé pour créer des événements † Si possible doit être annotée @GET (pas obligé) @Path("sse") public class HelloworldSseEndpoint { @GET @Produces(MediaType.SERVER_SENT_EVENTS) public void getHelloWorldWithSimpleSSE( @Context SseEventSink eventSink, @Context Sse sse) { ... // Voir transparent suivant } } 1 2 3 4 1 2 3 4
  79. - M. Baron - Page mickael-baron.fr @mickaelbaron Développement SSE avec

    JAX-RS : implémenter ? 79 SSE † La construction d’un événement SSE via l’utilisation de Sse † Pour la transmission de l’événement au client deux modes † Diffusion à un seul client † Diffusion à un ensemble de client (Broadcast) @GET @Produces(MediaType.SERVER_SENT_EVENTS) public void getHelloWorldWithSimpleSSE( @Context SseEventSink eventSink, @Context Sse sse) { OutboundSseEvent event = sse.newEventBuilder() .name("add-message") .data("HelloWorld") .comment("This is a new HelloWorld message.") .id("123") .reconnectDelay(1000) .build(); ... // Voir transparent suivant } => event (type d’évenemet) => data => commentaire => identifiant => retry (délai pour reconnexion) Sse utilisée pour créer des événements
  80. - M. Baron - Page mickael-baron.fr @mickaelbaron Développement SSE avec

    JAX-RS : diffusion unique 80 SSE † Exemple : envoyer plusieurs événements à un client @Path("withstreaming") @GET @Produces(MediaType.SERVER_SENT_EVENTS) public void getHelloWorldAndTimeWithSimpleSSE(@Context SseEventSink eventSink, @Context Sse sse) { new Thread(() -> { for (int i = 0; i < 10000; i++) { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } OutboundSseEvent event = sse.newEventBuilder() .name("add-message") .id(LocalTime.now().toString()) .data("HelloWorld" + LocalTime.now()) .comment("This is a new HelloWorld message published each 1 second.") .build(); if (!eventSink.isClosed()) { eventSink.send(event); } else { return; } } eventSink.close(); }).start(); } Transmission d’un événement à un seul client 10000 événements seront construits HelloWorldSseEndpoint.java du projet sse-helloworld Dés que tous les événements sont transmis fermeture de la connexion
  81. - M. Baron - Page mickael-baron.fr @mickaelbaron Développement SSE avec

    JAX-RS : diffusion unique 81 SSE † Exemple (suite) : envoyer plusieurs événements à un client $ curl http://localhost:9992/api/sse/withstreaming : This is a new HelloWorld message published each 1 second. event: add-message id: 0 data: HelloWorld12:35:10.595740 : This is a new HelloWorld message published each 1 second. event: add-message id: 1 data: HelloWorld12:35:11.610276 : This is a new HelloWorld message published each 1 second. event: add-message id: 2 data: HelloWorld12:35:12.614277 ... : This is a new HelloWorld message published each 1 second. event: add-message id: 10000
  82. - M. Baron - Page mickael-baron.fr @mickaelbaron Développement SSE avec

    JAX-RS : diffusion multiple 82 SSE † L’API SSE avec JAX-RS permet aussi d’envoyer un même événement à plusieurs clients (on parle de broadcast) † Plusieurs choses à réaliser pour faire du broadcast † Initialiser le broadcaster via une instance SseBroadcaster † Inscrire un client (SseEventSink) au broadcaster † Soumettre un événement au broadcaster qui transmettra à tous les clients inscrits 1 2 3
  83. - M. Baron - Page mickael-baron.fr @mickaelbaron Développement SSE avec

    JAX-RS : diffusion multiple 83 † Exemple : envoyer un événement à plusieurs clients @Singleton @Path("sse-broadcast") public class HelloWorldSseBroadcastResource { private Sse sse; private SseBroadcaster broadcaster; public HelloWorldSseBroadcastResource(@Context final Sse sse) { this.sse = sse; this.broadcaster = sse.newBroadcaster(); } @GET @Produces(MediaType.SERVER_SENT_EVENTS) public void subscribeToBroadcast(@Context SseEventSink eventSink) { this.broadcaster.register(eventSink); } @POST public String broadcastMessage(String message) { OutboundSseEvent event = sse.newEventBuilder().name("add-message").data(message) .comment("This is a new message.").id(LocalTime.now().toString()).build(); broadcaster.broadcast(event); return "Message '" + message + "' has been broadcast."; } } Initialisation du broadcaster dés la première connexion Inscription d’un client au broadcaster Transmission d’un événement au broadcaster HelloWorldSseBroadcastResource.java du projet sse-helloworld 1 2 3 SSE
  84. - M. Baron - Page mickael-baron.fr @mickaelbaron Développement SSE avec

    JAX-RS : diffusion multiple 84 † Exemple (suite) : envoyer un événement à plusieurs clients $ curl --request POST --data 'Mon événement' http://localhost:9992/api/sse-broadcast Message 'Mon événement' has been broadcast. $ curl http://localhost:9992/api/sse-broadcast : This is a new message. event: add-message id: 15:03:24.448687 data: Mon événement Trois clients en attente d’événements SSE Appel au service web (requête POST) pour transmettre un message et créer un événement
  85. - M. Baron - Page mickael-baron.fr @mickaelbaron Développement SSE avec

    JAX-RS : type personnalisé 85 † Par défaut le type MIME utilisé pour la valeur de la donnée d’un événement est text/plain † Possibilité d’utiliser des types personnalisés en utilisant des formats reconnus application/json † Cette information de format doit être transmise lors de la création de l’événement et pas via l’annotation @Produces † Ne pas oublier d’ajouter la dépendance vers une bibliothèque qui gère le binding vers le format considéré SSE <dependency> <groupId>org.glassfish.jersey.media</groupId> <artifactId> jersey-media-json-jackson</artifactId> </dependency>
  86. - M. Baron - Page mickael-baron.fr @mickaelbaron Développement SSE avec

    JAX-RS : type personnalisé 86 † Exemple : envoyer un événement avec du contenu JSON à plusieurs clients @Singleton @Path("sse-broadcast-json") public class HelloWorldSseBroadcastWithJSONResource { ... @POST public String broadcastMessage(String message) { Message newMessage = new Message(); newMessage.setContent(message); newMessage.setTime(LocalTime.now().toString()); OutboundSseEvent event = sse.newEventBuilder() .name("add-message") .mediaType(MediaType.APPLICATION_JSON_TYPE) .data(Message.class, newMessage) .comment("This is a new message.") .id(LocalTime.now().toString()) .build(); broadcaster.broadcast(event); return "Message '" + message + "' has been broadcast."; } } HelloWorldSseBroadcastWithJSONResource.java du projet sse-helloworld La donnée est transmise au client au format JSON SSE
  87. - M. Baron - Page mickael-baron.fr @mickaelbaron Plan du cours

    : Server Sent Events avec Java (JAX-RS) 87 SSE † Généralités SSE avec Java † Premier SSE avec Java et JAX-RS † Développement de SSE † Diffusion unique (simple) † Diffusion multiple (broadcast) † Client † Déploiement
  88. - M. Baron - Page mickael-baron.fr @mickaelbaron Client SSE 88

    SSE † JAX-RS fournit également une API spécifique pour consommer un événement SSE envoyé par un serveur † Elle se base sur l’API JAX-RS pour l’initialisation de la ressource † Création d’un client (déjà vu) † Création d’un chemin racine (déjà vu) † Création d’un objet SseEventSource pour la gestion du message reçu, des erreurs et de la déconnexion † Ouverture de la connexion Client client = ClientBuilder.newClient(); WebTarget webTarget = client.target("http://127.0.0.1:9992/sse-broadcast"); SseEventSource eventSource = SseEventSource.target(target).build(); eventSource.register(onEvent, onError, onComplete); eventSource.open();
  89. - M. Baron - Page mickael-baron.fr @mickaelbaron Client SSE 89

    † Exemple : consommer un événement SSE public class HelloWorldSseClient { private static final String url = "http://localhost:9992/api"; public static void main(String... args) throws Exception { Client client = ClientBuilder.newClient(); WebTarget target = client.target(url).path(args.length == 1 ? args[0] : "sse"); try (SseEventSource eventSource = SseEventSource.target(target).build()) { eventSource.register(onEvent, onError, onComplete); eventSource.open(); // Consuming events for one hour Thread.sleep(60 * 60 * 1000); } catch (InterruptedException e) { e.printStackTrace(); } client.close(); } private static Consumer<InboundSseEvent> onEvent = (inboundSseEvent) -> { String data = inboundSseEvent.readData();System.out.println(data); }; private static Consumer<Throwable> onError = (throwable) -> { throwable.printStackTrace(); }; private static Runnable onComplete = () -> { System.out.println("Done!"); }; } HelloWorldSseClient.java du projet sse-helloworld-cient Définition de trois fonctions pour traiter la gestion des messages reçus, erreurs et fermeture SSE
  90. - M. Baron - Page mickael-baron.fr @mickaelbaron Client SSE 90

    † Exemple : consommer un événement SSE avec JavaScript var ws; function connect() { source = new EventSource(document.getElementById("sseURI").value); source.onopen = function(event) { console.log(event); }; source.addEventListener('add-message', function(e) { append(e.data); }, false); source.onerror = function(event) { console.log(event); }; } function disConnect() { source.close(); } SSE see.js du projet sse-helloworld Doit correspondre à la valeur donnée à event ou name
  91. - M. Baron - Page mickael-baron.fr @mickaelbaron Plan du cours

    : Server Sent Events avec Java (JAX-RS) 91 SSE † Généralités SSE avec Java † Premier SSE avec Java et JAX-RS † Développement de SSE † Diffusion unique (simple) † Diffusion multiple (broadcast) † Client † Déploiement
  92. - M. Baron - Page mickael-baron.fr @mickaelbaron Déploiement SSE :

    serveur d’application Java 92 † Exemple : déployer un service web (packagé war) dans une instance Tomcat via Docker # Projet sse-helloworld-war $ mvn clean package # Compile et build le projet sse-helloworld-war => Fichier helloworldsse.war disponible dans le répertoire target/ $ docker pull tomcat:9-jre11-slim # Télécharge la version 9 de Tomcat avec une JRE 11 => Image Docker disponible $ docker run --rm --name helloworldsse-tomcat -v $(pwd)/target/helloworldsse.war:/usr/local/tomcat/webapps /helloworldsse.war -it -p 8080:8080 tomcat:9-jre11-slim => SSE disponible à l’adresse http://localhost:8080/helloworldsse/api/sse $ curl --request POST --data 'Mon événement' http://localhost:8080/helloworldsse/api/sse Message 'Mon événement' has been broadcast. Appel au service web (requête POST) pour transmettre un message et créer un événement SSE
  93. - M. Baron - Page mickael-baron.fr @mickaelbaron Déploiement SSE :

    application Java classique 93 SSE † Exemple : utiliser JAX-RS avec le support SSE avec Grizzly public class HelloWorldServerSetEventsLauncher { public static final URI BASE_URI = getBaseURI(); private static URI getBaseURI() { return UriBuilder.fromUri("http://localhost/api").port(9992).build(); } public static void main(String[] args) throws ... { ResourceConfig resourceConfig = new ResourceConfig(); resourceConfig.registerClasses(HelloWorldSseResource.class, HelloWorldSseBroadcastResource.class, HelloWorldSseBroadcastWithJSONResource.class); StaticHttpHandler staticHttpHandler = new StaticHttpHandler("./static"); HttpServer server = GrizzlyHttpServerFactory .createHttpServer(BASE_URI, resourceConfig); server.start(); System.out.println(String.format("Jersey app started available at %s” + "\nHit enter to stop it...", BASE_URI)); System.in.read(); server.shutdownNow(); } } HelloWorldServerSetEventsLauncher.java du projet sse-helloworld Permet de déployer aussi les ressources statiques (HTML et JS)
  94. SOA – Streaming Mickaël BARON – Février 2019 mailto:baron.mickael@gmail.com ou

    mailto:baron@ensma.fr @mickaelbaron mickael-baron.fr RabbitMQ (Publish/Subscribe)
  95. - M. Baron - Page mickael-baron.fr @mickaelbaron RabbitMQ 95