websocket 的创建使用

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"}
相关推荐
一路往蓝-Anbo17 小时前
第 12 章:Linux 侧 RPMsg 用户态驱动与数据接口
linux·运维·服务器·stm32·单片机·嵌入式硬件·网络协议
却尘17 小时前
一个 ERR_SSL_PROTOCOL_ERROR 让我们排查了三层问题,最后发现根本不是 SSL 的锅
前端·后端·网络协议
宁雨桥17 小时前
详解Web服务部署:IP+端口 vs IP+端口+目录 实战指南
前端·网络协议·tcp/ip
mftang18 小时前
WebSocket协议与其他通信协议有什么区别?
网络·websocket·网络协议
小灰灰搞电子18 小时前
ESP32 使用ESP-IDF实现Modbus TCP主机通信源码分享
网络·modbustcp·网络协议·tcp/ip·esp32
五阿哥永琪19 小时前
HTTP包含哪些内容?
网络·网络协议·http
hoududubaba1 天前
ORAN共享小区的级联FHM模式
网络·网络协议
小飞大王6661 天前
WebSocket技术与心跳检测
前端·javascript·websocket·网络协议·arcgis
我是Superman丶1 天前
Nginx反向代理流式输出延迟?一招解决SSE/WebSocket缓冲问题SpringBoot+SSE流式输出卡住?Nginx这个配置必须关!
运维·websocket·nginx