Slide 1

Slide 1 text

SOA – Streaming et Messages Mickaël BARON – Mai 2020 (Rev. Novembre 2023) mailto:baron.mickael@gmail.com ou mailto:baron@ensma.fr mickael-baron.fr mickaelbaron Mise en œuvre

Slide 2

Slide 2 text

- M. Baron - Page mickael-baron.fr mickaelbaron 2 Creative Commons Contrat Paternité Partage des Conditions Initiales à l'Identique 2.0 France creativecommons.org/licenses/by-sa/2.0/fr Licence Streaming et Messages - Outils

Slide 3

Slide 3 text

- 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 † Ancien responsable Java de Developpez.com (2011-2021) † 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 et Messages - Outils mickael-baron.fr mickaelbaron

Slide 4

Slide 4 text

- M. Baron - Page mickael-baron.fr mickaelbaron 4 Plan du cours † Technologies Push avec le langage Java † WebSocket (Tyrus) † SSE (JAX-RS) † gRPC (io.grpc) † Technologie basée sur l’envoi de messages † RabbitMQ (AMQP) Streaming et Messages - Outils

Slide 5

Slide 5 text

- 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 : github.com/mickaelbaron/ws-examples † SSE : github.com/mickaelbaron/sse-examples Streaming et Messages - Outils

Slide 6

Slide 6 text

- M. Baron - Page mickael-baron.fr mickaelbaron 6 Ressources : liens sur le Web † WebSocket † www.baeldung.com/java-websockets † github.com/eugenp/tutorials/tree/master/java-websocket † fr.slideshare.net/arungupta1/nuts-and-bolts-of-websocket-devoxx-2014 † github.com/arun-gupta/nuts-and-bolts-of-websocket † fr.slideshare.net/ouadielahdioui/realtime-applications-avec-la-spcification-java- jsr-356-et-le-protocole-websocket-rfc-6455 † Server-Sent Event † www.baeldung.com/java-ee-jax-rs-sse † eclipse-ee4j.github.io/jersey.github.io/documentation/latest/sse.html † RabbitMQ † www.rabbitmq.com/getstarted.html † www.baeldung.com/rabbitmq † www.cloudamqp.com/blog/index.html Streaming et Messages - Outils

Slide 7

Slide 7 text

- 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 et Messages - Outils

Slide 8

Slide 8 text

SOA – Streaming et Messages Mickaël BARON – Mai 2020 (Rev. Novembre 2023) mailto:baron.mickael@gmail.com ou mailto:baron@ensma.fr mickael-baron.fr mickaelbaron WebSocket avec Java (Tyrus)

Slide 9

Slide 9 text

- 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

Slide 10

Slide 10 text

- 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 Jakarta (détailler après) † Autres … WebSocket

Slide 11

Slide 11 text

- M. Baron - Page mickael-baron.fr mickaelbaron Généralités WebSocket avec Java : la spécification 11 WebSocket † 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) † Version courante est la 2.x † WebSocket : jakarta.ee/specifications/websocket † Code source API : github.com/jakartaee/websocket † Site projet : projects.eclipse.org/projects/ee4j.websocket

Slide 12

Slide 12 text

- M. Baron - Page mickael-baron.fr mickaelbaron Généralités WebSocket avec Java : l’implémentation 12 WebSocket † Tyrus : implémentation de référence fournie par Glassfish † Site projet : eclipse-ee4j.github.io/tyrus † Tomcat : serveur web de la fondation Apache † Site projet : tomcat.apache.org † Doc : tomcat.apache.org/tomcat-10.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/jetty-11/programming- guide/index.html#pg-server-websocket † Undertow : serveur web derrière Wildfly (Redhat) † Site projet : undertow.io † Doc : undertow.io/undertow-docs/undertow-docs-2.1.0/index.html #websockets

Slide 13

Slide 13 text

- 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 † Respectant la spécification JSR 356 et Jakarta WebSocket † 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 : eclipse-ee4j.github.io/tyrus † Code source : github.com/eclipse-ee4j/tyrus

