欢迎光临
我们一直在努力

Spring Boot + WebSocket超简单实战源码(前后端实时交互)

一、项目背景

在现代Web应用中,实时通信已成为必不可少的功能。无论是在线聊天、即时通知,还是实时数据展示,WebSocket技术都能提供高效的解决方案。本文将通过一个完整的实战案例,详细介绍如何使用Spring Boot + WebSocket + STOMP协议实现点对点消息推送功能。

二、技术选型

核心技术栈

  • Spring Boot 4.0.0 – 基础框架
  • WebSocket – HTML5提供的全双工通信协议
  • STOMP (Simple Text Oriented Messaging Protocol) – 简单的文本定向消息协议
  • SimMessagingTemplate – Spring提供的消息发送模板
  • Lombok – 简化Java代码
  • SockJS + STOMP.js – 前端WebSocket客户端库

为什么选择STOMP?

STOMP协议为WebSocket提供了消息路由功能,使得消息可以精确地发送到指定的订阅者,特别适合实现点对点消息推送。

三、项目结构

websocketdemo/
├── config/
│ └── WebSocketConfig.java # WebSocket配置类
├── controller/
│ └── WebSocketDemoController.java # REST接口控制器
├── service/
│ ├── WebSocketService.java # WebSocket服务接口
│ ├── MessageService.java # 消息服务接口
│ └── impl/
│ ├── WebSocketServiceImpl.java # WebSocket服务实现
│ └── MessageServiceImpl.java # 消息服务实现
└── vo/
└── MessageVO.java # 消息VO对象

四、核心实现详解

4.1 Maven依赖配置

首先在 pom.xml 中添加WebSocket依赖:

<!– Spring Boot WebSocket –>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>

<!– Lombok –>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>

4.2 WebSocket配置类

WebSocketConfig.java 是整个WebSocket功能的核心配置:

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

@Override
public void configureMessageBroker(MessageBrokerRegistry config) {
// 启用简单消息代理,客户端订阅前缀为 /topic 和 /queue
config.enableSimpleBroker("/topic", "/queue");
// 客户端发送消息前缀为 /app
config.setApplicationDestinationPrefixes("/app");
}

@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
// 注册WebSocket端点,允许跨域
registry.addEndpoint("/ws/message")
.setAllowedOrigins("*");
// 如需兼容不支持WebSocket的浏览器,可取消下面注释
// .withSockJS();
}
}

配置说明:

  • /topic – 广播模式,所有订阅该主题的客户端都能收到消息
  • /queue – 点对点模式,消息只发送给特定用户
  • /app – 客户端向服务器发送消息的前缀
  • /ws/message – WebSocket连接端点

4.3 消息VO对象

MessageVO.java 定义了消息的数据结构:

@Data
public class MessageVO implements Serializable {
private static final long serialVersionUID = 1L;

private Long userId; // 接收者ID
private String title; // 消息标题
private String content; // 消息内容
private String senderId; // 发送者ID
}

4.4 WebSocket服务层

服务接口

WebSocketService.java 定义了两个核心方法:

public interface WebSocketService {
// 广播消息
void sendMessage(String destination, Object payload);

// 点对点发送消息
void sendMessageToUser(String userId, String destination, Object payload);
}

服务实现

WebSocketServiceImpl.java 实现了消息发送逻辑:

@Service
@RequiredArgsConstructor
public class WebSocketServiceImpl implements WebSocketService {

private final SimpMessagingTemplate messagingTemplate;

@Override
public void sendMessage(String destination, Object payload) {
try {
messagingTemplate.convertAndSend(destination, payload);
log.debug("发送消息成功,目标: {}, 内容: {}", destination, payload);
} catch (Exception e) {
log.error("发送消息失败,目标: {}, 错误: {}", destination, e.getMessage(), e);
}
}

@Override
public void sendMessageToUser(String userId, String destination, Object payload) {
try {
// 发送到特定用户的队列
String userDestination = "/queue/user/" + userId + "/" + destination;
messagingTemplate.convertAndSend(userDestination, payload);
log.debug("发送消息给用户成功,用户: {}, 目标: {}, 内容: {}", userId, destination, payload);
} catch (Exception e) {
log.error("发送消息给用户失败,用户: {}, 目标: {}, 错误: {}", userId, destination, e.getMessage(), e);
}
}
}

关键点:

  • 使用 SimpMessagingTemplate 进行消息发送
  • 点对点消息使用 /queue/user/{userId}/{destination} 格式
  • 添加了异常处理和日志记录

4.5 消息服务层

MessageServiceImpl.java 封装了业务逻辑:

@Service
@RequiredArgsConstructor
public class MessageServiceImpl implements MessageService {

private final WebSocketService webSocketService;

@Override
public String sendMessage(MessageVO messageVO) {
// 发送WebSocket通知
webSocketService.sendMessageToUser(
messageVO.getUserId().toString(),
"new-message",
messageVO
);

log.info("发送消息成功,messageVO: {}", messageVO);
return "发送成功";
}
}

4.6 REST控制器

WebSocketDemoController.java 提供HTTP接口用于触发消息推送:

@RestController
@RequestMapping("/websockt-demo")
@RequiredArgsConstructor
public class WebSocketDemoController {

private final MessageService messageService;

/**
* 发送消息接口
* 请求地址: http://localhost:8091/demo-pro/websockt-demo/send?userId=1&content=hello
*/

@RequestMapping("/send")
public String send(@RequestParam("userId") Long userId,
@RequestParam("content") String content) {
MessageVO messageVO = new MessageVO();
messageVO.setUserId(userId);
messageVO.setContent(content);
messageVO.setSenderId("demo-pro-system");
messageVO.setTitle("通知");
messageService.sendMessage(messageVO);
return "success";
}
}

五、应用配置

application.yml 配置文件:

server:
port: 8091
servlet:
context-path: /demopro

spring:
application:
name: demopro

message:
websocket:
endpoint: /ws/message
allowed-origins: "*"

logging:
level:
org.springframework.web.socket: DEBUG
org.springframework.messaging: DEBUG

六、前端实现

