A Tutorial on Implementing a Chat Application like Whatsapp using WebSocket and Spring

What is Websocket?

When we run a server side application, we run it on a particular physical port e.g. 8080, 8081. And to access the server side application, we use an IP. Similarly, when we log in to our browser and ask for a particular site, we send to the request our computer’s IP as well as dynamically generated port number. So, we have four items which helps us complete communication between our computer and the server and these four items are unique for every request.

1. Server IP address => It is hidden in URL given to client and known to the client.

2. Server port  => It is also hidden in URL given to us and to the client.

3. Client IP address => Unique for every client

4. Client port => Unique for every client and is generated dynamically

When a client wants to connect to the server, a TCP socket is created to represent that connection at server side. Then, the client sends a packet from the client IP address and from the unique client port number. When the server gets the packet on its own port number, it stores the client IP address and the particular client port number. This separates that client’s traffic from all the other currently connected sockets. Server now triggers an event for that particular socket e.g. fetch the nearest cabs.

The server now wants to send a response to that client. Server derives the client’s IP address and client port number from its stored data and sends it back.

HTTP requests that we are more familiar with does what we just described above. After the response is sent back, it closes the connection. At all times, clients request for the data to server and server returns back the data. Also, there is high overload of initiating a connection.

What happens when we chat or make a tool like google docs?

Client can ask for the data by making a HTTP request at a repeated interval. But this won’t be near time, comes at high overhead of making connection every time and developers are left with lots of corner cases to solve.

– My friend Prathamesh, Amit and friends at work

Now, WebSocket says don’t close that connection until client says so. WebSocket connections start out with an HTTP connection and contains an “upgrade” header requesting the server to upgrade the protocol from HTTP to WebSocket. If the server agrees to the upgrade, then it returns a response that indicates that the protocol will be changed to the WebSocket protocol. In a way, both server and client agrees to change the way they were talking, from HTTP to WebSocket . Meanwhile the same port is servicing other WebSocket as well as HTTP request. After a WebSocket connection is established, server and client can talk bidirectionally and in any order; there is no concept of request and response.

What is STOMP?

STOMP is an abbreviation of Simple Text-Orientated Messaging Protocol. It defines an message semantics so that any of the available STOMP clients can communicate with any STOMP message broker. This enables us to provide easy and widespread messaging interoperability among languages and platforms. The said semantics are for following operations:

  • Connect
  • Subscribe
  • Unsubscribe
  • Send
  • Transaction

Implementation of Group Chat Using Spring boot

We will implement a whatspp group like chat. This chat app will have following two screen. This project can be easily converted to peer to peer chat. View or clone this project at github.

Login screen
chat screen

Generate the project

Log on to https://start.spring.io/. You will see following screen.

Spring Project generator

Enter your preference. I generated a maven project. And added Websocket ad the dependency. You can fill up your own artifact id and group id. Click Generate Project and you will see a project getting downloaded soon.

Enabling this project as WebSocket Project

Spring allows us to create a configuration class that enables the project as WebSocket Project. Following is my configuration class:

package com.nulpointerexception.npechatroom;
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfiguration implements WebSocketMessageBrokerConfigurer {
/**
*
* @param config
* Here we have enabled simple in memory message broker. We can register rabbit MQ also as message broker
* by using the MessageBrokerRegistry config methods/
*/
@Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.enableSimpleBroker("/chat-room");
config.setApplicationDestinationPrefixes("/chat-app");
}
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/sock").setAllowedOrigins("*").withSockJS();
}
}

WebSocketConfiguration class is annotated with @Configuration to indicate that it is a Spring configuration class. It is also annotated @EnableWebSocketMessageBroker that enables this project for WebSocket message handling, backed by a message broker. This class implements WebSocketMessageBrokerConfigurer which has methods to tell the project which MessageBroker has to be used, what is the endpoint for our webSocket. In our case, we have told project to use in-memory message broker. We could have configured external message broker such as rabbit MQ using the configureMessageBroker() method. We have also configured a message broker address as /chat-room which is where client will subscribe themselves. That means any message that will be sent to the /chat-room will be automatically read by all the subscribed clients.

It also designates the /chat-app prefix for messages that are bound for the server. For example, when we will send message, this prefix will be added to our send message address. The registerStompEndpoints() method registers the /sock endpoint to enable SockJS fallback options so as to use alternate transports if WebSocket is not available. This will be more clear when we write about the client.

Creating a event-handling controller

STOMP events can be routed to @Controller classes. For example, we will want methods which can be exposed to the client to add user to the chat or send message. Add this controller:

