Appearance
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
连接失败常见原因:
- CORS 配置问题
- 端点路径不匹配
- 安全配置过于严格
- 消息代理未正确启动
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 原始协议的复杂性,更为我们提供了企业级的实时通信解决方案! 🚀