0

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

Bryan E
  • 41
  • 2
  • 5

1 Answers1

0

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

Bryan E
  • 41
  • 2
  • 5
  • Thank you for the solution but I have some questions. 1) It looks like a winding road. Will there be a performance loss? 2) Are there any better solution. Why RedisTemplate not supporting convertAndSendToUser() function. – Orhan Gazi Oct 15 '21 at 13:43