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 de setAllowedOrigins("*") 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’interface InjectableRxStompConfig.
  • 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


30 commentaires

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 un handshake 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/207

En attendant tu peux contourner le problème de cette manière en surchargeant la classe InjectableRxStompConfig avec la config suivante :

import { InjectableRxStompConfig } from '@stomp/ng2-stompjs';

/**
 * Fix annoying TS2345 error when injecting InjectableRxStompConfig into
 * RxStomp.stompClient.configure method who don't need the rxStomp
 * configuration.
 */
export class FixedStompConfig extends InjectableRxStompConfig {
  constructor() {
    super();
  }

  beforeConnect?: () => void | Promise;
}

Il te suffira ensuite de remplacer tous les appels à InjectableRxStompConfig avec FixedStompConfig.

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

Même en ajoutant plusieurs endpoint, chaque websocket fonctionne de façon indépendante.

Heureusement 🙂

seul la dernière webSocket (updateTakeAwayStateWebSocketService) fonctionne.

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éthode registerStompEndpoints, 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 !!!!

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Ce site utilise Akismet pour réduire les indésirables. En savoir plus sur comment les données de vos commentaires sont utilisées.

Utilisation des WebSockets sous Angular avec Java Spring Boot

par Cyrille Perrot temps de lecture : 6 min
30