J'utilise Spring Websocket avec STOMP, Simple Message Broker. Dans mon @Controller
J'utilise au niveau de la méthode @SubscribeMapping
, Qui devrait abonner le client à un sujet afin que le client reçoive ensuite les messages de ce sujet. Disons que le client s'abonne au sujet "chat":
stompClient.subscribe('/app/chat', ...);
Comme le client s'est abonné à "/ app/chat", au lieu de "/ topic/chat", cet abonnement irait à la méthode qui est mappée en utilisant @SubscribeMapping
:
@SubscribeMapping("/chat")
public List getChatInit() {
return Chat.getUsers();
}
Voici ce que Spring ref. dit:
Par défaut, la valeur de retour d'une méthode @SubscribeMapping est envoyée sous forme de message directement au client connecté et ne passe pas par le courtier. Ceci est utile pour implémenter des interactions de message de demande-réponse; par exemple, pour récupérer les données d'application lorsque l'interface utilisateur de l'application est en cours d'initialisation.
D'accord, c'est ce que je voudrais, mais juste partiellement !! Envoi de données init après la souscription, eh bien. Mais qu'en est-il de abonnement? Il me semble que ce qui s'est passé ici est juste un demande-réponse, comme un service. L'abonnement est juste consommé. Veuillez me préciser si tel est le cas.
Si ici, le client n'est abonné à aucun endroit, je me demande pourquoi nous appelons cela "souscrire"; car le client ne reçoit qu'un seul message et non de futurs messages.
MODIFIER:
Pour vous assurer que l'abonnement a été réalisé, ce que j'ai essayé est le suivant:
côté SERVEUR:
Configuration:
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {
@Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.enableSimpleBroker("/topic");
config.setApplicationDestinationPrefixes("/app");
}
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/hello").withSockJS();
}
}
Contrôleur:
@Controller
public class GreetingController {
@MessageMapping("/hello")
@SendTo("/topic/greetings")
public Greeting greeting(HelloMessage message) throws Exception {
System.out.println("inside greeting");
return new Greeting("Hello, " + message.getName() + "!");
}
@SubscribeMapping("/topic/greetings")
public Greeting try1() {
System.out.println("inside TRY 1");
return new Greeting("Hello, " + "TRY 1" + "!");
}
}
côté client:
...
stompClient.subscribe('/topic/greetings', function(greeting){
console.log('RECEIVED !!!');
});
stompClient.send("/app/hello", {}, JSON.stringify({ 'name': name }));
...
Ce que j'aimerais arriver:
/topic/greetings
', La méthode try1
Est exécutée./app/hello
', Il doit recevoir le message de bienvenue qui serait @SendTo
'/topic/greetings
'.Résultats:
Si le client s'abonne à /topic/greetings
, La méthode try1
Ne peut pas l'attraper.
Lorsque le client envoie un msg à '/app/hello
', La méthode greeting
a été exécutée et le client a reçu le message de bienvenue. Nous avons donc compris qu'il avait été correctement souscrit à "/topic/greetings
".
Mais rappelez-vous que 1. a échoué. Après quelques essais, cela a été possible lorsque le client s'est abonné à '/app/topic/greetings'
, C'est-à-dire préfixé avec /app
(Ceci est compréhensible par la configuration).
Maintenant 1. fonctionne, mais cette fois 2. échoue: lorsque le client envoie un message à '/app/hello
', Oui, la méthode greeting
a été exécutée, mais le client n'a PAS reçu le message de salutations . (Parce que probablement maintenant le client était abonné au sujet avec le préfixe '/app
', Ce qui n'était pas souhaité.)
Donc, ce que j'ai obtenu, c'est 1 ou 2 de ce que j'aimerais, mais pas ces 2 ensemble.
Par défaut, la valeur de retour d'une méthode @SubscribeMapping est envoyée sous forme de message directement au client connecté et ne pas passer par le courtier .
(c'est moi qui souligne)
Ici, la documentation de Spring Framework décrit ce qui se passe avec le message de réponse , pas le message SUBSCRIBE
entrant.
Alors pour répondre à vos questions:
Avec le SimpleMessageBroker
, l'implémentation du courtier de messages réside dans votre instance d'application. Les inscriptions d'abonnement sont gérées par le DefaultSubscriptionRegistry
. Lors de la réception des messages, SimpleBrokerMessageHandler
gère SUBSCRIPTION
messages et enregistre les abonnements ( voir implémentation ici ).
Avec un "vrai" courtier de messages comme RabbitMQ, vous avez configuré un relais de courtier Stomp qui transmet les messages au courtier. Dans ce cas, les messages SUBSCRIBE
sont transmis au courtier, en charge de la gestion des abonnements ( voir implémentation ici ).
Si vous jetez un oeil à la documentation de référence sur le flux de messages STOMP , vous verrez que:
- Les abonnements à "/ topic/salutation" passent par le "clientInboundChannel" et sont transmis au courtier
- Les messages d'accueil envoyés à "/ app/Message d'accueil" passent par le "clientInboundChannel" et sont transmis au GreetingController. Le contrôleur ajoute l'heure actuelle et la valeur de retour est transmise via le "brokerChannel" en tant que message à "/ topic/salutation" (la destination est sélectionnée en fonction d'une convention mais peut être remplacée via @SendTo).
Alors ici, /topic/hello
est une destination de courtier; les messages qui y sont envoyés sont directement transmis au courtier. Tandis que /app/hello
est une destination d'application et est censé produire un message à envoyer à /topic/hello
, sauf si @SendTo
dit le contraire.
Maintenant, votre question mise à jour est en quelque sorte différente, et sans un cas d'utilisation plus précis, il est difficile de dire quel modèle est le meilleur pour résoudre ce problème. Voici quelques-uns:
/topic/hello
/topic/hello
/app/hello
avec un contrôleur qui répond tout de suite avec un message/app/hello
: utilisez une combinaison de @MessageMapping
, @SendTo
ou un modèle de messagerie.Si vous voulez un bon exemple, alors consultez cette application de chat montrant un journal des fonctionnalités de Websocket Spring avec un cas d'utilisation réel .
Donc, avoir les deux:
ne fonctionne pas comme vous l'avez vécu (ainsi que moi).
La façon de résoudre votre situation (comme je l'ai fait la mienne) est:
Implémenter un ApplicationListener
Si vous souhaitez répondre directement à un seul client, utilisez une destination utilisateur (voir websocket-stomp-user-destination ou vous pouvez également vous abonner à un sous-chemin d'accès, par exemple/topic/my-id-42, puis vous pouvez envoyer un message à ce sous-sujet (je ne connais pas votre cas d'utilisation exact, le mien est que j'ai des abonnements dédiés et je les répète si je veux faire un broadcast)
Envoyer un message dans votre méthode onApplicationEvent de ApplicationListener dès que vous recevez un StompCommand.SUBSCRIBE
Gestionnaire d'événements d'abonnement:
@Override
public void onApplicationEvent(SessionSubscribeEvent event) {
Message<byte[]> message = event.getMessage();
StompHeaderAccessor accessor = StompHeaderAccessor.wrap(message);
StompCommand command = accessor.getCommand();
if (command.equals(StompCommand.SUBSCRIBE)) {
String sessionId = accessor.getSessionId();
String stompSubscriptionId = accessor.getSubscriptionId();
String destination = accessor.getDestination();
// Handle subscription event here
// e.g. send welcome message to *destination*
}
}
J'ai fait face au même problème et j'ai finalement basculé vers la solution lorsque je m'abonne aux deux /topic
et /app
sur un client, mettant en mémoire tampon tout ce qui a été reçu sur /topic
gestionnaire jusqu'à /app
- lié va télécharger tout l'historique du chat, c'est ce que @SubscribeMapping
Retour. Ensuite, je fusionne toutes les entrées de discussion récentes avec celles reçues sur un /topic
- il pourrait y avoir des doublons dans mon cas.
Une autre approche de travail consistait à déclarer
registry.enableSimpleBroker("/app", "/topic");
registry.setApplicationDestinationPrefixes("/app", "/topic");
Évidemment, pas parfait. Mais a fonctionné :)
Salut Mert, bien que votre question soit posée il y a plus de 4 ans, mais j'essaierai toujours d'y répondre car je me suis gratté la tête sur le même problème récemment et l'ai finalement résolu.
L'élément clé ici est @SubscribeMapping
Est un échange de demande-réponse unique , donc la méthode try1()
dans votre contrôleur ne sera déclenché qu'une seule fois juste après l'exécution des codes client
stompClient.subscribe('/topic/greetings', callback)
après cela, il n'y a aucun moyen de déclencher try1()
par stompClient.send(...)
Un autre problème ici est que le contrôleur fait partie du gestionnaire de messages d'application, qui reçoit la destination avec le préfixe /app
Déchiré, donc pour atteindre @SubscribeMapping("/topic/greetings")
vous devez réellement écrire du code client comme celui-ci
stompClient.subscribe('/app/topic/greetings', callback)
puisque conventionnellement topic
est mappé avec des courtiers pour éviter toute ambiguïté, je recommande de modifier votre code en
@SubscribeMapping("/greetings")
stompClient.subscribe('/app/greetings', callback)
et maintenant console.log('RECEIVED !!!')
devrait fonctionner.
Le document officiel recommande également le scénario de cas d'utilisation de @SubscribeMapping
Lors du rendu initial de l'interface utilisateur.
Quand est-ce utile? Supposons que le courtier est mappé sur/topic et/queue, tandis que les contrôleurs d'application sont mappés sur/app. Dans cette configuration, le courtier stocke tous les abonnements à/topic et/queue qui sont destinés à des diffusions répétées, et l'application n'a pas besoin de s'impliquer. Un client peut également s'abonner à une destination/app, et un contrôleur peut renvoyer une valeur en réponse à cet abonnement sans impliquer le courtier sans stocker ou réutiliser l'abonnement (en fait, un échange de demande-réponse unique). Un cas d'utilisation pour cela est de remplir une interface utilisateur avec des données initiales au démarrage.
Ce n'est peut-être pas totalement lié, mais lorsque je m'abonnais à "app/test", il était impossible de recevoir des messages envoyés à "app/test".
J'ai donc trouvé que l'ajout d'un courtier était le problème (je ne sais pas pourquoi btw).
Voici donc mon code avant:
@Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.setApplicationDestinationPrefixes("/app");
config.enableSimpleBroker("/topic");
}
Après :
@Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.setApplicationDestinationPrefixes("/app");
// problem line deleted
}
Maintenant, quand je m'abonne à "app/test", cela fonctionne:
template.convertAndSend("/app/test", stringSample);
Dans mon cas, je n'en ai pas besoin de plus.