#init
This commit is contained in:
91
maibu-web-middleware/pom.xml
Normal file
91
maibu-web-middleware/pom.xml
Normal file
@@ -0,0 +1,91 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>com.maibu</groupId>
|
||||
<artifactId>MiddlePlatform</artifactId>
|
||||
<version>4.0.0</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>maibu-web-middleware</artifactId>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.logging.log4j</groupId>
|
||||
<artifactId>log4j-core</artifactId>
|
||||
<version>2.20.0</version> <!-- 使用最新版 -->
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.logging.log4j</groupId>
|
||||
<artifactId>log4j-api</artifactId>
|
||||
<version>2.20.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<version>1.18.8</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.java-websocket</groupId>
|
||||
<artifactId>Java-WebSocket</artifactId>
|
||||
<version>1.5.3</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.netty</groupId>
|
||||
<artifactId>netty-all</artifactId>
|
||||
<version>4.1.56.Final</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-lang3</artifactId>
|
||||
<version>3.12.0</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.vertx</groupId>
|
||||
<artifactId>vertx-core</artifactId>
|
||||
<version>4.5.7</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>cn.hutool</groupId>
|
||||
<artifactId>hutool-all</artifactId>
|
||||
<version>5.8.26</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>commons-codec</groupId>
|
||||
<artifactId>commons-codec</artifactId>
|
||||
<version>1.17.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.maibu</groupId>
|
||||
<artifactId>maibu-iot-service</artifactId>
|
||||
<version>${maibu.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.maibu</groupId>
|
||||
<artifactId>maibu-common</artifactId>
|
||||
<version>${maibu.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.maibu</groupId>
|
||||
<artifactId>maibu-netty-server</artifactId>
|
||||
<version>${maibu.version}</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<properties>
|
||||
<maven.compiler.source>8</maven.compiler.source>
|
||||
<maven.compiler.target>8</maven.compiler.target>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
</properties>
|
||||
|
||||
</project>
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.maibu.common;
|
||||
|
||||
public class MiddleCommandConstant {
|
||||
|
||||
public static byte remoteControl = (byte)0x00;
|
||||
|
||||
public static byte heartbeat = (byte)0xff;
|
||||
|
||||
public static byte path = (byte)0x01;
|
||||
|
||||
public static byte status = (byte)0x02;
|
||||
|
||||
public static byte connect = (byte)0x03;
|
||||
|
||||
public static byte interaction = (byte)0x12; //主要用于推送状态消息等
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.maibu.common;
|
||||
|
||||
import io.netty.util.AttributeKey;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class MiddleConstant {
|
||||
|
||||
public static final AttributeKey<String> ATT_MASTER_KEY = AttributeKey.valueOf("ATT_MASTER_KEY");
|
||||
|
||||
// public static String nettyApiUrl = "http://127.0.0.1:8081";
|
||||
public static String nettyApiUrl = "https://serviceri.satabot.com";
|
||||
public static String vertxIp = "1.95.137.212";
|
||||
// public static String nettyApiUrl = "http://127.0.0.1:8081";
|
||||
// public static String vertxIp = "127.0.0.1";
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.maibu.config;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.http.client.SimpleClientHttpRequestFactory;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
@Configuration
|
||||
public class RestTemplateConfig {
|
||||
@Bean
|
||||
public RestTemplate restTemplate() {
|
||||
SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
|
||||
factory.setConnectTimeout(5000); // 连接超时 5 秒
|
||||
factory.setReadTimeout(5000); // 读取超时 5 秒
|
||||
|
||||
RestTemplate restTemplate = new RestTemplate(factory);
|
||||
|
||||
// // 只保留JSON转换器,确保只处理JSON格式的数据
|
||||
// List<HttpMessageConverter<?>> converters = new ArrayList<>();
|
||||
// converters.add(new MappingJackson2HttpMessageConverter());
|
||||
// restTemplate.setMessageConverters(converters);
|
||||
|
||||
return restTemplate;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
package com.maibu.control;
|
||||
|
||||
public class DiffSteer {
|
||||
|
||||
public static final int Polarity_High = 0; //0
|
||||
public static final int Polarity_Low = 0; //1
|
||||
|
||||
private float turnSpeedScale;
|
||||
private float forwardSpeedScale;
|
||||
private int speedAmplLimit;
|
||||
private int leftWheelSpeedDir;
|
||||
private int rightWheelSpeedDir;
|
||||
private int xDir;
|
||||
private int yDir;
|
||||
private int leftWheelSpeed;
|
||||
private int rightWheelSpeed;
|
||||
|
||||
public int getLeftWheelSpeed() {
|
||||
return leftWheelSpeed;
|
||||
}
|
||||
|
||||
public void setLeftWheelSpeed(int leftWheelSpeed) {
|
||||
this.leftWheelSpeed = leftWheelSpeed;
|
||||
}
|
||||
|
||||
public int getRightWheelSpeed() {
|
||||
return rightWheelSpeed;
|
||||
}
|
||||
|
||||
public void setRightWheelSpeed(int rightWheelSpeed) {
|
||||
this.rightWheelSpeed = rightWheelSpeed;
|
||||
}
|
||||
|
||||
// 初始化函数
|
||||
public void init(int turnSpeedMax, int forwardSpeedMax, int xMax, int yMax, int speedAmplLimit) {
|
||||
this.turnSpeedScale = (float) turnSpeedMax / (float) xMax;
|
||||
this.forwardSpeedScale = (float) forwardSpeedMax / (float) yMax;
|
||||
this.speedAmplLimit = speedAmplLimit;
|
||||
this.leftWheelSpeedDir = Polarity_High;
|
||||
this.rightWheelSpeedDir = Polarity_High;
|
||||
this.xDir = Polarity_High;
|
||||
this.yDir = Polarity_High;
|
||||
}
|
||||
|
||||
// 设置方向极性
|
||||
public void setDirPolarity(int leftWheelSpeedDir, int rightWheelSpeedDir, int xDir, int yDir) {
|
||||
this.leftWheelSpeedDir = leftWheelSpeedDir;
|
||||
this.rightWheelSpeedDir = rightWheelSpeedDir;
|
||||
this.xDir = xDir;
|
||||
this.yDir = yDir;
|
||||
}
|
||||
|
||||
// 计算左右轮速度
|
||||
public void calcWheelSpeed(int x, int y) {
|
||||
int turnSpeed = signOperator((int) (x * turnSpeedScale), xDir);
|
||||
int forwardSpeed = signOperator((int) (y * forwardSpeedScale), yDir);
|
||||
|
||||
turnSpeed = (forwardSpeed >= 0 ? turnSpeed : -turnSpeed);
|
||||
leftWheelSpeed = signOperator(forwardSpeed + turnSpeed, leftWheelSpeedDir);
|
||||
rightWheelSpeed = signOperator(forwardSpeed - turnSpeed, rightWheelSpeedDir);
|
||||
|
||||
leftWheelSpeed = clamp(leftWheelSpeed, -speedAmplLimit, speedAmplLimit);
|
||||
rightWheelSpeed = clamp(rightWheelSpeed, -speedAmplLimit, speedAmplLimit);
|
||||
}
|
||||
|
||||
// 设置前向速比
|
||||
public void setTurnSpeedScale(int turnSpeedMax, int xMax) {
|
||||
this.turnSpeedScale = (float) turnSpeedMax / (float) xMax;
|
||||
}
|
||||
|
||||
// 设置转向速比
|
||||
public void setForwardSpeedScale(int forwardSpeedMax, int yMax) {
|
||||
this.forwardSpeedScale = (float) forwardSpeedMax / (float) yMax;
|
||||
}
|
||||
|
||||
// 设置电机转速的最大速度限制
|
||||
public void setSpeedAmplLimit(int speedAmplLimit) {
|
||||
this.speedAmplLimit = speedAmplLimit;
|
||||
}
|
||||
|
||||
// 辅助函数:符号操作
|
||||
private int signOperator(int num, int polarity) {
|
||||
return polarity != 0 ? -num : num;
|
||||
}
|
||||
|
||||
// 辅助函数:限制值在范围内
|
||||
private int clamp(int val, int min, int max) {
|
||||
return (val < min) ? min : ((val > max) ? max : val);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
package com.maibu.controller;
|
||||
|
||||
import com.fastbee.common.core.controller.BaseController;
|
||||
import com.fastbee.common.core.domain.AjaxResult;
|
||||
import com.fastbee.manager.DeviceSessionManager;
|
||||
import com.fastbee.memory.MiddleGlobalMemory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/websocket")
|
||||
public class WebMiddlewareController extends BaseController {
|
||||
|
||||
@Autowired
|
||||
private DeviceSessionManager deviceSessionManager;
|
||||
|
||||
|
||||
@GetMapping("/judgeConnection")
|
||||
public AjaxResult JudgeConnection(@RequestParam String userName, @RequestParam Integer sourceType, @RequestParam String token) {
|
||||
|
||||
String source = "web";
|
||||
if (sourceType == 1) {
|
||||
source = "web";
|
||||
} else if (sourceType == 2) {
|
||||
source = "app";
|
||||
} else if (sourceType == 3) {
|
||||
source = "pc";
|
||||
}
|
||||
String key = userName + ":" + source + ":" + token;
|
||||
if (MiddleGlobalMemory.onlineSockets.containsKey(key)) {
|
||||
return AjaxResult.success(true);
|
||||
} else {
|
||||
String keyContains = userName + ":" + source;
|
||||
boolean res = true;
|
||||
for (String s : MiddleGlobalMemory.onlineSockets.keySet()) {
|
||||
if (s.contains(keyContains)) {
|
||||
res = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return AjaxResult.success(res);
|
||||
}
|
||||
}
|
||||
|
||||
@GetMapping("/allMemory")
|
||||
public AjaxResult memory() {
|
||||
Map<String, Object> res = new HashMap<>();
|
||||
res.put("deviceMap", deviceSessionManager.getDeviceMap());
|
||||
res.put("channelToDeviceIdMap", deviceSessionManager.getChannelToDeviceIdMap());
|
||||
res.put("channelMap", deviceSessionManager.getChannelMap());
|
||||
res.put("onlineSockets", MiddleGlobalMemory.onlineSockets.size());
|
||||
res.put("controlMap", MiddleGlobalMemory.controlMap);
|
||||
List<String> nettyClients = new ArrayList<>();
|
||||
MiddleGlobalMemory.nettyClientMap.values().forEach(x -> {
|
||||
if (x != null) {
|
||||
nettyClients.add(x.userName +":"+ x.token);
|
||||
}
|
||||
});
|
||||
res.put("nettyClientMap", nettyClients);
|
||||
return AjaxResult.success(res);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package com.maibu.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
@Data
|
||||
public class DeviceRunningStatus {
|
||||
|
||||
private String deviceId;
|
||||
|
||||
private String voltage; //电压
|
||||
|
||||
private String leftTargetSpeed; // 左轮目标速度
|
||||
|
||||
private String rightTargetSpeed;// 右轮目标速度
|
||||
|
||||
private String leftMeasureSpeed;// 左轮测量速度
|
||||
|
||||
private String rightMeasureSpeed;// 右轮测量速度
|
||||
|
||||
private String leftCurrent;// 左轮电流
|
||||
|
||||
private String rightCurrent;// 右轮电流
|
||||
|
||||
private String leftMotorTemp;// 左轮温度
|
||||
|
||||
private String rightMotorTemp;// 右轮温度
|
||||
|
||||
private String chipTemp;//芯片温度
|
||||
|
||||
private String yaw;//偏航角
|
||||
|
||||
private String pitch;//仰俯角
|
||||
|
||||
private String roll;//翻滚角
|
||||
|
||||
private String satelliteCnt;//跟踪的卫星数
|
||||
|
||||
private String qual;//定位质量
|
||||
|
||||
private String headingStatus;//航向角状态
|
||||
|
||||
private String latitude;//纬度
|
||||
|
||||
private String longitude;//经度
|
||||
|
||||
private Date time;
|
||||
|
||||
private String cuttingSpeed;//割刀速度
|
||||
|
||||
private String controlMode;//控制模式
|
||||
|
||||
private String battery;//电池电量
|
||||
|
||||
private String workingArea;//作业面积
|
||||
|
||||
private String obstacleSign;//障碍物标志位
|
||||
|
||||
private Date createTime;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.maibu.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class DeviceStatusDetail {
|
||||
private String name;
|
||||
private String value;
|
||||
private String unit;
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
package com.maibu.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
|
||||
@Data
|
||||
public class LawnMoverProtocolEntity {
|
||||
|
||||
public LawnMoverProtocolEntity() {
|
||||
Header1 = (byte) 0xAB;
|
||||
Header2 = (byte) 0xAA;
|
||||
CommandType = 0x00;
|
||||
LeftWheelSpeed = 0x0000;
|
||||
RightWheelSpeed = 0x0000;
|
||||
ChassisLiftFlag = 0x00;
|
||||
MowerSpeedFlag = 0x00;
|
||||
IgnitionFlag = 0x00;
|
||||
EmergencyStopFlag = 0x00;
|
||||
Crc16 = 0x0000;
|
||||
EndFlag1 = (byte) 0xAA;
|
||||
EndFlag2 = (byte) 0xAB;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "LawnMoverProtocolEntity{" +
|
||||
"Header1=" + Header1 +
|
||||
", Header2=" + Header2 +
|
||||
", CommandType=" + CommandType +
|
||||
", LeftWheelSpeed=" + LeftWheelSpeed +
|
||||
", RightWheelSpeed=" + RightWheelSpeed +
|
||||
", ChassisLiftFlag=" + ChassisLiftFlag +
|
||||
", MowerSpeedFlag=" + MowerSpeedFlag +
|
||||
", IgnitionFlag=" + IgnitionFlag +
|
||||
", EmergencyStopFlag=" + EmergencyStopFlag +
|
||||
", Crc16=" + Crc16 +
|
||||
", EndFlag1=" + EndFlag1 +
|
||||
", EndFlag2=" + EndFlag2 +
|
||||
'}';
|
||||
}
|
||||
|
||||
public byte[] toBytes() {
|
||||
ByteBuffer buffer = ByteBuffer.allocate(15);
|
||||
buffer.order(ByteOrder.LITTLE_ENDIAN); // 根据协议决定字节序
|
||||
buffer.put(Header1);
|
||||
buffer.put(Header2);
|
||||
buffer.put(CommandType);
|
||||
buffer.putShort(LeftWheelSpeed);
|
||||
buffer.putShort(RightWheelSpeed);
|
||||
buffer.put(ChassisLiftFlag);
|
||||
buffer.put(MowerSpeedFlag);
|
||||
buffer.put(IgnitionFlag);
|
||||
buffer.put(EmergencyStopFlag);
|
||||
buffer.putShort(Crc16); // 如果要计算真实 CRC16 可在此插入计算逻辑
|
||||
buffer.put(EndFlag1);
|
||||
buffer.put(EndFlag2);
|
||||
return buffer.array();
|
||||
}
|
||||
|
||||
public static String printBytesToHex(byte[] bytes) {
|
||||
StringBuilder sb = new StringBuilder(bytes.length * 3);
|
||||
for (byte b : bytes) {
|
||||
sb.append(String.format("%02X ", b));
|
||||
}
|
||||
return sb.toString().trim();
|
||||
}
|
||||
|
||||
|
||||
|
||||
// 包头起始字节1
|
||||
private byte Header1;
|
||||
// 包头起始字节2
|
||||
private byte Header2;
|
||||
// 指令类型
|
||||
private byte CommandType;
|
||||
// 左轮速度:16位有符号
|
||||
private short LeftWheelSpeed;
|
||||
// 右轮速度:16位有符号
|
||||
private short RightWheelSpeed;
|
||||
// 底盘升降标志位
|
||||
private byte ChassisLiftFlag;
|
||||
// 割刀速度标志位
|
||||
private byte MowerSpeedFlag;
|
||||
// 点火标志位
|
||||
private byte IgnitionFlag;
|
||||
// 保留位
|
||||
private byte EmergencyStopFlag;
|
||||
// Crc校验
|
||||
private short Crc16;
|
||||
// 包尾字节1
|
||||
private byte EndFlag1;
|
||||
// 包尾字节2
|
||||
private byte EndFlag2;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.maibu.dto;
|
||||
|
||||
public enum MesType {
|
||||
AUTH, //登录连接权限web->be
|
||||
deviceInfo, //设备状态 be->web
|
||||
path,//路径下发 web->be
|
||||
have_logged_in,//消息
|
||||
device_status_changed,//上下线状态变化
|
||||
device_error_push, //错误推送
|
||||
heartbeat,
|
||||
remoteControl,
|
||||
detectionReport,//障碍物消息
|
||||
switchPermission,//切换控制申请
|
||||
switchResult;//切换结果
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.maibu.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class NettySwitchDeviceDTO {
|
||||
|
||||
/**
|
||||
* 主机名
|
||||
*/
|
||||
private String platform;
|
||||
/**
|
||||
* 设备id
|
||||
*/
|
||||
private String deviceId;
|
||||
/**
|
||||
* 设备别名
|
||||
*/
|
||||
private String deviceAlias;
|
||||
|
||||
/**
|
||||
* 用户名
|
||||
*/
|
||||
private String userId;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.maibu.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class ObstacleMessageDTO {
|
||||
private String data;
|
||||
private MesType type;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.maibu.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
public class RemoteData {
|
||||
|
||||
private List<Double> axes;
|
||||
private List<Boolean> buttons;
|
||||
private Boolean connected;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.maibu.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class ResultDTO {
|
||||
private Object code;
|
||||
private String msg;
|
||||
private Object data;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.maibu.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class WebAuthResponseDTO {
|
||||
private String userName;
|
||||
private String deviceId;
|
||||
private String platform;
|
||||
private Boolean isAuth;
|
||||
private MesType type;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.maibu.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class WebHasLoginDTO {
|
||||
private MesType type;
|
||||
private String userName;
|
||||
private String token;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.maibu.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class WebMessageBaseDTO {
|
||||
private MesType type;
|
||||
private String userName;
|
||||
private String deviceId;
|
||||
private String token;
|
||||
private String message;
|
||||
private RemoteData remoteData;
|
||||
private Boolean switchResult;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.maibu.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class WebOlineStatusMessageDTO {
|
||||
private String deviceId;
|
||||
private Integer onlineStatus;
|
||||
private Integer hasError;
|
||||
private String errorCode;
|
||||
private String description;
|
||||
private MesType type;
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.maibu.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
public class WebStatusMessageDTO {
|
||||
private List<DeviceStatusDetail> data;
|
||||
private MesType type;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.maibu.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class WebSwitchControlResponseDTO {
|
||||
private DeviceRespondDTO respond;
|
||||
private MesType type;
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
package com.maibu.memory;
|
||||
|
||||
import com.fastbee.control.DiffSteer;
|
||||
import com.fastbee.netty.NettyClient;
|
||||
import io.vertx.core.Vertx;
|
||||
import lombok.Data;
|
||||
import org.java_websocket.WebSocket;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
@Component
|
||||
@Data
|
||||
public class MiddleGlobalMemory {
|
||||
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(MiddleGlobalMemory.class);
|
||||
/**
|
||||
* -- GETTER --
|
||||
* 获取Vert.x实例
|
||||
*/
|
||||
public static Vertx vertx;
|
||||
|
||||
// 存储所有在线WebSocket连接
|
||||
// key: test:web:token
|
||||
public static final ConcurrentHashMap<String, WebSocket> onlineSockets = new ConcurrentHashMap<>();
|
||||
|
||||
// 当前账号控制中的设备 value: test:web:token key deviceId
|
||||
public static final ConcurrentHashMap<String, String> controlMap = new ConcurrentHashMap<>();
|
||||
|
||||
public static final ConcurrentHashMap<String, NettyClient> nettyClientMap = new ConcurrentHashMap<>();
|
||||
|
||||
public static DiffSteer diffSteer = null;
|
||||
|
||||
public static void initDiff() {
|
||||
if (diffSteer == null) {
|
||||
diffSteer = new DiffSteer();
|
||||
diffSteer.init(3000, 3000, 1000, 1000, 3000);
|
||||
diffSteer.setDirPolarity(
|
||||
DiffSteer.Polarity_Low,
|
||||
DiffSteer.Polarity_High,
|
||||
DiffSteer.Polarity_Low,
|
||||
DiffSteer.Polarity_High
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// web端操作的权限只能有一个web 端操作设备 key deviceId,value userName:web:token
|
||||
// // todo 发送请求 请求成功下发,一端下线后才能解锁
|
||||
// public static final ConcurrentHashMap<String, String> controlLockMap = new ConcurrentHashMap<>();
|
||||
|
||||
|
||||
//删除web端操作的控制锁
|
||||
// public static void removeLockByMaster(String value) {
|
||||
// if(!CollectionUtils.isEmpty(controlLockMap)){
|
||||
// controlLockMap.keySet().removeIf(controlLock -> controlLockMap.get(controlLock).equals(value));
|
||||
// }
|
||||
// }
|
||||
|
||||
/**
|
||||
* 关闭资源
|
||||
*/
|
||||
public void vertxClose() {
|
||||
if (vertx != null) {
|
||||
vertx.close(res -> {
|
||||
if (res.succeeded()) {
|
||||
logger.info("Vert.x 关闭成功");
|
||||
} else {
|
||||
logger.error("Vert.x 关闭失败: {}", res.cause().getMessage());
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public static String findBySocket(WebSocket socket) {
|
||||
return onlineSockets.entrySet().stream()
|
||||
.filter(entry -> Objects.equals(entry.getValue(), socket))
|
||||
.map(Map.Entry::getKey)
|
||||
.findFirst().orElse(null);
|
||||
}
|
||||
|
||||
public static String findNettyClient(NettyClient nettyClient) {
|
||||
return nettyClientMap.entrySet().stream()
|
||||
.filter(entry -> Objects.equals(entry.getValue(), nettyClient))
|
||||
.map(Map.Entry::getKey)
|
||||
.findFirst().orElse(null);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
package com.maibu.memory;
|
||||
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import javax.annotation.PostConstruct;
|
||||
import java.lang.management.ManagementFactory;
|
||||
import java.lang.management.OperatingSystemMXBean;
|
||||
import java.lang.management.ThreadMXBean;
|
||||
import java.util.Arrays;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
// 系统监控类
|
||||
@Component
|
||||
public class SystemMonitor {
|
||||
private final Logger logger = LoggerFactory.getLogger(SystemMonitor.class);
|
||||
|
||||
// 启动监控任务
|
||||
@PostConstruct
|
||||
public void startMonitoring() {
|
||||
// JVM 内存监控
|
||||
startMemoryMonitor();
|
||||
|
||||
// CPU 使用率监控
|
||||
startCpuMonitor();
|
||||
|
||||
// 线程状态监控
|
||||
startThreadMonitor();
|
||||
|
||||
// 连接数监控
|
||||
startConnectionMonitor();
|
||||
}
|
||||
|
||||
// 内存监控
|
||||
private void startMemoryMonitor() {
|
||||
ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor();
|
||||
executor.scheduleAtFixedRate(() -> {
|
||||
Runtime runtime = Runtime.getRuntime();
|
||||
long totalMemory = runtime.totalMemory() / (1024 * 1024);
|
||||
long freeMemory = runtime.freeMemory() / (1024 * 1024);
|
||||
long usedMemory = totalMemory - freeMemory;
|
||||
long maxMemory = runtime.maxMemory() / (1024 * 1024);
|
||||
|
||||
double memoryUsage = (double) usedMemory / maxMemory * 100;
|
||||
|
||||
logger.info("内存使用情况 - 总内存: {}MB, 已用内存: {}MB, 空闲内存: {}MB, 使用率: {}",
|
||||
totalMemory, usedMemory, freeMemory, String.format("%.2f", memoryUsage));
|
||||
|
||||
if (memoryUsage > 80) {
|
||||
logger.warn("内存使用率过高: {}", String.format("%.2f", memoryUsage));
|
||||
// 可以添加告警逻辑
|
||||
}
|
||||
}, 1, 5, TimeUnit.MINUTES);
|
||||
}
|
||||
|
||||
// CPU 监控
|
||||
private void startCpuMonitor() {
|
||||
ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor();
|
||||
executor.scheduleAtFixedRate(() -> {
|
||||
try {
|
||||
// 使用 OperatingSystemMXBean 获取 CPU 使用率
|
||||
OperatingSystemMXBean osBean = ManagementFactory.getOperatingSystemMXBean();
|
||||
if (osBean instanceof com.sun.management.OperatingSystemMXBean) {
|
||||
com.sun.management.OperatingSystemMXBean sunOsBean =
|
||||
(com.sun.management.OperatingSystemMXBean) osBean;
|
||||
double cpuUsage = sunOsBean.getSystemCpuLoad() * 100;
|
||||
|
||||
logger.info("CPU 使用率: {}", String.format("%.2f", cpuUsage));
|
||||
|
||||
if (cpuUsage > 80) {
|
||||
logger.warn("CPU 使用率过高: {}", String.format("%.2f", cpuUsage));
|
||||
// 可以添加告警逻辑
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.error("CPU 监控异常: {}", e.getMessage());
|
||||
}
|
||||
}, 1, 5, TimeUnit.MINUTES);
|
||||
}
|
||||
|
||||
// 线程监控
|
||||
private void startThreadMonitor() {
|
||||
ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor();
|
||||
executor.scheduleAtFixedRate(() -> {
|
||||
ThreadMXBean threadBean = ManagementFactory.getThreadMXBean();
|
||||
int threadCount = threadBean.getThreadCount();
|
||||
int peakThreadCount = threadBean.getPeakThreadCount();
|
||||
long daemonThreadCount = threadBean.getDaemonThreadCount();
|
||||
|
||||
logger.info("线程状态 - 当前线程数: {}, 峰值线程数: {}, 守护线程数: {}",
|
||||
threadCount, peakThreadCount, daemonThreadCount);
|
||||
|
||||
// 检查死锁
|
||||
long[] deadlockedThreads = threadBean.findDeadlockedThreads();
|
||||
if (deadlockedThreads != null && deadlockedThreads.length > 0) {
|
||||
logger.error("发现死锁线程: {}", Arrays.toString(deadlockedThreads));
|
||||
// 可以添加告警逻辑
|
||||
}
|
||||
}, 1, 10, TimeUnit.MINUTES);
|
||||
}
|
||||
|
||||
// 连接数监控
|
||||
private void startConnectionMonitor() {
|
||||
ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor();
|
||||
executor.scheduleAtFixedRate(() -> {
|
||||
int webSocketCount = MiddleGlobalMemory.onlineSockets.size();
|
||||
int nettyClientCount = MiddleGlobalMemory.nettyClientMap.size();
|
||||
|
||||
logger.info("连接数统计 - WebSocket连接数: {}, Netty客户端数: {}",
|
||||
webSocketCount, nettyClientCount);
|
||||
}, 1, 5, TimeUnit.MINUTES);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package com.maibu.netty;
|
||||
|
||||
import com.fastbee.memory.MiddleGlobalMemory;
|
||||
import io.vertx.core.Vertx;
|
||||
import io.vertx.core.VertxOptions;
|
||||
import lombok.Getter;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.boot.CommandLineRunner;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 系统初始化资源类
|
||||
* 实现CommandLineRunner接口,在Spring Boot启动时自动执行
|
||||
*/
|
||||
@Getter
|
||||
@Component
|
||||
public class InitResource implements CommandLineRunner {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(InitResource.class);
|
||||
|
||||
@Override
|
||||
public void run(String... args) throws Exception {
|
||||
logger.info("InitResource 开始初始化系统资源...");
|
||||
|
||||
// 初始化Vert.x
|
||||
initVertx();
|
||||
logger.info("InitResource 系统资源初始化完成");
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化Vert.x
|
||||
*/
|
||||
private void initVertx() {
|
||||
try {
|
||||
VertxOptions options = new VertxOptions()
|
||||
.setWorkerPoolSize(20)
|
||||
.setEventLoopPoolSize(10)
|
||||
.setWarningExceptionTime(5000000000L);
|
||||
|
||||
MiddleGlobalMemory.vertx = Vertx.vertx(options);
|
||||
logger.info("Vert.x 初始化成功");
|
||||
} catch (Exception e) {
|
||||
logger.error("Vert.x 初始化失败: {}", e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,223 @@
|
||||
package com.maibu.netty;
|
||||
|
||||
import com.fastbee.common.MiddleConstant;
|
||||
import com.fastbee.netty.handler.*;
|
||||
import io.netty.bootstrap.Bootstrap;
|
||||
import io.netty.channel.nio.NioEventLoopGroup;
|
||||
import io.netty.channel.socket.SocketChannel;
|
||||
import io.netty.channel.socket.nio.NioSocketChannel;
|
||||
import io.netty.handler.timeout.IdleStateHandler;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* Netty客户端实现类
|
||||
* 提供与TCP服务器的连接管理、消息发送、重连机制和心跳检测功能
|
||||
*/
|
||||
public class NettyClient {
|
||||
private static final Logger logger = LoggerFactory.getLogger(NettyClient.class);
|
||||
|
||||
// private static volatile NettyClient instance;
|
||||
|
||||
public String token;
|
||||
public String userName;
|
||||
|
||||
public String requestDeviceId; //前端需要连接的设备
|
||||
public String currentDeviceId; //当前连接的设备
|
||||
|
||||
public Long heartBeatTimer;
|
||||
|
||||
public String key; //userName + web + token
|
||||
|
||||
public String host;
|
||||
public int port;
|
||||
public int reconnectDelay = 5;
|
||||
public int heartbeatInterval = 5;
|
||||
|
||||
public EventLoopGroup group;
|
||||
public Bootstrap bootstrap;
|
||||
public Channel channel;
|
||||
|
||||
public Long lastSwitchTime = -1L;
|
||||
|
||||
public Long Interval = 5000L;
|
||||
|
||||
public static final byte[] HEARTBEAT_PACKET = new byte[]{(byte) 0xAB, (byte) 0xAA, (byte) 0xFF, (byte) 0xAA, (byte) 0xAB};
|
||||
|
||||
|
||||
// /**
|
||||
// * 获取单例实例(无回调)
|
||||
// */
|
||||
// public static NettyClient getInstance() {
|
||||
// if (instance == null) {
|
||||
// synchronized (NettyClient.class) {
|
||||
// if (instance == null) {
|
||||
// instance = new NettyClient();
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// return instance;
|
||||
// }
|
||||
|
||||
/**
|
||||
* 初始化并连接到服务器
|
||||
*/
|
||||
public void initConnect(String host, int port) {
|
||||
if (channel != null && channel.isActive()) {
|
||||
logger.info("Netty客户端已连接,不重复初始化");
|
||||
return;
|
||||
}
|
||||
this.host = host;
|
||||
this.port = port;
|
||||
connect();
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化Netty客户端
|
||||
*/
|
||||
public void initClient() {
|
||||
group = new NioEventLoopGroup();
|
||||
bootstrap = new Bootstrap();
|
||||
bootstrap.group(group)
|
||||
.channel(NioSocketChannel.class)
|
||||
.option(ChannelOption.SO_KEEPALIVE, true)
|
||||
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000)
|
||||
.handler(new ChannelInitializer<SocketChannel>() {
|
||||
@Override
|
||||
protected void initChannel(SocketChannel ch) {
|
||||
ChannelPipeline pipeline = ch.pipeline();
|
||||
// 设置共享属性
|
||||
ch.attr(MiddleConstant.ATT_MASTER_KEY).set(key);
|
||||
// 空闲检测处理器(心跳)
|
||||
pipeline.addLast(new IdleStateHandler(0, heartbeatInterval, 0, TimeUnit.SECONDS));
|
||||
pipeline.addLast(new MiddleHeaderFooterDecoder());
|
||||
// 编解码器
|
||||
pipeline.addLast(new MiddleHexDecoder());
|
||||
pipeline.addLast(new MiddleHexEncoder());
|
||||
// 自定义处理器
|
||||
pipeline.addLast(new ClientHandler());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 连接到服务器
|
||||
*/
|
||||
public void connect() {
|
||||
|
||||
if (bootstrap == null || host == null || port <= 0) {
|
||||
logger.error("Netty客户端未正确初始化");
|
||||
return;
|
||||
}
|
||||
|
||||
logger.info("正在连接到Netty服务器: {}:{}", host, port);
|
||||
bootstrap.connect(host, port).addListener((ChannelFutureListener) future -> {
|
||||
if (future.isSuccess()) {
|
||||
channel = future.channel();
|
||||
logger.info("Netty客户端连接成功: {}:{}", host, port);
|
||||
// notifyConnectionStatus(true);
|
||||
} else {
|
||||
logger.error("Netty客户端连接失败,{}秒后重试: {}", reconnectDelay, future.cause().getMessage());
|
||||
// notifyConnectionStatus(false);
|
||||
// 重连
|
||||
group.schedule(this::connect, reconnectDelay, TimeUnit.SECONDS);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 断开连接
|
||||
*/
|
||||
public void disconnect() {
|
||||
if (channel != null) {
|
||||
channel.close();
|
||||
channel = null;
|
||||
}
|
||||
// notifyConnectionStatus(false);
|
||||
logger.info("Netty客户端已断开连接");
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送数据
|
||||
*/
|
||||
public void sendData(byte[] data) {
|
||||
if (channel != null && channel.isActive()) {
|
||||
channel.writeAndFlush(data).addListener((ChannelFutureListener) future -> {
|
||||
if (future.isSuccess()) {
|
||||
logger.debug("Netty客户端发送数据成功: {}", bytesToHex(data));
|
||||
} else {
|
||||
logger.error("Netty客户端发送数据失败: {}", future.cause().getMessage());
|
||||
}
|
||||
});
|
||||
} else {
|
||||
logger.error("Netty客户端未连接,发送数据失败");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送十六进制字符串数据
|
||||
*/
|
||||
public void sendHexData(String hexData) {
|
||||
try {
|
||||
byte[] data = hexStringToBytes(hexData);
|
||||
sendData(data);
|
||||
} catch (IllegalArgumentException e) {
|
||||
logger.error("无效的十六进制字符串: {}", hexData);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 十六进制字符串转字节数组
|
||||
*/
|
||||
private static byte[] hexStringToBytes(String hexString) {
|
||||
hexString = hexString.replaceAll("\\s+", "");
|
||||
if (hexString.length() % 2 != 0) {
|
||||
throw new IllegalArgumentException("十六进制字符串长度必须为偶数");
|
||||
}
|
||||
byte[] result = new byte[hexString.length() / 2];
|
||||
for (int i = 0; i < hexString.length(); i += 2) {
|
||||
result[i / 2] = (byte) Integer.parseInt(hexString.substring(i, i + 2), 16);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 字节数组转十六进制字符串
|
||||
*/
|
||||
private static String bytesToHex(byte[] bytes) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (byte b : bytes) {
|
||||
sb.append(String.format("%02X ", b));
|
||||
}
|
||||
return sb.toString().trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查客户端是否已连接
|
||||
*/
|
||||
public boolean isConnected() {
|
||||
return channel != null && channel.isActive();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 设置重连延迟时间(秒)
|
||||
*/
|
||||
public void setReconnectDelay(int reconnectDelay) {
|
||||
if (reconnectDelay > 0) {
|
||||
this.reconnectDelay = reconnectDelay;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置心跳间隔时间(秒)
|
||||
*/
|
||||
public void setHeartbeatInterval(int heartbeatInterval) {
|
||||
if (heartbeatInterval > 0) {
|
||||
this.heartbeatInterval = heartbeatInterval;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,494 @@
|
||||
package com.maibu.netty.handler;
|
||||
|
||||
import cn.hutool.json.JSONObject;
|
||||
import com.fastbee.common.MiddleCommandConstant;
|
||||
import com.fastbee.common.MiddleConstant;
|
||||
import com.fastbee.common.utils.json.JsonUtils;
|
||||
import com.fastbee.common.utils.spring.SpringUtils;
|
||||
import com.fastbee.dto.*;
|
||||
import com.fastbee.memory.MiddleGlobalMemory;
|
||||
import com.fastbee.netty.NettyClient;
|
||||
import com.fastbee.utils.HttpService;
|
||||
import com.fastbee.websocket.WebsocketHandler;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.channel.ChannelInboundHandlerAdapter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.java_websocket.WebSocket;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* Netty客户端处理器
|
||||
*/
|
||||
@Slf4j
|
||||
public class ClientHandler extends ChannelInboundHandlerAdapter {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(ClientHandler.class);
|
||||
|
||||
private static final HttpService httpService = SpringUtils.getBean(HttpService.class);
|
||||
|
||||
@Override
|
||||
public void channelRead(ChannelHandlerContext ctx, Object msg) {
|
||||
try {
|
||||
byte[] data = (byte[]) msg;
|
||||
logger.debug("channelRead2: {}", Arrays.toString(data));
|
||||
//假如是心跳包
|
||||
String key = ctx.channel().attr(MiddleConstant.ATT_MASTER_KEY).get();
|
||||
WebSocket webSocket = MiddleGlobalMemory.onlineSockets.get(key);
|
||||
if (isHeartbeat(data)) {
|
||||
//websocket 不在了停止发 心跳
|
||||
if (webSocket != null && webSocket.isOpen()) {
|
||||
ctx.writeAndFlush(data);
|
||||
}
|
||||
NettyClient nettyClient = MiddleGlobalMemory.nettyClientMap.get(key);
|
||||
if (nettyClient != null && StringUtils.isEmpty(nettyClient.currentDeviceId)) {
|
||||
if (nettyClient.lastSwitchTime == -1 || System.currentTimeMillis() - nettyClient.lastSwitchTime > nettyClient.Interval) {
|
||||
if (!StringUtils.isEmpty(nettyClient.requestDeviceId) && !StringUtils.isEmpty(key)) {
|
||||
switchDevice(nettyClient, key);
|
||||
nettyClient.lastSwitchTime = System.currentTimeMillis();
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (isPushMsg(data)) {
|
||||
try {
|
||||
int dataStart = 3; // 跳过 0:AB 1:AA 2:cmd
|
||||
|
||||
// 数据体结束位置(不含CRC和帧尾)
|
||||
int dataEnd = data.length - 4; // 去掉 CRC_L、CRC_H、AA、AB
|
||||
|
||||
int dataLen = dataEnd - dataStart;
|
||||
if (dataLen <= 0) {
|
||||
System.out.println("数据体长度异常");
|
||||
return;
|
||||
}
|
||||
|
||||
byte[] jsonBytes = Arrays.copyOfRange(data, dataStart, dataEnd);
|
||||
String json = new String(jsonBytes, StandardCharsets.UTF_8);
|
||||
|
||||
logger.debug("收到推送2: {}", json);
|
||||
if (StringUtils.isNotBlank(json)) {
|
||||
JSONObject obj = new JSONObject(json);
|
||||
if (obj.get("event") != null) {
|
||||
//状态变化 上下线
|
||||
String eventType = obj.get("event").toString();
|
||||
String deviceId = obj.get("deviceId").toString();
|
||||
if ("device_status_changed".equals(eventType)) {
|
||||
String status = obj.get("status").toString();
|
||||
WebOlineStatusMessageDTO dto = new WebOlineStatusMessageDTO();
|
||||
dto.setDeviceId(deviceId);
|
||||
dto.setType(MesType.device_status_changed);
|
||||
if (status.equals("offline")) {
|
||||
dto.setOnlineStatus(0);
|
||||
} else {
|
||||
dto.setOnlineStatus(1);
|
||||
}
|
||||
WebsocketHandler.sendMessageToClient(webSocket, JsonUtils.toJsonString(dto));
|
||||
} else if ("device_error_push".equals(eventType)) {
|
||||
//todo 更新推送的错误
|
||||
String details = obj.get("details").toString();
|
||||
WebOlineStatusMessageDTO dto = new WebOlineStatusMessageDTO();
|
||||
dto.setDeviceId(deviceId);
|
||||
dto.setType(MesType.device_error_push);
|
||||
dto.setHasError(1);
|
||||
dto.setDescription("");
|
||||
dto.setErrorCode("");
|
||||
WebsocketHandler.sendMessageToClient(webSocket, JsonUtils.toJsonString(dto));
|
||||
}
|
||||
} else {
|
||||
/* 判断 respond 字段 */
|
||||
if (obj.get("request") != null) {
|
||||
String deviceId = obj.get("deviceId").toString();
|
||||
String userName = obj.get("userId").toString();
|
||||
String platform = obj.get("platform").toString();
|
||||
//直接转发 websocket 申请
|
||||
WebAuthResponseDTO request = new WebAuthResponseDTO();
|
||||
request.setType(MesType.switchPermission);
|
||||
request.setUserName(userName);
|
||||
request.setDeviceId(deviceId);
|
||||
request.setPlatform(platform);
|
||||
logger.debug("json: {}", JsonUtils.toJsonString(request));
|
||||
WebsocketHandler.sendMessageToClient(webSocket, JsonUtils.toJsonString(request));
|
||||
} else {
|
||||
if (obj.get("respond") != null) {
|
||||
if ("have_logged_in".equals(obj.get("respond").toString())) {
|
||||
/* 如果回复 have_logged_in 则调用logout */
|
||||
//System.out.println("检测到重复登录,执行 logout");
|
||||
//NettyClient nettyClient = GlobalMemory.nettyClientMap.get(key);
|
||||
//WebHasLoginDTO dto = new WebHasLoginDTO();
|
||||
//dto.setToken(nettyClient.token);
|
||||
//dto.setUserName(nettyClient.userName);
|
||||
//dto.setType(MesType.have_logged_in);
|
||||
//WebsocketHandler.sendMessageToClient(webSocket, JsonUtils.toJsonString(dto));
|
||||
} else {
|
||||
//todo respond netty_server返回结果处理
|
||||
DeviceRespondDTO respond = JsonUtils.parseObject(obj.get("respond").toString(), DeviceRespondDTO.class);
|
||||
if (respond != null) {
|
||||
//直接转发 websocket 申请
|
||||
WebSwitchControlResponseDTO responseDTO = new WebSwitchControlResponseDTO();
|
||||
responseDTO.setType(MesType.switchResult);
|
||||
responseDTO.setRespond(respond);
|
||||
WebsocketHandler.sendMessageToClient(webSocket, JsonUtils.toJsonString(responseDTO));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (obj.get("type").toString() != null && "detectionReport".equals(obj.get("type").toString())) {
|
||||
WebsocketHandler.sendMessageToClient(webSocket, json);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
//error 推送
|
||||
}
|
||||
|
||||
//TODO web 无切换控制请求
|
||||
} catch (Exception e) {
|
||||
logger.error("事件推送解析失败:{}", e.getMessage());
|
||||
}
|
||||
} else if (isStatusDate(data)) {
|
||||
//状态消息推送给前端
|
||||
WebStatusMessageDTO dto = createWebDeviceStatusMessage(data);
|
||||
WebsocketHandler.sendMessageToClient(webSocket, JsonUtils.toJsonString(dto));
|
||||
} else if (isPath(data)) {
|
||||
//已到达
|
||||
if (data[5] == (byte) 0x01) {
|
||||
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.error("channelRead error:{}", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public WebStatusMessageDTO createWebDeviceStatusMessage(byte[] data) {
|
||||
WebStatusMessageDTO webStatusMessageDTO = new WebStatusMessageDTO();
|
||||
if (data[2] == MiddleCommandConstant.status) {
|
||||
String deviceStatus = new String(data, 2, data.length - 4, StandardCharsets.UTF_8).trim();
|
||||
String[] split = deviceStatus.split(",");
|
||||
List<DeviceStatusDetail> transferData = transferStatusData(split);
|
||||
webStatusMessageDTO.setData(transferData);
|
||||
webStatusMessageDTO.setType(MesType.deviceInfo);
|
||||
}
|
||||
return webStatusMessageDTO;
|
||||
}
|
||||
|
||||
private List<DeviceStatusDetail> transferStatusData(String[] split) {
|
||||
List<DeviceStatusDetail> transferData = new ArrayList<>();
|
||||
|
||||
DeviceStatusDetail detail0 = new DeviceStatusDetail();
|
||||
detail0.setName("voltage");
|
||||
detail0.setValue(split[0]);
|
||||
detail0.setUnit("");
|
||||
transferData.add(detail0);
|
||||
|
||||
DeviceStatusDetail detail1 = new DeviceStatusDetail();
|
||||
detail1.setName("leftTargetSpeed");
|
||||
detail1.setValue(split[1]);
|
||||
detail1.setUnit("");
|
||||
transferData.add(detail1);
|
||||
|
||||
DeviceStatusDetail detail2 = new DeviceStatusDetail();
|
||||
detail2.setName("rightTargetSpeed");
|
||||
detail2.setValue(split[2]);
|
||||
detail2.setUnit("");
|
||||
transferData.add(detail2);
|
||||
|
||||
DeviceStatusDetail detail3 = new DeviceStatusDetail();
|
||||
detail3.setName("leftMeasureSpeed");
|
||||
detail3.setValue(split[3]);
|
||||
detail3.setUnit("");
|
||||
transferData.add(detail3);
|
||||
|
||||
DeviceStatusDetail detail4 = new DeviceStatusDetail();
|
||||
detail4.setName("rightMeasureSpeed");
|
||||
detail4.setValue(split[4]);
|
||||
detail4.setUnit("");
|
||||
transferData.add(detail4);
|
||||
|
||||
DeviceStatusDetail detail5 = new DeviceStatusDetail();
|
||||
detail5.setName("leftCurrent");
|
||||
detail5.setValue(split[5]);
|
||||
detail5.setUnit("");
|
||||
transferData.add(detail5);
|
||||
|
||||
DeviceStatusDetail detail6 = new DeviceStatusDetail();
|
||||
detail6.setName("rightCurrent");
|
||||
detail6.setValue(split[6]);
|
||||
detail6.setUnit("");
|
||||
transferData.add(detail6);
|
||||
|
||||
DeviceStatusDetail detail7 = new DeviceStatusDetail();
|
||||
detail7.setName("leftMotorTemp");
|
||||
detail7.setValue(split[7]);
|
||||
detail7.setUnit("");
|
||||
transferData.add(detail7);
|
||||
|
||||
DeviceStatusDetail detail8 = new DeviceStatusDetail();
|
||||
detail8.setName("rightMotorTemp");
|
||||
detail8.setValue(split[8]);
|
||||
detail8.setUnit("");
|
||||
transferData.add(detail8);
|
||||
|
||||
DeviceStatusDetail detail9 = new DeviceStatusDetail();
|
||||
detail9.setName("chipTemp");
|
||||
detail9.setValue(split[9]);
|
||||
detail9.setUnit("");
|
||||
transferData.add(detail9);
|
||||
|
||||
DeviceStatusDetail detail10 = new DeviceStatusDetail();
|
||||
detail10.setName("yaw");
|
||||
detail10.setValue(split[10]);
|
||||
detail10.setUnit("");
|
||||
transferData.add(detail10);
|
||||
|
||||
DeviceStatusDetail detail11 = new DeviceStatusDetail();
|
||||
detail11.setName("pitch");
|
||||
detail11.setValue(split[11]);
|
||||
detail11.setUnit("");
|
||||
transferData.add(detail11);
|
||||
|
||||
DeviceStatusDetail detail12 = new DeviceStatusDetail();
|
||||
detail12.setName("roll");
|
||||
detail12.setValue(split[12]);
|
||||
detail12.setUnit("");
|
||||
transferData.add(detail12);
|
||||
|
||||
DeviceStatusDetail detail13 = new DeviceStatusDetail();
|
||||
detail13.setName("satelliteCnt");
|
||||
detail13.setValue(split[13]);
|
||||
detail13.setUnit("");
|
||||
transferData.add(detail13);
|
||||
|
||||
DeviceStatusDetail detail14 = new DeviceStatusDetail();
|
||||
detail14.setName("headingStatus");
|
||||
detail14.setValue(split[14]);
|
||||
detail14.setUnit("");
|
||||
transferData.add(detail14);
|
||||
|
||||
DeviceStatusDetail detail15 = new DeviceStatusDetail();
|
||||
detail15.setName("headingStatus");
|
||||
detail15.setValue(split[15]);
|
||||
detail15.setUnit("");
|
||||
transferData.add(detail15);
|
||||
|
||||
DeviceStatusDetail detail16 = new DeviceStatusDetail();
|
||||
detail16.setName("latitude");
|
||||
detail16.setValue(split[16]);
|
||||
detail16.setUnit("");
|
||||
transferData.add(detail16);
|
||||
|
||||
DeviceStatusDetail detail17 = new DeviceStatusDetail();
|
||||
detail17.setName("longitude");
|
||||
detail17.setValue(split[17]);
|
||||
detail17.setUnit("");
|
||||
transferData.add(detail17);
|
||||
|
||||
//18 时间暂未给值
|
||||
|
||||
DeviceStatusDetail detail19 = new DeviceStatusDetail();
|
||||
detail19.setName("cuttingSpeed");
|
||||
detail19.setValue(split[19]);
|
||||
detail19.setUnit("");
|
||||
transferData.add(detail19);
|
||||
|
||||
DeviceStatusDetail detail20 = new DeviceStatusDetail();
|
||||
detail20.setName("controlMode");
|
||||
detail20.setValue(split[20]);
|
||||
detail20.setUnit("");
|
||||
transferData.add(detail20);
|
||||
|
||||
DeviceStatusDetail detail21 = new DeviceStatusDetail();
|
||||
detail21.setName("battery");
|
||||
detail21.setValue(split[21]);
|
||||
detail21.setUnit("");
|
||||
transferData.add(detail21);
|
||||
|
||||
DeviceStatusDetail detail22 = new DeviceStatusDetail();
|
||||
detail22.setName("workingArea");
|
||||
detail22.setValue(split[22]);
|
||||
detail22.setUnit("");
|
||||
transferData.add(detail22);
|
||||
|
||||
DeviceStatusDetail detail23 = new DeviceStatusDetail();
|
||||
detail23.setName("obstacleSign");
|
||||
detail23.setValue(split[23]);
|
||||
detail23.setUnit("");
|
||||
transferData.add(detail23);
|
||||
|
||||
return transferData;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void channelActive(ChannelHandlerContext ctx) {
|
||||
|
||||
logger.debug("连接开始channelActive:ChannelId:{} ", ctx.channel().id().asLongText());
|
||||
|
||||
//websocket连上后连接netty成功后发送登录认证包
|
||||
// key username + : web + token
|
||||
String key = ctx.channel().attr(MiddleConstant.ATT_MASTER_KEY).get();
|
||||
System.out.println(key);
|
||||
NettyClient nettyClient = MiddleGlobalMemory.nettyClientMap.get(key);
|
||||
if (nettyClient != null) {
|
||||
// 构造并发送认证包
|
||||
byte[] deviceNameBytes = key.getBytes(StandardCharsets.UTF_8);
|
||||
byte[] packet = new byte[3 + deviceNameBytes.length + 4];
|
||||
packet[0] = (byte) 0xAB;
|
||||
packet[1] = (byte) 0xAA;
|
||||
packet[2] = 0x03;
|
||||
System.arraycopy(deviceNameBytes, 0, packet, 3, deviceNameBytes.length);
|
||||
int tailStart = 3 + deviceNameBytes.length;
|
||||
packet[tailStart] = 0x00;
|
||||
packet[tailStart + 1] = 0x00;
|
||||
packet[tailStart + 2] = (byte) 0xAA;
|
||||
packet[tailStart + 3] = (byte) 0xAB;
|
||||
|
||||
ctx.writeAndFlush(packet);
|
||||
// startHeartbeat(nettyClient, ctx, key);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static void switchDevice(NettyClient nettyClient, String key) {
|
||||
try {
|
||||
// 2. 调用 POST 接口:传入 Token B(与 Token A 不同)
|
||||
String token = nettyClient.token;
|
||||
String postUrl = MiddleConstant.nettyApiUrl + "/forward/device/switchDevice";
|
||||
NettySwitchDeviceDTO dto = new NettySwitchDeviceDTO();
|
||||
dto.setDeviceId(nettyClient.requestDeviceId);
|
||||
dto.setPlatform("web");
|
||||
String result = httpService.doPost(postUrl, token, dto);
|
||||
ResultDTO resultDTO = JsonUtils.parseObject(result, ResultDTO.class);
|
||||
if (resultDTO != null && resultDTO.getData() != null) {
|
||||
if ((boolean) resultDTO.getData()) {
|
||||
logger.debug("用户:{}切换设备:{}成功", key, nettyClient.requestDeviceId);
|
||||
//切换成功推送 websocket
|
||||
nettyClient.currentDeviceId = nettyClient.requestDeviceId;
|
||||
// nettyClient.requestDeviceId = null;
|
||||
WebSocket webSocket = MiddleGlobalMemory.onlineSockets.get(key);
|
||||
if (webSocket != null && webSocket.isOpen()) {
|
||||
WebAuthResponseDTO responseDTO = new WebAuthResponseDTO();
|
||||
responseDTO.setDeviceId(nettyClient.currentDeviceId);
|
||||
responseDTO.setUserName(nettyClient.userName);
|
||||
responseDTO.setType(MesType.AUTH);
|
||||
responseDTO.setIsAuth(true);
|
||||
WebsocketHandler.sendMessageToClient(webSocket, JsonUtils.toJsonString(responseDTO));
|
||||
}
|
||||
} else {
|
||||
logger.debug("用户:{}切换设备:{}失败", key, nettyClient.requestDeviceId);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("切换设备,switchDevice发生错误", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 启动心跳检测
|
||||
*/
|
||||
private void startHeartbeat(NettyClient nettyClient, ChannelHandlerContext ctx, String key) {
|
||||
stopHeartbeat(nettyClient);
|
||||
MiddleGlobalMemory.vertx.setPeriodic(200, timerId -> {
|
||||
try {
|
||||
nettyClient.heartBeatTimer = timerId;
|
||||
if (ctx.channel() != null && ctx.channel().isActive()) {
|
||||
ctx.channel().writeAndFlush(NettyClient.HEARTBEAT_PACKET);
|
||||
logger.debug("Netty客户端:{}发送心跳包", nettyClient.key);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.error("循环任务执行异常:" + e.getMessage());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭心跳检测
|
||||
*/
|
||||
private void stopHeartbeat(NettyClient nettyClient) {
|
||||
if (nettyClient != null && nettyClient.heartBeatTimer != null) {
|
||||
MiddleGlobalMemory.vertx.cancelTimer(nettyClient.heartBeatTimer);
|
||||
nettyClient.heartBeatTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void channelInactive(ChannelHandlerContext ctx) {
|
||||
String key = ctx.channel().attr(MiddleConstant.ATT_MASTER_KEY).get();
|
||||
NettyClient nettyClient = MiddleGlobalMemory.nettyClientMap.get(key);
|
||||
// stopHeartbeat(nettyClient);
|
||||
nettyClient.currentDeviceId = null;
|
||||
logger.debug("连接断开channelInactive,ChannelId:{} ", ctx.channel().id().asLongText());
|
||||
WebSocket webSocket = MiddleGlobalMemory.onlineSockets.get(key);
|
||||
if (webSocket != null && webSocket.isOpen()) {
|
||||
ctx.channel().eventLoop().schedule(nettyClient::connect, nettyClient.reconnectDelay, TimeUnit.SECONDS);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
|
||||
String key = ctx.channel().attr(MiddleConstant.ATT_MASTER_KEY).get();
|
||||
NettyClient nettyClient = MiddleGlobalMemory.nettyClientMap.get(key);
|
||||
// stopHeartbeat(nettyClient);
|
||||
cause.printStackTrace();
|
||||
logger.error("异常断开exceptionCaught:{}", cause.getMessage());
|
||||
ctx.close();
|
||||
}
|
||||
|
||||
private boolean isHeartbeat(byte[] data) {
|
||||
return data.length == 5 &&
|
||||
data[0] == (byte) 0xAB &&
|
||||
data[1] == (byte) 0xAA &&
|
||||
data[2] == (byte) 0xFF &&
|
||||
data[3] == (byte) 0xAA &&
|
||||
data[4] == (byte) 0xAB;
|
||||
}
|
||||
|
||||
/// 服务器重新请求下位机连接指令。即重新发送连接码
|
||||
private boolean isReconnectCommand(byte[] data) {
|
||||
return data.length == 9 &&
|
||||
data[0] == (byte) 0xAB &&
|
||||
data[1] == (byte) 0xAA &&
|
||||
data[2] == (byte) 0x03 &&
|
||||
data[3] == (byte) 0x00 &&
|
||||
data[4] == (byte) 0x00 &&
|
||||
data[5] == (byte) 0x00 &&
|
||||
data[6] == (byte) 0x00 &&
|
||||
data[7] == (byte) 0xAA &&
|
||||
data[8] == (byte) 0xAB;
|
||||
}
|
||||
|
||||
private boolean isPushMsg(byte[] data) {
|
||||
return data[0] == (byte) 0xAB &&
|
||||
data[1] == (byte) 0xAA &&
|
||||
data[2] == (byte) 0x12 &&
|
||||
data[data.length - 2] == (byte) 0xAA &&
|
||||
data[data.length - 1] == (byte) 0xAB;
|
||||
|
||||
// // 只要是以 '{' 开头、以 '}' 结尾,就认为是事件 JSON
|
||||
// if (data == null || data.length < 2) return false;
|
||||
// return data[0] == '{' && data[data.length - 1] == '}';
|
||||
}
|
||||
|
||||
private boolean isStatusDate(byte[] data) {
|
||||
return data[0] == (byte) 0xAB &&
|
||||
data[1] == (byte) 0xAA &&
|
||||
data[2] == (byte) 0x02 &&
|
||||
data[data.length - 2] == (byte) 0xAA &&
|
||||
data[data.length - 1] == (byte) 0xAB;
|
||||
}
|
||||
|
||||
private boolean isPath(byte[] data) {
|
||||
return data[0] == (byte) 0xAB &&
|
||||
data[1] == (byte) 0xAA &&
|
||||
data[2] == (byte) 0x01 &&
|
||||
data[data.length - 2] == (byte) 0xAA &&
|
||||
data[data.length - 1] == (byte) 0xAB;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
package com.maibu.netty.handler;
|
||||
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.handler.codec.ByteToMessageDecoder;
|
||||
import org.apache.commons.codec.binary.Hex;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class MiddleHeaderFooterDecoder extends ByteToMessageDecoder {
|
||||
private static final byte[] HEADER = {(byte) 0xAB, (byte) 0xAA};
|
||||
private static final byte[] FOOTER = {(byte) 0xAA, (byte) 0xAB};
|
||||
private static final int MAX_FRAME_LENGTH = 500;
|
||||
private static final boolean DEBUG = true; // 可控制是否打印日志
|
||||
|
||||
@Override
|
||||
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) {
|
||||
if (DEBUG) {
|
||||
byte[] raw = new byte[in.readableBytes()];
|
||||
in.getBytes(in.readerIndex(), raw);
|
||||
System.out.println("收到原始数据:" + Hex.encodeHexString(raw).toUpperCase());
|
||||
}
|
||||
|
||||
int readerIndex = in.readerIndex();
|
||||
|
||||
while (readerIndex <= in.writerIndex() - HEADER.length) {
|
||||
// 找帧头
|
||||
int headerPos = findPattern(in, HEADER, readerIndex);
|
||||
if (headerPos == -1) {
|
||||
// 没找到帧头,丢弃无效数据
|
||||
in.readerIndex(in.writerIndex() - (HEADER.length - 1));
|
||||
return;
|
||||
}
|
||||
|
||||
// 从 header 之后找帧尾
|
||||
int searchStart = headerPos + HEADER.length;
|
||||
int footerPos = -1;
|
||||
while (searchStart <= in.writerIndex() - FOOTER.length) {
|
||||
footerPos = findPattern(in, FOOTER, searchStart);
|
||||
if (footerPos == -1) break; // 没找到尾,等待更多数据
|
||||
|
||||
// 检查是否有嵌套帧头
|
||||
int nestedHeader = findPattern(in, HEADER, searchStart);
|
||||
if (nestedHeader != -1 && nestedHeader < footerPos) {
|
||||
// 有嵌套帧头,从新头重新搜索
|
||||
headerPos = nestedHeader;
|
||||
searchStart = headerPos + HEADER.length;
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (footerPos == -1) {
|
||||
// 第一帧未完整,保留数据,等待更多
|
||||
in.readerIndex(headerPos);
|
||||
return;
|
||||
}
|
||||
|
||||
int frameLength = footerPos + FOOTER.length - headerPos;
|
||||
if (frameLength > MAX_FRAME_LENGTH) {
|
||||
// 超长帧,跳过异常数据
|
||||
in.readerIndex(headerPos + 1); // 从下一个字节重新找
|
||||
readerIndex = in.readerIndex();
|
||||
continue;
|
||||
}
|
||||
|
||||
// 提取完整帧
|
||||
in.readerIndex(headerPos);
|
||||
ByteBuf frame = in.readRetainedSlice(frameLength);
|
||||
out.add(frame);
|
||||
|
||||
// 下次从 footer 后面继续解析
|
||||
readerIndex = in.readerIndex();
|
||||
}
|
||||
|
||||
// 更新 readerIndex
|
||||
in.readerIndex(readerIndex);
|
||||
}
|
||||
|
||||
private int findPattern(ByteBuf buf, byte[] pattern, int fromIndex) {
|
||||
for (int i = fromIndex; i <= buf.writerIndex() - pattern.length; i++) {
|
||||
boolean match = true;
|
||||
for (int j = 0; j < pattern.length; j++) {
|
||||
if (buf.getByte(i + j) != pattern[j]) {
|
||||
match = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (match) return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.maibu.netty.handler;
|
||||
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.handler.codec.ByteToMessageDecoder;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class MiddleHexDecoder extends ByteToMessageDecoder {
|
||||
|
||||
@Override
|
||||
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) {
|
||||
byte[] data = new byte[in.readableBytes()];
|
||||
in.readBytes(data);
|
||||
out.add(data);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.maibu.netty.handler;
|
||||
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.handler.codec.MessageToByteEncoder;
|
||||
|
||||
public class MiddleHexEncoder extends MessageToByteEncoder<byte[]> {
|
||||
|
||||
@Override
|
||||
protected void encode(ChannelHandlerContext ctx, byte[] msg, ByteBuf out) {
|
||||
out.writeBytes(msg);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package com.maibu.utils;
|
||||
|
||||
import com.fastbee.common.MiddleCommandConstant;
|
||||
import com.fastbee.control.DiffSteer;
|
||||
import com.fastbee.dto.LawnMoverProtocolEntity;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.util.List;
|
||||
|
||||
public class ControlUtils {
|
||||
|
||||
|
||||
public static LawnMoverProtocolEntity getCarControlMessage(DiffSteer diffSteer, int originX, int originY, List<Boolean> buttons) {
|
||||
LawnMoverProtocolEntity state = new LawnMoverProtocolEntity();
|
||||
diffSteer.calcWheelSpeed(originX, originY);
|
||||
state.setCommandType(MiddleCommandConstant.remoteControl);
|
||||
state.setLeftWheelSpeed((short) diffSteer.getLeftWheelSpeed());
|
||||
state.setRightWheelSpeed((short) diffSteer.getRightWheelSpeed());
|
||||
|
||||
//对应的 button数组 L1 button【4】 R1 button【5】 A对应的button【0】 B对应button【1】 X对应button【2】 Y对应button【3】 button[7]急停
|
||||
//A升B降底盘 X割刀加速Y减速 L1点火 R1熄火
|
||||
Boolean A = buttons.get(0);
|
||||
Boolean B = buttons.get(1);
|
||||
Boolean X = buttons.get(2);
|
||||
Boolean Y = buttons.get(3);
|
||||
Boolean L1 = buttons.get(4);
|
||||
Boolean R1 = buttons.get(5);
|
||||
Boolean eStop = buttons.get(8); //急停
|
||||
//底盘升降
|
||||
int chassisLiftFlag = 0;
|
||||
if (A & !B) {
|
||||
chassisLiftFlag = 1;
|
||||
} else if (!A & B) {
|
||||
chassisLiftFlag = 2;
|
||||
}
|
||||
state.setChassisLiftFlag((byte) chassisLiftFlag);
|
||||
//割刀加减速
|
||||
int mowerSpeedFlag = 0;
|
||||
if (X & !Y) {
|
||||
mowerSpeedFlag = 1;
|
||||
} else if (!X & Y) {
|
||||
mowerSpeedFlag = 2;
|
||||
}
|
||||
state.setMowerSpeedFlag((byte) mowerSpeedFlag);
|
||||
//点火熄火
|
||||
int ignitionFlag = 0;
|
||||
if (L1 & !R1) {
|
||||
ignitionFlag = 1;
|
||||
} else if (!L1 & R1) {
|
||||
ignitionFlag = 2;
|
||||
}
|
||||
state.setIgnitionFlag((byte) ignitionFlag);
|
||||
|
||||
int emergencyStopFlag = eStop ? 1 : 0;
|
||||
state.setEmergencyStopFlag((byte) emergencyStopFlag);
|
||||
// val toBytes = state.toBytes()
|
||||
// println("toBytes==" + LawnMoverProtocolEntity.printBytesToHex(toBytes))
|
||||
return state;
|
||||
}
|
||||
|
||||
public static byte[] buildControlCommand(LawnMoverProtocolEntity entity) {
|
||||
ByteBuffer buffer = ByteBuffer.allocate(15);
|
||||
buffer.order(ByteOrder.LITTLE_ENDIAN);
|
||||
|
||||
buffer.put(entity.getHeader1());
|
||||
buffer.put(entity.getHeader2());
|
||||
buffer.put(entity.getCommandType());
|
||||
|
||||
buffer.putShort(entity.getLeftWheelSpeed());//左轮速度
|
||||
buffer.putShort(entity.getRightWheelSpeed());//右轮速度
|
||||
|
||||
buffer.put(entity.getChassisLiftFlag());//底盘升降
|
||||
buffer.put(entity.getMowerSpeedFlag());//割刀加减速
|
||||
buffer.put(entity.getIgnitionFlag());//点火状态
|
||||
buffer.put(entity.getEmergencyStopFlag());//急停
|
||||
|
||||
buffer.put((byte) 0x00);
|
||||
buffer.put((byte) 0x00);
|
||||
buffer.put(entity.getEndFlag1());
|
||||
buffer.put(entity.getEndFlag2());
|
||||
return buffer.array();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.maibu.utils;
|
||||
|
||||
public class HexUtils {
|
||||
|
||||
/**
|
||||
* 字节数组转十六进制字符串
|
||||
*/
|
||||
public static String bytesToHex(byte[] bytes) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (byte b : bytes) {
|
||||
sb.append(String.format("%02X ", b));
|
||||
}
|
||||
return sb.toString().trim();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
package com.maibu.utils;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.client.HttpClientErrorException;
|
||||
import org.springframework.web.client.HttpServerErrorException;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
@Service
|
||||
public class HttpService {
|
||||
|
||||
@Autowired
|
||||
private RestTemplate restTemplate;
|
||||
|
||||
|
||||
/**
|
||||
* 通用 GET 请求(手动传入 Token,每次可传不同值)
|
||||
*
|
||||
* @param apiUrl 目标接口地址
|
||||
* @param token 自定义 Token(可为 null,不传则不携带 Authorization 头)
|
||||
* @return 接口响应字符串
|
||||
*/
|
||||
public String doGet(String apiUrl, String token) {
|
||||
// 1. 构建请求头(携带传入的 Token)
|
||||
HttpHeaders headers = buildHeadersWithToken(token);
|
||||
// 2. 封装请求实体(仅携带请求头,GET 无需请求体)
|
||||
HttpEntity<Void> httpEntity = new HttpEntity<>(headers);
|
||||
// 3. 发送 GET 请求
|
||||
try {
|
||||
ResponseEntity<String> response = restTemplate.exchange(
|
||||
apiUrl,
|
||||
HttpMethod.GET,
|
||||
httpEntity,
|
||||
String.class
|
||||
);
|
||||
// 校验响应状态并返回结果
|
||||
if (response.getStatusCode().is2xxSuccessful()) {
|
||||
return response.getBody() == null ? "" : response.getBody();
|
||||
} else {
|
||||
throw new RuntimeException("GET 请求失败,响应码:" + response.getStatusCode() + ",响应体:" + response.getBody());
|
||||
}
|
||||
} catch (HttpClientErrorException | HttpServerErrorException e) {
|
||||
// 捕获 HTTP 4xx/5xx 异常
|
||||
throw new RuntimeException("GET 请求异常,响应码:" + e.getStatusCode() + ",响应体:" + e.getResponseBodyAsString(), e);
|
||||
} catch (Exception e) {
|
||||
// 捕获其他未知异常
|
||||
throw new RuntimeException("GET 请求未知异常:" + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 通用 POST 请求(手动传入 Token,支持 JSON 请求体,每次可传不同 Token)
|
||||
*
|
||||
* @param apiUrl 目标接口地址
|
||||
* @param token 自定义 Token(可为 null,不传则不携带 Authorization 头)
|
||||
* @param requestParam JSON 请求体(实体类/字符串均可,支持 null)
|
||||
* @return 接口响应字符串
|
||||
*/
|
||||
public String doPost(String apiUrl, String token, Object requestParam) {
|
||||
// 1. 构建请求头(携带传入的 Token)
|
||||
HttpHeaders headers = buildHeadersWithToken(token);
|
||||
// 2. 封装请求实体(携带请求头 + 请求体)
|
||||
HttpEntity<Object> httpEntity = new HttpEntity<>(requestParam, headers);
|
||||
// 3. 发送 POST 请求
|
||||
try {
|
||||
ResponseEntity<String> response = restTemplate.exchange(
|
||||
apiUrl,
|
||||
HttpMethod.POST,
|
||||
httpEntity,
|
||||
String.class
|
||||
);
|
||||
// 校验响应状态并返回结果
|
||||
if (response.getStatusCode().is2xxSuccessful()) {
|
||||
return response.getBody() == null ? "" : response.getBody();
|
||||
} else {
|
||||
throw new RuntimeException("POST 请求失败,响应码:" + response.getStatusCode() + ",响应体:" + response.getBody());
|
||||
}
|
||||
} catch (HttpClientErrorException | HttpServerErrorException e) {
|
||||
// 捕获 HTTP 4xx/5xx 异常
|
||||
throw new RuntimeException("POST 请求异常,响应码:" + e.getStatusCode() + ",响应体:" + e.getResponseBodyAsString(), e);
|
||||
} catch (Exception e) {
|
||||
// 捕获其他未知异常
|
||||
throw new RuntimeException("POST 请求未知异常:" + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建携带 Token 的通用请求头(默认 JSON 格式)
|
||||
*
|
||||
* @param token 自定义 Token(可为 null)
|
||||
* @return HttpHeaders
|
||||
*/
|
||||
private HttpHeaders buildHeadersWithToken(String token) {
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
// 设置默认 Content-Type 为 JSON 格式
|
||||
headers.setContentType(MediaType.APPLICATION_JSON_UTF8);
|
||||
headers.set("Accept", "application/json");
|
||||
// 若 Token 不为空,添加 Authorization 头(默认 Bearer 格式,可按需修改)
|
||||
if (token != null && !token.trim().isEmpty()) {
|
||||
// 如需去掉 Bearer 前缀,直接改为 headers.set("Authorization", token.trim())
|
||||
headers.set("Authorization", "Bearer " + token.trim());
|
||||
}
|
||||
// 可按需添加其他通用请求头(如 User-Agent)
|
||||
// headers.set("User-Agent", "RestTemplate-Common/1.0");
|
||||
return headers;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,303 @@
|
||||
package com.maibu.websocket;
|
||||
|
||||
import com.fastbee.common.CommandConstant;
|
||||
import com.fastbee.common.MiddleConstant;
|
||||
import com.fastbee.common.utils.json.JsonUtils;
|
||||
import com.fastbee.dto.*;
|
||||
import com.fastbee.enums.CommandRequestType;
|
||||
import com.fastbee.memory.MiddleGlobalMemory;
|
||||
import com.fastbee.netty.NettyClient;
|
||||
import com.fastbee.utils.CommandUtils;
|
||||
import com.fastbee.utils.ControlUtils;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.buffer.Unpooled;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.java_websocket.WebSocket;
|
||||
import org.java_websocket.handshake.ClientHandshake;
|
||||
import org.java_websocket.server.WebSocketServer;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.net.InetSocketAddress;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* WebSocket服务器实现类
|
||||
* 使用java-websocket库实现WebSocket服务端功能
|
||||
*/
|
||||
public class WebsocketHandler extends WebSocketServer {
|
||||
private static final Logger logger = LoggerFactory.getLogger(WebsocketHandler.class);
|
||||
|
||||
|
||||
// 存储客户端连接:key=WebSocket连接对象,value=最后一次活跃时间(毫秒)
|
||||
private final Map<WebSocket, Long> clientActiveTimeMap = new ConcurrentHashMap<>();
|
||||
// 空闲超时时间(10秒):超过10秒无消息则判定为断开
|
||||
private static final long IDLE_TIMEOUT = 10 * 1000;
|
||||
// 定时检测线程池(每隔10秒检测一次空闲连接)
|
||||
private final ScheduledExecutorService idleCheckExecutor = Executors.newSingleThreadScheduledExecutor();
|
||||
|
||||
|
||||
public WebsocketHandler(int port) {
|
||||
super(new InetSocketAddress(port));
|
||||
|
||||
// 启动空闲连接检测任务
|
||||
// startIdleCheckTask();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onOpen(WebSocket conn, ClientHandshake handshake) {
|
||||
logger.info("客户端连接成功: remoteAddress={}, 当前在线数: {}", conn.getRemoteSocketAddress(), MiddleGlobalMemory.onlineSockets.size());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClose(WebSocket conn, int code, String reason, boolean remote) {
|
||||
String key = MiddleGlobalMemory.findBySocket(conn);
|
||||
if (key != null) {
|
||||
releaseResource(key);
|
||||
logger.info("客户端断开连接: remoteAddress={}, code={}, reason={}, 当前在线数: {}", conn.getRemoteSocketAddress(), code, reason, MiddleGlobalMemory.onlineSockets.size());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMessage(WebSocket conn, String message) {
|
||||
try {
|
||||
WebMessageBaseDTO webMessageBaseDTO = JsonUtils.parseObject(message, WebMessageBaseDTO.class);
|
||||
logger.debug("收到websocket消息:{}", message);
|
||||
if (webMessageBaseDTO != null) {
|
||||
String token = webMessageBaseDTO.getToken();
|
||||
String userName = webMessageBaseDTO.getUserName();
|
||||
String deviceId = webMessageBaseDTO.getDeviceId();
|
||||
MesType type = webMessageBaseDTO.getType();
|
||||
String key = userName + ":web" + ":" + token;
|
||||
if (MesType.AUTH.equals(type)) {
|
||||
if (StringUtils.isEmpty(deviceId)) {
|
||||
return;
|
||||
}
|
||||
//todo 先关闭原先的
|
||||
WebSocket oldWebsocket = MiddleGlobalMemory.onlineSockets.get(key);
|
||||
NettyClient oldNettyClient = MiddleGlobalMemory.nettyClientMap.get(key);
|
||||
if (oldWebsocket != null && oldWebsocket.isOpen()) {
|
||||
oldWebsocket.close();
|
||||
}
|
||||
if (oldNettyClient != null && oldNettyClient.isConnected()) {
|
||||
oldNettyClient.disconnect();
|
||||
}
|
||||
MiddleGlobalMemory.onlineSockets.put(key, conn);
|
||||
//这里可以根据需要初始化Netty客户端
|
||||
NettyClient client = new NettyClient();
|
||||
client.key = key;
|
||||
client.requestDeviceId = deviceId;
|
||||
client.token = token;
|
||||
client.userName = userName;
|
||||
client.initClient();
|
||||
client.initConnect(MiddleConstant.vertxIp, 9001);
|
||||
MiddleGlobalMemory.nettyClientMap.put(key, client);
|
||||
} else if (MesType.path.equals(type)) {
|
||||
//todo下发路径 预留
|
||||
pathDistribute();
|
||||
} else if (MesType.remoteControl.equals(type)) {
|
||||
//遥控
|
||||
RemoteData remoteData = webMessageBaseDTO.getRemoteData();
|
||||
if (remoteData != null) {
|
||||
List<Double> axes = remoteData.getAxes();
|
||||
List<Boolean> buttons = remoteData.getButtons();
|
||||
Double y = axes.get(1) * 1000 * -1; //前后值反了
|
||||
Double x = axes.get(2) * 1000;
|
||||
int intX = (int) x.doubleValue();
|
||||
int intY = (int) y.doubleValue();
|
||||
MiddleGlobalMemory.initDiff();
|
||||
LawnMoverProtocolEntity entity = ControlUtils.getCarControlMessage(MiddleGlobalMemory.diffSteer, intX, intY, buttons);
|
||||
byte[] buf = ControlUtils.buildControlCommand(entity);
|
||||
logger.debug("Netty客户端:{}发送遥控数据: {}", userName, JsonUtils.toJsonString(entity));
|
||||
NettyClient nettyClient = MiddleGlobalMemory.nettyClientMap.get(key);
|
||||
if (nettyClient != null && nettyClient.isConnected()) {
|
||||
// if (!nettyClient.isConnected()) nettyClient.connect();
|
||||
nettyClient.sendData(buf);
|
||||
}
|
||||
}
|
||||
}
|
||||
// else if(MesType.heartbeat.equals(type)){
|
||||
// clientActiveTimeMap.put(conn,System.currentTimeMillis());
|
||||
// }
|
||||
else if(MesType.switchPermission.equals(type)){
|
||||
DeviceRequestDTO requestDTO = new DeviceRequestDTO();
|
||||
requestDTO.setRequest(CommandRequestType.switch_control);
|
||||
requestDTO.setToken(token);
|
||||
requestDTO.setPlatform("web");
|
||||
requestDTO.setDeviceId(deviceId);
|
||||
requestDTO.setUserId(userName);
|
||||
logger.debug("Netty客户端:{}发送操控请求数据: {}", userName, JsonUtils.toJsonString(requestDTO));
|
||||
NettyClient nettyClient = MiddleGlobalMemory.nettyClientMap.get(key);
|
||||
ByteBuf buf = Unpooled.buffer();
|
||||
CommandUtils.buildCommand(buf,requestDTO, CommandConstant.interaction);
|
||||
if (nettyClient != null && nettyClient.isConnected()) {
|
||||
// if (!nettyClient.isConnected()) nettyClient.connect();
|
||||
nettyClient.sendData(buf.array());
|
||||
}
|
||||
} else if(MesType.switchResult.equals(type)){
|
||||
DeviceRequestDTO requestDTO = new DeviceRequestDTO();
|
||||
requestDTO.setToken(token);
|
||||
requestDTO.setPlatform("web");
|
||||
requestDTO.setDeviceId(deviceId);
|
||||
requestDTO.setUserId(userName);
|
||||
DeviceRespondDTO respond = new DeviceRespondDTO();
|
||||
respond.setSwitchResult(webMessageBaseDTO.getSwitchResult());
|
||||
respond.setDeviceId(deviceId);
|
||||
requestDTO.setRespond(respond);
|
||||
logger.debug("Netty客户端:{}发送切换操控权请求反馈: {}", userName, JsonUtils.toJsonString(requestDTO));
|
||||
NettyClient nettyClient = MiddleGlobalMemory.nettyClientMap.get(key);
|
||||
ByteBuf buf = Unpooled.buffer();
|
||||
CommandUtils.buildCommand(buf,requestDTO, CommandConstant.interaction);
|
||||
if (nettyClient != null && nettyClient.isConnected()) {
|
||||
// if (!nettyClient.isConnected()) nettyClient.connect();
|
||||
nettyClient.sendData(buf.array());
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.error("onMessage error:{}", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public void releaseResource(String key) {
|
||||
MiddleGlobalMemory.onlineSockets.remove(key);
|
||||
if (MiddleGlobalMemory.nettyClientMap.containsKey(key)) {
|
||||
NettyClient client = MiddleGlobalMemory.nettyClientMap.get(key);
|
||||
if (client != null) {
|
||||
if (client.isConnected()) {
|
||||
client.disconnect();
|
||||
}
|
||||
MiddleGlobalMemory.nettyClientMap.remove(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void pathDistribute() {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(WebSocket conn, Exception ex) {
|
||||
String key = MiddleGlobalMemory.findBySocket(conn);
|
||||
logger.error("WebSocket错误: clientId={}, remoteAddress={}, error={}", key, conn != null ? conn.getRemoteSocketAddress() : null, ex.getMessage(), ex);
|
||||
// 发生错误时清理连接
|
||||
if (key != null) {
|
||||
releaseResource(key);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStart() {
|
||||
logger.info("WebSocket服务端启动成功,监听端口: {}", getPort());
|
||||
}
|
||||
|
||||
/**
|
||||
* 给指定WebSocket连接发送消息
|
||||
*/
|
||||
public static void sendMessageToClient(WebSocket conn, String message) {
|
||||
if (conn != null && conn.isOpen()) {
|
||||
conn.send(message);
|
||||
String clientId = MiddleGlobalMemory.findBySocket(conn);
|
||||
logger.debug("发送消息给客户端: clientId={}, message={}", clientId, message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 广播消息给所有连接的客户端
|
||||
*/
|
||||
public void broadcastMessage(String message) {
|
||||
Collection<WebSocket> connections = getConnections();
|
||||
for (WebSocket conn : connections) {
|
||||
if (conn.isOpen()) {
|
||||
conn.send(message);
|
||||
}
|
||||
}
|
||||
logger.debug("广播消息给所有客户端: message={}, 客户端数量: {}", message, connections.size());
|
||||
}
|
||||
|
||||
|
||||
// 核心:定时检测空闲连接(每隔10秒执行一次)
|
||||
private void startIdleCheckTask() {
|
||||
idleCheckExecutor.scheduleAtFixedRate(() -> {
|
||||
long now = System.currentTimeMillis();
|
||||
// 遍历所有连接,检测是否超时
|
||||
for (Map.Entry<WebSocket, Long> entry : clientActiveTimeMap.entrySet()) {
|
||||
WebSocket conn = entry.getKey();
|
||||
Long lastActiveTime = entry.getValue();
|
||||
|
||||
// 超时判定:当前时间 - 最后活跃时间 > 空闲超时时间
|
||||
if (now - lastActiveTime > IDLE_TIMEOUT) {
|
||||
System.out.println("客户端空闲超时:" + conn.getRemoteSocketAddress() + ",强制关闭连接");
|
||||
// 强制关闭连接(会触发 onClose,但 remote=true 表示服务端主动关闭)
|
||||
conn.close(1001, "idle timeout");
|
||||
}
|
||||
}
|
||||
}, 10, 10, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
// 核心新增:停止WebSocket服务的方法(释放端口+线程池+Netty资源)
|
||||
public void stopServer() throws InterruptedException {
|
||||
logger.info("开始关闭WebSocket服务,监听端口: {}", getPort());
|
||||
|
||||
// 1. 关闭所有客户端连接
|
||||
Collection<WebSocket> connections = getConnections();
|
||||
for (WebSocket conn : connections) {
|
||||
if (conn.isOpen()) {
|
||||
try {
|
||||
conn.close(1000, "server shutdown"); // 正常关闭客户端
|
||||
String key = MiddleGlobalMemory.findBySocket(conn);
|
||||
if (key != null) {
|
||||
releaseResource(key); // 清理对应Netty资源
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.warn("关闭客户端连接失败: {}", conn.getRemoteSocketAddress(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
// clientActiveTimeMap.clear(); // 清空空闲记录
|
||||
//
|
||||
// // 2. 关闭定时检测线程池(核心:终止非守护线程)
|
||||
// if (!idleCheckExecutor.isShutdown()) {
|
||||
// idleCheckExecutor.shutdownNow(); // 立即关闭线程池
|
||||
// try {
|
||||
// // 等待线程池终止,最多等3秒
|
||||
// if (!idleCheckExecutor.awaitTermination(3, TimeUnit.SECONDS)) {
|
||||
// logger.warn("空闲检测线程池未正常终止");
|
||||
// }
|
||||
// } catch (InterruptedException e) {
|
||||
// idleCheckExecutor.shutdownNow();
|
||||
// Thread.currentThread().interrupt();
|
||||
// }
|
||||
// }
|
||||
|
||||
// 3. 批量关闭所有Netty客户端连接
|
||||
for (NettyClient client : MiddleGlobalMemory.nettyClientMap.values()) {
|
||||
if (client != null && client.isConnected()) {
|
||||
try {
|
||||
client.disconnect();
|
||||
} catch (Exception e) {
|
||||
logger.warn("关闭Netty客户端失败", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
MiddleGlobalMemory.nettyClientMap.clear();
|
||||
MiddleGlobalMemory.onlineSockets.clear();
|
||||
|
||||
// 4. 关闭WebSocketServer本身,释放9002端口(java-websocket核心方法)
|
||||
try {
|
||||
this.stop(3000); // 3秒超时关闭服务端
|
||||
logger.info("WebSocket服务端关闭成功,端口: {}", getPort());
|
||||
} catch (InterruptedException e) {
|
||||
logger.error("关闭WebSocket服务端失败", e);
|
||||
this.stop(); // 强制关闭
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user