How to send websocket message from server to specific user only?
My webapp has spring security setup and uses websocket. I'm encountering tricky problem trying to send message from server to specific user only.
My understanding from reading the manual is from the server we can do
simpMessagingTemplate.convertAndSend("/user/{username}/reply", reply);
And on the client side:
stompClient.subscribe('/user/reply', handler);
But I could never get the subscription callback invoked. I have tried many different path but no luck.
If I send it to /topic/reply it works but all other connected users will receive it too.
To illustrate the problem I've created this small project on github: https://github.com/gerrytan/wsproblem
Steps to reproduce:
1) Clone and build the project (make sure you're using jdk 1.7 and maven 3.1)
$ git clone https://github.com/gerrytan/wsproblem.git
$ cd wsproblem
$ mvn jetty:run
2) Navigate to http://localhost:8080
, login using either bob/test or jim/test
3) Click "Request user specific msg". Expected: a message "hello {username}" is displayed next to "Received Message To Me Only" for this user only, Actual: nothing is received
With WebSockets: the server can send a message to the client without the client explicitly requesting something. the client and the server can talk to each other simultaneously. very little data overhead needs to be exchanged to send messages.
To send a message through the WebSocket connection you call the send() method on your WebSocket instance; passing in the data you want to transfer. socket. send(data); You can send both text and binary data through a WebSocket.
Websocket client connections may drop due to intermittent network issue and when connections drop, messages will also be lost.
Oh, client side no need to known about current user
, server will do that for you.
On server side, using following way to send message to an user:
simpMessagingTemplate.convertAndSendToUser(username, "/queue/reply", message);
Note: Using queue
, not topic
, Spring always using queue
with sendToUser
On client side
stompClient.subscribe("/user/queue/reply", handler);
Explain
When any websocket connection is open, Spring will assign it a session id
(not HttpSession
, assign per connection). And when your client subscribe to an channel start with /user/
, eg: /user/queue/reply
, your server instance will subscribe to a queue named queue/reply-user[session id]
When use send message to user, eg: username is admin
You will write simpMessagingTemplate.convertAndSendToUser("admin", "/queue/reply", message);
Spring will determine which session id
mapped to user admin
. Eg: It found two session wsxedc123
and thnujm456
, Spring will translate it to 2 destination queue/reply-userwsxedc123
and queue/reply-userthnujm456
, and it send your message with 2 destinations to your message broker.
The message broker receive the messages and provide it back to your server instance that holding session corresponding to each session (WebSocket sessions can be hold by one or more server). Spring will translate the message to destination
(eg: user/queue/reply
) and session id
(eg: wsxedc123
). Then, it send the message to corresponding Websocket session
Ah I found what my problem was. First I didn't register the /user
prefix on the simple broker
<websocket:simple-broker prefix="/topic,/user" />
Then I don't need the extra /user
prefix when sending:
convertAndSendToUser(principal.getName(), "/reply", reply);
Spring will automatically prepend "/user/" + principal.getName()
to the destination, hence it resolves into "/user/bob/reply".
This also means in javascript I had to subscribe to different address per user
stompClient.subscribe('/user/' + userName + '/reply,...)
I created a sample websocket project using STOMP as well. What I am noticing is that
@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();
}
it works whether or not "/user" is included in config.enableSimpleBroker(...
My solution of that based on Thanh Nguyen Van's best explanation, but in addition I have configured MessageBrokerRegistry:
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {
@Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.enableSimpleBroker("/queue/", "/topic/");
...
}
...
}
Exactly i did the same and it is working without using user
@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");
}
}
In the following solution, I wrote code snippets for both the client and backend sides. We need to put /user
at the start of the socket's topic for client code. Otherwise, the client can not listen to the socket.
Dependency
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
WebSocketConfig.java
package com.oktaykcr.notificationservice.config;
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 WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/socket").setAllowedOriginPatterns("*");
registry.addEndpoint("/socket").setAllowedOriginPatterns("*").withSockJS();
}
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.enableSimpleBroker("/file");
registry.setApplicationDestinationPrefixes("/app");
}
}
WebSocketContoller.java
package com.oktaykcr.notificationservice.controller;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.Payload;
import org.springframework.messaging.handler.annotation.SendTo;
import org.springframework.stereotype.Controller;
@Controller
public class WebSocketController {
Logger logger = LoggerFactory.getLogger(WebSocketController.class);
@MessageMapping("/socket")
@SendTo("/file/status")
public String fileStatus(@Payload String message) {
logger.info(message);
return message;
}
}
You can send message to socket from anywhere. In my case my principal.getName() is equal to userId.
simpMessagingTemplate.convertAndSendToUser(userId, "/file/status", socketMessage);
Client (ReactJs with react-stomp) App.js
import './App.css';
import SockJsClient from 'react-stomp'
import { useRef } from 'react';
function App() {
const clientRef = useRef();
const sendMessage = (msg) => {
clientRef.current.sendMessage('/app/socket', msg);
}
return (
<div className="App">
<div>
<button onClick={() => sendMessage("Hola")}>Send</button>
</div>
<SockJsClient url='http://localhost:9090/notification-service/socket' topics={['/user/file/status']}
onMessage={(msg) => { console.log(msg); }}
ref={(client) => { clientRef.current = client }} />
</div>
);
}
export default App;
The url attribute of the SockJsClient element is http://localhost:9090/notification-service/socket
. http://localhost:9090
is Api Gateway ip address, notification-service
is name of the microservice and /socket
is defined in the WebSocketConfig.java.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With