WebSocket:从零开始到实战项目

一、WebSocket 概述与 Spring Boot 集成方式

1.1 WebSocket 核心优势

WebSocket 是一种全双工通信协议,在单个 TCP 连接上实现客户端与服务器的双向实时通信。相比传统 HTTP 轮询,它具有以下显著优势:

  • 低延迟:连接建立后,数据可即时传输,无需等待 HTTP 请求响应
  • 低开销:头部信息极小(仅 2-10 字节),远小于 HTTP 请求的几十到上百字节
  • 全双工:客户端和服务器可同时发送数据
  • 持久连接:一次握手,长期保持连接状态

Websocket官网

1.2 Spring Boot 中 WebSocket 的两种主流实现

Spring Boot 提供了两种成熟的 WebSocket 实现方案,适用于不同的业务场景:

特性维度 原生注解模式 (JSR-356) Spring STOMP 模式
协议支持 原生 WebSocket 协议 STOMP (Simple Text Oriented Messaging Protocol)
依赖注入 较麻烦,需手动处理 完美支持,可直接使用 @Autowired
消息路由 需手动管理 Session 集合 内置消息代理,支持 /topic/queue 路由
适用场景 简单实时通讯、低并发、轻量级需求 复杂业务交互、权限控制、消息分组、高并发
前端对接 标准 WebSocket API 通常配合 SockJS 和 Stomp.js 使用
性能表现 连接延迟 120ms,10K 消息吞吐 980ms 连接延迟 150ms,10K 消息吞吐 1250ms
内存占用 约 18MB 约 24MB

二、原生 WebSocket 实现

2.1 基础配置与依赖

  1. 添加 Maven 依赖
xml 复制代码
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
  1. 启用 WebSocket 支持
java 复制代码
@Configuration
@EnableWebSocket
// 放在Config文件夹下
public class NativeWebSocketConfiguration {
    /**
     * 注册 ServerEndpointExporter Bean
     * 该 Bean 会自动注册使用 @ServerEndpoint 注解声明的 WebSocket 端点
     */
    @Bean
    public ServerEndpointExporter serverEndpointExporter() {
        return new ServerEndpointExporter();
    }
}

2.2 编写 WebSocket 端点

java 复制代码
@ServerEndpoint("/native/chat/{roomId}/{userId}")
@Component
@Slf4j
// 在线聊天室案例
public class NativeChatEndpoint {
    // 线程安全的 Session 集合,用于存储所有活跃连接
    private static final ConcurrentHashMap<String, ConcurrentHashMap<String, Session>> roomSessions = new ConcurrentHashMap<>();

    /**
     * 连接建立成功时调用
     */
    @OnOpen
    public void onOpen(Session session, 
                      @PathParam("roomId") String roomId, 
                      @PathParam("userId") String userId) {
        // 将 Session 加入对应房间
        roomSessions.computeIfAbsent(roomId, k -> new ConcurrentHashMap<>())
                    .put(userId, session);
        
        log.info("用户 {} 加入房间 {},当前房间人数: {}", userId, roomId, 
                roomSessions.get(roomId).size());
        
        // 广播用户加入消息
        broadcast(roomId, "系统消息: 用户 " + userId + " 加入了聊天室");
    }

    /**
     * 收到客户端消息时调用
     */
    @OnMessage
    public void onMessage(String message, Session session, 
                         @PathParam("roomId") String roomId, 
                         @PathParam("userId") String userId) {
        log.info("收到用户 {} 在房间 {} 的消息: {}", userId, roomId, message);
        
        // 广播消息给房间内所有用户
        broadcast(roomId, userId + ": " + message);
    }

    /**
     * 连接关闭时调用
     */
    @OnClose
    public void onClose(Session session, 
                       @PathParam("roomId") String roomId, 
                       @PathParam("userId") String userId) {
        // 从房间中移除用户
        if (roomSessions.containsKey(roomId)) {
            roomSessions.get(roomId).remove(userId);
            if (roomSessions.get(roomId).isEmpty()) {
                roomSessions.remove(roomId);
            }
        }
        
        log.info("用户 {} 离开房间 {},当前房间人数: {}", userId, roomId, 
                roomSessions.getOrDefault(roomId, new ConcurrentHashMap<>()).size());
        
        // 广播用户离开消息
        broadcast(roomId, "系统消息: 用户 " + userId + " 离开了聊天室");
    }

