Create Spring Boot + WebSocket Application using STOMP and SockJS

Previously we already walk through Spring Boot application with:

Let's revisit what we done so far:

WebSocket and STOMP Protocols

WebSocket allowing us to implement bidirectional communication between applications. WebSocket itself is a low-level protocol and even difficult to implement more complex applications like how to route or process a message without writing additional code. The good news is, the WebSocket specification allowing several sub-protocols operate on a higher level. One of them is STOMP (Simple Text-based Messaging Protocol) that allows STOMP clients (not only specific to Java) to talk with any message broker supporting the protocol.

Now we will add more flavor to our application. We will add one library called SockJS. Why we need SockJS if we already have WebSocket + STOMP? One of the reason is, SockJS provides best available fallback options whenever WebSocket connection fails or unavailable.

SockJS

SockJS is WebSocket emulation library. It gives you a coherent, cross-browser, Javascript API which creates a low latency, full duplex, cross-domain communication channel between the browser and the web server, with WebSockets or without.

We are still using the same project used in two previous article.

Changes in WebSocketMessageBrokerConfig

One minor change in the endpoint configuration (refer to here) is, to add one more endpoint with SockJS. This is to enable SockJS fallback options. :

@Override public void registerStompEndpoints(StompEndpointRegistry registry) { registry.addEndpoint("/broadcast"); // it is OK to leave it here registry.addEndpoint("/broadcast").withSockJS(); }

Two endpoints /broadcast with and "without" SockJS are working in our example.

Changes in Client

To make use of SockJS in the client code (refer to here), you require to include sockjs.js in html. Since we are using webjars, you need to add following dependency in Maven's pom.xml:

<!-- https://mvnrepository.com/artifact/org.webjars/sockjs-client --> <dependency> <groupId>org.webjars</groupId> <artifactId>sockjs-client</artifactId> <version>1.1.2</version> </dependency>

Then add the javascript in the html:

<script th:src="@{/webjars/sockjs-client/1.1.2/sockjs.js}" type="text/javascript"></script>

And changes the javascript, from Stomp client

function connect() { var url = '<url here>'; stompClient = Stomp.client(url); ... }

Changes into Stomp over SockJS, as like following code:

function connect() { var socket = new SockJS('/broadcast'); stompClient = Stomp.over(socket); ... }

Test Changes

Run our WebSocket application, and open http://localhost:8080/sockjs-broadcast in the browser. Here one screenshot of my "broadcast" test:

http://localhost:8080/sockjs-broadcast

http://localhost:8080/sockjs-broadcast

But you can see even connecting to http://localhost:8080/stomp-broadcast (without SockJS) also able to connect and broadcast to the same endpoint:

http://localhost:8080/stomp-broadcast

http://localhost:8080/stomp-broadcast

Now, let me show you something interesting. This is taken under Network - Messages from Mozilla Firefox Web Developer tool:

Firefox Web Developer - Network - Messages

Firefox Web Developer - Network - Messages

What you see is client receiving heartbeat from server. By default, Spring SockJS send a heartbeat on every 25 seconds (if the connection idle - no other messages are sent on the connection). We can override the default setting 25 seconds by set our custom timing:

// custom heartbeat, every 60 sec registry.addEndpoint("/broadcast").withSockJS().setHeartbeatTime(60_000);

Complete Codes

Here the full codes for your reference:

WebSocketMessageBrokerConfig.java
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketMessageBrokerConfig implements WebSocketMessageBrokerConfigurer {

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

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/broadcast");  // it is OK to leave it here
        // registry.addEndpoint("/broadcast").withSockJS();
        // custom heartbeat, every 60 sec
        registry.addEndpoint("/broadcast").withSockJS().setHeartbeatTime(60_000);
        
    }
}
                    

WebSocketBroadcastController.java
@Controller
public class WebSocketBroadcastController {

    // ... code truncated for efficiency ...
    
    @GetMapping("/sockjs-broadcast")
    public String getWebSocketWithSockJsBroadcast() {
        return "sockjs-broadcast";
    }
    
    @MessageMapping("/broadcast")
    @SendTo("/topic/broadcast")
    public ChatMessage send(ChatMessage chatMessage) throws Exception {
        return new ChatMessage(chatMessage.getFrom(), chatMessage.getText(), "ALL");
    }
}
                    

Function getWebSocketWithSockJsBroadcast() will return sockjs-broadcast.html:

sockjs-broadcast.html
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
    <head>
        <title>WebSocket With STOMP & SockJS Broadcast Example</title>
        <th:block th:include="fragments/common.html :: headerfiles"></th:block>        
    </head>
    <body>
        <div class="container">
            <div class="py-5 text-center">
                <h2>WebSocket</h2>
                <p class="lead">WebSocket Broadcast - with STOMP & SockJS.</p>
            </div>
            <div class="row">
                <div class="col-md-6">
                    <div class="mb-3">
                        <div class="input-group">
                            <input type="text" id="from" class="form-control" placeholder="Choose a nickname"/>
                            <div class="btn-group">
                                <button type="button" id="connect" class="btn btn-sm btn-outline-secondary" onclick="connect()">Connect</button>
                            <button type="button" id="disconnect" class="btn btn-sm btn-outline-secondary" onclick="disconnect()" disabled>Disconnect</button>
                            </div>                        
                        </div>
                    </div>
                    <div class="mb-3">
                        <div class="input-group" id="sendmessage" style="display: none;">
                            <input type="text" id="message" class="form-control" placeholder="Message">
                            <div class="input-group-append">
                                <button id="send" class="btn btn-primary" onclick="send()">Send</button>
                            </div>
                        </div>
                    </div>
                </div>
                <div class="col-md-6">
                    <div id="content"></div>
                    <div>
                        <span class="float-right">
                            <button id="clear" class="btn btn-primary" onclick="clearBroadcast()" style="display: none;">Clear</button>
                        </span>
                    </div>                    
                </div>
            </div>
        </div>

        <script th:src="@{/webjars/sockjs-client/1.1.2/sockjs.js}" type="text/javascript"></script>
        <script th:src="@{/webjars/stomp-websocket/2.3.3-1/stomp.js}" type="text/javascript"></script>
        <script type="text/javascript">
            var stompClient = null;
            
            function setConnected(connected) {
                $("#from").prop("disabled", connected);
                $("#connect").prop("disabled", connected);
                $("#disconnect").prop("disabled", !connected);
                if (connected) {
                    $("#sendmessage").show();
                } else {
                    $("#sendmessage").hide();
                }
            }
            
            function connect() {
                var socket = new SockJS('/broadcast');
                stompClient = Stomp.over(socket);
                stompClient.connect({}, function () {
                    stompClient.subscribe('/topic/broadcast', function (output) {
                        showBroadcastMessage(createTextNode(JSON.parse(output.body)));
                    });
                    
                    sendConnection(' connected to server');                
                    setConnected(true);
                }, function (err) {
                    alert('error' + err);
                });                
            }

            function disconnect() {
                if (stompClient != null) {
                    sendConnection(' disconnected from server'); 
                    
                    stompClient.disconnect(function() {
                        console.log('disconnected...');
                        setConnected(false);
                    });                    
                }                
            }
            
            function sendConnection(message) {
                var from = $("#from").val();
                var text = from + message;
                clientSend({'from': 'server', 'text': text});
            }
            
            function clientSend(json) {
                stompClient.send("/app/broadcast", {}, JSON.stringify(json));
            }
            
            function send() {
                var from = $("#from").val();
                var text = $("#message").val();
                clientSend({'from': from, 'text': text});                
                $("#message").val("");
            }

            function createTextNode(messageObj) {
                return '<div class="row alert alert-info"><div class="col-md-8">' +
                        messageObj.text +
                        '</div><div class="col-md-4 text-right"><small>[<b>' +
                        messageObj.from +
                        '</b> ' +
                        messageObj.time + 
                        ']</small>' +
                        '</div></div>';
            }
            
            function showBroadcastMessage(message) {
                $("#content").html($("#content").html() + message);
                $("#clear").show();
            }
            
            function clearBroadcast() {
                $("#content").html("");
                $("#clear").hide();
            }
        </script>
    </body>
</html>
                    

For WebSocketExampleApplication, ChatMessage, and StringUtils, please refer to previous article in the series.


References: