Spring+WebSocket+STOMP. Message to specific session (NOT user) - java

I am trying to set up basic message broker on Spring framework, using a recipe I found here
Author claims it has worked well, but I am unable to receive messages on client, though no visible errors were found.
Goal:
What I am trying to do is basically the same - a client connects to server and requests some async operation. After operation completes the client should receive an event. Important note: client is not authenticated by Spring, but an event from async back-end part of the message broker contains his login, so I assumed it would be enough to store concurrent map of Login-SessionId pairs for sending messages directly to particular session.
Client code:
//app.js
var stompClient = null;
var subscription = '/user/queue/response';
//invoked after I hit "connect" button
function connect() {
//reading from input text form
var agentId = $("#agentId").val();
var socket = new SockJS('localhost:5555/cti');
stompClient = Stomp.over(socket);
stompClient.connect({'Login':agentId}, function (frame) {
setConnected(true);
console.log('Connected to subscription');
stompClient.subscribe(subscription, function (response) {
console.log(response);
});
});
}
//invoked after I hit "send" button
function send() {
var cmd_str = $("#cmd").val();
var cmd = {
'command':cmd_str
};
console.log("sending message...");
stompClient.send("/app/request", {}, JSON.stringify(cmd));
console.log("message sent");
}
Here is my configuration.
//message broker configuration
#Configuration
#EnableWebSocketMessageBroker
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer{
#Override
public void configureMessageBroker(MessageBrokerRegistry config) {
/** queue prefix for SUBSCRIPTION (FROM server to CLIENT) */
config.enableSimpleBroker("/topic");
/** queue prefix for SENDING messages (FROM client TO server) */
config.setApplicationDestinationPrefixes("/app");
}
#Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry
.addEndpoint("/cti")
.setAllowedOrigins("*")
.withSockJS();
}
}
Now, after basic config I should implement an application event handler to provide session-related information on client connect.
//application listener
#Service
public class STOMPConnectEventListener implements ApplicationListener<SessionConnectEvent> {
#Autowired
//this is basically a concurrent map for storing pairs "sessionId - login"
WebAgentSessionRegistry webAgentSessionRegistry;
#Override
public void onApplicationEvent(SessionConnectEvent event) {
StompHeaderAccessor sha = StompHeaderAccessor.wrap(event.getMessage());
String agentId = sha.getNativeHeader("Login").get(0);
String sessionId = sha.getSessionId();
/** add new session to registry */
webAgentSessionRegistry.addSession(agentId,sessionId);
//debug: show connected to stdout
webAgentSessionRegistry.show();
}
}
All good so far. After I run my spring webapp in IDE and connected my "clients" from two browser tabs I got this in IDE console:
session_id / agent_id
-----------------------------
|kecpp1vt|user1|
|10g5e10n|user2|
-----------------------------
Okay, now let's try to implement message mechanics.
//STOMPController
#Controller
public class STOMPController {
#Autowired
//our registry we have already set up earlier
WebAgentSessionRegistry webAgentSessionRegistry;
#Autowired
//a helper service which I will post below
MessageSender sender;
#MessageMapping("/request")
public void handleRequestMessage() throws InterruptedException {
Map<String,String> params = new HashMap(1);
params.put("test","test");
//a custom object for event, not really relevant
EventMessage msg = new EventMessage("TEST",params);
//send to user2 (just for the sake of it)
String s_id = webAgentSessionRegistry.getSessionId("user2");
System.out.println("Sending message to user2. Target session: "+s_id);
sender.sendEventToClient(msg,s_id);
System.out.println("Message sent");
}
}
A service to send messages from any part of the application:
//MessageSender
#Service
public class MessageSender implements IMessageSender{
#Autowired
WebAgentSessionRegistry webAgentSessionRegistry;
#Autowired
SimpMessageSendingOperations messageTemplate;
private String qName = "/queue/response";
private MessageHeaders createHeaders(String sessionId) {
SimpMessageHeaderAccessor headerAccessor = SimpMessageHeaderAccessor.create(SimpMessageType.MESSAGE);
headerAccessor.setSessionId(sessionId);
headerAccessor.setLeaveMutable(true);
return headerAccessor.getMessageHeaders();
}
#Override
public void sendEventToClient(EventMessage event,String sessionId) {
messageTemplate.convertAndSendToUser(sessionId,qName,event,createHeaders(sessionId));
}
}
Now, let's try to test it. I run my IDE, opened Chrome and created 2 tabs form which I connected to server. User1 and User2. Result console:
session_id / agent_id
-----------------------------
|kecpp1vt|user1|
|10g5e10n|user2|
-----------------------------
Sending message to user2. Target session: 10g5e10n
Message sent
But, as I mentioned in the beginning - user2 got absolutely nothing, though he is connected and subscribed to "/user/queue/response". No errors either.
A question is, where exactly I am missing the point? I have read many articles on the subject, but to no avail.
SPR-11309 says it's possible and should work. Maybe, id-s aren't actual session id-s?
And well maybe someone knows how to monitor if the message actually has been sent, not dropped by internal Spring mechanics?
SOLUTION UPDATE:
A misconfigured bit:
//WebSocketConfig.java:
....
#Override
public void configureMessageBroker(MessageBrokerRegistry config) {
/** queue prefix for SUBSCRIPTION (FROM server to CLIENT) */
// + parameter "/queue"
config.enableSimpleBroker("/topic","/queue");
/** queue prefix for SENDING messages (FROM client TO server) */
config.setApplicationDestinationPrefixes("/app");
}
....
I've spent a day debugging internal spring mechanics to find out where exactly it goes wrong:
//AbstractBrokerMessageHandler.java:
....
protected boolean checkDestinationPrefix(String destination) {
if ((destination == null) || CollectionUtils.isEmpty(this.destinationPrefixes)) {
return true;
}
for (String prefix : this.destinationPrefixes) {
if (destination.startsWith(prefix)) {
//guess what? this.destinationPrefixes contains only "/topic". Surprise, surprise
return true;
}
}
return false;
}
....
Although I have to admit I still think the documentation mentioned that user personal queues aren't to be configured explicitly cause they "already there". Maybe I just got it wrong.