    /**
     * 发生错误时调用
     */
    @OnError
    public void onError(Session session, Throwable error) {
        log.error("WebSocket 发生错误", error);
    }

    /**
     * 广播消息给指定房间的所有用户
     */
    private void broadcast(String roomId, String message) {
        if (!roomSessions.containsKey(roomId)) {
            return;
        }
        
        for (Session session : roomSessions.get(roomId).values()) {
            try {
                if (session.isOpen()) {
                    session.getBasicRemote().sendText(message);
                }
            } catch (IOException e) {
                log.error("发送消息失败", e);
            }
        }
    }

    /**
     * 发送点对点消息
     */
    public void sendPrivateMessage(String roomId, String targetUserId, String message) {
        if (!roomSessions.containsKey(roomId) || !roomSessions.get(roomId).containsKey(targetUserId)) {
            log.warn("用户 {} 不在房间 {} 中", targetUserId, roomId);
            return;
        }
        
        Session session = roomSessions.get(roomId).get(targetUserId);
        try {
            if (session.isOpen()) {
                session.getBasicRemote().sendText(message);
            }
        } catch (IOException e) {
            log.error("发送点对点消息失败", e);
        }
    }
}

2.3 原生 WebSocket 的依赖注入问题

由于 @ServerEndpoint 注解的类由 WebSocket 容器管理,而非 Spring 容器,直接使用 @Autowired 会导致注入失败。解决方案:

java 复制代码
@ServerEndpoint(value = "/native/chat/{roomId}/{userId}", configurator = SpringContextConfigurator.class)
@Component
@Slf4j
public class NativeChatEndpoint {
    // 现在可以正常注入 Spring Bean
    @Autowired
    private UserService userService;
    
    // ... 其他代码不变
}

/**
 * 自定义配置器,用于从 Spring 上下文中获取 Bean
 */
public class SpringContextConfigurator extends ServerEndpointConfig.Configurator {
    private static ApplicationContext applicationContext;

    public static void setApplicationContext(ApplicationContext context) {
        SpringContextConfigurator.applicationContext = context;
    }

    @Override
    public <T> T getEndpointInstance(Class<T> endpointClass) throws InstantiationException {
        return applicationContext.getBean(endpointClass);
    }
}

// 在启动类中设置 ApplicationContext
@SpringBootApplication
public class WebSocketApplication {
    public static void main(String[] args) {
        ApplicationContext context = SpringApplication.run(WebSocketApplication.class, args);
        SpringContextConfigurator.setApplicationContext(context);
    }
}

三、STOMP 协议实现(企业首选)

STOMP 是一种基于文本的消息传输协议,它在 WebSocket 之上提供了更高级的消息模式,包括发布-订阅和点对点通信。这是企业开发中最常用的 WebSocket 实现方式。

3.1 基础配置

java 复制代码
@Configuration
@EnableWebSocketMessageBroker
@Slf4j
public class StompWebSocketConfig implements WebSocketMessageBrokerConfigurer {

    /**
     * 注册 STOMP 端点
     * 客户端通过这些端点建立 WebSocket 连接
     */
    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/ws")
                // 允许跨域(生产环境应配置具体域名)
                .setAllowedOriginPatterns("*")
                // 启用 SockJS 回退选项,为不支持 WebSocket 的浏览器提供兼容
                .withSockJS();
    }

    /**
     * 配置消息代理
     */
    @Override
    public void configureMessageBroker(MessageBrokerRegistry config) {
        // 启用简单内存消息代理,用于处理订阅和广播消息
        // "/topic" 前缀用于广播消息,"/queue" 前缀用于点对点消息
        config.enableSimpleBroker("/topic", "/queue");
        
        // 设置应用程序目的地前缀,客户端发送到这些前缀的消息会被路由到 @MessageMapping 方法
        config.setApplicationDestinationPrefixes("/app");
        
        // 设置用户目的地前缀,用于点对点消息
        config.setUserDestinationPrefix("/user");
    }

    /**
     * 配置客户端入站通道
     */
    @Override
    public void configureClientInboundChannel(ChannelRegistration registration) {
        // 设置线程池大小
        registration.taskExecutor()
                   .corePoolSize(10)
                   .maxPoolSize(50)
                   .queueCapacity(1000);
    }

    /**
     * 配置客户端出站通道
     */
    @Override
    public void configureClientOutboundChannel(ChannelRegistration registration) {
        registration.taskExecutor()
                   .corePoolSize(10)
                   .maxPoolSize(50)
                   .queueCapacity(1000);
    }
}

3.2 消息控制器

java 复制代码
@Controller
@Slf4j
public class StompChatController {

    @Autowired
    private SimpMessagingTemplate messagingTemplate;

    /**
     * 处理客户端发送到 /app/chat 的消息
     * 并将结果广播到 /topic/chat 主题
     */
    @MessageMapping("/chat")
    @SendTo("/topic/chat")
    public ChatMessage handleChatMessage(ChatMessage message, 
                                        SimpMessageHeaderAccessor accessor) {
        // 获取当前用户信息
        String username = accessor.getUser().getName();
        log.info("收到用户 {} 的消息: {}", username, message.getContent());
        
        // 设置消息发送者和时间
        message.setSender(username);
        message.setTimestamp(LocalDateTime.now());
        
        return message;
    }

    /**
     * 处理点对点消息
     * 客户端发送到 /app/private/{targetUserId}
     */
    @MessageMapping("/private/{targetUserId}")
    public void handlePrivateMessage(@DestinationVariable String targetUserId,
                                    @Payload ChatMessage message,
                                    SimpMessageHeaderAccessor accessor) {
        String senderId = accessor.getUser().getName();
        log.info("用户 {} 向用户 {} 发送私信: {}", senderId, targetUserId, message.getContent());
        
        message.setSender(senderId);
        message.setTimestamp(LocalDateTime.now());
        
        // 发送点对点消息给目标用户
        messagingTemplate.convertAndSendToUser(
            targetUserId,
            "/queue/private",
            message
        );
    }

    /**
     * 处理加入房间请求
     */
    @MessageMapping("/room/{roomId}/join")
    @SendTo("/topic/room/{roomId}")
    public ChatMessage joinRoom(@DestinationVariable String roomId,
                               SimpMessageHeaderAccessor accessor) {
        String username = accessor.getUser().getName();
        log.info("用户 {} 加入房间 {}", username, roomId);
        
        ChatMessage message = new ChatMessage();
        message.setType(ChatMessage.MessageType.JOIN);
        message.setSender(username);
        message.setContent("用户 " + username + " 加入了房间");
        message.setTimestamp(LocalDateTime.now());
        
        return message;
    }

    /**
     * 处理离开房间请求
     */
    @MessageMapping("/room/{roomId}/leave")
    @SendTo("/topic/room/{roomId}")
    public ChatMessage leaveRoom(@DestinationVariable String roomId,
                                SimpMessageHeaderAccessor accessor) {
        String username = accessor.getUser().getName();
        log.info("用户 {} 离开房间 {}", username, roomId);
        
        ChatMessage message = new ChatMessage();
        message.setType(ChatMessage.MessageType.LEAVE);
        message.setSender(username);
        message.setContent("用户 " + username + " 离开了房间");
        message.setTimestamp(LocalDateTime.now());
        
        return message;
    }
}

3.3 消息实体类

java 复制代码
@Data
@NoArgsConstructor
@AllArgsConstructor
public class ChatMessage {
    private MessageType type;
    private String content;
    private String sender;
    private LocalDateTime timestamp;

    public enum MessageType {
        CHAT, JOIN, LEAVE, SYSTEM
    }
}

3.4 前端客户端代码

javascript 复制代码
// 连接 WebSocket
function connect() {
    // 创建 SockJS 连接
    const socket = new SockJS('/ws');
    // 创建 STOMP 客户端
    stompClient = Stomp.over(socket);
    
    // 连接服务器
    stompClient.connect({}, function(frame) {
        console.log('连接成功: ' + frame);
        
        // 订阅公共聊天主题
        stompClient.subscribe('/topic/chat', function(message) {
            showMessage(JSON.parse(message.body));
        });
        
        // 订阅私人消息队列
        stompClient.subscribe('/user/queue/private', function(message) {
            showPrivateMessage(JSON.parse(message.body));
        });
        
        // 订阅房间消息
        stompClient.subscribe('/topic/room/1', function(message) {
            showRoomMessage(JSON.parse(message.body));
        });
    }, function(error) {
        console.error('连接失败: ' + error);
        // 自动重连
        setTimeout(connect, 5000);
    });
}

// 发送公共消息
function sendMessage() {
    const content = document.getElementById('messageInput').value;
    if (content && stompClient) {
        stompClient.send('/app/chat', {}, JSON.stringify({
            content: content
        }));
        document.getElementById('messageInput').value = '';
    }
}

