Websocket with ActiveMQ support multi session? - spring-boot

I have spring boot application use websocket and embeded ActiveMQ, when user (TestUser) subscribe /user/TestUser/reply in two different browser at the same time ,then send message to him one browser received the another not , send again new one the second receive but first one not and so on…… .
What I expected when send message to /user/TestUser/reply , if he is open two browsers as the same time should receive the message in two browser at the same time.
POM.xml
<!-- WebSocket libraries -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-activemq</artifactId>
</dependency>
<dependency>
<groupId>org.apache.activemq</groupId>
<artifactId>activemq-stomp</artifactId>
</dependency>
<dependency>
<groupId>org.apache.activemq</groupId>
<artifactId>activemq-kahadb-store</artifactId>
<scope>runtime</scope>
</dependency>
WebSocketConfig
#Configuration
#EnableWebSocketMessageBroker
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {
#Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.setApplicationDestinationPrefixes("/app")
.setUserDestinationPrefix("/user")
.enableStompBrokerRelay("/user");
}
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/websocket").addInterceptors(new HttpHandshakeInterceptor()).withSockJS();
}
#Override
public void configureClientInboundChannel(ChannelRegistration registration) {
registration.setInterceptors(new TopicSubscriptionInterceptor());
}
#Bean
public BrokerService broker() throws Exception {
BrokerService broker = new BrokerService();
broker.setSchedulePeriodForDestinationPurge(10000);
broker.addConnector("stomp://localhost:61613");
PolicyMap policyMap = new PolicyMap();
PolicyEntry policyEntry = new PolicyEntry();
policyEntry.setGcInactiveDestinations(true);
policyEntry.setInactiveTimeoutBeforeGC(30000);
policyEntry.setQueue(">");
List<PolicyEntry> entries = new ArrayList<PolicyEntry>();
entries.add(policyEntry);
policyMap.setPolicyEntries(entries);
broker.setDestinationPolicy(policyMap);
return broker;
}
UI
function connect() {
var socket = new SockJS('/websocket');
stompClient = Stomp.over(socket);
stompClient.connect({}, function (frame) {
setConnected(true);
console.log('Connected: ' + frame);
stompClient.subscribe('/user/TestUser/reply', function (greeting) {
message = greeting;
showGreeting(greeting);
}, {'ack': 'client-individual'});
});
}

If you change your UI code to use topic, like this:
stompClient.subscribe('/topic/reply/'+ frame.headers['user-name'], function (greeting) { ...
and configure relay to /topic
config.setApplicationDestinationPrefixes("/app")
.enableStompBrokerRelay("/topic");
and send messages like :
simpMessageTemplate.convertAndSend("/topic/reply/" + principal, payload, headers);
than every connected client will recieve his message copy.
frame.headers['user-name'] some spring magic header with current logged in principal, returned on connect. don't know who adds it, maybe spring-security.
If you want to do it with anon user, you have to generate user-id on client side, store it somewhere ( localstorage or cookie ) and pass to server side to use as principal name.
EDIT:
If you want to restict connection to topic to only one user, you can do this like this (in TopicSubscriptionInterceptor):
#Override
public Message<?> preSend(Message<?> message, MessageChannel channel) {
StompHeaderAccessor sha = StompHeaderAccessor.wrap(message);
switch (sha.getCommand()) {
case SUBSCRIBE:
case SEND:
if(!sha.getDestination().equals("/topic/reply/" + sha.getUser().getName()) return null;
break;
}
...
return message;
}

Related

How to send messages to one of the multiple topics based on condition in Spring Cloud Stream Kafka application

Currently i have a spring clound funtion which consumes a topic and publish in to another topic. Now I have multiple topics and need to publish message to one of the multiple topic based on certain checks from spring cloud function. How can I achieve this? Here is current implementation.
#Bean("producerBean")
public Function<Message<SourceMessage>, Message<SinkMessage>> producerBean(SinkService<SourceMessage> sinkService) {
return sinkService::processMessage;
}
#Service("SinkService")
public class SinkService<T> {
public Message<SinkMessage> processMessage(Message<SourceMessage> message) {
log.info("Message consumed at {} \n{}", message.getHeaders().getTimestamp(), message.getPayload());
try {
if (message.getPayload().isManaged()) {
/*
Need to add one more check here.
if (type==2)
send to topic1
else if(type==4)
send to topic2
else
Just log the type, do not send to any topic.
*/
Message<SinkMessage> output = new GenericMessage<>(new SinkMessage());
output.getPayload().setPayload(message.getPayload());
return output;
}
} catch (Exception exception) {
exception.printStackTrace();
}
return null;
}
}
application.properties
spring.cloud.stream.kafka.binder.brokers=${bootstrap.servers}
spring.cloud.stream.kafka.binder.configuration.enable.idempotence=false
spring.cloud.stream.binders.test_binder.type=kafka
spring.cloud.stream.bindings.producerBean.binder=test_binder
spring.cloud.stream.bindings.producerBean-in-0.destination=${input-destination}
spring.cloud.stream.bindings.producerBean-in-0.group=${input-group}
spring.cloud.stream.bindings.producerBean-out-0.destination=topic1
spring.cloud.stream.bindings.producerBean-out-1.destination=topic2
pom.xml
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-stream-kafka</artifactId>
<version>3.2.5</version>
</dependency>
You can use StreamBridge with kafka-topicname and spring-cloud will bind it automatically in runtime. That approach also auto creates topic if that not exist, you can turn it off.
#Autowired
private final StreamBridge streamBridge;
public void sendDynamically(Message message, String topicName) {
streamBridge.send(route, topicName);
}
https://docs.spring.io/spring-cloud-stream/docs/current/reference/html/spring-cloud-stream.html#_streambridge_and_dynamic_destinations

Why isnt my sockets onsubscribe event getting used?

I am using java springboot with maven in order to get the spring boot starter socket package. My clients are using angular with stompjs and sockjs-client.
I am trying to set up a simple web socket application that allows for multiple rooms based on a roomId. When a client joins a room they should receive the last five messages sent in that room.
My Springboot app has three classes, the basic Application.java that I use to run the app, a web socket config class and a web socket controller:
#Controller
public class WebSocketController {
private final SimpMessagingTemplate template;
#Autowired
WebSocketController(SimpMessagingTemplate template){
this.template = template;
}
#MessageMapping("/meeting/{roomId}")
private void sendMessageTpPrivateRoom(
String message,
#DestinationVariable String roomId
) throws IOException {
System.out.println("message sent to: " + roomId);
this.template.convertAndSend("/meeting/" + roomId, message);
addToHistory(roomId, message);
}
#SubscribeMapping("/meeting/{roomId}")
public String chatInit(#DestinationVariable String roomId) {
System.out.println("Someone joined room: " + roomId);
return getLastFiveMessages(roomId);
}
}
#Configuration
#EnableWebSocketMessageBroker
public class WebSocketConfiguration
extends AbstractWebSocketMessageBrokerConfigurer {
#Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/socket")
.setAllowedOrigins("*")
.withSockJS();
}
#Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.setApplicationDestinationPrefixes("/app")
.enableSimpleBroker("/meeting");
}
}
my clients are subscribing to the socket like so:
stompClient.subscribe(`app/meeting/${roomId}`, (message) => {
if (message.body) {
console.log(message.body);
messages += '<br>' + message.body;
}
});
and sending messages like so:
this.stompClient.send(`/app/meeting/${this.roomId}` , {}, message);
The message sending and handling is working great, when I set up three clients, two in room one, and one in room two, the room two messages are not being seen in room one and the room one messages are seen by both clients.
However the on subscribe event is not firing no matter what room I join. It is very necessary that when a client joins room one, they should receive some sort of history of that room. Any advice as to why my SubscribeMapping method is not being triggered when a client subscribes to the room?
The /meeting part will be implicitly added to URL you provide when subscribing. So your mapping will look like this:
#SubscribeMapping("/${roomId}")
public String chatInit(#DestinationVariable String roomId) {
System.out.println("Someone joined room: " + roomId);
return getLastFiveMessages(roomId);
}
Source: https://docs.spring.io/spring/docs/5.0.0.BUILD-SNAPSHOT/spring-framework-reference/html/websocket.html

Send websocket message to user across dynos

I have a spring boot application running on heroku. I make use of websockets for sending messages to and from client and server for a specific user . I use spring boot's SimpMessagingTemplate.convertAndSendToUser to send and receive messages, which works fine for when a user needs get a message back from the server. I use Heroku session affinity which means that even if I scale up the number of sessions the user and websocket still share the same session.
My problem comes when I need a user to send a message to another user. It works fine if both users are sharing the session, but not if the message will not come through.
Is it possible to send a message from one user to another across different sessions using, SimpMessagingTemple? Or would I need to use a message broker, eg Redis.
I was looking into implementing sending a message using StringRedisTemplate but not sure how to send a message to a particular user.
private SimpMessagingTemplate messagingTemplate;
#Autowired
public MessageController(SimpMessagingTemplate messagingTemplate) {
this.messagingTemplate = messagingTemplate;
}
#MessageMapping("/secured/user-in")
public void sendToDevice(Message msg, #AuthenticationPrincipal User principal) throws Exception {
if (msg.getTo() != null) {
String email = msg.getTo();
Message out = new Message();
out.setMsg(msg.getMsg());
out.setFrom(msg.getFrom());
out.setTo(msg.getTo());
out.setSentTime(new Date());
out.setStatus(msg.getStatus());
messagingTemplate.convertAndSendToUser(email, "/secured/topic", out);
}
}
JS
function connect() {
var socket = new SockJS('/secured/user-in');
ST.stompClient = Stomp.over(socket);
var headers = {};
headers[ST.getHeader()] = ST.getToken();
ST.getStompClient().connect(headers, function (frame) {
retries = 1;
console.log('Connected: ' + frame);
ST.getStompClient().subscribe('/user/secured/topic', function (event){
var msg = JSON.parse(event.body);
showMessage(msg.msg);
});
});
}
UPDATE 1
I am guessing I could do something like this, as done here:
SimpMessageHeaderAccessor headerAccessor = SimpMessageHeaderAccessor
.create(SimpMessageType.MESSAGE);
headerAccessor.setSessionId(sessionId);
headerAccessor.setLeaveMutable(true);
messagingTemplate.convertAndSendToUser(sessionId,"/queue/something", payload,
headerAccessor.getMessageHeaders());
But how could I get the session id of another user, I am using Redis to store session info: #EnableRedisHttpSession
I had my terminology a bit mixed up I was trying to send a message to another user on another dyno rather than session.
Ended up using redis sub/pub.
So when a message is receive by the controller it is published to redis, and the redis MessageListenerAdapter envokes the convertAndSendToUser method.
#MessageMapping("/secured/user-in")
public void sendToDevice(Message msg, #AuthenticationPrincipal User principal) throws Exception {
publishMessageToRedis(msg);
}
private void publishMessageToRedis(Message message) throws JsonProcessingException {
ObjectMapper objectMapper = new ObjectMapper();
String messageString = objectMapper.writeValueAsString(message);
stringRedisTemplate.convertAndSend("message", messageString);
}
redis config
#Bean
RedisMessageListenerContainer container( MessageListenerAdapter chatMessageListenerAdapter) throws URISyntaxException {
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
container.setConnectionFactory(connectionFactory());
container.addMessageListener(chatMessageListenerAdapter, new PatternTopic("message"));
return container;
}
#Bean("chatMessageListenerAdapter")
MessageListenerAdapter chatMessageListenerAdapter(RedisReceiver redisReceiver) {
return new MessageListenerAdapter(redisReceiver, "receiveChatMessage");
}
public class RedisReceiver {
private static final Logger LOG = LogManager.getLogger(RedisReceiver.class);
private final WebSocketMessageService webSocketMessageService;
#Autowired
public RedisReceiver(WebSocketMessageService webSocketMessageService) {
this.webSocketMessageService = webSocketMessageService;
}
// Invoked when message is publish to "chat" channel
public void receiveChatMessage(String messageStr) throws IOException {
ObjectMapper objectMapper = new ObjectMapper();
Message message = objectMapper.readValue(messageStr, Message.class);
webSocketMessageService.sendChatMessage(message);
}
}
#Service
public class WebSocketMessageService {
private final SimpMessagingTemplate template;
private static final Logger LOG = LogManager.getLogger(WebSocketMessageService.class);
public WebSocketMessageService(SimpMessagingTemplate template) {
this.template = template;
}
public void sendChatMessage(Message message) {
template.convertAndSendToUser(message.getTo(), "/secured/topic", message);
}
}
Solution was based off this git repository

How publish event for more instance from command side axon

I tried to implement application with cqrs and event sourcing with axon framework. I implement command side and query part as a separate micro-service and replicate(scale up) query micro-service. I use message broker as RabbitMq. If the command part publish event that not update all query micro-service. It work as round robin way. how can i update all micro-services same time.
Here is my dependency file
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<dependency>
<groupId>org.axonframework</groupId>
<artifactId>axon-amqp</artifactId>
<version>${axon.version}</version>
</dependency>
<dependency>
<groupId>org.axonframework</groupId>
<artifactId>axon-spring-boot-starter</artifactId>
<version>${axon.version}</version>
</dependency>
this is my configs in command side
#Bean
public Exchange exchange() {
return ExchangeBuilder.fanoutExchange("SeatReserveEvents").build();
}
#Bean
public Queue queue() {
return QueueBuilder.durable("SeatReserveEvents").build();
}
#Bean
public Binding binding() {
return BindingBuilder.bind(queue()).to(exchange()).with("*").noargs();
}
#Autowired
public void configure(AmqpAdmin admin) {
admin.declareExchange(exchange());
admin.declareQueue(queue());
admin.declareBinding(binding());
}
This is application.yml
axon:
amqp:
exchange: SeatReserveEvents
This is command side configurations
#Bean
public SpringAMQPMessageSource statisticsQueue(Serializer serializer) {
return new SpringAMQPMessageSource(new DefaultAMQPMessageConverter(serializer)) {
#RabbitListener(queues = "SeatReserveEvents")
#Override
public void onMessage(Message arg0, Channel arg1) throws Exception {
super.onMessage(arg0, arg1);
}
};
}
this is handler
#Component
#ProcessingGroup("statistics")
public class EventLoggingHandler
{
#EventHandler
protected void on(SeatResurvationCreateEvent event) {
System.err.println(event);
}
#EventHandler
protected void on(SeatReservationUpdateEvent event) {
System.err.println(event);
}
}
this is application.yml
axon:
eventhandling:
processors:
statistics.source: statisticsQueue
I'd say this is more an AMQP/RabbitMQ configuration setting than an Axon Framework specific question. That said, you'd want to set up RabbitMQ to not do Round Robin, but Pub/Sub, like described in this tutorial here.
I do however have another, more Axon Framework specific response in mind.
Why immediately publish your events on a queue, if you could also pull the events from the store directly? So, you'd have TrackingEventProcessors on the Query Side of you application, which pull events from the event store as they get appended by the Command Side of your application.
That's how a monolith version of an Axon Framework application incorporating CQRS would initially look like any way. Hence the simplest next step to split up that CQRS application in a Command and Query side, would be to leave the way of receiving events as is, without adding the queue in between.
If you've got specific requirements to publish over a queue however, or you just prefer to use a queue instead of letting the Query applications pull from the Event Store directly, please disregard this comment and revert back to the RabbitMQ tutorial.
we need to change RabbitMq configuration to publish event for more instance from command side axon. For that we have to change configuration in publisher side as below.
#Bean
public FanoutExchange fanoutExchange() {
FanoutExchange exchange = new FanoutExchange("SeatReserveEvents");
return exchange;
}
#Autowired
public void configure(AmqpAdmin admin) {
admin.declareExchange(fanoutExchange());
}
and next thing is subscriber side we have to change bean like below
#Bean
public SpringAMQPMessageSource statisticsQueue(Serializer serializer) {
return new SpringAMQPMessageSource(new DefaultAMQPMessageConverter(serializer)) {
#RabbitListener(bindings = #QueueBinding(
value = #Queue,
exchange = #Exchange(value ="SeatReserveEvents",type = ExchangeTypes.FANOUT),
key = "orderRoutingKey")
)
#Override
public void onMessage(Message arg0, Channel arg1) throws Exception {
super.onMessage(arg0, arg1);
}
};
}
now we can replicate consumer for more instance. This pattern is publisher/subscriber pattern. and exchange type is fanout