Overall it looks good, but could you change from
config.enableSimpleBroker("/topic");
to
config.enableSimpleBroker("/queue");
... and see if this works? Hope this help.

Related

Sending triggers to client using websockets in spring boot and angular

I want to initiate a trigger(maybe a notification) from backend(based in spring boot) to a particular user whose userId is xyz.
the one way i have found is:
initially i connect to a websocket end point and subscribe to channel "/user/Notifications/xyz"
following is the relevant code in my angular typescript
connectToUserWebSocket(userId) {
let socket = new SockJS('http://localhost:5000/fellowGenius');
this.ws = Stomp.over(socket);
let that = this;
this.ws.connect(
{},
(frame) => {
that.ws.subscribe('/user/Notifications/' +userId, (message) => {
console.log("user subscribed");
});
},
(error) => {
alert('STOMP error ' + error);
}
);
}
Now once i have subscribed to my channel . I want to send a trigger to client which is initiated by backend itself so i run a code in my java service.
My relevant java code is:
#SendTo("/user/Notifications/{userId}")
public String sendMeetingNotificationWebSocket(#DestinationVariable String userId) {
return "hello";
}
my websocket configurations are:
#Configuration
#EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer{
#Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/fellowGenius").setAllowedOrigins("*").addInterceptors(new HttpSessionHandshakeInterceptor()).withSockJS();
}
#Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.enableSimpleBroker("/inbox/","/user/Notifications/");
}
}
But the problem is that even i can see one web socket connected in my spring boot console.
But i don't get a response from the function on the client side.
Please help me with this problem.

How to handle socket disconnection and heartbeat messages?

