o7planning

Create a simple Chat application with Spring Boot and Websocket

View more Tutorials:

1- What is WebSocket?

WebSocket is a communication protocol, which helps establish a two-way communication channel between client and server. A communication protocol that you are familiar with is HTTP, and now, we will compare the characteristics of these two protocols:
HTTP (Hypertext Transfer Protocol): a request-response protocol. When Client (Browser) wants something, it will send a request to the Server, and the Server responds such request. HTTP is a one-way communication protocol. The purpose herein is to solve "How do you do to create a request at the client, and how do you to respond the request of the client", and this is reason so that the HTTP shines.
WebSocket is not a request-response protocol, where only Client can send a request to the Server. When a connection with WebSocket protocol is established, client & server can give data to each other, until  the lower layer connection such asTCP is closed. The WebSocket is basically similar to the TCP Socket concept. The difference is that the WebSocket is created to use for Web applications.

STOMP

STOMP (Streaming Text Oriented Messaging Protocol): a communication protocol, a branch of the WebSocket. When the client and the server contact each other by this protocol, they will send each other text message data. The relationship between STOMP and WebSocket is also similar to the one between HTTP and TCP.
In addition, the STOMP also provides a specific way to solve the following functions:  
FunctionDescription
ConnectProvide ways so that the client and the server can connect to each other.
SubscribeProvide ways so that the  client  subscribes receipt of messages of a topic.
UnsubscribeProvide ways so that the  client unsubscribes the receipt of messages of a topic.
SendHow the client sends messages to the server.
MessageHow to send a message sent from the server to the client.
Transaction managementTransaction management during data transfer (BEGIN, COMMIT, ROLLBACK,...)

2- Objective of lesson

In this leson, I am going to guide you for creating a simple Chat application, using Spring Boot and WebSocket. The Chat application is perhaps a classic and easiest-to- understand application in order to learn about the WebSocket.
This is  the preview of this application:

3- Create a Spring Boot project

On the Eclipse, create a Spring Boot project:
The full content of pom.xml file:
pom.xml
<?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>org.o7planning</groupId>
    <artifactId>SpringBootWebSocket</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>

    <name>SpringBootWebSocket</name>
    <description>Spring Boot + WebSocket Example</description>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.0.2.RELEASE</version>
        <relativePath /> <!-- lookup parent from repository -->
    </parent>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>

        <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>
SpringBootWebSocketApplication.java
package org.o7planning.springbootwebsocket;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class SpringBootWebSocketApplication {

    public static void main(String[] args) {
        SpringApplication.run(SpringBootWebSocketApplication.class, args);
    }
    
}

4- Configure the WebSocket

There are several concepts related to the WebSocket that you need to understand.

MessageBroker

MessageBroker is an intermediate program, which receives messages sent prior to distribution to necessary addresses. Therefore, you need to tell Spring to enable this program to work.
The following figure describes the structure of MessageBroker:
The MessageBroker exposes an endpoint  so that the client can contact and form a connection. To contact, the client uses the SockJS library to do this.
** javascript code **
var socket = new SockJS('/ws');
stompClient = Stomp.over(socket);

stompClient.connect({}, onConnected, onError);

// See more in main.js file.
And the MessageBroker also exposes 2 kinds of destinations  (1) & (2)
  1. Destination (1) means the topic that the client can subscribe, when a topic has messages. The messages will be sent to the clients which have subscribed this topic.
  2. Destination (2) means the places where the client can give messages to the WebSocket Server.

SockJS

Not all browsers support the WebSocket protocol. Therefore, the SockJS is a fallback option, which will be activated for the browsers not supporting the WebSocket. The SockJS is simply a JavaScript library.
WebSocketConfig.java
package org.o7planning.springbootwebsocket.config;

