Websockets & Spring Boot Application

Yair Harel
3 min readMay 23, 2018

--

Recently I had to implement a Websocket support to a spring boot application.

The application is clustered ( many nodes behind a Load Balancer) so it requires more complicate solution than just a simple java server which can send and receive websocket frames. Because of that, we decided to use a STOMP Broker which will allow us to scale for multiple nodes.

There are few Brokers which implements the STOMP protocol, we have chosen RabbitMQ since it has larger community and it seems more reliable(it also has nice administration UI).

We have created a RabbitMQ Docker with STOMP enabled:

FROM rabbitmqRUN rabbitmq-plugins enable — offline rabbitmq_management
RUN rabbitmq-plugins enable — offline rabbitmq_stomp
RUN rabbitmq-plugins enable — offline rabbitmq_web_stomp
EXPOSE 15671 15672 15674 61613

In our spring application we configured the STOPM broker:

@Configuration
@EnableWebSocketMessageBroker
@EnableWebSocket
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer
implements ApplicationListener<BrokerAvailabilityEvent> {
private static Logger logger = LoggerFactory.getLogger(WebSocketConfig.class);


@Value(“${ws.broker.host}”)
private String brokerHost;

@Value(“${ws.broker.port}”)
private int brokerPort;

@Value(“${ws.broker.user}”)
private String brokerUserName;

@Value(“${ws.broker.password}”)
private String brokerUserPassword;




@Override
public void configureMessageBroker(final MessageBrokerRegistry config) {

logger.info(“Going to start relay broker on host {}”, brokerHost);

config.enableStompBrokerRelay(“/topic”, “/queue”)
.setAutoStartup(true)
.setClientLogin(brokerUserName)
.setClientPasscode(brokerUserPassword)
.setSystemLogin(brokerUserName)
.setSystemPasscode(brokerUserPassword)
.setRelayHost(brokerHost)
.setRelayPort(brokerPort)

.setVirtualHost(“/”);

config.setApplicationDestinationPrefixes(“/websocket”);
}


@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint(“/ws”).setAllowedOrigins(“*”);
}
@Override
public void onApplicationEvent(BrokerAvailabilityEvent e) {
logger.info(“broker available: “ + e.isBrokerAvailable());
}
}

We also added a Stomp listener for debugging and logging the STOMP events:

@Component
public class StompEventListener implements ApplicationListener<SessionConnectEvent> {
private static Logger logger = LoggerFactory.getLogger(StompEventListener.class);

@Override
public void onApplicationEvent(SessionConnectEvent event) {

String userId = event.getUser().getName();
StompHeaderAccessor sha = StompHeaderAccessor.wrap(event.getMessage());
boolean isConnect = sha.getCommand()== StompCommand.CONNECT;
boolean isDisconnect = sha.getCommand()== StompCommand.DISCONNECT;
logger.debug(“Connect: “+ isConnect +”,disconnect:” +isDisconnect +
“, event [sessionId: “ + sha.getSessionId() +”;” + userId +” ,command=” +sha.getCommand() );

}

@EventListener
public void onSocketConnected(SessionConnectedEvent event) {
StompHeaderAccessor sha = StompHeaderAccessor.wrap(event.getMessage());
logger.info(“[Connected] “ + sha.getUser().getName());
}
@EventListener
public void onSocketDisconnected(SessionDisconnectEvent event) {
StompHeaderAccessor sha = StompHeaderAccessor.wrap(event.getMessage());
logger.info(“[Disonnected] “ + sha.getUser().getName());
}
}

After this point, we had a working websocket server. We were able to send and receive to/from clients websocket frames.

Our client is written in React Native using native WS support in javascript and we implemented our own STOMP client ( we faced some problems integrating stomp.js with react-native and we were having problems debugging it, so we ended up writing our own adopting it to RN and stomp1.2 protocol).

We have started to see weird behaviors:

  1. Connection lost: the connection is not stable and its disconnect after few minutes.
  2. Android is not able to connect.

Connection Lost

  1. Tomcat does not update session timestamp on websocket frames. this caused that a session was timedout and closed the WS connection. In our app, we switched to Redis based sessions (due to other requirements we had) and that solved the issue since Tomcat does not kill session connections when the session is expired ( Redis session expired is only TTL on the session key).
  2. Part of the STOMP protocol is heartbeat (optional), this is to close a connection once one of the sides is not responsive. The protocol for heartbeat is a “new line” (\n), in case the client sends something other than that (we had a client bug which sent \\n) the STOMP broker kills the connection after the timeout.

Android Connection Issue

The stomp protocol requires that every frame will end with a null (/0). Seems like on Android the frames are sent without this last character which breaks the protocol. So we wrote a WebSocketHandlerDecorator which adds the \0 to the end of the payload (except the heartbeat)

public class CustomWebSocketHandlerDecorator extends WebSocketHandlerDecorator {public CustomWebSocketHandlerDecorator(WebSocketHandler delegate) {
super(delegate);
}


@Override
public void handleMessage(final WebSocketSession session, final WebSocketMessage<?> message) throws Exception {
if (message instanceof TextMessage) {
TextMessage msg = (TextMessage) message;
String payload = msg.getPayload();


// only add \00 if not present (iOS / Android)
if (!payload.substring(payload.length() — 1).equals(“\u0000”) && !payload.equals(“\n”)) {
super.handleMessage(session, new TextMessage(payload + “\u0000”));
return;
}
}
super.handleMessage(session, message);
}
}

and we override the decorator method in the WebSocketConfig.java

@Override
public void configureWebSocketTransport(WebSocketTransportRegistration registration) {
registration.addDecoratorFactory(new WebSocketHandlerDecoratorFactory() {
@Override
public WebSocketHandler decorate(WebSocketHandler webSocketHandler) {
return new CustomWebSocketHandlerDecorator(webSocketHandler);
}
});
}

Now our websocket is solid and reliable.

--

--