一、为什么需要WebSocket组件?
- 实时通信需求
- 传统HTTP轮询效率低,WebSocket提供全双工通信
- 适用于即时聊天、实时数据监控、协同编辑等场景
- 分布式系统挑战
- 多节点部署时需解决会话同步问题
- 跨节点消息广播需借助中间件(Redis/RocketMQ等)
- 统一管理诉求
- 会话管理、安全认证、消息路由等共性功能需抽象
二、组件核心功能全景
功能模块 | 实现要点 |
---|---|
会话管理 | 按用户维度存储WebSocketSession,支持按ID/用户类型/用户ID查询 |
消息路由 | 根据消息类型分发到对应处理器(WebSocketMessageListener) |
消息广播 | 支持本地/Redis/RocketMQ等多种发送策略,适配不同集群规模 |
安全认证 | 握手阶段Token验证,用户信息绑定到Session |
并发控制 | 使用ConcurrentWebSocketSessionDecorator防止并发发送异常 |
三、组件工作全流程图解
1. 连接建立流程
前端 LoginUserHandshakeInterceptor WebSocketSession WebSocketSessionHandlerDecorator WebSocketSessionManagerImpl 发起连接?token=xxx 解析Token获取LoginUser setAttribute(LOGIN_USER) addSession(session) 存储到idSessions/userSessions 前端 LoginUserHandshakeInterceptor WebSocketSession WebSocketSessionHandlerDecorator WebSocketSessionManagerImpl
2. 消息接收处理流程
前端 JsonWebSocketMessageHandler WebSocketMessageListener 发送消息 解析消息类型(type) 忽略/返回错误 根据type查找监听器 记录错误日志 触发onMessage() 执行业务逻辑 alt [未找到该消费类型的监听器] [找到该消费类型的监听器] alt [无效消息] [有效消息] 前端 JsonWebSocketMessageHandler WebSocketMessageListener
3.消息发送流程(以RocketMQ为例)
业务代码 WebSocketMessageSender RocketMQWebSocketMessageSender RocketMQTemplate RocketMQ RocketMQWebSocketMessageConsumer AbstractWebSocketMessageSender WebSocketSession sendObject() send() 发送RocketMQ消息 发布到指定topic 订阅消费消息 doSend() 遍历会话发送 业务代码 WebSocketMessageSender RocketMQWebSocketMessageSender RocketMQTemplate RocketMQ RocketMQWebSocketMessageConsumer AbstractWebSocketMessageSender WebSocketSession
四、四步构建WebSocket核心链路
步骤一:连接建立(认证与会话管理)
相关类
WebSocketFrameworkUtils
java
import org.springframework.web.socket.WebSocketSession;
import java.util.Map;
/**
* 专属于 web 包的工具类
*
* @author dyh
*/
public class WebSocketFrameworkUtils {
public static final String ATTRIBUTE_LOGIN_USER = "LOGIN_USER";
/**
* 设置当前用户
*
* @param loginUser 登录用户
* @param attributes Session
*/
public static void setLoginUser(LoginUser loginUser, Map<String, Object> attributes) {
attributes.put(ATTRIBUTE_LOGIN_USER, loginUser);
}
/**
* 获取当前用户
*
* @return 当前用户
*/
public static LoginUser getLoginUser(WebSocketSession session) {
return (LoginUser) session.getAttributes().get(ATTRIBUTE_LOGIN_USER);
}
/**
* 获得当前用户的编号
*
* @return 用户编号
*/
public static Long getLoginUserId(WebSocketSession session) {
LoginUser loginUser = getLoginUser(session);
return loginUser != null ? loginUser.getId() : null;
}
/**
* 获得当前用户的类型
*
* @return 用户编号
*/
public static Integer getLoginUserType(WebSocketSession session) {
LoginUser loginUser = getLoginUser(session);
return loginUser != null ? loginUser.getUserType() : null;
}
/**
* 获得当前用户的租户编号
*
* @param session Session
* @return 租户编号
*/
public static Long getTenantId(WebSocketSession session) {
LoginUser loginUser = getLoginUser(session);
return loginUser != null ? loginUser.getTenantId() : null;
}
}
JsonWebSocketMessage
java
/**
* JSON 格式的 WebSocket 消息帧
*
* @author dyh
*/
@Data
public class JsonWebSocketMessage implements Serializable {
/**
* 消息类型
*
* 目的:用于分发到对应的 {@link WebSocketMessageListener} 实现类
*/
private String type;
/**
* 消息内容
*
* 要求 JSON 对象
*/
private String content;
}
涉及核心类
设置用户属性 注册会话 LoginUserHandshakeInterceptor WebSocketSessionHandlerDecorator WebSocketSessionManagerImpl WebSocketSession
实现步骤
1. 握手拦截器(认证用户)
LoginUserHandshakeInterceptor
java
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.server.HandshakeInterceptor;
import java.util.Map;
/**
* 登录用户的 {@link HandshakeInterceptor} 实现类
*
* 流程如下:
* 1. 前端连接 websocket 时,会通过拼接 ?token={token} 到 ws:// 连接后,这样它可以被 {@link TokenAuthenticationFilter} 所认证通过
* 2. {@link LoginUserHandshakeInterceptor} 负责把 {@link LoginUser} 添加到 {@link WebSocketSession} 中
*
* @author dyh
*/
public class LoginUserHandshakeInterceptor implements HandshakeInterceptor {
@Override
public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response,
WebSocketHandler wsHandler, Map<String, Object> attributes) {
LoginUser loginUser = SecurityFrameworkUtils.getLoginUser();
if (loginUser != null) {
WebSocketFrameworkUtils.setLoginUser(loginUser, attributes);
}
return true;
}
@Override
public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response,
WebSocketHandler wsHandler, Exception exception) {
// do nothing
}
}
2. 会话装饰器(线程安全增强)
WebSocketSessionHandlerDecorator
java
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.ConcurrentWebSocketSessionDecorator;
import org.springframework.web.socket.handler.WebSocketHandlerDecorator;
/**
* {@link WebSocketHandler} 的装饰类,实现了以下功能:
*
* 1. {@link WebSocketSession} 连接或关闭时,使用 {@link #sessionManager} 进行管理
* 2. 封装 {@link WebSocketSession} 支持并发操作
*
* @author dyh
*/
public class WebSocketSessionHandlerDecorator extends WebSocketHandlerDecorator {
/**
* 发送时间的限制,单位:毫秒
*/
private static final Integer SEND_TIME_LIMIT = 1000 * 5;
/**
* 发送消息缓冲上线,单位:bytes
*/
private static final Integer BUFFER_SIZE_LIMIT = 1024 * 100;
private final WebSocketSessionManager sessionManager;
public WebSocketSessionHandlerDecorator(WebSocketHandler delegate,
WebSocketSessionManager sessionManager) {
super(delegate);
this.sessionManager = sessionManager;
}
@Override
public void afterConnectionEstablished(WebSocketSession session) {
// 实现 session 支持并发
session = new ConcurrentWebSocketSessionDecorator(session, SEND_TIME_LIMIT, BUFFER_SIZE_LIMIT);
// 添加到 WebSocketSessionManager 中
sessionManager.addSession(session);
}
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus closeStatus) {
sessionManager.removeSession(session);
}
}
3. 会话管理器(存储结构)
WebSocketSessionManager
java
import org.springframework.web.socket.WebSocketSession;
import java.util.Collection;
/**
* {@link WebSocketSession} 管理器的接口
*
* @author dyh
*/
public interface WebSocketSessionManager {
/**
* 添加 Session
*
* @param session Session
*/
void addSession(WebSocketSession session);
/**
* 移除 Session
*
* @param session Session
*/
void removeSession(WebSocketSession session);
/**
* 获得指定编号的 Session
*
* @param id Session 编号
* @return Session
*/
WebSocketSession getSession(String id);
/**
* 获得指定用户类型的 Session 列表
*
* @param userType 用户类型
* @return Session 列表
*/
Collection<WebSocketSession> getSessionList(Integer userType);
/**
* 获得指定用户编号的 Session 列表
*
* @param userType 用户类型
* @param userId 用户编号
* @return Session 列表
*/
Collection<WebSocketSession> getSessionList(Integer userType, Long userId);
}
WebSocketSessionManagerImpl
java
import org.springframework.web.socket.WebSocketSession;
import cn.hutool.core.collection.CollUtil;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.CopyOnWriteArrayList;
/**
* 默认的 {@link WebSocketSessionManager} 实现类
* 负责管理WebSocket会话,包含双重存储结构:
* 1. 基于会话ID的快速查找
* 2. 基于用户类型+用户ID的分层存储
* @author dyh
*/
public class WebSocketSessionManagerImpl implements WebSocketSessionManager {
/**
* 会话ID与WebSocketSession的映射关系(线程安全)
* Key格式:Session ID(WebSocket自动生成的唯一标识)
* Value:对应的WebSocketSession对象
*/
private final ConcurrentMap<String, WebSocketSession> idSessions = new ConcurrentHashMap<>();
/**
* 用户维度存储WebSocketSession(三级嵌套线程安全结构)
* Key1:用户类型(Integer,如1-管理员 2-普通用户)
* Key2:用户ID(Long类型)
* Value:该用户的所有WebSocket会话列表(线程安全的CopyOnWriteArrayList)
*/
private final ConcurrentMap<Integer, ConcurrentMap<Long, CopyOnWriteArrayList<WebSocketSession>>> userSessions = new ConcurrentHashMap<>();
@Override
public void addSession(WebSocketSession session) {
// 1. 加入ID索引
idSessions.put(session.getId(), session);
// 2. 加入用户维度索引
LoginUser user = WebSocketFrameworkUtils.getLoginUser(session);
if (user == null) return; // 未认证会话不记录用户维度
// 2.1 获取或创建用户类型层(双重检查锁模式)
userSessions.computeIfAbsent(user.getUserType(), k -> new ConcurrentHashMap<>())
// 2.2 获取或创建用户ID层
.computeIfAbsent(user.getId(), k -> new CopyOnWriteArrayList<>())
// 2.3 添加会话到列表(线程安全操作)
.add(session);
}
@Override
public void removeSession(WebSocketSession session) {
// 1. 从ID索引移除
idSessions.remove(session.getId());
// 2. 从用户维度索引移除
LoginUser user = WebSocketFrameworkUtils.getLoginUser(session);
if (user == null) return;
userSessions.computeIfPresent(user.getUserType(), (userType, userMap) -> {
userMap.computeIfPresent(user.getId(), (userId, sessions) -> {
// 2.1 移除指定会话(根据ID精准匹配)
sessions.removeIf(s -> s.getId().equals(session.getId()));
// 2.2 列表为空时自动清理用户ID层
return sessions.isEmpty() ? null : sessions;
});
// 2.3 用户ID层空时自动清理用户类型层
return userMap.isEmpty() ? null : userMap;
});
}
@Override
public WebSocketSession getSession(String id) {
return idSessions.get(id); // O(1)时间复杂度直接查找
}
@Override
public Collection<WebSocketSession> getSessionList(Integer userType) {
// 1. 获取指定用户类型的所有会话映射
ConcurrentMap<Long, CopyOnWriteArrayList<WebSocketSession>> userMap = userSessions.get(userType);
if (CollUtil.isEmpty(userMap)) return Collections.emptyList();
// 2. 多租户过滤处理
Long currentTenantId = TenantContextHolder.getTenantId();
LinkedList<WebSocketSession> result = new LinkedList<>(); // 避免ArrayList扩容开销
userMap.values().forEach(sessions -> {
if (CollUtil.isEmpty(sessions)) return;
// 2.1 租户隔离检查(第一个会话代表用户所属租户)
Long userTenantId = WebSocketFrameworkUtils.getTenantId(sessions.get(0));
if (currentTenantId != null && !currentTenantId.equals(userTenantId)) return;
result.addAll(sessions);
});
return result;
}
@Override
public Collection<WebSocketSession> getSessionList(Integer userType, Long userId) {
// 直接获取指定用户的所有会话(带租户上下文过滤)
return Optional.ofNullable(userSessions.get(userType))
.map(userMap -> userMap.get(userId))
.filter(CollUtil::isNotEmpty)
.map(ArrayList::new) // 返回拷贝避免直接操作内部集合
.orElseGet(ArrayList::new);
}
}
设计模式应用
- 装饰器模式:ConcurrentWebSocketSessionDecorator增强原生Session的并发能力
- 工厂方法模式:通过拦截器创建带有用户属性的Session对象
步骤二:接收消息(路由与处理)
涉及核心类
路由调用 实现 JsonWebSocketMessageHandler -Map listeners +handleTextMessage() <<interface>> WebSocketMessageListener +onMessage() +getType() WebSocketMessageListener具体实现类 +onMessage() +getType()
实现步骤
1. 消息处理器(路由分发)
JsonWebSocketMessageHandler
java
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.TextWebSocketHandler;
import cn.hutool.core.util.StrUtil;
import cn.hutool.core.util.TypeUtil;
import java.lang.reflect.Type;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.function.Consumer;
/**
* JSON 格式 {@link WebSocketHandler} 实现类
*
* 基于 {@link JsonWebSocketMessage#getType()} 消息类型,调度到对应的 {@link WebSocketMessageListener} 监听器。
*
* @author dyh
*/
@Slf4j
public class JsonWebSocketMessageHandler extends TextWebSocketHandler {
/**
* type 与 WebSocketMessageListener 的映射
*/
private final Map<String, WebSocketMessageListener<Object>> listeners = new HashMap<>();
@SuppressWarnings({"rawtypes", "unchecked"})
public JsonWebSocketMessageHandler(List<? extends WebSocketMessageListener> listenersList) {
listenersList.forEach((Consumer<WebSocketMessageListener>)
listener -> listeners.put(listener.getType(), listener));
}
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
// 1.1 空消息,跳过
if (message.getPayloadLength() == 0) {
return;
}
// 1.2 ping 心跳消息,直接返回 pong 消息。
if (message.getPayloadLength() == 4 && Objects.equals(message.getPayload(), "ping")) {
session.sendMessage(new TextMessage("pong"));
return;
}
// 2.1 解析消息
try {
JsonWebSocketMessage jsonMessage = JsonUtils.parseObject(message.getPayload(), JsonWebSocketMessage.class);
if (jsonMessage == null) {
log.error("[handleTextMessage][session({}) message({}) 解析为空]", session.getId(), message.getPayload());
return;
}
if (StrUtil.isEmpty(jsonMessage.getType())) {
log.error("[handleTextMessage][session({}) message({}) 类型为空]", session.getId(), message.getPayload());
return;
}
// 2.2 获得对应的 WebSocketMessageListener
WebSocketMessageListener<Object> messageListener = listeners.get(jsonMessage.getType());
if (messageListener == null) {
log.error("[handleTextMessage][session({}) message({}) 监听器为空]", session.getId(), message.getPayload());
return;
}
// 2.3 处理消息
Type type = TypeUtil.getTypeArgument(messageListener.getClass(), 0);
Object messageObj = JsonUtils.parseObject(jsonMessage.getContent(), type);
Long tenantId = WebSocketFrameworkUtils.getTenantId(session);
TenantUtils.execute(tenantId, () -> messageListener.onMessage(session, messageObj));
} catch (Throwable ex) {
log.error("[handleTextMessage][session({}) message({}) 处理异常]", session.getId(), message.getPayload());
}
}
}
2. 定义监听器接口
WebSocketMessageListener
java
import org.springframework.web.socket.WebSocketSession;
/**
* WebSocket 消息监听器接口
*
* 目的:前端发送消息给后端后,处理对应 {@link #getType()} 类型的消息
*
* @param <T> 泛型,消息类型
* @author dyh
*/
public interface WebSocketMessageListener<T> {
/**
* 处理消息
*
* @param session Session
* @param message 消息
*/
void onMessage(WebSocketSession session, T message);
/**
* 获得消息类型
*
* @see JsonWebSocketMessage#getType()
* @return 消息类型
*/
String getType();
}
3. 业务监听器(使用示例)
注意
:这里提供的是使用示例,具体接收到前端消息处理逻辑请根据业务实现
OrderMessageListener
java
@Component
public class OrderMessageListener implements WebSocketMessageListener<OrderMessage> {
@Override
public String getType() { return "order-msg"; }
@Override
public void onMessage(WebSocketSession session, OrderMessage message) {
orderService.process(message);
}
}
设计模式应用
- 策略模式:每个业务监听器对应一种消息处理策略
- 观察者模式:将消息类型与处理逻辑解耦,方便扩展新消息类型。
设计原则
- 依赖注入:Spring自动注入所有监听器实现
- 开闭原则:新增消息类型只需添加Listener实现类
- 单一职责:Handler只负责路由,Listener专注业务处理
步骤三:发送消息(策略化广播)
注意
:此处只给了rocketmq
消息策略,还可以自行实现redis
,kafka
,rabbitmq
策略
相关类
RocketMQWebSocketMessage
java
import lombok.Data;
/**
* RocketMQ 广播 WebSocket 的消息
*
* @author dyh
*/
@Data
public class RocketMQWebSocketMessage {
/**
* Session 编号
*/
private String sessionId;
/**
* 用户类型
*/
private Integer userType;
/**
* 用户编号
*/
private Long userId;
/**
* 消息类型
*/
private String messageType;
/**
* 消息内容
*/
private String messageContent;
}
涉及核心类
触发实际发送 <<interface>> WebSocketMessageSender +send(Integer userType, Long userId, String type, String content) : void AbstractWebSocketMessageSender -WebSocketSessionManager sessionManager +send(...) : void #doSend(...) : void RocketMQWebSocketMessageSender -RocketMQTemplate rocketMQTemplate +doSend(...) : void RedisWebSocketMessageSender -RedisMQTemplate redisMQTemplate +doSend(...) : void RocketMQWebSocketMessageConsumer +onMessage(RocketMQWebSocketMessage message) : void
实现步骤
1. 定义发送器接口与抽象类
WebSocketMessageSender
java
/**
* WebSocket 消息的发送器接口
*
* @author dyh
*/
public interface WebSocketMessageSender {
/**
* 发送消息给指定用户
*
* @param userType 用户类型
* @param userId 用户编号
* @param messageType 消息类型
* @param messageContent 消息内容,JSON 格式
*/
void send(Integer userType, Long userId, String messageType, String messageContent);
/**
* 发送消息给指定用户类型
*
* @param userType 用户类型
* @param messageType 消息类型
* @param messageContent 消息内容,JSON 格式
*/
void send(Integer userType, String messageType, String messageContent);
/**
* 发送消息给指定 Session
*
* @param sessionId Session 编号
* @param messageType 消息类型
* @param messageContent 消息内容,JSON 格式
*/
void send(String sessionId, String messageType, String messageContent);
default void sendObject(Integer userType, Long userId, String messageType, Object messageContent) {
send(userType, userId, messageType, JsonUtils.toJsonString(messageContent));
}
default void sendObject(Integer userType, String messageType, Object messageContent) {
send(userType, messageType, JsonUtils.toJsonString(messageContent));
}
default void sendObject(String sessionId, String messageType, Object messageContent) {
send(sessionId, messageType, JsonUtils.toJsonString(messageContent));
}
}
AbstractWebSocketMessageSender
java
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import java.io.IOException;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
/**
* WebSocketMessageSender 实现类
*
* @author dyh
*/
@Slf4j
@RequiredArgsConstructor
public abstract class AbstractWebSocketMessageSender implements WebSocketMessageSender {
private final WebSocketSessionManager sessionManager;
@Override
public void send(Integer userType, Long userId, String messageType, String messageContent) {
send(null, userType, userId, messageType, messageContent);
}
@Override
public void send(Integer userType, String messageType, String messageContent) {
send(null, userType, null, messageType, messageContent);
}
@Override
public void send(String sessionId, String messageType, String messageContent) {
send(sessionId, null, null, messageType, messageContent);
}
/**
* 发送消息
*
* @param sessionId Session 编号
* @param userType 用户类型
* @param userId 用户编号
* @param messageType 消息类型
* @param messageContent 消息内容
*/
public void send(String sessionId, Integer userType, Long userId, String messageType, String messageContent) {
// 1. 获得 Session 列表
List<WebSocketSession> sessions = Collections.emptyList();
if (StrUtil.isNotEmpty(sessionId)) {
WebSocketSession session = sessionManager.getSession(sessionId);
if (session != null) {
sessions = Collections.singletonList(session);
}
} else if (userType != null && userId != null) {
sessions = (List<WebSocketSession>) sessionManager.getSessionList(userType, userId);
} else if (userType != null) {
sessions = (List<WebSocketSession>) sessionManager.getSessionList(userType);
}
if (CollUtil.isEmpty(sessions)) {
if (log.isDebugEnabled()) {
log.debug("[send][sessionId({}) userType({}) userId({}) messageType({}) messageContent({}) 未匹配到会话]",
sessionId, userType, userId, messageType, messageContent);
}
}
// 2. 执行发送
doSend(sessions, messageType, messageContent);
}
/**
* 发送消息的具体实现
*
* @param sessions Session 列表
* @param messageType 消息类型
* @param messageContent 消息内容
*/
public void doSend(Collection<WebSocketSession> sessions, String messageType, String messageContent) {
JsonWebSocketMessage message = new JsonWebSocketMessage().setType(messageType).setContent(messageContent);
String payload = JsonUtils.toJsonString(message); // 关键,使用 JSON 序列化
sessions.forEach(session -> {
// 1. 各种校验,保证 Session 可以被发送
if (session == null) {
log.error("[doSend][session 为空, message({})]", message);
return;
}
if (!session.isOpen()) {
log.error("[doSend][session({}) 已关闭, message({})]", session.getId(), message);
return;
}
// 2. 执行发送
try {
session.sendMessage(new TextMessage(payload));
log.info("[doSend][session({}) 发送消息成功,message({})]", session.getId(), message);
} catch (IOException ex) {
log.error("[doSend][session({}) 发送消息失败,message({})]", session.getId(), message, ex);
}
});
}
}
2. 实现具体策略(以RocketMQ为例)
RocketMQWebSocketMessageSender
java
import lombok.extern.slf4j.Slf4j;
import org.apache.rocketmq.spring.core.RocketMQTemplate;
/**
* 基于 RocketMQ 的 {@link WebSocketMessageSender} 实现类
*
* @author dyh
*/
@Slf4j
public class RocketMQWebSocketMessageSender extends AbstractWebSocketMessageSender {
private final RocketMQTemplate rocketMQTemplate;
private final String topic;
public RocketMQWebSocketMessageSender(WebSocketSessionManager sessionManager,
RocketMQTemplate rocketMQTemplate,
String topic) {
super(sessionManager);
this.rocketMQTemplate = rocketMQTemplate;
this.topic = topic;
}
@Override
public void send(Integer userType, Long userId, String messageType, String messageContent) {
sendRocketMQMessage(null, userId, userType, messageType, messageContent);
}
@Override
public void send(Integer userType, String messageType, String messageContent) {
sendRocketMQMessage(null, null, userType, messageType, messageContent);
}
@Override
public void send(String sessionId, String messageType, String messageContent) {
sendRocketMQMessage(sessionId, null, null, messageType, messageContent);
}
/**
* 通过 RocketMQ 广播消息
*
* @param sessionId Session 编号
* @param userId 用户编号
* @param userType 用户类型
* @param messageType 消息类型
* @param messageContent 消息内容
*/
private void sendRocketMQMessage(String sessionId, Long userId, Integer userType,
String messageType, String messageContent) {
RocketMQWebSocketMessage mqMessage = new RocketMQWebSocketMessage()
.setSessionId(sessionId).setUserId(userId).setUserType(userType)
.setMessageType(messageType).setMessageContent(messageContent);
rocketMQTemplate.syncSend(topic, mqMessage);
}
}
3. 消息消费者处理广播
RocketMQWebSocketMessageConsumer
java
import lombok.RequiredArgsConstructor;
import org.apache.rocketmq.spring.annotation.MessageModel;
import org.apache.rocketmq.spring.annotation.RocketMQMessageListener;
import org.apache.rocketmq.spring.core.RocketMQListener;
/**
* {@link RocketMQWebSocketMessage} 广播消息的消费者,真正把消息发送出去
*
* @author dyh
*/
@RocketMQMessageListener( // 重点:添加 @RocketMQMessageListener 注解,声明消费的 topic
topic = "${dyh.websocket.sender-rocketmq.topic}",
consumerGroup = "${dyh.websocket.sender-rocketmq.consumer-group}",
messageModel = MessageModel.BROADCASTING // 设置为广播模式,保证每个实例都能收到消息
)
@RequiredArgsConstructor
public class RocketMQWebSocketMessageConsumer implements RocketMQListener<RocketMQWebSocketMessage> {
private final RocketMQWebSocketMessageSender rocketMQWebSocketMessageSender;
@Override
public void onMessage(RocketMQWebSocketMessage message) {
rocketMQWebSocketMessageSender.send(message.getSessionId(),
message.getUserType(), message.getUserId(),
message.getMessageType(), message.getMessageContent());
}
}
设计模式应用
- 策略模式:WebSocketMessageSender 接口统一发送策略,RedisWebSocketMessageSender 和 RocketMQWebSocketMessageSender 实现不同策略。
- 模板方法模式:AbstractWebSocketMessageSender
设计原则
- 开闭原则:新增发送策略只需继承AbstractWebSocketMessageSender抽象类,无需修改核心逻辑
- 依赖倒置:高层模块依赖WebSocketMessageSender抽象,不关心具体实现
步骤四:Spring 自动装配
相关类
WebSocketProperties
java
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.validation.annotation.Validated;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
/**
* WebSocket 配置项
*
* @author dyh
*/
@ConfigurationProperties("dyh.websocket")
@Data
@Validated
public class WebSocketProperties {
/**
* WebSocket 的连接路径
*/
@NotEmpty(message = "WebSocket 的连接路径不能为空")
private String path = "/ws";
/**
* 消息发送器的类型
*
* 可选值:local、redis、rocketmq、kafka、rabbitmq
*/
@NotNull(message = "WebSocket 的消息发送者不能为空")
private String senderType = "local";
}
WebSocket 自动配置
DyhWebSocketAutoConfiguration
java
import org.apache.rocketmq.spring.core.RocketMQTemplate;
import org.springframework.amqp.core.TopicExchange;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.server.HandshakeInterceptor;
import java.util.List;
/**
* WebSocket 自动配置
*
* @author dyh
*/
@AutoConfiguration(before = DyhRedisMQConsumerAutoConfiguration.class) // before DyhRedisMQConsumerAutoConfiguration 的原因是,需要保证 RedisWebSocketMessageConsumer 先创建,才能创建 RedisMessageListenerContainer
@EnableWebSocket // 开启 websocket
@ConditionalOnProperty(prefix = "dyh.websocket", value = "enable", matchIfMissing = true) // 允许使用 dyh.websocket.enable=false 禁用 websocket
@EnableConfigurationProperties(WebSocketProperties.class)
public class DyhWebSocketAutoConfiguration {
@Bean
public WebSocketConfigurer webSocketConfigurer(HandshakeInterceptor[] handshakeInterceptors,
WebSocketHandler webSocketHandler,
WebSocketProperties webSocketProperties) {
return registry -> registry
// 添加 WebSocketHandler
.addHandler(webSocketHandler, webSocketProperties.getPath())
.addInterceptors(handshakeInterceptors)
// 允许跨域,否则前端连接会直接断开
.setAllowedOriginPatterns("*");
}
@Bean
public HandshakeInterceptor handshakeInterceptor() {
return new LoginUserHandshakeInterceptor();
}
@Bean
public WebSocketHandler webSocketHandler(WebSocketSessionManager sessionManager,
List<? extends WebSocketMessageListener<?>> messageListeners) {
// 1. 创建 JsonWebSocketMessageHandler 对象,处理消息
JsonWebSocketMessageHandler messageHandler = new JsonWebSocketMessageHandler(messageListeners);
// 2. 创建 WebSocketSessionHandlerDecorator 对象,处理连接
return new WebSocketSessionHandlerDecorator(messageHandler, sessionManager);
}
@Bean
public WebSocketSessionManager webSocketSessionManager() {
return new WebSocketSessionManagerImpl();
}
@Bean
public WebSocketAuthorizeRequestsCustomizer webSocketAuthorizeRequestsCustomizer(WebSocketProperties webSocketProperties) {
return new WebSocketAuthorizeRequestsCustomizer(webSocketProperties);
}
// ==================== Sender 相关 ====================
@Configuration
@ConditionalOnProperty(prefix = "dyh.websocket", name = "sender-type", havingValue = "local")
public class LocalWebSocketMessageSenderConfiguration {
@Bean
public LocalWebSocketMessageSender localWebSocketMessageSender(WebSocketSessionManager sessionManager) {
return new LocalWebSocketMessageSender(sessionManager);
}
}
@Configuration
@ConditionalOnProperty(prefix = "dyh.websocket", name = "sender-type", havingValue = "redis")
public class RedisWebSocketMessageSenderConfiguration {
@Bean
public RedisWebSocketMessageSender redisWebSocketMessageSender(WebSocketSessionManager sessionManager,
RedisMQTemplate redisMQTemplate) {
return new RedisWebSocketMessageSender(sessionManager, redisMQTemplate);
}
@Bean
public RedisWebSocketMessageConsumer redisWebSocketMessageConsumer(
RedisWebSocketMessageSender redisWebSocketMessageSender) {
return new RedisWebSocketMessageConsumer(redisWebSocketMessageSender);
}
}
@Configuration
@ConditionalOnProperty(prefix = "dyh.websocket", name = "sender-type", havingValue = "rocketmq")
public class RocketMQWebSocketMessageSenderConfiguration {
@Bean
public RocketMQWebSocketMessageSender rocketMQWebSocketMessageSender(
WebSocketSessionManager sessionManager, RocketMQTemplate rocketMQTemplate,
@Value("${dyh.websocket.sender-rocketmq.topic}") String topic) {
return new RocketMQWebSocketMessageSender(sessionManager, rocketMQTemplate, topic);
}
@Bean
public RocketMQWebSocketMessageConsumer rocketMQWebSocketMessageConsumer(
RocketMQWebSocketMessageSender rocketMQWebSocketMessageSender) {
return new RocketMQWebSocketMessageConsumer(rocketMQWebSocketMessageSender);
}
}
@Configuration
@ConditionalOnProperty(prefix = "dyh.websocket", name = "sender-type", havingValue = "rabbitmq")
public class RabbitMQWebSocketMessageSenderConfiguration {
@Bean
public RabbitMQWebSocketMessageSender rabbitMQWebSocketMessageSender(
WebSocketSessionManager sessionManager, RabbitTemplate rabbitTemplate,
TopicExchange websocketTopicExchange) {
return new RabbitMQWebSocketMessageSender(sessionManager, rabbitTemplate, websocketTopicExchange);
}
@Bean
public RabbitMQWebSocketMessageConsumer rabbitMQWebSocketMessageConsumer(
RabbitMQWebSocketMessageSender rabbitMQWebSocketMessageSender) {
return new RabbitMQWebSocketMessageConsumer(rabbitMQWebSocketMessageSender);
}
/**
* 创建 Topic Exchange
*/
@Bean
public TopicExchange websocketTopicExchange(@Value("${dyh.websocket.sender-rabbitmq.exchange}") String exchange) {
return new TopicExchange(exchange,
true, // durable: 是否持久化
false); // exclusive: 是否排它
}
}
@Configuration
@ConditionalOnProperty(prefix = "dyh.websocket", name = "sender-type", havingValue = "kafka")
public class KafkaWebSocketMessageSenderConfiguration {
@Bean
public KafkaWebSocketMessageSender kafkaWebSocketMessageSender(
WebSocketSessionManager sessionManager, KafkaTemplate<Object, Object> kafkaTemplate,
@Value("${dyh.websocket.sender-kafka.topic}") String topic) {
return new KafkaWebSocketMessageSender(sessionManager, kafkaTemplate, topic);
}
@Bean
public KafkaWebSocketMessageConsumer kafkaWebSocketMessageConsumer(
KafkaWebSocketMessageSender kafkaWebSocketMessageSender) {
return new KafkaWebSocketMessageConsumer(kafkaWebSocketMessageSender);
}
}
}
五、使用指南(含代码示例)
1. 发送消息示例
NotifyController
java
@RestController
public class NotifyController {
@Autowired
private WebSocketMessageSender sender;
// 发送给指定用户
@PostMapping("/send")
public void sendToUser(@RequestParam Long userId) {
sender.sendObject(
UserTypeEnum.ADMIN.getValue(),
userId,
"demo-message",
new DemoMessage("Hello from server!")
);
}
}
2. 接收消息示例
DemoMessageListener
java
@Component
public class DemoMessageListener implements WebSocketMessageListener<DemoMessage> {
@Override
public String getType() {
return "demo-message"; // 声明处理的消息类型
}
@Override
public void onMessage(WebSocketSession session, DemoMessage message) {
// 1. 获取当前用户
Long userId = WebSocketFrameworkUtils.getLoginUserId(session);
// 2. 处理业务逻辑
System.out.println("收到来自用户:" + userId
+ " 的消息:" + message.getContent());
}
}
六、设计模式与开发原则实践
1. 经典设计模式
模式名称 | 应用场景 | 代码示例 |
---|---|---|
装饰器模式 | 增强WebSocketHandler的会话管理能力 | WebSocketSessionHandlerDecorator |
策略模式 | 可切换的消息发送策略(Local/Redis/RocketMQ) | WebSocketMessageSender多实现 |
观察者模式 | 消息类型与监听器的解耦 | WebSocketMessageListener+消息路由 |
模板方法 | 定义消息发送流程骨架 | AbstractWebSocketMessageSender#send() |
2. 开发原则体现
- 单一职责原则:会话管理、消息路由、安全认证模块分离
- 开闭原则:新增消息类型只需实现新Listener,无需修改核心逻辑
- 依赖倒置:高层模块依赖WebSocketMessageSender抽象,不关心具体实现
七、从组件设计中学到的经验
- 分层架构的价值
- 将会话管理、消息路由、网络传输分层处理,提高可维护性
- 通过抽象接口隔离变化点(如消息发送策略)
- 分布式设计要点
- 会话信息存储需考虑线程安全(ConcurrentHashMap)
- 广播消息需借助中间件实现最终一致性
- 扩展性设计技巧
- 使用Spring条件装配实现策略动态切换
- 监听器机制方便业务功能扩展
- 生产环境考量
- 心跳机制:内置Ping/Pong处理
- 流量控制:ConcurrentWebSocketSessionDecorator限制缓冲区大小