What You Will Learn
In this tutorial we will create a simple real-time chat application. It will feature a chat-panel that stores messages received after you join, a list of currently connected users, and an input field to send messages from. We will be using WebSockets for this, as WebSockets provides us with full-duplex communication channels over a single TCP connection, meaning we won’t have to make additional HTTP requests to send and receive messages. A WebSocket connection stays open, greatly reducing latency (and complexity).
A live demo can be found here (might load slowly first time)
Create a Spark Maven Project
First, we need to create a Spark Maven project (→ Tutorial)
Dependencies
Other than Spark, we are going to use a simple Java HTML Builder (j2html) to generate our chat messages. Our POM.xml should have the following dependencies:
<dependencies>
<dependency>
<groupId>com.sparkjava</groupId>
<artifactId>spark-core</artifactId>
<version>2.5.5</version>
</dependency>
<dependency>
<groupId>com.j2html</groupId>
<artifactId>j2html</artifactId>
<version>1.2.0</version>
</dependency>
<dependency>
<groupId>org.json</groupId>
<artifactId>json</artifactId>
<version>20160810</version>
</dependency>
</dependencies>
Creating the Java Application
We need to keep track of all our users and assign them usernames. We create a map (userUsernameMap) that maps sessions to usernames, an int for the next username (nextUserNumber), and the Spark server code:
public class Chat {
// this map is shared between sessions and threads, so it needs to be thread-safe (http://stackoverflow.com/a/2688817)
static Map<Session, String> userUsernameMap = new ConcurrentHashMap<>();
static int nextUserNumber = 1; //Used for creating the next username
public static void main(String[] args) {
staticFileLocation("/public"); //index.html is served at localhost:4567 (default port)
webSocket("/chat", ChatWebSocketHandler.class);
init();
}
}
We also need to create a few methods for sending messages to all our connected users. We will only send messages to users whose session has the status open (Session::isOpen). We use a stream and a filter to reduce our list (the keySet of our userUsernameMap), then send out a JSON structure containing a HTML message and a list of usernames (the values of our userUsernameMap):
//Sends a message from one user to all users, along with a list of current usernames
public static void broadcastMessage(String sender, String message) {
userUsernameMap.keySet().stream().filter(Session::isOpen).forEach(session -> {
try {
session.getRemote().sendString(String.valueOf(new JSONObject()
.put("userMessage", createHtmlMessageFromSender(sender, message))
.put("userlist", userUsernameMap.values())
));
} catch (Exception e) {
e.printStackTrace();
}
});
}
To create the HTML, we will use (j2html). We will create an article-tag containing the name of the sender, the message itself, and a timestamp:
//Builds a HTML element with a sender-name, a message, and a timestamp,
private static String createHtmlMessageFromSender(String sender, String message) {
return article().with(
b(sender + " says:"),
p(message),
span().withClass("timestamp").withText(new SimpleDateFormat("HH:mm:ss").format(new Date()))
).render();
}
Setting up Our WebSocketHandler
The approach we will use is very straightforward: Add the user to our userUsernameMap when he connects, remove him when he disconnects, and send all his messages to all users. Since we want usernames, but don’t want complexity, we will generate usernames based on when someone connects to the server:
@WebSocket
public class ChatWebSocketHandler {
private String sender, msg;
@OnWebSocketConnect
public void onConnect(Session user) throws Exception {
String username = "User" + Chat.nextUserNumber++;
Chat.userUsernameMap.put(user, username);
Chat.broadcastMessage(sender = "Server", msg = (username + " joined the chat"));
}
@OnWebSocketClose
public void onClose(Session user, int statusCode, String reason) {
String username = Chat.userUsernameMap.get(user);
Chat.userUsernameMap.remove(user);
Chat.broadcastMessage(sender = "Server", msg = (username + " left the chat"));
}
@OnWebSocketMessage
public void onMessage(Session user, String message) {
Chat.broadcastMessage(sender = Chat.userUsernameMap.get(user), msg = message);
}
}
That’s it for the Java part, the rest is HTML and JavaScript.
Building a JavaScript Client
In order to use demonstrate that our application works, we can build a JavaScript client. First we create our index.html:
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>WebsSockets</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div id="chatControls">
<input id="message" placeholder="Type your message">
<button id="send">Send</button>
</div>
<ul id="userlist"> <!-- Built by JS --> </ul>
<div id="chat"> <!-- Built by JS --> </div>
<script src="websocketDemo.js"></script>
</body>
</html>
As you can see, we reference a stylesheet called style.css, which can be found on GitHub
The final step needed for completing our chat application is creating websocketDemo.js:
//Establish the WebSocket connection and set up event handlers
var webSocket = new WebSocket("ws://" + location.hostname + ":" + location.port + "/chat/");
webSocket.onmessage = function (msg) { updateChat(msg); };
webSocket.onclose = function () { alert("WebSocket connection closed") };
//Send message if "Send" is clicked
id("send").addEventListener("click", function () {
sendMessage(id("message").value);
});
//Send message if enter is pressed in the input field
id("message").addEventListener("keypress", function (e) {
if (e.keyCode === 13) { sendMessage(e.target.value); }
});
//Send a message if it's not empty, then clear the input field
function sendMessage(message) {
if (message !== "") {
webSocket.send(message);
id("message").value = "";
}
}
//Update the chat-panel, and the list of connected users
function updateChat(msg) {
var data = JSON.parse(msg.data);
insert("chat", data.userMessage);
id("userlist").innerHTML = "";
data.userlist.forEach(function (user) {
insert("userlist", "<li>" + user + "</li>");
});
}
//Helper function for inserting HTML as the first child of an element
function insert(targetId, message) {
id(targetId).insertAdjacentHTML("afterbegin", message);
}
//Helper function for selecting element by id
function id(id) {
return document.getElementById(id);
}
And that’s it! Now try opening localhost:4567 in a couple of different browser windows (that you can see simultaneously) and talk to yourself.
Conlusion
Well, that was fast! We have a working real-time chat application implemented without polling, written in a total of less than 100 lines of Java and JavaScript. The implementation is very basic though, and we should at least split up the sending of the userlist and the messages (so that we don’t rebuild the user list every time anyone sends a message), but since the focus of this tutorial was supposed to be on WebSockets, I chose to do the implementation as minimal as I could be comfortable with.
The source code for this tutorial can be found on GitHub