// 发送私人消息
function sendPrivateMessage(targetUserId) {
    const content = document.getElementById('privateMessageInput').value;
    if (content && stompClient) {
        stompClient.send('/app/private/' + targetUserId, {}, JSON.stringify({
            content: content
        }));
        document.getElementById('privateMessageInput').value = '';
    }
}

// 显示消息
function showMessage(message) {
    const messageElement = document.createElement('div');
    messageElement.className = 'message';
    messageElement.innerHTML = `<strong>${message.sender}</strong> [${formatTime(message.timestamp)}]: ${message.content}`;
    document.getElementById('messages').appendChild(messageElement);
}

四、高级特性

4.1 用户认证与权限控制

WebSocket 认证通常在握手阶段进行,确保只有合法用户才能建立连接。

4.1.1 基于 Token 的握手拦截器
java 复制代码
@Component
@Slf4j
public class JwtHandshakeInterceptor implements HandshakeInterceptor {

    @Autowired
    private JwtTokenProvider jwtTokenProvider;

    @Override
    public boolean beforeHandshake(ServerHttpRequest request, 
                                  ServerHttpResponse response, 
                                  WebSocketHandler wsHandler, 
                                  Map<String, Object> attributes) throws Exception {
        // 从请求参数或请求头中获取 Token
        String token = getTokenFromRequest(request);
        
        if (token == null || !jwtTokenProvider.validateToken(token)) {
            log.warn("无效的 Token,拒绝 WebSocket 连接");
            response.setStatusCode(HttpStatus.UNAUTHORIZED);
            return false;
        }
        
        // 从 Token 中解析用户信息
        String username = jwtTokenProvider.getUsernameFromToken(token);
        UserDetails userDetails = jwtTokenProvider.getUserDetails(username);
        
        // 将用户信息存入 attributes,供后续使用
        attributes.put("username", username);
        attributes.put("authorities", userDetails.getAuthorities());
        
        log.info("用户 {} 认证成功,允许建立 WebSocket 连接", username);
        return true;
    }

    @Override
    public void afterHandshake(ServerHttpRequest request, 
                              ServerHttpResponse response, 
                              WebSocketHandler wsHandler, 
                              Exception exception) {
        // 握手后的处理逻辑
    }

    private String getTokenFromRequest(ServerHttpRequest request) {
        // 从请求参数中获取 Token
        String token = request.getURI().getQuery();
        if (token != null && token.startsWith("token=")) {
            return token.substring(6);
        }
        
        // 从请求头中获取 Token
        List<String> authHeaders = request.getHeaders().get("Authorization");
        if (authHeaders != null && !authHeaders.isEmpty()) {
            String bearerToken = authHeaders.get(0);
            if (bearerToken.startsWith("Bearer ")) {
                return bearerToken.substring(7);
            }
        }
        
        return null;
    }
}
4.1.2 配置认证拦截器
java 复制代码
@Configuration
@EnableWebSocketMessageBroker
public class StompWebSocketConfig implements WebSocketMessageBrokerConfigurer {

    @Autowired
    private JwtHandshakeInterceptor jwtHandshakeInterceptor;

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/ws")
                .setAllowedOriginPatterns("*")
                // 添加认证拦截器
                .addInterceptors(jwtHandshakeInterceptor)
                .withSockJS();
    }

    // ... 其他配置不变
}
4.1.3 消息级权限控制
java 复制代码
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class MethodSecurityConfig extends GlobalMethodSecurityConfiguration {
}

@Controller
public class SecureChatController {

    /**
     * 只有具有 ROLE_ADMIN 权限的用户才能发送系统公告
     */
    @MessageMapping("/system/announcement")
    @SendTo("/topic/system")
    @PreAuthorize("hasRole('ADMIN')")
    public SystemMessage sendAnnouncement(SystemMessage message) {
        message.setTimestamp(LocalDateTime.now());
        return message;
    }

    /**
     * 只能向自己发送消息(防止冒充他人)
     */
    @MessageMapping("/user/{userId}/message")
    public void sendUserMessage(@DestinationVariable String userId,
                               @Payload UserMessage message,
                               Principal principal) {
        if (!userId.equals(principal.getName())) {
            throw new AccessDeniedException("无权向其他用户发送消息");
        }
        
        // 处理消息
    }
}

4.2 异常处理

java 复制代码
@ControllerAdvice
@Slf4j
public class WebSocketExceptionHandler {

