在本文中,您将学习如何在 Spring Boot 中使用 WebSocket API,并在最后构建一个简单的群聊应用程序。
您只需输入您的姓名并开始与他人聊天。如果聊天室中没有可用的人,那么您可以在两个选项卡中打开应用程序,使用不同的用户名登录并开始发送消息。
以下是我们将在本教程中构建的聊天应用程序的屏幕截图 -
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
@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
了接口并为其提供了一些方法来配置 websocket 连接。
在第一种方法中,我们注册了一个 websocket 端点,客户端将使用该端点连接到我们的 websocket 服务器。
注意withSockJS()
端点配置的使用。SockJS用于为不支持 websocket 的浏览器启用后备选项。
您可能已经注意到方法名称中的STOMP一词。这些方法来自 Spring 框架的 STOMP 实现。STOMP 代表简单的面向文本的消息传递协议。它是一种消息协议,定义了数据交换的格式和规则。
为什么我们需要 STOMP?好吧,WebSocket 只是一种通信协议。它没有定义诸如 - 如何仅向订阅特定主题的用户发送消息,或如何向特定用户发送消息。我们需要 STOMP 来实现这些功能。
在第二种方法中,我们正在配置一个消息代理,它将用于将消息从一个客户端路由到另一个客户端。
第一行定义目的地以“/app”开头的消息应该被路由到消息处理方法(我们将很快定义这些方法)。
并且,第二行定义了目的地以“/topic”开头的消息应该被路由到消息代理。消息代理向订阅特定主题的所有连接的客户端广播消息。
在上面的示例中,我们启用了一个简单的内存消息代理。但是您可以自由使用任何其他功能齐全的消息代理,例如RabbitMQ或ActiveMQ。
创建 ChatMessage 模型
ChatMessage
model 是将在客户端和服务器之间交换的消息负载。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;
@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;
}
}
如果您从 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;
@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 事件中,我们编写了代码来从 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
和stomp
javascript 库。
SockJS 是一个 WebSocket 客户端,它尝试使用本机 WebSockets,并为不支持 WebSocket 的旧浏览器提供智能回退选项。STOMP JS 是 javascript 的 stomp 客户端。
以下是完整的代码index.html
-
<!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://cdnjs.cloudflare.com/ajax/libs/sockjs-client/1.1.4/sockjs.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/stomp.js/2.3.3/stomp.min.js"></script>
<script src="/js/main.js"></script>
</body>
</html>
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
文件中添加以下依赖项 -
<!-- 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>
添加上述依赖项后,您可以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 存储库中找到此应用程序的完整代码。如果您喜欢您所读的内容