Search code examples
spring-bootwebsocketspring-websocketstomp

How to limit the number of stomp clients in Spring, subscribing to a specific topic, based on a condition?


I have been researching for a way to limit the number of clients who can subscribe to a specific stomp topic but have not yet understood, which could be the right approach according to my needs.

My use case is a game, which I am developing in Angular (ng2-stompjs stomp client) and Spring Boot Websockets (for the moment, the Spring in-memory message broker is in use).

The idea is that a user can be connected and subscribed to a "/lobby" stomp topic, and there he sees the opened game rooms, that could be in different statuses. for example, in-play or not started yet due to the low number of players joined. I'd like to intercept and programmatically restrict a possible subscription of a client, to a specific "/room/{roomId}" topic, IF the MAX number of players has been reached, for example, 4. There could also be some simple client-side validation to restrict that, but I believe only client-side is not sufficient

So my main questions are: How can a specific stomp topic subscription be intercepted in Spring? Is it possible to return to the client-requestor some kind of error message that subscription could not be done?

I'd really appreciate your help, thank you in advance!


Solution

  • You could implement a StompEventListener which listens for subscriptions, in this we can have map mapping a destination(room number) versus the count of number of players in that particular room. if the count is already at max reject the subscription.

    @Service
    class StompEventListener() {
       private Map<String, int> roomIdVsPlayerCount = new HashMap<>();
       
       @EventListener
       public void handleSubscription(SessionSubscribe event) {
         StompHeaderAccessor accessor = StompHeaderAccessor.wrap(event.getMessage());
         String destination = accessor.getDestination();
    
         String roomId = destination.substring(...); //Parsed RoomID
         if(roomIdVsPlayerCount.get(roomId) == MAX_ALLOWED_PLAYERS) {
           //Throw exception which will terminate that client connection 
             or, send an error message like so:
           simpMessagingTemplate.convertAndSend(<some_error_message>);
           return;
         }
         //So it is not at maximum do further logic to actually subscribe 
           user and
         roomIdVsPlayerCount.get(roomId) += 1; 
       }
    
       @EventListener
       public void handleUnsubscription(SessionUnsubscribe event) {
        ...
       }
    }
    
    

    Useful References:

    1. SessionSubscribeEvent (For handling the subscriptions)
    2. ConvertAndSend. (For sending the error messages to client.)

    EDIT

    Please try sending the exception from a channel Interceptor since the above did not send the exception , so that it gets propagated to the client. The map we defined earlier can be defined as a bean in a separate class accessible(with @Autowired) to both event handler(for incrementing and decrementing) and TopicSubscriptionInterceptor(for validation).

    @Component
    class TopicSubscriptionInterceptor implements ChannelInterceptor {
        @Override
        public Message<?> preSend(Message<?> message, MessageChannel channel){
          StompHeaderAccessor accessor = StompHeaderAccessor.wrap(message);
          String destination = accessor.getDestination();
    
          String roomId = destination.substring(...); //Parsed RoomID
          if(roomIdVsPlayerCount.get(roomId) == MAX_ALLOWED_PLAYERS) {
            //Throw exception which will terminate that client connection 
          }
         
         //Since it is not at limit continue 
        
        }
    }
    

    Useful reference for implementing a TopicSubscriptionInterceptor: TopicSubscriptionInterceptor