13
votes

We have a Spring over WebSockets connection that we're passing a CONNECT frame:

CONNECT\naccept-version:1.2\nheart-beat:10000,10000\n\n\u0000

Which the handler acknowledges, starts a new session, and than returns:

CONNECTED
version:1.2
heart-beat:0,0

However, we want the heart-beats so we can keep the WebSocket open. We're not using SockJS.

I stepped through the Spring Message Handler:

StompHeaderAccessor [headers={simpMessageType=CONNECT, stompCommand=CONNECT, nativeHeaders={accept-version=[1.2], heart-beat=[5000,0]}, simpSessionAttributes={}, simpHeartbeat=[J@5eba717, simpSessionId=46e855c9}]

After it gets the heart-beat (native header), it sets what looks like a memory address simpHeartbeat=[J@5eba717, simpSessionId=46e855c9}]

Of note, after the broker authenticates:

Processing CONNECT session=46e855c9 (the sessionId here is different than simpSessionId)?

When running earlier TRACE debugging I saw a notice "Scheduling heartbeat..." or something to that effect...though I'm not seeing it now?

Any idea what's going on?

Thanks

I have found the explanation in the documentation:

SockJS Task Scheduler stats from thread pool of the SockJS task scheduler which is used to send heartbeats. Note that when heartbeats are negotiated on the STOMP level the SockJS heartbeats are disabled.

Are SockJS heartbeats different than STOMP heart-beats?

3

3 Answers

10
votes

Yes SockJS heartbeats are different. Fundamentally the same thing but their purpose in the SockJS protocol are to ensure that the connection doesn't look like it's "dead" in which case proxies can close it pro-actively. More generally a heartbeat allows each side to detect connectivity issues pro-actively and clean up resources.

When using STOMP and SockJS at the transport layer there is no need to have both which is why the SockJS heartbeats are turned off if STOMP heartbeats are in use. However you're not using SockJS here.

You're not showing any configuration but my guess is that you're using the built-in simple broker which does not automatically send heartbeats. When configuring it you will see an option to enable heartbeats and you also need to set a task scheduler.

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
         // ...
    }

    @Override
    public void configureMessageBroker(MessageBrokerRegistry registry) {
        registry.enableStompBrokerRelay(...)
                .setTaskScheduler(...)
                .setHeartbeat(...);
    }

}
15
votes

Starting Spring 4.2 you can have full control, from the server side, of the heartbeat negotiation outcome using Stomp over SockJS with the built-in SimpleBroker:

public class WebSocketConfigurer extends AbstractWebSocketMessageBrokerConfigurer {

    @Override
    public void configureMessageBroker(MessageBrokerRegistry config) {
        ThreadPoolTaskScheduler te = new ThreadPoolTaskScheduler();
        te.setPoolSize(1);
        te.setThreadNamePrefix("wss-heartbeat-thread-");
        te.initialize();

        config.enableSimpleBroker("/")
                /**
                 * Configure the value for the heartbeat settings. The first number
                 * represents how often the server will write or send a heartbeat.
                 * The second is how often the client should write. 0 means no heartbeats.
                 * <p>By default this is set to "0, 0" unless the {@link #setTaskScheduler
                 * taskScheduler} in which case the default becomes "10000,10000"
                 * (in milliseconds).
                 * @since 4.2
                 */
                .setHeartbeatValue(new long[]{heartbeatServer, heartbeatClient})
                .setTaskScheduler(te);
    }

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint(.....)
                .setAllowedOrigins(....)
                .withSockJS();
    }
}
7
votes

We got same problem with Spring, Websockets, STOMP and Spring Sessions - no heartbeats and Spring session may expire while websocket doesn't receive messages on server side. We ended up with enable STOMP heartbeats from browser every 20000ms and add SimpMessageType.HEARTBEAT to Spring sessionRepositoryInterceptor matches to keep Spring session last access time updated on STOMP heartbeats without messages. We had to use AbstractSessionWebSocketMessageBrokerConfigurer as a base to enable in-build Spring session and websocket session binding. Spring manual, second example. In official example Spring session is updated on inbound websocket CONNECT/MESSAGE/SUBSCRIBE/UNSUBSCRIBE messages, but not heartbeats, that's why we need to re-configure 2 things - enable at least inbound heartbeats and adjust Spring session to react to websocket heartbeats

public class WebSocketConfig extends AbstractSessionWebSocketMessageBrokerConfigurer<ExpiringSession> {

   @Autowired
   SessionRepositoryMessageInterceptor sessionRepositoryInterceptor;

   @Override
   public void configureMessageBroker(MessageBrokerRegistry config) {
       sessionRepositoryInterceptor.setMatchingMessageTypes(EnumSet.of(SimpMessageType.CONNECT,
               SimpMessageType.MESSAGE, SimpMessageType.SUBSCRIBE,
               SimpMessageType.UNSUBSCRIBE, SimpMessageType.HEARTBEAT));

       config.setApplicationDestinationPrefixes(...);
       config.enableSimpleBroker(...)
             .setTaskScheduler(new DefaultManagedTaskScheduler())
             .setHeartbeatValue(new long[]{0,20000});
   }
}

Another way we tried is some re-implementing of SessionRepositoryMessageInterceptor functionality to update Spring sessions last access time on outbound websocket messages plus maintain websocket session->Spring session map via listeners, but code above did the trick.