Créer une application Chat simple avec Spring Boot et Websocket

View more Tutorials:

1- Qu'est-ce qu'un WebSocket ?

WebSocket est un protocole de communication (communication protocol), il permet d'établir une chaine de communication bilatérale (two-way communication channel) entre  client et  server. Un protocole de communication que vous connaissez bien est  HTTP, et maintenant nous allons comparer les caractéristiques de ces deux protocoles :
HTTP (Hypertext Transfer Protocol) : est un protocole  request-response (Demande - Réponse). Lorsque  Client (Le navigateur) veut quelque chose, il envoie une demande au  Server et le  Server répond à cette demande- là. HTTP est un protocole de communication à sens unique. L'objectif ici est de résoudre  "Comment faites-vous pour créer une demande sur le client, et comment répondez-vous à la demande du client", et c'est une raison pour que le HTTP brille.
WebSocket : N'est pas un protocole  request-response (Demande - Réponse), où il n'y a que  Client qui peut envoyer une demande au  Server. Lorsqu'une connexion avec le protocole  WebSocket est établie, client & server peuvent partager ses données, jusqu'à quand ce que la connexion de couche inférieure telle que TCP soit fermée. À la base, le  WebSocket est similaire au concept de  TCP Socket, la différence est que le  WebSocket est créé pour utiliser des applications  Web.

STOMP

STOMP (Streaming Text Oriented Messaging Protocol): (protocole textuel orienté messages) est un protocole de communication, une branche du  WebSocket. Lorsque le  client et le  server se contactent via ce protocole, ils enverront des données textuelles orientées messages. La relation entre  STOMP et  WebSocket est silimaire à celle de  HTTP et  TCP.
En plus, le  STOMP propose également une manière précise afin de résoudre les fonctions suivantes :
Fonction
Description
Connect Fournit des façon pour que le client et le server puissent se connecter.
Subscribe Fournit des façon pour que le  client s'inscrivent (subscribe) de recevoir le message d'un sujet.
Unsubscribe Fournit des façon pour que le  client déinsrivent (unsubscribe) de recevoir le message d'un sujet.
Send Comment le client envoie des messages au server.
Message Comment envoyer un message envoyé du server au client.
Transaction management Gestion des transactions pendant le transfert de données (BEGIN, COMMIT, ROLLBACK,...)

2- Objectif de la leçon

Dans cette leçon, je vous donnerai des instructions de la création d'une application  Chat simple à l'aide de  Spring Boot et  WebSocket. L'application  Chat est peut-être une application classique et facile à comprendre pour étudier sur  WebSocket.
Voici la avant-première de cette application :

3- Créer un projet Spring Boot

Sur  Eclipse, créez un projet  Spring Boot :
Le contenu complet du fichier  pom.xml :
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- Configurer le WebSocket

Il y a quelques concepts relatifs au  WebSocket que vous devez vous informer.

MessageBroker

MessageBroker est un programme intermédiaire. Il reçoit les messages envoyés avant la distribution aux adresses nécessaires. Par conséquent, vous devez indiquer à Spring de permettre à ce programme de fonctionner (enable).
La figure suivante décrit la structure de  MessageBroker :
Le  MessageBroker expose un  endpoint (le dernier point) pour que le  client puisse se connecter et établir une connexion. Pour se connecter, le  client utilise la bibliothèque  SockJS pour le faire.
** javascript code **
var socket = new SockJS('/ws');
stompClient = Stomp.over(socket);

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

// See more in main.js file.
En parallèle, le MessageBroker expose également deux types de destination (1) & (2)
  1. La destination (1) est le sujet (topic) que le client peut "s'inscrire" (subscribe), lorsqu'un sujet recoit des message. Les messages seront envoyés aux client qui se sont inscrits à ce sujet.
  2. La destination (2) est les lieux où le client peut envoyer des messages au WebSocket Server.

SockJS

Tous les navigateurs ne soutiennent pas de protocole  WebSocket. Donc le  SockJS est une option de repli (fallback option), qui sera activée pour les navigateurs qui ne sont pas soutenus pas. Le  WebSocket. SockJS n'est qu'une bibliothèque  JavaScript.
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

Cette annotation indique à  Spring de permettre (enable) d'allumer le  WebSocket Server.

HTTP Handshake

L'infrastructure existante entraîne des limitations du déploiement de WebSocket. Normalement, HTTP utilise les ports 80 & 443, par conséquent, le WebSocket doit utiliser d'autres ports, alors que la plupart des Firewalls (des pares- feu) qui bloquent les ports autres que 80 & 443, en utilisant des Proxys (Procurations), ont aussi beaucoup de problèmes. Ainsi, pour pouvoir se déployer facilement, le WebSocket utilise HTTP Handshake (Établissement de liaison avec HTTP) pour s’équiper. Cela signifie que pour la première fois, le client envoie une demande HTTP au server, indiquant au server qu'il ne s'agit pas d'un HTTP en lui demandant de s'équiper le WebSocket. Ils forment finalement une connexion.
La classe  HttpHandshakeInterceptor sert à gérer des événements immédiatement avant et après le  WebSocket met en connexion avec le  HTTP. Vous pouvez faire quelque chose dans cette classe.
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: