How to implement a request-response pattern with paho mqtt java? - 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.

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

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

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?

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.

Netty, how to implement an HTTP connection limiter which sends a response (503) prior to closing channel

Currently in my pipeline I have a simple handler to reject connections when my server gets overloaded:
public class RequestFilter extends SimpleChannelHandler {
#Override
public void channelConnected(final ChannelHandlerContext ctx, final ChannelStateEvent e) throws Exception {
requestLimiter(ctx, e);
super.channelConnected(ctx, e);
}
}
private void requestLimiter(final ChannelHandlerContext ctx, final ChannelStateEvent e) {
if(threshold < counter) {
ctx.getChannel().close();
}
}
When the counter exceeds the threshold, the channel is closed, that all seems to work fine.
Now I'd like to enhance this by first sending an HTTP 503 response prior to closing the channel. What i've tried so far is this method below, instead of closing the channel immediatly I try to write a response to the channel and then handle closing it with a channelfuture listener so it's closed when the write is complete. However whats happening is I get a ton of exceptions about "already sent a response, can't send more than 1" followed by stack overflow.
protected void sendResponse(Channel channel, HttpResponse response) {
if (channel.isConnected()) {
channel.write(response).addListener(ChannelFutureListener.CLOSE);
log.trace("response sent");
} else if (!channel.isConnected()) {
log.trace("attempted to send response, but the channel was closed");
} else {
log.trace("Not sure why this would happen");
}
}
Any thoughts or examples I could look at? thanks
Edit: stacktrace
java.lang.IllegalStateException: cannot send more responses than requests
at org.jboss.netty.handler.codec.http.HttpContentEncoder.writeRequested(HttpContentEncoder.java:104)
at org.jboss.netty.handler.timeout.WriteTimeoutHandler.writeRequested(WriteTimeoutHandler.java:152)
at org.jboss.netty.handler.stream.ChunkedWriteHandler.flush(ChunkedWriteHandler.java:262)
at org.jboss.netty.handler.stream.ChunkedWriteHandler.handleDownstream(ChunkedWriteHandler.java:119)
at org.jboss.netty.handler.execution.ExecutionHandler.handleDownstream(ExecutionHandler.java:165)
at org.jboss.netty.channel.Channels.write(Channels.java:605)
at org.jboss.netty.channel.Channels.write(Channels.java:572)
at org.jboss.netty.channel.AbstractChannel.write(AbstractChannel.java:245)
at com.test.RequestFilter.sendResponse(RequestFilter.java:98)
I don't think it is sending multiple responses. I think that it's [trying] to send one, which is one more than the number of requests which is zero because the the event is being triggered by the connect channel event and the pipeline has yet to see any http requests.
I would change your code to not do this on connect, but rather, trigger the 503 response on the first request. If the channel is then closed, adios client, but if the client's first request sneaks in under the threshold, then remove the bouncer from the pipeline (assuming that once a client is in, they're in for good).
Make sense ?

Categories

Resources