    /**
     * 处理所有 WebSocket 消息异常
     * 将异常信息发送给用户的错误队列
     */
    @MessageExceptionHandler(Exception.class)
    @SendToUser("/queue/errors")
    public ErrorResponse handleException(Exception ex) {
        log.error("WebSocket 消息处理异常", ex);
        
        ErrorResponse errorResponse = new ErrorResponse();
        errorResponse.setTimestamp(LocalDateTime.now());
        
        if (ex instanceof AccessDeniedException) {
            errorResponse.setCode("ACCESS_DENIED");
            errorResponse.setMessage("权限不足,无法执行此操作");
        } else if (ex instanceof IllegalArgumentException) {
            errorResponse.setCode("INVALID_ARGUMENT");
            errorResponse.setMessage("参数无效: " + ex.getMessage());
        } else {
            errorResponse.setCode("INTERNAL_ERROR");
            errorResponse.setMessage("服务器内部错误");
        }
        
        return errorResponse;
    }
}

@Data
public class ErrorResponse {
    private String code;
    private String message;
    private LocalDateTime timestamp;
}

4.3 心跳检测与连接管理

java 复制代码
@Configuration
@EnableWebSocketMessageBroker
public class StompWebSocketConfig implements WebSocketMessageBrokerConfigurer {

    @Override
    public void configureWebSocketTransport(WebSocketTransportRegistration registration) {
        // 设置消息大小限制
        registration.setMessageSizeLimit(1024 * 1024); // 1MB
        
        // 设置发送超时时间
        registration.setSendTimeLimit(10 * 1000); // 10秒
        
        // 设置发送缓冲区大小
        registration.setSendBufferSizeLimit(512 * 1024); // 512KB
    }

    @Bean
    public ServletServerContainerFactoryBean createWebSocketContainer() {
        ServletServerContainerFactoryBean container = new ServletServerContainerFactoryBean();
        
        // 设置最大文本消息大小
        container.setMaxTextMessageBufferSize(1024 * 1024);
        
        // 设置最大二进制消息大小
        container.setMaxBinaryMessageBufferSize(1024 * 1024);
        
        // 设置空闲超时时间(毫秒)
        container.setMaxSessionIdleTimeout(30 * 60 * 1000); // 30分钟
        
        // 设置异步发送超时时间
        container.setAsyncSendTimeout(10 * 1000); // 10秒
        
        return container;
    }
}

4.4 事件监听

Spring WebSocket 提供了丰富的事件监听机制,可以监控连接的生命周期和消息处理过程。

java 复制代码
@Component
@Slf4j
public class WebSocketEventListener {

    @Autowired
    private SimpMessageSendingOperations messagingTemplate;

    /**
     * 监听连接建立事件
     */
    @EventListener
    public void handleSessionConnected(SessionConnectedEvent event) {
        String username = event.getUser().getName();
        String sessionId = SimpMessageHeaderAccessor.getSessionId(event.getMessage().getHeaders());
        
        log.info("用户 {} 建立 WebSocket 连接,Session ID: {}", username, sessionId);
        
        // 发送欢迎消息
        ChatMessage welcomeMessage = new ChatMessage();
        welcomeMessage.setType(ChatMessage.MessageType.SYSTEM);
        welcomeMessage.setContent("欢迎 " + username + " 加入聊天室!");
        welcomeMessage.setTimestamp(LocalDateTime.now());
        
        messagingTemplate.convertAndSendToUser(username, "/queue/notifications", welcomeMessage);
    }

    /**
     * 监听连接断开事件
     */
    @EventListener
    public void handleSessionDisconnect(SessionDisconnectEvent event) {
        String username = event.getUser() != null ? event.getUser().getName() : "未知用户";
        String sessionId = event.getSessionId();
        
        log.info("用户 {} 断开 WebSocket 连接,Session ID: {}", username, sessionId);
    }

    /**
     * 监听订阅事件
     */
    @EventListener
    public void handleSessionSubscribe(SessionSubscribeEvent event) {
        String username = event.getUser().getName();
        String destination = SimpMessageHeaderAccessor.getDestination(event.getMessage().getHeaders());
        
        log.info("用户 {} 订阅了目的地: {}", username, destination);
    }

    /**
     * 监听取消订阅事件
     */
    @EventListener
    public void handleSessionUnsubscribe(SessionUnsubscribeEvent event) {
        String username = event.getUser().getName();
        String destination = SimpMessageHeaderAccessor.getDestination(event.getMessage().getHeaders());
        
        log.info("用户 {} 取消订阅了目的地: {}", username, destination);
    }
}

