SpringBoot 2.x STOMP SockJS构建企业级实时通信系统的实践指南在当今快速发展的互联网应用中实时通信功能已成为提升用户体验的关键要素。从在线客服系统到协同办公工具从多人在线游戏到金融交易平台实时双向通信能力正在重塑我们与数字世界的互动方式。本文将带您深入探索如何利用SpringBoot 2.x框架结合STOMP协议和SockJS库构建一个功能完善、稳定可靠的实时通信系统。不同于简单的Hello World式教程我们将重点关注生产环境中实际会遇到的技术挑战和解决方案。您将学习到如何设计一个支持用户上下线通知的完整聊天系统如何处理网络不稳定情况下的连接恢复以及如何优化系统性能以满足不同规模的应用需求。1. 技术选型与架构设计在开始编码之前理解各个技术组件的作用及其相互关系至关重要。我们的系统架构基于以下几个核心组件SpringBoot 2.x提供了简洁的配置和快速的开发体验是Java生态中最受欢迎的微服务框架之一STOMP协议建立在WebSocket之上的简单文本协议为实时通信提供了标准的消息格式和交互模式SockJSJavaScript库在WebSocket不可用时提供优雅降级方案确保在各种浏览器和环境中的兼容性为什么选择STOMP而非原生WebSocket API虽然原生WebSocket API提供了最基本的双向通信能力但在实际项目中我们往往会遇到以下挑战需要自行设计消息格式和通信协议缺乏内置的消息路由和订阅机制难以实现复杂的消息交互模式浏览器兼容性问题处理复杂STOMP协议恰好解决了这些问题它提供了明确定义的消息帧格式目的地(Destination)和订阅(Subscription)概念支持多种消息交互模式(点对点、发布/订阅)丰富的消息头(Header)支持// STOMP帧示例 SEND destination:/app/chat content-type:application/json {sender:John,content:Hello!}2. 环境配置与基础搭建2.1 项目初始化与依赖管理首先创建一个新的SpringBoot项目确保使用2.x版本。在pom.xml中添加必要的依赖dependencies !-- WebSocket支持 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-websocket/artifactId /dependency !-- 前端模板引擎(可选) -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-thymeleaf/artifactId /dependency !-- 开发工具 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-devtools/artifactId scoperuntime/scope /dependency /dependencies2.2 WebSocket配置类实现创建WebSocket配置类是整合STOMP和SockJS的核心步骤。这个类需要实现WebSocketMessageBrokerConfigurer接口Configuration EnableWebSocketMessageBroker public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { Override public void registerStompEndpoints(StompEndpointRegistry registry) { registry.addEndpoint(/ws) .setAllowedOrigins(*) .withSockJS() .setInterceptors(httpSessionHandshakeInterceptor()); } Override public void configureMessageBroker(MessageBrokerRegistry registry) { registry.enableSimpleBroker(/topic, /queue); registry.setApplicationDestinationPrefixes(/app); registry.setUserDestinationPrefix(/user); } Bean public HandshakeInterceptor httpSessionHandshakeInterceptor() { return new HttpSessionHandshakeInterceptor(); } }关键配置说明/ws是WebSocket端点客户端将连接到此URLsetAllowedOrigins(*)允许跨域访问(生产环境应更严格限制)withSockJS()启用SockJS支持enableSimpleBroker启用了基于内存的消息代理支持/topic和/queue前缀setApplicationDestinationPrefixes定义了应用目的地的前缀提示在生产环境中应考虑使用RabbitMQ或ActiveMQ等外部消息代理替代简单内存代理以获得更好的扩展性和可靠性。3. 核心功能实现3.1 消息模型设计良好的消息模型是实时通信系统的基础。我们设计一个包含多种消息类型的灵活结构public class ChatMessage { public enum MessageType { CHAT, JOIN, LEAVE, TYPING, READ } private MessageType type; private String content; private String sender; private String timestamp; private String sessionId; // 省略getter和setter }消息类型说明类型描述使用场景CHAT普通聊天消息用户发送文本消息JOIN加入通知用户连接时广播LEAVE离开通知用户断开连接时广播TYPING输入状态显示对方正在输入READ已读回执消息已读确认3.2 消息控制器实现消息控制器负责处理客户端发送的消息并将其路由到正确的目的地Controller public class ChatController { private static final Logger logger LoggerFactory.getLogger(ChatController.class); MessageMapping(/chat.send) SendTo(/topic/public) public ChatMessage sendMessage(Payload ChatMessage chatMessage, SimpMessageHeaderAccessor headerAccessor) { chatMessage.setTimestamp(Instant.now().toString()); logger.info(收到消息: {}, chatMessage.getContent()); return chatMessage; } MessageMapping(/chat.addUser) SendTo(/topic/public) public ChatMessage addUser(Payload ChatMessage chatMessage, SimpMessageHeaderAccessor headerAccessor) { String sessionId headerAccessor.getSessionId(); headerAccessor.getSessionAttributes().put(username, chatMessage.getSender()); headerAccessor.getSessionAttributes().put(sessionId, sessionId); chatMessage.setSessionId(sessionId); chatMessage.setTimestamp(Instant.now().toString()); logger.info(用户 {} 加入会话ID: {}, chatMessage.getSender(), sessionId); return chatMessage; } }3.3 连接事件监听器为了准确追踪用户连接状态我们需要实现WebSocket事件监听Component public class WebSocketEventListener { private static final Logger logger LoggerFactory.getLogger(WebSocketEventListener.class); Autowired private SimpMessageSendingOperations messagingTemplate; EventListener public void handleWebSocketConnectListener(SessionConnectedEvent event) { StompHeaderAccessor headerAccessor StompHeaderAccessor.wrap(event.getMessage()); logger.info(新连接建立: {}, headerAccessor.getSessionId()); } EventListener public void handleWebSocketDisconnectListener(SessionDisconnectEvent event) { StompHeaderAccessor headerAccessor StompHeaderAccessor.wrap(event.getMessage()); String username (String) headerAccessor.getSessionAttributes().get(username); String sessionId headerAccessor.getSessionId(); if (username ! null) { logger.info(用户断开连接: {}, 会话ID: {}, username, sessionId); ChatMessage chatMessage new ChatMessage(); chatMessage.setType(ChatMessage.MessageType.LEAVE); chatMessage.setSender(username); chatMessage.setSessionId(sessionId); chatMessage.setTimestamp(Instant.now().toString()); messagingTemplate.convertAndSend(/topic/public, chatMessage); } } }4. 前端实现与优化4.1 基础HTML结构前端页面需要包含用户登录区和聊天区!DOCTYPE html html xmlns:thhttp://www.thymeleaf.org head meta charsetUTF-8 title实时聊天室/title link relstylesheet href/css/style.css /head body div idlogin-page div classlogin-container h1欢迎加入聊天室/h1 form idloginForm input typetext idusername placeholder输入用户名 required button typesubmit进入/button /form /div /div div idchat-page classhidden div classchat-container div classchat-header h2实时聊天室/h2 div classstatus连接状态: span idconnection-status未连接/span/div /div ul idmessageArea/ul form idmessageForm input typetext idmessage placeholder输入消息... autocompleteoff button typesubmit发送/button /form /div /div script srchttps://cdn.jsdelivr.net/npm/sockjs-client1.5.0/dist/sockjs.min.js/script script srchttps://cdn.jsdelivr.net/npm/stompjs2.3.3/lib/stomp.min.js/script script src/js/app.js/script /body /html4.2 JavaScript核心逻辑前端JavaScript代码负责建立连接、处理消息和用户交互// 连接状态管理 const ConnectionState { DISCONNECTED: 0, CONNECTING: 1, CONNECTED: 2 }; let currentState ConnectionState.DISCONNECTED; let stompClient null; let username null; let reconnectAttempts 0; const maxReconnectAttempts 5; const reconnectDelay 3000; function connect() { if (currentState ! ConnectionState.DISCONNECTED) return; currentState ConnectionState.CONNECTING; updateConnectionStatus(); const socket new SockJS(/ws); stompClient Stomp.over(socket); stompClient.connect({}, onConnected, onError); } function onConnected() { currentState ConnectionState.CONNECTED; reconnectAttempts 0; updateConnectionStatus(); // 订阅公共频道 stompClient.subscribe(/topic/public, onMessageReceived); // 发送用户加入通知 const joinMessage { sender: username, type: JOIN, timestamp: new Date().toISOString() }; stompClient.send(/app/chat.addUser, {}, JSON.stringify(joinMessage)); } function onError(error) { console.error(连接错误:, error); currentState ConnectionState.DISCONNECTED; updateConnectionStatus(); if (reconnectAttempts maxReconnectAttempts) { reconnectAttempts; setTimeout(connect, reconnectDelay); } } function onMessageReceived(payload) { const message JSON.parse(payload.body); const messageElement document.createElement(li); switch(message.type) { case JOIN: messageElement.classList.add(event-message); messageElement.textContent ${message.sender} 加入了聊天室; break; case LEAVE: messageElement.classList.add(event-message); messageElement.textContent ${message.sender} 离开了聊天室; break; case CHAT: messageElement.classList.add(chat-message); messageElement.innerHTML span classsender${message.sender}/span span classtime${formatTime(message.timestamp)}/span p classcontent${message.content}/p ; break; } document.getElementById(messageArea).appendChild(messageElement); scrollToBottom(); } // 初始化事件监听 document.getElementById(loginForm).addEventListener(submit, function(e) { e.preventDefault(); username document.getElementById(username).value.trim(); if (username) { document.getElementById(login-page).classList.add(hidden); document.getElementById(chat-page).classList.remove(hidden); connect(); } }); document.getElementById(messageForm).addEventListener(submit, function(e) { e.preventDefault(); const messageInput document.getElementById(message); const content messageInput.value.trim(); if (content stompClient) { const chatMessage { sender: username, content: content, type: CHAT, timestamp: new Date().toISOString() }; stompClient.send(/app/chat.send, {}, JSON.stringify(chatMessage)); messageInput.value ; } });4.3 用户体验优化为了提升用户体验我们可以添加以下功能连接状态指示器实时显示连接状态和网络质量消息已读回执显示消息是否已被对方阅读输入状态提示显示对方正在输入的提示消息历史记录在连接恢复后获取错过的消息断线自动重连在网络中断时自动尝试重新连接// 断线自动重连实现 function setupReconnect() { window.addEventListener(offline, () { console.log(网络连接断开); updateConnectionStatus(); }); window.addEventListener(online, () { console.log(网络连接恢复); if (currentState ConnectionState.DISCONNECTED) { connect(); } }); // 心跳检测 setInterval(() { if (currentState ConnectionState.CONNECTED (!stompClient || !stompClient.connected)) { console.log(检测到连接异常尝试重新连接); currentState ConnectionState.DISCONNECTED; connect(); } }, 5000); } // 初始化时调用 setupReconnect();5. 高级功能与生产环境考量5.1 用户认证与授权在生产环境中我们需要确保只有授权用户才能连接WebSocket端点Configuration public class WebSocketSecurityConfig extends AbstractSecurityWebSocketMessageBrokerConfigurer { Override protected void configureInbound(MessageSecurityMetadataSourceRegistry messages) { messages .simpDestMatchers(/app/**).authenticated() .simpSubscribeDestMatchers(/topic/public).permitAll() .simpSubscribeDestMatchers(/user/**).authenticated(); } Override protected boolean sameOriginDisabled() { return true; // 禁用CSRF保护以便测试生产环境应配置正确 } }5.2 性能监控与指标收集监控是生产环境不可或缺的部分我们可以使用Spring Boot Actuator收集WebSocket相关指标Configuration public class WebSocketMetricsConfig { Bean public WebSocketMetrics webSocketMetrics() { return new WebSocketMetrics(); } Bean ExportMetricWriter public MetricWriter metricWriter() { return new InMemoryMetricRepository(); } } Component public class WebSocketMetrics { private final Counter connectionCounter; private final Counter messageCounter; Autowired public WebSocketMetrics(MetricRegistry metricRegistry) { this.connectionCounter metricRegistry.counter(websocket.connections); this.messageCounter metricRegistry.counter(websocket.messages); } EventListener public void handleWebSocketConnectListener(SessionConnectedEvent event) { connectionCounter.inc(); } EventListener public void handleWebSocketMessageEvent(MessageEvent event) { messageCounter.inc(); } }5.3 集群部署方案当系统需要横向扩展时简单的内存消息代理不再适用。我们可以集成RabbitMQ作为外部消息代理# application.yml spring: rabbitmq: host: localhost port: 5672 username: guest password: guest更新WebSocket配置以使用RabbitMQConfiguration EnableWebSocketMessageBroker public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { Override public void configureMessageBroker(MessageBrokerRegistry registry) { registry.enableStompBrokerRelay(/topic, /queue) .setRelayHost(localhost) .setRelayPort(61613) .setClientLogin(guest) .setClientPasscode(guest); registry.setApplicationDestinationPrefixes(/app); registry.setUserDestinationPrefix(/user); } // 其他配置保持不变 }5.4 消息压缩与优化对于传输大量消息的场景我们可以启用消息压缩以减少带宽消耗Bean public WebSocketMessageBrokerConfigurer webSocketMessageBrokerConfigurer() { return new WebSocketMessageBrokerConfigurer() { Override public void configureWebSocketTransport(WebSocketTransportRegistration registration) { registration.setMessageSizeLimit(128 * 1024); // 128KB registration.setSendBufferSizeLimit(512 * 1024); // 512KB registration.setSendTimeLimit(10 * 1000); // 10秒 registration.setDecoratorFactories(new WebSocketHandlerDecoratorFactory() { Override public WebSocketHandler decorate(WebSocketHandler handler) { return new CompressionWebSocketHandlerDecorator(handler); } }); } }; }6. 测试策略与调试技巧6.1 单元测试与集成测试确保WebSocket功能的可靠性需要全面的测试覆盖SpringBootTest(webEnvironment WebEnvironment.RANDOM_PORT) public class WebSocketIntegrationTest { LocalServerPort private int port; private SockJsClient sockJsClient; private WebSocketStompClient stompClient; BeforeEach public void setup() { ListTransport transports new ArrayList(); transports.add(new WebSocketTransport(new StandardWebSocketClient())); this.sockJsClient new SockJsClient(transports); this.stompClient new WebSocketStompClient(sockJsClient); this.stompClient.setMessageConverter(new MappingJackson2MessageConverter()); } Test public void testWebSocketIntegration() throws Exception { BlockingQueueChatMessage messages new LinkedBlockingQueue(); StompSessionHandler handler new TestSessionHandler(messages); StompSession session stompClient.connect( ws://localhost:{port}/ws, handler, this.port).get(); // 测试消息发送 ChatMessage message new ChatMessage(); message.setType(ChatMessage.MessageType.CHAT); message.setSender(testUser); message.setContent(Hello World); session.send(/app/chat.send, message); // 验证是否收到消息 ChatMessage received messages.poll(5, TimeUnit.SECONDS); assertNotNull(received); assertEquals(Hello World, received.getContent()); } private static class TestSessionHandler extends StompSessionHandlerAdapter { private final BlockingQueueChatMessage messages; public TestSessionHandler(BlockingQueueChatMessage messages) { this.messages messages; } Override public void afterConnected(StompSession session, StompHeaders connectedHeaders) { session.subscribe(/topic/public, new StompFrameHandler() { Override public Type getPayloadType(StompHeaders headers) { return ChatMessage.class; } Override public void handleFrame(StompHeaders headers, Object payload) { messages.offer((ChatMessage) payload); } }); } } }6.2 浏览器调试工具使用现代浏览器提供了强大的WebSocket调试工具Chrome开发者工具Network → WS 标签页可以查看WebSocket连接和消息Firefox开发者工具网络 → WS 可以监控WebSocket通信Wireshark用于深度分析网络层面的WebSocket通信STOMP客户端插件如STOMP.js调试工具6.3 常见问题排查连接无法建立检查服务端是否正常运行验证端点URL是否正确检查跨域配置查看服务器日志是否有错误信息消息无法接收验证订阅路径是否正确检查消息代理配置确认消息是否发送到正确的目的地检查是否有过滤器或拦截器阻止了消息性能问题监控消息处理时间检查网络延迟评估消息大小和频率考虑启用消息压缩7. 安全最佳实践7.1 输入验证与过滤所有来自客户端的消息都应进行验证MessageMapping(/chat.send) SendTo(/topic/public) public ChatMessage sendMessage(Payload ChatMessage chatMessage, SimpMessageHeaderAccessor headerAccessor) { // 验证消息内容 if (chatMessage.getContent() null || chatMessage.getContent().trim().isEmpty()) { throw new IllegalArgumentException(消息内容不能为空); } // 防止XSS攻击 String sanitizedContent HtmlUtils.htmlEscape(chatMessage.getContent()); chatMessage.setContent(sanitizedContent); // 验证发送者 String sender (String) headerAccessor.getSessionAttributes().get(username); if (sender null || !sender.equals(chatMessage.getSender())) { throw new SecurityException(无效的用户身份); } chatMessage.setTimestamp(Instant.now().toString()); return chatMessage; }7.2 CSRF防护虽然WebSocket本身不受CSRF影响但建立WebSocket连接的HTTP请求可能受到攻击Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { Override protected void configure(HttpSecurity http) throws Exception { http .csrf() .ignoringAntMatchers(/ws/**) // 禁用CSRF保护WebSocket端点 .and() .headers() .frameOptions().sameOrigin(); } }7.3 消息加密对于敏感信息应考虑在传输层或应用层进行加密Bean public WebSocketMessageBrokerConfigurer webSocketMessageBrokerConfigurer() { return new WebSocketMessageBrokerConfigurer() { Override public void configureWebSocketTransport(WebSocketTransportRegistration registration) { registration.setDecoratorFactories(new WebSocketHandlerDecoratorFactory() { Override public WebSocketHandler decorate(WebSocketHandler handler) { return new EncryptionWebSocketHandlerDecorator(handler); } }); } }; }7.4 速率限制防止滥用和DDoS攻击Component public class RateLimitingInterceptor implements ChannelInterceptor { private final RateLimiter rateLimiter RateLimiter.create(100); // 每秒100条消息 Override public Message? preSend(Message? message, MessageChannel channel) { if (!rateLimiter.tryAcquire()) { throw new RateLimitExceededException(消息发送频率过高); } return message; } } Configuration public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { Autowired private RateLimitingInterceptor rateLimitingInterceptor; Override public void configureClientInboundChannel(ChannelRegistration registration) { registration.interceptors(rateLimitingInterceptor); } }8. 扩展功能与定制开发8.1 私聊功能实现除了公共聊天室我们还可以实现用户间的私聊功能MessageMapping(/chat.private.{userId}) SendToUser(/queue/private) public ChatMessage sendPrivateMessage(Payload ChatMessage chatMessage, DestinationVariable String userId, SimpMessageHeaderAccessor headerAccessor) { // 验证发送者权限等逻辑 chatMessage.setTimestamp(Instant.now().toString()); return chatMessage; }前端订阅私聊频道stompClient.subscribe(/user/queue/private, onPrivateMessageReceived); function sendPrivateMessage(recipientId, content) { const message { sender: username, recipient: recipientId, content: content, type: CHAT, timestamp: new Date().toISOString() }; stompClient.send(/app/chat.private.${recipientId}, {}, JSON.stringify(message)); }8.2 消息持久化将聊天消息保存到数据库以便历史查询Entity public class PersistentChatMessage { Id GeneratedValue(strategy GenerationType.IDENTITY) private Long id; private String type; private String content; private String sender; private String recipient; // 为空表示公共消息 private Instant timestamp; // 省略getter和setter } Repository public interface ChatMessageRepository extends JpaRepositoryPersistentChatMessage, Long { ListPersistentChatMessage findByTimestampAfterAndRecipient( Instant after, String recipient); ListPersistentChatMessage findByTimestampAfterAndRecipientIsNull( Instant after); } Component public class MessagePersistenceService { Autowired private ChatMessageRepository repository; EventListener public void handleChatMessage(MessageEvent event) { ChatMessage message event.getMessage(); PersistentChatMessage persistentMessage new PersistentChatMessage(); // 转换并保存消息 repository.save(persistentMessage); } }8.3 文件传输支持扩展系统以支持文件传输MessageMapping(/file.upload) public void handleFileUpload(Payload byte[] fileData, Header(filename) String filename, SimpMessageHeaderAccessor headerAccessor) { String sender (String) headerAccessor.getSessionAttributes().get(username); String fileId UUID.randomUUID().toString(); // 保存文件 fileStorageService.store(fileId, filename, fileData); // 通知接收方 ChatMessage notification new ChatMessage(); notification.setType(FILE); notification.setSender(sender); notification.setContent(fileId); notification.setTimestamp(Instant.now().toString()); messagingTemplate.convertAndSend(/topic/files, notification); }前端文件上传实现function uploadFile(file) { const reader new FileReader(); reader.onload function(event) { const fileData event.target.result; const headers { filename: file.name, content-type: application/octet-stream }; stompClient.send(/app/file.upload, headers, fileData); }; reader.readAsArrayBuffer(file); }8.4 在线用户列表维护并广播在线用户列表Component public class UserPresenceService { private final SetString onlineUsers ConcurrentHashMap.newKeySet(); Autowired private SimpMessageSendingOperations messagingTemplate; public void addUser(String username) { onlineUsers.add(username); broadcastUserList(); } public void removeUser(String username) { onlineUsers.remove(username); broadcastUserList(); } private void broadcastUserList() { messagingTemplate.convertAndSend(/topic/users, new ArrayList(onlineUsers)); } } // 在连接和断开事件中调用 EventListener public void handleWebSocketConnectListener(SessionConnectedEvent event) { String username // 从session获取用户名 userPresenceService.addUser(username); } EventListener public void handleWebSocketDisconnectListener(SessionDisconnectEvent event) { String username // 从session获取用户名 userPresenceService.removeUser(username); }前端显示在线用户stompClient.subscribe(/topic/users, function(userList) { const userListElement document.getElementById(onlineUsers); userListElement.innerHTML ; JSON.parse(userList.body).forEach(user { const li document.createElement(li); li.textContent user; userListElement.appendChild(li); }); });9. 性能优化与高级配置9.1 连接参数调优根据实际需求调整WebSocket连接参数Configuration public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { Override public void configureWebSocketTransport(WebSocketTransportRegistration registration) { registration .setMessageSizeLimit(5 * 1024 * 1024) // 5MB .setSendBufferSizeLimit(10 * 1024 * 1024) // 10MB .setSendTimeLimit(20 * 1000); // 20秒 } }9.2 消息批处理对于高频消息场景可以考虑实现消息批处理Component public class MessageBatchProcessor { private final QueueChatMessage messageQueue new ConcurrentLinkedQueue(); private final ScheduledExecutorService scheduler Executors.newSingleThreadScheduledExecutor(); Autowired private SimpMessageSendingOperations messagingTemplate; PostConstruct public void init() { scheduler.scheduleAtFixedRate(this::processBatch, 100, 100, TimeUnit.MILLiseconds); } public void addMessage(ChatMessage message) { messageQueue.offer(message); } private void processBatch() { ListChatMessage batch new ArrayList(); ChatMessage message; while ((message messageQueue.poll()) ! null batch.size() 50) { batch.add(message); } if (!batch.isEmpty()) { messagingTemplate.convertAndSend(/topic/batched, batch); } } }9.3 负载测试策略使用工具如JMeter进行WebSocket负载测试测试不同并发用户数下的性能表现测量消息延迟和吞吐量评估服务器资源使用情况(CPU、内存、网络)确定系统瓶颈和最大容量# 使用wrk进行简单压力测试 wrk -t12 -c400 -d30s --latency http://localhost:8080/ws9.4 监控与告警集成监控系统以实时跟踪WebSocket性能指标Bean public MeterRegistryCustomizerMeterRegistry metricsCommonTags() { return registry - registry.config().commonTags( application, websocket-chat, region, us-east-1 ); } Bean public WebSocketMetrics webSocketMetrics(MeterRegistry meterRegistry) { return new WebSocketMetrics(meterRegistry); } Component public class WebSocketMetrics { private final Counter messageCounter; private final Gauge connectionGauge; private final AtomicInteger connections new AtomicInteger(); public WebSocketMetrics(MeterRegistry meterRegistry) { this.messageCounter meterRegistry.counter(websocket.messages); this.connectionGauge meterRegistry.gauge(websocket.connections, connections); } EventListener public void handleMessage(MessageEvent event) { messageCounter.increment(); } EventListener public void handleConnect(SessionConnectedEvent event) { connections.incrementAndGet(); } EventListener public void handleDisconnect(SessionDisconnectEvent event) { connections.decrementAndGet(); } }10. 移动端适配与跨平台考虑10.1 响应式设计调整确保聊天界面在不同设备上都能良好显示/* 移动端适配 */ media (max-width: 768px) { .chat-container { width: 100%; height: 100vh; margin: 0; border-radius: 0; } #messageForm input { width: calc(100% - 80px); } .chat-message { padding-left: 50px; } }10.2 移动端特殊处理移动设备上需要考虑以下因素虚拟键盘弹出时的布局调整触摸事件优化网络切换处理(4G/Wi-Fi)应用休眠状态下的连接管理// 处理虚拟键盘 window.addEventListener(resize, function() { if (document.activeElement.tagName INPUT) { setTimeout(scrollToBottom, 300); } }); // 网络状态监听 function setupNetworkListeners() { window.addEventListener(online, function() { showStatusMessage(网络已恢复); if (currentState ConnectionState.DISCONNECTED) { connect(); } }); window.addEventListener(offline, function() { showStatusMessage(网络连接断开); }); }10.3 原生应用集成在原生移动应用中集成WebSocket功能Android示例(Kotlin):class WebSocketManager(private val context: Context) { private var stompClient: StompClient? null fun connect(username: String) { val socket OkHttpWebSocketClient( OkHttpClient.Builder().build(), Request.Builder().url(ws://yourserver.com/ws).build() ) stompClient Stomp.over(socket) stompClient?.connect(mapOf(), object : StompClientCallback { override fun onConnected(stompClient: StompClient) { subscribeToTopics() sendJoinMessage(username) } override fun onError(error: Exception) { // 处理错误 } }) } private fun subscribeToTopics() { stompClient?.subscribe(/topic/public) { message - val chatMessage Gson().fromJson(message.payload, ChatMessage::class.java) // 更新UI } } private fun sendJoinMessage(username: String) { val message ChatMessage( type JOIN, sender username, timestamp Instant.now().toString() ) stompClient?.send(/app/chat.addUser, Gson().toJson(message)) } }iOS示例(Swift):import StompClientLib class WebSocketManager { private var socketClient: StompClientLib? func connect(username: String) { let url URL(string: ws://yourserver.com/ws)! socketClient StompClientLib() socketClient?.openSocketWithURLRequest( request: NSURLRequest(url: url), delegate: self ) } } extension WebSocketManager: StompClientLibDelegate { func stompClientDidConnect(client: StompClientLib!) { let topic /topic/public client.subscribe(destination: topic) let joinMessage [ type: JOIN, sender: username, timestamp: ISO8601DateFormatter().string(from: Date()) ] client.sendJSONForDict( dict: joinMessage as AnyObject, toDestination: /app/chat.addUser ) } func stompClientDidDisconnect(client: StompClientLib!) { // 处理断开连接 } func stompClient(client: StompClientLib!, didReceiveMessageWithJSONBody jsonBody: AnyObject?, withHeader header: [String : String]?, withDestination destination: String) { // 处理接收到的消息 } }10.4
SpringBoot 2.x + STOMP + SockJS:手把手教你从零搭建一个带用户上下线通知的WebSocket聊天室
发布时间:2026/6/6 2:34:15
SpringBoot 2.x STOMP SockJS构建企业级实时通信系统的实践指南在当今快速发展的互联网应用中实时通信功能已成为提升用户体验的关键要素。从在线客服系统到协同办公工具从多人在线游戏到金融交易平台实时双向通信能力正在重塑我们与数字世界的互动方式。本文将带您深入探索如何利用SpringBoot 2.x框架结合STOMP协议和SockJS库构建一个功能完善、稳定可靠的实时通信系统。不同于简单的Hello World式教程我们将重点关注生产环境中实际会遇到的技术挑战和解决方案。您将学习到如何设计一个支持用户上下线通知的完整聊天系统如何处理网络不稳定情况下的连接恢复以及如何优化系统性能以满足不同规模的应用需求。1. 技术选型与架构设计在开始编码之前理解各个技术组件的作用及其相互关系至关重要。我们的系统架构基于以下几个核心组件SpringBoot 2.x提供了简洁的配置和快速的开发体验是Java生态中最受欢迎的微服务框架之一STOMP协议建立在WebSocket之上的简单文本协议为实时通信提供了标准的消息格式和交互模式SockJSJavaScript库在WebSocket不可用时提供优雅降级方案确保在各种浏览器和环境中的兼容性为什么选择STOMP而非原生WebSocket API虽然原生WebSocket API提供了最基本的双向通信能力但在实际项目中我们往往会遇到以下挑战需要自行设计消息格式和通信协议缺乏内置的消息路由和订阅机制难以实现复杂的消息交互模式浏览器兼容性问题处理复杂STOMP协议恰好解决了这些问题它提供了明确定义的消息帧格式目的地(Destination)和订阅(Subscription)概念支持多种消息交互模式(点对点、发布/订阅)丰富的消息头(Header)支持// STOMP帧示例 SEND destination:/app/chat content-type:application/json {sender:John,content:Hello!}2. 环境配置与基础搭建2.1 项目初始化与依赖管理首先创建一个新的SpringBoot项目确保使用2.x版本。在pom.xml中添加必要的依赖dependencies !-- WebSocket支持 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-websocket/artifactId /dependency !-- 前端模板引擎(可选) -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-thymeleaf/artifactId /dependency !-- 开发工具 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-devtools/artifactId scoperuntime/scope /dependency /dependencies2.2 WebSocket配置类实现创建WebSocket配置类是整合STOMP和SockJS的核心步骤。这个类需要实现WebSocketMessageBrokerConfigurer接口Configuration EnableWebSocketMessageBroker public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { Override public void registerStompEndpoints(StompEndpointRegistry registry) { registry.addEndpoint(/ws) .setAllowedOrigins(*) .withSockJS() .setInterceptors(httpSessionHandshakeInterceptor()); } Override public void configureMessageBroker(MessageBrokerRegistry registry) { registry.enableSimpleBroker(/topic, /queue); registry.setApplicationDestinationPrefixes(/app); registry.setUserDestinationPrefix(/user); } Bean public HandshakeInterceptor httpSessionHandshakeInterceptor() { return new HttpSessionHandshakeInterceptor(); } }关键配置说明/ws是WebSocket端点客户端将连接到此URLsetAllowedOrigins(*)允许跨域访问(生产环境应更严格限制)withSockJS()启用SockJS支持enableSimpleBroker启用了基于内存的消息代理支持/topic和/queue前缀setApplicationDestinationPrefixes定义了应用目的地的前缀提示在生产环境中应考虑使用RabbitMQ或ActiveMQ等外部消息代理替代简单内存代理以获得更好的扩展性和可靠性。3. 核心功能实现3.1 消息模型设计良好的消息模型是实时通信系统的基础。我们设计一个包含多种消息类型的灵活结构public class ChatMessage { public enum MessageType { CHAT, JOIN, LEAVE, TYPING, READ } private MessageType type; private String content; private String sender; private String timestamp; private String sessionId; // 省略getter和setter }消息类型说明类型描述使用场景CHAT普通聊天消息用户发送文本消息JOIN加入通知用户连接时广播LEAVE离开通知用户断开连接时广播TYPING输入状态显示对方正在输入READ已读回执消息已读确认3.2 消息控制器实现消息控制器负责处理客户端发送的消息并将其路由到正确的目的地Controller public class ChatController { private static final Logger logger LoggerFactory.getLogger(ChatController.class); MessageMapping(/chat.send) SendTo(/topic/public) public ChatMessage sendMessage(Payload ChatMessage chatMessage, SimpMessageHeaderAccessor headerAccessor) { chatMessage.setTimestamp(Instant.now().toString()); logger.info(收到消息: {}, chatMessage.getContent()); return chatMessage; } MessageMapping(/chat.addUser) SendTo(/topic/public) public ChatMessage addUser(Payload ChatMessage chatMessage, SimpMessageHeaderAccessor headerAccessor) { String sessionId headerAccessor.getSessionId(); headerAccessor.getSessionAttributes().put(username, chatMessage.getSender()); headerAccessor.getSessionAttributes().put(sessionId, sessionId); chatMessage.setSessionId(sessionId); chatMessage.setTimestamp(Instant.now().toString()); logger.info(用户 {} 加入会话ID: {}, chatMessage.getSender(), sessionId); return chatMessage; } }3.3 连接事件监听器为了准确追踪用户连接状态我们需要实现WebSocket事件监听Component public class WebSocketEventListener { private static final Logger logger LoggerFactory.getLogger(WebSocketEventListener.class); Autowired private SimpMessageSendingOperations messagingTemplate; EventListener public void handleWebSocketConnectListener(SessionConnectedEvent event) { StompHeaderAccessor headerAccessor StompHeaderAccessor.wrap(event.getMessage()); logger.info(新连接建立: {}, headerAccessor.getSessionId()); } EventListener public void handleWebSocketDisconnectListener(SessionDisconnectEvent event) { StompHeaderAccessor headerAccessor StompHeaderAccessor.wrap(event.getMessage()); String username (String) headerAccessor.getSessionAttributes().get(username); String sessionId headerAccessor.getSessionId(); if (username ! null) { logger.info(用户断开连接: {}, 会话ID: {}, username, sessionId); ChatMessage chatMessage new ChatMessage(); chatMessage.setType(ChatMessage.MessageType.LEAVE); chatMessage.setSender(username); chatMessage.setSessionId(sessionId); chatMessage.setTimestamp(Instant.now().toString()); messagingTemplate.convertAndSend(/topic/public, chatMessage); } } }4. 前端实现与优化4.1 基础HTML结构前端页面需要包含用户登录区和聊天区!DOCTYPE html html xmlns:thhttp://www.thymeleaf.org head meta charsetUTF-8 title实时聊天室/title link relstylesheet href/css/style.css /head body div idlogin-page div classlogin-container h1欢迎加入聊天室/h1 form idloginForm input typetext idusername placeholder输入用户名 required button typesubmit进入/button /form /div /div div idchat-page classhidden div classchat-container div classchat-header h2实时聊天室/h2 div classstatus连接状态: span idconnection-status未连接/span/div /div ul idmessageArea/ul form idmessageForm input typetext idmessage placeholder输入消息... autocompleteoff button typesubmit发送/button /form /div /div script srchttps://cdn.jsdelivr.net/npm/sockjs-client1.5.0/dist/sockjs.min.js/script script srchttps://cdn.jsdelivr.net/npm/stompjs2.3.3/lib/stomp.min.js/script script src/js/app.js/script /body /html4.2 JavaScript核心逻辑前端JavaScript代码负责建立连接、处理消息和用户交互// 连接状态管理 const ConnectionState { DISCONNECTED: 0, CONNECTING: 1, CONNECTED: 2 }; let currentState ConnectionState.DISCONNECTED; let stompClient null; let username null; let reconnectAttempts 0; const maxReconnectAttempts 5; const reconnectDelay 3000; function connect() { if (currentState ! ConnectionState.DISCONNECTED) return; currentState ConnectionState.CONNECTING; updateConnectionStatus(); const socket new SockJS(/ws); stompClient Stomp.over(socket); stompClient.connect({}, onConnected, onError); } function onConnected() { currentState ConnectionState.CONNECTED; reconnectAttempts 0; updateConnectionStatus(); // 订阅公共频道 stompClient.subscribe(/topic/public, onMessageReceived); // 发送用户加入通知 const joinMessage { sender: username, type: JOIN, timestamp: new Date().toISOString() }; stompClient.send(/app/chat.addUser, {}, JSON.stringify(joinMessage)); } function onError(error) { console.error(连接错误:, error); currentState ConnectionState.DISCONNECTED; updateConnectionStatus(); if (reconnectAttempts maxReconnectAttempts) { reconnectAttempts; setTimeout(connect, reconnectDelay); } } function onMessageReceived(payload) { const message JSON.parse(payload.body); const messageElement document.createElement(li); switch(message.type) { case JOIN: messageElement.classList.add(event-message); messageElement.textContent ${message.sender} 加入了聊天室; break; case LEAVE: messageElement.classList.add(event-message); messageElement.textContent ${message.sender} 离开了聊天室; break; case CHAT: messageElement.classList.add(chat-message); messageElement.innerHTML span classsender${message.sender}/span span classtime${formatTime(message.timestamp)}/span p classcontent${message.content}/p ; break; } document.getElementById(messageArea).appendChild(messageElement); scrollToBottom(); } // 初始化事件监听 document.getElementById(loginForm).addEventListener(submit, function(e) { e.preventDefault(); username document.getElementById(username).value.trim(); if (username) { document.getElementById(login-page).classList.add(hidden); document.getElementById(chat-page).classList.remove(hidden); connect(); } }); document.getElementById(messageForm).addEventListener(submit, function(e) { e.preventDefault(); const messageInput document.getElementById(message); const content messageInput.value.trim(); if (content stompClient) { const chatMessage { sender: username, content: content, type: CHAT, timestamp: new Date().toISOString() }; stompClient.send(/app/chat.send, {}, JSON.stringify(chatMessage)); messageInput.value ; } });4.3 用户体验优化为了提升用户体验我们可以添加以下功能连接状态指示器实时显示连接状态和网络质量消息已读回执显示消息是否已被对方阅读输入状态提示显示对方正在输入的提示消息历史记录在连接恢复后获取错过的消息断线自动重连在网络中断时自动尝试重新连接// 断线自动重连实现 function setupReconnect() { window.addEventListener(offline, () { console.log(网络连接断开); updateConnectionStatus(); }); window.addEventListener(online, () { console.log(网络连接恢复); if (currentState ConnectionState.DISCONNECTED) { connect(); } }); // 心跳检测 setInterval(() { if (currentState ConnectionState.CONNECTED (!stompClient || !stompClient.connected)) { console.log(检测到连接异常尝试重新连接); currentState ConnectionState.DISCONNECTED; connect(); } }, 5000); } // 初始化时调用 setupReconnect();5. 高级功能与生产环境考量5.1 用户认证与授权在生产环境中我们需要确保只有授权用户才能连接WebSocket端点Configuration public class WebSocketSecurityConfig extends AbstractSecurityWebSocketMessageBrokerConfigurer { Override protected void configureInbound(MessageSecurityMetadataSourceRegistry messages) { messages .simpDestMatchers(/app/**).authenticated() .simpSubscribeDestMatchers(/topic/public).permitAll() .simpSubscribeDestMatchers(/user/**).authenticated(); } Override protected boolean sameOriginDisabled() { return true; // 禁用CSRF保护以便测试生产环境应配置正确 } }5.2 性能监控与指标收集监控是生产环境不可或缺的部分我们可以使用Spring Boot Actuator收集WebSocket相关指标Configuration public class WebSocketMetricsConfig { Bean public WebSocketMetrics webSocketMetrics() { return new WebSocketMetrics(); } Bean ExportMetricWriter public MetricWriter metricWriter() { return new InMemoryMetricRepository(); } } Component public class WebSocketMetrics { private final Counter connectionCounter; private final Counter messageCounter; Autowired public WebSocketMetrics(MetricRegistry metricRegistry) { this.connectionCounter metricRegistry.counter(websocket.connections); this.messageCounter metricRegistry.counter(websocket.messages); } EventListener public void handleWebSocketConnectListener(SessionConnectedEvent event) { connectionCounter.inc(); } EventListener public void handleWebSocketMessageEvent(MessageEvent event) { messageCounter.inc(); } }5.3 集群部署方案当系统需要横向扩展时简单的内存消息代理不再适用。我们可以集成RabbitMQ作为外部消息代理# application.yml spring: rabbitmq: host: localhost port: 5672 username: guest password: guest更新WebSocket配置以使用RabbitMQConfiguration EnableWebSocketMessageBroker public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { Override public void configureMessageBroker(MessageBrokerRegistry registry) { registry.enableStompBrokerRelay(/topic, /queue) .setRelayHost(localhost) .setRelayPort(61613) .setClientLogin(guest) .setClientPasscode(guest); registry.setApplicationDestinationPrefixes(/app); registry.setUserDestinationPrefix(/user); } // 其他配置保持不变 }5.4 消息压缩与优化对于传输大量消息的场景我们可以启用消息压缩以减少带宽消耗Bean public WebSocketMessageBrokerConfigurer webSocketMessageBrokerConfigurer() { return new WebSocketMessageBrokerConfigurer() { Override public void configureWebSocketTransport(WebSocketTransportRegistration registration) { registration.setMessageSizeLimit(128 * 1024); // 128KB registration.setSendBufferSizeLimit(512 * 1024); // 512KB registration.setSendTimeLimit(10 * 1000); // 10秒 registration.setDecoratorFactories(new WebSocketHandlerDecoratorFactory() { Override public WebSocketHandler decorate(WebSocketHandler handler) { return new CompressionWebSocketHandlerDecorator(handler); } }); } }; }6. 测试策略与调试技巧6.1 单元测试与集成测试确保WebSocket功能的可靠性需要全面的测试覆盖SpringBootTest(webEnvironment WebEnvironment.RANDOM_PORT) public class WebSocketIntegrationTest { LocalServerPort private int port; private SockJsClient sockJsClient; private WebSocketStompClient stompClient; BeforeEach public void setup() { ListTransport transports new ArrayList(); transports.add(new WebSocketTransport(new StandardWebSocketClient())); this.sockJsClient new SockJsClient(transports); this.stompClient new WebSocketStompClient(sockJsClient); this.stompClient.setMessageConverter(new MappingJackson2MessageConverter()); } Test public void testWebSocketIntegration() throws Exception { BlockingQueueChatMessage messages new LinkedBlockingQueue(); StompSessionHandler handler new TestSessionHandler(messages); StompSession session stompClient.connect( ws://localhost:{port}/ws, handler, this.port).get(); // 测试消息发送 ChatMessage message new ChatMessage(); message.setType(ChatMessage.MessageType.CHAT); message.setSender(testUser); message.setContent(Hello World); session.send(/app/chat.send, message); // 验证是否收到消息 ChatMessage received messages.poll(5, TimeUnit.SECONDS); assertNotNull(received); assertEquals(Hello World, received.getContent()); } private static class TestSessionHandler extends StompSessionHandlerAdapter { private final BlockingQueueChatMessage messages; public TestSessionHandler(BlockingQueueChatMessage messages) { this.messages messages; } Override public void afterConnected(StompSession session, StompHeaders connectedHeaders) { session.subscribe(/topic/public, new StompFrameHandler() { Override public Type getPayloadType(StompHeaders headers) { return ChatMessage.class; } Override public void handleFrame(StompHeaders headers, Object payload) { messages.offer((ChatMessage) payload); } }); } } }6.2 浏览器调试工具使用现代浏览器提供了强大的WebSocket调试工具Chrome开发者工具Network → WS 标签页可以查看WebSocket连接和消息Firefox开发者工具网络 → WS 可以监控WebSocket通信Wireshark用于深度分析网络层面的WebSocket通信STOMP客户端插件如STOMP.js调试工具6.3 常见问题排查连接无法建立检查服务端是否正常运行验证端点URL是否正确检查跨域配置查看服务器日志是否有错误信息消息无法接收验证订阅路径是否正确检查消息代理配置确认消息是否发送到正确的目的地检查是否有过滤器或拦截器阻止了消息性能问题监控消息处理时间检查网络延迟评估消息大小和频率考虑启用消息压缩7. 安全最佳实践7.1 输入验证与过滤所有来自客户端的消息都应进行验证MessageMapping(/chat.send) SendTo(/topic/public) public ChatMessage sendMessage(Payload ChatMessage chatMessage, SimpMessageHeaderAccessor headerAccessor) { // 验证消息内容 if (chatMessage.getContent() null || chatMessage.getContent().trim().isEmpty()) { throw new IllegalArgumentException(消息内容不能为空); } // 防止XSS攻击 String sanitizedContent HtmlUtils.htmlEscape(chatMessage.getContent()); chatMessage.setContent(sanitizedContent); // 验证发送者 String sender (String) headerAccessor.getSessionAttributes().get(username); if (sender null || !sender.equals(chatMessage.getSender())) { throw new SecurityException(无效的用户身份); } chatMessage.setTimestamp(Instant.now().toString()); return chatMessage; }7.2 CSRF防护虽然WebSocket本身不受CSRF影响但建立WebSocket连接的HTTP请求可能受到攻击Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { Override protected void configure(HttpSecurity http) throws Exception { http .csrf() .ignoringAntMatchers(/ws/**) // 禁用CSRF保护WebSocket端点 .and() .headers() .frameOptions().sameOrigin(); } }7.3 消息加密对于敏感信息应考虑在传输层或应用层进行加密Bean public WebSocketMessageBrokerConfigurer webSocketMessageBrokerConfigurer() { return new WebSocketMessageBrokerConfigurer() { Override public void configureWebSocketTransport(WebSocketTransportRegistration registration) { registration.setDecoratorFactories(new WebSocketHandlerDecoratorFactory() { Override public WebSocketHandler decorate(WebSocketHandler handler) { return new EncryptionWebSocketHandlerDecorator(handler); } }); } }; }7.4 速率限制防止滥用和DDoS攻击Component public class RateLimitingInterceptor implements ChannelInterceptor { private final RateLimiter rateLimiter RateLimiter.create(100); // 每秒100条消息 Override public Message? preSend(Message? message, MessageChannel channel) { if (!rateLimiter.tryAcquire()) { throw new RateLimitExceededException(消息发送频率过高); } return message; } } Configuration public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { Autowired private RateLimitingInterceptor rateLimitingInterceptor; Override public void configureClientInboundChannel(ChannelRegistration registration) { registration.interceptors(rateLimitingInterceptor); } }8. 扩展功能与定制开发8.1 私聊功能实现除了公共聊天室我们还可以实现用户间的私聊功能MessageMapping(/chat.private.{userId}) SendToUser(/queue/private) public ChatMessage sendPrivateMessage(Payload ChatMessage chatMessage, DestinationVariable String userId, SimpMessageHeaderAccessor headerAccessor) { // 验证发送者权限等逻辑 chatMessage.setTimestamp(Instant.now().toString()); return chatMessage; }前端订阅私聊频道stompClient.subscribe(/user/queue/private, onPrivateMessageReceived); function sendPrivateMessage(recipientId, content) { const message { sender: username, recipient: recipientId, content: content, type: CHAT, timestamp: new Date().toISOString() }; stompClient.send(/app/chat.private.${recipientId}, {}, JSON.stringify(message)); }8.2 消息持久化将聊天消息保存到数据库以便历史查询Entity public class PersistentChatMessage { Id GeneratedValue(strategy GenerationType.IDENTITY) private Long id; private String type; private String content; private String sender; private String recipient; // 为空表示公共消息 private Instant timestamp; // 省略getter和setter } Repository public interface ChatMessageRepository extends JpaRepositoryPersistentChatMessage, Long { ListPersistentChatMessage findByTimestampAfterAndRecipient( Instant after, String recipient); ListPersistentChatMessage findByTimestampAfterAndRecipientIsNull( Instant after); } Component public class MessagePersistenceService { Autowired private ChatMessageRepository repository; EventListener public void handleChatMessage(MessageEvent event) { ChatMessage message event.getMessage(); PersistentChatMessage persistentMessage new PersistentChatMessage(); // 转换并保存消息 repository.save(persistentMessage); } }8.3 文件传输支持扩展系统以支持文件传输MessageMapping(/file.upload) public void handleFileUpload(Payload byte[] fileData, Header(filename) String filename, SimpMessageHeaderAccessor headerAccessor) { String sender (String) headerAccessor.getSessionAttributes().get(username); String fileId UUID.randomUUID().toString(); // 保存文件 fileStorageService.store(fileId, filename, fileData); // 通知接收方 ChatMessage notification new ChatMessage(); notification.setType(FILE); notification.setSender(sender); notification.setContent(fileId); notification.setTimestamp(Instant.now().toString()); messagingTemplate.convertAndSend(/topic/files, notification); }前端文件上传实现function uploadFile(file) { const reader new FileReader(); reader.onload function(event) { const fileData event.target.result; const headers { filename: file.name, content-type: application/octet-stream }; stompClient.send(/app/file.upload, headers, fileData); }; reader.readAsArrayBuffer(file); }8.4 在线用户列表维护并广播在线用户列表Component public class UserPresenceService { private final SetString onlineUsers ConcurrentHashMap.newKeySet(); Autowired private SimpMessageSendingOperations messagingTemplate; public void addUser(String username) { onlineUsers.add(username); broadcastUserList(); } public void removeUser(String username) { onlineUsers.remove(username); broadcastUserList(); } private void broadcastUserList() { messagingTemplate.convertAndSend(/topic/users, new ArrayList(onlineUsers)); } } // 在连接和断开事件中调用 EventListener public void handleWebSocketConnectListener(SessionConnectedEvent event) { String username // 从session获取用户名 userPresenceService.addUser(username); } EventListener public void handleWebSocketDisconnectListener(SessionDisconnectEvent event) { String username // 从session获取用户名 userPresenceService.removeUser(username); }前端显示在线用户stompClient.subscribe(/topic/users, function(userList) { const userListElement document.getElementById(onlineUsers); userListElement.innerHTML ; JSON.parse(userList.body).forEach(user { const li document.createElement(li); li.textContent user; userListElement.appendChild(li); }); });9. 性能优化与高级配置9.1 连接参数调优根据实际需求调整WebSocket连接参数Configuration public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { Override public void configureWebSocketTransport(WebSocketTransportRegistration registration) { registration .setMessageSizeLimit(5 * 1024 * 1024) // 5MB .setSendBufferSizeLimit(10 * 1024 * 1024) // 10MB .setSendTimeLimit(20 * 1000); // 20秒 } }9.2 消息批处理对于高频消息场景可以考虑实现消息批处理Component public class MessageBatchProcessor { private final QueueChatMessage messageQueue new ConcurrentLinkedQueue(); private final ScheduledExecutorService scheduler Executors.newSingleThreadScheduledExecutor(); Autowired private SimpMessageSendingOperations messagingTemplate; PostConstruct public void init() { scheduler.scheduleAtFixedRate(this::processBatch, 100, 100, TimeUnit.MILLiseconds); } public void addMessage(ChatMessage message) { messageQueue.offer(message); } private void processBatch() { ListChatMessage batch new ArrayList(); ChatMessage message; while ((message messageQueue.poll()) ! null batch.size() 50) { batch.add(message); } if (!batch.isEmpty()) { messagingTemplate.convertAndSend(/topic/batched, batch); } } }9.3 负载测试策略使用工具如JMeter进行WebSocket负载测试测试不同并发用户数下的性能表现测量消息延迟和吞吐量评估服务器资源使用情况(CPU、内存、网络)确定系统瓶颈和最大容量# 使用wrk进行简单压力测试 wrk -t12 -c400 -d30s --latency http://localhost:8080/ws9.4 监控与告警集成监控系统以实时跟踪WebSocket性能指标Bean public MeterRegistryCustomizerMeterRegistry metricsCommonTags() { return registry - registry.config().commonTags( application, websocket-chat, region, us-east-1 ); } Bean public WebSocketMetrics webSocketMetrics(MeterRegistry meterRegistry) { return new WebSocketMetrics(meterRegistry); } Component public class WebSocketMetrics { private final Counter messageCounter; private final Gauge connectionGauge; private final AtomicInteger connections new AtomicInteger(); public WebSocketMetrics(MeterRegistry meterRegistry) { this.messageCounter meterRegistry.counter(websocket.messages); this.connectionGauge meterRegistry.gauge(websocket.connections, connections); } EventListener public void handleMessage(MessageEvent event) { messageCounter.increment(); } EventListener public void handleConnect(SessionConnectedEvent event) { connections.incrementAndGet(); } EventListener public void handleDisconnect(SessionDisconnectEvent event) { connections.decrementAndGet(); } }10. 移动端适配与跨平台考虑10.1 响应式设计调整确保聊天界面在不同设备上都能良好显示/* 移动端适配 */ media (max-width: 768px) { .chat-container { width: 100%; height: 100vh; margin: 0; border-radius: 0; } #messageForm input { width: calc(100% - 80px); } .chat-message { padding-left: 50px; } }10.2 移动端特殊处理移动设备上需要考虑以下因素虚拟键盘弹出时的布局调整触摸事件优化网络切换处理(4G/Wi-Fi)应用休眠状态下的连接管理// 处理虚拟键盘 window.addEventListener(resize, function() { if (document.activeElement.tagName INPUT) { setTimeout(scrollToBottom, 300); } }); // 网络状态监听 function setupNetworkListeners() { window.addEventListener(online, function() { showStatusMessage(网络已恢复); if (currentState ConnectionState.DISCONNECTED) { connect(); } }); window.addEventListener(offline, function() { showStatusMessage(网络连接断开); }); }10.3 原生应用集成在原生移动应用中集成WebSocket功能Android示例(Kotlin):class WebSocketManager(private val context: Context) { private var stompClient: StompClient? null fun connect(username: String) { val socket OkHttpWebSocketClient( OkHttpClient.Builder().build(), Request.Builder().url(ws://yourserver.com/ws).build() ) stompClient Stomp.over(socket) stompClient?.connect(mapOf(), object : StompClientCallback { override fun onConnected(stompClient: StompClient) { subscribeToTopics() sendJoinMessage(username) } override fun onError(error: Exception) { // 处理错误 } }) } private fun subscribeToTopics() { stompClient?.subscribe(/topic/public) { message - val chatMessage Gson().fromJson(message.payload, ChatMessage::class.java) // 更新UI } } private fun sendJoinMessage(username: String) { val message ChatMessage( type JOIN, sender username, timestamp Instant.now().toString() ) stompClient?.send(/app/chat.addUser, Gson().toJson(message)) } }iOS示例(Swift):import StompClientLib class WebSocketManager { private var socketClient: StompClientLib? func connect(username: String) { let url URL(string: ws://yourserver.com/ws)! socketClient StompClientLib() socketClient?.openSocketWithURLRequest( request: NSURLRequest(url: url), delegate: self ) } } extension WebSocketManager: StompClientLibDelegate { func stompClientDidConnect(client: StompClientLib!) { let topic /topic/public client.subscribe(destination: topic) let joinMessage [ type: JOIN, sender: username, timestamp: ISO8601DateFormatter().string(from: Date()) ] client.sendJSONForDict( dict: joinMessage as AnyObject, toDestination: /app/chat.addUser ) } func stompClientDidDisconnect(client: StompClientLib!) { // 处理断开连接 } func stompClient(client: StompClientLib!, didReceiveMessageWithJSONBody jsonBody: AnyObject?, withHeader header: [String : String]?, withDestination destination: String) { // 处理接收到的消息 } }10.4