2

The documentation for Spring WebSockets states:

4.4.13. User Destinations

An application can send messages targeting a specific user, and Spring’s STOMP support recognizes destinations prefixed with "/user/" for this purpose. For example, a client might subscribe to the destination "/user/queue/position-updates". This destination will be handled by the UserDestinationMessageHandler and transformed into a destination unique to the user session, e.g. "/queue/position-updates-user123". This provides the convenience of subscribing to a generically named destination while at the same time ensuring no collisions with other users subscribing to the same destination so that each user can receive unique stock position updates.

Is this supposed to work in a multi-server environment with RabbitMQ as broker?

As far as I can tell, the queue name for a user is generated by appending the simpSessionId. When using the recommended client library stomp.js this results in the first user getting the queue name "/queue/position-updates-user0", the next gets "/queue/position-updates-user1" and so on. This in turn means the first users to connect to different servers will subscribe to the same queue ("/queue/position-updates-user0").

The only reference to this I can find in the documentation is this:

In a multi-application server scenario a user destination may remain unresolved because the user is connected to a different server. In such cases you can configure a destination to broadcast unresolved messages to so that other servers have a chance to try. This can be done through the userDestinationBroadcast property of the MessageBrokerRegistry in Java config and the user-destination-broadcast attribute of the message-broker element in XML.

But this only makes the it possible to communicate with a user from a different server than the one where the web socket is established.

I feel I'm missing something? Is there anyway to configure Spring to be able to safely use MessagingTemplate.convertAndSendToUser(principal.getName(), destination, payload) in a multi-server environment?

Andy
  • 21
  • 4

1 Answers1

0

If they need to be authenticated (I assume their credentials are stored in a database) you can always use their database unique user id to subscribe to.

What I do is when a user logs in they are automatically subscribed to two topics an account|system topic for system wide broadcasts and account|<userId> topic for specific broadcasts.

You could try something like notification|<userid> for each person to subscribe to then send messages to that topic and they will receive it.

Since user Ids are unique to each user you shouldn't have an issue within a clustered environment as long as each environment is hitting the same database information.

Here is my send method:

  public static boolean send(Object msg, String topic) {
    try {
      String destination = topic;
      String payload = toJson(msg); //jsonfiy the message 
      Message<byte[]> message = MessageBuilder.withPayload(payload.getBytes("UTF-8")).build();
      template.send(destination, message);
      return true;
    } catch (Exception ex) {
      logger.error(CommService.class.getName(), ex);
      return false;
    }
  }

My destinations are preformatted so if i want to send a message to user with id of one the destinations looks something like /topic/account|1.

Ive created a ping pong controller that tests websockets for users who connect to see if their environment allows for websockets. I don't know if this will help you but this does work in my clustered environment.

/**
   * Play ping pong between the client and server to see if web sockets work
   * @param input the ping pong input
   * @return the return data to check for connectivity
   * @throws Exception exception
   */
  @MessageMapping("/ping")
  @SendToUser(value="/queue/pong", broadcast=false) // send only to the session that sent the request
  public PingPong ping(PingPong input) throws Exception {
    int receivedBytes = input.getData().length;
    int pullBytes = input.getPull();

    PingPong response = input;
    if (pullBytes == 0) {
      response.setData(new byte[0]);
    } else if (pullBytes != receivedBytes)  {
      // create random byte array
      byte[] data =  randomService.nextBytes(pullBytes);
      response.setData(data);
    }
    return response;
  }
locus2k
  • 2,802
  • 1
  • 14
  • 21
  • Thanks for the reply. That might be a solution. So in this case I assume you can't use SimpMessagingTemplate.convertAndSendToUser(principal.getName(), destination, payload) and have Spring resolve the destinations for you? – Andy Mar 05 '18 at 14:07
  • You should be able to I use `SimpleMessagingTemplate` and works fine in my clustered environment but i customize my destinations before sending it so my destination string looks something like `/topic/account|1` if i want to send a message to user with id of 1 but i dont use the `convertAndSendTouser` method. I just use the simple `send` method. See my edit for my send method. – locus2k Mar 05 '18 at 14:26
  • 1
    Your solution is a good workaround, but I'm curious if there's any way to handle this without having to bypass the built-in user destination mechanism. I've updated my question. – Andy Mar 06 '18 at 13:27
  • I added a code snippet that uses a controller to send messages back and forth to the client to test for websockets. – locus2k Mar 06 '18 at 13:57