import org.o7planning.springbootwebsocket.interceptor.HttpHandshakeInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
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 WebSocketConfig implements WebSocketMessageBrokerConfigurer {
    
    @Autowired
    private HttpHandshakeInterceptor handshakeInterceptor;

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/ws").withSockJS().setInterceptors(handshakeInterceptor);
    }

    @Override
    public void configureMessageBroker(MessageBrokerRegistry registry) {
        registry.setApplicationDestinationPrefixes("/app");
        registry.enableSimpleBroker("/topic");
    }

}

@EnableWebSocketMessageBroker

This annotation tell with the Spring that "let's enable WebSocket Server".

HTTP Handshake

Existing infrastructure causes limitations for the deployment of the WebSocket.  Normally, HTTP uses ports 80 & 443, therefore, the WebSocket has to use other ports, while most Firewalls which block the ports other than 80 & 443, using Proxies, also have many problems. So, to be able to deploy easily, the WebSocket uses HTTP Handshake to upgrade. That means that for the first time the client sends an HTTP-based request to the server, telling the server that it is not HTTP, upgrade to the WebSocket, and thus they form a connection.
HttpHandshakeInterceptor class is used to handle events immediately before and after  WebSocket shakes hands with the HTTP. You can do something with this class.
HttpHandshakeInterceptor.java
package org.o7planning.springbootwebsocket.interceptor;

import java.util.Map;

import javax.servlet.http.HttpSession;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.server.HandshakeInterceptor;

@Component
public class HttpHandshakeInterceptor implements HandshakeInterceptor {

    private static final Logger logger = LoggerFactory.getLogger(HttpHandshakeInterceptor.class);
    
    @Override
    public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler,
            Map<String, Object> attributes) throws Exception {
        
        logger.info("Call beforeHandshake");
        
        if (request instanceof ServletServerHttpRequest) {
            ServletServerHttpRequest servletRequest = (ServletServerHttpRequest) request;
            HttpSession session = servletRequest.getServletRequest().getSession();
            attributes.put("sessionId", session.getId());
        }
        return true;
    }

    public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler,
            Exception ex) {
        logger.info("Call afterHandshake");
    }
    
}

5- Listener, Model, Controller

ChatMessage.java
package org.o7planning.springbootwebsocket.model;

public class ChatMessage {
    
    private MessageType type;
    private String content;
    private String sender;

    public enum MessageType {
        CHAT, JOIN, LEAVE
    }

    public MessageType getType() {
        return type;
    }

    public void setType(MessageType type) {
        this.type = type;
    }

    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;
    }
    
}
WebSocketEventListener.java
package org.o7planning.springbootwebsocket.listener;

import org.o7planning.springbootwebsocket.model.ChatMessage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.event.EventListener;
import org.springframework.messaging.simp.SimpMessageSendingOperations;
import org.springframework.messaging.simp.stomp.StompHeaderAccessor;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.messaging.SessionConnectedEvent;
import org.springframework.web.socket.messaging.SessionDisconnectEvent;

@Component
public class WebSocketEventListener {

    private static final Logger logger = LoggerFactory.getLogger(WebSocketEventListener.class);

    @Autowired
    private SimpMessageSendingOperations messagingTemplate;

    @EventListener
    public void handleWebSocketConnectListener(SessionConnectedEvent event) {
        logger.info("Received a new web socket connection");
    }

    @EventListener
    public void handleWebSocketDisconnectListener(SessionDisconnectEvent event) {
        StompHeaderAccessor headerAccessor = StompHeaderAccessor.wrap(event.getMessage());

        String username = (String) headerAccessor.getSessionAttributes().get("username");
        
        if(username != null) {
            logger.info("User Disconnected : " + username);

            ChatMessage chatMessage = new ChatMessage();
            chatMessage.setType(ChatMessage.MessageType.LEAVE);
            chatMessage.setSender(username);

            messagingTemplate.convertAndSend("/topic/publicChatRoom", chatMessage);
        }
    }
    
}
MainController.java
package org.o7planning.springbootwebsocket.controller;

import javax.servlet.http.HttpServletRequest;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;

