Envío de mensaje a un usuario específico en Spring Websocket

85

¿Cómo enviar un mensaje websocket desde el servidor solo a un usuario específico?

Mi aplicación web tiene una configuración de seguridad de primavera y usa websocket. Me encuentro con un problema complicado al intentar enviar un mensaje desde el servidor solo a un usuario específico .

Mi comprensión de la lectura del manual es del servidor que podemos hacer

simpMessagingTemplate.convertAndSend("/user/{username}/reply", reply);

Y del lado del cliente:

stompClient.subscribe('/user/reply', handler);

Pero nunca pude invocar la devolución de llamada de suscripción. He probado muchos caminos diferentes pero no tuve suerte.

Si lo envío a / topic / reply , funciona, pero todos los demás usuarios conectados también lo recibirán.

Para ilustrar el problema, he creado este pequeño proyecto en github: https://github.com/gerrytan/wsproblem

Pasos para reproducir:

1) Clone y compile el proyecto (asegúrese de usar jdk 1.7 y maven 3.1)

$ git clone https://github.com/gerrytan/wsproblem.git
$ cd wsproblem
$ mvn jetty:run

2) Vaya a http://localhost:8080, inicie sesión usando bob / test o jim / test

3) Haga clic en "Solicitar mensaje específico del usuario". Esperado: se muestra un mensaje "hola {nombre de usuario}" junto a "Mensaje recibido solo para mí" solo para este usuario, Real: no se recibe nada

gerrytan
fuente
¿Ha estado mirando convertAndSendToUser (usuario de cadena, destino de cadena, mensaje T)? docs.spring.io/spring/docs/4.0.0.M3/javadoc-api/org/…
Viktor K.
Sí, lo intenté también, pero no
tuve
He estado trabajando en un proyecto privado y este es el método que usamos y nos está funcionando. Creo que un problema potencial podría ser que te suscribas a "/ user / reply" y estés enviando mensajes a "/ user / {username} / reply". Creo que deberías eliminar la parte {username} y usar convertAndSendToUser (usuario de cadena, destino de cadena, mensaje T).
Viktor K.
Gracias, pero lo intenté simpMessagingTemplate.convertAndSendToUser(principal.getName(), "/user/reply", reply);y cuando el mensaje se envía desde el servidor arroja esta excepciónjava.lang.IllegalArgumentException: Expected destination pattern "/principal/{userId}/**"
gerrytan
@ViktorK. tiene razón, y estuvo bastante cerca de la solución correcta. Su suscripción en el lado del cliente era correcta, simplemente tenía que probar:convertAndSendToUser(principal.getName(), "/reply", reply);
Tip-Sy

Respuestas:

81

Oh, el client side no need to known about current userservidor lo hará por ti.

En el lado del servidor, use la siguiente forma para enviar un mensaje a un usuario:

simpMessagingTemplate.convertAndSendToUser(username, "/queue/reply", message);

Nota: Usando queue, no topic, Spring siempre usando queueconsendToUser

Del lado del cliente

stompClient.subscribe("/user/queue/reply", handler);

Explique

Cuando cualquier conexión websocket está abierta, Spring le asignará un session id(no HttpSession, asignar por conexión). Y cuando su cliente se suscribe a un canal comienza con /user/, por ejemplo:, /user/queue/replysu instancia de servidor se suscribirá a una cola llamadaqueue/reply-user[session id]

Cuando use enviar mensaje al usuario, por ejemplo: el nombre de usuario es admin Usted escribirásimpMessagingTemplate.convertAndSendToUser("admin", "/queue/reply", message);

Spring determinará cuál se session idasignó al usuario admin. Por ejemplo: encontró dos sesiones wsxedc123y thnujm456, Spring lo traducirá a 2 destinos queue/reply-userwsxedc123y queue/reply-userthnujm456, y enviará su mensaje con 2 destinos a su agente de mensajes.

El intermediario de mensajes recibe los mensajes y los devuelve a la instancia de su servidor que tiene la sesión correspondiente a cada sesión (las sesiones de WebSocket pueden ser retenidas por uno o más servidores). Spring traducirá el mensaje a destination(ej . user/queue/reply:) y session id(ej wsxedc123.:). Luego, envía el mensaje al correspondienteWebsocket session

