[Введение в WebSocket] Создание многопользовательского онлайн-чата WebSocket (SpringBoot+WebSocket)

WebSocket

предисловие

В этой статье создается простой чат для нескольких человек с использованием основных функций WebSocket.

Исходный код взят из хорошей статьи иностранца:

woohoo.call ICO people.com/spring-boot…

Краткое содержание этой статьи:

  • До и после окончания предварительного понимания логики взаимодействия WebSocket
  • Используйте SpringBoot + Websocket, чтобы построить демонстрацию чата с несколькими лицами
  • Исходный код и его объяснение
  • Страница внешнего интерфейса

Также в следующей статье мы сделаем:

  • Сделайте этот чат WebSocketРаспределенное преобразование, и одновременно развернуть несколько компьютеров в виде кластера для поддержки высокой степени параллелизма.
  • Сохраните сеанс пользователя и на кластереРеализовать синхронизацию сеансов, такие как отображение текущих онлайн-пользователей в режиме реального времени!

текст

Многопользовательский онлайн-чат WebSocket

Исходный код этого проекта:

Github.com/qq Xian Xia 6661 / билет ...

Новое строительство

Мы создаем новый проект SpringBoot2 и в зависимостях по умолчанию добавляем зависимости websocket:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-websocket</artifactId>
</dependency>

Конфигурация веб-сокета

Давайте сначала настроим конфигурацию websocket, создадим новую папку конфигурации и создадим в ней новый класс WebSocketConfig.

import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.*;

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/ws").withSockJS();
    }

    @Override
    public void configureMessageBroker(MessageBrokerRegistry registry) {
        registry.setApplicationDestinationPrefixes("/app");
        registry.enableSimpleBroker("/topic");
    }
}

Объяснение кода:

@EnableWebsocketMessageBroker используется для включения нашего сервера WebSocket.

Мы реализуем интерфейс WebSocketMessageBrokerConfigurer и реализуем в нем методы.

В первом методе мы регистрируем конечную точку веб-сокета, которую клиенты будут использовать для подключения к нашему серверу веб-сокета.

withSockJS() используется для включения резервных опций для браузеров, которые не поддерживают веб-сокеты, используя SockJS.

STOMP в имени метода взят из реализации STOMP Spring Framework. STOMP расшифровывается как Simple Text Oriented Messaging Protocol. Это протокол обмена сообщениями, который определяет формат и правила обмена данными. Зачем нам эта штука? Потому что WebSocket — это просто протокол связи. Он не определяет такие вещи, как: как отправлять сообщения только пользователям, подписавшимся на определенную тему, или как отправлять сообщения определенным пользователям. Нам нужен STOMP для этих функций.

В методе configureMessageBroker мы настраиваем брокера сообщений для маршрутизации сообщений от одного клиента к другому.

Первая строка определяет, что сообщения, начинающиеся с «/app», должны направляться в метод обработки сообщений (который будет определен позже).

Вторая строка определяет сообщение с «/topic» в начале сообщения, которое должно быть направлено агенту. Соединение Message Agent со всеми клиентами, подписавшимися на ту или иную тему трансляции новостей, заканчивается.

В приведенном выше примере мы используем брокера сообщений в памяти.

Впоследствии также можно использовать другие брокеры сообщений, такие как RabbitMQ или ActiveMQ.

Создать объект ChatMessage

ChatMessage используется для взаимодействия между клиентом и сервером.

Мы создаем новую папку модели и создаем класс сущности ChatMessage.

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

В сущности есть три поля:

  • тип: тип сообщения
  • содержание: содержание сообщения
  • отправитель: отправитель

Есть три типа:

  • ЧАТ: сообщение
  • ПРИСОЕДИНЯЙТЕСЬ: присоединяйтесь
  • ВЫЙТИ: уйти

Создайте контроллер для получения и отправки сообщений

Создать папку контроллера в папке добавляется класс контроллера ChatController

import com.example.websocketdemo.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 ChatController {

    @MessageMapping("/chat.sendMessage")
    @SendTo("/topic/public")
    public ChatMessage sendMessage(@Payload ChatMessage chatMessage) {
        return chatMessage;
    }

    @MessageMapping("/chat.addUser")
    @SendTo("/topic/public")
    public ChatMessage addUser(@Payload ChatMessage chatMessage, 
                               SimpMessageHeaderAccessor headerAccessor) {
        // Add username in web socket session
        headerAccessor.getSessionAttributes().put("username", chatMessage.getSender());
        return chatMessage;
    }
}