What I am trying to do
I have a lobby with players and when someone leaves the lobby I want to update it for every client so the actual list of players is displayed.
What I have done
To avoid cyclical requests being sent from frontend to backend I decided to use web sockets. When someone leaves the lobby then request is sent to REST api and then backend, upon receiving this request, does all the business logic and afterwards "pokes" this lobby using socket in order to update all clients in the lobby.
My problem
Everything works fine except the case when user closes the browser or the tab because I can't send a request in this scenario. (as far as I know this is impossible to do using javascript and beforeunload event, onDestroy() methods, etc..)
My question
Is it possible to check on the server side whether any socket disconnected and if yes then how can I do this? I also tried to use heartbeat which is being sent from frontend to backend but I don't know how to handle this heartbeat message on the server side.
Server side (Spring boot)
#Configuration
#EnableWebSocketMessageBroker
public class WebSocketConfiguartion implements WebSocketMessageBrokerConfigurer {
#Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/api/socket")
.setAllowedOrigins("*")
.withSockJS();
}
#Override
public void configureMessageBroker(MessageBrokerRegistry config) {
ThreadPoolTaskScheduler te = new ThreadPoolTaskScheduler();
te.setPoolSize(1);
te.setThreadNamePrefix("wss-heartbeat-thread-");
te.initialize();
config.enableSimpleBroker("/lobby")
.setHeartbeatValue(new long[]{0, 1000})
.setTaskScheduler(te);
}
}
#Controller
public class WebSocketController {
private final SimpMessagingTemplate template;
WebSocketController(SimpMessagingTemplate template) {
this.template = template;
}
public void pokeLobby(#DestinationVariable String lobbyName, SocketMessage message) {
this.template.convertAndSend("/lobby/"+lobbyName.toLowerCase(), message);
}
}
Client side
connectToLobbyWebSocket(lobbyName: string): void {
const ws = new SockJS(this.addressStorage.apiAddress + '/socket');
this.stompClient = Stomp.over(ws);
// this.stompClient.debug = null;
const that = this;
this.stompClient.connect({}, function () {
that.stompClient.subscribe('/lobby/' + lobbyName, (message) => {
if (message.body) {
that.socketMessage.next(message.body); // do client logic
}
});
});
}
You can listen for SessionDisconnectEvent in your application and send messages to other clients when you receive such an event.
Event raised when the session of a WebSocket client using a Simple Messaging Protocol (e.g. STOMP) as the WebSocket sub-protocol is closed.
Note that this event may be raised more than once for a single session and therefore event consumers should be idempotent and ignore a duplicate event.
There are other types of events also.

Should Spring SseEmitter.complete() trigger an EventSource reconnect - how to close connection server-side

