betacode

Создайте простое приложение Chat с Spring Boot и Websocket

  1. Что такое WebSocket?
  2. Цель статьи
  3. Создать проект Spring Boot
  4. Конфигурировать WebSocket
  5. Listener, Model, Controller
  6. Html, Javascript, Css

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. Пункт назначения (1) это тема (topic), на которую client может "подписаться" (subscribe). Когда тема имеет сообщения, сообщения будут отправлены к client, которые подписались на данную тему.
  2. Пункт назначения (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

Show More