欢迎您访问365答案网,请分享给你的朋友!
生活常识 学习资料

使用SpringBoot和WebSocket构建聊天应用程序

时间:2023-06-22

在本文中,您将学习如何在 Spring Boot 中使用 WebSocket API,并在最后构建一个简单的群聊应用程序。

您可以通过单击此链接来探索应用程序的现场演示 - https://spring-ws-chat.herokuapp.com/。

您只需输入您的姓名并开始与他人聊天。如果聊天室中没有可用的人,那么您可以在两个选项卡中打开应用程序,使用不同的用户名登录并开始发送消息。

以下是我们将在本教程中构建的聊天应用程序的屏幕截图 -

WebSocket是一种通信协议,可以在服务器和客户端之间建立双向通信通道。

WebSocket 的工作原理是首先与服务器建立常规 HTTP 连接,然后通过发送Upgrade标头将其升级为双向 Websocket 连接。

大多数现代 Web 浏览器都支持 WebSocket,对于不支持它的浏览器,我们有一些库可以为其他技术(如comet和long-polling )提供回退。

好了,既然我们知道了 websocket 是什么以及它是如何工作的,那么让我们开始实现我们的聊天应用程序。

创建应用程序

让我们使用 Spring Boot CLI 来引导我们的应用程序。查看官方 Spring Boot 文档以获取有关如何安装 Spring Boot CLI 的说明。

安装 Spring Boot CLI 后,在终端中键入以下命令以生成项目 -

$ spring init --name=websocket-demo -dependencies=websocket websocket-demo

如果您不想安装 Spring Boot CLI,不用担心,您可以使用Spring Initializer Web 工具生成项目。按照以下步骤使用 Spring Initializer 生成项目 -

转到http://start.spring.io/。将 Artifact 的值输入为websocket-demo。在依赖项部分添加Websocket 。单击生成项目以下载项目。解压下载的 zip 文件。

生成项目后,将其导入您喜欢的 IDE。项目的目录结构应如下所示 -

WebSocket 配置

第一步是配置 websocket 端点和消息代理。config在包内创建一个新com.example.websocketdemo包,然后在包内创建一个具有以下内容的新WebSocketConfig类config-

package com.example.websocketdemo.config;import org.springframework.context.annotation.Configuration;import org.springframework.messaging.simp.config.MessageBrokerRegistry;import org.springframework.web.socket.config.annotation.*;@Configuration@EnableWebSocketMessageBrokerpublic 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了接口并为其提供了一些方法来配置 websocket 连接。

在第一种方法中,我们注册了一个 websocket 端点,客户端将使用该端点连接到我们的 websocket 服务器。

注意withSockJS()端点配置的使用。SockJS用于为不支持 websocket 的浏览器启用后备选项。

您可能已经注意到方法名称中的STOMP一词。这些方法来自 Spring 框架的 STOMP 实现。STOMP 代表简单的面向文本的消息传递协议。它是一种消息协议,定义了数据交换的格式和规则。

为什么我们需要 STOMP?好吧,WebSocket 只是一种通信协议。它没有定义诸如 - 如何仅向订阅特定主题的用户发送消息,或如何向特定用户发送消息。我们需要 STOMP 来实现这些功能。

在第二种方法中,我们正在配置一个消息代理,它将用于将消息从一个客户端路由到另一个客户端。

第一行定义目的地以“/app”开头的消息应该被路由到消息处理方法(我们将很快定义这些方法)。

并且,第二行定义了目的地以“/topic”开头的消息应该被路由到消息代理。消息代理向订阅特定主题的所有连接的客户端广播消息。

在上面的示例中,我们启用了一个简单的内存消息代理。但是您可以自由使用任何其他功能齐全的消息代理,例如RabbitMQ或ActiveMQ。

创建 ChatMessage 模型

ChatMessagemodel 是将在客户端和服务器之间交换的消息负载。model在包内创建一个新包com.example.websocketdemo,然后在包内创建具有以下内容的ChatMessage类-model

package com.example.websocketdemo.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;    }}

创建用于发送和接收消息的控制器

我们将在控制器中定义消息处理方法。这些方法将负责从一个客户端接收消息,然后将其广播给其他客户端。

在基础包中创建一个新包controller,然后使用以下内容创建 ChatController 类 -

package com.example.websocketdemo.controller;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;@Controllerpublic 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;    }}

如果您从 websocket 配置中回想一下,所有从客户端发送的目标以 开头的/app消息都将路由到这些以@MessageMapping.

例如,带有目的地的消息/app/chat.sendMessage将被路由到sendMessage()方法,带有目的地的消息/app/chat.addUser将被路由到addUser()方法。

添加 WebSocket 事件监听器

我们将使用事件侦听器来侦听套接字连接和断开连接事件,以便我们可以记录这些事件并在用户加入或离开聊天室时广播它们 -

package com.example.websocketdemo.controller;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;@Componentpublic 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 事件中,我们编写了代码来从 websocket 会话中提取用户名并向所有连接的客户端广播用户离开事件。

创建前端

在目录中创建以下文件夹和文件src/main/resources-

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

该src/main/resources/static文件夹是 Spring Boot 中静态文件的默认位置。

1、创建 HTML -index.html

HTML 文件包含用于显示聊天消息的用户界面。它包括sockjs和stompjavascript 库。

SockJS 是一个 WebSocket 客户端,它尝试使用本机 WebSockets,并为不支持 WebSocket 的旧浏览器提供智能回退选项。STOMP JS 是 javascript 的 stomp 客户端。

以下是完整的代码index.html-

              Spring Boot WebSocket Chat Application      Type your username                                                                Spring WebSocket Chat Demo                                        Connecting...                        

2、Javascript -main.js

现在让我们添加连接到 websocket 端点以及发送和接收消息所需的 javascript。首先,将以下代码添加到main.js文件中,然后我们将探索该文件中的一些重要方法——

'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

最后,将以下样式添加到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;    }}

运行应用程序

您可以通过在终端中键入以下命令来运行 Spring Boot 应用程序 -

$ mvn spring-boot:run

应用程序在 Spring Boot 的默认端口 8080 上启动。您可以在http://localhost:8080浏览应用程序。

使用 RabbitMQ 作为消息代理

如果你想使用像 RabbitMQ 这样的全功能消息代理而不是简单的内存消息代理,那么只需在pom.xml文件中添加以下依赖项 -

    org.springframework.boot    spring-boot-starter-amqp    org.springframework.boot    spring-boot-starter-reactor-netty

添加上述依赖项后,您可以WebSocketConfig.java像这样在文件中启用 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");}

结论

恭喜各位!在本教程中,我们使用 Spring Boot 和 WebSocket 从头开始​​构建了一个成熟的聊天应用程序。

您可以在我的 github 存储库中找到此应用程序的完整代码。如果您喜欢您所读的内容,请在github上给我一颗星,并与您的朋友和同事分享这篇文章。

Copyright © 2016-2020 www.365daan.com All Rights Reserved. 365答案网 版权所有 备案号:

部分内容来自互联网,版权归原作者所有,如有冒犯请联系我们,我们将在三个工作时内妥善处理。