Объяснение кода:

В нашей конфигурации веб-сокета все сообщения, отправленные от клиентов, назначение которых начинается с /app, будут перенаправляться на эти методы обработки сообщений, аннотированные @MessageMapping.

Например, сообщения с пунктом назначения /app/chat.sendMessage будут перенаправлены в метод sendMessage(), а сообщения с пунктом назначения /app/chat.addUser будут перенаправлены в метод addUser().

Добавить прослушиватель событий WebSocket

После выполнения приведенного выше кода нам также необходимо отслеживать события подключения и отключения сокета, чтобы мы могли транслировать такие операции, как вход и выход пользователей.

Создайте папку прослушивателя и создайте новый класс WebSocketEventListener.

import com.example.websocketdemo.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/public", chatMessage);
        }
    }
}

Объяснение кода:

Мы транслировали событие присоединения пользователя в методе addUser(), определенном в ChatController. Поэтому нам не нужно ничего делать в событии SessionConnected.

В событии SessionDisconnect напишите код для извлечения имени пользователя из сеанса веб-сокета и трансляции события ухода пользователя всем подключенным клиентам.

Создайте интерфейсную страницу чата

Мы создаем файл интерфейса в файле src/main/resources, структура аналогична этой:

static
  └── css
       └── main.css
  └── js
       └── main.js
  └── index.html   

1. HTML-файл index.html

Файл HTML содержит пользовательский интерфейс для отображения сообщений чата. Он включает в себя две библиотеки js, sockjs и stomp.

Sockjs - это клиент Websocket, который пытается использовать эти устройства и поддержку старых браузеров, которые не поддерживают WebSocket. STOMP JS - это клиент STOMP для JavaScript.

Автор использует отечественный источник CDN в файле

<!DOCTYPE html>
<html>
  <head>
      <meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0">
      <title>Spring Boot WebSocket Chat Application</title>
      <link rel="stylesheet" href="/css/main.css" />
  </head>
  <body>
    <noscript>
      <h2>Sorry! Your browser doesn't support Javascript</h2>
    </noscript>

    <div id="username-page">
        <div class="username-page-container">
            <h1 class="title">Type your username</h1>
            <form id="usernameForm" name="usernameForm">
                <div class="form-group">
                    <input type="text" id="name" placeholder="Username" autocomplete="off" class="form-control" />
                </div>
                <div class="form-group">
                    <button type="submit" class="accent username-submit">Start Chatting</button>
                </div>
            </form>
        </div>
    </div>

    <div id="chat-page" class="hidden">
        <div class="chat-container">
            <div class="chat-header">
                <h2>Spring WebSocket Chat Demo</h2>
            </div>
            <div class="connecting">
                Connecting...
            </div>
            <ul id="messageArea">

            </ul>
            <form id="messageForm" name="messageForm">
                <div class="form-group">
                    <div class="input-group clearfix">
                        <input type="text" id="message" placeholder="Type a message..." autocomplete="off" class="form-control"/>
                        <button type="submit" class="primary">Send</button>
                    </div>
                </div>
            </form>
        </div>
    </div>

    <script src="https://cdn.bootcss.com/sockjs-client/1.1.4/sockjs.min.js"></script>
    <script src="https://cdn.bootcss.com/stomp.js/2.3.3/stomp.min.js"></script>
    <script src="/js/main.js"></script>
  </body>
</html>

2. JavaScript main.js

Добавьте javascript, необходимый для подключения к конечной точке веб-сокета, а также для отправки и получения сообщений.

'use strict';

var usernamePage = document.querySelector('#username-page');
var chatPage = document.querySelector('#chat-page');
var usernameForm = document.querySelector('#usernameForm');
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;

var colors = [
    '#2196F3', '#32c787', '#00BCD4', '#ff5652',
    '#ffc107', '#ff85af', '#FF9800', '#39bbb0'
];