test-websocket.html 提供了一个完整的测试页面:

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>WebSocket测试</title>
<style>
body { font-family: Arial, sans-serif; padding: 20px; max-width: 1200px; margin: 0 auto; }
#status { margin-bottom: 20px; padding: 10px; background: #f0f0f0; border-radius: 4px; }
.connected { background: #d4edda; color: #155724; }
.disconnected { background: #f8d7da; color: #721c24; }
.connecting { background: #fff3cd; color: #856404; }
#messages { border: 1px solid #ccc; padding: 10px; height: 400px; overflow-y: auto; background: #fafafa; }
.message { margin: 5px 0; padding: 10px; background: #e9ecef; border-left: 3px solid #007bff; border-radius: 4px; }
.log { color: #666; font-size: 12px; margin: 3px 0; }
.error { color: #dc3545; background: #f8d7da; padding: 10px; margin: 10px 0; border-radius: 4px; }
.info { color: #004085; background: #cce5ff; padding: 10px; margin: 10px 0; border-radius: 4px; }
.controls { margin: 20px 0; padding: 15px; background: #f8f9fa; border-radius: 4px; }
input[type="text"] { padding: 8px; border: 1px solid #ccc; border-radius: 4px; width: 200px; }
button { padding: 8px 16px; margin: 0 5px; border: none; border-radius: 4px; cursor: pointer; background: #007bff; color: white; }
button:hover { background: #0056b3; }
button:disabled { background: #ccc; cursor: not-allowed; }
</style>
</head>
<body>
<h1>WebSocket连接测试</h1>

<div id="status" class="disconnected">⚠️ 未连接</div>

<div class="controls">
<label>接收者ID:</label>
<input type="text" id="userId" value="1" placeholder="输入用户ID">
<button onclick="connect()" id="connectBtn">连接</button>
<button onclick="disconnect()" id="disconnectBtn" disabled>断开</button>
<button onclick="clearMessages()">清空消息</button>
</div>

<div id="messages"></div>

<script>
let stompClient = null;

// 加载脚本函数,支持多个CDN源
function loadScript(src, fallbackSrc) {
return new Promise((resolve, reject) => {
const script = document.createElement('script');
script.src = src;
script.onload = () => resolve();
script.onerror = () => {
if (fallbackSrc) {
const fallback = document.createElement('script');
fallback.src = fallbackSrc;
fallback.onload = () => resolve();
fallback.onerror = () => reject(new Error('Failed to load ' + src + ' and ' + fallbackSrc));
document.head.appendChild(fallback);
} else {
reject(new Error('Failed to load ' + src));
}
};
document.head.appendChild(script);
});
}

// 页面加载时初始化
window.addEventListener('load', async function() {
try {
addLog('正在加载SockJS和STOMP库…');

// 使用多个CDN源作为备份
await loadScript(
'https://cdn.jsdelivr.net/npm/sockjs-client@1.6.1/dist/sockjs.min.js',
'https://cdnjs.cloudflare.com/ajax/libs/sockjs-client/1.6.1/sockjs.min.js'
);
addLog('✓ SockJS加载成功');

await loadScript(
'https://cdn.jsdelivr.net/npm/stompjs@2.3.3/lib/stomp.min.js',
'https://cdnjs.cloudflare.com/ajax/libs/stomp.js/2.3.3/stomp.min.js'
);
addLog('✓ STOMP加载成功');

addInfo('所有库加载完成,可以开始连接WebSocket');

// 检查库是否正确加载
if (typeof SockJS === 'undefined' || typeof Stomp === 'undefined') {
throw new Error('库加载失败,请检查网络连接');
}

} catch (error) {
addError('库加载失败: ' + error.message);
addError('请尝试以下解决方案:');
addError('1. 检查网络连接');
addError('2. 手动下载库文件到本地:');
addError(' – SockJS: https://cdn.jsdelivr.net/npm/sockjs-client@1.6.1/dist/sockjs.min.js');
addError(' – STOMP: https://cdn.jsdelivr.net/npm/stompjs@2.3.3/lib/stomp.min.js');
addError('3. 将下载的文件放在与HTML文件同级目录,并修改script标签');
}
});

function connect() {
const userId = document.getElementById('userId').value.trim();

if (!userId) {
addError('请输入接收者ID');
return;
}

if (typeof SockJS === 'undefined' || typeof Stomp === 'undefined') {
addError('库未加载,请刷新页面重试');
return;
}

updateStatus('connecting', '🔄 正在连接…');
document.getElementById('connectBtn').disabled = true;

try {
const socket = new WebSocket('ws://localhost:8091/demo-pro/ws/message');
stompClient = Stomp.over(socket);

// 开启调试日志
stompClient.debug = function(str) {
console.log(str);
};

// 设置连接超时
const timeout = setTimeout(() => {
if (stompClient && !stompClient.connected) {
addError('连接超时,请检查:');
addError('1. message-center服务是否正在运行');
addError('2. 端口10120是否正确');
addError('3. 浏览器控制台是否有错误信息');
disconnect();
}
}, 10000); // 10秒超时

stompClient.connect({}, function (frame) {
clearTimeout(timeout);
console.log('Connected: ' + frame);
updateStatus('connected', '✅ 已连接');
document.getElementById('disconnectBtn').disabled = false;

// 订阅用户的专属队列
const subscription = '/queue/user/' + userId + '/new-message';
stompClient.subscribe(subscription, function (message) {
try {
const msg = JSON.parse(message.body);
console.log('Received message:', msg);
showMessage(msg);
} catch (e) {
addError('解析消息失败: ' + e.message);
console.error('Parse error:', message.body);
}
});

addInfo('✅ 已连接并订阅队列: ' + subscription);
addInfo('等待接收消息…');
}, function (error) {
clearTimeout(timeout);
console.error('Connection error:', error);
updateStatus('disconnected', '❌ 连接失败');
document.getElementById('connectBtn').disabled = false;

if (error && error.headers && error.headers.message) {
addError('连接失败: ' + error.headers.message);
} else {
addError('连接失败: ' + JSON.stringify(error));
}

addError('请检查:');
addError('1. message-center服务是否正在运行');
addError('2. 端口10120是否正确');
addError('3. 浏览器控制台(F12)是否有详细错误信息');
});

} catch (error) {
addError('连接异常: ' + error.message);
updateStatus('disconnected', '❌ 连接异常');
document.getElementById('connectBtn').disabled = false;
}
}

function disconnect() {
if (stompClient !== null && stompClient.connected) {
stompClient.disconnect();
stompClient = null;
addInfo('已断开连接');
}
updateStatus('disconnected', '⚠️ 未连接');
document.getElementById('connectBtn').disabled = false;
document.getElementById('disconnectBtn').disabled = true;
}

function showMessage(message) {
const messagesDiv = document.getElementById('messages');
const messageDiv = document.createElement('div');
messageDiv.className = 'message';

const sendTime = message.sendTime || new Date().toLocaleString();
const title = message.title || '无标题';
const content = message.content || '无内容';
const sender = message.senderId || '未知发送者';

messageDiv.innerHTML = `
<strong>📅
${sendTime}</strong><br>
<strong>📌 标题:</strong>
${title}<br>
<strong>👤 发送者:</strong>
${sender}<br>
<strong>💬 内容:</strong>
${content}
`
;
messagesDiv.insertBefore(messageDiv, messagesDiv.firstChild);
}

function addLog(log) {
const messagesDiv = document.getElementById('messages');
const logDiv = document.createElement('div');
logDiv.className = 'log';
logDiv.textContent = '[' + new Date().toLocaleTimeString() + '] ' + log;
messagesDiv.insertBefore(logDiv, messagesDiv.firstChild);
}

function addError(error) {
const messagesDiv = document.getElementById('messages');
const errorDiv = document.createElement('div');
errorDiv.className = 'error';
errorDiv.textContent = '❌ ' + error;
messagesDiv.insertBefore(errorDiv, messagesDiv.firstChild);
}

function addInfo(info) {
const messagesDiv = document.getElementById('messages');
const infoDiv = document.createElement('div');
infoDiv.className = 'info';
infoDiv.textContent = 'ℹ️ ' + info;
messagesDiv.insertBefore(infoDiv, messagesDiv.firstChild);
}

function updateStatus(status, message) {
const statusDiv = document.getElementById('status');
statusDiv.textContent = message;
statusDiv.className = status;
}

function clearMessages() {
document.getElementById('messages').innerHTML = '';
addLog('消息已清空');
}
</script>
</body>
</html>

连接WebSocket

function connect() {
const userId = document.getElementById('userId').value.trim();
const socket = new WebSocket('ws://localhost:8091/demo-pro/ws/message');
stompClient = Stomp.over(socket);

stompClient.connect({}, function (frame) {
// 订阅用户的专属队列
const subscription = '/queue/user/' + userId + '/new-message';
stompClient.subscribe(subscription, function (message) {
const msg = JSON.parse(message.body);
showMessage(msg);
});
});
}

消息展示

function showMessage(message) {
const messagesDiv = document.getElementById('messages');
const messageDiv = document.createElement('div');
messageDiv.className = 'message';

messageDiv.innerHTML = `
<strong>📅
${sendTime}</strong><br>
<strong>📌 标题:</strong>
${title}<br>
<strong>👤 发送者:</strong>
${sender}<br>
<strong>💬 内容:</strong>
${content}
`
;
messagesDiv.insertBefore(messageDiv, messagesDiv.firstChild);
}

七、消息流程图

消息代理

SimpMessagingTemplate

WebSocket服务

消息服务

REST控制器

客户端

消息代理

SimpMessagingTemplate

WebSocket服务

消息服务

REST控制器

客户端

#mermaid-svg-nMqrod1XZvoB2Zqe{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-nMqrod1XZvoB2Zqe .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-nMqrod1XZvoB2Zqe .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-nMqrod1XZvoB2Zqe .error-icon{fill:#552222;}#mermaid-svg-nMqrod1XZvoB2Zqe .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-nMqrod1XZvoB2Zqe .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-nMqrod1XZvoB2Zqe .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-nMqrod1XZvoB2Zqe .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-nMqrod1XZvoB2Zqe .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-nMqrod1XZvoB2Zqe .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-nMqrod1XZvoB2Zqe .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-nMqrod1XZvoB2Zqe .marker{fill:#333333;stroke:#333333;}#mermaid-svg-nMqrod1XZvoB2Zqe .marker.cross{stroke:#333333;}#mermaid-svg-nMqrod1XZvoB2Zqe svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-nMqrod1XZvoB2Zqe p{margin:0;}#mermaid-svg-nMqrod1XZvoB2Zqe .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-nMqrod1XZvoB2Zqe text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-nMqrod1XZvoB2Zqe .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-nMqrod1XZvoB2Zqe .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-nMqrod1XZvoB2Zqe .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-nMqrod1XZvoB2Zqe .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-nMqrod1XZvoB2Zqe #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-nMqrod1XZvoB2Zqe .sequenceNumber{fill:white;}#mermaid-svg-nMqrod1XZvoB2Zqe #sequencenumber{fill:#333;}#mermaid-svg-nMqrod1XZvoB2Zqe #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-nMqrod1XZvoB2Zqe .messageText{fill:#333;stroke:none;}#mermaid-svg-nMqrod1XZvoB2Zqe .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-nMqrod1XZvoB2Zqe .labelText,#mermaid-svg-nMqrod1XZvoB2Zqe .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-nMqrod1XZvoB2Zqe .loopText,#mermaid-svg-nMqrod1XZvoB2Zqe .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-nMqrod1XZvoB2Zqe .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-nMqrod1XZvoB2Zqe .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-nMqrod1XZvoB2Zqe .noteText,#mermaid-svg-nMqrod1XZvoB2Zqe .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-nMqrod1XZvoB2Zqe .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-nMqrod1XZvoB2Zqe .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-nMqrod1XZvoB2Zqe .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-nMqrod1XZvoB2Zqe .actorPopupMenu{position:absolute;}#mermaid-svg-nMqrod1XZvoB2Zqe .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-nMqrod1XZvoB2Zqe .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-nMqrod1XZvoB2Zqe .actor-man circle,#mermaid-svg-nMqrod1XZvoB2Zqe line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-nMqrod1XZvoB2Zqe :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}

HTTP请求 /send?userId=1&content=hello

sendMessage(messageVO)

sendMessageToUser(userId, destination, payload)

convertAndSend(/queue/user/1/new-message, payload)

转发消息

推送消息到订阅者

八、测试步骤

1. 启动应用

mvn spring-boot:run

或者使用IDEA的话,直接启动应用。

2. 打开测试页面

访问 http://localhost:8091/demo-pro/test_websocket/test-websocket.html 要等待页面所需依赖加载完成⬇️⬇️⬇️,再点击连接 在这里插入图片描述

3. 连接WebSocket

  • 输入用户ID(如:1)
  • 点击"连接"按钮
  • 等待连接成功 在这里插入图片描述

4. 发送消息

在浏览器或Postman中访问:

http://localhost:8091/demo-pro/websockt-demo/send?userId=1&content=你好,这是一条测试消息

在这里插入图片描述

5. 查看结果

客户端页面会实时显示收到的消息 在这里插入图片描述

九、核心概念总结

STOMP消息路由规则

前缀用途示例
/topic 广播消息,所有订阅者都能收到 /topic/notifications
/queue 点对点消息,只发送给特定用户 /queue/user/1/new-message
/app 客户端向服务器发送消息 /app/hello

消息发送方式

  • 广播模式 – 所有订阅 /topic 的客户端都能收到
  • 点对点模式 – 只有特定用户能收到消息
  • 用户会话模式 – 基于用户会话的精确推送
  • 十、进阶优化建议

    1. 消息持久化

    将消息存储到数据库,实现历史消息查询和离线消息推送。

    2. 消息确认机制

    实现消息送达确认,确保重要消息不会丢失。

    3. 集群支持

    使用Redis或RabbitMQ作为消息代理,支持多实例部署。

    4. 安全增强

    • 添加WebSocket连接认证
    • 实现消息加密传输
    • 限制消息频率防止滥用

    5. 性能优化

    • 使用连接池管理WebSocket连接
    • 实现消息批量发送
    • 添加消息压缩功能

    十一、常见问题

    Q1: WebSocket连接失败怎么办?

    检查以下几点:

    • 服务器端口是否正确
    • 防火墙是否允许WebSocket连接
    • 浏览器是否支持WebSocket
    • 查看服务器日志获取详细错误信息

    Q2: 如何实现离线消息推送?

    在用户上线时,查询数据库中的未读消息,通过WebSocket推送给用户。

    Q3: 如何处理消息重复?

    在消息VO中添加唯一ID,客户端收到消息后进行去重处理。

    十二、总结

    本文通过一个完整的实战案例,详细介绍了Spring Boot + WebSocket + STOMP的实现方案。该方案具有以下优势:

    ✅ 实时性强 – WebSocket提供全双工通信,消息推送延迟低 ✅ 扩展性好 – 基于STOMP协议,易于实现复杂的消息路由 ✅ 易于集成 – Spring Boot提供了完善的支持,开发效率高 ✅ 生产可用 – 包含异常处理、日志记录等生产环境必需功能

    通过以上代码,就可以独立实现一个最简单的基本功能完善的实时消息推送系统。如有疑问,欢迎在评论区交流讨论!

    赞(0)
    未经允许不得转载:171主机测评 » Spring Boot + WebSocket超简单实战源码(前后端实时交互)
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址