Kotlin WebSocket Chat using SparkJava

I have been using Kotlin for a little while now, but in a small way. I have been flitting between Java, Scala and Kotlin for a few years, but with the announcement from Google of now officially supporting Kotlin for Android, I decided to jump back in again.

My typical use case when exploring new languages is a web application that I have been building on and off for a few years, which is essentially a Single Page App (JavaScript, usually Angular) on the front end, and a REST back end that doesn't have to make me work too hard.

I have used a number of frameworks in the past, but for Scala I have always stayed pretty loyal to Play Framework. For Java, I have used SparkJava (or Play!), and for Kotlin, I went searching…Very quickly I decided that SparkJava was still a good solution to my use case.

My intention is to write a series of blogs on using SparkJava and Kotlin with associated other technologies to make a complete end to end solution.

To demo WebSockets, I am going to build a simple Chat application, using Kotlin, SparkJava and native browser WebSockets (no shims, frameworks etc). I am also going to only use JQuery, rather than a SPA framework like Angular, as not distract from the WebSockets.

All the code for this demo can be found on GitHub here → github.com/codemwnci/kotlinwschat

Setup

Assuming you already have Kotlin up and running (I have used IntelliJ), first we will create a POM file. If you are using intelliJ, then create a Maven project, and copy the following code.

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
<groupId>codemwnci</groupId>
    <artifactId>kotlin-ws-chat</artifactId>
    <version>1.0-SNAPSHOT</version>
<properties>
        <kotlin.version>1.1.3-2</kotlin.version>
    </properties>
<dependencies>
        <dependency>
            <groupId>org.jetbrains.kotlin</groupId>
            <artifactId>kotlin-stdlib-jre8</artifactId>
            <version>${kotlin.version}</version>
        </dependency>
        <dependency>
            <groupId>org.jetbrains.kotlin</groupId>
            <artifactId>kotlin-test</artifactId>
            <version>${kotlin.version}</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>com.sparkjava</groupId>
            <artifactId>spark-core</artifactId>
            <version>2.5.5</version>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.module</groupId>
            <artifactId>jackson-module-kotlin</artifactId>
            <version>2.8.4</version>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-simple</artifactId>
            <version>1.7.22</version>
        </dependency>
    </dependencies>
<build>
        <plugins>
            <plugin>
                <groupId>org.jetbrains.kotlin</groupId>
                <artifactId>kotlin-maven-plugin</artifactId>
                <version>${kotlin.version}</version>
                <executions>
                    <execution>
                        <id>compile</id>
                        <phase>compile</phase>
                        <goals>
                            <goal>compile</goal>
                        </goals>
                    </execution>
                    <execution>
                        <id>test-compile</id>
                        <phase>test-compile</phase>
                        <goals>
                            <goal>test-compile</goal>
                        </goals>
                    </execution>
                </executions>
                <configuration>
                    <jvmTarget>1.8</jvmTarget>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

The items to not on here is that we have included dependencies for SparkJava, Jackson (for JSON processing), and SLF4J (which is used by Spark to output logger info).

If you have set up the project as a Maven project, then you will have a standard maven directory structure. We will need to create two further directories.

  1. We need to create a directory called public inside of src\main\resources\
  2. We need to create a directory to put our package structure in. So, inside of src\main\java we need to put a package. I have chosen codemwnci.

If you have chosen the same approach as me, you will now have a directory structure like the following

- src
  - main
    - java    
      - codemwnci
    - resources
      - public

Let's Start Coding

I am not going to go into much detail on the normal REST approach to SparkJava. You can read that on their website, or you can watch the video at the top of this page if you are interested. Instead, we are going to jump straight into the configuration of SparkJava for WebSockets. So, let's create a new Kotlin file, called ChatController.kt

Copy the following code

package codemwnci 
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import org.eclipse.jetty.websocket.api.Session
import org.eclipse.jetty.websocket.api.annotations.*
import spark.Spark.*
import java.util.concurrent.atomic.AtomicLong 
fun main(args: Array<String>) {    
    port(9000)    
    staticFileLocation("/public")    
    webSocket("/chat", ChatWSHandler::class.java)    
    init()
}
@WebSocket
class ChatWSHandler {
}

We don't need much more at the moment. This code just imports a few things we will need now and later, and then starts a main function to configure SparkJava with the following attributes.

  1. Running on port 9000
  2. Pointing to the the publicdirectory that we set up in our resources directory
  3. Configuring the /chat URL to work websockets
  4. Starting up the SparkJava server

If we run this code now, it doesn't do much other than starting the server. If we navigated to any URL on port 9000 we would get a 404 - Not Found error, as we have put no further routes in our code, nor put in any html files in our public directory.

So let's fix that, and create our index.html file in the /resources/public directory.

