Skip to content

Spring WebSocket STOMP 深度解析 🚀

什么是 STOMP?为什么需要它?

想象一下,你和朋友通过对讲机聊天。对讲机能传输声音(就像 WebSocket 能传输数据),但你们需要约定一套"暗号"来确保彼此理解——比如"收到请回答"、"通话结束"等。STOMP 就是 WebSocket 世界里的这套"暗号"!

NOTE

STOMP(Simple Text Oriented Messaging Protocol)是一个简单的面向文本的消息传输协议,它为 WebSocket 提供了标准化的消息格式和交互规范。

没有 STOMP 会怎样? 🤔

kotlin
// 客户端 A 发送的消息格式
websocket.send("chat:hello world")

// 客户端 B 发送的消息格式  
websocket.send("{type:'message', content:'hi there'}")

// 服务端需要解析各种不同的格式
when {
    message.startsWith("chat:") -> handleChat(message.substring(5))
    message.contains("type") -> handleJson(message)
    else -> handleUnknown(message) 
}
kotlin
// 所有客户端都使用统一的 STOMP 格式
SEND
destination:/app/chat
content-type:text/plain

hello world

STOMP 的核心价值 ✨

1. 统一的消息格式

STOMP 定义了标准的帧结构,就像邮件有固定的格式(收件人、主题、正文)一样:

COMMAND
header1:value1
header2:value2

Body^@

2. 发布-订阅模式支持

STOMP 天然支持发布-订阅模式,让消息分发变得简单优雅:

在 Spring Boot 中启用 STOMP 🚀

基础配置

kotlin
@Configuration
@EnableWebSocketMessageBroker
class WebSocketConfig : WebSocketMessageBrokerConfigurer {

    override fun configureMessageBroker(config: MessageBrokerRegistry) {
        // 启用简单消息代理,处理 /topic 和 /queue 前缀的消息
        config.enableSimpleBroker("/topic", "/queue") 
        
        // 设置应用程序目的地前缀,客户端发送消息的目标前缀
        config.setApplicationDestinationPrefixes("/app") 
    }

    override fun registerStompEndpoints(registry: StompEndpointRegistry) {
        // 注册 STOMP 端点,客户端连接的 URL
        registry.addEndpoint("/ws")
            .setAllowedOriginPatterns("*") // 允许跨域
            .withSockJS() // 启用 SockJS 降级支持
    }
}

TIP

目的地前缀的作用

  • /app:客户端发送消息给服务器处理
  • /topic:发布-订阅模式,一对多广播
  • /queue:点对点模式,一对一消息

创建消息处理控制器

kotlin
@Controller
class ChatController {

    @MessageMapping("/chat.sendMessage") 
    @SendTo("/topic/public") 
    fun sendMessage(@Payload chatMessage: ChatMessage): ChatMessage {
        // 处理聊天消息并广播给所有订阅者
        return chatMessage.copy(
            timestamp = System.currentTimeMillis(),
            sender = getCurrentUser() // 设置发送者信息
        )
    }

    @MessageMapping("/chat.addUser")
    @SendTo("/topic/public")
    fun addUser(
        @Payload chatMessage: ChatMessage,
        headerAccessor: SimpMessageHeaderAccessor
    ): ChatMessage {
        // 将用户名添加到 WebSocket 会话中
        headerAccessor.sessionAttributes?.put("username", chatMessage.sender) 
        
        return chatMessage.copy(
            type = MessageType.JOIN,
            content = "${chatMessage.sender} 加入了聊天室!"
        )
    }
}

data class ChatMessage(
    val type: MessageType,
    val content: String,
    val sender: String,
    val timestamp: Long = 0
)

enum class MessageType {
    CHAT, JOIN, LEAVE
}

消息流转过程详解 🔄

让我们通过一个完整的聊天室例子来理解 STOMP 消息的流转:

实战案例:构建实时通知系统 📢

场景描述

构建一个电商系统的实时通知功能,当有新订单时,管理员能实时收到通知。

