Chapter Four: Creating a simple chat-app with WebSockets
Aim
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). Dependencies
First, we need to create a Maven project with some dependencies: (→ Tutorial)
1 <dependencies>
2 <dependency>
3 <groupId>io.javalin</groupId>
4 <artifactId>javalin</artifactId>
5 <version>1.7.0</version>
6 </dependency>
7 <dependency>
8 <groupId>org.slf4j</groupId>
9 <artifactId>slf4j-simple</artifactId>
10 <version>1.7.13</version>
11 </dependency>
12 <dependency>
13 <groupId>org.json</groupId>
14 <artifactId>json</artifactId>
15 <version>20160810</version>
16 </dependency>
17 <dependency>
18 <groupId>com.j2html</groupId>
19 <artifactId>j2html</artifactId>
20 <version>1.2.0</version>
21 </dependency>
22 </dependencies>
The Java application
The Java application is pretty straightforward. We need:
- a map to keep track of session/username pairs.
- a counter for number of users (nicknames are auto-incremented)
- websocket handlers for connect/message/close
- a method for broadcasting a message to all users
- a method for creating the message in HTML (or JSON if you prefer)
1 public class Chat {
2
3 private static Map<Session, String> userUsernameMap =\
4 new ConcurrentHashMap<>();
5 private static int nextUserNumber = 1; // Assign to u\
6 sername for next connecting user
7
8 public static void main(String[] args) {
9 Javalin.create()
10 .port(7070)
11 .enableStaticFiles("/public")
12 .ws("/chat", ws -> {
13 ws.onConnect(session -> {
14 String username = "User" + nextUserNu\
15 mber++;
16 userUsernameMap.put(session, username\
17 );
18 broadcastMessage("Server", (username \
19 + " joined the chat"));
20 });
21 ws.onClose((session, status, message) -> {
22 String username = userUsernameMap.get\
23 (session);
24 userUsernameMap.remove(session);
25 broadcastMessage("Server", (username \
26 + " left the chat"));
27 });
28 ws.onMessage((session, message) -> {
29 broadcastMessage(userUsernameMap.get(\
30 session), message);
31 });
32 })
33 .start();
34 }
35
36 // Sends a message from one user to all users, along \
37 with a list of current usernames
38 private static void broadcastMessage(String sender, S\
39 tring message) {
40 userUsernameMap.keySet().stream().filter(Session:\
41 :isOpen).forEach(session -> {
42 try {
43 session.getRemote().sendString(
44 new JSONObject()
45 .put("userMessage", createHtmlMes\
46 sageFromSender(sender, message))
47 .put("userlist", userUsernameMap.\
48 values()).toString()
49 );
50 } catch (Exception e) {
51 e.printStackTrace();
52 }
53 });
54 }
55
56 // Builds a HTML element with a sender-name, a messag\
57 e, and a timestamp
58 private static String createHtmlMessageFromSender(Str\
59 ing sender, String message) {
60 return article(
61 b(sender + " says:"),
62 span(attrs(".timestamp"), new SimpleDateForma\
63 t("HH:mm:ss").format(new Date())),
64 p(message)
65 ).render();
66 }
67
68 }
Building a JavaScript Client
In order to demonstrate that our application works, we can build a JavaScript client. First we create our index.html:
1 <!DOCTYPE html>
2 <html>
3 <head>
4 <meta name="viewport" content="width=device-width, in\
5 itial-scale=1">
6 <title>WebsSockets</title>
7 <link rel="stylesheet" href="style.css">
8 </head>
9 <body>
10 <div id="chatControls">
11 <input id="message" placeholder="Type your messag\
12 e">
13 <button id="send">Send</button>
14 </div>
15 <ul id="userlist"> <!-- Built by JS --> </ul>
16 <div id="chat"> <!-- Built by JS --> </div>
17 <script src="websocketDemo.js"></script>
18 </body>
19 </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:
1 // small helper function for selecting element by id
2 let id = id => document.getElementById(id);
3
4 //Establish the WebSocket connection and set up event han\
5 dlers
6 let ws = new WebSocket("ws://" + location.hostname + ":" \
7 + location.port + "/chat");
8 ws.onmessage = msg => updateChat(msg);
9 ws.onclose = () => alert("WebSocket connection closed");
10
11 // Add event listeners to button and input field
12 id("send").addEventListener("click", () => sendAndClear(i\
13 d("message").value));
14 id("message").addEventListener("keypress", function (e) {
15 if (e.keyCode === 13) { // Send message if enter is p\
16 ressed in input field
17 sendAndClear(e.target.value);
18 }
19 });
20
21 function sendAndClear(message) {
22 if (message !== "") {
23 ws.send(message);
24 id("message").value = "";
25 }
26 }
27
28 function updateChat(msg) { // Update chat-panel and list \
29 of connected users
30 let data = JSON.parse(msg.data);
31 id("chat").insertAdjacentHTML("afterbegin", data.user\
32 Message);
33 id("userlist").innerHTML = data.userlist.map(user => \
34 "<li>" + user + "</li>").join("");
35 }
And that’s it! Now try opening localhost:7070 in a couple of different browser windows (that you can see simultaneously) and talk to yourself.
Conclusion
Well, that was easy! 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.