A Quick Tutorial of SpringBoot with WebSocket Capabilities

In this article, we will introduce STOMP and SockJS and show how to integrate WebSocket with SpringBoot.

Overview

What is WebSocket?

Similar to HTTP, WebSocket is a communication protocol that provides two-way, full-duplex communication between a server and client. WebSocket enables streams of messages on top of TCP. Once a WebSocket connection is established the connection stays open until the client or server decides to close this connection.

A typical use of WebSocket could be when an application involves multiple users communicating with each other, WebSocket is preferred over HTTP in this scenario, because it provides a persistent connection for the frequent messages, as for HTTP connection, it will be closed once a request is served and needed opening again for next communication.

What is STOMP?

STOMP stands for Streaming Text Oriented Messaging Protocol, as wiki, STOMP is a simple text-based protocol, designed for working with messages -oriented middleware(MOM). It provides an interoperable wire format that allows STOMP clients to talk with any message broker supporting the protocol. 

What is SockJS?

WebSocket is not supported in all browsers yet and maybe precluded by restrictive network proxies. This is why Spring provides fallback options that emulate the WebSocket API as close as possible based on the SockJS protocol which is designed for use in browsers.

The goal of SockJS is to let applications use a WebSocket API but fall back to non-WebSocket alternatives when necessary at runtime, i.e. without the need to change application code.

Integrating WebSocket with SpringBoot

We introduced the integration of SQS, Redis with spring-boot application in the past. In this chapter, we will introduce some of the components that enable the WebSocket capabilities, below are the steps we will take to implement our WebSocket services:

BackEnd:

  1. Maven Dependencies
  2. Enable STOMP over WebSocket
  3. Create the Message Model
  4. Create the Message-Handling Controller
  5. CORS support in SockJS

FrontEnd:

  1. Create a Browser Client

And the final step is to test our WebSocket services.

Now we’re looking into these step by step and implement our WebSocket.

BackEnd:

Maven Dependencies
        <parent>
            <groupId>org.springframework.boot</groupId>      
            <artifactId>spring-boot-starter-parent</artifactId>         
            <version>2.1.6.RELEASE</version>    
       </parent>

   <!-->websocket<-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-websocket</artifactId>
        </dependency>
        <dependency>
            <groupId>org.webjars</groupId>
            <artifactId>webjars-locator-core</artifactId>
        </dependency>
        <dependency>
            <groupId>org.webjars</groupId>
            <artifactId>sockjs-client</artifactId>
            <version>1.0.2</version>
        </dependency>
        <dependency>
            <groupId>org.webjars</groupId>
            <artifactId>stomp-websocket</artifactId>
            <version>2.3.3</version>
        </dependency>
        <dependency>
            <groupId>org.webjars</groupId>
            <artifactId>bootstrap</artifactId>
            <version>3.3.7</version>
        </dependency>
        <dependency>
            <groupId>org.webjars</groupId>
            <artifactId>jquery</artifactId>
            <version>3.1.0</version>
        </dependency>
   <!--><-->
Enable STOMP over WebSocket

The Spring Framework provides support for using STOMP over WebSocket through the spring-messaging and Spring-WebSocket modules.

Here is an example of exposing a STOMP WebSocket/SockJS endpoint at the URL path /myWebSocket where messages whose destination starts with /app are routed to message-handling methods (i.e. application work) and messages whose destinations start with /topic will be routed to the message broker (i.e. broadcasting to other connected clients):

By creating WebSocketConfig.java as shown below:

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

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

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/myWebSocket").setAllowedOrigins("*");
        registry.addEndpoint("/myWebSocket").setAllowedOrigins("*").withSockJS();
    }
}

WebSocketConfig is annotated with @Configuration to indicate that it is a Spring configuration class. It is also annotated with @EnableWebSocketMessageBroker. As its name suggests, @EnableWebSocketMessageBroker enables WebSocket message handling, backed by a message broker.

The configureMessageBroker() method implements the default method in WebSocketMessageBrokerConfigurer to configure the message broker. It starts by calling enableSimpleBroker() to enable a simple memory-based message broker to carry the greeting messages back to the client on destinations prefixed with /topic. It also designates the /app prefix for messages that are bound for methods annotated with @MessageMapping in GreetingController. This prefix will be used to define all the message mappings. For example, /app/sayHello is the endpoint that the GreetingController.sayHello() method is mapped to handle.

