How to acknowledge websocket (spring websocket + stomp + sockjs) message delivery - java

I am creating a simple client-server demo using websockets. In this demo client can subscribe to different topics. Whenever server has anything to send, it will just send the message on the appropriate topic and only subscribed clients will get the same message.
My server should get acknowledgment whenever it sends the message to any topic, but I am little bit confused, how should I get this acknowledgment of the sent message as the sending method returns void
below is my WebSocketConfig class,
#Configuration
#EnableWebSocketMessageBroker
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {
#Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.enableSimpleBroker("/testApp");
config.setApplicationDestinationPrefixes("/app");
}
#Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/message").withSockJS();
}
}
Below is how topic subscription happens at client side,
var topic = '/testApp/testTopic';
stompClient.subscribe(topic, function(greeting){
});
below is how my server sending message to particular topic,
String message = "test message";
this.template.convertAndSend(topic, new Chat(message));
above method(i.e. this.template.convertAndSend(topic, new Chat(message));) returns void
at the end I am able to successfully send message to all/particular subscribed client but don't know how to get assure whether the message is successfully delivered at the other end or not?

Related

How to implement a request-response pattern with paho mqtt java?

I'd like to use Paho MQTT Java and implement some "request-response" pattern. What I mean by that is that in some instances the client has to talk to the server and expects a specific answer to a specific request. It feels like this lib has everything needed to match a request and a response, but I can't quite put it together.
I found I can retrieve a token via deliveryComplete, and that I can do a setActionCallback on this token. But first, I'm not entirely sure what an "action" means. Then, if it means what I think it means, how to get the actual response to my request from there?
sampleClient.setCallback(new MqttCallback() {
#Override
public void connectionLost(Throwable cause) {
}
#Override
public void messageArrived(String topic, MqttMessage message) throws Exception {
System.out.println(topic);
System.out.println("setCallback: "+ message.toString());
}
#Override
public void deliveryComplete(IMqttDeliveryToken token) {
System.out.println(token.getMessageId());
token.setActionCallback(new IMqttActionListener() {
#Override
public void onSuccess(IMqttToken asyncActionToken) {
var response = asyncActionToken.getResponse();
try {
System.out.println(new String(response.getPayload()));
} catch (MqttException e) {
e.printStackTrace();
}
}
#Override
public void onFailure(IMqttToken asyncActionToken, Throwable exception) {
}
});
}
});
MQTT is overkill for any kind of 1 Request - 1 Response....that is what APIs are for. MQTT is best for one node sending out data to one or more nodes who are tapped into the Topic. The sending node (Publisher) doesn't need to know about any of the receiving nodes (Subscribers)...they just all have to agree on the Topic used.
That being said, you could implement a "Query" Publish, and look for a "Reply" MQTT message back that you would subscribe to. I have done this it the past where multiple nodes can benefit from knowing what the Reply is. So it might look something like this: Publish - q/system/status and Subscribe to r/system/status or r/system/#.
This can be done using MQTT5.
This is how it works:
Suppose you have two MQTT clients, one of which acts as a server.
client1, client2(server).
client1 publishes a message with a response topic in the message property.
client2 receives the message reads the message properties and publishes the response on that topic
client 1 gets the message on the response topic.
Here is a small implementation:
Use the following dependency:
<dependency>
<groupId>org.apache.camel</groupId>
<artifactId>camel-paho-mqtt5</artifactId>
<version>3.17.0</version>
</dependency>
Client 1 Publish:
public void sendMessage(String messageStr){
MqttMessage message = new MqttMessage();
MqttProperties properties = new MqttProperties();
properties.setResponseTopic("response");
properties.setCorrelationData("correlation".getBytes());
message.setProperties(properties);
message.setPayload(messageStr.getBytes());
message.setQos(1);
message.setRetained(true);
try {
client.publish("test",message);
} catch (MqttException e) {
e.printStackTrace();
}
}
Client 2 (Server) Subscriber MessageListener:
import lombok.extern.slf4j.Slf4j;
import org.eclipse.paho.mqttv5.client.IMqttClient;
import org.eclipse.paho.mqttv5.client.IMqttMessageListener;
import org.eclipse.paho.mqttv5.common.MqttMessage;
import org.eclipse.paho.mqttv5.common.packet.MqttProperties;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
#Slf4j
#Component
public class MessageListener implements IMqttMessageListener {
#Autowired
private IMqttClient client;
#Override
public void messageArrived(String topic, MqttMessage message) throws Exception {
log.info("Topic : {}, Message: {}", topic,message.toString());
MqttProperties properties = new MqttProperties();
if(message.getProperties()!=null && StringUtils.hasText(message.getProperties().getResponseTopic())){
MqttMessage responseMessage = new MqttMessage();
properties.setCorrelationData(message.getProperties().getCorrelationData());
responseMessage.setProperties(properties);
responseMessage.setPayload("Response".getBytes());
responseMessage.setQos(1);
responseMessage.setRetained(true);
client.publish(message.getProperties().getResponseTopic(),responseMessage);
}
}
}
Then Client 1 should subscribe to topic "response" to receive the response from client 2.
Here is more on this:
http://www.steves-internet-guide.com/mqttv5-request-response/
There is NO end to end delivery notification in the MQTT protocol. The delivery complete callback is only called to notify that the message has travelled between the publishing client and the broker, it says nothing about if the message has been delivered to any down stream subscribers (there could 0 to many other clients subscribed to a given topic).
Once a message reaches the broker it may even be queued for an offline client with a persistent session.
Even assuming just 1 client receives the message, any reply would be a totally separate message not linked to the first request.
MQTT is not natively a request/response system like say HTTP, it's a way to send messages to 0 to many clients that have subscribed to a give topic.
MQTT v5 starts to introduce the concepts of requests/response to MQTT, but even then it's just an extra optional slot in the header to include a topic to reply on. And you can set a collation id in the message header to link the response message to the request.

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.

