Detecting WebSocket Connects and Disconnects in Spring 4

Presence detection is an essential feature in real-time applications like games, collaborative apps, chats… In order to do that, we should be able to detect when a client connects and disconnects.

STOMP frames

We’ll start by understanding how STOMP works. To establish a TPC connection the client sends a CONNECT frame similar to the following one:

CONNECT
company:1
accept-version:1.1,1.0
heart-beat:10000,10000

^@

If the server accepts the connection request, it will respond with a CONNECTED frame:

CONNECTED
heart-beat:10000,10000
session:session-l9cWyaqq8AwEXldiFE4Zdw
server:RabbitMQ/3.2.4
version:1.1

^@

In the same fashion, the client can terminate the connection by sending a DISCONNECT frame:

DISCONNECT

^@

Now that we know how STOMP works, let’s see how to detect these frames in Spring 4.

First approach: ChannelInterceptors

All STOMP messages are passed to message channels and can be intercepted with a channel interceptor. The support configuration classes provide two hooks in order to customize the inbound channel (for incoming STOMP messages like CONNECT / DISCONNECT) and the outbound channel (for outgoing STOMP messages like CONNECTED). Here’s our presence channel interceptor:


public class PresenceChannelInterceptor extends ChannelInterceptorAdapter {

	private final Log logger = LogFactory.getLog(PresenceChannelInterceptor.class);

	@Override
	public void postSend(Message<?> message, MessageChannel channel, boolean sent) {

		StompHeaderAccessor sha = StompHeaderAccessor.wrap(message);

		// ignore non-STOMP messages like heartbeat messages
		if(sha.getCommand() == null) {
			return;
		}

		String sessionId = sha.getSessionId();

		switch(sha.getCommand()) {
			case CONNECT:
				logger.debug("STOMP Connect [sessionId: " + sessionId + "]");
				break;
			case CONNECTED:
				logger.debug("STOMP Connected [sessionId: " + sessionId + "]");
				break;
			case DISCONNECT:
				logger.debug("STOMP Disconnect [sessionId: " + sessionId + "]");
				break;
			default:
				break;

		}
	}
}

By wrapping the message with the StompHeaderAccessor, we can access the message type (among many other properties like the headers of the STOMP message) and decide what to do accordingly. But you may wonder at this point how we correlate connection and disconnection messages. How do we know these belong to the same user? This is quite straightforward as every message has a sessionId which can be used to correlate them.

We just need to add the interceptor to the client inbound and outbound channel using the hooks we have available:

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {

	public void registerStompEndpoints(StompEndpointRegistry registry) {
		registry.addEndpoint("/ws").withSockJS();
	}

	@Bean
	public PresenceChannelInterceptor presenceChannelInterceptor() {
		return new PresenceChannelInterceptor();
	}

	@Override
	public void configureClientInboundChannel(ChannelRegistration registration) {
		registration.setInterceptors(presenceChannelInterceptor());
	}

	@Override
	public void configureClientOutboundChannel(ChannelRegistration registration) {
		registration.taskExecutor().corePoolSize(8);
		registration.setInterceptors(presenceChannelInterceptor());
	}

	@Override
	public void configureMessageBroker(MessageBrokerRegistry registry) {
		registry.enableSimpleBroker("/queue/", "/topic/");
		registry.setApplicationDestinationPrefixes("/app");
	}
}

A better approach: ApplicationEvents

Even if the previous approach works, there’s a better way to do it. By using ApplicationEvents, we are going to have a more decoupled solution with no extra configuration. We can implement listeners for the following events: SessionConnectEvent, SessionConnectedEvent, SessionDisconnectEvent (these events were introduced in Spring 4.0.3, released on 26/03/2014).

Here’s an example of a listener for the SessionConnectEvent:

public class StompConnectEvent implements ApplicationListener<SessionConnectEvent> {

	private final Log logger = LogFactory.getLog(StompConnectEvent.class);

	public void onApplicationEvent(SessionConnectEvent event) {
		StompHeaderAccessor sha = StompHeaderAccessor.wrap(event.getMessage());

		String  company = sha.getNativeHeader("company").get(0);
		logger.debug("Connect event [sessionId: " + sha.getSessionId() +"; company: "+ company + " ]");
	}
}

In the above example, we also show how to get access to custom STOMP headers that can be passed to the connect method in stomp.js:

        var socket = new SockJS('/ws');
        stompClient = Stomp.over(socket);

        stompClient.connect({company: "1"}, function(frame) {

         });

As you can see, using ApplicationEvents is a clean and decoupled way to detect connects and disconnects having access to all the required properties and sticking to the Spring programming model.