<!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>     
  <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/");        
   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>

This code starts off by including JQuery and Bootstrap, and then using Bootstrap to add a few DIVs to create two rows and two columns on each. The script then executes and carries out the following activities

  1. Opens a websocket connection to our /chat URL Creates a function that calls the receiveMsg function when a message is received from the server.
  2. Creates a function that handles a socket close (disconnect from the server), that outputs a simple alert.
  3. Creates a function that handles the socket open event. Here we ask the user to input their name, and then sends this to the server as a JSON message via the sendMessage function that is created in point 6.
  4. Next we create a click event (on send button click) and button press event (to capture an enter key). Both send a JSON message with the text that was entered in the text box.
  5. The sendMessage function sends a JSON object via the websocket to the server, and then clears the text box.
  6. Finally the receiveMessage function that handles websocket messages is created. This deals with a "say" request, by adding a new paragraph to the chat window. The "join" request adds a new list item to the list of users. The "users" request iterates through all the users in the array and adds them to the list of users. Finally, the "left" request removes a user from the list.

Now if we restart the server, we should see the chat window, but still won't see anything happening because we haven't completed our server-side code. So, let's finish off that code.

class User(val id: Long, val name: String)
class Message(val msgType: String, val data: Any)

@WebSocket
class ChatWSHandler {

    val users = HashMap<Session, User>()
    var uids = AtomicLong(0)

    @OnWebSocketConnect
    fun connected(session: Session) = println("session connected")

    @OnWebSocketMessage
    fun message(session: Session, message: String) {
        val json = ObjectMapper().readTree(message)
        // {type: "join/say", data: "name/msg"}
        when (json.get("type").asText()) {
            "join" -> {
                val user = User(uids.getAndIncrement(), json.get("data").asText())
                users.put(session, user)
                // tell this user about all other users
                emit(session, Message("users", users.values))
                // tell all other users, about this user
                broadcastToOthers(session, Message("join", user))
            }
            "say" -> {
                broadcast(Message("say", json.get("data").asText()))
            }
        }
        println("json msg ${message}")
    }


    @OnWebSocketClose
    fun disconnect(session: Session, code: Int, reason: String?) {
        // remove the user from our list
        val user = users.remove(session)
        // notify all other users this user has disconnected
        if (user != null) broadcast(Message("left", user))
    }


    fun emit(session: Session, message: Message) = session.remote.sendString(jacksonObjectMapper().writeValueAsString(message))
    fun broadcast(message: Message) = users.forEach() { emit(it.key, message) }
    fun broadcastToOthers(session: Session, message: Message) = users.filter { it.key != session }.forEach() { emit(it.key, message)}

}

First off, we create two data classes User and Message to represent the JSON objects that we send back to the HTML file that we have just completed.

Inside of the websocket handler, we start by creating some state for our chat application, with a list of users and their associated Sessions. The Sessions are the objects that keep hold of the TCP connection, and will be used later when communicating to the websockets.

We then create 3 annotation to deal with open, close and message events.

Open: we don't really need this for our chat application, so we simply println that a user has connected.

Close: for this event, we simply remove the session from our list of users in the chat room and then broadcast the message that this user has left to all remaining users. Warning: Don't forget the Nullable ? String reason of the disconnect function. If you miss this, the OnWebSocketClose function will not be triggered, but you won't necessarily get an error message to explain why!

Message: for this event, we deal with two types of messages. A "say" event, or a "join" event. Here we make use of Kotlin's great where keyword. When a "say" event is received, we simply broadcast this to all users in the chat room. When a "join" event is received, we create a new User object (also getting and incrementing the ID), add the user to the list of users, send all the User objects to the client who just joined, and then sends a "join" message to all other sessions.

Finally, to finish off, we create the 3 functions that we have just used in the Close / Message functions to broadcast to all, send to one (emit), and send to all others (broadcastToOthers). These are all very similar functions, so we will explain each in turn.

emit: this is the basis for all our broadcast functions, and serves as a function to send a message to a single websocket Session. The code picks up the connection via the remote property, and then calls sendString, passing in a JSON string that has been converted via the JacksonObjectMapper.

broadcast: this function simply calls the forEach function on the HashMap of users, which gives us access to each websocket Session object. From this, the emit function (described above) is called for each one.

broadcastToOthers: this function is a slight variation on the broadcast function described previously. in that the forEach is only called once a filter is executed which excludes the passed in Session object, to ensure that only the other Sessions are sent the message.

And, that folks, is it. WebSockets, with no magic, and no trickery. Just Kotlin and SparkJava.

If you would like more in-depth explanations of anything, please leave a comment. If you like the YouTube video, I'd also appreciate it if you could subscribe to the channel. I hope you have found this interesting and informative!