Slide 14

Slide 14 text

- 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 jakarta.websocket jakarta.websocket-api 2.0 provided Nécessaire pour l’accès à l’API (pour la compilation) org.glassfish.tyrus tyrus-server ${tyrus.version} WebSocket org.glassfish.tyrus tyrus-client ${tyrus.version} Différentes implémentations existes (voir section Cliente et Déploiement)

Slide 15

Slide 15 text

- M. Baron - Page mickael-baron.fr mickaelbaron Généralités WebSocket avec Tyrus : développement 15 † Le développement de WebSocket 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

Slide 16

Slide 16 text

- 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

Slide 17

Slide 17 text

- 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

Slide 18

Slide 18 text

- 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

Slide 19

Slide 19 text

- 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 WebSocket † 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

Slide 20

Slide 20 text

- 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

Slide 21

Slide 21 text

- 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

Slide 22

Slide 22 text

- 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

Slide 23

Slide 23 text

- 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

Slide 24

Slide 24 text

- M. Baron - Page mickael-baron.fr mickaelbaron WebSocket (serveur) : cycle de vie 24 † L’API WebSocket 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

Slide 25

Slide 25 text

- 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

Slide 26

Slide 26 text

- 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

Slide 27

Slide 27 text

- 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 allSessions = new HashMap<>(); // Key = session id private static Map allUsers = new HashMap<>(); // Key = session id private static Map 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

Slide 28

Slide 28 text

- 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

Slide 29

Slide 29 text

- 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

Slide 30

Slide 30 text

- 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 allUsers = new HashMap<>(); // Key = session id private static Map 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

Slide 31

Slide 31 text

- 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

Slide 32

Slide 32 text

- 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 allUsers = new HashMap<>(); // Key = session id private static Map 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

Slide 33

Slide 33 text

- 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

Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

- 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

Slide 36

Slide 36 text

- 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 getPathParameters() : paramètres de chemin † String getQueryString() : les paramètres de requêtes † Map> 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

Slide 37

Slide 37 text

- 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

Slide 38

Slide 38 text

- 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

Slide 39

Slide 39 text

- 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

Slide 40

Slide 40 text

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

Slide 41

Slide 41 text

- 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

Slide 42

Slide 42 text

- 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

Slide 43

Slide 43 text

- 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

Slide 44

Slide 44 text

- 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

Slide 45

Slide 45 text

- 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 sendText(String) WebSocket

Slide 46

Slide 46 text

- 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

Slide 47

Slide 47 text

- 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

Slide 48

Slide 48 text

- 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

Slide 49

Slide 49 text

- 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 WebSocket 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

Slide 50

Slide 50 text

- 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

Slide 51

Slide 51 text

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

Slide 52

Slide 52 text

- 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

Slide 53

Slide 53 text

- 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

Slide 54

Slide 54 text

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

Slide 55

Slide 55 text

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

Slide 56

Slide 56 text

- 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

Slide 57

Slide 57 text

- 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

Slide 58

Slide 58 text

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

Slide 59

Slide 59 text

- 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

Slide 60

Slide 60 text

