springspring-bootwebsocketstompsockjs

Spring Boot - WebSocket - Doesn't show subscribers


I was going over the basic Spring Boot WebSocket Tutorial: https://spring.io/guides/gs/messaging-stomp-websocket/

I decided to modify it to print out how many users are subscribed to a channel in the console but couldn't figure it out for hours. I've seen a few StackOverflow posts but they don't help. The last one I check was this: https://stackoverflow.com/a/51113021/11200149 which says to add try this:

@Autowired private SimpUserRegistry simpUserRegistry;

public Set<SimpUser> getUsers() { 
    return simpUserRegistry.getUsers();
}

So, I added the above to my controller, and here is the change:

@Controller
public class GreetingController {
    @Autowired
    private SimpUserRegistry userRegistry;

    @MessageMapping("/hello")
    @SendTo("/topic/greetings")
    public Greeting greeting(HelloMessage message) throws Exception {
        Set<SimpUser> subscribedUsers = userRegistry.getUsers();
        System.out.println("User amount: " + subscribedUsers.size()); // always prints: 0
        return new Greeting("Hello, " + HtmlUtils.htmlEscape(message.getName()) + "!");
    }
}

This always prints 0:

System.out.println("User amount: " + subscribedUsers.size());

I'm coming from Socket.IO so maybe things work a bit differently because I've seen people implement their own manual Subscription Service classes. In socket.io this would be a piece of cake so I would assume Spring Boot would have this, but I just can't seem to find it.

Edit: This post does a great explanation for this problem. Principal is null for every Spring websocket event


Solution

  • Maybe you can try to add custom HandshakeHandler class into registry and override the determineUser method to return the Principal object that containing subscriber name so that the SimpUserRegistry can work properly.

    If you would like to see the effect, the below is what I'm trying.

    app.js (sending out a user name through request parameter)

    function connect() {
        var socket = new SockJS('/gs-guide-websocket?name=' + $('#name').val());
        stompClient = Stomp.over(socket);
        stompClient.connect({}, function (frame) {
            setConnected(true);
            console.log('Connected: ' + frame);
            stompClient.subscribe('/topic/greetings', function (greeting) {
                showGreeting(JSON.parse(greeting.body).content);
            });
        });
    }
    

    custom class extends DefaultHandshakeHandler.class

    @Component
    public class WebSocketHandShakeHandler extends DefaultHandshakeHandler {
    
        @Override
        protected Principal determineUser(ServerHttpRequest request, WebSocketHandler wsHandler, Map<String, Object> attributes) {
            ServletServerHttpRequest servletRequest = (ServletServerHttpRequest) request;
            HttpServletRequest httpServletRequest = servletRequest.getServletRequest();
            String name = httpServletRequest.getParameter("name");
    
            return new MyPrincipal(name);
        }
    }
    

    custom object implement Principal.class

    @Data
    @NoArgsConstructor
    @AllArgsConstructor
    public class MyPrincipal implements Principal {
        private String name;
    }
    

    WebSocketConfig.class

    @Configuration
    @EnableWebSocketMessageBroker
    public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
        @Autowired
        private WebSocketHandShakeHandler webSocketHandShakeHandler;
    
        @Override
        public void configureMessageBroker(MessageBrokerRegistry config) {
            config.enableSimpleBroker("/topic");
            config.setApplicationDestinationPrefixes("/app");
        }
    
        @Override
        public void registerStompEndpoints(StompEndpointRegistry registry) {
            registry.addEndpoint("/gs-guide-websocket")
                    .setHandshakeHandler(webSocketHandShakeHandler)
                    .withSockJS();
        }
    }
    

    Show all subscribers

    @RestController
    public class ApiController {
        @Autowired
        private SimpUserRegistry simpUserRegistry;
    
        @GetMapping("/users")
        public List<String> connectedEquipments() {
            return this.simpUserRegistry
                    .getUsers()
                    .stream()
                    .map(SimpUser::getName).toList();
        }
    }
    

    Result

    enter image description here

    By the way, you can check the DefaultSimpUserRegistry.class to observe the process of putting name into subscribers user map. enter image description here