devstory

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

  1. Qu'est-ce qu'un WebSocket ?
  2. Objectif de la leçon
  3. Créer un projet Spring Boot
  4. Configurer le WebSocket
  5. Listener, Model, Controller
  6. Html, Javascript, Css

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);

Tutoriels Spring Boot

Show More