在本文中,您将学习如何在 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-amqporg.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上给我一颗星,并与您的朋友和同事分享这篇文章。