一、WebSocket 概述与 Spring Boot 集成方式
1.1 WebSocket 核心优势
WebSocket 是一种全双工通信协议,在单个 TCP 连接上实现客户端与服务器的双向实时通信。相比传统 HTTP 轮询,它具有以下显著优势:
- 低延迟:连接建立后,数据可即时传输,无需等待 HTTP 请求响应
- 低开销:头部信息极小(仅 2-10 字节),远小于 HTTP 请求的几十到上百字节
- 全双工:客户端和服务器可同时发送数据
- 持久连接:一次握手,长期保持连接状态
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 基础配置与依赖
- 添加 Maven 依赖
xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
- 启用 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 单机性能优化
- 线程池调优
java
@Override
public void configureClientInboundChannel(ChannelRegistration registration) {
registration.taskExecutor()
.corePoolSize(Runtime.getRuntime().availableProcessors() * 2)
.maxPoolSize(Runtime.getRuntime().availableProcessors() * 4)
.queueCapacity(10000)
.keepAliveSeconds(60);
}
- 消息压缩
java
@Bean
public WebSocketHandlerDecoratorFactory compressionDecoratorFactory() {
return handler -> new WebSocketHandlerDecorator(handler) {
@Override
public void handleMessage(WebSocketSession session, WebSocketMessage<?> message) throws Exception {
// 实现消息压缩逻辑
super.handleMessage(session, message);
}
};
}
- 使用 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 生产环境最佳实践
- 使用 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
- 配置心跳检测
java
@Bean
public ServletServerContainerFactoryBean createWebSocketContainer() {
ServletServerContainerFactoryBean container = new ServletServerContainerFactoryBean();
container.setMaxSessionIdleTimeout(30 * 60 * 1000); // 30分钟
return container;
}
- 实现客户端指数退避重连
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('达到最大重连次数,停止重连');
}
});
}
- 添加监控指标
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 连接经常意外断开。
解决方案:
- 增加心跳检测
- 配置更长的超时时间
- 实现客户端自动重连
- 检查防火墙和负载均衡器的超时设置
7.3 消息丢失问题
问题:高并发场景下出现消息丢失。
解决方案:
- 使用外部消息代理(如 RabbitMQ)
- 实现消息确认机制
- 增加消息持久化
- 优化线程池配置
7.4 内存泄漏问题
问题:长时间运行后内存占用持续增长。
解决方案:
- 及时清理断开的 Session
- 避免在 Session 中存储大对象
- 使用弱引用存储临时数据
- 定期进行内存分析