Send notification to offline user with persistent queue STOMP - spring

I have this code: Client-side with javascript:
socket = new SockJS(context.backend + '/myWebSocketEndPoint');
stompClient = Stomp.over(socket);
stompClient.connect({},function (frame) {
stompClient.subscribe('/queue/'+clientId+'/notification', function(response){
alert(angular.fromJson(response.body));
});
});
In this code, a client when connects, subscribe to receive notification using '/queue/'+ his client id + '/notification/. So i have a queue for every client. I use stomp with sockjs
In my server (Java + spring boot) i have a notification listener which when an event is published, it send a notification to all clients. So i have:
#Configuration
#EnableWebSocketMessageBroker
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer{
#Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.enableSimpleBroker("/queue");
}
#Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/myWebSocketEndPoint")
.setAllowedOrigins("*")
.withSockJS();
}
}
the class MenuItemNotificationChannel who call MenuItemNotificationSender to send the notification to the users.
#Component
public class MenuItemNotificationChannel extends AbstractNotificationChannel {
#Autowired
private MenuItemNotificationSender menuItemNotificationSender;
#Autowired
private UserRepository userRepository;
#Override
public void sendNotification(KitaiEvent<?> event, Map<String, Object> notificationConfiguration) throws Exception {
String menuItem = Optional.ofNullable((String) notificationConfiguration.get(MENU_ENTRY_KEY)).orElseThrow(IllegalArgumentException::new);
List<User> userList = userRepository.findAll();
for(User u: userList){
menuItemNotificationSender.sendNotification(new MenuItemDto(menuItem),u.getId());
}
MenuItemNotificationSender class is:
#Component
public class MenuItemNotificationSender {
#Autowired
private SimpMessagingTemplate messagingTemplate;
#Autowired
public MenuItemNotificationSender(SimpMessagingTemplate messagingTemplate){
this.messagingTemplate = messagingTemplate;
}
public void sendNotification(MenuItemDto menuItem,Long id) {
String address = "/queue/"+id+"/notification";
messagingTemplate.convertAndSend(address, menuItem);
}
}
This code works perfectly: notifications are sent to every user. But if a user is not online, notifications are losts. My questions are:
How can i verify whit stomp what subscriptions are active and what are not?? (If i can verify if a subscription is active, i solve my problem because i save notification for users offline and then send them when they do login)
Can i use persistent queues? (i read something about it, but i have not understand if i can use it only with stomp and sockjs)
Sorry for my english! :D

You can put a spring event listener on the session connected event and the session disconnect event
I tested this one with spring 4.3.4
#Component
public class WebSocketSessionListener
{
private static final Logger logger = LoggerFactory.getLogger(WebSocketSessionListener.class.getName());
private List<String> connectedClientId = new ArrayList<String>();
#EventListener
public void connectionEstablished(SessionConnectedEvent sce)
{
MessageHeaders msgHeaders = sce.getMessage().getHeaders();
Principal princ = (Principal) msgHeaders.get("simpUser");
StompHeaderAccessor sha = StompHeaderAccessor.wrap(sce.getMessage());
List<String> nativeHeaders = sha.getNativeHeader("userId");
if( nativeHeaders != null )
{
String userId = nativeHeaders.get(0);
connectedClientId.add(userId);
if( logger.isDebugEnabled() )
{
logger.debug("Connessione websocket stabilita. ID Utente "+userId);
}
}
else
{
String userId = princ.getName();
connectedClientId.add(userId);
if( logger.isDebugEnabled() )
{
logger.debug("Connessione websocket stabilita. ID Utente "+userId);
}
}
}
#EventListener
public void webSockectDisconnect(SessionDisconnectEvent sde)
{
MessageHeaders msgHeaders = sde.getMessage().getHeaders();
Principal princ = (Principal) msgHeaders.get("simpUser");
StompHeaderAccessor sha = StompHeaderAccessor.wrap(sde.getMessage());
List<String> nativeHeaders = sha.getNativeHeader("userId");
if( nativeHeaders != null )
{
String userId = nativeHeaders.get(0);
connectedClientId.remove(userId);
if( logger.isDebugEnabled() )
{
logger.debug("Disconnessione websocket. ID Utente "+userId);
}
}
else
{
String userId = princ.getName();
connectedClientId.remove(userId);
if( logger.isDebugEnabled() )
{
logger.debug("Disconnessione websocket. ID Utente "+userId);
}
}
}
public List<String> getConnectedClientId()
{
return connectedClientId;
}
public void setConnectedClientId(List<String> connectedClientId)
{
this.connectedClientId = connectedClientId;
}
}
When a client is connected you add in the List of clients id the client id; when it disconnects you remove it
Then you can inject this bean or its List where you want to check if the client is active or less and then you can check if the client id is between the connected clients ID you can send the message, otherwise you must save it and resend later
On client side you can do something like this:
var socket = new SockJS('/ws');
stompClient = Stomp.over(socket);
stompClient.connect({userId:"customUserId"}, function (frame) {
});
Angelo

why not using some events like below, you can export classes to differents files and use SessionConnectedEvent and SessionDisconnectEvent OR SessionSubscribeEvent and SessionUnsubscribeEvent.
see doc here http://docs.spring.io/spring/docs/current/spring-framework-reference/html/websocket.html#websocket-stomp-appplication-context-events
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import org.springframework.context.ApplicationListener;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.messaging.SessionConnectedEvent;
import org.springframework.web.socket.messaging.SessionDisconnectEvent;
import org.springframework.web.socket.messaging.SessionSubscribeEvent;
import org.springframework.web.socket.messaging.SessionUnsubscribeEvent;
#Component
public class SessionConnectedListener extends SessionsListener implements ApplicationListener<SessionConnectedEvent> {
#Override
public void onApplicationEvent(SessionConnectedEvent event) {
users.add(event.getUser().getName());
}
}
#Component
class SessionDisconnectListener extends SessionsListener implements ApplicationListener<SessionDisconnectEvent> {
#Override
public void onApplicationEvent(SessionDisconnectEvent event) {
users.remove(event.getUser().getName());
}
}
#Component
class SessionSubscribeListener extends SessionsListener implements ApplicationListener<SessionSubscribeEvent> {
#Override
public void onApplicationEvent(SessionSubscribeEvent event) {
users.add(event.getUser().getName());
}
}
#Component
class SessionUnsubscribeListener extends SessionsListener implements ApplicationListener<SessionUnsubscribeEvent> {
#Override
public void onApplicationEvent(SessionUnsubscribeEvent event) {
users.remove(event.getUser().getName());
}
}
class SessionsListener {
protected List<String> users = Collections.synchronizedList(new LinkedList<String>());
public List<String> getUsers() {
return users;
}
}
and change your code :
#Autowired
private SessionsListener sessionsListener;
#Override
public void sendNotification(KitaiEvent<?> event, Map<String, Object> notificationConfiguration) throws Exception {
String menuItem = Optional.ofNullable((String) notificationConfiguration.get(MENU_ENTRY_KEY)).orElseThrow(IllegalArgumentException::new);
List<String> userList = sessionsListener.getUsers();
for(String u: userList){
menuItemNotificationSender.sendNotification(new MenuItemDto(menuItem),u);
}

Related

#RabbitListener for multiple object types

In SpringBoot I use RabbitTemplate and #RabbitListener to produce and receive Messages.
I have an abstract class to send and receive RabbitMQ message of a specific type and send those messages to a STOMP web socket.
I use one RabbitMQ topic exchange which is bind to one RabbitMQ queue. In the topic-exchange I send messages (Java objects) of 2 different types and I am using 2 #RabbitListener to consume these messages.
#Configuration
public class WsRabbitMqConfiguration {
public static final String WS_EXCHANGE_NAME = "websocket.replication";
#Getter
#Value(value = "${spring.application.name}")
private String routingKey;
#Bean
TopicExchange wsReplicationExchange() {
return new TopicExchange(
WS_EXCHANGE_NAME,
true,
false,
null);
}
#Bean
public Queue wsReplicationQueue() {
return new AnonymousQueue();
}
#Bean
public Binding bindingWsAttributeValueQueue(TopicExchange wsReplicationExchange,
Queue wsReplicationQueue) {
return BindingBuilder
.bind(wsReplicationQueue)
.to(wsReplicationExchange)
.with(routingKey);
}
}
#RequiredArgsConstructor
public abstract class AbstractWebsocketService<T> implements WebSocketService {
public static final String HEADER_WS_DESTINATION = "x-Websocket-Destination";
private final RabbitTemplate rabbitTemplate;
private final WsRabbitMqConfiguration wsRabbitMqConfiguration;
#Override
public final <T> void send(String destination, T payload) {
rabbitTemplate.convertAndSend(WsRabbitMqConfiguration.WS_EXCHANGE_NAME, wsRabbitMqConfiguration.getRoutingKey(), payload,
message -> putDestination(message, destination));
}
protected abstract void handleWebsocketSending(T payload, String destination);
#Service
public class FooWebSocketService extends AbstractWebsocketService<Foo> {
public FooWebSocketService(RabbitTemplate rabbitTemplate,
WsRabbitMqConfiguration rabbitMqConfiguration) {
super(rabbitTemplate, rabbitMqConfiguration);
}
#RabbitListener(queues = "#{wsReplicationQueue.name}", ackMode = "NONE")
protected void handleWebsocketSending(#Payload Foo payload, #Header(HEADER_WS_DESTINATION) String destination) {
// logic here
}
}
#Service
public class BarWebSocketService extends AbstractWebsocketService<Bar> {
public BarWebSocketService(RabbitTemplate rabbitTemplate,
WsRabbitMqConfiguration rabbitMqConfiguration) {
super(rabbitTemplate, rabbitMqConfiguration);
}
#RabbitListener(queues = "#{wsReplicationQueue.name}", ackMode = "NONE")
protected void handleWebsocketSending(#Payload Bar payload, #Header(HEADER_WS_DESTINATION) String destination) {
// logic here
}
}
From another service class I want to send RMQ messages, but randomly wrong #RabbitListener is activated.
Eg.
#Service
#RequiredArgsConstructor
public class TestService {
private final BarWebSocketService barWebSocketService;
public void sendMessage(Bar bar) {
// 1st message
barWebSocketService.send("bar-destination", bar);
// 2nd message
barWebSocketService.send("bar-destination2", bar);
}
}
For the 1st message #RabbitListener in FooWebSocketService is activated (which is wrong) and for the 2nd message #RabbitListener in BarWebSocketService (which is right).
Any suggestions what I am doing wrong? Thank you!

Flutter Client Subscription to Spring Boot Websocket Server

Below is my Spring Boot Code for scheduling messages to its connected clients.
But my FLUTTER application is not able to receive the the pushed messages from the websocket server.
#Service
public class GreetingService {
private final SimpMessagingTemplate simpMessagingTemplate;
private static final String WS_MESSAGE_TRANSFER_DESTINATION = "/topic/greetings";
private List<String> userNames = new ArrayList<>();
GreetingService(SimpMessagingTemplate simpMessagingTemplate) {
this.simpMessagingTemplate = simpMessagingTemplate;
}
public void sendMessages() {
for (String userName : userNames) {
simpMessagingTemplate.convertAndSendToUser(userName, WS_MESSAGE_TRANSFER_DESTINATION,
"Hallo " + userName + " at " + new Date().toString());
}
}
public void addUserName(String username) {
userNames.add(username);
}
}
Flutter Code :-
var channel = IOWebSocketChannel.connect("ws://1f470ad1bdc8.ngrok.io/ws");
channel.stream.listen((message) {
channel.sink.add("received!");
});
You have to create a Spring Configuration class for initializing the subscription paths.
#Configuration
#EnableWebSocketMessageBroker
public class WSocketBrokerConfiguration implements WebSocketMessageBrokerConfigurer {
#Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.enableSimpleBroker("/topic");
config.setApplicationDestinationPrefixes("/app");
}
#Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/socket")
.setAllowedOrigins("*");
}
}

Spring Boot 4.3.5 WebSocket Chat with jwt authorization. No destination in GenericMessage

I'm trying to implement a 1-1 chat for a mobile app(ionic 3) with a spring boot back-end. Seems like run into some config problems.
Can't send message probably because the target channel wasn't created
Back-End:
ChatController:
#RestController
public class ChatController {
#Autowired
private PrivateChatService privateChatService;
private final static Logger logger = LogManager.getLogger(ChatController.class.getName());
#RequestMapping(value = "/chat/messages/{item_id}/chat_with/{buyer_login}", method = RequestMethod.GET, produces = "application/json")
public ResponseEntity<String> getExistingChatMessages(#PathVariable("item_id") String itemId, #PathVariable("buyer_login") String buyerLogin) {
List<ChatMessage> messages = privateChatService.getExistingChatMessages(itemId, buyerLogin);
logger.info("Here get messages");
return JSONResponseHelper.createResponse(messages, HttpStatus.OK);
}
#MessageMapping("/chat/{item_id}/send")
#SendTo("/topic/chat/{item_id}/chat_with/{buyer_login}")
public ChatMessage send(#Payload ChatMessage message,
#DestinationVariable("item_id") String item_id) throws Exception {
// logger.info(principal.getName());
logger.info(message.toString());
logger.info(item_id);
privateChatService.submitMessage(message);
return message;
}
}
WebSocketConfig:
#Configuration
#EnableWebSocketMessageBroker
#Order(Ordered.HIGHEST_PRECEDENCE + 99)
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {
private final static Logger logger = LogManager.getLogger(WebSocketConfig.class.getName());
#Autowired
private JwtTokenProvider jwtTokenProvider;
#Autowired
private PrivateChatService privateChatService;
private static final String MESSAGE_PREFIX = "/topic";
private static final String END_POINT = "/chat";
private static final String APPLICATION_DESTINATION_PREFIX = "/live";
#Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
if (registry != null) {
registry.addEndpoint(END_POINT).setAllowedOrigins("*").withSockJS();
}
}
#Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
if (registry != null) {
registry.enableSimpleBroker(MESSAGE_PREFIX);
registry.setApplicationDestinationPrefixes(APPLICATION_DESTINATION_PREFIX);
}
}
#Override
public void configureClientInboundChannel(ChannelRegistration registration) {
registration.setInterceptors(new ChannelInterceptorAdapter() {
#Override
public Message<?> preSend(Message<?> message, MessageChannel channel) {
StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
if (StompCommand.CONNECT.equals(accessor.getCommand())) {
String authToken = accessor.getFirstNativeHeader("Authentication");
String jwt = JwtUtils.resolveToken(authToken);
if (jwtTokenProvider.validateToken(jwt)) {
Authentication authentication = jwtTokenProvider.getAuthentication(jwt);
accessor.setUser(authentication);
String itemId = accessor.getFirstNativeHeader("item_id");
accessor.setDestination("/topic" + privateChatService.getChannelId(itemId, authentication.getName()));
logger.info(accessor.getDestination()); //ex: /topic/chat/3434/chat_with/user3797474342423
}
}
return message;
}
});
}
}
WebSocketSecurityConfig
#Configuration
public class WebSocketSecurityConfig extends AbstractSecurityWebSocketMessageBrokerConfigurer {
#Override
protected boolean sameOriginDisabled() {
return true;
}
}
Mobile client, ng2-stomp-service:
private _initWebsock(auth_token:string, item_id: number) {
let headers: Object = {
Authentication: `Bearer ${auth_token}`,
item_id: item_id
};
this.stomp.configure({
host :this.websocketApi + 'chat',
headers: headers,
queue:{'init':false}
});
console.log("Connecting stomp socket...");
//start connection
this.stomp.startConnect().then(() => {
this.stomp.done('init');
console.log('connected');
//subscribe
this.subscription = this.stomp.subscribe(`/chat/${item_id}/`, this.socketListener);
});
}
public socketListener = (data) => {
console.log(data)
};
send(msg: ChatMessage, item_id: number){
//send data
console.log(msg);
this.stomp.send(`/live/chat/${item_id}/send`, {}, JSON.stringify(msg));
}
Problem 1(probably):
In the browser console it shows that a client subscribes to /chat/item_id instead of /topic/chat/3434/chat_with/user3797474342423 => seems like configureClientInboundChannel doesn't work?
Problem 2:
When trying to execute this.stomp.send(/live/chat/${item_id}/send, {}, JSON.stringify(msg));, getting
o.s.m.s.b.DefaultSubscriptionRegistry : No destination in GenericMessage [payload=byte[2], headers={simpMessageType=MESSAGE.... Error.
https://docs.spring.io/spring/docs/current/spring-framework-reference/web.html#websocket-stomp-authentication
and
https://stackoverflow.com/a/33962402/8336511
This is how I solved this problem:
When user authenticates with Spring Security, WebSocket module creates
unique channel for that user based on his Principal. Example
"/user/queue/position-updates" is translated to
"/queue/position-updates-user123"
So on the client side all I had to do, was subscribe to
/user/queue/requests
And on the server side, send messages to
/user/{username}/queue/requests with
convertAndSendToUser(request.getFromUser(), "/queue/requests",
request) and Spring handles the rest.

spring web socket server and react client

i have to develop a web socket server with spring that send to the client a message avery 5 sec. The client is written in react js. This is my server code:
#SpringBootApplication
#EnableAsync
#EnableScheduling
public class TestwsApplication {
public static void main(String[] args) {
SpringApplication.run(TestwsApplication.class, args);
}
}
#Configuration
#EnableWebSocketMessageBroker
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {
#Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.enableSimpleBroker("/topic");
}
#Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/chatWS").setAllowedOrigins("*").withSockJS();
}
This is my scheduler that send to the channel /topic/message a message every 5 sec
#Component
public class ScheduledTasks {
#Autowired
WebSocketListener listener;
int i=0;
private static final Logger log = LoggerFactory.getLogger(ScheduledTasks.class);
private static final SimpleDateFormat dateFormat = new SimpleDateFormat("HH:mm:ss");
#Scheduled(fixedRate = 5000)
public void reportCurrentTime() {
if (i==0){
listener.pushSystemStatusToWebSocket("ok");
i=1;
}else{
listener.pushSystemStatusToWebSocket("errore");
i=0;
}
}
}
This is my service used by the scheduler to send the message to the clients
#Service
public class WebSocketListener {
#Autowired
private SimpMessagingTemplate webSocket;
#Async
public void pushSystemStatusToWebSocket (String newStatus){
System.out.println("inviooooooooooooooooooooooooooo");
webSocket.convertAndSend("/topic/messages", newStatus);
}
}
This is my react component
import SockJS from 'sockjs-client';
class Main extends React.Component {
constructor() {
super();
this.state = {
clickCount: 0,
};
}
componentDidMount(){
// this is an "echo" websocket service
console.log('didmount')
var sock = new SockJS('http://localhost:8080/chatWS');
sock.onopen = function() {
console.log('open socket ');
sock.send('test');
};
sock.onmessage = function(e) {
console.log('message');
console.log('message', e.data);
sock.close();
};
sock.onclose = function() {
console.log('close');
};
}
on the log i see only the post open socket ... i dont see any log inserted in onmessage... so the client did not receive the message. Why ? Can you help me ?
Thanks
Esoni

Spring Websocket - How can I detect client disconnect

I am new to spring
I have this class :
public class Server extends TextWebSocketHandler implements WebSocketHandler {
WebSocketSession clientsession;
#Override
public void handleTextMessage(WebSocketSession session, TextMessage message) {
clientsession = session;
}
I need to detect a client disconnect on clientsession.
implement ApplicationListener but its not clear how I can register the listener?
do I need to do it in my web.xml ?
The WebSocketHandler afterConnectionClosed function is called after a websocket client disconnects. You simply need to override this in the manner that you override handleTextMessage.
#Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus closeStatus){
// your code here
}
There may be substantial delay between the client disconnect and your server event detection. See details about real-time disconnection detection.
You will need to override configureClientOutboundChannel and configureClientInboundChannel of AbstractWebSocketMessageBrokerConfigurer, providing your interceptor
Another way is using ApplicationEvents.
Both methods are described here:
http://www.sergialmar.com/2014/03/detect-websocket-connects-and-disconnects-in-spring-4/
public class StompConnectEvent implements ApplicationListener<SessionConnectEvent> {
private final Log logger = LogFactory.getLog(StompConnectEvent.class);
public void onApplicationEvent(SessionConnectEvent event) {
StompHeaderAccessor sha = StompHeaderAccessor.wrap(event.getMessage());
String company = sha.getNativeHeader("company").get(0);
logger.debug("Connect event [sessionId: " + sha.getSessionId() +"; company: "+ company + " ]");
}
}
I hope that help. Let me know if I need to explain more.
You can use listeners to detect when session is connected or closed.
More information about listeners you can find by this link.
Example how to detect connected session:
#Component
public class SessionConnectedEventListener implements ApplicationListener<SessionConnectedEvent> {
private IWebSocketSessionService webSocketSessionService;
public SessionConnectedEventListener(IWebSocketSessionService webSocketSessionService) {
this.webSocketSessionService = webSocketSessionService;
}
#Override
public void onApplicationEvent(SessionConnectedEvent event) {
webSocketSessionService.saveSession(event);
}
}
Example how to detect when session is disconneted:
#Component
public class SessionDisconnectEventListener implements ApplicationListener<SessionDisconnectEvent> {
private IWebSocketSessionService webSocketSessionService;
public SessionDisconnectEventListener(IWebSocketSessionService webSocketSessionService) {
this.webSocketSessionService = webSocketSessionService;
}
#Override
public void onApplicationEvent(SessionDisconnectEvent event) {
webSocketSessionService.removeSession(event);
}
}
What you probably want to achieve is maintaining multiple sessions. Something like this:
public class Server extends TextWebSocketHandler implements WebSocketHandler {
private List<WebSocketSession> sessions = new ArrayList<>();
#Override
public void afterConnectionEstablished(WebSocketSession session) {
sessions.add(session);
}
#Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) {
sessions.remove(session);
}
#Override
public void handleTextMessage(WebSocketSession session, TextMessage message) {
// clientsession = session;
// Send individual or broadcast messages here ...
session.sendMessage(new TextMessage(textMessage + "!"));
}
}

Resources