Skip to content

Spring WebSocket API 深度解析 🚀

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

在传统的 HTTP 通信中,客户端和服务器之间的交互遵循"请求-响应"模式:客户端发送请求,服务器返回响应,然后连接关闭。这种模式对于大多数 Web 应用来说是足够的,但在某些场景下就显得力不从心了。

NOTE

想象一下聊天应用、实时游戏、股票价格监控等场景,如果还用传统的 HTTP 轮询方式,客户端需要不断地向服务器发送请求询问"有新消息吗?",这不仅浪费带宽,还增加了服务器负担。

WebSocket 的核心价值在于建立一个持久的、全双工的通信通道,让客户端和服务器可以随时互相发送消息,就像打电话一样实时对话。

Spring WebSocket 核心组件解析

1. WebSocketHandler - 消息处理的核心

WebSocketHandler 是 Spring WebSocket 的核心接口,它定义了处理 WebSocket 连接生命周期的方法。Spring 提供了两个便利的实现类:

  • TextWebSocketHandler:专门处理文本消息
  • BinaryWebSocketHandler:专门处理二进制消息
kotlin
@Component
class ChatWebSocketHandler : TextWebSocketHandler() {
    
    private val sessions = mutableSetOf<WebSocketSession>() 
    
    override fun afterConnectionEstablished(session: WebSocketSession) {
        sessions.add(session) 
        println("新用户连接: ${session.id}")
        
        // 向新用户发送欢迎消息
        session.sendMessage(TextMessage("欢迎加入聊天室!"))
    }
    
    override fun handleTextMessage(session: WebSocketSession, message: TextMessage) {
        val payload = message.payload
        println("收到消息: $payload")
        
        // 广播消息给所有连接的用户
        sessions.forEach { s ->
            if (s.isOpen) {
                s.sendMessage(TextMessage("用户${session.id}: $payload"))
            }
        }
    }
    
    override fun afterConnectionClosed(session: WebSocketSession, status: CloseStatus) {
        sessions.remove(session) 
        println("用户断开连接: ${session.id}")
    }
    
    override fun handleTransportError(session: WebSocketSession, exception: Throwable) {
        println("传输错误: ${exception.message}") 
        sessions.remove(session)
    }
}
kotlin
// 如果没有 WebSocket,实现实时聊天需要这样做:
@RestController
class TraditionalChatController {
    
    @GetMapping("/messages")
    fun getMessages(): List<Message> {
        // 客户端需要不断轮询这个接口
        return messageService.getLatestMessages() 
    }
    
    @PostMapping("/messages")
    fun sendMessage(@RequestBody message: Message) {
        messageService.saveMessage(message)
        // 其他用户无法立即知道有新消息
    }
}

TIP

注意代码中的 sessions 集合,这是实现群聊功能的关键。每当有新消息时,我们遍历所有活跃的连接并广播消息。

2. WebSocket 配置 - 让一切运转起来

Spring 提供了简洁的配置方式来注册 WebSocket 处理器:

kotlin
@Configuration
@EnableWebSocket
class WebSocketConfig : WebSocketConfigurer {
    
    @Autowired
    private lateinit var chatHandler: ChatWebSocketHandler
    
    override fun registerWebSocketHandlers(registry: WebSocketHandlerRegistry) {
        registry.addHandler(chatHandler, "/chat") 
            .setAllowedOrigins("*") // 生产环境中应该限制具体域名
    }
}

IMPORTANT

@EnableWebSocket 注解是启用 WebSocket 功能的关键,它会自动配置必要的基础设施。

WebSocket 握手机制深度解析

WebSocket 连接的建立需要经过一个特殊的 HTTP 握手过程:

握手拦截器 - 增强握手过程

HandshakeInterceptor 允许我们在握手前后执行自定义逻辑:

kotlin
@Component
class AuthHandshakeInterceptor : HandshakeInterceptor {
    
    override fun beforeHandshake(
        request: ServerHttpRequest,
        response: ServerHttpResponse,
        wsHandler: WebSocketHandler,
        attributes: MutableMap<String, Any>
    ): Boolean {
        // 从请求中获取用户信息
        val token = request.headers.getFirst("Authorization")
        
        return if (isValidToken(token)) {
            // 将用户信息存储到 WebSocket 会话属性中
            attributes["userId"] = extractUserIdFromToken(token)
            attributes["username"] = extractUsernameFromToken(token)
            true // 允许握手继续
        } else {
            false // 拒绝握手
        }
    }
    
    override fun afterHandshake(
        request: ServerHttpRequest,
        response: ServerHttpResponse,
        wsHandler: WebSocketHandler,
        exception: Exception?
    ) {
        if (exception != null) {
            println("握手失败: ${exception.message}") 
        } else {
            println("握手成功完成") 
        }
    }
    