function connect(event) {
    username = document.querySelector('#name').value.trim();

    if(username) {
        usernamePage.classList.add('hidden');
        chatPage.classList.remove('hidden');

        var socket = new SockJS('/ws');
        stompClient = Stomp.over(socket);

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


function onConnected() {
    // Subscribe to the Public Topic
    stompClient.subscribe('/topic/public', 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 avatarElement = document.createElement('i');
        var avatarText = document.createTextNode(message.sender[0]);
        avatarElement.appendChild(avatarText);
        avatarElement.style['background-color'] = getAvatarColor(message.sender);

        messageElement.appendChild(avatarElement);

        var usernameElement = document.createElement('span');
        var usernameText = document.createTextNode(message.sender);
        usernameElement.appendChild(usernameText);
        messageElement.appendChild(usernameElement);
    }

    var textElement = document.createElement('p');
    var messageText = document.createTextNode(message.content);
    textElement.appendChild(messageText);

    messageElement.appendChild(textElement);

    messageArea.appendChild(messageElement);
    messageArea.scrollTop = messageArea.scrollHeight;
}


function getAvatarColor(messageSender) {
    var hash = 0;
    for (var i = 0; i < messageSender.length; i++) {
        hash = 31 * hash + messageSender.charCodeAt(i);
    }
    var index = Math.abs(hash % colors.length);
    return colors[index];
}

usernameForm.addEventListener('submit', connect, true)
messageForm.addEventListener('submit', sendMessage, true)

Объяснение кода:

Функция connect() использует SockJS и клиент stomp для подключения к конечной точке /ws, которую мы настроили в Spring Boot.

После успешного подключения клиент подписывается на /topic/public и сообщает серверу имя пользователя, отправляя сообщение в пункт назначения /app/chat.addUser.

Функция stompClient.subscribe() использует метод обратного вызова, который вызывается всякий раз, когда в тему, на которую подписаны, приходит сообщение.

Дополнительный код используется для отображения и форматирования сообщения на экране.

3. CSS main.css

* {
    -webkit-box-sizing: border-box;
    -moz-box-sizing: border-box;
    box-sizing: border-box;
}

html,body {
    height: 100%;
    overflow: hidden;
}

body {
    margin: 0;
    padding: 0;
    font-weight: 400;
    font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
    font-size: 1rem;
    line-height: 1.58;
    color: #333;
    background-color: #f4f4f4;
    height: 100%;
}

body:before {
    height: 50%;
    width: 100%;
    position: absolute;
    top: 0;
    left: 0;
    background: #128ff2;
    content: "";
    z-index: 0;
}

.clearfix:after {
    display: block;
    content: "";
    clear: both;
}

.hidden {
    display: none;
}

.form-control {
    width: 100%;
    min-height: 38px;
    font-size: 15px;
    border: 1px solid #c8c8c8;
}

.form-group {
    margin-bottom: 15px;
}

input {
    padding-left: 10px;
    outline: none;
}

h1, h2, h3, h4, h5, h6 {
    margin-top: 20px;
    margin-bottom: 20px;
}

h1 {
    font-size: 1.7em;
}

a {
    color: #128ff2;
}

button {
    box-shadow: none;
    border: 1px solid transparent;
    font-size: 14px;
    outline: none;
    line-height: 100%;
    white-space: nowrap;
    vertical-align: middle;
    padding: 0.6rem 1rem;
    border-radius: 2px;
    transition: all 0.2s ease-in-out;
    cursor: pointer;
    min-height: 38px;
}

button.default {
    background-color: #e8e8e8;
    color: #333;
    box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.12);
}

button.primary {
    background-color: #128ff2;
    box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.12);
    color: #fff;
}

button.accent {
    background-color: #ff4743;
    box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.12);
    color: #fff;
}

#username-page {
    text-align: center;
}

.username-page-container {
    background: #fff;
    box-shadow: 0 1px 11px rgba(0, 0, 0, 0.27);
    border-radius: 2px;
    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;
}

.username-page-container .username-submit {
    margin-top: 10px;
}


#chat-page {
    position: relative;
    height: 100%;
}

.chat-container {
    max-width: 700px;
    margin-left: auto;
    margin-right: auto;
    background-color: #fff;
    box-shadow: 0 1px 11px rgba(0, 0, 0, 0.27);
    margin-top: 30px;
    height: calc(100% - 60px);
    max-height: 600px;
    position: relative;
}

#chat-page 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-page #messageForm {
    padding: 20px;
}

#chat-page ul li {
    line-height: 1.5rem;
    padding: 10px 20px;
    margin: 0;
    border-bottom: 1px solid #f4f4f4;
}

#chat-page ul li p {
    margin: 0;
}

#chat-page .event-message {
    width: 100%;
    text-align: center;
    clear: both;
}

#chat-page .event-message p {
    color: #777;
    font-size: 14px;
    word-wrap: break-word;
}