kotlin
@RestController
@RequestMapping("/api/orders")
class OrderController(
    private val messagingTemplate: SimpMessagingTemplate
) {

    @PostMapping
    fun createOrder(@RequestBody order: Order): ResponseEntity<Order> {
        // 1. 保存订单
        val savedOrder = orderService.save(order)
        
        // 2. 发送实时通知给管理员
        val notification = OrderNotification(
            orderId = savedOrder.id,
            customerName = savedOrder.customerName,
            amount = savedOrder.totalAmount,
            message = "新订单 #${savedOrder.id} 需要处理"
        )
        
        // 发送到管理员专用频道
        messagingTemplate.convertAndSend("/topic/admin/orders", notification)
        
        return ResponseEntity.ok(savedOrder)
    }
}

@Controller
class NotificationController {

    @MessageMapping("/admin.subscribe") 
    @SendToUser("/queue/notifications") 
    fun subscribeToAdminNotifications(principal: Principal): String {
        // 验证管理员权限
        if (!isAdmin(principal)) {
            throw AccessDeniedException("需要管理员权限") 
        }
        return "订阅成功,您将收到实时订单通知"
    }
}

前端 JavaScript 集成

点击查看完整的前端代码示例
javascript
// 建立 STOMP 连接
const socket = new SockJS('/ws');
const stompClient = Stomp.over(socket);

stompClient.connect({}, function (frame) {
    console.log('Connected: ' + frame);
    
    // 订阅管理员通知
    stompClient.subscribe('/topic/admin/orders', function (message) {
        const notification = JSON.parse(message.body);
        showNotification(notification);
    });
    
    // 订阅个人消息队列
    stompClient.subscribe('/user/queue/notifications', function (message) {
        const personalMessage = JSON.parse(message.body);
        showPersonalMessage(personalMessage);
    });
});

function showNotification(notification) {
    // 显示桌面通知
    if (Notification.permission === 'granted') {
        new Notification('新订单通知', {
            body: notification.message,
            icon: '/icons/order.png'
        });
    }
    
    // 更新页面 UI
    const notificationElement = document.createElement('div');
    notificationElement.className = 'notification';
    notificationElement.innerHTML = `
        <h4>订单 #${notification.orderId}</h4>
        <p>客户:${notification.customerName}</p>
        <p>金额:¥${notification.amount}</p>
    `;
    document.getElementById('notifications').appendChild(notificationElement);
}

高级特性:用户专属消息 👤

STOMP 支持向特定用户发送私有消息,这在实现个人通知、私聊等功能时非常有用:

kotlin
@Controller
class PrivateMessageController(
    private val messagingTemplate: SimpMessagingTemplate
) {

    @MessageMapping("/private.message")
    fun sendPrivateMessage(
        @Payload message: PrivateMessage,
        principal: Principal
    ) {
        // 验证发送者权限
        if (principal.name != message.from) {
            throw SecurityException("无权代替他人发送消息") 
        }
        
        // 发送私有消息给特定用户
        messagingTemplate.convertAndSendToUser(
            message.to,                    // 目标用户
            "/queue/private",              // 私有消息队列
            message.copy(timestamp = System.currentTimeMillis())
        )
        
        // 同时给发送者一个确认
        messagingTemplate.convertAndSendToUser(
            message.from,
            "/queue/sent",
            "消息已发送给 ${message.to}"
        )
    }
}

data class PrivateMessage(
    val from: String,
    val to: String,
    val content: String,
    val timestamp: Long = 0
)

IMPORTANT

用户目的地的工作原理

  • 当使用 convertAndSendToUser(user, "/queue/private", message)
  • Spring 会自动将目的地转换为 /user/{username}/queue/private
  • 只有对应的用户会收到这条消息

性能优化与最佳实践 ⚡

1. 消息代理选择

kotlin
@Configuration
class SimpleWebSocketConfig : WebSocketMessageBrokerConfigurer {
    
    override fun configureMessageBroker(config: MessageBrokerRegistry) {
        // 简单内存代理,单机部署
        config.enableSimpleBroker("/topic", "/queue")
            .setHeartbeatValue(longArrayOf(10000, 10000)) // 心跳检测
    }
}
kotlin
@Configuration  
class ExternalBrokerConfig : WebSocketMessageBrokerConfigurer {
    