I'm trying to set up a Spring SseEmitter to send a sequence of updates of the status of a running job. It seems to be working but:
Whenever I call emitter.complete() in in my Java server code, the javascript EventSource client calls the registered onerror function and then calls my Java endpoint again with a new connection. This happens in both Firefox and Chrome.
I can probably send an explicit "end-of-data" message from Java and then detect that and call eventSource.close() on the client, but is there a better way?
What is the purpose of emitter.complete() in that case?
Also, if I always have to terminate the connection on the client end, then I guess every connection on the server side will be terminated by either a timeout or a write error, in which case I probably want to manually send back a heartbeat of some kind every few seconds?
It feels like I'm missing something if I'm having to do all this.
I have added the following to my Spring boot application to trigger the SSE connection close()
Server Side:
Create a simple controller which returns SseEmitter.
Wrap the backend logic in a single thread executor service.
Send your events to the SseEmitter.
On complete send an event of type complete via the SseEmitter.
#RestController
public class SearchController {
#Autowired
private SearchDelegate searchDelegate;
#GetMapping(value = "/{customerId}/search")
#ResponseStatus(HttpStatus.OK)
#ApiOperation(value = "Search Sources", notes = "Search Sources")
#ApiResponses(value = {
#ApiResponse(code = 201, message = "OK"),
#ApiResponse(code = 401, message = "Unauthorized")
})
#ResponseBody
public SseEmitter search(#ApiParam(name = "searchCriteria", value = "searchCriteria", required = true) #ModelAttribute #Valid final SearchCriteriaDto searchCriteriaDto) throws Exception {
return searchDelegate.route(searchCriteriaDto);
}
}
#Service
public class SearchDelegate {
public static final String SEARCH_EVENT_NAME = "SEARCH";
public static final String COMPLETE_EVENT_NAME = "COMPLETE";
public static final String COMPLETE_EVENT_DATA = "{\"name\": \"COMPLETED_STREAM\"}";
#Autowired
private SearchService searchService;
private ExecutorService executor = Executors.newCachedThreadPool();
public SseEmitter route(SearchCriteriaDto searchCriteriaDto) throws Exception {
SseEmitter emitter = new SseEmitter();
executor.execute(() -> {
try {
if(!searchCriteriaDto.getCustomerSources().isEmpty()) {
searchCriteriaDto.getCustomerSources().forEach(customerSource -> {
try {
SearchResponse searchResponse = searchService.search(searchCriteriaDto);
emitter.send(SseEmitter.event()
.id(customerSource.getSourceId())
.name(SEARCH_EVENT_NAME)
.data(searchResponse));
} catch (Exception e) {
log.error("Error while executing query for customer {} with source {}, Caused by {}",
customerId, source.getType(), e.getMessage());
}
});
}else {
log.debug("No available customerSources for the specified customer");
}
emitter.send(SseEmitter.event().
id(String.valueOf(System.currentTimeMillis()))
.name(COMPLETE_EVENT_NAME)
.data(COMPLETE_EVENT_DATA));
emitter.complete();
} catch (Exception ex) {
emitter.completeWithError(ex);
}
});
return emitter;
}
}
Client Side:
Since we specified the name of event on our SseEmitter, an event will be dispatched on the browser to the listener for the specified event name; the website source code should use addEventListener() to listen for named events. (Notice: The onmessage handler is called if no event name is specified for a message)
Call the EventSource on the COMPLETE event to release the client connection.
https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events
var sse = new EventSource('http://localhost:8080/federation/api/customers/5d96348feb061d13f46aa6ce/search?nativeQuery=true&queryString=*&size=10&customerSources=1,2,3&start=0');
sse.addEventListener("SEARCH", function(evt) {
var data = JSON.parse(evt.data);
console.log(data);
});
sse.addEventListener("COMPLETE", function(evt) {
console.log(evt);
sse.close();
});
According to the HTML standard for Server-sent events
Clients will reconnect if the connection is closed; a client can be told to stop reconnecting using the HTTP 204 No Content response code.
So Spring's SseEmitter behaves as expected and the purpose of complete() is to make sure all the events were sent and then to close the connection.
You need to either implement server-side logic that would return 204 http code on subsequent requests (e.g. by checking session id) or to send a special event and close the connection from client side after receiving it as suggested by Ashraf Sarhan

How to Implement RabbitMQ consumer part(receiver ) inside the Spring boot API

