starter - ¿Spring @SubscribeMapping realmente suscribe al cliente a algún tema?
websocket spring boot angular 4 (3)
Estoy usando Spring Websocket con STOMP, Simple Message Broker. En mi @Controller
utilizo @SubscribeMapping
nivel de @SubscribeMapping
, que debería suscribir al cliente a un tema para que luego reciba los mensajes de ese tema. Digamos que el cliente se suscribe al tema "chat" :
stompClient.subscribe(''/app/chat'', ...);
Como el cliente se suscribió a "/ app / chat ", en lugar de "/ topic / chat" , esta suscripción irá al método que se asigna mediante @SubscribeMapping
:
@SubscribeMapping("/chat")
public List getChatInit() {
return Chat.getUsers();
}
Esto es lo que la primavera ref. dice:
De forma predeterminada, el valor de retorno de un método @SubscribeMapping se envía como un mensaje directamente al cliente conectado y no pasa por el intermediario. Esto es útil para implementar interacciones de mensajes de solicitud-respuesta; por ejemplo, para obtener datos de la aplicación cuando se está inicializando la IU de la aplicación.
De acuerdo, esto era lo que querría, ¡pero solo parcialmente ! Enviando algunos datos de inicio después de suscribirse, bueno. Pero ¿qué pasa con la suscripción ? Me parece que lo que sucedió aquí es solo una solicitud de respuesta , como un servicio. La suscripción se acaba de consumir. Por favor aclarame si este es el caso.
- ¿Se suscribió el cliente a algún lugar, si el corredor no está involucrado en esto?
- Si luego deseo enviar algún mensaje a los subcriptores de "chat", ¿lo recibirá el cliente? No lo parece.
- ¿Quién realiza suscripciones realmente? ¿Corredor? ¿O alguien más?
Si aquí el cliente no se está suscribiendo a ninguna parte, me pregunto por qué llamamos a esto como "suscribir"; Porque el cliente recibe solo un mensaje y no mensajes futuros.
EDITAR:
Para asegurarme de que la suscripción se haya realizado, lo que intenté es lo siguiente:
Lado del servidor:
Configuración:
@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();
}
}
Controlador:
@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" + "!");
}
}
Lado del cliente:
...
stompClient.subscribe(''/topic/greetings'', function(greeting){
console.log(''RECEIVED !!!'');
});
stompClient.send("/app/hello", {}, JSON.stringify({ ''name'': name }));
...
Lo que me gustaría que sucediera:
- Cuando el cliente se suscribe a ''
/topic/greetings
'', se ejecuta el métodotry1
. - Cuando el cliente envía msg a ''
/app/hello
'', debería recibir el mensaje de saludos que sería@SendTo
''/topic/greetings
''.
Resultados:
Si el cliente se suscribe a
/topic/greetings
, el métodotry1
NO PUEDE capturarlo.Cuando el cliente envía msg a ''
/app/hello
'', se ejecutó el método degreeting
y el cliente recibió el mensaje de saludo. Así que entendimos que se había suscrito correctamente a ''/topic/greetings
''.Pero recuerda 1. fue un fracaso. Después de algunos intentos, ha sido posible cuando el cliente se suscribió a
''/app/topic/greetings''
, es decir, con el prefijo/app
(Esto es comprensible por la configuración).Ahora 1. está funcionando, sin embargo, esta vez 2. falla: cuando el cliente envía msg a ''
/app/hello
'', sí, se ejecutó el método degreeting
, pero el cliente NO recibió el mensaje de saludo. (Porque probablemente ahora el cliente estaba suscrito al tema prefijado con ''/app
'', que no era deseado).
Entonces, lo que obtuve es 1 o 2 de lo que me gustaría, pero no estos 2 juntos.
- ¿Cómo logro esto con esta estructura (configurando correctamente las rutas de mapeo)?
De forma predeterminada, el valor de retorno de un método @SubscribeMapping se envía como un mensaje directamente al cliente conectado y no pasa por el intermediario .
(énfasis mío)
Aquí, la documentación de Spring Framework describe lo que sucede con el mensaje de respuesta , no el mensaje SUBSCRIBE
entrante.
Así que para responder a sus preguntas:
- Sí, el cliente está suscrito al tema.
- sí, los clientes suscritos a ese tema recibirán un mensaje si utiliza ese tema para enviarlo
- El intermediario de mensajes se encarga de gestionar las suscripciones.
Más sobre la gestión de suscripciones
Con SimpleMessageBroker
, la implementación del intermediario de mensajes se encuentra en su instancia de aplicación. Los registros de suscripción son gestionados por DefaultSubscriptionRegistry
. Al recibir mensajes, el SimpleBrokerMessageHandler
maneja los mensajes de SUBSCRIPTION
y registra las suscripciones ( vea la implementación aquí ).
Con un intermediario de mensajes "real" como RabbitMQ, ha configurado un relé de intermediario Stomp que reenvía los mensajes al intermediario. En ese caso, los mensajes de SUBSCRIBE
se reenvían al intermediario, a cargo de administrar las suscripciones ( consulte la implementación aquí ).
Actualización - más sobre el flujo de mensajes STOMP
Si echa un vistazo a la documentación de referencia sobre el flujo de mensajes STOMP , verá que:
- Las suscripciones a "/ topic / greeting" pasan a través de "clientInboundChannel" y se envían al agente
- Los saludos enviados a "/ app / greeting" pasan a través de "clientInboundChannel" y se envían al GreetingController. El controlador agrega la hora actual, y el valor de retorno se pasa a través de "brokerChannel" como un mensaje a "/ topic / greeting" (el destino se selecciona según una convención, pero se puede invalidar mediante @SendTo).
Así que aquí, /topic/hello
es un destino de intermediario; Los mensajes enviados allí se reenvían directamente al agente. Mientras que /app/hello
es un destino de la aplicación, y se supone que produce un mensaje para enviarlo a /topic/hello
, a menos que @SendTo
diga lo contrario.
Ahora su pregunta actualizada es de alguna manera diferente, y sin un caso de uso más preciso, es difícil decir qué patrón es el mejor para resolver esto. Aquí hay algunos:
- desea que el cliente tenga en cuenta cada vez que suceda algo, de forma asíncrona: SUSCRÍBASE a un tema en particular
/topic/hello
- desea transmitir un mensaje: envíe un mensaje a un tema en particular
/topic/hello
- desea obtener comentarios inmediatos sobre algo, por ejemplo, para inicializar el estado de su aplicación: SUSCRÍBASE a un destino
/app/hello
con un controlador que responde con un mensaje de inmediato - desea enviar uno o más mensajes a cualquier destino de aplicación
/app/hello
: use una combinación de@MessageMapping
,@SendTo
o una plantilla de mensajería.
Si desea un buen ejemplo, eche un vistazo a esta aplicación de chat que muestra un registro de las características de websocket de Spring con un caso de uso real .
Entonces teniendo ambos:
- Usando un tema para manejar la suscripción
- Uso de @SubscribeMapping en ese tema para entregar una respuesta de conexión
no funciona como lo experimentaste (tan bien como yo).
La forma de resolver tu situación (como hice con la mía) es:
- Elimine el @SubscribeMapping - solo funciona con el prefijo / app
- Suscríbase al tema / tal como lo haría naturalmente (sin el prefijo / app)
Implementar un ApplicationListener
Si desea responder directamente a un solo cliente, use un destino de usuario (consulte websocket-stomp-user-destination o también puede suscribirse a una ruta secundaria, por ejemplo, / topic / my-id-42, entonces puede enviar un mensaje a este Subtema (no sé cuál es su caso de uso exacto, el mío es que tengo suscripciones dedicadas y hago una iteración sobre ellas si quiero hacer una transmisión )
Envíe un mensaje en su método onApplicationEvent de ApplicationListener tan pronto como reciba un StompCommand.SUBSCRIBE
Gestor de eventos de suscripción:
@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*
}
}
Me enfrenté con el mismo problema, y finalmente cambié a solución cuando me suscribí a ambos /topic
y /app
en un cliente, almacenando en búfer todo lo recibido en el controlador de /topic
hasta que /app
-bound se descargue todo el historial de chat, eso es lo que @SubscribeMapping
devuelve. Luego fusiono todas las entradas de chat recientes con las recibidas en un /topic
, podría haber duplicados en mi caso.
Otro enfoque de trabajo fue declarar
registry.enableSimpleBroker("/app", "/topic");
registry.setApplicationDestinationPrefixes("/app", "/topic");
Obviamente, no es perfecto. Pero funcionó :)