    override fun configureMessageBroker(config: MessageBrokerRegistry) {
        // 使用 RabbitMQ 作为消息代理
        config.enableStompBrokerRelay("/topic", "/queue")
            .setRelayHost("localhost")
            .setRelayPort(61613)
            .setClientLogin("guest")
            .setClientPasscode("guest")
            .setSystemLogin("guest")
            .setSystemPasscode("guest")
    }
}

2. 连接管理和监控

kotlin
@Component
class WebSocketEventListener {

    private val logger = LoggerFactory.getLogger(WebSocketEventListener::class.java)

    @EventListener
    fun handleWebSocketConnectListener(event: SessionConnectedEvent) {
        logger.info("新的 WebSocket 连接建立: ${event.message}")
        // 可以在这里记录连接统计信息
    }

    @EventListener
    fun handleWebSocketDisconnectListener(event: SessionDisconnectEvent) {
        val headerAccessor = StompHeaderAccessor.wrap(event.message)
        val username = headerAccessor.sessionAttributes?.get("username") as? String
        
        if (username != null) {
            logger.info("用户 $username 断开连接")
            
            // 通知其他用户该用户已离线
            val chatMessage = ChatMessage(
                type = MessageType.LEAVE,
                sender = username,
                content = "$username 离开了聊天室"
            )
            
            messagingTemplate.convertAndSend("/topic/public", chatMessage)
        }
    }
}

安全性考虑 🔒

认证与授权

kotlin
@Configuration
@EnableWebSocketSecurity
class WebSocketSecurityConfig {

    @Bean
    fun authorizationManager(): AuthorizationManager<Message<*>> {
        val messages = AuthorizationManagerMessageMatcherRegistry()
        
        // 只有认证用户才能连接
        messages.simpConnect().authenticated() 
        
        // 管理员才能访问管理频道
        messages.simpDestMatchers("/app/admin/**").hasRole("ADMIN") 
        
        // 用户只能发送到自己的私有队列
        messages.simpDestMatchers("/app/private/**").access { authentication, message ->
            // 自定义权限检查逻辑
            val principal = authentication.get().principal as UserPrincipal
            val destination = message.get().headers["simpDestination"] as String
            
            // 检查用户是否有权限访问该目的地
            checkUserPermission(principal, destination)
        }
        
        return messages.build()
    }
}

故障排查与调试 🔧

常见问题及解决方案

WARNING

连接失败常见原因

  1. CORS 配置问题
  2. 端点路径不匹配
  3. 安全配置过于严格
  4. 消息代理未正确启动
kotlin
@Configuration
class WebSocketDebugConfig : WebSocketMessageBrokerConfigurer {

    override fun configureClientInboundChannel(registration: ChannelRegistration) {
        registration.interceptors(object : ChannelInterceptor {
            override fun preSend(message: Message<*>, channel: MessageChannel): Message<*>? {
                val accessor = StompHeaderAccessor.wrap(message)
                logger.debug("接收到消息: ${accessor.command} -> ${accessor.destination}") 
                return message
            }
        })
    }

    override fun configureClientOutboundChannel(registration: ChannelRegistration) {
        registration.interceptors(object : ChannelInterceptor {
            override fun preSend(message: Message<*>, channel: MessageChannel): Message<*>? {
                val accessor = StompHeaderAccessor.wrap(message)
                logger.debug("发送消息: ${accessor.command} -> ${accessor.destination}") 
                return message
            }
        })
    }
}

总结 🎯

STOMP 为 WebSocket 提供了标准化的消息传输协议,让实时通信变得:

  • 简单:统一的消息格式,无需自定义协议
  • 可靠:内置心跳检测和错误处理机制
  • 灵活:支持发布-订阅和点对点两种模式
  • 安全:完整的认证授权体系
  • 可扩展:支持外部消息代理,适合集群部署

TIP

选择建议

  • 开发阶段:使用简单代理快速原型开发
  • 生产环境:使用 RabbitMQ 或 Redis 等外部代理
  • 高并发场景:考虑消息分片和负载均衡策略

通过 STOMP,我们可以轻松构建聊天室、实时通知、协作编辑、在线游戏等各种实时应用。它不仅解决了 WebSocket 原始协议的复杂性,更为我们提供了企业级的实时通信解决方案! 🚀