1
votes

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?

1

1 Answers

0
votes

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