五、实际使用场景分析

5.1 实时聊天室

这是 WebSocket 最经典的应用场景,支持多人实时聊天、私聊、房间管理等功能。

核心实现要点:

  • 使用 /topic/room/{roomId} 实现房间内广播
  • 使用 /user/queue/private 实现点对点私聊
  • 结合事件监听实现用户上下线通知
  • 使用消息类型区分聊天、系统通知、用户加入/离开

5.2 实时通知系统

用于向用户推送系统通知、消息提醒、任务状态更新等。

核心实现要点:

  • 使用 /user/queue/notifications 向特定用户推送通知
  • 使用 /topic/announcements 向所有用户推送系统公告
  • 结合数据库存储未读通知,用户上线时同步未读消息
  • 支持通知已读状态同步
java 复制代码
@Service
public class NotificationService {

    @Autowired
    private SimpMessagingTemplate messagingTemplate;

    @Autowired
    private NotificationRepository notificationRepository;

    /**
     * 发送通知给指定用户
     */
    public void sendNotification(String userId, Notification notification) {
        // 保存通知到数据库
        notification.setUserId(userId);
        notification.setRead(false);
        notification.setCreateTime(LocalDateTime.now());
        notificationRepository.save(notification);
        
        // 推送通知给在线用户
        messagingTemplate.convertAndSendToUser(userId, "/queue/notifications", notification);
    }

    /**
     * 发送系统公告
     */
    public void sendAnnouncement(Announcement announcement) {
        announcement.setCreateTime(LocalDateTime.now());
        // 保存公告到数据库
        announcementRepository.save(announcement);
        
        // 广播公告给所有在线用户
        messagingTemplate.convertAndSend("/topic/announcements", announcement);
    }

    /**
     * 用户上线时同步未读通知
     */
    @EventListener
    public void handleUserConnected(SessionConnectedEvent event) {
        String userId = event.getUser().getName();
        
        // 查询用户未读通知
        List<Notification> unreadNotifications = notificationRepository.findByUserIdAndReadFalse(userId);
        
        // 发送未读通知给用户
        for (Notification notification : unreadNotifications) {
            messagingTemplate.convertAndSendToUser(userId, "/queue/notifications", notification);
        }
    }
}

5.3 实时数据监控与仪表盘

用于展示实时数据,如服务器监控、股票行情、物流轨迹、订单状态等。

核心实现要点:

  • 使用定时任务定期获取数据并推送给客户端
  • 使用 /topic/data/{metric} 按指标分类推送
  • 支持客户端订阅特定指标,减少不必要的数据传输
  • 结合缓存提高数据获取效率
java 复制代码
@Component
@EnableScheduling
public class RealTimeDataService {

    @Autowired
    private SimpMessagingTemplate messagingTemplate;

    @Autowired
    private SystemMetricsService metricsService;

    /**
     * 每秒推送一次系统监控数据
     */
    @Scheduled(fixedRate = 1000)
    public void pushSystemMetrics() {
        SystemMetrics metrics = metricsService.getSystemMetrics();
        messagingTemplate.convertAndSend("/topic/metrics/system", metrics);
    }

    /**
     * 每5秒推送一次股票行情数据
     */
    @Scheduled(fixedRate = 5000)
    public void pushStockQuotes() {
        List<StockQuote> quotes = stockService.getLatestQuotes();
        messagingTemplate.convertAndSend("/topic/stocks/quotes", quotes);
    }

    /**
     * 推送特定订单的状态更新
     */
    public void pushOrderStatusUpdate(String orderId, OrderStatus status) {
        OrderStatusUpdate update = new OrderStatusUpdate();
        update.setOrderId(orderId);
        update.setStatus(status);
        update.setUpdateTime(LocalDateTime.now());
        
        // 推送给订单所属用户
        String userId = orderService.getOrderUserId(orderId);
        messagingTemplate.convertAndSendToUser(userId, "/queue/orders/" + orderId, update);
    }
}

5.4 协同编辑系统

支持多人同时编辑同一文档,实时同步编辑内容。

核心实现要点:

  • 使用操作转换(OT)算法解决并发编辑冲突
  • 按文档 ID 划分主题,如 /topic/documents/{docId}
  • 推送增量更新而非完整文档,减少数据传输量
  • 支持光标位置同步和用户在线状态显示

