Создайте простое приложение Chat с Spring Boot и Websocket
1. Что такое WebSocket?
WebSocket это протокол связи (communication protocol), он помогает установить канал двусторонней связи (two-way communication channel) между client и server. Протокол связи, с которым вы знакомы это HTTP, и теперь мы будем сравнивать характеристики этих двух протоколов:
HTTP (Hypertext Transfer Protocol): это протокол request-response (Запрос - Ответ). Client (Браузер) что-то хочет, то отправляет запрос на Server, и Server отвечает на тот запрос. HTTP является односторонним протоколом связию Целью здесь является разрешить "Как создать запрос в client, и как ответить на запрос client", и это причина для использования HTTP.
WebSocket: Не является протоколом request-response (Запрос - Ответ), где только Client может отправить запрос к Server. Когда устанавливается соединение с протоколом WebSocket, client & server могут отправлять данные друг другу, до тех пор пока соединение на уровне ниже как TCP не закроется. WebSocket на самом деле очень похож на принцип TCP Socket, разницей является то, что WebSocket создан для использования Web приложениями.
STOMP
STOMP (Streaming Text Oriented Messaging Protocol): (Протокол обмена сообщениями) является протоколом коммуникации, веткой WebSocket. Когда client и server контактируют друг с другом через данный протокол, они будут отправлять друг другу данные в виде текстовых сообщений. Отношение между STOMP и WebSocket тоже почти похоже на отношение между HTTP и TCP.
Помимо этого, STOMP так же предоставляет определенный способ для разрешения следующих функций:
Функция | Описание |
Connect | Предоставляет способ, чтобы client и server могли соединиться друг с другом. |
Subscribe | Предоставляет способ, чтобы client подписался (subscribe) на получение сообщений на определенную тему. |
Unsubscribe | Предоставляет способ, чтобы client отменил подписку (unsubscribe) на получение сообщений на определенную тему. |
Send | Как client отправляет сообщения к server. |
Message | Как отправить сообщение от server к client. |
Transaction management | Управление транзакцией при передаче данных (BEGIN, COMMIT, ROLLBACK,...) |
2. Цель статьи
В данной статье я покажу вам как создать простое приложение Chat, используя Spring Boot и WebSocket. Приложение Chat наверно является классическим приложением и самое простое для изучения WebSocket.
Это предварительный просмотр изображения данного приложения:
3. Создать проект Spring Boot
На Eclipse создать проект Spring Boot:
Полное содержание файла 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. Конфигурировать WebSocket
Есть некоторые принципы связанные с WebSocket, когторые вам нужно понять.
MessageBroker
MessageBroker это промежуточная программа, она принимает отправленные сообщение до того как дистрибъютировать к необходимым адресам. Поэтому вас нужно сказать Spring включить (enable) данную программу для работы.
Изображение ниже описывает структуру MessageBroker:
MessageBroker выставляет endpoint (конечную точку), чтобы client мог контактировать и формировать соединение. Чтобы контактировать client использует библиотеку SockJS для этого.
** javascript code **
var socket = new SockJS('/ws');
stompClient = Stomp.over(socket);
stompClient.connect({}, onConnected, onError);
// See more in main.js file.
В то же время MessageBroker так же выставляет 2 вида пункта назначения (destination) (1) & (2)
- Пункт назначения (1) это тема (topic), на которую client может "подписаться" (subscribe). Когда тема имеет сообщения, сообщения будут отправлены к client, которые подписались на данную тему.
- Пункт назначения (2) это место, где client может отправить сообщение на WebSocket Server.
SockJS
Не все браузеры поддерживают протокол WebSocket. Поэтому SockJS является запасным вариантом (fallback option), он будет активирован для браузеров не поддерживающих WebSocket. SockJS просто является библиотекой 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
Данная аннотация говорит Spring включить (enable) WebSocket Server.
HTTP Handshake
Существующая инфраструктура может создать ограничения для развертывания WebSocket, обычно HTTP использует порт 80 & 443, поэтому WebSocket должен использовать другие порты, при этом почти все Firewall (противопожарная стена) предотвращающая порты отличающиеся от 80 & 443, используя Proxy (уполномоченный) так же происходят разные проблемы. Поэтому чтобы легко можно развертывать, WebSocket использует HTTP Handshake (Рукопожатие с HTTP) для обновления. Это означает в первый раз когда client отправляет запрос на основе HTTP к server, говорит server что это не HTTP, обновить на WebSocket, таким образом они формируют соединение.
Класс HttpHandshakeInterceptor используется для разрешения событий сразу перед и после того, как WebSocket пожал руку с HTTP. Вы можете сделать что-то с данным классом.
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);
Руководства Spring Boot
- Установите Spring Tool Suite для Eclipse
- Руководство Spring для начинающих
- Руководство Spring Boot для начинающих
- Общие свойства Spring Boot
- Руководство Spring Boot и Thymeleaf
- Руководство Spring Boot и FreeMarker
- Руководство Spring Boot и Groovy
- Руководство Spring Boot и Mustache
- Руководство Spring Boot и JSP
- Руководство Spring Boot, Apache Tiles, JSP
- Используйте Logging в Spring Boot
- Мониторинг приложений с помощью Spring Boot Actuator
- Создание веб-приложения с несколькими языками с помощью Spring Boot
- Используйте несколько ViewResolver в Spring Boot
- Используйте Twitter Bootstrap в Spring Boot
- Руководство Spring Boot Interceptor
- Руководство Spring Boot, Spring JDBC и Spring Transaction
- Руководство Spring JDBC
- Руководство Spring Boot, JPA и Spring Transaction
- Руководство Spring Boot и Spring Data JPA
- Руководство Spring Boot, Hibernate и Spring Transaction
- Интеграция Spring Boot, JPA и H2 Database
- Руководство Spring Boot и MongoDB
- Используйте несколько DataSources с Spring Boot и JPA
- Используйте несколько DataSource с Spring Boot и RoutingDataSource
- Создайте приложение для входа с Spring Boot, Spring Security, Spring JDBC
- Создайте приложение для входа с Spring Boot, Spring Security, JPA
- Создайте приложение регистрации пользователей с помощью Spring Boot, Spring Form Validation
- Пример OAuth2 Social Login в Spring Boot.
- Запускать фоновые запланированные задачи в Spring
- Пример CRUD Restful Web Service c Spring Boot
- Пример Spring Boot Restful Client c RestTemplate
- Пример CRUD с Spring Boot, REST и AngularJS
- Защита Spring Boot RESTful Service используя Basic Authentication
- Защита Spring Boot RESTful Service используя Auth0 JWT
- Пример Upload file c Spring Boot
- Пример Download file c Spring Boot
- Пример Upload file c Spring Boot и jQuery Ajax
- Пример Upload file c Spring Boot и AngularJS
- Создание веб-приложения для корзины покупок с помощью Spring Boot, Hibernate
- Руководство Spring Email
- Создайте простое приложение Chat с Spring Boot и Websocket
- Разверните приложение Spring Boot на Tomcat Server
- Развертывание приложения Spring Boot на Oracle WebLogic Server
- Установите бесплатный сертификат Let's Encrypt SSL для Spring Boot
- Настройте Spring Boot для перенаправления HTTP на HTTPS
Show More