    private fun isValidToken(token: String?): Boolean {
        // 实现 token 验证逻辑
        return token?.startsWith("Bearer ") == true
    }
    
    private fun extractUserIdFromToken(token: String): String {
        // 从 JWT token 中提取用户 ID
        return "user123" // 简化示例
    }
    
    private fun extractUsernameFromToken(token: String): String {
        return "张三" // 简化示例
    }
}

然后在配置中使用这个拦截器:

kotlin
@Configuration
@EnableWebSocket
class WebSocketConfig : WebSocketConfigurer {
    
    @Autowired
    private lateinit var chatHandler: ChatWebSocketHandler
    
    @Autowired
    private lateinit var authInterceptor: AuthHandshakeInterceptor
    
    override fun registerWebSocketHandlers(registry: WebSocketHandlerRegistry) {
        registry.addHandler(chatHandler, "/chat")
            .addInterceptors(authInterceptor) 
            .setAllowedOrigins("https://mydomain.com")
    }
}

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

让我们构建一个完整的实时通知系统,展示 WebSocket 在实际业务中的应用:

完整的实时通知系统实现
kotlin
// 通知数据类
data class Notification(
    val id: String,
    val userId: String,
    val type: NotificationType,
    val title: String,
    val content: String,
    val timestamp: LocalDateTime = LocalDateTime.now()
)

enum class NotificationType {
    SYSTEM, ORDER, MESSAGE, PROMOTION
}

// WebSocket 处理器
@Component
class NotificationWebSocketHandler : TextWebSocketHandler() {
    
    // 用户ID到WebSocket会话的映射
    private val userSessions = ConcurrentHashMap<String, WebSocketSession>()
    
    override fun afterConnectionEstablished(session: WebSocketSession) {
        val userId = session.attributes["userId"] as? String
        if (userId != null) {
            userSessions[userId] = session 
            println("用户 $userId 连接到通知系统")
            
            // 发送连接成功消息
            sendNotificationToUser(userId, Notification(
                id = UUID.randomUUID().toString(),
                userId = userId,
                type = NotificationType.SYSTEM,
                title = "连接成功",
                content = "您已成功连接到实时通知系统"
            ))
        }
    }
    
    override fun handleTextMessage(session: WebSocketSession, message: TextMessage) {
        // 客户端可以发送心跳包或其他控制消息
        val payload = message.payload
        when (payload) {
            "ping" -> session.sendMessage(TextMessage("pong"))
            "getOnlineUsers" -> {
                val onlineCount = userSessions.size
                session.sendMessage(TextMessage("当前在线用户数: $onlineCount"))
            }
        }
    }
    
    override fun afterConnectionClosed(session: WebSocketSession, status: CloseStatus) {
        val userId = session.attributes["userId"] as? String
        if (userId != null) {
            userSessions.remove(userId) 
            println("用户 $userId 断开连接")
        }
    }
    
    // 向特定用户发送通知
    fun sendNotificationToUser(userId: String, notification: Notification) {
        val session = userSessions[userId]
        if (session?.isOpen == true) {
            try {
                val json = ObjectMapper().writeValueAsString(notification)
                session.sendMessage(TextMessage(json))
            } catch (e: Exception) {
                println("发送通知失败: ${e.message}") 
            }
        }
    }
    
    // 广播通知给所有在线用户
    fun broadcastNotification(notification: Notification) {
        userSessions.values.forEach { session ->
            if (session.isOpen) {
                try {
                    val json = ObjectMapper().writeValueAsString(notification)
                    session.sendMessage(TextMessage(json))
                } catch (e: Exception) {
                    println("广播通知失败: ${e.message}") 
                }
            }
        }
    }
}

// 通知服务
@Service
class NotificationService {
    
    @Autowired
    private lateinit var webSocketHandler: NotificationWebSocketHandler
    
    // 发送订单状态更新通知
    fun sendOrderStatusNotification(userId: String, orderId: String, status: String) {
        val notification = Notification(
            id = UUID.randomUUID().toString(),
            userId = userId,
            type = NotificationType.ORDER,
            title = "订单状态更新",
            content = "您的订单 $orderId 状态已更新为: $status"
        )
        
        webSocketHandler.sendNotificationToUser(userId, notification) 
    }
    
    // 发送系统维护通知
    fun sendMaintenanceNotification() {
        val notification = Notification(
            id = UUID.randomUUID().toString(),
            userId = "ALL", // 表示所有用户
            type = NotificationType.SYSTEM,
            title = "系统维护通知",
            content = "系统将于今晚 23:00-24:00 进行维护,请提前保存您的工作"
        )
        
        webSocketHandler.broadcastNotification(notification) 
    }
}

// REST 控制器 - 用于触发通知
@RestController
@RequestMapping("/api/notifications")
class NotificationController {
    
    @Autowired
    private lateinit var notificationService: NotificationService
    
