Kotlin, SpringBoot and WebSockets

Using Kotlin to do websockets the SpringBoot way

Introduction

For my latest Blog post, I have decided to continue the Kotlin theme, and this time explore SpringBoot further by revisiting the WebSocket Chat Application example that I started with many months, but converting it from SparkJava to SpringBoot. Whilst I personally find SparkJava very expressive and simple to use, Spring as a community has a very strong following, but I have found very little information on Kotlin and Websockets using Spring. This post is an attempt to rectify that.

First off, I should point out that there are two methods of implementing WebSockets in SpringBoot. This first, and the one you will find in the tutorials, is STOMP (Streaming Text Oriented Message Protocol). However, I prefer a little extra control, and a little less boilerplate, so I am gong to focus on the WebSocketHandler approach.

Given that I have already covered this topic previously, I am not going to go into too much detail on the client side. The focus of this topic is Kotlin and Spring; as such I am going to largely lift the implementation from the SparkJava code I created in the previous post. However, the main change to that is that rather than using straight WebSockets on the javascript side, I will instead make use of SockJS. This will give us fallback options if the browser the client is using does not support Websockets…albeit this is becoming less and less, SpringBoot implements SockJS with ease, so we may as well take the increased functionality for the few lines of extra code.

Let’s start coding

We only need 3 files for this implementation. These are

  1. build.gradle
  2. src/main/resources/static/index.html
  3. src/main/kotlin/codemwnci/bootsocket/ChatApplication.kt

The source code can be found on GitHub below

The Build File

To begin, I have used Gradle for the build script, rather than Maven for this implementation, but if you prefer Maven it is almost a standard spring boot set up, with the Jackson Kotlin module for easy JSON integration. You can use start.spring.io, or can copy directly from here.

buildscript {
    ext {
        kotlinVersion = '1.2.10'
        springBootVersion = '2.0.0.M7'
    }
    repositories {
        mavenCentral()
        maven { url "https://repo.spring.io/snapshot" }
        maven { url "https://repo.spring.io/milestone" }
    }
    dependencies {
        classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
        classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:${kotlinVersion}")
        classpath("org.jetbrains.kotlin:kotlin-allopen:${kotlinVersion}")
    }
}

apply plugin: 'kotlin'
apply plugin: 'kotlin-spring'
//apply plugin: 'eclipse'
apply plugin: 'org.springframework.boot'
apply plugin: 'io.spring.dependency-management'

sourceCompatibility = 1.8
compileKotlin {
    kotlinOptions.jvmTarget = "1.8"
}
compileTestKotlin {
    kotlinOptions.jvmTarget = "1.8"
}

repositories {
    mavenCentral()
    maven { url "https://repo.spring.io/snapshot" }
    maven { url "https://repo.spring.io/milestone" }
}


dependencies {
    compile('org.springframework.boot:spring-boot-starter-websocket')
    compile("org.jetbrains.kotlin:kotlin-stdlib-jre8")
    compile("org.jetbrains.kotlin:kotlin-reflect")
    compile "com.fasterxml.jackson.module:jackson-module-kotlin:2.9.0"
    testCompile('org.springframework.boot:spring-boot-starter-test')
}

The client code

As mentioned in the introduction, the client side chat code is mostly a copy-paste from the previous example. As you can see from the code, to open a socket using SockJS is as simple as giving it a relative path and creating a SockJS object, rather than a standard WebSocket object. The only other change is to include the SockJS script in the head. The rest of the code is identical.

For those who have not read the previous example, the cost is fairly straightforward. There is a DIV for the chat area, a DIV for the users logged in, and a final DIV with a text input and button to send a message. The javascript is also reasonably straightforward. It initially opens the socket, and sets three function callbacks to deal with open, close and message events.

  1. The onopen event simply asks the user to choose their username, and then sends a message to the websocket server to inform who has joined
  2. The onclose event just outputs an alert to the user that the server has closed the connection
  3. The onmessage event calls the receiveMessage function, which in turn adds or removes a user to/from the userlist, or adds a new chat message to the message DIV.

Finally, the javascript has a click / keypress listener on the button and text box, that executes the sendMessage function. This function simply sends a message to the server with a JSON string containing the chat message or the join request.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Chat - Websocket Kotlin</title>

    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css">
    <link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css">
    <script src="//ajax.googleapis.com/ajax/libs/jquery/3.1.1/jquery.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/sockjs-client@1/dist/sockjs.min.js"></script>

    <style>
        .full-height { height: 100%; }
        .input-text  { height: 10%;  }
        .chat-text   { height: 90%;  }
        .border      { border: 1px black; }
    </style>
</head>
<body class="full-height">
<div class="container-fluid full-height">
    <div class="row chat-text">
        <div class="col-md-10 border chat-text" id="chatbox">
        </div>
        <div class="col-md-2 border chat-text">
            <ul id="userlist"></ul>
        </div>
    </div>
    <div class="row input-text border">
        <div class="col-md-10">
            <input type="text" class="btn-block" id="msg" />
        </div>
        <div class="col-md-2">
            <button type="button" class="btn btn-primary btn-block btn-lg" id="send">
                Send
            </button>
        </div>
    </div>
</div>