- 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() { @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

Slide 61

Slide 61 text

- 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() { @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

Slide 62

Slide 62 text

- 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

Slide 63

Slide 63 text

- 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

Slide 64

Slide 64 text

- 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

Slide 65

Slide 65 text

- M. Baron - Page mickael-baron.fr mickaelbaron Déploiement : serveur d’application Java 65 † Les serveurs d’application implémentant Jakarta WebSocket † compatibles Java EE (Glassfish, Wildfly…) † les autres (Jetty, Tomcat…) † 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 jakarta.websocket jakarta.websocket-api 2.0 provided

Slide 66

Slide 66 text

- 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

Slide 67

Slide 67 text

- 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 org.glassfish.tyrus tyrus-server ${tyrus.version} org.glassfish.tyrus tyrus-container-grizzly-server ${tyrus.version} WebSocket

Slide 68

Slide 68 text

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

Slide 69

Slide 69 text

SOA – Streaming et Messages Mickaël BARON – Mai 2020 (Rev. Novembre 2023) mailto:baron.mickael@gmail.com ou mailto:baron@ensma.fr mickael-baron.fr mickaelbaron JAX-RS et Jersey (SSE)

Slide 70

Slide 70 text

- M. Baron - Page mickael-baron.fr mickaelbaron † 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 Plan du cours : Server Sent Events avec Java (JAX-RS) 70 SSE

Slide 71

Slide 71 text

- M. Baron - Page mickael-baron.fr mickaelbaron † 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://mickael-baron.fr/soa/developper-serviceweb-rest-jaxrs † 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) Généralités SSE avec Java 71 SSE

Slide 72

Slide 72 text

- 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

Slide 73

Slide 73 text

- 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 org.glassfish.jersey.media jersey-media-sse org.glassfish.jersey.media jersey-media-json-jackson

Slide 74

Slide 74 text

- 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

Slide 75

Slide 75 text

- 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 HelloworldSseResource { @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); } } HelloWorldSseResource.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

Slide 76

Slide 76 text

- 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 -H "Accept:text/event-stream" 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

Slide 77

Slide 77 text

- M. Baron - Page mickael-baron.fr mickaelbaron † 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 Plan du cours : Server Sent Events avec Java (JAX-RS) 77 SSE

Slide 78

Slide 78 text

- 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

Slide 79

Slide 79 text

- 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

Slide 80

Slide 80 text

- 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 HelloWorldSseResource.java du projet sse-helloworld Dés que tous les événements sont transmis fermeture de la connexion

Slide 81

Slide 81 text

- 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 -H "Accept:text/event-stream" 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

Slide 82

Slide 82 text

- 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

Slide 83

Slide 83 text

- 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

Slide 84

Slide 84 text

- 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 -H "Accept:text/event-stream" 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

Slide 85

Slide 85 text

- 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 org.glassfish.jersey.media jersey-media-json-jackson

Slide 86

Slide 86 text

- 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

Slide 87

Slide 87 text

- M. Baron - Page mickael-baron.fr mickaelbaron † 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 Plan du cours : Server Sent Events avec Java (JAX-RS) 87 SSE

Slide 88

Slide 88 text

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

Slide 89

Slide 89 text

- 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 onEvent = (inboundSseEvent) -> { String data = inboundSseEvent.readData();System.out.println(data); }; private static Consumer onError = (throwable) -> { throwable.printStackTrace(); }; private static Runnable onComplete = () -> { System.out.println("Done!"); }; } HelloWorldSseClient.java du projet sse-helloworld-client Définition de trois fonctions pour traiter la gestion des messages reçus, erreurs et fermeture SSE

Slide 90

Slide 90 text

- 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

Slide 91

Slide 91 text

- M. Baron - Page mickael-baron.fr mickaelbaron † 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 Plan du cours : Server Sent Events avec Java (JAX-RS) 91 SSE

Slide 92

Slide 92 text

- 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:jre11-openjdk-slim # Télécharge la version 10 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:jre11-openjdk-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

Slide 93

Slide 93 text

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

Slide 94

Slide 94 text

SOA – Streaming et Messages Mickaël BARON – Mai 2020 (Rev. Novembre 2023) mailto:baron.mickael@gmail.com ou mailto:baron@ensma.fr mickael-baron.fr mickaelbaron io.grpc (gRPC)

Slide 95

Slide 95 text

- M. Baron - Page mickael-baron.fr mickaelbaron gRPC 95 gRPC

Slide 96

Slide 96 text

SOA – Streaming et Messages Mickaël BARON – Mai 2020 (Rev. Novembre 2023) mailto:baron.mickael@gmail.com ou mailto:baron@ensma.fr mickael-baron.fr mickaelbaron RabbitMQ (AMQP)

Slide 97

Slide 97 text

- M. Baron - Page mickael-baron.fr mickaelbaron RabbitMQ 97 RabbitMQ