Créer une application Chat simple avec Spring Boot et Websocket
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)
- 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.
- 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
- Installer Spring Tool Suite pour Eclipse
- Le Tutoriel de Spring pour débutant
- Le Tutoriel de Spring Boot pour débutant
- Propriétés communes de Spring Boot
- Le Tutoriel de Spring Boot et Thymeleaf
- Le Tutoriel de Spring Boot et FreeMarker
- Le Tutoriel de Spring Boot et Groovy
- Le Tutoriel de Spring Boot et Mustache
- Le Tutoriel de Spring Boot et JSP
- Le Tutoriel de Spring Boot, Apache Tiles, JSP
- Utiliser Logging dans Spring Boot
- Surveillance des applications avec Spring Boot Actuator
- Créer une application Web multilingue avec Spring Boot
- Utiliser plusieurs ViewResolvers dans Spring Boot
- Utiliser Twitter Bootstrap dans Spring Boot
- Le Tutoriel de Spring Boot Interceptor
- Le Tutoriel de Spring Boot, Spring JDBC et Spring Transaction
- Le Tutoriel de Spring JDBC
- Le Tutoriel de Spring Boot, JPA et Spring Transaction
- Le Tutoriel de Spring Boot et Spring Data JPA
- Le Tutoriel de Spring Boot, Hibernate et Spring Transaction
- Intégration de Spring Spring, JPA et H2 Database
- Le Tutoriel de Spring Boot et MongoDB
- Utiliser plusieurs DataSources avec Spring Boot et JPA
- Utiliser plusieurs DataSources avec Spring Boot et RoutingDataSource
- Créer une application de connexion avec Spring Boot, Spring Security, Spring JDBC
- Créer une application de connexion avec Spring Boot, Spring Security, JPA
- Créer une application d'enregistrement d'utilisateur avec Spring Boot, Spring Form Validation
- Exemple de OAuth2 Social Login dans Spring Boot
- Exécuter des tâches planifiées en arrière-plan dans Spring
- Exemple CRUD Restful WebService avec Spring Boot
- Exemple Spring Boot Restful Client avec RestTemplate
- Exemple CRUD avec Spring Boot, REST et AngularJS
- Sécurité Spring RESTful Service utilisant Basic Authentication
- Sécuriser Spring Boot RESTful Service en utilisant Auth0 JWT
- Exemple Upload file avec Spring Boot
- Le exemple de Download file avec Spring Boot
- Le exemple de Upload file avec Spring Boot et jQuery Ajax
- Le exemple de Upload file avec Spring Boot et AngularJS
- Créer une application Web Panier avec Spring Boot, Hibernate
- Le Tutoriel de Spring Email
- Créer une application Chat simple avec Spring Boot et Websocket
- Déployer le application Spring Boot sur Tomcat Server
- Déployer le application Spring Boot sur Oracle WebLogic Server
- Installer un certificat SSL gratuit Let's Encrypt pour Spring Boot
- Configurer Spring Boot pour rediriger HTTP vers HTTPS
Show More