The registerStompEndpoints() method registers the /myWebSocket endpoint, enabling SockJS fallback options so that alternate transports can be used if WebSocket is not available. The SockJS client will attempt to connect to /myWebSocket and use the best available transport (websocket, xhr-streaming, xhr-polling, and so on).

Create the Message Model

Now that we’ve set up the project and configured the WebSocket capabilities, we need to create a message to send. The endpoint will accept messages that contain a name in a STOMP message whose body is a JSON object.

We create class HelloMessage and Greeting to demonstrate this.

In HelloMessage.java:

public class HelloMessage {
    private String name;

    public HelloMessage() {
    }

    public HelloMessage(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

The message might look like this:

{
  "name": "Ryan!"
} 

Upon receiving the message and extracting the name, the service Controller will process it by creating a greeting (/app/sayHello)and sending that greeting to a separate destination(/topic/greetings) to which the client is subscribed.

The greeting is modeled in Greeting.java:

public class Greeting {
    private String content;

    public Greeting() {
    }

    public Greeting(String content) {
        this.content = content;
    }

    public String getContent() {
        return content;
    }
}

The greeting might look like this:

{
   "content": "Hello, Ryan!" 
} 

Spring will use the Jackson JSON library to automatically marshal instances of type Greeting into JSON.

Next, we will create a controller to create a hello message and send a greeting message to the subscribed destination.

Create the Message-Handling Controller

Spring’s approach to working with STOMP messaging is to associate a controller method to the configured endpoint. STOMP messages can be routed to @Controller classes. For example, the GreetingController is mapped to handle messages to the /sayHello (/app/sayHello) destination, as the following listing shows:

By creating GreetingController as shown below:

@Controller
public class GreetingController {

    @MessageMapping("/sayHello")
    @SendTo("/topic/greetings")
    public Greeting sayHello(HelloMessage message) throws Exception {
        Thread.sleep(1000);
        return new Greeting("Hello, " + HtmlUtils.htmlEscape(message.getName()) + "!");
    }

}

The @MessageMapping annotation ensures that, if a message is sent to the /sayHello destination, the sayHello() method is called.

The payload of the message is bound to a HelloMessage object, which is passed into sayHello() method.

When the client is calling the sayHello() method, he will need to pass the HelloMessage object into the sayHello() method, this method will simulate a one-second delay and create a Greeting Object and return it to the destination /topic/greetings. The return value is broadcast to all the subscribers of /topic/greetings, as specified in the @SendTo annotation.

When implementing this, we may encounter some problems relating to the CORS support of SockJS(see more in Spring document). Below are some tips to the SockJS:

Allow origins and CORS Headers for SockJS

As of Spring Framework 4.1.5, the default behavior for WebSocket and SockJS is to accept only same-origin requests. It is also possible to allow all or a specified list of origins. 

The 3 possible behaviors are:

  • Allow only same-origin requests (default): in this mode, when SockJS is enabled, the Iframe HTTP response header X-Frame-Options is set to SAMEORIGIN, and JSONP transport is disabled since it does not allow to check the origin of a request. As a consequence, IE6 and IE7 are not supported when this mode is enabled.
  • Allow a specified list of origins: each provided allowed origin must start with http:// or https://. In this mode, when SockJS is enabled, both IFrame and JSONP based transports are disabled. As a consequence, IE6 through IE9 are not supported when this mode is enabled.
  • Allow all origins: to enable this mode, you should provide * as the allowed origin value. In this mode, all transports are available.


WebSocket and SockJS allowed origins can be configured as shown below:

 registry.addEndpoint("/myWebSocket").setAllowedOrigins("*").withSockJS(); 

If you allow cross-origin requests, the SockJS protocol uses CORS for cross-domain support in the XHR streaming and polling transports. Therefore CORS headers are added automatically unless the presence of CORS headers in the response is detected. So if an application is already configured to provide CORS support, e.g. through a CorsFilter, Spring’s SockJsService will skip this part.

The following is the list of headers and values expected by SockJS:

  • "Access-Control-Allow-Origin" – initialized from the value of the “Origin” request header.
  • "Access-Control-Allow-Origin" – always set to true.
  • "Access-Control-Request-Headers" – initialized from values from the equivalent request header.
  • "Access-Control-Allow-Methods" – the HTTP methods a transport supports.
  • "Access-Control-Max-Age" – set to 31536000 (1 year).

In order to configure the CORS support in CorsFilter, headers should be added for the use of SockJS.

FrontEnd:

Create a Browser Client

With the server-side configuration for WebSocket in place, we will now create the JavaScript client that will send messages and receive messages from the server.

Create a sockJs.html file similar to the following:

<!DOCTYPE html>
<html>
<head>
    <title>Hello WebSocket</title>
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" crossorigin="anonymous">
    <link href="../css/socket.css" rel="stylesheet">
    <script src="https://code.jquery.com/jquery-3.3.1.slim.min.js" crossorigin="anonymous"></script>
    <script src="https://cdn.jsdelivr.net/npm/sockjs-client@1/dist/sockjs.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/stomp.js/2.3.3/stomp.min.js"></script>
    <script src="../js/socket.js"></script>
</head>
<body>
<noscript><h2 style="color: #ff0000">Seems your browser doesn't support Javascript! Websocket relies on Javascript being
    enabled. Please enable
    Javascript and reload this page!</h2></noscript>
<div id="main-content" class="container">
    <div class="row">
        <div class="col-md-6">This application has no explicit mapping for /error, so you are seeing this as a fallback.
            <form class="form-inline">
                <div class="form-group">
                    <label for="connect">WebSocket connection:</label>
                    <button id="connect" class="btn btn-default" type="submit">Connect</button>
                    <button id="disconnect" class="btn btn-default" type="submit" disabled="disabled">Disconnect
                    </button>
                </div>
            </form>
        </div>
        <div class="col-md-6">
            <form class="form-inline">
                <div class="form-group">
                    <label for="name">What is your name?</label>
                    <input type="text" id="name" class="form-control" placeholder="Your name here...">
                </div>
                <button id="send" class="btn btn-default" type="submit">Send</button>
            </form>
        </div>
    </div>
    <div class="row">
        <div class="col-md-12">
            <table id="conversation" class="table table-striped">
                <thead>
                <tr>
                    <th>Greetings</th>
                </tr>
                </thead>
                <tbody id="greetings">
                </tbody>
            </table>
        </div>
    </div>
</div>
</body>
</html>

This HTML file imports the SockJs and STOMP javascript libraries that will be used to communicate with our server through STOMP over WebSocket.

We also import socket.js, which contains the logic of the client as shown in the following:

var stompClient = null;

function setConnected(connected) {
    $("#connect").prop("disabled", connected);
    $("#disconnect").prop("disabled", !connected);
    if (connected) {
        $("#conversation").show();
    }
    else {
        $("#conversation").hide();
    }
    $("#greetings").html("");
}

function connect() {
    var socket = new SockJS("http://localhost:8080/myWebSocket");
    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);
        });
    });
}

function disconnect() {
    if (stompClient !== null) {
        stompClient.disconnect();
    }
    setConnected(false);
    console.log("Disconnected");
}

function sendName() {
    stompClient.send("/app/sayHello",{}, JSON.stringify({'name': $("#name").val()}));
}

function showGreeting(message) {
    $("#greetings").append("<tr><td>" + message + "</td></tr>");
}

$(function () {
    $("form").on('submit', function (e) {
        e.preventDefault();
    });
    $( "#connect" ).click(function() { connect(); });
    $( "#disconnect" ).click(function() { disconnect(); });
    $( "#send" ).click(function() { sendName(); });

});

The main parts of socket.js are functions connect() and sendName():

The connect() function uses SockJS and STOMP to open a connection to server endpoint /myWebSocket. Upon a successful connection, the client subscribes to the /topic/greetins destination, where the server will publish greeting messages. When a greeting is received on that destination, it will append a paragraph element to the DOM to display the greeting message.

The sendName() function retrieves the name entered by the user and uses the STOMP client to send it to the /app/sayHello destination (where GreetingController.greeting() will receive it.

Test our WebSocket Services

Now the service is running, we can open our browser and access http://localhost:8080/sockJs.html and click the Connect button.

Upon opening a connection, you are asked for your name. Enter your name and click Send. Your name is sent to the server as a JSON message over STOMP. After a one-second simulated delay, the server sends a message back with a “Hello” greeting that is displayed on the page. At this point, you can send another name or you can click the Disconnect button to close the connection.

Acknowledgment:

Thanks for the blog “Spring Boot + Websocket Example” from Dhiraj and baeldung’s “Intro to WebSockets with Spring”.

See more about WebSocket Support in SpringDoc.

Github: https://github.com/mostzac

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.