@Controller
public class MainController {

    @RequestMapping("/")
    public String index(HttpServletRequest request, Model model) {
        String username = (String) request.getSession().getAttribute("username");

        if (username == null || username.isEmpty()) {
            return "redirect:/login";
        }
        model.addAttribute("username", username);

        return "chat";
    }

    @RequestMapping(path = "/login", method = RequestMethod.GET)
    public String showLoginPage() {
        return "login";
    }

    @RequestMapping(path = "/login", method = RequestMethod.POST)
    public String doLogin(HttpServletRequest request, @RequestParam(defaultValue = "") String username) {
        username = username.trim();

        if (username.isEmpty()) {
            return "login";
        }
        request.getSession().setAttribute("username", username);

        return "redirect:/";
    }

    @RequestMapping(path = "/logout")
    public String logout(HttpServletRequest request) {
        request.getSession(true).invalidate();
        
        return "redirect:/login";
    }
    
}
WebSocketController.java
package org.o7planning.springbootwebsocket.controller;

import org.o7planning.springbootwebsocket.model.ChatMessage;
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.stereotype.Controller;

@Controller
public class WebSocketController {



    @MessageMapping("/chat.sendMessage")
    @SendTo("/topic/publicChatRoom")
    public ChatMessage sendMessage(@Payload ChatMessage chatMessage) {
        return chatMessage;
    }

    @MessageMapping("/chat.addUser")
    @SendTo("/topic/publicChatRoom")
    public ChatMessage addUser(@Payload ChatMessage chatMessage, SimpMessageHeaderAccessor headerAccessor) {
        // Add username in web socket session
        headerAccessor.getSessionAttributes().put("username", chatMessage.getSender());
        return chatMessage;
    }

}

6- Html, Javascript, Css

login.html
<!DOCTYPE html>
<html>
   <head>
      <title>Login</title>
      <link rel="stylesheet" href="/css/main.css" />
   </head>
   <body>
      <div id="login-container">
         <h1 class="title">Enter your username</h1>
         <form id="loginForm" name="loginForm" method="POST">
            <input type="text" name="username" />
            <button type="submit">Login</button>
         </form>
      </div>
   </body>
</html>
 
chat.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
   <head>
      <title>Spring Boot WebSocket</title>
      <link rel="stylesheet" th:href="@{/css/main.css}" />
      
      <!-- https://cdnjs.com/libraries/sockjs-client -->
      <script src="https://cdnjs.cloudflare.com/ajax/libs/sockjs-client/1.1.4/sockjs.min.js"></script>
      <!-- https://cdnjs.com/libraries/stomp.js/ -->
      <script  src="https://cdnjs.cloudflare.com/ajax/libs/stomp.js/2.3.3/stomp.min.js"></script>
      
   </head>
   <body>
      <div id="chat-container">
         <div class="chat-header">
            <div class="user-container">
               <span id="username" th:utext="${username}"></span>
               <a th:href="@{/logout}">Logout</a>
            </div>
            <h3>Spring WebSocket Chat Demo</h3>
         </div>
         
         <hr/>
         
         <div id="connecting">Connecting...</div>
         <ul id="messageArea">
         </ul>
         <form id="messageForm" name="messageForm">
            <div class="input-message">
               <input type="text" id="message" autocomplete="off"
                  placeholder="Type a message..."/>
               <button type="submit">Send</button>
            </div>
         </form>
      </div>
      
      <script th:src="@{/js/main.js}"></script>
      
   </body>
</html>
 
main.css
* {
    -webkit-box-sizing: border-box;
    -moz-box-sizing: border-box;
    box-sizing: border-box;
}

html, body {
    height: 100%;
    overflow: hidden;
}
 
#login-page {
    text-align: center;
}

.nickname  {
    color:blue;margin-right:20px;
}
.hidden {
    display: none;
}

.user-container {
    float: right;
    margin-right:5px;
}

