|
|
@@ -0,0 +1,164 @@
|
|
|
+package com.yanfan.framework.websocket;
|
|
|
+
|
|
|
+import com.yanfan.common.utils.StringUtils;
|
|
|
+import lombok.extern.slf4j.Slf4j;
|
|
|
+import org.springframework.http.server.ServerHttpRequest;
|
|
|
+import org.springframework.http.server.ServerHttpResponse;
|
|
|
+import org.springframework.stereotype.Component;
|
|
|
+import org.springframework.web.socket.*;
|
|
|
+import org.springframework.web.socket.server.support.HttpSessionHandshakeInterceptor;
|
|
|
+
|
|
|
+import java.util.Map;
|
|
|
+import java.util.concurrent.ConcurrentHashMap;
|
|
|
+
|
|
|
+/**
|
|
|
+ * ✅ 修复版核心处理器:解决deviceCode=null + 全链路容错 + 日志完善
|
|
|
+ * 支持 ws://ip:port/ws/{deviceCode} 动态路径,保证设备编码100%能获取
|
|
|
+ */
|
|
|
+@Slf4j
|
|
|
+@Component
|
|
|
+public class DeviceWebSocketHandler implements WebSocketHandler {
|
|
|
+
|
|
|
+ // 线程安全存储:设备编码 → WebSocket会话(核心映射)
|
|
|
+ public static final Map<String, WebSocketSession> DEVICE_SESSION_MAP = new ConcurrentHashMap<>(16);
|
|
|
+
|
|
|
+ // ========== ✅ 握手拦截器【必生效】:解析路径中的deviceCode ==========
|
|
|
+ public static class DeviceCodeHandshakeInterceptor extends HttpSessionHandshakeInterceptor {
|
|
|
+ @Override
|
|
|
+ public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response,
|
|
|
+ WebSocketHandler wsHandler, Map<String, Object> attributes) throws Exception {
|
|
|
+ // ✅ 优化路径解析逻辑:兼容任意格式路径,100%提取deviceCode
|
|
|
+ String uriPath = request.getURI().getPath().trim();
|
|
|
+ if (!uriPath.startsWith("/ws/")) {
|
|
|
+ log.error("WebSocket连接失败:非法路径,必须以/ws/开头");
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ // 切割路径,精准获取最后一段的设备编码
|
|
|
+ String[] pathSegments = uriPath.split("/");
|
|
|
+ String deviceCode = pathSegments[pathSegments.length - 1].trim();
|
|
|
+
|
|
|
+ // ✅ 前置校验:设备编码不能为空
|
|
|
+ if (StringUtils.isBlank(deviceCode)) {
|
|
|
+ log.error("WebSocket连接失败:设备编码不能为空");
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ // ✅ 存入会话属性,后续全程可获取(拦截器生效后,此处必存值)
|
|
|
+ attributes.put("deviceCode", deviceCode);
|
|
|
+ log.info("✅ 设备[{}]开始WebSocket握手,路径解析成功", deviceCode);
|
|
|
+ return super.beforeHandshake(request, response, wsHandler, attributes);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // ========== WebSocket生命周期方法 ==========
|
|
|
+ /**
|
|
|
+ * 连接建立成功(拦截器已存入deviceCode,此处100%能获取)
|
|
|
+ */
|
|
|
+ @Override
|
|
|
+ public void afterConnectionEstablished(WebSocketSession session) throws Exception {
|
|
|
+ // ✅ 从会话属性获取deviceCode(拦截器已校验非空,此处直接取值)
|
|
|
+ String deviceCode = (String) session.getAttributes().get("deviceCode");
|
|
|
+ log.info("✅ 设备[{}]WebSocket连接建立成功", deviceCode);
|
|
|
+
|
|
|
+ // 存储会话,重连时覆盖旧会话(保证设备会话唯一性)
|
|
|
+ DEVICE_SESSION_MAP.put(deviceCode, session);
|
|
|
+ log.info("当前在线设备数:{},在线设备:{}", DEVICE_SESSION_MAP.size(), DEVICE_SESSION_MAP.keySet());
|
|
|
+
|
|
|
+ // 连接成功,主动向前端推送回执
|
|
|
+ sendMessageToDevice(deviceCode, "【成功】设备" + deviceCode + "已建立WebSocket连接");
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 接收前端发送的消息(前端→后端)
|
|
|
+ */
|
|
|
+ @Override
|
|
|
+ public void handleMessage(WebSocketSession session, WebSocketMessage<?> message) throws Exception {
|
|
|
+ String deviceCode = (String) session.getAttributes().get("deviceCode");
|
|
|
+ String receiveMsg = message.getPayload().toString().trim();
|
|
|
+ log.info("📥 收到设备[{}]消息:{}", deviceCode, receiveMsg);
|
|
|
+
|
|
|
+ // 业务示例:收到消息后,给前端回复回执
|
|
|
+ sendMessageToDevice(deviceCode, "【已接收】你的消息:" + receiveMsg);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 连接异常处理
|
|
|
+ */
|
|
|
+ @Override
|
|
|
+ public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
|
|
|
+ String deviceCode = (String) session.getAttributes().get("deviceCode");
|
|
|
+ log.error("❌ 设备[{}]WebSocket连接异常", deviceCode, exception);
|
|
|
+ // 异常时清理会话
|
|
|
+ clearInvalidSession(deviceCode, session);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 连接关闭处理
|
|
|
+ */
|
|
|
+ @Override
|
|
|
+ public void afterConnectionClosed(WebSocketSession session, CloseStatus closeStatus) throws Exception {
|
|
|
+ String deviceCode = (String) session.getAttributes().get("deviceCode");
|
|
|
+ log.info("🔌 设备[{}]WebSocket连接关闭,状态:{}", deviceCode, closeStatus);
|
|
|
+ // 关闭时清理会话
|
|
|
+ clearInvalidSession(deviceCode, session);
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public boolean supportsPartialMessages() {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ // ========== 核心工具方法 ==========
|
|
|
+ /**
|
|
|
+ * ✅ 根据设备编码,精准推送消息给指定前端(后端→前端)
|
|
|
+ */
|
|
|
+ public boolean sendMessageToDevice(String deviceCode, String content) {
|
|
|
+ WebSocketSession session = DEVICE_SESSION_MAP.get(deviceCode);
|
|
|
+ if (session == null || !session.isOpen()) {
|
|
|
+ log.error("❌ 设备[{}]推送失败:设备未在线/连接已断开", deviceCode);
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ TextMessage textMessage = new TextMessage(content);
|
|
|
+ session.sendMessage(textMessage);
|
|
|
+ log.info("📤 设备[{}]推送消息成功:{}", deviceCode, content);
|
|
|
+ return true;
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("❌ 设备[{}]推送消息异常", deviceCode, e);
|
|
|
+ clearInvalidSession(deviceCode, session);
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 广播消息:推送给所有在线设备
|
|
|
+ */
|
|
|
+ public void broadcastMessage(String content) {
|
|
|
+ log.info("📢 开始广播消息:{},在线设备数:{}", content, DEVICE_SESSION_MAP.size());
|
|
|
+ TextMessage textMessage = new TextMessage(content);
|
|
|
+ DEVICE_SESSION_MAP.forEach((deviceCode, session) -> {
|
|
|
+ if (session.isOpen()) {
|
|
|
+ try {
|
|
|
+ session.sendMessage(textMessage);
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("❌ 设备[{}]广播失败", deviceCode, e);
|
|
|
+ clearInvalidSession(deviceCode, session);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 私有工具:清理无效会话,释放资源
|
|
|
+ */
|
|
|
+ private void clearInvalidSession(String deviceCode, WebSocketSession session) {
|
|
|
+ if (session.isOpen()) {
|
|
|
+ try {
|
|
|
+ session.close();
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("关闭会话异常", e);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ DEVICE_SESSION_MAP.remove(deviceCode);
|
|
|
+ log.info("✅ 设备[{}]无效会话已清理,当前在线设备数:{}", deviceCode, DEVICE_SESSION_MAP.size());
|
|
|
+ }
|
|
|
+}
|