Dans notre article sur la mise en place d’un Batch Spring, j’ai évoqué la possibilité de visualiser la progression d’exécution du Batch en utilisant le protocole WebSocket. Celui-ci permet d’amorcer un canal de communication bi-directionnel entre votre page web et le serveur via un socket TCP. L’avantage d’une telle connexion est de permettre au serveur d’émettre des notifications vers le client Web sans recevoir au préalable une requête de la part du client.
Dans cet article nous allons voir comment mettre en place, émettre et réceptionner un message émis par notre serveur Java pour permettre la lecture en temps réel depuis une application Web sous Angular.
Spring propose bien entendu, une solution à l’utilisation de WebSocket. Vous devez d’abord injecter les dépendances suivantes dans votre projet Maven:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-messaging</artifactId>
</dependency>
Configuration sous Spring Boot
Dans un premier temps, vous devez définir une classe de configuration héritant de WebSocketMessageBrokerConfigurer
, cela permet à Spring Boot d’orienter les appelles utilisant le protocole WebSocket vers les endpoints dédiés de votre application Java.
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketBrokerConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.setApplicationDestinationPrefixes("/app");
registry.enableSimpleBroker("/topic");
}
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/stomp").setAllowedOrigins("*");
}
}
3 notions ici :
setApplicationDestinationPrefixes
: définit le préfixe d’accès aux éventuels contrôleurs que les clients pourront consommer dans votre API.enableSimpleBroker
: définit le préfixe d’accès au flux émis par le broker pour les clients souhaitant s’y inscrire.addEndpoint
: définit le point d’entrée pour le handshake entre le client et le serveur. Cela permet d’établir la connexion ouverte entre les deux services. Remarquez le présence desetAllowedOrigins("*")
permettant de gérer les CORS lors de l’appel.
Contrairement aux API de type REST, les API WebSocket ne permettent pas de passer d’arguments supplémentaires dans les headers de la requête ce qui devient compliqué pour les autorisations. De ce fait, il devient nécessaire d’ajouter un filtre supplémentaire à notre configuration de sécurité Spring :
@Override
public void configure(HttpSecurity http) throws Exception {
// @formatter:off
http
.cors()
.and()
.headers()
.frameOptions().disable()
.and()
.csrf().disable()
.authorizeRequests()
.antMatchers("/stomp").permitAll() // On autorise l'appel handshake entre le client et le serveur
.anyRequest()
.authenticated();
// @formatter:on
}
Il est maintenant possible d’émettre un appelle de n’importe où depuis notre API auquel les clients peuvent souscrire, il suffit d’ajouter la commande suivante dans notre code Java :
messagingTemplate.convertAndSend("/topic/progress", "Hello world");
Le client qui a souscrit au message en provenance du endpoint « topic/progress » de notre API recevra la chaîne « Hello world ».
Dans l’exemple de projet fournit en bas de cet article j’ai ajouté, par commodité, l’envoi d’une notification dans un CommandLineRunner
situé dans le point d’entrée du projet Spring Boot. Ici, j’émet toutes les trois secondes deux valeurs que je pousse ensuite vers le client.
/**
* Generate random numbers publish with WebSocket protocol each 3 seconds.
* @return a command line runner.
*/
@Bean
public CommandLineRunner websocketDemo() {
return (args) -> {
while (true) {
try {
Thread.sleep(3*1000); // Each 3 sec.
progress.put("num1", randomWithRange(0, 100));
progress.put("num2", randomWithRange(0, 100));
messagingTemplate.convertAndSend("/topic/progress", this.progress);
} catch (Exception e) {
e.printStackTrace();
}
}
};
}
Installation des WebSocket sous Angular
Vous avez besoin de deux composants dans votre application Angular pour utiliser les WebSocket de manière simplifiée :
npm i @stomp/ng2-stompjs --save
npm i @types/sockjs-client --save
Le premier composant est une librairie vous permettant de vous connecter à un Broker STOMP via le protocole WebSocket. C’est une surcharge du projet @stomp/stompjs permettant une meilleur compatibilité avec les projets Angular 6+. Le deuxième composant permet simplement d’injecter les définitions de type du client SockJs. Celui-ci est utilisé par @stomp/ng2-stompjs.
Vous devez ensuite fournir une initialisation du service depuis votre application Angular dans les providers du module.
import { RxStompService } from '@stomp/ng2-stompjs';
@NgModule({
[...]
providers: [
RxStompService
],
[...]
})
WebSocket Service
Nous avons maintenant besoin d’un service générique dédié à l’utilisation de WebSocket que l’on pourrait positionner ainsi dans notre projet Angular : src/app/services/websocket.service.ts
. Celui-ci prends en paramètre 3 éléments:
- Une injection du service
RxStompService
permettant l’initialisation d’accès au Broker de messagerie. - Une éventuelle configuration du service
RxStompService
via l’interfaceInjectableRxStompConfig
. - Une classe contenant d’éventuelles options supplémentaires, dont notamment le endpoint du broker.
import { InjectableRxStompConfig, RxStompService } from '@stomp/ng2-stompjs';
import { Observable } from 'rxjs';
import { SocketResponse, WebSocketOptions } from '../models';
/**
* A WebSocket service allowing subscription to a broker.
*/
export class WebSocketService {
private obsStompConnection: Observable<any>;
private subscribers: Array<any> = [];
private subscriberIndex = 0;
private stompConfig: InjectableRxStompConfig = {
heartbeatIncoming: 0,
heartbeatOutgoing: 20000,
reconnectDelay: 10000,
debug: (str) => { console.log(str); }
};
constructor(
private stompService: RxStompService,
private updatedStompConfig: InjectableRxStompConfig,
private options: WebSocketOptions
) {
// Update StompJs configuration.
this.stompConfig = {...this.stompConfig, ...this.updatedStompConfig};
// Initialise a list of possible subscribers.
this.createObservableSocket();
// Activate subscription to broker.
this.connect();
}
private createObservableSocket = () => {
this.obsStompConnection = new Observable(observer => {
const subscriberIndex = this.subscriberIndex++;
this.addToSubscribers({ index: subscriberIndex, observer });
return () => {
this.removeFromSubscribers(subscriberIndex);
};
});
}
private addToSubscribers = subscriber => {
this.subscribers.push(subscriber);
}
private removeFromSubscribers = index => {
for (let i = 0; i < this.subscribers.length; i++) {
if (i === index) {
this.subscribers.splice(i, 1);
break;
}
}
}
/**
* Connect and activate the client to the broker.
*/
private connect = () => {
this.stompService.stompClient.configure(this.stompConfig);
this.stompService.stompClient.onConnect = this.onSocketConnect;
this.stompService.stompClient.onStompError = this.onSocketError;
this.stompService.stompClient.activate();
}
/**
* On each connect / reconnect, we subscribe all broker clients.
*/
private onSocketConnect = frame => {
this.stompService.stompClient.subscribe(this.options.brokerEndpoint, this.socketListener);
}
private onSocketError = errorMsg => {
console.log('Broker reported error: ' + errorMsg);
const response: SocketResponse = {
type: 'ERROR',
message: errorMsg
};
this.subscribers.forEach(subscriber => {
subscriber.observer.error(response);
});
}
private socketListener = frame => {
this.subscribers.forEach(subscriber => {
subscriber.observer.next(this.getMessage(frame));
});
}
private getMessage = data => {
const response: SocketResponse = {
type: 'SUCCESS',
message: JSON.parse(data.body)
};
return response;
}
/**
* Return an observable containing a subscribers list to the broker.
*/
public getObservable = () => {
return this.obsStompConnection;
}
}
Ce service active à son initialisation un observable, createObservableSocket(),
regroupant la liste des éventuelles souscriptions au broker permettant l’envoi simultané à l’ensemble des souscrits des mises à jour du message émis par le serveur.
Le message émis peut ensuite être lu de n’importe où de la manière suivante :
const obs = this.progressWebsocketService.getObservable();
obs.subscribe({
next: this.onNewProgressMsg,
error: (err) => { console.log(err); }
});
Progress WebSocket Service
progressWebsocketService
est une classe héritant de WebSocketService
qui nous permet d’instancier un WebSocket personnalisé.
import { Injectable } from '@angular/core';
import { InjectableRxStompConfig, RxStompService } from '@stomp/ng2-stompjs';
import { WebSocketService } from '../websocket.service';
import { WebSocketOptions } from '../../models';
export const progressStompConfig: InjectableRxStompConfig = {
webSocketFactory: () => {
return new WebSocket('ws://localhost:8080/stomp');
}
};
@Injectable()
export class ProgressWebsocketService extends WebSocketService {
constructor(stompService: RxStompService) {
super(stompService, progressStompConfig, new WebSocketOptions('/topic/progress'));
}
}
Notre client va souscrire au endpoint « /topic/progress » de notre serveur pour écouter les notifications émises en temps réel. On remarque également l’initialisation de l’objet WebSocket
permettant l’utilisation du protocole avec le endpoint « ws://localhost:8080/stomp » correspondant au handshake entre le client et le serveur. Cela permet d’établir une connexion ouverte entre eux.
Pensez à rajouter votre service dans les providers de votre module Angular :
import { RxStompService } from '@stomp/ng2-stompjs';
import { ProgressWebsocketService } from './services/progress.websocket.service';
@NgModule({
[...]
providers: [
ProgressWebsocketService,
RxStompService
],
[...]
})
Conclusion
Nous avons vu dans cet article comment:
- Paramétrer notre serveur Java Spring Boot pour intégrer les WebSockets et émettre une notification.
- Ajouter un service générique utilisant le protocole WebSocket sous Angular
- Personnaliser un service pouvant souscrire à une notification serveur spécifique.
L’ensemble des sources du projet peut être téléchargé ici : websocket-with-angular.zip
Une répo GitHub est également disponible à cette adresse : Websocket-With-Angular
Références
- Projet GitHub @stomp/ng2-stompjs: https://github.com/stomp-js/ng2-stompjs
- Client WebSocket: https://developer.mozilla.org/fr/docs/Web/API/WebSockets_API/Writing_WebSocket_client_applications
36 commentaires
Cyrille P. · 6 août 2021 à 11 h 27 min
Bonjour Saida, Angular n’est pas nécessaire et n’est utilisé ici que pour afficher la page. Le service peut s’utiliser avec du vanilla JS et des appels ajax classiques.
Saida · 27 juillet 2021 à 15 h 05 min
Bonjours, s’il vous plait je suis entrain de développer une application avec spring boot et j’ai besoin d’un système de notification entre les utilisateurs dans mon app. devrai-je utiliser Angular aussi ou je peux le faire seulement avec spring boot et JS?
Merci d’avance
Cyrille P. · 12 avril 2021 à 9 h 48 min
Bonjour ! C’est plutôt simple à mettre en place. Quoi qu’il arrive, la configuration du broker reste la même, par contre il faut pouvoir émettre un message à un identifiant précis. Pour ça il faut rajouter dans
configureMessageBroker
la configuration suivante :registry.setUserDestinationPrefix("/user");
. Cela va permettre d’intercepter via l’annotation@MessageMapping
les messages qui pourrait arriver par/app/topic
et de le retourner à un utilisateur spécifique via la méthodemessagingTemplate.convertAndSendToUser()
. Tu as un très bon exemple d’utilisation à cette adresse : https://www.thetechplatform.com/post/building-persistable-one-to-one-chat-application-using-spring-boot-and-websocketsmootez · 9 avril 2021 à 23 h 17 min
salut, je veux avoir une communication one to one ? qu’est ce que je peux faire ?
Cyrille P. · 5 avril 2021 à 16 h 40 min
Bonjour Kenza, oui c’est un problème connu et tu en as la réponse un peu plus bas dans les commentaires.
kenza · 5 avril 2021 à 11 h 50 min
5: Argument of type ‘InjectableRxStompConfig’ is not assignable to parameter of type ‘StompConfig’.
Types of property ‘beforeConnect’ are incompatible.
Type ‘(client: RxStomp) => void | Promise’ is not assignable to type ‘() => void | Promise’.
jai eu cette erreur
Cyrille Perrot · 25 mai 2020 à 8 h 48 min
@Slim > regardes comment est configuré mon fichier
SecurityConfig.java
dans mon code de démo. Si tu utilises Spring Security pour sécuriser tes appels cela devrait te permettre d’avoir unhandshake
public.Slim · 23 mai 2020 à 5 h 02 min
Bonjour Cyrille,
J’ai une Application Spring Boot qui délivre deux Frontends : l’une est publique est l’une est privée (nécessite une authentification). Le problème est que j’utilise les WebSockets uniquement pour communiquer avec la partie publique et là Spring Security les bloquent.
Ce que j’ai compris est que le problème vient probablement du handshake qu’envoie le Front vers l’Application qui est une requête HTTP. Savez-vous donc comment configurer Spring Boot de telle sorte que l’application accepte la connexion des Websockets qui sont publiques ?
Merci d’avance!
Cyrille Perrot · 22 mai 2020 à 8 h 35 min
@Loran > Merci 🙂
Slim · 21 mai 2020 à 4 h 55 min
Je voulais dire [les WebSockets sont bidirectionnelles], petite faute ..
Sinon merci beaucoup pour l’article qui est très instructif ! 🙂
Bonne continuation
Loran · 20 mai 2020 à 15 h 45 min
Excellent tuto.
Bravo !
Cyrille Perrot · 20 mai 2020 à 9 h 15 min
Y a un article très bien documenté qui explique tous les types de connexion unidirectionnelle en HTTP: https://www.smashingmagazine.com/2018/02/sse-websockets-data-flow-http2/
L’auteur semble considérer que la meilleure approche serait d’utiliser le SSE. Concernant la sécurité, il est possible de protéger l’appel du
handshake
à l’aide de Spring Security (j’en parle un peu plus bas dans les commentaires).Slim · 20 mai 2020 à 6 h 17 min
Merci beaucoup pour votre temps.
Je viens juste de penser que les WebSockets sont unidirectionnelles. Or, je suis en train de développer une application où le serveur envoie les données vers le frontend c-à-d unidirectionnel ! Ici je pense (intuitivement) que les Websockets peuvent poser un gros problème de sécurité.. Est-ce le cas et si c’est un oui que me conseillerez vous d’utiliser à la place ?
Cyrille Perrot · 15 mai 2020 à 9 h 01 min
@Slim > Oui, c’est tout le principe. Ton API va recevoir deux demandes de handshake de deux applications différentes, donc de deux sessions différentes. C’est la même chose que si tu as plusieurs utilisateurs sur une même application, il y aura plusieurs appels. Fais le test en lançant l’interface sur deux ports différents.
Cyrille Perrot · 15 mai 2020 à 8 h 57 min
@Bassem > Ton URL ne semble pas mener à ton API java correctement. Tu utiliserais pas un proxy ou une appli intermédiaire pour tes appels ?
Slim · 14 mai 2020 à 6 h 05 min
Bonjour, j’ai une question qui est la suivante : Si on procède de la même manière que vous (ça marche parfaitement) et que par contre j’implémente la même WebSocket avec le même url dans deux FrontEnd différents, est-ce-que ça marchera quand même ?
Merci d’avance !
Bassem · 13 mai 2020 à 9 h 05 min
Bonjour, Merci bcp pour ce tuto,
Par contre j’ai cette erreur :
» WebSocket connection to ‘ws://localhost:8080/stomp’ failed: Error during WebSocket handshake: Unexpected response code: 200 »
une idée ?
Dylan Deleplanque · 12 mai 2020 à 14 h 34 min
Merci Cyrille pour le fix du Stomp config.
Il est toujours d’actualité.
Slim · 14 avril 2020 à 14 h 34 min
Ouffff.. Merci infiniment !!
Bon courage 🙂
Cyrille Perrot · 14 avril 2020 à 8 h 42 min
Oui, j’ai également détecté ce problème. Cela vient d’une mise à jour réalisée par l’auteur du composant
stomp-js/rx-stomp
. J’ai ouvert une issue sur son compte GitHub : https://github.com/stomp-js/rx-stomp/issues/207En attendant tu peux contourner le problème de cette manière en surchargeant la classe
InjectableRxStompConfig
avec la config suivante :Il te suffira ensuite de remplacer tous les appels à
InjectableRxStompConfig
avecFixedStompConfig
.Slim · 14 avril 2020 à 3 h 56 min
Bonjour, merci pour le tuto!!
Cependant j’ai un petit problème et j’ai besoin de votre aide, si possible.
Quand j’ai cloné le repo git tout a fonctionné à merveille.. mais quand j’ai essayé d’intégrer le code dans mon propre projet ça a créer un problème au niveau de websocket.service.ts [Argument of type ‘InjectableRxStompConfig’ is not assignable to parameter of type ‘StompConfig’.
Types of property ‘beforeConnect’ are incompatible.
Type ‘(client: RxStomp) => void | Promise’ is not assignable to type ‘() => void | Promise’.ts(2345)].
Merci d’avance!
simon · 3 avril 2020 à 10 h 14 min
J’utilise une authentification via Keycloak. Merci beaucoup pour la réponse et la rapidité.
Cyrille Perrot · 2 avril 2020 à 19 h 31 min
Oui c’est tout à fait possible. Dans le cas qui nous concerne, il suffit de jouer sur l’authentification des utilisateurs et d’utiliser Spring Security pour limiter l’accès au endpoint du handshake aux personnes authentifiées. Il suffira de rajouter manuellement depuis le front un token d’authentification (tout dépends du type de sécurité mis en place) en paramètre de la requête par exemple. Spring Security se chargera du reste. Il y a un très bon exemple sur le lien suivant :
https://www.baeldung.com/spring-security-websockets
simon · 2 avril 2020 à 16 h 46 min
Bonjour,
Merci beaucoup pour ce tutoriel très clair et bien fait.
Est-ce qu’il est compliqué de sécuriser, ou plutôt y-a-t-il un moyen de sécuriser l’url du handshake qui pourrait s’intégrer dans cette configuration ?
Merci beaucoup et bravo encore pour le tuto.
Cyrille Perrot · 1 avril 2020 à 10 h 09 min
L’état de l’art veut que tu utilises le corps du message pour émettre la notification. Le header ne doit être utilisé que si tu souhaites ajouter des meta données.
dylan deleplanque · 31 mars 2020 à 18 h 34 min
De mon côté, je l’ai récupéré dans le header de la SocketResponse. Est-ce une bonne pratique ?
dylan deleplanque · 31 mars 2020 à 18 h 28 min
Cela fonctionne en mettant une liste de WebSocketOptions.
Quel est la best practice pour récupérer la destination lorsque l’on fait le subscribe afin d’adapter le comportement en fonction de si la destination si c’est un Create ou Update ?
dylan deleplanque · 31 mars 2020 à 17 h 46 min
Tu veux dire changer le paramètre du constructeur de la class WebSocketService ?
Au lieu d’avoir un WebSocketOptions, avoir une liste de WebSocketOptions ?
Cyrille Perrot · 31 mars 2020 à 17 h 18 min
Heureusement 🙂
Il faudrait que je vois comment tu as structuré tes deux services. Mais dans les faits, as tu réellement besoin d’avoir deux websockets distincts pour ce que tu veux faire ? Je veux dire, à partir du moment que tu as établi un handshake avec l’API, il te suffit d’avoir plusieurs brokers et d’écouter le broker dédié à la création et l’autre à la mise à jour (Ou je n’ai pas exactement compris ce que tu veux faire).
dylan deleplanque · 31 mars 2020 à 15 h 20 min
Bonjour,
Même en ajoutant plusieurs endpoint, chaque websocket fonctionne de façon indépendante.
Par contre, côté front, si dans mon composant, je déclare plusieurs webSocketService :
constructor(private createTakeAwayWebsocketService: CreateTakeAwayWebsocketService, private updateTakeAwayStateWebSocketService: UpdateTakeAwayStateWebsocketService) {
}
seul la dernière webSocket (updateTakeAwayStateWebSocketService) fonctionne.
Cyrille Perrot · 30 mars 2020 à 17 h 58 min
Normalement oui, dans notre cas on est sur du STOMP messaging. Si tu regardes l’implémentation du
registry.addEndpoint
situé dans la méthoderegisterStompEndpoints
, tu remarqueras qu’il prends en paramètre un String varargs. Il est donc possible de définir plusieurs endpoints.dylan deleplanque · 30 mars 2020 à 17 h 34 min
Bonjour Cyrille,
J’avais une petite question, est-il possible de déclarer deux webSocket dans un même constructeur. J’ai essayé, mais seul le dernier fonctionne.
Dans le cas où c’est impossible, peut-on utiliser deux webSocket dans un même composant ?
Merci 🙂
Cyrille Perrot · 27 mars 2020 à 11 h 03 min
Bonjour Dylan ! Oui effectivement je suis allé un peu vite sur l’envoi d’une notif. Je vais suivre ton conseil.
dylan deleplanque · 26 mars 2020 à 19 h 13 min
Bonjour Cyrille,
Très bon tuto, le seul moment auquel j’ai dû aller regarder le code source c’est pour savoir où place le: « messagingTemplate.convertAndSend »,
Le tuto serait parfait si tu ajoutes ton controller. Mais sinon très très très bien.
Merci beaucoup, j’avais jamais essayer de mettre en place des websocket et j’ai pû le faire en quelques minutes.
Cyrille Perrot · 16 mars 2020 à 8 h 44 min
Merci, n’hésitez pas à me dire si vous avez eu des difficultés avec certaines parties de l’article ou si vous avez des remarques.
Abousalih · 13 mars 2020 à 20 h 52 min
Merci bcp,
Exemple trés trés simple à implémenter, Chapeau !!!!