package com.nulpointerexception.npechatroom;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.handler.annotation.DestinationVariable;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.Payload;
import org.springframework.messaging.handler.annotation.SendTo;
import org.springframework.messaging.simp.SimpMessageHeaderAccessor;
import org.springframework.messaging.simp.SimpMessageSendingOperations;
import org.springframework.stereotype.Controller;
import static java.lang.String.format;
@Controller
public class ChatRoomController {
private static final Logger logger = LoggerFactory.getLogger(ChatRoomController.class);
@Autowired
private SimpMessageSendingOperations messagingTemplate;
@MessageMapping("/chat/{roomId}/sendMessage")
public void sendMessage(@DestinationVariable String roomId, @Payload Message chatMessage) {
logger.info(roomId+" Chat messahe recieved is "+chatMessage.getContent());
messagingTemplate.convertAndSend(format("/chat-room/%s", roomId), chatMessage);
}
@MessageMapping("/chat/{roomId}/addUser")
public void addUser(@DestinationVariable String roomId, @Payload Message chatMessage,
SimpMessageHeaderAccessor headerAccessor) {
String currentRoomId = (String) headerAccessor.getSessionAttributes().put("room_id", roomId);
if (currentRoomId != null) {
Message leaveMessage = new Message();
leaveMessage.setType(Message.MessageType.LEAVE);
leaveMessage.setSender(chatMessage.getSender());
messagingTemplate.convertAndSend(format("/chat-room/%s", currentRoomId), leaveMessage);
}
headerAccessor.getSessionAttributes().put("name", chatMessage.getSender());
messagingTemplate.convertAndSend(format("/chat-room/%s", roomId), chatMessage);
}
}

/Here, The @MessageMapping annotation ensures that if a message is sent to destination /chat/{roomId}/sendMessage, sendMessage() method is called. Here, the destination is dynamically generated. The method in our case, sends the message to the message broker at the /chat-room/{roomId}. Now all the subscribed client at the said topic will get message automatically.

Creating a model of event