<script>

        //var webSocket = new WebSocket("ws://" + location.hostname + ":" + location.port + "/chat");
        var webSocket = new SockJS('/chat');

        webSocket.onmessage = function (msg) { receievMsg(JSON.parse(msg.data)) }
        webSocket.onclose = function() { alert("Server Disconnect You"); }
        webSocket.onopen = function() {
            var name = "";
            while (name == "") name = prompt("Enter your name");
            sendMessage("join", name);
        }

        $("#send").click(function () {
            sendMessage("say", $("#msg").val());
        });
        $("#msg").keypress(function(e) {
            if(e.which == 13) sendMessage("say", e.target.value);
        });
        function sendMessage(type, data) {
            if (data !== "") {
                webSocket.send(JSON.stringify({type: type, data: data}));
                $("#msg").val("");
                $("#msg").focus();
            }
        }
        function receievMsg(msg) {
            if (msg.msgType == "say") {
                $("#chatbox").append("<p>"+msg.data+"</p>");
            }
            else if (msg.msgType == "join") {
                addUser(msg.data);
            }
            else if (msg.msgType == "users") {
                msg.data.forEach(function(el) { addUser(el); });
            }
            else if (msg.msgType == "left") {
                $("#user-"+msg.data.id).remove();
            }
        }
        function addUser(user) {
            $("#userlist").append("<li id='user-"+user.id+"'>"+user.name+"</li>");
        }
    </script>
</body>
</html>

The server code

The server code can be split into three distinct parts; the SpringBoot bootstrapping, the websocket handler registration configuration, and finally the websocket handler itself.

The springboot boostrapping is the same as any other Kotlin springboot implementation. We won’t discuss this in any detail (lines 58–63).

The websocket registration (lines 50–55) is a simple configuration class with a couple of annotations that enable websockets for the SpringBoot application. This code looks somewhat overkill in its 6 lines of code, however if your application is more than just this simple example, this separation will look far less overkill and instead a neat separation of concerns. It should be noted here that as part of the websocket registration, the “withSockJS()” method is called, which gives us the server side effort for SockJS fallback.

The ChatHandler class is where the interesting parts of the Websocket implementation takes place. We are extending the TextWebSocketHandler, which when not using STOMP will be the most useful parent class for most websocket implementations.

For our implementation, there are 5 functions that require implementation, two of which are overridden methods from the Java parent class that we are extending, and 3 helper functions for communicating back to the connected web sockets. I will deconstruct this class line by line to explain how our serverside implementation works.

Line 14–15: Here we are creating two DTO classes. They will both be used to convert to JSON when we send the information back to the client.

Line 19: This is a hashmap that will contain all the sessions that have connected and joined the chat.

Line 20: So that we can inform the clients when users are removed, we need to ensure we have a unique identifier for each user, and therefore we give each User that joins a UID. We use AtomicLong to ensure that we not have any synchronisation issues.

Line 22–25: This overridden method is called when a websocket disconnects from the server. On this event, we remove the session from the hashmap. We could also at this stage inform the client that the user has left, but for brevity of this example, we will not perform this action.

Before discussing the main function (handleTextMessage), we will briefly discuss the three helper functions that are all used as part of the handleTextMessage function.

Line 45: This helper function (emit) simply takes the websocket we wish to send a message to, and the message we wish to send. We call the sendMessage function on the websocket, construct a new TextMessage object, which is part of the Spring Websocket implementation, and then using Jackson JSON we transform the message we wish to send into a JSON string.

Line 46: This helper function (broadcast) simply takes the message we wish to send to all websockets, and iterates over each element of the hashmap that contains all of the sessions using the forEach function, and then simply calls the emit function we have just described passing in the session object from each iteration, and the passed in message.

Line 47: This helper function (broadcastToOthers) is very similar to the broadcast helper function, except it specifically excludes a single passed in websocket. This is achieved by again using one of the functional utilities of the Kotlin collections by filtering the hashmap (to include every session except the one we wish to exclude) and then executing the forEach in the same way as the broadcast function previously described.

Finally, we will walk through the handleTextMessage function. Starting at Line 27, this takes in a websocket (the one that has send the message), and a message that they have sent. The first step is to turn this message into JSON, which we do on Line 28 using the Jackson ObjectMapper object and getting the message from the payload property of the passed in message.

Line 30: Now that we have the JSON message, we can determine what we need to do using the message type contained in the message JSON string. Using the kotlin WHEN clause, we can execute two different paths based on the “join” or “say” message type.

Line 31–38: When a user requests to “join” the chat, we first create a new user object by incrementing the counter, and reading the user’s name from the JSON message. The user is then added to the sessionlist and we send two messages. To the user joining, we send a message directly to them using the emit helper function to tell them all the users in the current chat. We then broadcast to all the other users to tell them that this new user has joined, by calling the broadcastToOthers helper function.

Line 39–41: When a user sends a chat message (“say”), we simply call the broadcast function sending the text that they inputted to all connected sessions, including themselves.

And that is it. The boilerplate from SpringBoot is minimal. The addition of SockJS is easy and not intrusive. Yet, the extensibility of the code is there without having to do any meaningful refactoring.

Hopefully this has been an informative article that will give you suitable ideas of how you yourself could use the ease of SpringBoot and the power of WebSockets to develop real-time applications.