5.5 实时游戏与对战系统

用于实现实时多人游戏、答题对战、在线白板等功能。

核心实现要点:

  • 低延迟通信,确保游戏体验流畅
  • 使用房间机制隔离不同游戏对局
  • 支持游戏状态同步和玩家操作广播
  • 异常断开处理和游戏自动结算

六、性能优化与集群部署

6.1 单机性能优化

  1. 线程池调优
java 复制代码
@Override
public void configureClientInboundChannel(ChannelRegistration registration) {
    registration.taskExecutor()
               .corePoolSize(Runtime.getRuntime().availableProcessors() * 2)
               .maxPoolSize(Runtime.getRuntime().availableProcessors() * 4)
               .queueCapacity(10000)
               .keepAliveSeconds(60);
}
  1. 消息压缩
java 复制代码
@Bean
public WebSocketHandlerDecoratorFactory compressionDecoratorFactory() {
    return handler -> new WebSocketHandlerDecorator(handler) {
        @Override
        public void handleMessage(WebSocketSession session, WebSocketMessage<?> message) throws Exception {
            // 实现消息压缩逻辑
            super.handleMessage(session, message);
        }
    };
}
  1. 使用 ConcurrentWebSocketSessionDecorator
java 复制代码
@Override
public void configureWebSocketTransport(WebSocketTransportRegistration registration) {
    registration.addDecoratorFactory(handler -> new ConcurrentWebSocketSessionDecorator(
        handler,
        10 * 1000, // 发送超时时间
        512 * 1024 // 发送缓冲区大小
    ));
}

6.2 集群部署方案

单机 WebSocket 服务无法满足高并发和高可用需求,需要进行集群部署。

6.2.1 使用 RabbitMQ 作为外部消息代理
java 复制代码
@Configuration
@EnableWebSocketMessageBroker
public class ClusterWebSocketConfig implements WebSocketMessageBrokerConfigurer {

    @Override
    public void configureMessageBroker(MessageBrokerRegistry config) {
        // 使用 RabbitMQ 作为外部消息代理,替代简单内存代理
        config.enableStompBrokerRelay("/topic", "/queue")
              .setRelayHost("rabbitmq-host")
              .setRelayPort(61613)
              .setClientLogin("guest")
              .setClientPasscode("guest")
              .setSystemLogin("guest")
              .setSystemPasscode("guest");
        
        config.setApplicationDestinationPrefixes("/app");
        config.setUserDestinationPrefix("/user");
    }

    // ... 其他配置不变
}
6.2.2 Nginx 负载均衡配置
nginx 复制代码
# nginx.conf
upstream websocket_servers {
    server 192.168.1.101:8080;
    server 192.168.1.102:8080;
    server 192.168.1.103:8080;
}

server {
    listen 80;
    server_name ws.example.com;

    location /ws {
        proxy_pass http://websocket_servers;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        
        # 设置超时时间
        proxy_connect_timeout 60s;
        proxy_send_timeout 60s;
        proxy_read_timeout 3600s;
    }
}

6.3 生产环境最佳实践

  1. 使用 WSS 加密传输
properties 复制代码
# application.properties
server.ssl.enabled=true
server.ssl.key-store=classpath:keystore.p12
server.ssl.key-store-password=your-password
server.ssl.key-store-type=PKCS12
server.ssl.key-alias=websocket
  1. 配置心跳检测
java 复制代码
@Bean
public ServletServerContainerFactoryBean createWebSocketContainer() {
    ServletServerContainerFactoryBean container = new ServletServerContainerFactoryBean();
    container.setMaxSessionIdleTimeout(30 * 60 * 1000); // 30分钟
    return container;
}
  1. 实现客户端指数退避重连
javascript 复制代码
let reconnectAttempts = 0;
const maxReconnectAttempts = 10;

function connect() {
    const socket = new SockJS('/ws');
    stompClient = Stomp.over(socket);
    
    stompClient.connect({}, function(frame) {
        console.log('连接成功');
        reconnectAttempts = 0; // 重置重连计数器
        
        // 订阅主题
        subscribeToTopics();
    }, function(error) {
        console.error('连接失败: ' + error);
        
        if (reconnectAttempts < maxReconnectAttempts) {
            // 指数退避重连
            const delay = Math.pow(2, reconnectAttempts) * 1000;
            console.log(`将在 ${delay/1000} 秒后尝试重连...`);
            
            setTimeout(function() {
                reconnectAttempts++;
                connect();
            }, delay);
        } else {
            console.error('达到最大重连次数,停止重连');
        }
    });
}
  1. 添加监控指标