I am going to do send my DATA toRabbitMq producer(message sender) and get responsible data from RabbitMq consumer(message receiver). producer part is working fine .now my problem is how to implement consumer part (receiver part) in side the Spring boot API. .Below is My spring boot API and i written ProducerAndConsumer one class.
ProducerAndConsumer.class
#Component
public class ProducerAndConsumer {
#Autowired
private RabbitTemplate rabbitTemplate;
//MessageProducer part (send part)
public boolean sendMessage(String message) {
rabbitTemplate.convertAndSend(RobbitMqConfig.ROUTING_KEY, message);
System.out.println("Is listener returned ::: ==========="+rabbitTemplate.isReturnListener());
return rabbitTemplate.isReturnListener();
}
//Consumer part (receiver part)
#RabbitListener(queues = RobbitMqConfig.QUEUE_NAME1)
public void receiveMessage ( final Message message){
System.out.println("Received message====Receiver=====" + message.getPayload());
}
}
API part
#PostMapping(value = {"/sendFilesName"})
public ResponseEntity<?> sendFilesName(#RequestBody SendFileNameRequest sendFileNameRequest, HttpServletRequest request) throws ParseException {
System.out.println("FileNameArray="+sendFileNameRequest.getFileNameArray());
if(sendFileNameRequest.getFileNameArray().size()!=0) {
List<String> message = sendFileNameRequest.getFileNameArray();
**//see here i send my message array data**
if(producerAndConsumer.sendMessage(message.toString())){
**//here i want implement my receiver part how to?**
return ResponseEntity.ok(new ApiResponse(true, "fileName List sent successfully", "",true));
}else {
return ResponseEntity.ok(new ApiResponse(false, "fileName List sent Fails", "",true));
}
}else {
return ResponseEntity.ok(new ApiResponse(false, "fileName List not present ", "",true));
}
}
The routing algorithm behind a direct exchange is simple - a message goes to the queues whose binding key exactly matches the routing key of the message.
spring amqp
Note: Check the routing key and queues binded using rabbitmq admin console to figure out whats going on or share the rabbitmq configuration.

Send Notification to specific user in spring boot websocket

I want to send notification to specific client.
e.g username user
#Configuration
#EnableWebSocketMessageBroker
public class WebSocketConfiguration extends
AbstractWebSocketMessageBrokerConfigurer {
#Override
public void registerStompEndpoints(StompEndpointRegistry stompEndpointRegistry) {
stompEndpointRegistry.addEndpoint("/socket")
.setAllowedOrigins("*")
.withSockJS();
}
#Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.enableSimpleBroker("/topic", "/queue");
registry.setApplicationDestinationPrefixes("/app");
}
Controller
#GetMapping("/notify")
public String getNotification(Principal principal) {
String username = "user";
notifications.increment();
logger.info("counter" + notifications.getCount() + "" + principal.getName());
// logger.info("usersend:"+sha.getUser().getName()) ; //user
template.convertAndSendToUser(principal.getName(), "queue/notification", notifications);
return "Notifications successfully sent to Angular !";
}
Client-Side
Angular Service
connect() {
let socket = new SockJs(`api/socket`);
let stompClient = Stomp.over(socket);
return stompClient;
}
Angular Component
let stompClient = this.webSocketService.connect();
stompClient.connect({}, frame => {
stompClient.subscribe('/user/queue/notification', notifications => {
console.log('test'+notifications)
this.notifications = JSON.parse(notifications.body).count;
}) });
I am have searched many other questions and tried but none of them worked for me
e.g here answered by Thanh Nguyen Van and here
Console
Opening Web Socket...
stomp.js:134 Web Socket Opened...
stomp.js:134 >>> CONNECT
accept-version:1.1,1.0
heart-beat:10000,10000
stomp.js:134 <<< CONNECTED
version:1.1
heart-beat:0,0
stomp.js:134 connected to server undefined
reminder.component.ts:18 test callsed
stomp.js:134 >>> SUBSCRIBE
id:sub-0
destination:/user/queue/notification
thanks in advance .
The answer of gerrytan to Sending message to specific user on Spring Websocket mentions a web socket configuration change, to register the /user prefix. In your case I guess it means to replace
registry.enableSimpleBroker("/topic", "/queue");
with
registry.enableSimpleBroker("/topic", "/queue", "/user");
He also says that in controller you don't need the /user prefix because it is added automatically. So you could try this:
template.convertAndSendToUser(principal.getName(), "/queue/notification", notifications);
and this:
template.convertAndSendToUser(principal.getName(), "/user/queue/notification", notifications);
On the client side you need to provide the username that you used to connect to server. You might insert it directly:
stompClient.subscribe('/user/naila/queue/notification', ...)
or get it from a header. But Markus says at How to send websocket message to concrete user? that even here you don't need the username, so it might work like this:
stompClient.subscribe('/user/queue/notification', ...)
Seems you are missing a slash in your destination:
template.convertAndSendToUser(principal.getName(), "/queue/notification", notifications);

Categories

Resources