
🏆本文收录于《滚雪球学Spring Boot》,专门攻坚指数提升,2025 年国内最系统+最强(更新中)。
本专栏致力打造最硬核 Spring Boot 从零基础到进阶系列学习内容,🚀均为全网独家首发,打造精品专栏,专栏持续更新中…欢迎大家订阅持续学习。 如果想快速定位学习,可以看这篇【SpringBoot教程导航帖】,你想学习的都被收集在内,快速投入学习!!两不误。
演示环境说明:
- 开发工具:IDEA 2021.3
- JDK版本: JDK 17(推荐使用 JDK 17 或更高版本,因为 Spring Boot 3.x 系列要求 Java 17,Spring Boot 3.5.4 基于 Spring Framework 6.x 和 Jakarta EE 9,它们都要求至少 JDK 17。)
- Spring Boot版本:3.5.4(于25年7月24日发布)
- Maven版本:3.8.2 (或更高)
- Gradle:(如果使用 Gradle 构建工具的话):推荐使用 Gradle 7.5 或更高版本,确保与 JDK 17 兼容。
- 操作系统:Windows 11
全文目录:
🌟 前言:从轮询地狱到实时天堂的救赎之路
各位亲爱的程序猿们!😭 还记得那些年我们为了实现一个"实时"聊天功能,疯狂使用Ajax轮询的黑暗日子吗?每隔几秒就向服务器发个请求问:"有新消息吗?有新消息吗?"服务器被问得不胜其烦,带宽被浪费得一塌糊涂,用户体验差得让人想砸手机!😤
但是!WebSocket的出现就像是黑暗中的一束光,彻底改变了我们对实时通信的认知!🌟 今天,就让我这个在实时推送领域摸爬滚打了N年的老码农,带大家深入探索WebSocket的神奇世界!相信我,学会了WebSocket,你就掌握了现代Web应用的核心竞争力!💪
🎯 目录导航
- 🤔 WebSocket到底解决了什么痛点?
- ⚡ Spring Boot中的WebSocket实现深度剖析
- 🛠️ STOMP协议:让WebSocket更加优雅
- 💻 实战案例:从零搭建实时聊天系统
- 🔥 集群部署:Redis+WebSocket的完美配合
- 📊 监控与调试:让你的WebSocket应用稳如老狗
- 💡 性能优化:千万级连接的终极秘籍
- 🎊 总结:实时推送的未来趋势
🤔 WebSocket到底解决了什么痛点?
说起WebSocket之前的时代,那真是一把辛酸泪啊!😢 想要实现实时功能,我们只能用各种奇葩的方案来"模拟"实时效果。
📡 传统方案的各种坑
1. 短轮询:服务器被烦死 🔄
// 😰 每秒发一次请求,服务器:我太难了!
setInterval(function() {
$.get('/api/messages/new', function(data) {
if (data.length > 0) {
updateMessageList(data);
}
});
}, 1000); // 每秒钟问一次:有新消息吗?
2. 长轮询:连接超时头疼 ⏰
// 服务器端要hold住连接,各种超时问题
@GetMapping("/messages/longpoll")
public DeferredResult<List<Message>> longPoll() {
DeferredResult<List<Message>> result = new DeferredResult<>(30000L);
// 30秒内如果没有新消息,就超时返回
// 各种边界情况处理,头都大了!😵💫
return result;
}
3. SSE:单向推送的局限 📤
// 只能服务器向客户端推送,客户端想发消息?另想办法!
@GetMapping(value = "/events", produces = "text/event-stream")
public SseEmitter streamEvents() {
SseEmitter emitter = new SseEmitter();
// 只能单向推送,想要双向通信?做梦!😅
return emitter;
}
🚀 WebSocket的革命性变化
WebSocket的出现彻底改变了游戏规则!它提供了真正的全双工通信,就像是在客户端和服务器之间建立了一条专属的"热线电话"!📞
WebSocket的核心优势:
- 真正的双向通信 🔄 - 客户端和服务器都可以主动发消息
- 低延迟 ⚡ - 没有HTTP请求头的冗余,数据传输更高效
- 持久连接 🔗 - 一次握手,长期使用
- 更少的服务器资源消耗 💚 - 不用频繁建立连接
// 🎉 WebSocket让实时通信变得如此简单!
@MessageMapping("/chat.sendMessage")
@SendTo("/topic/public")
public ChatMessage sendMessage(@Payload ChatMessage chatMessage) {
return chatMessage; // 就这么简单,消息瞬间推送给所有在线用户!
}
⚡ Spring Boot中的WebSocket实现深度剖析
Spring Boot对WebSocket的支持那叫一个贴心!只需要几个注解,就能快速搭建一个WebSocket应用。但要想玩得溜,还得深入理解其中的奥秘!🔍
🎪 基础配置:让WebSocket跑起来
// 📝 WebSocket配置类 - 一切的开始
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(new ChatWebSocketHandler(), "/websocket/chat")
.setAllowedOrigins("*") // 跨域处理
.withSockJS(); // SockJS支持,兜底方案
}
}
// 🎭 自定义WebSocket处理器
@Component
public class ChatWebSocketHandler extends TextWebSocketHandler {
// 存储所有活跃连接 - 这就是我们的"用户在线列表"
private final Map<String, WebSocketSession> sessions = new ConcurrentHashMap<>();
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
String userId = getUserId(session); // 从session中获取用户ID
sessions.put(userId, session);
log.info("用户{}连接成功,当前在线用户数:{}", userId, sessions.size()); // 😊 新用户上线
// 向所有用户广播有新用户加入
broadcastMessage(new ChatMessage("SYSTEM", userId + "加入了聊天室", MessageType.JOIN));
}
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
String userId = getUserId(session);
String content = message.getPayload();
log.info("收到用户{}的消息:{}", userId, content);
// 解析消息并广播
ChatMessage chatMessage = parseMessage(content);
chatMessage.setSender(userId);
chatMessage.setTimestamp(System.currentTimeMillis());
broadcastMessage(chatMessage); // 📢 消息瞬间传播给所有在线用户
}
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
String userId = getUserId(session);
sessions.remove(userId);
log.info("用户{}断开连接,当前在线用户数:{}", userId, sessions.size()); // 😢 用户离线
// 广播用户离开消息
broadcastMessage(new ChatMessage("SYSTEM", userId + "离开了聊天室", MessageType.LEAVE));
}
// 🎯 核心方法:消息广播
private void broadcastMessage(ChatMessage message) {
String messageJson = JsonUtils.toJson(message);
sessions.values().parallelStream().forEach(session -> {
try {
if (session.isOpen()) {
session.sendMessage(new TextMessage(messageJson));
}
} catch (Exception e) {
log.error("发送消息失败", e);
// 连接异常,从列表中移除
sessions.entrySet().removeIf(entry -> entry.getValue().equals(session));
}
});
log.info("消息已广播给{}个在线用户", sessions.size());
}
}
🎨 进阶功能:房间管理和私聊
基础的群聊太简单了,让我们来点高级的!实现聊天室分组和私聊功能!🏠
// 🏰 聊天室管理器
@Service
public class ChatRoomManager {
// 房间 -> 用户会话映射
private final Map<String, Set<WebSocketSession>> roomSessions = new ConcurrentHashMap<>();
// 用户 -> 房间映射
private final Map<String, String> userRooms = new ConcurrentHashMap<>();
// 🚪 用户加入房间
public void joinRoom(String userId, String roomId, WebSocketSession session) {
// 先退出原房间
leaveCurrentRoom(userId);
roomSessions.computeIfAbsent(roomId, k -> ConcurrentHashMap.newKeySet()).add(session);
userRooms.put(userId, roomId);
log.info("用户{}加入房间{},房间内用户数:{}", userId, roomId, roomSessions.get(roomId).size());
// 通知房间内其他用户
broadcastToRoom(roomId, new ChatMessage("SYSTEM",
userId + "加入了房间", MessageType.JOIN), session);
}
// 📤 房间内消息广播
public void broadcastToRoom(String roomId, ChatMessage message, WebSocketSession excludeSession) {
Set<WebSocketSession> sessions = roomSessions.get(roomId);
if (sessions == null || sessions.isEmpty()) return;
String messageJson = JsonUtils.toJson(message);
sessions.parallelStream()
.filter(session -> !session.equals(excludeSession) && session.isOpen())
.forEach(session -> {
try {
session.sendMessage(new TextMessage(messageJson));
} catch (Exception e) {
log.error("发送房间消息失败", e);
sessions.remove(session); // 清理无效连接
}
});
}
// 💌 私聊功能
public void sendPrivateMessage(String fromUserId, String toUserId, String content) {
WebSocketSession targetSession = findUserSession(toUserId);
if (targetSession != null && targetSession.isOpen()) {
try {
ChatMessage privateMessage = new ChatMessage(fromUserId, content, MessageType.PRIVATE);
targetSession.sendMessage(new TextMessage(JsonUtils.toJson(privateMessage)));
log.info("私聊消息已发送:{} -> {}", fromUserId, toUserId);
} catch (Exception e) {
log.error("发送私聊消息失败", e);
}
} else {
log.warn("目标用户{}不在线,私聊消息发送失败", toUserId);
}
}
}
🛠️ STOMP协议:让WebSocket更加优雅
原生WebSocket虽然强大,但用起来还是有点底层。STOMP(Simple Text Oriented Messaging Protocol)就像是WebSocket的高级封装,让消息传递变得更加优雅!✨
🎪 STOMP配置与使用
// 🚀 STOMP配置 - 比原生WebSocket优雅多了
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketStompConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.enableSimpleBroker("/topic", "/queue"); // 简单消息代理
config.setApplicationDestinationPrefixes("/app"); // 应用目的地前缀
config.setUserDestinationPrefix("/user"); // 用户专属前缀
}
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/ws")
.setAllowedOriginPatterns("*")
.withSockJS(); // SockJS支持
}
// 🎯 自定义拦截器 - 身份验证和连接管理
@Override
public void configureClientInboundChannel(ChannelRegistration registration) {
registration.interceptors(new ChannelInterceptor() {
@Override
public Message<?> preSend(Message<?> message, MessageChannel channel) {
StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
if (StompCommand.CONNECT.equals(accessor.getCommand())) {
// 连接时的身份验证
String token = accessor.getFirstNativeHeader("Authorization");
if (isValidToken(token)) {
String userId = extractUserId(token);
accessor.setUser(() -> userId); // 设置用户标识
log.info("用户{}通过WebSocket认证", userId); // ✅ 认证通过
} else {
log.warn("WebSocket认证失败"); // ❌ 认证失败
return null; // 阻止连接
}
}
return message;
}
});
}
}
// 🎭 STOMP消息控制器
@Controller
public class WebSocketStompController {
@Autowired
private SimpMessagingTemplate messagingTemplate;
@Autowired
private ChatService chatService;
// 📢 群聊消息处理
@MessageMapping("/chat.sendMessage")
@SendTo("/topic/public")
public ChatMessage sendMessage(@Payload ChatMessage chatMessage,
SimpMessageHeaderAccessor headerAccessor) {
String userId = headerAccessor.getUser().getName();
chatMessage.setSender(userId);
chatMessage.setTimestamp(System.currentTimeMillis());
// 保存消息到数据库
chatService.saveMessage(chatMessage);
log.info("群聊消息:{} -> {}", userId, chatMessage.getContent());
return chatMessage; // 🚀 自动广播给所有订阅者
}
// 🏠 房间消息处理
@MessageMapping("/chat.sendToRoom")
public void sendToRoom(@Payload RoomMessage message,
SimpMessageHeaderAccessor headerAccessor) {
String userId = headerAccessor.getUser().getName();
message.setSender(userId);
// 发送到指定房间
messagingTemplate.convertAndSend("/topic/room/" + message.getRoomId(), message);
log.info("房间{}消息:{} -> {}", message.getRoomId(), userId, message.getContent());
}
// 💌 私聊消息处理
@MessageMapping("/chat.sendPrivate")
public void sendPrivateMessage(@Payload PrivateMessage message,
SimpMessageHeaderAccessor headerAccessor) {
String fromUserId = headerAccessor.getUser().getName();
String toUserId = message.getToUserId();
message.setFromUserId(fromUserId);
message.setTimestamp(System.currentTimeMillis());
// 发送给指定用户
messagingTemplate.convertAndSendToUser(toUserId, "/queue/private", message);
// 也给发送者一份(确认消息已发送)
messagingTemplate.convertAndSendToUser(fromUserId, "/queue/private", message);
log.info("私聊消息:{} -> {} : {}", fromUserId, toUserId, message.getContent());
}
// 📊 在线用户列表
@MessageMapping("/chat.getOnlineUsers")
@SendTo("/topic/onlineUsers")
public List<OnlineUser> getOnlineUsers() {
return chatService.getOnlineUsers(); // 返回当前在线用户列表
}
}
🎨 前端JavaScript集成
// 🌐 前端WebSocket客户端实现
class ChatClient {
constructor(userId, token) {
this.userId = userId;
this.token = token;
this.stompClient = null;
this.connected = false;
}
// 🔌 连接WebSocket
connect() {
const socket = new SockJS('/ws');
this.stompClient = Stomp.over(socket);
// 设置认证头
const headers = {
'Authorization': this.token
};
this.stompClient.connect(headers,
(frame) => {
console.log('WebSocket连接成功: ' + frame);
this.connected = true;
this.onConnected(); // 🎉 连接成功回调
},
(error) => {
console.error('WebSocket连接失败: ', error);
this.onError(error); // 😰 连接失败处理
// 5秒后重连
setTimeout(() => this.connect(), 5000);
}
);
}
// ✅ 连接成功后的初始化
onConnected() {
// 订阅群聊消息
this.stompClient.subscribe('/topic/public', (message) => {
const chatMessage = JSON.parse(message.body);
this.displayMessage(chatMessage); // 🎨 显示群聊消息
});
// 订阅私聊消息
this.stompClient.subscribe('/user/queue/private', (message) => {
const privateMessage = JSON.parse(message.body);
this.displayPrivateMessage(privateMessage); // 💌 显示私聊消息
});
// 订阅在线用户列表更新
this.stompClient.subscribe('/topic/onlineUsers', (message) => {
const onlineUsers = JSON.parse(message.body);
this.updateOnlineUsersList(onlineUsers); // 👥 更新在线用户
});
// 通知用户加入
this.sendMessage({
type: 'JOIN',
content: this.userId + '加入了聊天室'
});
}
// 📤 发送群聊消息
sendMessage(message) {
if (this.connected) {
this.stompClient.send('/app/chat.sendMessage', {}, JSON.stringify(message));
} else {
console.warn('WebSocket未连接,消息发送失败'); // ⚠️ 连接检查
}
}
// 💌 发送私聊消息
sendPrivateMessage(toUserId, content) {
const privateMessage = {
toUserId: toUserId,
content: content,
type: 'PRIVATE'
};
this.stompClient.send('/app/chat.sendPrivate', {}, JSON.stringify(privateMessage));
}
// 🔌 断开连接
disconnect() {
if (this.stompClient !== null) {
this.stompClient.disconnect();
this.connected = false;
console.log('WebSocket已断开'); // 👋 再见
}
}
}
// 🎪 使用示例
const chatClient = new ChatClient('user123', 'jwt-token-here');
chatClient.connect();
// 发送消息
document.getElementById('sendBtn').addEventListener('click', function() {
const messageContent = document.getElementById('messageInput').value;
chatClient.sendMessage({
content: messageContent,
type: 'CHAT'
});
});
💻 实战案例:从零搭建实时聊天系统
光说不练假把式!让我们来搭建一个真正能用的实时聊天系统,包含用户认证、消息持久化、文件传输等实用功能!🛠️
🗄️ 数据库设计
-- 👤 用户表
CREATE TABLE users (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
username VARCHAR(50) UNIQUE NOT NULL,
nickname VARCHAR(100),
avatar_url VARCHAR(255),
status ENUM('ONLINE', 'OFFLINE', 'AWAY') DEFAULT 'OFFLINE',
last_seen TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 🏠 聊天室表
CREATE TABLE chat_rooms (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(100) NOT NULL,
description TEXT,
owner_id BIGINT,
type ENUM('PUBLIC', 'PRIVATE') DEFAULT 'PUBLIC',
max_members INT DEFAULT 100,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (owner_id) REFERENCES users(id)
);
-- 📝 消息表
CREATE TABLE messages (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
room_id BIGINT,
sender_id BIGINT,
receiver_id BIGINT, -- 私聊时使用
content TEXT NOT NULL,
message_type ENUM('TEXT', 'IMAGE', 'FILE', 'SYSTEM') DEFAULT 'TEXT',
file_url VARCHAR(255), -- 文件消息的URL
reply_to BIGINT, -- 回复消息ID
is_deleted BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (room_id) REFERENCES chat_rooms(id),
FOREIGN KEY (sender_id) REFERENCES users(id),
FOREIGN KEY (receiver_id) REFERENCES users(id),
FOREIGN KEY (reply_to) REFERENCES messages(id)
);
-- 👥 房间成员表
CREATE TABLE room_members (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
room_id BIGINT,
user_id BIGINT,
role ENUM('OWNER', 'ADMIN', 'MEMBER') DEFAULT 'MEMBER',
joined_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (room_id) REFERENCES chat_rooms(id),
FOREIGN KEY (user_id) REFERENCES users(id),
UNIQUE KEY unique_room_user (room_id, user_id)
);
🎯 核心业务服务实现
// 💎 聊天服务核心实现
@Service
@Transactional
public class ChatService {
@Autowired
private UserRepository userRepository;
@Autowired
private MessageRepository messageRepository;
@Autowired
private ChatRoomRepository chatRoomRepository;
@Autowired
private SimpMessagingTemplate messagingTemplate;
@Autowired
private RedisTemplate<String, Object> redisTemplate;
// 💾 保存消息
public Message saveMessage(ChatMessage chatMessage) {
Message message = new Message();
message.setContent(chatMessage.getContent());
message.setMessageType(MessageType.valueOf(chatMessage.getType()));
message.setSenderId(chatMessage.getSenderId());
message.setRoomId(chatMessage.getRoomId());
message.setCreatedAt(LocalDateTime.now());
Message savedMessage = messageRepository.save(message);
log.info("消息已保存到数据库,ID:{}", savedMessage.getId()); // 💾 持久化成功
return savedMessage;
}
// 📜 获取历史消息
public List<Message> getHistoryMessages(Long roomId, int page, int size) {
Pageable pageable = PageRequest.of(page, size, Sort.by("createdAt").descending());
// 🚀 先从Redis缓存获取
String cacheKey = "chat:history:" + roomId + ":" + page;
List<Message> cachedMessages = (List<Message>) redisTemplate.opsForValue().get(cacheKey);
if (cachedMessages != null) {
log.info("从缓存获取历史消息,房间ID:{},页码:{}", roomId, page);
return cachedMessages;
}
// 缓存未命中,查询数据库
List<Message> messages = messageRepository.findByRoomIdAndIsDeletedFalse(roomId, pageable);
// 缓存结果,5分钟过期
redisTemplate.opsForValue().set(cacheKey, messages, Duration.ofMinutes(5));
log.info("从数据库获取历史消息,房间ID:{},消息数:{}", roomId, messages.size());
return messages;
}
// 📁 文件消息处理
public void handleFileMessage(MultipartFile file, Long roomId, String senderId) {
try {
// 🔄 文件上传到OSS(这里简化为本地存储)
String fileName = System.currentTimeMillis() + "_" + file.getOriginalFilename();
String filePath = "/uploads/" + fileName;
// 保存文件
Path targetPath = Paths.get("uploads", fileName);
Files.copy(file.getInputStream(), targetPath, StandardCopyOption.REPLACE_EXISTING);
// 创建文件消息
ChatMessage fileMessage = new ChatMessage();
fileMessage.setSenderId(Long.parseLong(senderId));
fileMessage.setRoomId(roomId);
fileMessage.setType("FILE");
fileMessage.setContent(file.getOriginalFilename()); // 显示原文件名
fileMessage.setFileUrl(filePath);
fileMessage.setFileSize(file.getSize());
// 保存到数据库
saveMessage(fileMessage);
// 🚀 推送给房间内所有用户
messagingTemplate.convertAndSend("/topic/room/" + roomId, fileMessage);
log.info("文件消息处理完成:{}", fileName); // 📁 文件上传成功
} catch (Exception e) {
log.error("文件消息处理失败", e);
throw new RuntimeException("文件上传失败"); // 💥 上传失败
}
}
// 👥 获取在线用户列表
public List<OnlineUser> getOnlineUsers() {
Set<String> onlineUserIds = redisTemplate.opsForSet().members("online:users");
return onlineUserIds.stream()
.map(userId -> {
User user = userRepository.findById(Long.parseLong(userId)).orElse(null);
if (user != null) {
return new OnlineUser(user.getId(), user.getNickname(), user.getAvatarUrl());
}
return null;
})
.filter(Objects::nonNull)
.collect(Collectors.toList());
}
// 🔔 消息提醒处理
public void handleMessageNotification(Message message) {
// 获取接收者列表
List<Long> receiverIds = getMessageReceivers(message);
for (Long receiverId : receiverIds) {
// 检查用户是否在线
if (isUserOnline(receiverId)) {
// 在线用户直接推送
messagingTemplate.convertAndSendToUser(
receiverId.toString(),
"/queue/notification",
new MessageNotification(message)
);
} else {
// 离线用户存储未读消息计数
String unreadKey = "unread:" + receiverId;
redisTemplate.opsForValue().increment(unreadKey);
// 可以在这里集成推送服务,发送APP推送或短信
// pushNotificationService.sendPush(receiverId, message);
}
}
}
}
🔥 集群部署:Redis+WebSocket的完美配合
单机的WebSocket玩起来很爽,但是当你需要集群部署的时候就会发现问题了:用户A连接到服务器1,用户B连接到服务器2,他们怎么聊天?😅 这时候就需要Redis来做消息中转了!
🎪 Redis消息中转实现
// 🚀 基于Redis的集群WebSocket解决方案
@Service
public class ClusterWebSocketService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Autowired
private SimpMessagingTemplate localMessagingTemplate;
private final String SERVER_ID = UUID.randomUUID().toString(); // 服务器唯一标识
// 🌐 跨服务器消息广播
public void broadcastMessage(String channel, Object message) {
// 添加服务器标识,避免重复处理
RedisMessage redisMessage = new RedisMessage();
redisMessage.setServerId(SERVER_ID);
redisMessage.setChannel(channel);
redisMessage.setPayload(message);
redisMessage.setTimestamp(System.currentTimeMillis());
// 发布到Redis
redisTemplate.convertAndSend("websocket:" + channel, redisMessage);
log.info("消息已广播到Redis频道:{}", channel); // 📡 消息广播
}
// 📨 处理来自Redis的消息
@EventListener
public void handleRedisMessage(RedisMessage redisMessage) {
// 忽略自己发送的消息,避免循环
if (SERVER_ID.equals(redisMessage.getServerId())) {
return;
}
String channel = redisMessage.getChannel();
Object payload = redisMessage.getPayload();
// 根据频道类型处理消息
if (channel.startsWith("room:")) {
// 房间消息
localMessagingTemplate.convertAndSend("/topic/" + channel, payload);
} else if (channel.startsWith("user:")) {
// 私聊消息
String userId = channel.substring(5); // 去掉"user:"前缀
localMessagingTemplate.convertAndSendToUser(userId, "/queue/private", payload);
} else if (channel.equals("global")) {
// 全局广播
localMessagingTemplate.convertAndSend("/topic/public", payload);
}
log.info("处理Redis消息:{} -> {}", channel, payload); // 🔄 消息处理
}
// 👥 用户上线处理
public void userOnline(String userId, String serverId) {
// 记录用户在哪台服务器上
redisTemplate.opsForHash().put("user:server:mapping", userId, serverId);
// 加入在线用户集合
redisTemplate.opsForSet().add("online:users", userId);
// 设置过期时间,防止服务器异常关闭时用户状态不更新
redisTemplate.expire("user:server:mapping", Duration.ofHours(1));
log.info("用户{}在服务器{}上线", userId, serverId); // 👋 用户上线
}
// 👤 用户下线处理
public void userOffline(String userId) {
redisTemplate.opsForHash().delete("user:server:mapping", userId);
redisTemplate.opsForSet().remove("online:users", userId);
log.info("用户{}下线", userId); // 👋 用户下线
}
// 🎯 定向发送消息给指定用户
public void sendToUser(String userId, Object message) {
// 查找用户所在的服务器
String targetServerId = (String) redisTemplate.opsForHash().get("user:server:mapping", userId);
if (targetServerId == null) {
log.warn("用户{}不在线,消息发送失败", userId); // ⚠️ 用户离线
return;
}
if (SERVER_ID.equals(targetServerId)) {
// 用户在当前服务器,直接发送
localMessagingTemplate.convertAndSendToUser(userId, "/queue/private", message);
} else {
// 用户在其他服务器,通过Redis转发
broadcastMessage("user:" + userId, message);
}
}
}
// 🎧 Redis消息监听器配置
@Configuration
public class RedisMessageListenerConfig {
@Autowired
private ClusterWebSocketService clusterWebSocketService;
@Bean
public RedisMessageListenerContainer redisMessageListenerContainer(
RedisConnectionFactory connectionFactory) {
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
container.setConnectionFactory(connectionFactory);
// 监听WebSocket相关频道
container.addMessageListener(new MessageListenerAdapter(clusterWebSocketService, "handleRedisMessage"),
new ChannelTopic("websocket:*"));
return container;
}
}
📊 连接状态管理与监控
// 📈 WebSocket连接监控服务
@Service
public class WebSocketMonitorService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
private final MeterRegistry meterRegistry;
// 当前服务器连接数
private final AtomicInteger localConnections = new AtomicInteger(0);
public WebSocketMonitorService(MeterRegistry meterRegistry) {
this.meterRegistry = meterRegistry;
// 注册监控指标
Gauge.builder("websocket.connections.local")
.description("本地WebSocket连接数")
.register(meterRegistry, localConnections, AtomicInteger::get);
}
// 📊 连接统计
public void onConnectionEstablished(String userId, String sessionId) {
localConnections.incrementAndGet();
// Redis中记录连接信息
String connectionKey = "websocket:connection:" + sessionId;
ConnectionInfo info = new ConnectionInfo(userId, sessionId,
InetAddress.getLocalHost().getHostName(), System.currentTimeMillis());
redisTemplate.opsForValue().set(connectionKey, info, Duration.ofHours(2));
// 更新全局统计
redisTemplate.opsForValue().increment("websocket:total:connections");
log.info("WebSocket连接建立 - 用户:{},会话:{},当前本地连接数:{}",
userId, sessionId, localConnections.get()); // 📈 连接统计
}
public void onConnectionClosed(String userId, String sessionId) {
localConnections.decrementAndGet();
// 清理连接信息
redisTemplate.delete("websocket:connection:" + sessionId);
redisTemplate.opsForValue().decrement("websocket:total:connections");
log.info("WebSocket连接关闭 - 用户:{},会话:{},当前本地连接数:{}",
userId, sessionId, localConnections.get()); // 📉 连接关闭
}
// 🔍 获取全局连接统计
public ConnectionStats getGlobalStats() {
Long totalConnections = (Long) redisTemplate.opsForValue().get("websocket:total:connections");
Set<String> onlineUsers = redisTemplate.opsForSet().members("online:users");
return new ConnectionStats(
totalConnections != null ? totalConnections : 0,
onlineUsers != null ? onlineUsers.size() : 0,
localConnections.get()
);
}
// 🧹 定时清理过期连接
@Scheduled(fixedRate = 300000) // 每5分钟执行一次
public void cleanupExpiredConnections() {
Set<String> connectionKeys = redisTemplate.keys("websocket:connection:*");
int cleanedCount = 0;
for (String key : connectionKeys) {
ConnectionInfo info = (ConnectionInfo) redisTemplate.opsForValue().get(key);
if (info != null) {
// 检查连接是否超过2小时无活动
if (System.currentTimeMillis() - info.getLastActivity() > 7200000) {
redisTemplate.delete(key);
cleanedCount++;
}
}
}
if (cleanedCount > 0) {
log.info("清理了{}个过期WebSocket连接", cleanedCount); // 🧹 清理完成
}
}
}
📊 监控与调试:让你的WebSocket应用稳如老狗
WebSocket应用一旦上线,监控就变得至关重要!你需要知道有多少用户在线,消息传输是否正常,哪里可能出现瓶颈。让我们来搭建一套完整的监控体系!📈
🎪 自定义监控端点
// 📊 WebSocket监控端点
@RestController
@RequestMapping("/api/websocket/monitor")
public class WebSocketMonitorController {
@Autowired
private WebSocketMonitorService monitorService;
@Autowired
private ChatService chatService;
// 📈 获取实时统计
@GetMapping("/stats")
public ResponseEntity<Map<String, Object>> getStats() {
ConnectionStats stats = monitorService.getGlobalStats();
List<OnlineUser> onlineUsers = chatService.getOnlineUsers();
Map<String, Object> result = new HashMap<>();
result.put("totalConnections", stats.getTotalConnections());
result.put("onlineUsers", stats.getOnlineUsers());
result.put("localConnections", stats.getLocalConnections());
result.put("onlineUserList", onlineUsers);
result.put("timestamp", System.currentTimeMillis());
return ResponseEntity.ok(result);
}
// 🔍 获取连接详情
@GetMapping("/connections")
public ResponseEntity<List<ConnectionInfo>> getConnections() {
List<ConnectionInfo> connections = monitorService.getAllConnections();
return ResponseEntity.ok(connections);
}
// 📊 获取消息统计
@GetMapping("/message-stats")
public ResponseEntity<MessageStats> getMessageStats(
@RequestParam @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate date) {
MessageStats stats = chatService.getMessageStatsForDate(date);
return ResponseEntity.ok(stats);
}
// 🎯 测试消息推送
@PostMapping("/test-broadcast")
public ResponseEntity<String> testBroadcast(@RequestParam String message) {
ChatMessage testMessage = new ChatMessage("SYSTEM", message, MessageType.SYSTEM);
// 广播测试消息
messagingTemplate.convertAndSend("/topic/public", testMessage);
return ResponseEntity.ok("测试消息已发送:" + message);
}
// 🔧 健康检查
@GetMapping("/health")
public ResponseEntity<Map<String, String>> healthCheck() {
Map<String, String> health = new HashMap<>();
try {
// 检查Redis连接
redisTemplate.opsForValue().get("health:check");
health.put("redis", "UP");
} catch (Exception e) {
health.put("redis", "DOWN");
}
// 检查数据库连接
try {
chatService.healthCheck();
health.put("database", "UP");
} catch (Exception e) {
health.put("database", "DOWN");
}
health.put("websocket", "UP");
health.put("timestamp", LocalDateTime.now().toString());
return ResponseEntity.ok(health);
}
}
🎨 前端监控界面
// 📊 WebSocket监控前端界面
class WebSocketMonitor {
constructor() {
this.chartInstance = null;
this.statsInterval = null;
this.initChart();
this.startMonitoring();
}
// 📈 初始化图表
initChart() {
const ctx = document.getElementById('connectionChart').getContext('2d');
this.chartInstance = new Chart(ctx, {
type: 'line',
data: {
labels: [],
datasets: [{
label: '在线连接数',
data: [],
borderColor: 'rgb(75, 192, 192)',
backgroundColor: 'rgba(75, 192, 192, 0.2)',
tension: 0.1
}]
},
options: {
responsive: true,
plugins: {
title: {
display: true,
text: 'WebSocket连接实时监控'
}
},
scales: {
y: {
beginAtZero: true
}
}
}
});
}
// 🔄 开始监控
startMonitoring() {
this.statsInterval = setInterval(() => {
this.fetchStats();
}, 5000); // 每5秒更新一次
// 立即执行一次
this.fetchStats();
}
// 📊 获取统计数据
async fetchStats() {
try {
const response = await fetch('/api/websocket/monitor/stats');
const stats = await response.json();
// 更新界面
this.updateUI(stats);
// 更新图表
this.updateChart(stats);
} catch (error) {
console.error('获取监控数据失败:', error);
this.showError('监控数据获取失败'); // ❌ 获取失败
}
}
// 🎨 更新界面
updateUI(stats) {
document.getElementById('totalConnections').textContent = stats.totalConnections;
document.getElementById('onlineUsers').textContent = stats.onlineUsers;
document.getElementById('localConnections').textContent = stats.localConnections;
// 更新在线用户列表
const userList = document.getElementById('onlineUsersList');
userList.innerHTML = '';
stats.onlineUserList.forEach(user => {
const userElement = document.createElement('div');
userElement.className = 'online-user';
userElement.innerHTML = `
<img src="${user.avatarUrl}" alt="${user.nickname}" class="avatar">
<span>${user.nickname}</span>
<span class="status online">在线</span>
`;
userList.appendChild(userElement);
});
}
// 📈 更新图表
updateChart(stats) {
const now = new Date().toLocaleTimeString();
// 保持最近50个数据点
if (this.chartInstance.data.labels.length >= 50) {
this.chartInstance.data.labels.shift();
this.chartInstance.data.datasets[0].data.shift();
}
this.chartInstance.data.labels.push(now);
this.chartInstance.data.datasets[0].data.push(stats.totalConnections);
this.chartInstance.update('none'); // 无动画更新,提高性能
}
// 🧪 测试功能
async testBroadcast() {
const message = prompt('请输入测试消息:');
if (message) {
try {
const response = await fetch('/api/websocket/monitor/test-broadcast', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: `message=${encodeURIComponent(message)}`
});
if (response.ok) {
this.showSuccess('测试消息发送成功'); // ✅ 发送成功
} else {
this.showError('测试消息发送失败'); // ❌ 发送失败
}
} catch (error) {
this.showError('网络错误: ' + error.message); // 🌐 网络错误
}
}
}
// ⏹️ 停止监控
stopMonitoring() {
if (this.statsInterval) {
clearInterval(this.statsInterval);
this.statsInterval = null;
}
}
}
// 🚀 启动监控
const monitor = new WebSocketMonitor();
// 页面卸载时停止监控
window.addEventListener('beforeunload', () => {
monitor.stopMonitoring();
});
💡 性能优化:千万级连接的终极秘籍
当你的WebSocket应用需要支持百万甚至千万级连接时,就需要考虑更多的优化策略了!这里分享一些我在大规模部署中积累的经验!🚀
🎪 连接池与资源优化
// ⚡ 高性能WebSocket配置
@Configuration
public class HighPerformanceWebSocketConfig {
// 🎯 自定义线程池配置
@Bean("websocketTaskExecutor")
public TaskExecutor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
// 核心线程数 = CPU核数
executor.setCorePoolSize(Runtime.getRuntime().availableProcessors());
// 最大线程数 = CPU核数 * 2
executor.setMaxPoolSize(Runtime.getRuntime().availableProcessors() * 2);
// 队列容量
executor.setQueueCapacity(1000);
// 线程名前缀
executor.setThreadNamePrefix("websocket-");
// 拒绝策略:调用者运行
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
// 等待任务完成后关闭线程池
executor.setWaitForTasksToCompleteOnShutdown(true);
executor.setAwaitTerminationSeconds(60);
executor.initialize();
return executor;
}
// 🔧 WebSocket传输配置优化
@Bean
public ServletServerContainerFactoryBean createWebSocketContainer() {
ServletServerContainerFactoryBean container = new ServletServerContainerFactoryBean();
// 设置最大文本消息大小:1MB
container.setMaxTextMessageBufferSize(1024 * 1024);
// 设置最大二进制消息大小:10MB
container.setMaxBinaryMessageBufferSize(10 * 1024 * 1024);
// 设置会话空闲超时:30分钟
container.setMaxSessionIdleTimeout(30 * 60 * 1000L);
// 设置异步发送超时:30秒
container.setAsyncSendTimeout(30 * 1000L);
return container;
}
}
// 🚀 高性能消息处理器
@Component
public class HighPerformanceMessageHandler extends TextWebSocketHandler {
// 使用ConcurrentHashMap提高并发性能
private final ConcurrentHashMap<String, WebSocketSession> sessions = new ConcurrentHashMap<>();
// 消息队列,避免阻塞
private final BlockingQueue<MessageTask> messageQueue = new LinkedBlockingQueue<>(10000);
// 异步消息处理线程池
private final ExecutorService messageProcessor = Executors.newFixedThreadPool(
Runtime.getRuntime().availableProcessors() * 2,
new ThreadFactoryBuilder().setNameFormat("msg-processor-%d").build()
);
@PostConstruct
public void init() {
// 启动消息处理线程
for (int i = 0; i < Runtime.getRuntime().availableProcessors(); i++) {
messageProcessor.submit(this::processMessages);
}
log.info("高性能消息处理器初始化完成"); // 🚀 初始化完成
}
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
String userId = extractUserId(session);
sessions.put(userId, session);
// 异步处理连接建立事件
messageQueue.offer(new MessageTask(MessageTaskType.CONNECTION_ESTABLISHED, userId, null, session));
log.info("用户{}连接建立,当前连接数:{}", userId, sessions.size());
}
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
String userId = extractUserId(session);
// 将消息处理任务放入队列,避免阻塞I/O线程
messageQueue.offer(new MessageTask(MessageTaskType.TEXT_MESSAGE, userId, message.getPayload(), session));
}
// 🎯 异步消息处理主循环
private void processMessages() {
while (!Thread.currentThread().isInterrupted()) {
try {
MessageTask task = messageQueue.take(); // 阻塞等待任务
switch (task.getType()) {
case CONNECTION_ESTABLISHED:
handleConnectionEstablishedAsync(task);
break;
case TEXT_MESSAGE:
handleTextMessageAsync(task);
break;
case CONNECTION_CLOSED:
handleConnectionClosedAsync(task);
break;
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
} catch (Exception e) {
log.error("消息处理异常", e);
}
}
}
// ⚡ 批量消息发送优化
private void broadcastMessageBatch(List<String> userIds, String message) {
// 使用并行流提高发送性能
userIds.parallelStream().forEach(userId -> {
WebSocketSession session = sessions.get(userId);
if (session != null && session.isOpen()) {
try {
// 使用同步发送,避免缓冲区溢出
synchronized (session) {
session.sendMessage(new TextMessage(message));
}
} catch (Exception e) {
log.warn("发送消息失败,用户:{}", userId, e);
// 清理无效连接
sessions.remove(userId);
}
}
});
}
}
📊 内存和连接优化策略
// 💾 内存优化的会话管理器
@Service
public class OptimizedSessionManager {
// 使用弱引用避免内存泄漏
private final Map<String, WeakReference<WebSocketSession>> sessions = new ConcurrentHashMap<>();
// 连接时间戳,用于清理长时间无活动的连接
private final Map<String, Long> lastActivity = new ConcurrentHashMap<>();
// 使用Bloom Filter减少Redis查询
private final BloomFilter<String> onlineUsersFilter = BloomFilter.create(
Funnels.stringFunnel(Charset.defaultCharset()),
1_000_000, // 预期元素数量:100万
0.01 // 误报率:1%
);
@Autowired
private RedisTemplate<String, Object> redisTemplate;
// 📝 注册会话
public void registerSession(String userId, WebSocketSession session) {
sessions.put(userId, new WeakReference<>(session));
lastActivity.put(userId, System.currentTimeMillis());
onlineUsersFilter.put(userId);
// 异步更新Redis
CompletableFuture.runAsync(() -> {
redisTemplate.opsForSet().add("online:users", userId);
redisTemplate.expire("online:users", Duration.ofHours(1));
});
log.debug("会话已注册:{}", userId);
}
// 🗑️ 移除会话
public void removeSession(String userId) {
sessions.remove(userId);
lastActivity.remove(userId);
// 异步更新Redis
CompletableFuture.runAsync(() -> {
redisTemplate.opsForSet().remove("online:users", userId);
});
log.debug("会话已移除:{}", userId);
}
// 🔍 检查用户是否在线(使用Bloom Filter优化)
public boolean isUserOnline(String userId) {
if (!onlineUsersFilter.mightContain(userId)) {
return false; // Bloom Filter说不在,那一定不在
}
// Bloom Filter说可能在,需要进一步验证
WeakReference<WebSocketSession> sessionRef = sessions.get(userId);
if (sessionRef != null) {
WebSocketSession session = sessionRef.get();
return session != null && session.isOpen();
}
return false;
}
// 🧹 定时清理无效连接
@Scheduled(fixedRate = 300000) // 每5分钟执行一次
public void cleanupInactiveSessions() {
long currentTime = System.currentTimeMillis();
long inactiveThreshold = 30 * 60 * 1000; // 30分钟无活动
List<String> inactiveUsers = lastActivity.entrySet().parallelStream()
.filter(entry -> currentTime - entry.getValue() > inactiveThreshold)
.map(Map.Entry::getKey)
.collect(Collectors.toList());
inactiveUsers.forEach(this::removeSession);
if (!inactiveUsers.isEmpty()) {
log.info("清理了{}个无活动连接", inactiveUsers.size()); // 🧹 清理完成
}
// 清理弱引用
sessions.entrySet().removeIf(entry -> entry.getValue().get() == null);
}
// 📊 获取连接统计
public SessionStats getStats() {
int activeSessions = (int) sessions.values().parallelStream()
.filter(ref -> ref.get() != null && ref.get().isOpen())
.count();
return new SessionStats(
sessions.size(),
activeSessions,
lastActivity.size()
);
}
}
🎊 总结:实时推送的未来趋势
写到这里,我的手指都快敲断了!😅 回顾这一路走来,从Ajax轮询的痛苦,到WebSocket的惊喜,再到现在的大规模应用,技术的发展真是让人感慨万千!
🌟 WebSocket带来的变革
用户体验的革命: 真正的实时通信让用户体验上了一个台阶!聊天不再有延迟,游戏操作更加流畅,协作应用更加顺滑!✨
开发效率的提升: Spring Boot的WebSocket支持让我们可以快速构建实时应用,不用再为各种兼容性问题头疼!🚀
系统架构的进化: 从传统的请求-响应模式到事件驱动的架构,WebSocket推动了整个系统设计思维的转变!🔄
🚀 学习路径建议
对于想要掌握WebSocket的小伙伴们,我的建议是:
- 先理解协议 🤝 - WebSocket是什么,解决了什么问题
- 动手实践 💻 - 从简单的聊天室开始,一步步增加功能
- 深入原理 🔍 - 了解底层实现,掌握性能优化技巧
- 项目实战 🏗️ - 在真实项目中应用,积累经验
- 持续学习 📚 - 关注新技术发展,如WebRTC、HTTP/3等
🔮 技术趋势展望
HTTP/3和QUIC: 未来可能会有更好的传输协议,但WebSocket的编程模型依然有价值!🌐
边缘计算: WebSocket结合边缘计算,可以进一步降低延迟,提供更好的用户体验!⚡
AI集成: 智能消息路由、自动翻译、情感分析等AI功能将让实时通信更加智能!🤖
💝 最后的话
技术的魅力在于它能解决实际问题,WebSocket不仅仅是一个协议,更是我们构建现代Web应用的重要工具!希望这篇文章能帮助大家更好地理解和应用WebSocket技术!🎉
记住,没有完美的技术,只有合适的场景。选择技术方案时,要根据具体需求来决定,不要为了技术而技术!实用才是王道!💪
愿每一个开发者都能在实时通信的世界里找到属于自己的乐趣!如果你有任何问题或想法,欢迎随时来找我讨论!让我们一起在技术的海洋中乘风破浪!🏄♂️
📚 参考资源:
- WebSocket RFC 6455规范
- Spring WebSocket官方文档
- STOMP协议规范
- Redis发布订阅机制
- 个人大规模WebSocket部署经验
代码改变世界,实时连接你我! 🌍✨
🧧福利赠与你🧧
无论你是计算机专业的学生,还是对编程有兴趣的小伙伴,都建议直接毫无顾忌的学习此专栏「滚雪球学SpringBoot」,bug菌郑重承诺,凡是学习此专栏的同学,均能获取到所需的知识和技能,全网最快速入门SpringBoot,就像滚雪球一样,越滚越大, 无边无际,指数级提升。
最后,如果这篇文章对你有所帮助,帮忙给作者来个一键三连,关注、点赞、收藏,您的支持就是我坚持写作最大的动力。
同时欢迎大家关注公众号:「猿圈奇妙屋」 ,以便学习更多同类型的技术文章,免费白嫖最新BAT互联网公司面试题、4000G PDF编程电子书、简历模板、技术文章Markdown文档等海量资料。
ps:本文涉及所有源代码,均已上传至Gitee开源,供同学们一对一参考 Gitee传送门,同时,原创开源不易,欢迎给个star🌟,想体验下被🌟的感jio,非常感谢❗
✨️ Who am I?
我是bug菌,CSDN | 掘金 | InfoQ | 51CTO | 华为云 | 阿里云 | 腾讯云 等社区博客专家,C站博客之星Top30,华为云多年度十佳博主及影响力最佳博主,掘金多年度人气作者Top40,掘金等各大社区平台签约作者,51CTO年度博主Top12,掘金/InfoQ/51CTO等社区优质创作者;全网粉丝合计 30w+;更多精彩福利点击这里;硬核微信公众号「猿圈奇妙屋」,欢迎你的加入!免费白嫖最新BAT互联网公司面试真题、4000G PDF电子书籍、简历模板等海量资料,你想要的我都有,关键是你不来拿。

-End-