#login-container {
    background: #f4f6f6 ;
    border: 2px solid #ccc;
    width: 100%;
    max-width: 500px;
    display: inline-block;
    margin-top: 42px;
    vertical-align: middle;
    position: relative;
    padding: 35px 55px 35px;
    min-height: 250px;
    position: absolute;
    top: 50%;
    left: 0;
    right: 0;
    margin: 0 auto;
    margin-top: -160px;
}
 

#chat-container {
    position: relative;
    height: 100%;
}

#chat-container #messageForm {
    padding: 20px;
}

#chat-container {
    border: 2px solid #d5dbdb;
    background-color:  #d5dbdb ;
    max-width: 500px;
    margin-left: auto;
    margin-right: auto;
    
    margin-top: 30px;
    height: calc(100% - 60px);
    max-height: 600px;
    position: relative;
    
}

#chat-container ul {
    list-style-type: none;
    background-color: #fff;
    margin: 0;
    overflow: auto;
    overflow-y: scroll;
    padding: 0 20px 0px 20px;
    height: calc(100% - 150px);
}
 
#chat-container #messageForm {
    padding: 20px;
}

#chat-container ul li {
    line-height: 1.5rem;
    padding: 10px 20px;
    margin: 0;
    border-bottom: 1px solid #f4f4f4;
}

#chat-container ul li p {
    margin: 0;
}

#chat-container .event-message {
    width: 100%;
    text-align: center;
    clear: both;
}

#chat-container .event-message p {
    color: #777;
    font-size: 14px;
    word-wrap: break-word;
}

#chat-container .chat-message {
    position: relative;
}

#messageForm .input-message  {
    float: left;
    width: calc(100% - 85px);
}

.connecting {
    text-align: center;
    color: #777;
    width: 100%;
}
main.js
'use strict';


var messageForm = document.querySelector('#messageForm');
var messageInput = document.querySelector('#message');
var messageArea = document.querySelector('#messageArea');
var connectingElement = document.querySelector('#connecting');

var stompClient = null;
var username = null;
 

function connect() {
    username = document.querySelector('#username').innerText.trim();
     
    var socket = new SockJS('/ws');
    stompClient = Stomp.over(socket);

    stompClient.connect({}, onConnected, onError);
}

// Connect to WebSocket Server.
connect();

function onConnected() {
    // Subscribe to the Public Topic
    stompClient.subscribe('/topic/publicChatRoom', onMessageReceived);

    // Tell your username to the server
    stompClient.send("/app/chat.addUser",
        {},
        JSON.stringify({sender: username, type: 'JOIN'})
    )

    connectingElement.classList.add('hidden');
}


function onError(error) {
    connectingElement.textContent = 'Could not connect to WebSocket server. Please refresh this page to try again!';
    connectingElement.style.color = 'red';
}


function sendMessage(event) {
    var messageContent = messageInput.value.trim();
    if(messageContent && stompClient) {
        var chatMessage = {
            sender: username,
            content: messageInput.value,
            type: 'CHAT'
        };
        stompClient.send("/app/chat.sendMessage", {}, JSON.stringify(chatMessage));
        messageInput.value = '';
    }
    event.preventDefault();
}


function onMessageReceived(payload) {
    var message = JSON.parse(payload.body);

    var messageElement = document.createElement('li');

    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 usernameElement = document.createElement('strong');
        usernameElement.classList.add('nickname');
        var usernameText = document.createTextNode(message.sender);
        var usernameText = document.createTextNode(message.sender);
        usernameElement.appendChild(usernameText);
        messageElement.appendChild(usernameElement);
    }

    var textElement = document.createElement('span');
    var messageText = document.createTextNode(message.content);
    textElement.appendChild(messageText);

    messageElement.appendChild(textElement);

    messageArea.appendChild(messageElement);
    messageArea.scrollTop = messageArea.scrollHeight;
}
 
 
messageForm.addEventListener('submit', sendMessage, true);

View more Tutorials: