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依赖:

xml 复制代码
<!-- 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功能的核心配置:

java 复制代码
@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 定义了消息的数据结构:

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 定义了两个核心方法:

java 复制代码
public interface WebSocketService {
    // 广播消息
    void sendMessage(String destination, Object payload);
    
    // 点对点发送消息
    void sendMessageToUser(String userId, String destination, Object payload);
}
服务实现

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

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 封装了业务逻辑:

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接口用于触发消息推送:

java 复制代码
@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 配置文件:

yaml 复制代码
server:
  port: 8091
  servlet:
    context-path: /demo-pro

spring:
  application:
    name: demo-pro

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

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

六、前端实现

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

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

javascript 复制代码
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);
        });
    });
}

消息展示

javascript 复制代码
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控制器 客户端 HTTP请求 /send?userId=1&content=hello sendMessage(messageVO) sendMessageToUser(userId, destination, payload) convertAndSend(/queue/user/1/new-message, payload) 转发消息 推送消息到订阅者

八、测试步骤

1. 启动应用

bash 复制代码
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

消息发送方式

  1. 广播模式 - 所有订阅 /topic 的客户端都能收到
  2. 点对点模式 - 只有特定用户能收到消息
  3. 用户会话模式 - 基于用户会话的精确推送

十、进阶优化建议

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提供了完善的支持,开发效率高

生产可用 - 包含异常处理、日志记录等生产环境必需功能

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

相关推荐
都小事儿2 小时前
U-boot:自搬移
linux·spring boot
小雨下雨的雨2 小时前
Flutter跨平台开发实战: 鸿蒙与循环交互艺术:ListView 的视口循环与内存复用
flutter·ui·华为·交互·harmonyos·鸿蒙系统
麦兜*2 小时前
Spring Boot 日志配置 + Logback vs Log4j2 性能对比 + 选型建议
spring boot·log4j·logback
牧小七2 小时前
springboot配置maven激活配置文件
spring boot·后端·maven
yangminlei2 小时前
Spring Boot 实现 DOCX 转 PDF
开发语言·spring boot·python
麦兜*2 小时前
Spring Boot整合Swagger 3.0:自动生成API文档并在线调试
java·spring boot·后端
小雨下雨的雨2 小时前
Flutter跨平台开发实战:鸿蒙循环交互艺术系列-无限加载:分页逻辑与循环骨架屏设计
flutter·华为·交互·harmonyos·鸿蒙系统
计算机毕设指导62 小时前
基于微信小程序的咖啡店点餐系统【源码文末联系】
java·spring boot·微信小程序·小程序·tomcat·maven·intellij-idea
小雨下雨的雨2 小时前
Flutter跨平台开发实战:鸿蒙系列-循环交互艺术系列——瀑布流:不规则网格的循环排布算法
算法·flutter·华为·交互·harmonyos·鸿蒙系统