Cannot send user message with Spring Websocket - java

I read Spring docs about user destinations.
I would like to use convertAndSendToUser method to send a message only to a particular user.
This is the java code:
#Controller
public class WebsocketTest {
#Autowired
public SimpMessageSendingOperations messagingTemplate;
#PostConstruct
public void init(){
ScheduledExecutorService statusTimerExecutor=Executors.newSingleThreadScheduledExecutor();
statusTimerExecutor.scheduleAtFixedRate(new Runnable() {
#Override
public void run() {
messagingTemplate.convertAndSendToUser("myuser","/queue/test", new Return("test"));
}
}, 5000,5000, TimeUnit.MILLISECONDS);
}
}
This is the client js code:
var socket = new WebSocket('ws://localhost:8080/hello');
stompClient = Stomp.over(socket);
stompClient.connect("myuser","mypass", function(frame) {
setConnected(true);
console.log('Connected: ' + frame);
stompClient.subscribe('/topic/greetings', function(greeting){
showGreeting(JSON.parse(greeting.body).value);
});
stompClient.subscribe('/user/queue/test', function(greeting){
showGreeting(JSON.parse(greeting.body).value);
});
});
First of all is it correct to conside the user value this one?
stompClient.connect("myuser",...
Why this test doesn't work? This user did not receveive any message.
If I switch destination to /topic/greetings and change method to convertAndSend() this works, but obviously as broadcast and not only to particular user as requested.
Little update
I tried to setup a reply to single user with this code:
#MessageMapping("/hello")
#SendToUser("/queue/test")
public Return test(){
return new Return("test");
}
This works, this is not what I need becase this reply only to a message from client. Seems that I cannot use convertAndSendToUser() for unsollicited messaged from server.

Subscribing to /user/queue/test on the client side and using convertAndSendToUser to send a message to the /topic/test topic looks about right.
Now how are you getting the "myUser" value?
You can get this value from either request.getUserPrincipal() during the handshake, or inject the Principal when receiving a message; the sessionId works as well here. You can check this commit for more details on the matter.

Maybe its late to answer the question but for the people who may face similar issue, here is what I was missing:
The websocket URL which you are connecting to should also be a secured URL. If its not then there is no mapping between the user and the session and hence you are not able to send message to the user using convertAndSendToUser().
So, for e.g. you are connecting to the endpoint with URL pattern /hello then it should be a secured one.

Related

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.

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);

How to create a multipart form post request handler in vertx

Hello I am using vertx in java on a standalone app to read a port in my web app. I want to get the post data from a form.
My web app has a form with a post method that I am looking to read.
My logic is
Create verticle
Create http Server
Create handler request ( it does not take all the parameters in my
code)
Listen to httpServer
Expect Multipart Request in Handler
Multimap and get form attributes
I dont know what I am missing , I am new to vertx .
For some reason I cannot post code on here.
My code:
https://pastebin.com/A1hjXFb6
Make sure that your HTML form is having enctype="multipart/form-data" attribute.
Then in your vertx code:
public class loginLogic extends AbstractVerticle {
//Vertx vertx;
private HttpServer httpServer = null;
#Override
public void start() {
Router router = Router.router(vertx);
router.route().handler(BodyHandler.create().setUploadsDirectory("uploads"));
router.post("/form").handler(ctx -> {
ctx.response().putHeader("Content-Type", "text/plain");
ctx.response().setChunked(true);
for (FileUpload f : ctx.fileUploads()) {
System.out.println("f");
ctx.response().write("Filename: " + f.fileName());
ctx.response().write("\n");
ctx.response().write("Size: " + f.size());
}
ctx.response().end();
});
vertx.createHttpServer().requestHandler(router::accept).listen(8080);
}
}

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

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.

Send STOMP ERROR from Spring Websocket program

I have a Spring Websocket Stomp application that accepts SUBSCRIBE requests.
In application I have a handler for SUBSCRIBE, that is,
#Component
public class SubscribeStompEventHandler implements ApplicationListener<SessionSubscribeEvent> {
#Override
public void onApplicationEvent(SessionSubscribeEvent event) {}
}
that I use to validate subscription.
I would check something in the onApplicationEvent and send STOMP ERROR message back to client from this function.
I found this recipe How to send ERROR message to STOMP clients with Spring WebSocket? but I need to understand how to get outboundChannel.
I tried also the following code:
public void sendStompError(SimpMessagingTemplate simpMessagingTemplate, String sessionId, String topic, String errorMessage) {
StompHeaderAccessor headerAccessor = StompHeaderAccessor.create(StompCommand.ERROR);
headerAccessor.setMessage(errorMessage);
headerAccessor.setSessionId(sessionId);
headerAccessor.setLeaveMutable(true);
simpMessagingTemplate.convertAndSendToUser(sessionId, topic, new byte[0], headerAccessor.getMessageHeaders());
}
and I tried topic to be some subsciption topic and /queue/error topic. However I did not see messages propagating to client.
In Client, I use:
stompClient.connect(headers
, function (frame) {
console.log("Conn OK " + url);
}, function (error) {
console.log("Conn NOT OK " + url + ": " + JSON.stringify(error));
});
}
and my goal is to have function(error) called when I send STOMP ERROR.
Please advice me how exactly I can send proper STOMP ERROR, e.g. by getting Outboundchannel.
You can send ERROR Message like this:
StompHeaderAccessor headerAccessor = StompHeaderAccessor.create(StompCommand.ERROR);
headerAccessor.setMessage(error.getMessage());
headerAccessor.setSessionId(sessionId);
this.clientOutboundChannel.send(MessageBuilder.createMessage(new byte[0], headerAccessor.getMessageHeaders()));
The following is just enough to inject that clientOutboundChannel:
#Autowired
#Qualifier("clientOutboundChannel")
private MessageChannel clientOutboundChannel;
Just because clientOutboundChannel bean is declared in the AbstractMessageBrokerConfiguration.
UPDATE
STOMP ERROR always closes connection? I am getting this effect. Code 1002.
Yes, it is. See StompSubProtocolHandler.sendToClient():
if (StompCommand.ERROR.equals(command)) {
try {
session.close(CloseStatus.PROTOCOL_ERROR);
}
catch (IOException ex) {
// Ignore
}
}

Categories

Resources