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
}
}
Related
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.
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.
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.
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.
Is there a way to use WebSockets with SockJS client and Spring 4 server but not using STOMP?
Based on this tutorial from Spring's website, I know how to set up a WebSocket based application using Stomp and Spring 4. On the client side, we have:
var socket = new SockJS('/hello');
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 on the server side, we have the following in the controller:
#MessageMapping("/hello")
#SendTo("/topic/greetings")
public Greeting greeting(HelloMessage message) throws Exception {
Thread.sleep(3000); // simulated delay
return new Greeting("Hello, " + message.getName() + "!");
}
Now, I understand that #MessageMapping("/hello") ensures that if a message is sent to a destination "/hello", then the greeting() method will be called. And since the stompClient is subscribed to "/topic/greetings", the #SendTo("/topic/greetings") will send the message back to the stompClient.
But the problem with the above is that stompClient is a Stomp object. And I want to simply use sock.send('test'); and have it delivered to my server's destination. And I want to do #SendTo("myownclientdestinationmap"), I can receive it by
sock.onmessage = function(e) {
console.log('message', e.data);
};
So, any way to do this with Spring 4, SockJS and without Stomp? Or does Spring 4 WebSocket only supports Stomp?
Spring supports STOMP over WebSocket but the use of a subprotocol is not mandatory, you can deal with the raw websocket. When using a raw websocket, the message sent lacks of information to make Spring route it to a specific message handler method (we don't have any messaging protocol), so instead of annotating your controller, you'll have to implement a WebSocketHandler:
public class GreetingHandler extends TextWebSocketHandler {
#Override
public void handleTextMessage(WebSocketSession session, TextMessage message) {
Thread.sleep(3000); // simulated delay
TextMessage msg = new TextMessage("Hello, " + message.getPayload() + "!");
session.sendMessage(msg);
}
}
And then add your handler to the registry in the configuration (you can add more than one handler and use SockJS for fallback options):
#Configuration
#EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
#Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(greetingHandler(), "/greeting").withSockJS();
}
#Bean
public WebSocketHandler greetingHandler() {
return new GreetingHandler();
}
}
The client side will be something like this:
var sock = new SockJS('http://localhost:8080/greeting');
sock.onmessage = function(e) {
console.log('message', e.data);
}