Sending message to specific user not working in STOMP, sockjs, spring

I have an issue with Spring and Stomp, the messages are not being received by the client. I have tried everything and not sure what can be causing it!
I have the following set up so far. The first subscribe /event/{eventId} works fine and is receiving messages. The specific user subscription reaches the controller but the response from convertandsendtouser is not being received. Any ideas?
function connect() {
var socket = new SockJS('/events');
stompClient = Stomp.over(socket);
stompClient.connect({}, function(frame) {
console.log('Connected: ' + frame);
stompClient.subscribe('/topic/' + eventIdStomp, function(messageOutput) {
showMessageOutput(JSON.parse(messageOutput.body));
alert("Subscribed to event id");
});
stompClient.subscribe("/user/queue/reply", function(responseOutput) {
alert("Hey it worked, subscribed to user queue");
showMessageOutput(JSON.parse(responseOutput.body));
});
});
}
function sendMessage() {
var text = document.getElementById('text').value;
stompClient.send("/app/events/" + eventIdStomp, {},
JSON.stringify({'from':from, 'text':text}));
stompClient.send("/app/events/personal", {},
JSON.stringify({'from':from, 'text':text}));
}
and on the server side
#MessageMapping("/events/personal")
public void personal(Message message, Principal principal) throws Exception {
System.out.println("im in side personal methods");
String time = new SimpleDateFormat("HH:mm").format(new Date());
/* Set author */
User user = (User) ((Authentication) principal).getPrincipal();
if(user!=null) {
System.out.println("in inside user id");
/* Check message content in Knowledge Base */
// If there is any indication that the message contains material against the code of conduct,
// then a message should be sent to that person only and not to everybody.
OutputMessage custom_response = new OutputMessage(user.getUsername(), "I can see you...", time);
simpMessagingTemp.convertAndSendToUser(user.getUsername(), "/queue/reply", custom_response);
// End of KB
System.out.println("after mnessage sent");
}
}
with the config
#Configuration
#EnableWebSocketMessageBroker
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {
#Override
public void configureMessageBroker(MessageBrokerRegistry config) {
/* RabbitMQ As Broker, using CloudAMQP as a cloud service */
config.enableStompBrokerRelay("/queue", "/topic").setRelayHost("swan.rmq.cloudamqp.com")
/* System admin login */
.setSystemLogin("yyy").setSystemPasscode("xxx")
.setVirtualHost("yyy")
/* for presentation purposes, client can login as system admin */
.setClientLogin("yyy").setClientPasscode("xxx");
config.setApplicationDestinationPrefixes("/app");
}
/*
* When we create a connection from client, this is the URL which clients
* connect to Websocket URL prefix
*/
#Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/events").withSockJS().setSessionCookieNeeded(true);
}
}

Resources