I have an application with a large number of groups, where my server is using a message queue (RabbitMQ) to observe the groups and post notification to the user upon changes over WebSocket. I'm using Spring boot and their WebSocket implementation inspired by this guide: https://spring.io/guides/gs/messaging-stomp-websocket/
Here is an example of the JavaScript client subscribing to the channel:
var socket = new SockJS('http://localhost/ws');
stompClient = Stomp.over(socket);
stompClient.connect({}, function (frame) {
console.log('Connected: ' + frame);
stompClient.subscribe('/topic/group/1/notification', function (message) {
// to something..
});
});
My Java Spring WebSocket controller has this broadcastNotification method sending messages to the /topic/group/{groupId}/notification channel.
#Controller
public class GroupController {
private SimpMessagingTemplate template;
#Autowired
public GroupController(SimpMessagingTemplate template) {
this.template = template;
}
public void broadcastNotification(int groupId, Notification notification) {
this.template.convertAndSend("/topic/group/." + tenantId + "/notification", Notification);
}
}
Thats working fine, but with performacne in mind I would like my to business logic to only observe groups currently beeing subscribed on WebSocket.
How can I be notified on my server when clients subscribe to the /topic/group/1/notification or /topic/group/1/* channel? The web users will be subscribing and unsubscribing as they browse the web page.
You can listen to the event SessionSubscribeEvent like this:
#Component
public class WebSocketEventListener {
#EventListener
public void handleSessionSubscribeEvent(SessionSubscribeEvent event) {
GenericMessage message = (GenericMessage) event.getMessage();
String simpDestination = (String) message.getHeaders().get("simpDestination");
if (simpDestination.startsWith("/topic/group/1")) {
// do stuff
}
}
}
You can use annotation-driven event listener (Kotlin code):
#EventListener
private fun onSubscribeEvent(event: SessionSubscribeEvent) {
// do stuff...
}
Such event listeners can be registered on any public method of a managed bean via the #EventListener annotation.
Spring websockets events docs
Spring events examples
You can detect when a client subscribes to a topic using interceptors in the WebSocketConfig class:
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.simp.config.ChannelRegistration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.messaging.simp.stomp.StompCommand;
import org.springframework.messaging.simp.stomp.StompHeaderAccessor;
import org.springframework.messaging.support.ChannelInterceptor;
import org.springframework.messaging.support.MessageHeaderAccessor;
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 configureMessageBroker(MessageBrokerRegistry config) {
config.enableSimpleBroker("/topic");
config.setApplicationDestinationPrefixes("/app");
}
#Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/gs-guide-websocket").withSockJS();
}
#Override
public void configureClientInboundChannel(ChannelRegistration registration){
registration.interceptors(new ChannelInterceptor() {
#Override
public Message<?> preSend(Message<?> message, MessageChannel channel) {
StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
if(StompCommand.CONNECT.equals(accessor.getCommand())){
System.out.println("Connect ");
} else if(StompCommand.SUBSCRIBE.equals(accessor.getCommand())){
System.out.println("Subscribe ");
} else if(StompCommand.SEND.equals(accessor.getCommand())){
System.out.println("Send message " );
} else if(StompCommand.DISCONNECT.equals(accessor.getCommand())){
System.out.println("Exit ");
} else {
}
return message;
}
});
}
}
The accessor object contains all information sent from the client.
Related
This question already has an answer here:
Spring websocket end-poind and send message
(1 answer)
Closed 7 months ago.
I'm trying to build a Spring STOMP websocket + ActiveMQ service. I have set the websocket and the ActiveMQ queue.
ActiveMQ queue works just fine but I'm not able to make my websocket endpoint send messages to the clients connected to the topic.
Websocket client seems to connect just fine also. The thing is that when the controller receives information it is not caught on the client.
--WebsocketConfig.java--
#Configuration
#EnableWebSocketMessageBroker
public class WebsocketConfig implements WebSocketMessageBrokerConfigurer {
#Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.enableSimpleBroker("/topic");
config.setApplicationDestinationPrefixes("/app");
}
#Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/websocket").withSockJS();
}
}
--WebsocketController.java--
#Controller
public class WebsocketController {
#Autowired
private ItemService itemService;
#JmsListener(destination = "items-queue")
#MessageMapping("/websocket")
#SendTo("/topic/items")
public String itemsWebsocket(Iterable<Item> items) {
System.out.println("Websocket controller reached");
for (Item item : items) System.out.println(item.getName());
return "hi from websocket";
}
}
--app.js--
let stompClient = null;
function connect() {
let socket = new SockJS('/websocket');
stompClient = Stomp.over(socket);
stompClient.connect({}, function (frame) {
console.log('Connected: ' + frame);
stompClient.subscribe('/topic/items', function (items) {
appendItems(items);
});
});
}
function disconnect() {
if (stompClient !== null) {
stompClient.disconnect();
}
console.log("Disconnected");
}
function appendItems(items) {
console.log(items);
const itemListContainer = document.getElementById("item-list");
itemListContainer.innerText = "";
Array.from(items).forEach( item => {
const itemContainer = document.createElement("div");
itemContainer.innerText = item.name;
itemListContainer.append(itemContainer);
});
}
connect();
I just found out that if I apply the changes from here it does the thing.
I'm implementing a simple socket like this :
#MessageMapping("/hello")
#SendTo("/topic/greetings")
public Greeting greeting(HelloMessage message) throws Exception {
Thread.sleep(1000); // simulated delay
return new Greeting("Hello, " + message.getName() + "!");
}
From the client-side :
function connect() {
var socket = new SockJS('/gs-guide-websocket');
stompClient = Stomp.over(socket);
stompClient.connect({}, function (frame) {
setConnected(true);
console.log('Connected: ' + frame);
stompClient.subscribe('/topic/greetings', function (greeting) {
showGreeting(JSON.parse(greeting.body).content);
});
});
}
And the websocket 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("/gs-guide-websocket").withSockJS();
}
}
Now I would like to apply authentication for the socket so that not all clients can connect to server. I may look like
var socket = new SockJS('/gs-guide-websocket?token= a JWT token'); //or sth similar
from the client-side.
Let just assume that I can hide the token from people who inspect the frontend code, how do I get that token and verify it from the server side ? (Provided that I have the function to verify the JWT already)
Or is there any better way of implementing security for socket connection that you can suggest ?
SOLVED: see comment
I am currently trying to open a WebSockets connection between a spring server and an angular client with a x-auth-token. Therefore I have the following pieces of code:
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.messaging.simp.config.ChannelRegistration;
import org.springframework.messaging.support.ChannelInterceptorAdapter;
import org.springframework.messaging.support.MessageHeaderAccessor;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.simp.stomp.StompCommand;
import org.springframework.messaging.simp.stomp.StompSession;
import org.springframework.messaging.simp.stomp.StompHeaders;
import org.springframework.messaging.simp.stomp.StompHeaderAccessor;
import org.springframework.web.socket.config.annotation.AbstractWebSocketMessageBrokerConfigurer;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
#Configuration
#EnableWebSocketMessageBroker
#Order(Ordered.HIGHEST_PRECEDENCE + 99)
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {
private final Logger LOGGER = LoggerFactory.getLogger(this.getClass());
#Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.enableSimpleBroker("/topic");
config.setApplicationDestinationPrefixes("/app");
}
#Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
LOGGER.info("registering websockets");
registry.addEndpoint("/api/v1/websocket").setAllowedOrigins("*").withSockJS();
}
#Override
public void handleException(StompSession session, StompCommand command, StompHeaders headers, byte[] payload, Throwable exceptio) {
LOGGER.info("ERRORORRRRRRRR");
exception.printStackTrace();
}
#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);
LOGGER.info("in override");
if (StompCommand.CONNECT.equals(accessor.getCommand())) {
String authToken = accessor.getFirstNativeHeader("x-auth-token");
LOGGER.info("Header auth token: " + authToken);
// Principal user = ... ; // access authentication header(s)
//accessor.setUser(user);
}
return message;
}
});
}
}
As you can see from my WebSocketConfig I am currently not really processing the x-auth-token, but rather trying to log it once. In the end I was trying to follow the Spring documentation: https://docs.spring.io/spring/docs/current/spring-framework-reference/html/websocket.html#websocket-stomp-authentication-token-based
The only problem I have with this is that in configureClientInboundChannel() the preSend() method seems not get called - and I cannot figure out why.
On the client side, the following error is displayed:
As you can see by comparing the error log and the endpoint defined in registerStompEndpoints() shows the actual endpoint is the correct one. Still I do not understand, why LOGGER.info("in override"); in configureClientInboundChannel() is not called.
I would really like to go with the auth-token being transferred via the stomp headers instead of the hacky token-as-URL-paramter-solution. Does someone here have an idea? Did I miss something in the already linked spring documentation?
Thanks in advance for your help!
I have followed Quetion1 and Quetion2 from stack overflow to send messages to specific client, based on its sessionId but could not find success.
Below is my sample RestController class
#RestController
public class SpringSessionTestApi {
#Autowired
public SimpMessageSendingOperations messagingTemplate;
#MessageMapping("/messages")
public void greeting(HelloMessage message, SimpMessageHeaderAccessor headerAccessor) throws Exception {
String sessionId = (String) headerAccessor.getSessionAttributes().get("SPRING.SESSION.ID");
messagingTemplate.convertAndSendToUser(sessionId,"/queue/test",message, createHeaders(sessionId));
}
private MessageHeaders createHeaders(String sessionId) {
SimpMessageHeaderAccessor headerAccessor = SimpMessageHeaderAccessor.create(SimpMessageType.MESSAGE);
headerAccessor.setSessionId(sessionId);
headerAccessor.setLeaveMutable(true);
return headerAccessor.getMessageHeaders();
}
}
Session Id: when client sends createSession request, new spring sessionId is generated and same is stored in MongoDB as well. After that when client sends web socket connect request, same sessionId is received which was stored in mongoDb as expected. Till This everything is working fine.
Now my job is to send response back to the client based on the sessionId.
For that I have below web socket class:
#Configuration
#EnableScheduling
#EnableWebSocketMessageBroker
public class WebSocketConfig extends
AbstractSessionWebSocketMessageBrokerConfigurer<ExpiringSession> {
#Override
protected void configureStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/messages");
}
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.enableSimpleBroker("/queue");
registry.setApplicationDestinationPrefixes("/app");
}
}
and the sample client code that I am using to connect is:
function connect() {
stompClient = Stomp.client('ws://localhost:8016/messages');
stompClient.debug = null;
stompClient.connect({}, function (frame) {
setConnected(true);
console.log('Connected: ' + frame);
stompClient.subscribe('/user/queue/test', function (greeting) {
console.log("Hello "+greeting);
console.log("Greeting body "+JSON.parse(greeting.body));
});
});
}
Please help, Where I am doing wrong in this?
Thanks in Advance!
If you are using /user channel as you do, try to pass the user as stated here.
#MessageMapping("/messages")
public void greeting(HelloMessage message, SimpMessageHeaderAccessor headerAccessor, Principal principal)
throws Exception {
messagingTemplate.convertAndSendToUser(principal.getName(), "/queue/test", message);
}
I've found a full workable Spring Stomp Chat project in git, the link is here. You can refer to it.
https://gist.github.com/theotherian/9906304
I have a WebSocket Application using Spring MVC that I have defined as per this tutorial. The file that configures the WebSocket is the following:
package com.myapp.spring.web.controller;
import java.io.IOException;
import javax.websocket.OnClose;
import javax.websocket.OnError;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.ServerEndpoint;
import org.springframework.web.socket.server.standard.SpringConfigurator;
#ServerEndpoint(value="/serverendpoint", configurator = SpringConfigurator.class)
public class serverendpoint {
#OnOpen
public void handleOpen () {
System.out.println("JAVA: Client is now connected...");
}
#OnMessage
public String handleMessage (Session session, String message) throws IOException {
if (message.equals("ping")) {
// return "pong"
session.getBasicRemote().sendText("pong");
}
else if (message.equals("close")) {
handleClose();
return null;
}
System.out.println("JAVA: Received from client: "+ message);
MyClass mc = new MyClass(message);
String res = mc.action();
session.getBasicRemote().sendText(res);
return res;
}
#OnClose
public void handleClose() {
System.out.println("JAVA: Client is now disconnected...");
}
#OnError
public void handleError (Throwable t) {
t.printStackTrace();
}
}
My Question is if I have a Javascript client trying to connect to this WebSocket, what Uri should I use if the WebSocket is mapped at "/serverendpoint" as seen from the above #serverendpoint annotation?
var wsUri = "??????"
var webSocket = new WebSocket(wsUri);
What should wsUri be?
Here is my Spring MVC project hierarchy:
It is the same address for your server running the http port