Java based websocket client for Spring based websocket server

I have this websocket server developed using Spring Boot. The server is working fine with a js based client.
#Configuration
#EnableWebSocketMessageBroker
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {
#Override
public void configureMessageBroker(final MessageBrokerRegistry config) {
config.enableSimpleBroker("/topic");
config.setApplicationDestinationPrefixes("/app");
}
#Override
public void registerStompEndpoints(final StompEndpointRegistry registry) {
registry.addEndpoint("/chat").withSockJS();
registry.addEndpoint("/chat");
}
}
The controller:
#Controller
public class ChatController {
#MessageMapping("/chat")
#SendTo("/topic/messages")
public OutputMessage send(final Message message) throws Exception {
final String time = new SimpleDateFormat("HH:mm").format(new Date());
return new OutputMessage(message.getFrom(), message.getText(), time);
}
}
This is the server side. Now, for the client, I have created a #ClientEndpoint and when I connect to the URI "ws://localhost:8080/spring-mvc-java/chat", I am able to establish a connection and I can see that the #OnOpen callback of #ClientEndpoint is triggered.
However, it seems that the userSession.getAsyncRemote().sendText(message) does not have any effect. I don't see the response from the server.
I can see the js client is:
Connecting to the server var socket = new SockJS('/spring-mvc-java/chat')
Subscribing stompClient.subscribe('/topic/messages',...
Send the message stompClient.send("/app/chat",...
I am able to achieve the first step. How to achieve the 2nd and the 3rd step in a Java based client?
Thanks
First of all you need a websocketclient and websocketstompclient
WebSocketClient client = new StandardWebSocketClient();
WebSocketStompClient stompClient = new WebSocketStompClient(client);
stompClient.setMessageConverter(new MappingJackson2MessageConverter());
You have to custom handler from StompSessionHandler for
getPayloadType
handleFrame
afterConnected
handleException
handleTransportError
methods
StompSessionHandler sessionHandler = new CustomStompSessionHandler();
You can connect your sockets like this. You can accomplish sending and receiving messages
StompSession stompSession=stompClient.connect("ws://localhost:8080/chat",sessionHandler).get();
This two trigger your websocket topic/messages give you messages through the sockets
/app/chat sending Message to sockets
stompSession.subscribe("/topic/messages", sessionHandler);
stompSession.send("/app/chat", new Message("Hi", "user"));
do you want like this?

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
}
}

Wait for Response on REST Request JAVA

I created a java project with glassfish and posted a simple REST GET service like this:
#Path("/get")
public class Rest {
#Path("test/{user}/")
#GET
public String getTest(#PathParam("user") String id) throws IOException {
//send message to websocket client and wait for response
//return "websocket client response";
}
}
this works fine.
I also have a websocket server implementation in the same project. This implementation allows me to send data to the connected clients.
This is my WebSocket implementation:
#ServerEndpoint("/websocket")
public class WebSocketServer {
#OnOpen
public void onOpen(Session userSession){
System.out.println("Se conecto un nuevo cliente");
Modelo.getInstance().users.add(userSession);
}
#OnMessage
public void onMessage(String message,Session userSession) throws IOException{
String username=(String) userSession.getUserProperties().get("username");
if(username==null){
userSession.getUserProperties().put("username", message);
userSession.getBasicRemote().sendText(Modelo.getInstance().buildJsonData("Servidor","nuevo cliente conectado como: "+message));
}else{
Iterator<Session> iterator=Modelo.getInstance().users.iterator();
while(iterator.hasNext()){
iterator.next().getBasicRemote().sendText(Modelo.getInstance().buildJsonData(username,message));
}
}
}
#OnClose
public void onClose(Session userSession){
Modelo.getInstance().users.remove(userSession);
}
#OnError
public void onError(Throwable t){
t.printStackTrace();
}
}
this works fine too.
When the REST method is called i can send successfully a message to one of my websockets clients.
The thing is that i want to return as the REST response, the data that the WebSocket client sends me.
So...
1)Receive REST GET request in Java Server
2)Send via websocket to the client i want to get the info from
3)Respond the REST GET request with the message the websocket client send me.
How can i accomplish this?
[SOLVED]?
I found a way to do this, please i would like to know what do you think.
I found this article: here about async rest reponses.
So i implemented, its the first thing come to my mind, i save the websocket client message in an array, and the REST request is responded when the array has a message.
#Path("/resource")
#GET
public void asyncGet(#Suspended final AsyncResponse asyncResponse) throws IOException {
Modelo.getInstance().enviarMensaje("5", "escenas");
new Thread(new Runnable() {
#Override
public void run() {
String result = veryExpensiveOperation();
asyncResponse.resume(result);
}
private String veryExpensiveOperation() {
while(Modelo.getInstance().responses.size()==0){
}
String result=Modelo.getInstance().responses.get(0);
Modelo.getInstance().responses.clear();
return result;
// ... very expensive operation
}
}).start();
}
I know there a more things to validate this reponses, but at first it works.
I also edit the websockerserver.java to save in the array the response.
Thank you very much
REST works over HTTP which is a request/response model of communication. Which means you need to send a request in order to get a response. Web Sockets is a full duplex socket model. This means the client or the server can send a message as long as the connection is up. The challenge is you're trying to send a response with REST without a request. You could queue the response from the web socket and then send it back with the next REST response. This would however require the REST client to poll the server periodically since you would not have an indication of when the Web Socket client responded.

Categories

Resources