websocket 的创建使用
java
package websocket.heartbeat;
import io.netty.util.HashedWheelTimer;
import io.netty.util.Timeout;
import io.netty.util.TimerTask;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.WebSocketSession;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
/**
* 基于HashedWheelTimer的WebSocket连接心跳检测管理器
*/
@Slf4j
@Component
public class WebSocketHeartbeatManager {
private HashedWheelTimer hashedWheelTimer;
// 存储活跃的WebSocket连接及其超时任务
private final ConcurrentHashMap<String, WebSocketSessionWrapper> activeSessions = new ConcurrentHashMap<>();
// 心跳超时倍数
private static final int TIMEOUT_MULTIPLIER = 2;
// 默认心跳间隔(毫秒)
private static final long DEFAULT_HEARTBEAT_INTERVAL = 30000; // 30秒
@PostConstruct
public void init() {
// 初始化HashedWheelTimer 0.1ms * 256 = 25.6s 约等于默认的心跳间隔
this.hashedWheelTimer = new HashedWheelTimer(
new ThreadFactory() {
private final AtomicInteger threadNumber = new AtomicInteger(1);
@Override
public Thread newThread(Runnable r) {
Thread thread = new Thread(r, "websocket-heartbeat-thread-" + threadNumber.getAndIncrement());
thread.setDaemon(true); // 设置为守护线程
thread.setPriority(Thread.NORM_PRIORITY - 1); // 稍低优先级
return thread;
}
},
100, // 时间精度100ms
TimeUnit.MILLISECONDS,
256 // 256个槽位, 建议使用2的幂次
);
log.info("WebSocket心跳检测管理器初始化完成");
}
/**
* 为WebSocket连接启动心跳检测
*/
public void startHeartbeatCheck(WebSocketSession session, String sessionId) {
if (session == null || sessionId == null) {
log.warn("无法启动心跳检测:session或sessionId为空");
return;
}
try {
// 创建超时任务
Timeout timeout = hashedWheelTimer.newTimeout(
new HeartbeatTimeoutTask(session, sessionId),
getHeartbeatTimeout(),
TimeUnit.MILLISECONDS
);
// 包装会话信息
WebSocketSessionWrapper wrapper = WebSocketSessionWrapper.builder()
.webSocketSession(session)
.timeout(timeout)
.sessionId(sessionId)
.lastHeartbeatTime(System.currentTimeMillis())
.build();
// 存储到活跃连接映射中
activeSessions.put(sessionId, wrapper);
log.info("为会话 {} 启动心跳检测,超时时间: {}ms", sessionId, getHeartbeatTimeout());
} catch (Exception e) {
log.error("启动心跳检测失败,会话ID: {}", sessionId, e);
}
}
/**
* 处理客户端心跳消息
*/
public void handleHeartbeat(String sessionId) {
WebSocketSessionWrapper wrapper = activeSessions.get(sessionId);
if (wrapper != null) {
// 更新最后心跳时间
wrapper.setLastHeartbeatTime(System.currentTimeMillis());
// 取消当前超时任务
Timeout oldTimeout = wrapper.getTimeout();
if (oldTimeout != null && !oldTimeout.isCancelled()) {
oldTimeout.cancel();
}
// 重新启动超时检测
try {
Timeout newTimeout = hashedWheelTimer.newTimeout(
new HeartbeatTimeoutTask(wrapper.getWebSocketSession(), sessionId),
getHeartbeatTimeout(),
TimeUnit.MILLISECONDS
);
wrapper.setTimeout(newTimeout);
log.debug("会话 {} 心跳更新,重新设置超时任务", sessionId);
} catch (Exception e) {
log.error("重新设置心跳检测失败,会话ID: {}", sessionId, e);
}
} else {
log.warn("收到心跳但找不到对应会话,会话ID: {}", sessionId);
}
}
/**
* 停止指定连接的心跳检测
*/
public void stopHeartbeatCheck(String sessionId) {
WebSocketSessionWrapper wrapper = activeSessions.remove(sessionId);
if (wrapper != null) {
Timeout timeout = wrapper.getTimeout();
if (timeout != null && !timeout.isCancelled()) {
timeout.cancel();
}
log.info("停止会话 {} 的心跳检测", sessionId);
}
}
/**
* 获取当前活跃连接数
*/
public int getActiveSessionCount() {
return activeSessions.size();
}
/**
* 获取所有活跃会话ID
*/
public Iterable<String> getActiveSessionIds() {
return activeSessions.keySet();
}
/**
* 计算心跳超时时间
*/
private long getHeartbeatTimeout() {
return TIMEOUT_MULTIPLIER * DEFAULT_HEARTBEAT_INTERVAL;
}
@PreDestroy
public void destroy() {
if (hashedWheelTimer != null) {
// 停止所有定时任务
hashedWheelTimer.stop();
log.info("WebSocket心跳检测管理器已销毁");
}
}
/**
* 心跳超时任务
*/
private class HeartbeatTimeoutTask implements TimerTask {
private final WebSocketSession session;
private final String sessionId;
public HeartbeatTimeoutTask(WebSocketSession session, String sessionId) {
this.session = session;
this.sessionId = sessionId;
}
@Override
public void run(Timeout timeout) throws Exception {
if (!timeout.isCancelled()) {
handleHeartbeatTimeout();
}
}
private void handleHeartbeatTimeout() {
try {
log.info("会话 {} 心跳超时,准备关闭连接", sessionId);
// 从活跃连接中移除
activeSessions.remove(sessionId);
// 关闭WebSocket连接
if (session.isOpen()) {
session.close();
log.info("会话 {} 连接已关闭", sessionId);
}
} catch (Exception e) {
log.error("处理心跳超时异常,会话ID: {}", sessionId, e);
}
}
}
/**
* WebSocket会话包装类
*/
@lombok.Builder
@lombok.Data
public static class WebSocketSessionWrapper {
private WebSocketSession webSocketSession;
private Timeout timeout;
private String sessionId;
private long lastHeartbeatTime;
}
}
创建WebSocket处理器
java
package websocket.heartbeat;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.*;
import java.util.UUID;
@Slf4j
@Component
public class HeartbeatWebSocketHandler implements WebSocketHandler {
@Autowired
private WebSocketHeartbeatManager heartbeatManager;
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
// 生成会话ID
String sessionId = UUID.randomUUID().toString();
session.getAttributes().put("sessionId", sessionId);
// 启动心跳检测
heartbeatManager.startHeartbeatCheck(session, sessionId);
// 发送连接确认消息
JSONObject response = new JSONObject();
response.put("type", "connected");
response.put("sessionId", sessionId);
response.put("timestamp", System.currentTimeMillis());
response.put("heartbeatInterval", 30000); // 30秒
sendMessage(session, response.toJSONString());
log.info("WebSocket连接建立,会话ID: {}", sessionId);
}
@Override
public void handleMessage(WebSocketSession session, WebSocketMessage<?> message) throws Exception {
String sessionId = (String) session.getAttributes().get("sessionId");
if (!(message instanceof TextMessage)) {
log.warn("只支持文本消息,会话ID: {}", sessionId);
return;
}
String payload = ((TextMessage) message).getPayload();
log.debug("收到消息,会话ID: {}, 内容: {}", sessionId, payload);
try {
JSONObject request = JSON.parseObject(payload);
String messageType = request.getString("type");
switch (messageType) {
case "ping":
handlePing(session, sessionId);
break;
case "pong":
handlePong(session, sessionId);
break;
case "heartbeat":
handleHeartbeat(session, sessionId, request);
break;
default:
handleUnknownMessage(session, sessionId, payload);
break;
}
} catch (Exception e) {
log.error("处理消息异常,会话ID: {}", sessionId, e);
sendErrorMessage(session, "消息处理异常: " + e.getMessage());
}
}
@Override
public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
String sessionId = (String) session.getAttributes().get("sessionId");
log.error("WebSocket传输错误,会话ID: {}", sessionId, exception);
}
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus closeStatus) throws Exception {
String sessionId = (String) session.getAttributes().get("sessionId");
// 停止心跳检测
heartbeatManager.stopHeartbeatCheck(sessionId);
log.info("WebSocket连接关闭,会话ID: {}, 状态: {}", sessionId, closeStatus);
}
@Override
public boolean supportsPartialMessages() {
return false;
}
// 处理ping消息
private void handlePing(WebSocketSession session, String sessionId) throws Exception {
JSONObject response = new JSONObject();
response.put("type", "pong");
response.put("timestamp", System.currentTimeMillis());
sendMessage(session, response.toJSONString());
log.debug("处理ping消息,会话ID: {}", sessionId);
}
// 处理pong消息(客户端响应)
private void handlePong(WebSocketSession session, String sessionId) throws Exception {
// 更新心跳状态
heartbeatManager.handleHeartbeat(sessionId);
JSONObject response = new JSONObject();
response.put("type", "heartbeat_ack");
response.put("timestamp", System.currentTimeMillis());
sendMessage(session, response.toJSONString());
log.debug("处理pong消息,会话ID: {}", sessionId);
}
// 处理心跳消息
private void handleHeartbeat(WebSocketSession session, String sessionId, JSONObject request) throws Exception {
// 更新心跳状态
heartbeatManager.handleHeartbeat(sessionId);
JSONObject response = new JSONObject();
response.put("type", "heartbeat_response");
response.put("timestamp", System.currentTimeMillis());
response.put("serverTime", System.currentTimeMillis());
sendMessage(session, response.toJSONString());
log.debug("处理心跳消息,会话ID: {}", sessionId);
}
// 处理未知消息
private void handleUnknownMessage(WebSocketSession session, String sessionId, String payload) throws Exception {
JSONObject response = new JSONObject();
response.put("type", "error");
response.put("message", "未知的消息类型");
response.put("received", payload);
response.put("timestamp", System.currentTimeMillis());
sendMessage(session, response.toJSONString());
log.warn("收到未知消息类型,会话ID: {}, 消息: {}", sessionId, payload);
}
// 发送错误消息
private void sendErrorMessage(WebSocketSession session, String errorMessage) throws Exception {
JSONObject response = new JSONObject();
response.put("type", "error");
response.put("message", errorMessage);
response.put("timestamp", System.currentTimeMillis());
sendMessage(session, response.toJSONString());
}
// 发送消息的通用方法
private void sendMessage(WebSocketSession session, String message) throws Exception {
if (session.isOpen()) {
session.sendMessage(new TextMessage(message));
}
}
}
创建WebSocket配置类
java
package websocket.heartbeat;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
@Slf4j
@Configuration
@EnableWebSocket
public class HeartbeatWebSocketConfig implements WebSocketConfigurer {
@Autowired
private HeartbeatWebSocketHandler heartbeatWebSocketHandler;
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(heartbeatWebSocketHandler, "/heartbeat/ws")
.setAllowedOrigins("*") // 生产环境应该限制具体域名
.withSockJS(); // 支持SockJS回退
log.info("心跳WebSocket处理器注册完成,访问路径: /heartbeat/ws");
}
}
创建WebSocket服务类
java
package com.gomefinance.consumerfinance.ccp.phb.service.websocket.heartbeat;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import java.util.Map;
@Slf4j
@Service
public class HeartbeatWebSocketService {
@Autowired
private WebSocketHeartbeatManager heartbeatManager;
/**
* 向指定会话发送消息
*/
public boolean sendMessageToSession(String sessionId, String message) {
// 这里需要获取WebSocketSession,可以通过其他方式存储session引用
// 暂时返回false,实际使用时需要完善session管理
log.warn("sendMessageToSession方法需要完善session存储机制");
return false;
}
/**
* 广播消息到所有活跃连接
*/
public void broadcastMessage(String message) {
// 需要完善session存储和获取逻辑
log.warn("broadcastMessage方法需要完善session存储机制");
}
/**
* 获取连接统计信息
*/
public ConnectionStats getConnectionStats() {
return new ConnectionStats(
heartbeatManager.getActiveSessionCount(),
heartbeatManager.getActiveSessionIds()
);
}
/**
* 强制关闭指定会话
*/
public boolean disconnectSession(String sessionId) {
try {
heartbeatManager.stopHeartbeatCheck(sessionId);
log.info("强制断开会话 {} 成功", sessionId);
return true;
} catch (Exception e) {
log.error("强制断开会话 {} 失败", sessionId, e);
return false;
}
}
// 连接统计信息类
public static class ConnectionStats {
private final int activeConnections;
private final Iterable<String> sessionIds;
public ConnectionStats(int activeConnections, Iterable<String> sessionIds) {
this.activeConnections = activeConnections;
this.sessionIds = sessionIds;
}
public int getActiveConnections() { return activeConnections; }
public Iterable<String> getSessionIds() { return sessionIds; }
}
}
最后创建控制器用于测试和监控
java
package com.gomefinance.consumerfinance.ccp.phb.controller.websocket;
import com.gomefinance.consumerfinance.ccp.phb.service.websocket.heartbeat.HeartbeatWebSocketService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
@Slf4j
@RestController
@RequestMapping("/api/websocket")
public class WebSocketMonitorController {
@Autowired
private HeartbeatWebSocketService websocketService;
/**
* 获取WebSocket连接统计信息
*/
@GetMapping("/stats")
public Object getConnectionStats() {
HeartbeatWebSocketService.ConnectionStats stats = websocketService.getConnectionStats();
return Map.of(
"activeConnections", stats.getActiveConnections(),
"sessionIds", stats.getSessionIds(),
"timestamp", System.currentTimeMillis()
);
}
/**
* 强制断开指定会话
*/
@DeleteMapping("/disconnect/{sessionId}")
public Object disconnectSession(@PathVariable String sessionId) {
boolean success = websocketService.disconnectSession(sessionId);
return Map.of(
"success", success,
"sessionId", sessionId,
"message", success ? "会话断开成功" : "会话断开失败"
);
}
/**
* WebSocket测试页面
*/
@GetMapping("/test")
public String getTestPage() {
return """
<!DOCTYPE html>
<html>
<head>
<title>WebSocket心跳测试</title>
</head>
<body>
<h2>WebSocket心跳检测测试</h2>
<button onclick="connect()">连接WebSocket</button>
<button onclick="sendHeartbeat()">发送心跳</button>
<button onclick="disconnect()">断开连接</button>
<br><br>
<div id="messages" style="height: 300px; overflow-y: scroll; border: 1px solid #ccc; padding: 10px;"></div>
<script>
let socket = null;
let heartbeatInterval = null;
function connect() {
if (socket && socket.readyState === WebSocket.OPEN) {
appendMessage('已经连接');
return;
}
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
socket = new WebSocket(protocol + '//' + window.location.host + '/heartbeat/ws');
socket.onopen = function(event) {
appendMessage('连接已建立');
startHeartbeat();
};
socket.onmessage = function(event) {
const data = JSON.parse(event.data);
appendMessage('收到消息: ' + JSON.stringify(data));
if (data.type === 'connected') {
appendMessage('服务器分配的会话ID: ' + data.sessionId);
}
};
socket.onclose = function(event) {
appendMessage('连接已关闭');
stopHeartbeat();
};
socket.onerror = function(error) {
appendMessage('连接错误: ' + error);
};
}
function sendHeartbeat() {
if (socket && socket.readyState === WebSocket.OPEN) {
const message = JSON.stringify({
type: 'heartbeat',
timestamp: Date.now()
});
socket.send(message);
appendMessage('发送心跳: ' + message);
} else {
appendMessage('连接未建立');
}
}
function disconnect() {
if (socket) {
socket.close();
socket = null;
}
}
function startHeartbeat() {
// 每25秒发送一次心跳(服务器超时时间60秒)
heartbeatInterval = setInterval(sendHeartbeat, 25000);
appendMessage('心跳检测已启动');
}
function stopHeartbeat() {
if (heartbeatInterval) {
clearInterval(heartbeatInterval);
heartbeatInterval = null;
appendMessage('心跳检测已停止');
}
}
function appendMessage(message) {
const messagesDiv = document.getElementById('messages');
const time = new Date().toLocaleTimeString();
messagesDiv.innerHTML += '<div>[' + time + '] ' + message + '</div>';
messagesDiv.scrollTop = messagesDiv.scrollHeight;
}
</script>
</body>
</html>
""";
}
}
监控接口:
- GET /api/websocket/stats - 查看连接统计
- DELETE /api/websocket/disconnect/{sessionId} - 断开指定连接
- GET /api/websocket/test - 测试页面
json
// 心跳消息
{"type": "heartbeat", "timestamp": 1234567890}
// ping消息
{"type": "ping"}