    @PostMapping("/order-status")
    fun updateOrderStatus(@RequestBody request: OrderStatusRequest) {
        // 处理订单状态更新逻辑...
        
        // 发送实时通知
        notificationService.sendOrderStatusNotification(
            request.userId, 
            request.orderId, 
            request.status
        )
        
        return ResponseEntity.ok("通知已发送")
    }
    
    @PostMapping("/maintenance")
    fun sendMaintenanceNotification() {
        notificationService.sendMaintenanceNotification()
        return ResponseEntity.ok("维护通知已广播")
    }
}

data class OrderStatusRequest(
    val userId: String,
    val orderId: String,
    val status: String
)

服务器配置优化

在生产环境中,我们需要对 WebSocket 服务器进行适当的配置以确保性能和稳定性:

kotlin
@Configuration
class WebSocketServerConfig {
    
    @Bean
    fun createWebSocketContainer(): ServletServerContainerFactoryBean {
        return ServletServerContainerFactoryBean().apply {
            maxTextMessageBufferSize = 8192 // 文本消息缓冲区大小
            maxBinaryMessageBufferSize = 8192 // 二进制消息缓冲区大小
            maxSessionIdleTimeout = 600000L // 会话空闲超时时间(10分钟)
        }
    }
}

WARNING

在高并发场景下,需要特别注意消息发送的线程安全性。JSR-356 标准不允许并发发送消息,Spring 提供了 ConcurrentWebSocketSessionDecorator 来解决这个问题。

kotlin
// 线程安全的会话装饰器
class SafeWebSocketHandler : TextWebSocketHandler() {
    
    override fun afterConnectionEstablished(session: WebSocketSession) {
        // 使用装饰器确保线程安全
        val safeSession = ConcurrentWebSocketSessionDecorator(
            session, 
            5000, // 发送超时时间
            1024 * 1024 // 缓冲区大小
        )
        
        // 存储安全的会话对象
        sessions[session.id] = safeSession
    }
}

跨域配置与安全考虑

WebSocket 的跨域配置对于现代 Web 应用至关重要:

kotlin
@Configuration
@EnableWebSocket
class WebSocketConfig : WebSocketConfigurer {
    
    override fun registerWebSocketHandlers(registry: WebSocketHandlerRegistry) {
        registry.addHandler(chatHandler, "/chat")
            .setAllowedOrigins( 
                "https://mydomain.com",
                "https://app.mydomain.com"
            )
            // 或者在开发环境中允许所有来源(不推荐用于生产环境)
            // .setAllowedOrigins("*")
    }
}

CAUTION

在生产环境中,绝不要使用 setAllowedOrigins("*"),这会带来严重的安全风险。始终明确指定允许的域名。

客户端连接示例

为了完整展示 WebSocket 的使用,这里提供一个简单的 JavaScript 客户端示例:

javascript
// 建立 WebSocket 连接
const socket = new WebSocket('ws://localhost:8080/chat');

// 连接建立时
socket.onopen = function(event) {
    console.log('WebSocket 连接已建立');
    // 发送认证信息或初始化消息
    socket.send('ping');
};

// 接收消息时
socket.onmessage = function(event) {
    const notification = JSON.parse(event.data);
    console.log('收到通知:', notification);
    
    // 在页面上显示通知
    showNotification(notification);
};

// 连接关闭时
socket.onclose = function(event) {
    console.log('WebSocket 连接已关闭');
    // 可以实现重连逻辑
    setTimeout(() => {
        console.log('尝试重新连接...');
        // 重新建立连接
    }, 5000);
};

// 连接错误时
socket.onerror = function(error) {
    console.error('WebSocket 错误:', error);
};

function showNotification(notification) {
    // 创建通知 UI 元素
    const notificationElement = document.createElement('div');
    notificationElement.className = 'notification';
    notificationElement.innerHTML = `
        <h4>${notification.title}</h4>
        <p>${notification.content}</p>
        <small>${notification.timestamp}</small>
    `;
    
    document.getElementById('notifications').appendChild(notificationElement);
}

总结与最佳实践 ✅

Spring WebSocket API 为我们提供了构建实时 Web 应用的强大工具。通过本文的学习,你应该掌握了:

  1. WebSocket 的核心价值:解决实时通信需求,避免低效的轮询
  2. WebSocketHandler 的使用:处理连接生命周期和消息
  3. 握手机制和拦截器:实现认证和会话管理
  4. 实际业务应用:构建通知系统等实时功能

最佳实践建议

  • 始终处理连接异常和重连逻辑
  • 在生产环境中限制跨域来源
  • 使用心跳机制检测连接状态
  • 考虑使用消息队列处理高并发场景
  • 实现适当的认证和授权机制

WebSocket 技术让我们的 Web 应用变得更加生动和实时,是现代 Web 开发不可或缺的技术之一。掌握了 Spring WebSocket API,你就能为用户提供更好的实时交互体验! 🎉