What is WebSocket?
When we run a server side application, we run it on a particular physical port like 8080, 8081 etc. To access the server side application, we use an IP. Similarly, when we log in to our web browser and request a particular website, we send a request to our computer’s IP as well as a dynamically generated port number. Hence there are four components which helps us complete the communication between our computer and the server, and these items are unique for each request.
Server IP address: It is hidden in the URL given to the client and known to the client.
Server port: It is also hidden in URL given to us and to the client.
Client IP address: It is unique for every client
Client port: It is unique for every client and is generated dynamically
When a client wants to connect to the server,
The server creates a special channel called a TCP socket to handle the connection.
The client sends a package of information, including its own IP address and a unique port number, to the server.
Upon receiving the package on a specific port, the server stores the client's IP address and port number. This helps the server keep track of the client's communication separately from others.
The server then initiates an action for that particular connection, such as fetching information about nearby cabs.
Once the server has the necessary response for the client, it retrieves the client's IP address and port number from its stored data and sends the response back. This process is similar to what happens when we make HTTP requests that we often encounter while browsing the web.
After the server sends the response, it closes the connection.
Clients always initiate the request for data, and servers respond with the requested information. Also, there can be a high demand for establishing connections, which keeps the servers busy handling multiple client requests simultaneously.
What happens when we chat or create a tool like google docs?
The client can ask for the data by making a HTTP request at a repeated interval. But this won’t be near time, comes at the high overhead of making a connection each time and developers are left with a lot of edge cases to handle.
WebSocket, istead of closing the connection right away, keeps it open until the client decides to close it. A WebSocket connection begins with an HTTP connection, including an "upgrade" header that requests the server to switch the protocol from from HTTP to WebSocket. If the server approves, it responds, indicating the protocol change to WebSocket.
In this process, both the server and client agree to switch from their previous way of communication (HTTP) to WebSocket. Interestingly, the same port can handle multiple WebSocket and HTTP requests simultaneously. Once a WebSocket connection is established, the server and client can communicate bidirectionally, without a strict request-response structure. They can exchange messages in any order and have a real-time conversation.
WebSocket provides a more flexible and efficient way for continuous communication between the server and client, allowing for seamless data exchange without the constraints of traditional request-response patterns.
What is STOMP?
STOMP is an abbreviation of Simple Text-Orientated Messaging Protocol. It defines a 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 like group 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.
Generate the project
Log on to https://start.spring.io/. You will see following screen.
Enter your preferences. We generated a maven project in this example. And added a Websocket as the dependency. You can fill up your own artifact id and group id. Click Generate Project and you will see a project getting downloaded.
Enabling this project as WebSocket Project
Spring allows us to create a configuration class that enables the project as WebSocket Project. Following is our 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 and what is the endpoint for our webSocket.
In our case, we have told the 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. This means that 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 a message, this prefix will be added to our send message address. The registerStompEndpoints() method registers the /sock endpoint to enable SockJS fallback options, to use alternate transports if WebSocket is not available. This will be more clear when we write about the client.
Creating an event-handling controller
STOMP events can be routed to @Controller
classes. For example, we 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;
}
}
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" 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>
<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>
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" 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/" 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/" 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>
<script src="https://cdnjs.cloudflare.com/ajax/libs/sockjs-client/1.3.0/sockjs.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/stomp.js/2.3.3/stomp.js"></script>
<script src="https://cdn.jsdelivr.net/npm/js-cookie@2/src/js.cookie.min.js"></script>
<script src="/js/mychat.js"></script>
</body>
</html>
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);
});
Final Project Structure
Run the project
In the project directory:
mvn package
java -jar target/npe-chatroom-0.0.1-SNAPSHOT.jar
You have to replace your jar file name in case it is different.
Debugging
Posting screenshot of our terminal log while the application starts. Please notice the highlighted log in your logs.
Clone the project from github
References and Further Reading
A sincere thanks to Rajeev Singh at Callicoder for his awesome tutorial.
One post should be on long poll vs websocket. :)