#chat-page .chat-message {
    padding-left: 68px;
    position: relative;
}

#chat-page .chat-message i {
    position: absolute;
    width: 42px;
    height: 42px;
    overflow: hidden;
    left: 10px;
    display: inline-block;
    vertical-align: middle;
    font-size: 18px;
    line-height: 42px;
    color: #fff;
    text-align: center;
    border-radius: 50%;
    font-style: normal;
    text-transform: uppercase;
}

#chat-page .chat-message span {
    color: #333;
    font-weight: 600;
}

#chat-page .chat-message p {
    color: #43464b;
}

#messageForm .input-group input {
    float: left;
    width: calc(100% - 85px);
}

#messageForm .input-group button {
    float: left;
    width: 80px;
    height: 38px;
    margin-left: 5px;
}

.chat-header {
    text-align: center;
    padding: 15px;
    border-bottom: 1px solid #ececec;
}

.chat-header h2 {
    margin: 0;
    font-weight: 500;
}

.connecting {
    padding-top: 5px;
    text-align: center;
    color: #777;
    position: absolute;
    top: 65px;
    width: 100%;
}


@media screen and (max-width: 730px) {

    .chat-container {
        margin-left: 10px;
        margin-right: 10px;
        margin-top: 10px;
    }
}

@media screen and (max-width: 480px) {
    .chat-container {
        height: calc(100% - 30px);
    }

    .username-page-container {
        width: auto;
        margin-left: 15px;
        margin-right: 15px;
        padding: 25px;
    }

    #chat-page ul {
        height: calc(100% - 120px);
    }

    #messageForm .input-group button {
        width: 65px;
    }

    #messageForm .input-group input {
        width: calc(100% - 70px);
    }

    .chat-header {
        padding: 10px;
    }

    .connecting {
        top: 60px;
    }

    .chat-header h2 {
        font-size: 1.1em;
    }
}

Вся структура проекта выглядит следующим образом:

запускать

Запустите проект SpringBoot

Эффект следующий:

Добавлено: использование RabbitMQ вместо памяти в качестве брокера сообщений.

Добавить зависимости:

<!-- RabbitMQ Starter Dependency -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-amqp</artifactId>
</dependency>

<!-- Following additional dependency is required for Full Featured STOMP Broker Relay -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-reactor-netty</artifactId>
</dependency>

Затем измените метод configureMessageBroker в классе WebSocketConfig, чтобы использовать RabbitMq, готово!

public void configureMessageBroker(MessageBrokerRegistry registry) {
    registry.setApplicationDestinationPrefixes("/app");

    // Use this for enabling a Full featured broker like RabbitMQ
    registry.enableStompBrokerRelay("/topic")
            .setRelayHost("localhost")
            .setRelayPort(61613)
            .setClientLogin("guest")
            .setClientPasscode("guest");
}

Таким образом, вы можете подписаться на сообщения через RabbitMq.

Суммировать

В этой статье мы создадим простой чат для нескольких человек, используя возможности WebSocket.

Исходный код этого проекта:

GitHub.com/QQ Xianxia6661/билеты…

В следующей статье мы сделаем:

  • Websocket BEРаспределенное преобразование, и одновременно развернуть несколько компьютеров в виде кластера для поддержки высокой степени параллелизма.
  • Сохраните сеанс пользователя и на кластереРеализовать синхронизацию сеансов, такие как отображение текущих онлайн-пользователей в режиме реального времени!

Следующая статья будет размещена через два дня.Также можете обратить внимание на мой официальный аккаунт: Rude3Knife, не забудете прочитать, хахахаха.

Подписывайтесь на меня

В настоящее время я инженер по разработке backend. В основном обратите внимание на направление развития задней части, безопасность данных, рептилию, расчет края.

WeChat: yangzd1102

Гитхаб:@qqxx6661

личный блог:

Оригинальное основное содержание блога

  • Полное руководство по обзору знаний Java
  • Анализ вопросов алгоритма Leetcode
  • Меч относится к анализу проблемы алгоритма предложения.
  • Боевая серия для новичков SpringCloud
  • Боевая серия новичков SpringBoot
  • Технические статьи о краулерах
  • Технические статьи, связанные с бэкенд-разработкой

Личный общедоступный номер: Rude3Knife

个人公众号:Rude3Knife

Если статья была вам полезна, пожалуйста, соберите ее и перешлите своим друзьям~