java 复制代码
@Component
public class WebSocketMetrics {

    private final MeterRegistry meterRegistry;
    private final Counter connectionCounter;
    private final Counter disconnectionCounter;
    private final Counter messageSentCounter;
    private final Counter messageReceivedCounter;
    private final Gauge activeConnectionsGauge;

    private final AtomicInteger activeConnections = new AtomicInteger(0);

    public WebSocketMetrics(MeterRegistry meterRegistry) {
        this.meterRegistry = meterRegistry;
        this.connectionCounter = Counter.builder("websocket.connections.total")
                                       .description("Total number of WebSocket connections")
                                       .register(meterRegistry);
        this.disconnectionCounter = Counter.builder("websocket.disconnections.total")
                                          .description("Total number of WebSocket disconnections")
                                          .register(meterRegistry);
        this.messageSentCounter = Counter.builder("websocket.messages.sent")
                                        .description("Number of messages sent")
                                        .register(meterRegistry);
        this.messageReceivedCounter = Counter.builder("websocket.messages.received")
                                            .description("Number of messages received")
                                            .register(meterRegistry);
        this.activeConnectionsGauge = Gauge.builder("websocket.connections.active")
                                           .description("Number of active WebSocket connections")
                                           .register(meterRegistry, activeConnections, AtomicInteger::get);
    }

    @EventListener
    public void handleSessionConnected(SessionConnectedEvent event) {
        connectionCounter.increment();
        activeConnections.incrementAndGet();
    }

    @EventListener
    public void handleSessionDisconnect(SessionDisconnectEvent event) {
        disconnectionCounter.increment();
        activeConnections.decrementAndGet();
    }

    public void incrementMessageSent() {
        messageSentCounter.increment();
    }

    public void incrementMessageReceived() {
        messageReceivedCounter.increment();
    }
}

七、常见问题与解决方案

7.1 跨域问题

问题:前端无法连接到不同域名的 WebSocket 服务。

解决方案

java 复制代码
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
    registry.addEndpoint("/ws")
            // 生产环境应配置具体域名,不要使用 "*"
            .setAllowedOriginPatterns("https://example.com", "https://www.example.com")
            .withSockJS();
}

7.2 连接断开问题

问题:WebSocket 连接经常意外断开。

解决方案

  1. 增加心跳检测
  2. 配置更长的超时时间
  3. 实现客户端自动重连
  4. 检查防火墙和负载均衡器的超时设置

7.3 消息丢失问题

问题:高并发场景下出现消息丢失。

解决方案

  1. 使用外部消息代理(如 RabbitMQ)
  2. 实现消息确认机制
  3. 增加消息持久化
  4. 优化线程池配置

7.4 内存泄漏问题

问题:长时间运行后内存占用持续增长。

解决方案

  1. 及时清理断开的 Session
  2. 避免在 Session 中存储大对象
  3. 使用弱引用存储临时数据
  4. 定期进行内存分析
相关推荐
说不得明天1 小时前
网络管理:AutoarNM部分
c语言·网络·mcu·汽车·autosar
xhbh6661 小时前
无公网IP环境下的宽带端口映射:80km穿云箭部署与性能测试
网络·智能路由器
胡志辉的博客1 小时前
邮件中点击“加载图片”,你的IP地址已经被泄漏
网络协议·user-agent·加载图片 ip 泄漏·邮件远程图片·追踪像素·邮件隐私保护·tracking pixel
lularible1 小时前
PTP协议精讲(4.4):从时钟程序实现——时间的“追随者“
网络·网络协议·开源·嵌入式·ptp
小辰记事本1 小时前
RDMA:AI算力集群的“网络命脉”
网络·人工智能·网络协议·rdma
缪懿1 小时前
javaEE:网络编程基础
java·网络·java-ee
BizViewStudio1 小时前
2026 年网站建设行业白皮书:AI 深度融合与合规驱动下的 6 大变革方向——附优质开发商
大数据·网络·人工智能·microsoft·媒体
切糕师学AI2 小时前
深入解析 gRPC:高性能开源 RPC 框架的原理与实战
网络协议·rpc·开源·grpc
500佰2 小时前
我唯一的一个变现产品,说说它的逻辑
网络·职场和发展·idea·个人开发·软件需求