package com.nulpointerexception.npechatroom;
public class Message {
public enum MessageType {
CHAT, JOIN, LEAVE
}
private MessageType messageType;
private String content;
private String sender;
public MessageType getType() {
return messageType;
}
public void setType(MessageType messageType) {
this.messageType = messageType;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
public String getSender() {
return sender;
}
public void setSender(String sender) {
this.sender = sender;
}
}
view raw Message.java hosted with ❤ by GitHub

This is how we will send the event to the controller.

Dependencies

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0&quot; xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance&quot;
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"&gt;
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.4.RELEASE</version>
<relativePath/> <!– lookup parent from repository –>
</parent>
<groupId>com.nulpointerexception</groupId>
<artifactId>npe-chatroom</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>npe-chatroom</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
view raw pom.xml hosted with ❤ by GitHub

Main Class

package com.nulpointerexception.npechatroom;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class NpeChatroomApplication {
public static void main(String[] args) {
SpringApplication.run(NpeChatroomApplication.class, args);
}
}

Browser HTML page to serve as client

<!doctype html>
<html lang="en">
<head>
<!– Required meta tags –>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<!– Bootstrap CSS –>
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css&quot; integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
<title>Hello, world!</title>
</head>
<body>
<div id="userJoin" class="container">
<br>
<br>
<div class="card">
<div class="card-body">
<h1>My Chat App Example – nulPointerException.com</h1>
<a class="btn btn-primary" href="https://nulpointerexception.com/&quot; role="button">More tutorials at nulPointerException.com</a>
</div>
</div>
<br>
<br>
<form id="userJoinForm" name="userJoinForm">
<div class="form-group">
<label for="name">Enter Name:</label>
<input type="text" class="form-control" id="name" aria-describedby="name" placeholder="Enter name">
</div>
<div class="form-group">
<label for="room">Enter Room:</label>
<input type="text" class="form-control" id="room" aria-describedby="exampleInputRoom" placeholder="Enter room">
</div>
<button type="submit" class="btn btn-primary">Submit</button>
</form>
</div>
<div id="chatPage" class="container d-none">
<div class="card">
<div class="card-body">
<h1>My Chat App Example – nulPointerException.com</h1>
<a class="btn btn-primary" href="https://nulpointerexception.com/&quot; role="button">More tutorials at nulPointerException.com</a>
</div>
</div>
<div class="chat-header">
<h2>Chatroom [<span id="room-id-display"></span>]</h2>
</div>
<div class="waiting">
We are waiting to enter the room.
</div>
<div class="card">
<div class="card-body">
<ul id="messageArea">
</div>
</div>
</ul>
<form id="messagebox" name="messagebox">
<div class="form-group">
<label for="message">Enter Message:</label>
<input type="text" class="form-control" id="message" aria-describedby="name" placeholder="Enter message to chat ….">
</div>
<button type="submit" class="btn btn-primary">Send</button>
</form>
</div>
<!– Optional JavaScript –>
<!– jQuery first, then Popper.js, then Bootstrap JS –>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js"></script&gt;
<script src="https://cdnjs.cloudflare.com/ajax/libs/sockjs-client/1.3.0/sockjs.js"></script&gt;
<script src="https://cdnjs.cloudflare.com/ajax/libs/stomp.js/2.3.3/stomp.js"></script&gt;
<script src="https://cdn.jsdelivr.net/npm/js-cookie@2/src/js.cookie.min.js"></script&gt;
<script src="/js/mychat.js"></script>
</body>
</html>
view raw index.html hosted with ❤ by GitHub

Associated JS file

'use strict';
var stompClient = null;
var usernamePage = document.querySelector('#userJoin');
var chatPage = document.querySelector('#chatPage');
var room = $('#room');
var name = $("#name").val().trim();
var waiting = document.querySelector('.waiting');
var roomIdDisplay = document.querySelector('#room-id-display');
var stompClient = null;
var currentSubscription;
var topic = null;
var username;
function connect(event) {
var name1 = $("#name").val().trim();
Cookies.set('name', name1);
usernamePage.classList.add('d-none');
chatPage.classList.remove('d-none');
var socket = new SockJS('/sock');
stompClient = Stomp.over(socket);
stompClient.connect({}, onConnected, onError);
event.preventDefault();
}
function onConnected() {
enterRoom(room.val());
waiting.classList.add('d-none');
}
function onError(error) {
waiting.textContent = 'uh oh! service unavailable';
}
function enterRoom(newRoomId) {
var roomId = newRoomId;
Cookies.set('roomId', room);
roomIdDisplay.textContent = roomId;
topic = `/chat-app/chat/${newRoomId}`;
currentSubscription = stompClient.subscribe(`/chat-room/${roomId}`, onMessageReceived);
var username = $("#name").val().trim();
stompClient.send(`${topic}/addUser`,
{},
JSON.stringify({sender: username, type: 'JOIN'})
);
}
function onMessageReceived(payload) {
}
function sendMessage(event) {
var messageContent = $("#message").val().trim();
var username = $("#name").val().trim();
var newRoomId = $('#room').val().trim();
topic = `/chat-app/chat/${newRoomId}`;
if(messageContent && stompClient) {
var chatMessage = {
sender: username,
content: messageContent,
type: 'CHAT'
};
stompClient.send(`${topic}/sendMessage`, {}, JSON.stringify(chatMessage));
document.querySelector('#message').value = '';
}
event.preventDefault();
}
function onMessageReceived(payload) {
var message = JSON.parse(payload.body);
var messageElement = document.createElement('li');
var divCard = document.createElement('div');
divCard.className = 'card';
if(message.type === 'JOIN') {
messageElement.classList.add('event-message');
message.content = message.sender + ' joined!';
} else if (message.type === 'LEAVE') {
messageElement.classList.add('event-message');
message.content = message.sender + ' left!';
} else {
messageElement.classList.add('chat-message');
var avatarElement = document.createElement('i');
var avatarText = document.createTextNode(message.sender[0]);
avatarElement.appendChild(avatarText);
messageElement.appendChild(avatarElement);
var usernameElement = document.createElement('span');
var usernameText = document.createTextNode(message.sender);
usernameElement.appendChild(usernameText);
messageElement.appendChild(usernameElement);
var divCardBody = document.createElement('div');
divCardBody.className = 'card-body';
divCardBody.appendChild(messageElement);
divCard.appendChild(divCardBody);
}
var textElement = document.createElement('p');
var messageText = document.createTextNode(message.content);
textElement.appendChild(messageText);
messageElement.appendChild(textElement);
var messageArea = document.querySelector('#messageArea');
messageArea.appendChild(divCard);
messageArea.scrollTop = messageArea.scrollHeight;
}
$(document).ready(function() {
userJoinForm.addEventListener('submit', connect, true);
messagebox.addEventListener('submit', sendMessage, true);
});
view raw mychat.js hosted with ❤ by GitHub

Final Project Structure

Final Project Structure in Intellj

Run the project

In the project directory:

mvn package
java -jar target/npe-chatroom-0.0.1-SNAPSHOT.jar

You got to replace your jar file name in case its different.

Debugging

Posting screenshot of my terminal log while the application starts. Please notice the highlighted log in your logs.

logs

Clone the project from github

Github Link of the project


Reference and Further Reading

STOMP

SO answer on understanding websocket

Callicoder

ddycai on github

Spring using WebSocket

A sincere thanks to Rajeev Singh at Callicoder for his awesome tutorial.

If you liked this article and would like one such blog to land in your inbox every week, consider subscribing to our newsletter: https://skillcaptain.substack.com

4 thoughts on “A Tutorial on Implementing a Chat Application like Whatsapp using WebSocket and Spring

Add yours

  1. I want to Implementing a Chat Application like Whatsapp with image sharing, profile icon and single chat, can you please guide me.

    Like

Leave a Reply

Blog at WordPress.com.

Up ↑