从零构建SSM框架的WebSocket实时通信:从依赖配置到集群部署
【免费下载链接】ssm 手把手教你整合最优雅SSM框架:SpringMVC + Spring + MyBatis 项目地址: https://gitcode.com/gh_mirrors/ss/ssm
引言:传统SSM架构的实时通信痛点与解决方案
你是否在使用SSM(SpringMVC + Spring + MyBatis)框架开发时遇到以下挑战:用户操作后需要手动刷新页面才能看到最新数据?订单状态变更无法实时通知?多人协作时数据同步存在延迟?本文将详细介绍如何在SSM架构中整合WebSocket技术,构建全双工实时通信通道,彻底解决这些问题。
读完本文你将掌握:
- WebSocket与HTTP协议的核心差异及应用场景
- 在SSM框架中从零配置WebSocket的完整步骤
- 实现单服务器与集群环境下的实时消息推送
- 连接管理、心跳检测与异常处理的最佳实践
- 基于BookService实现图书预约状态的实时通知功能
一、WebSocket技术基础与协议解析
1.1 HTTP与WebSocket的本质区别
| 特性 | HTTP | WebSocket |
|---|---|---|
| 连接方式 | 短连接,请求-响应模式 | 长连接,全双工通信 |
| 通信方向 | 客户端主动发起,服务器被动响应 | 客户端与服务器双向平等通信 |
| 头部开销 | 每次请求携带完整头部(约800B) | 仅握手阶段携带头部,后续通信头部极小(2-10B) |
| 实时性 | 低(需轮询/长轮询) | 高(毫秒级延迟) |
| 适用场景 | 页面加载、数据查询 | 实时通知、聊天系统、协同编辑 |
1.2 WebSocket协议握手过程
二、SSM框架整合WebSocket的环境准备
2.1 Maven依赖配置
在pom.xml中添加WebSocket相关依赖:
<!-- WebSocket核心依赖 -->
<dependency>
<groupId>javax.websocket</groupId>
<artifactId>javax.websocket-api</artifactId>
<version>1.1</version>
<scope>provided</scope>
</dependency>
<!-- Tomcat WebSocket实现 -->
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-websocket</artifactId>
<version>8.5.23</version>
<scope>provided</scope>
</dependency>
<!-- Spring WebSocket支持 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-websocket</artifactId>
<version>4.1.7.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-messaging</artifactId>
<version>4.1.7.RELEASE</version>
</dependency>
2.2 项目结构调整
在现有SSM项目结构基础上,添加WebSocket相关组件:
src/main/java/com/soecode/lyf/
├── websocket/ # WebSocket核心包
│ ├── config/ # 配置类
│ │ ├── WebSocketConfig.java # 基础配置
│ │ └── WebSocketMessageBrokerConfig.java # 消息代理配置
│ ├── controller/ # WebSocket控制器
│ │ └── BookWebSocketController.java # 图书相关WebSocket处理
│ ├── handler/ # 处理器
│ │ ├── WebSocketAuthHandler.java # 认证处理器
│ │ └── WebSocketErrorHandler.java # 错误处理器
│ └── model/ # 消息模型
│ ├── WebSocketMessage.java # 通用消息体
│ └── BookStatusMessage.java # 图书状态消息
└── service/impl/
└── BookServiceImpl.java # 修改服务层,添加WebSocket通知功能
三、WebSocket服务端实现与配置
3.1 Spring配置类实现
创建WebSocket配置类,启用WebSocket支持:
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
@Autowired
private BookWebSocketHandler bookWebSocketHandler;
@Autowired
private WebSocketHandshakeInterceptor handshakeInterceptor;
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
// 注册图书WebSocket端点
registry.addHandler(bookWebSocketHandler, "/ws/book")
.addInterceptors(handshakeInterceptor)
.setAllowedOrigins("*"); // 生产环境需限制源域名
// 提供SockJS降级支持
registry.addHandler(bookWebSocketHandler, "/sockjs/book")
.addInterceptors(handshakeInterceptor)
.withSockJS();
}
}
3.2 消息处理器实现
实现核心消息处理逻辑:
@Component
public class BookWebSocketHandler extends TextWebSocketHandler {
// 存储连接会话,key:用户ID,value:WebSocket会话
private static final Map<String, WebSocketSession> SESSIONS =
new ConcurrentHashMap<>();
// 连接建立时调用
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
// 从握手信息中获取用户ID
String userId = (String) session.getAttributes().get("userId");
if (userId != null) {
SESSIONS.put(userId, session);
// 发送连接成功消息
sendMessage(session, buildMessage("CONNECTED", "WebSocket连接成功"));
log.info("用户[{}]WebSocket连接建立,当前在线:{}人", userId, SESSIONS.size());
} else {
session.close(CloseStatus.POLICY_VIOLATION.withReason("未认证连接"));
}
}
// 处理接收到的消息
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
String userId = (String) session.getAttributes().get("userId");
log.info("收到用户[{}]消息:{}", userId, message.getPayload());
try {
// 解析消息
WebSocketMessage msg = new ObjectMapper().readValue(
message.getPayload(), WebSocketMessage.class);
// 处理不同类型消息
switch (msg.getType()) {
case "SUBSCRIBE":
handleSubscribe(userId, msg.getContent());
break;
case "UNSUBSCRIBE":
handleUnsubscribe(userId, msg.getContent());
break;
default:
sendMessage(session, buildMessage("ERROR", "不支持的消息类型"));
}
} catch (Exception e) {
sendMessage(session, buildMessage("ERROR", "消息格式错误"));
log.error("消息处理异常", e);
}
}
// 连接关闭时调用
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
String userId = (String) session.getAttributes().get("userId");
if (userId != null) {
SESSIONS.remove(userId);
log.info("用户[{}]WebSocket连接关闭,当前在线:{}人", userId, SESSIONS.size());
}
}
// 发送消息给指定用户
public void sendToUser(String userId, String message) {
WebSocketSession session = SESSIONS.get(userId);
if (session != null && session.isOpen()) {
try {
sendMessage(session, message);
} catch (Exception e) {
log.error("发送消息给用户[{}]失败", userId, e);
}
}
}
// 广播消息给所有用户
public void broadcast(String message) {
for (WebSocketSession session : SESSIONS.values()) {
if (session.isOpen()) {
try {
sendMessage(session, message);
} catch (Exception e) {
log.error("广播消息失败", e);
}
}
}
}
// 构建标准消息格式
private String buildMessage(String type, String content) {
WebSocketMessage message = new WebSocketMessage();
message.setType(type);
message.setContent(content);
message.setTimestamp(System.currentTimeMillis());
try {
return new ObjectMapper().writeValueAsString(message);
} catch (JsonProcessingException e) {
return "{\"type\":\"ERROR\",\"content\":\"消息构建失败\"}";
}
}
// 发送消息
private void sendMessage(WebSocketSession session, String message) throws IOException {
session.sendMessage(new TextMessage(message));
}
// 处理订阅逻辑
private void handleSubscribe(String userId, String bookId) {
// 实现订阅图书状态的逻辑
// ...
}
// 处理取消订阅逻辑
private void handleUnsubscribe(String userId, String bookId) {
// 实现取消订阅图书状态的逻辑
// ...
}
}
四、整合业务逻辑:图书预约状态实时通知
4.1 改造BookService实现消息推送
修改BookServiceImpl,在图书预约状态变更时发送WebSocket通知:
@Service
public class BookServiceImpl implements BookService {
@Autowired
private BookDao bookDao;
@Autowired
private AppointmentDao appointmentDao;
@Autowired
private BookWebSocketHandler webSocketHandler;
@Override
public AppointExecution appoint(long bookId, long studentId) {
try {
// 1.尝试预约图书
int update = appointmentDao.insertAppointment(bookId, studentId);
if (update <= 0) {
// 没有更新成功,说明已被预约
throw new RepeatAppointException("图书已被预约");
} else {
// 2.更新库存
int reduce = bookDao.reduceNumber(bookId);
if (reduce <= 0) {
// 库存不足
throw new NoNumberException("库存不足");
} else {
// 3.查询预约结果
Appointment appointment = appointmentDao.queryByKeyWithBook(bookId, studentId);
// 4.发送WebSocket通知
BookStatusMessage message = new BookStatusMessage();
message.setBookId(bookId);
message.setStatus("APPOINTED");
message.setAppointmentId(appointment.getAppointId());
message.setStudentId(studentId);
message.setAppointTime(appointment.getAppointTime());
// 广播图书状态变更
webSocketHandler.broadcast(
new ObjectMapper().writeValueAsString(message)
);
return new AppointExecution(bookId, AppointStateEnum.SUCCESS, appointment);
}
}
} catch (Exception e) {
// 异常处理
// ...
}
}
}
4.2 前端页面整合WebSocket客户端
创建图书预约状态实时显示页面(bookStatus.jsp):
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>图书预约状态实时监控</title>
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
<!-- 引入SockJS客户端库 -->
<script src="https://cdn.bootcdn.net/ajax/libs/sockjs-client/1.5.1/sockjs.min.js"></script>
</head>
<body>
<h1>图书预约状态实时监控</h1>
<div id="messageContainer" style="margin-top:20px;height:400px;border:1px solid #ccc;padding:10px;overflow-y:auto;"></div>
<script type="text/javascript">
// 初始化WebSocket连接
function initWebSocket() {
// 获取当前用户ID(实际项目从登录信息获取)
var userId = "${userId}";
// 判断浏览器是否支持WebSocket
if ('WebSocket' in window) {
// 标准WebSocket连接
var ws = new WebSocket("ws://" + window.location.host + "/ssm/ws/book?userId=" + userId);
} else {
// 不支持则使用SockJS降级
var ws = new SockJS("http://" + window.location.host + "/ssm/sockjs/book?userId=" + userId);
}
// 连接成功回调
ws.onopen = function() {
console.log("WebSocket连接已建立");
showMessage("系统消息", "WebSocket连接成功,可以接收图书状态通知");
// 订阅图书状态更新
ws.send(JSON.stringify({
"type": "SUBSCRIBE",
"content": "${bookId}" // 当前页面图书ID
}));
};
// 接收消息回调
ws.onmessage = function(event) {
console.log("收到消息: " + event.data);
var message = JSON.parse(event.data);
// 处理不同类型消息
if (message.type === "BOOK_STATUS_UPDATE") {
showMessage("图书状态更新",
"图书《" + message.bookName + "》已被预约,预约号:" + message.appointmentId);
} else if (message.type === "ERROR") {
showMessage("错误消息", message.content, "error");
} else {
showMessage("系统消息", message.content);
}
};
// 连接关闭回调
ws.onclose = function() {
console.log("WebSocket连接已关闭");
showMessage("系统消息", "连接已关闭,正在尝试重连...", "warning");
// 尝试重连
setTimeout(initWebSocket, 3000);
};
// 连接错误回调
ws.onerror = function(error) {
console.error("WebSocket错误: " + error);
showMessage("错误消息", "连接发生错误,请刷新页面重试", "error");
};
// 页面关闭时关闭连接
window.onbeforeunload = function() {
ws.close();
};
// 将WebSocket对象存储在window对象中,方便调试
window.bookWebSocket = ws;
}
// 显示消息到页面
function showMessage(title, content, type) {
var container = document.getElementById("messageContainer");
var msgDiv = document.createElement("div");
msgDiv.className = "message-item " + (type || "info");
var time = new Date().toLocaleTimeString();
msgDiv.innerHTML = `
<div class="message-header">
<span class="message-title">${title}</span>
<span class="message-time">${time}</span>
</div>
<div class="message-content">${content}</div>
`;
container.prepend(msgDiv); // 最新消息显示在顶部
// 添加动画效果
setTimeout(() => {
msgDiv.classList.add("show");
}, 10);
}
// 页面加载完成后初始化WebSocket
window.onload = initWebSocket;
</script>
<style>
.message-item {
margin-bottom: 15px;
padding: 10px;
border-radius: 5px;
background-color: #f5f5f5;
opacity: 0;
transform: translateY(10px);
transition: opacity 0.3s, transform 0.3s;
}
.message-item.show {
opacity: 1;
transform: translateY(0);
}
.message-item.info {
border-left: 4px solid #3498db;
}
.message-item.warning {
border-left: 4px solid #f39c12;
background-color: #fef5e7;
}
.message-item.error {
border-left: 4px solid #e74c3c;
background-color: #fdedeb;
}
.message-header {
display: flex;
justify-content: space-between;
margin-bottom: 5px;
font-weight: bold;
}
.message-time {
font-size: 0.8em;
color: #777;
font-weight: normal;
}
</style>
</body>
</html>
五、WebSocket连接管理与性能优化
5.1 连接管理机制
5.2 性能优化策略
- 连接池化:使用NIO模型,设置合理的连接池大小
- 消息压缩:对大型消息启用PerMessageDeflate压缩
- 批量发送:合并小消息,减少网络往返
- 异步处理:使用消息队列异步处理非实时消息
- 合理心跳:设置恰当的心跳间隔(建议30-60秒)
// 配置WebSocket压缩
@Bean
public ServletServerContainerFactoryBean createWebSocketContainer() {
ServletServerContainerFactoryBean container = new ServletServerContainerFactoryBean();
// 配置最大消息大小
container.setMaxTextMessageBufferSize(8192);
container.setMaxBinaryMessageBufferSize(8192);
// 启用压缩
container.setProperty("org.apache.tomcat.websocket.textBufferSize", "8192");
container.setProperty("org.apache.tomcat.websocket.binaryBufferSize", "8192");
container.setProperty("org.apache.tomcat.websocket.perMessageDeflateSize", "1024");
// 配置超时时间
container.setAsyncWriteTimeout(5000);
container.setMaxSessionIdleTimeout(300000); // 5分钟空闲超时
return container;
}
六、集群环境下的WebSocket部署方案
6.1 基于Redis发布订阅的集群通信
实现Redis发布订阅适配器:
@Component
public class RedisPubSubAdapter {
@Autowired
private StringRedisTemplate redisTemplate;
@Autowired
private BookWebSocketHandler webSocketHandler;
@PostConstruct
public void init() {
// 订阅WebSocket消息频道
redisTemplate.execute(new RedisCallback<Void>() {
@Override
public Void doInRedis(RedisConnection connection) throws DataAccessException {
connection.subscribe(new MessageListener() {
@Override
public void onMessage(Message message, byte[] pattern) {
String channel = new String(pattern);
String messageContent = new String(message.getBody());
// 处理集群消息
handleClusterMessage(channel, messageContent);
}
}, "webSocket:broadcast".getBytes());
return null;
}
});
}
// 处理集群消息
private void handleClusterMessage(String channel, String messageContent) {
try {
// 解析消息
BookStatusMessage message = new ObjectMapper().readValue(
messageContent, BookStatusMessage.class);
// 转发给本地连接的相关客户端
webSocketHandler.broadcast(messageContent);
} catch (Exception e) {
log.error("处理集群WebSocket消息失败", e);
}
}
// 发布消息到集群
public void publish(String channel, Object message) {
try {
redisTemplate.convertAndSend(channel,
new ObjectMapper().writeValueAsString(message));
} catch (Exception e) {
log.error("发布集群WebSocket消息失败", e);
}
}
}
6.2 负载均衡配置(Nginx示例)
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 book.example.com;
# WebSocket配置
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 4s;
proxy_send_timeout 60s;
proxy_read_timeout 3600s; # WebSocket长连接超时
}
# SockJS降级支持
location /sockjs/ {
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_cache_bypass $http_upgrade;
}
# 普通HTTP请求
location / {
proxy_pass http://websocket_servers;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
七、测试与调试工具
7.1 浏览器开发工具调试
Chrome开发者工具中的WebSocket调试功能:
- 网络面板:筛选WebSocket连接,查看握手和消息帧
- 控制台:通过
window.bookWebSocket直接调用API发送测试消息 - 性能面板:分析WebSocket消息对页面性能的影响
7.2 命令行测试工具
使用wscat进行命令行测试:
# 安装wscat
npm install -g wscat
# 连接WebSocket服务
wscat -c ws://localhost:8080/ssm/ws/book?userId=test123
# 发送测试消息
> {"type":"SUBSCRIBE","content":"1001"}
< {"type":"CONNECTED","content":"WebSocket连接成功","timestamp":1620000000000}
八、安全加固与最佳实践
8.1 安全措施实现
- 身份认证与授权
@Component
public class WebSocketHandshakeInterceptor implements HandshakeInterceptor {
@Autowired
private UserService userService;
@Override
public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response,
WebSocketHandler wsHandler, Map<String, Object> attributes) throws Exception {
// 从请求参数获取token
String token = extractToken(request);
if (token == null || token.isEmpty()) {
response.setStatusCode(HttpStatus.UNAUTHORIZED);
return false;
}
try {
// 验证token
String userId = userService.verifyToken(token);
if (userId == null) {
response.setStatusCode(HttpStatus.UNAUTHORIZED);
return false;
}
// 检查用户权限
if (!hasWebSocketPermission(userId)) {
response.setStatusCode(HttpStatus.FORBIDDEN);
return false;
}
// 将用户ID存入属性,供后续使用
attributes.put("userId", userId);
return true;
} catch (Exception e) {
response.setStatusCode(HttpStatus.UNAUTHORIZED);
return false;
}
}
// 提取token
private String extractToken(ServerHttpRequest request) {
// 从请求头或参数中提取token
HttpHeaders headers = request.getHeaders();
List<String> tokens = headers.get("Authorization");
if (tokens != null && !tokens.isEmpty()) {
String bearerToken = tokens.get(0);
if (bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7);
}
}
// 从查询参数获取
URI uri = request.getURI();
String query = uri.getQuery();
if (query != null) {
String[] params = query.split("&");
for (String param : params) {
String[] keyValue = param.split("=");
if (keyValue.length == 2 && "token".equals(keyValue[0])) {
return keyValue[1];
}
}
}
return null;
}
// 检查用户是否有WebSocket访问权限
private boolean hasWebSocketPermission(String userId) {
// 实现权限检查逻辑
// ...
return true;
}
@Override
public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response,
WebSocketHandler wsHandler, Exception ex) {
// 握手后处理
}
}
- 消息内容验证
// 消息验证工具类
public class MessageValidator {
// 验证消息格式
public static boolean validateMessageFormat(String message) {
try {
JsonNode node = new ObjectMapper().readTree(message);
// 检查必要字段
if (!node.has("type") || !node.has("content")) {
return false;
}
// 验证消息类型
String type = node.get("type").asText();
if (!Arrays.asList("SUBSCRIBE", "UNSUBSCRIBE", "HEARTBEAT", "MESSAGE").contains(type)) {
return false;
}
// 验证内容长度
if (node.get("content").asText().length() > 2048) {
return false;
}
return true;
} catch (Exception e) {
return false;
}
}
// 防XSS过滤
public static String sanitizeContent(String content) {
if (content == null) return null;
// HTML转义
content = content.replaceAll("<", "<")
.replaceAll(">", ">")
.replaceAll("&", "&")
.replaceAll("\"", """)
.replaceAll("'", "'");
// 过滤危险字符
content = content.replaceAll("[\\x00-\\x1F\\x7F]", "");
return content;
}
}
8.2 生产环境部署清单
- 配置正确的跨域策略,限制允许的源域名
- 启用SSL/TLS,使用wss://协议加密传输
- 实施连接速率限制,防止DoS攻击
- 配置适当的超时和心跳机制
- 实现完善的日志记录,包括连接、消息和错误
- 部署监控系统,跟踪连接数、消息量和响应时间
- 配置自动重连和故障转移机制
- 定期进行安全审计和渗透测试
结论与展望
本文详细介绍了在SSM框架中整合WebSocket实现实时通信的完整方案,从基础配置到集群部署,涵盖了开发、测试、部署的全流程。通过图书预约状态实时通知的实际案例,展示了WebSocket技术如何显著提升用户体验。
随着Web技术的发展,未来可以进一步探索:
- 基于WebRTC实现更复杂的实时交互(如视频咨询)
- 使用GraphQL Subscription替代部分WebSocket场景
- 结合Server-Sent Events (SSE)实现单向高效推送
- 利用WebAssembly提升客户端消息处理性能
通过这些技术的综合应用,可以构建更加高效、稳定、实时的Web应用系统。
附录:常见问题解决方案
-
连接建立失败
- 检查WebSocket端点URL是否正确
- 确认服务器端是否启用WebSocket支持
- 检查防火墙和代理设置是否阻止WebSocket连接
-
消息发送不出去
- 验证连接状态是否为OPEN
- 检查消息格式是否符合约定
- 查看服务器日志是否有错误信息
-
集群环境下消息不同步
- 检查Redis发布订阅配置是否正确
- 验证各节点是否连接到同一Redis实例
- 确认消息序列化/反序列化逻辑一致
-
浏览器兼容性问题
- 提供SockJS降级方案
- 对老旧浏览器实施功能检测和优雅降级
- 使用polyfill补充缺失的API
【免费下载链接】ssm 手把手教你整合最优雅SSM框架:SpringMVC + Spring + MyBatis 项目地址: https://gitcode.com/gh_mirrors/ss/ssm
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考



