feat(websocket): 实现基于Redis的消息队列WebSocket通信功能

- 添加Redis消息队列配置类RedisMessageConfig,实现消息监听容器
- 重构KitWsMessage消息结构,将数据字段改为内部Data类并添加getter/setter
- 集成RedisTemplate实现WebSocket消息发布功能,支持主动推送和实时聊天
- 实现WsMessageSubscriber消息订阅者,处理来自Redis的消息队列数据
- 添加WebSocket业务代码常量接口KitWsBizCodeConst
- 更新前端Demo组件适配新的消息格式和业务代码参数
- 优化WebSocket服务器端消息处理逻辑和错误处理机制
This commit is contained in:
2026-02-25 13:18:01 +08:00
parent ae0c0482cf
commit d74bcd5337
13 changed files with 454 additions and 54 deletions
@@ -0,0 +1,60 @@
/*
* Copyright 2021-2026 Odboy
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.odboy.framework.redis;
import cn.odboy.framework.websocket.WsMessageSubscriber;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.listener.ChannelTopic;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import org.springframework.data.redis.listener.adapter.MessageListenerAdapter;
/**
* Redis消息队列配置
*
* @author odboy
*/
@Configuration
public class RedisMessageConfig {
/**
* 消息监听容器
*
* @param connectionFactory /
* @param listenerAdapter /
* @return /
*/
@Bean
public RedisMessageListenerContainer container(RedisConnectionFactory connectionFactory, MessageListenerAdapter listenerAdapter) {
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
container.setConnectionFactory(connectionFactory);
// 指定监听的主题
container.addMessageListener(listenerAdapter, new ChannelTopic("WebSocketMessage"));
return container;
}
/**
* 将订阅者与消息处理方法绑定
*
* @param subscriber /
* @return /
*/
@Bean
public MessageListenerAdapter listenerAdapter(WsMessageSubscriber subscriber) {
return new MessageListenerAdapter(subscriber, "onMessage");
}
}
@@ -0,0 +1,27 @@
/*
* Copyright 2021-2026 Odboy
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.odboy.framework.websocket;
/**
* WebSocket业务代码常量
*
* @author odboy
*/
public interface KitWsBizCodeConst {
String AutomaticPush = "100"; // 主动推送
String IM = "101"; // 实时聊天
}
@@ -0,0 +1,55 @@
/*
* Copyright 2021-2026 Odboy
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.odboy.framework.websocket;
import cn.odboy.framework.websocket.context.KitWsMessage;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
@Slf4j
@Component
public class KitWsMessagePublisher {
@Autowired
private RedisTemplate<Object, Object> redisTemplate;
private KitWsMessage innerBuild(String bizCode, String fromUser, String toUser, String message) {
KitWsMessage wsMessage = new KitWsMessage();
wsMessage.setBizCode(bizCode);
KitWsMessage.Data data = new KitWsMessage.Data();
data.setFormUser(fromUser);
data.setToUser(toUser);
data.setMessage(message);
wsMessage.setData(data);
return wsMessage;
}
public void automaticPush(String fromUser, String toUser, String message) {
KitWsMessage wsMessage = this.innerBuild(KitWsBizCodeConst.AutomaticPush, fromUser, toUser, message);
redisTemplate.convertAndSend("WebSocketMessage", wsMessage);
log.info("发送AutomaticPush消息");
}
public void imPush(String fromUser, String toUser, String message) {
KitWsMessage wsMessage = this.innerBuild(KitWsBizCodeConst.IM, fromUser, toUser, message);
redisTemplate.convertAndSend("WebSocketMessage", wsMessage);
log.info("发送IM消息");
}
}
@@ -0,0 +1,51 @@
/*
* Copyright 2021-2026 Odboy
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.odboy.framework.websocket;
import cn.odboy.framework.websocket.context.KitWsMessage;
import cn.odboy.framework.websocket.context.KitWsServer;
import com.alibaba.fastjson2.JSON;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* Ws消息订阅
*
* @author odboy
*/
@Slf4j
@Component
public class WsMessageSubscriber {
@Autowired
private KitWsServer kitWsServer;
// /**
// * 无效写法
// */
// public void onMessage(KitWsMessage message) {
// log.info("收到来自redis消息队列的数据, {}", JSON.toJSONString(message));
// KitWsMessage.Data data = message.getData();
// kitWsServer.sendMessage(data.getMessage(), data.getToUsername());
// }
public void onMessage(String message, String channel) {
log.info("收到来自redis消息队列的message={}, channel={}", JSON.toJSONString(message), channel);
KitWsMessage wsMessage = JSON.parseObject(message, KitWsMessage.class);
kitWsServer.sendMessage(wsMessage.getData().getMessage(), wsMessage.getData().getToUser());
}
}
@@ -25,10 +25,14 @@ import lombok.Setter;
public class KitWsMessage extends KitObject {
private String bizCode;
private String data;
private Data data;
public KitWsMessage(String bizCode, String data) {
this.bizCode = bizCode;
this.data = data;
@Getter
@Setter
public static class Data extends KitObject {
private String formUser;
private String toUser;
private String message;
}
}
@@ -13,23 +13,22 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.odboy.framework.websocket.context;
import cn.hutool.core.util.StrUtil;
import cn.odboy.framework.context.KitSpringBeanHolder;
import cn.odboy.framework.websocket.KitWsMessagePublisher;
import com.alibaba.fastjson2.JSON;
import jakarta.websocket.OnClose;
import jakarta.websocket.OnError;
import jakarta.websocket.OnMessage;
import jakarta.websocket.OnOpen;
import jakarta.websocket.Session;
import jakarta.websocket.*;
import jakarta.websocket.server.PathParam;
import jakarta.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.Objects;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.util.Objects;
@Slf4j
@Component
@ServerEndpoint("/websocket/{sid}")
@@ -51,7 +50,6 @@ public class KitWsServer {
@OnOpen
public void onOpen(Session session, @PathParam("sid") String sid) {
this.session = session;
// {username}#{bizCode}#{contextParams}
this.sid = sid;
KitWsClientManager.addClient(sid, this);
}
@@ -64,10 +62,12 @@ public class KitWsServer {
@OnMessage
public void onMessage(String message, Session session) throws IOException {
KitWsMessage wsMessage = JSON.parseObject(message, KitWsMessage.class);
String bizCode = wsMessage.getBizCode();
Object data = wsMessage.getData();
log.info("收到来 sid={} 的信息: message={}, bizCode={}, data={}", sid, message, bizCode, JSON.toJSONString(data));
session.getBasicRemote().sendText(message);
// String bizCode = wsMessage.getBizCode();
// Object data = wsMessage.getData();
log.info("收到来 sid={} 的信息: message={}", sid, message);
// session.getBasicRemote().sendText(message);
KitWsMessagePublisher messagePublisher = KitSpringBeanHolder.getBean(KitWsMessagePublisher.class);
messagePublisher.imPush(wsMessage.getData().getFormUser(), wsMessage.getData().getToUser(), wsMessage.getData().getMessage());
}
/**
@@ -84,7 +84,7 @@ public class KitWsServer {
@OnError
public void onError(Session session, Throwable error) {
// log.error("WebSocket sid={} 发生错误", this.sid, error);
log.error("WebSocket sid={} 发生错误", this.sid, error);
try {
KitWsClientManager.removeClient(this.sid);
} catch (Exception e) {
@@ -102,26 +102,26 @@ public class KitWsServer {
/**
* 精准推送消息
*/
public void sendMessage(KitWsMessage message, @PathParam("sid") String sid) throws IOException {
String body = JSON.toJSONString(message);
// log.info("推送消息到{}, 推送内容:{}", sid, message);
public void sendMessage(String message, @PathParam("sid") String sid) {
log.info("推送消息到{}, 推送内容:{}", sid, message);
try {
if (sid == null) {
sendToAll(body);
if (StrUtil.isBlank(sid)) {
sendToAll(message);
} else {
KitWsServer wsClient = KitWsClientManager.getClientBySid(sid);
if (wsClient != null) {
wsClient.innerSendMessage(body);
wsClient.innerSendMessage(message);
}
}
} catch (Exception ignored) {
} catch (IOException e) {
log.warn("推送消息到客户端失败", e);
}
}
/**
* 群发消息
*/
public void sendToAll(String message) {
private void sendToAll(String message) {
for (KitWsServer item : KitWsClientManager.getAllClient()) {
try {
item.innerSendMessage(message);