Thanh Nguyen Van
fuente
7
¿Puede explicar cómo sabe el nombre de usuario? en su ejemplo, dijo que el nombre de usuario es 'admin', ¿recibe el nombre de usuario del usuario?
Jorj
1
El nombre de usuario se obtuvo HttpSessioncuando se inició una conexión websocket
Thanh Nguyen Van
1
¿Podría darnos más información sobre cómo configurar el nombre de usuario? ¿De todos modos puedo enviar el nombre de usuario en el mensaje de suscripción?
Andrés
1
@Andres: Puede extender DefaultHandshakeHandlery anular el métododetermineUser
Thanh Nguyen Van
1
Tu explicación funciona muy bien. Sin embargo, ¿dónde está la queue/reply-user[session id]parte en el documento oficial?
hbrls
35

Ah, encontré cuál era mi problema. Primero no registré el /userprefijo en el corredor simple

<websocket:simple-broker prefix="/topic,/user" />

Entonces no necesito el /userprefijo adicional al enviar:

convertAndSendToUser(principal.getName(), "/reply", reply);

Spring se antepondrá automáticamente "/user/" + principal.getName()al destino, por lo que se resuelve en "/ user / bob / reply".

Esto también significa que en javascript tuve que suscribirme a diferentes direcciones por usuario

stompClient.subscribe('/user/' + userName + '/reply,...) 
gerrytan
fuente
3
Mmm ... ¿Hay alguna manera de evitar configurar el nombre de usuario del lado del cliente? Al cambiar ese valor (por ejemplo, usando otro nombre de usuario), podrá ver los mensajes de otras personas.
vdenotaris
2
Vea mi solución: stackoverflow.com/questions/25646671/…
vdenotaris
¿Cómo suscribirse para responder al usuario de los métodos anotados con @RequestMappingy también @MessageMappingver aquí Cómo suscribirse usando la integración de Sping Websocket en un nombre de usuario específico (userId) + recibir notificaciones del método anotado con @RequestMapping?
Shantaram Tupe
Oye amigo mío, ¿puedes decirme esto? ¿Qué es este "usuario" de todos modos? Estoy usando Spring Security y mis usuarios (nombre de usuario) son direcciones de correo electrónico. Cuando cada usuario inicia sesión, obtiene su token JWT en Spring Security.
Francisco Souza
Enfrenté los mismos problemas, busqué durante horas antes de llegar a su respuesta. SÓLO tu explicación me ayudó. ¡¡Muchas gracias!!
Navaneeth
3

También creé un proyecto de websocket de muestra usando STOMP. Lo que estoy notando es que

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {

@Override
public void configureMessageBroker(MessageBrokerRegistry config) {
    config.enableSimpleBroker("/topic", "/queue");// including /user also works
    config.setApplicationDestinationPrefixes("/app");
}

@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
    registry.addEndpoint("/getfeeds").withSockJS();
}

}

funciona tanto si "/ user" está incluido en config.enableSimpleBroker (...

Kans
fuente
2

Mi solución de eso se basó en la mejor explicación de Thanh Nguyen Van, pero además he configurado MessageBrokerRegistry:

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {

    @Override
    public void configureMessageBroker(MessageBrokerRegistry config) {
        config.enableSimpleBroker("/queue/", "/topic/");
        ...
    }
    ...
}
Nikolay Shabak
fuente
2

Exactamente hice lo mismo y está funcionando sin usar el usuario.

@Configuration
@EnableWebSocketMessageBroker  
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
       registry.addEndpoint("/gs-guide-websocket").withSockJS();
    }

    @Override
    public void configureMessageBroker(MessageBrokerRegistry config) {
        config.enableSimpleBroker("/topic" , "/queue");
        config.setApplicationDestinationPrefixes("/app");
    }
}
Sid
fuente
Oye, ¿dónde usas esto /app? ¿En ese caso?
Francisco Souza