maven的xml引入,前提是项目加入了
bash
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-redis</artifactId>
<version>1.4.7.RELEASE</version>
</dependency>
单独加入
bash
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-codec-mqtt</artifactId>
<version>4.2.7.Final</version>
</dependency>
yam配置文件
bash
# netty 服务
netty:
server:
enabled: true
port: 1883
boss-threads: 2
worker-threads: 8
bash
package com.cqcloud.platform.config;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import com.cqcloud.platform.handler.NettyServerFactory;
import com.cqcloud.platform.service.NettyService;
import io.netty.bootstrap.ServerBootstrap;
/**
* @author weimeilayer@gmail.com ✨
* @date 💓💕 2023年9月1日 🐬🐇 💓💕
*/
@Configuration
@ConditionalOnClass(ServerBootstrap.class)
@EnableConfigurationProperties(NettyServerProperties.class)
@ConditionalOnProperty(prefix = "netty.server", name = "enabled", havingValue = "true", matchIfMissing = true)
public class NettyServerAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public NettyServerFactory nettyServerFactory(NettyServerProperties properties, NettyService nettyService) {
return new NettyServerFactory(properties, nettyService);
}
}
bash
package com.cqcloud.platform.config;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* @author weimeilayer@gmail.com ✨
* @date 💓💕 2023年9月1日 🐬🐇 💓💕
*/
@ConfigurationProperties(prefix = "netty.server")
public class NettyServerProperties {
private int port = 1883;
private boolean enabled = true;
private int bossThreads = 1;
private int workerThreads = 0; // 0表示使用默认
public int getPort() {
return port;
}
public void setPort(int port) {
this.port = port;
}
public boolean isEnabled() {
return enabled;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
public int getBossThreads() {
return bossThreads;
}
public void setBossThreads(int bossThreads) {
this.bossThreads = bossThreads;
}
public int getWorkerThreads() {
return workerThreads;
}
public void setWorkerThreads(int workerThreads) {
this.workerThreads = workerThreads;
}
}
bash
package com.cqcloud.platform.handler;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import java.io.UnsupportedEncodingException;
import java.util.List;
import java.util.Random;
/**
* MQTT 命令发送者
* @author weimeilayer@gmail.com ✨
* @date 💓💕 2023年8月27日 🐬🐇 💓💕
*/
public class MqttCommandSender {
public static void sendIoOutput(String camId, String ioNum, String action) {
try {
ObjectMapper mapper = new ObjectMapper();
ObjectNode gpio = mapper.createObjectNode();
gpio.put("ioNum", ioNum).put("action", action);
ObjectNode msg = mapper.createObjectNode();
msg.put("cmd", "ioOutput")
.put("msgId", generateMessageId())
.put("utcTs", System.currentTimeMillis() / 1000)
.set("gpioData", gpio);
TcpMqttServerHandler.sendToDevice(camId, msg.toString());
} catch (Exception e) {
e.printStackTrace();
}
}
public static void sendRs485Display(String camId, List<String> dataList) {
try {
ObjectMapper mapper = new ObjectMapper();
ArrayNode chn1Data = mapper.createArrayNode();
for (String data : dataList) {
ObjectNode dataNode = mapper.createObjectNode();
dataNode.put("data", data);
chn1Data.add(dataNode);
}
ObjectNode msg = mapper.createObjectNode();
msg.put("cmd", "rs485Transmit")
.put("msgId", generateMessageId())
.put("utcTs", System.currentTimeMillis() / 1000)
.put("encodeType", "hex2string")
.set("chn1Data", chn1Data);
TcpMqttServerHandler.sendToDevice(camId, msg.toString());
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 生成消息ID
*
* @return 生成的消息ID
*/
private static String generateMessageId() {
long millis = System.currentTimeMillis();
String random = String.format("%07d", new Random().nextInt(10000000));
return millis + random;
}
/**
* 将中文字符串转换为 GBK 编码的十六进制字符串
*
* @param input 要转换的中文字符串
* @return GBK 编码对应的十六进制字符串,如果编码失败则返回空字符串
*/
public static String stringToHex(String input) {
try {
// 1. 将字符串转换为 GBK 编码的字节数组
byte[] bytes = input.getBytes("GBK");
// 2. 将字节数组转换为十六进制字符串
StringBuilder hexString = new StringBuilder();
for (byte b : bytes) {
// 将每个字节转换为两位十六进制,并确保是大写
String hex = String.format("%02X", b);
hexString.append(hex);
}
return hexString.toString();
} catch (UnsupportedEncodingException e) {
System.err.println("不支持 GBK 编码: " + e.getMessage());
return ""; // 或者抛出异常
}
}
}
bash
package com.cqcloud.platform.handler;
import io.netty.channel.*;
import io.netty.handler.codec.LengthFieldBasedFrameDecoder;
import io.netty.handler.codec.LineBasedFrameDecoder;
import io.netty.handler.codec.mqtt.MqttDecoder;
import io.netty.handler.codec.mqtt.MqttEncoder;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.InitializingBean;
import com.cqcloud.platform.config.NettyServerProperties;
import com.cqcloud.platform.service.NettyService;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;
import java.nio.charset.StandardCharsets;
/**
* Netty服务工厂类
* @author weimeilayer@gmail.com ✨
* @date 💓💕 2023年9月1日 🐬🐇 💓💕
*/
public class NettyServerFactory implements InitializingBean, DisposableBean {
private final NettyServerProperties properties;
private final NettyService nettyService;
private EventLoopGroup bossGroup;
private EventLoopGroup workerGroup;
private ChannelFuture channelFuture;
public NettyServerFactory(NettyServerProperties properties, NettyService nettyService) {
this.properties = properties;
this.nettyService = nettyService;
}
@Override
public void afterPropertiesSet() throws Exception {
if (!properties.isEnabled()) {
return;
}
// 配置线程组
bossGroup = properties.getBossThreads() > 0 ?
new NioEventLoopGroup(properties.getBossThreads()) :
new NioEventLoopGroup();
workerGroup = properties.getWorkerThreads() > 0 ?
new NioEventLoopGroup(properties.getWorkerThreads()) :
new NioEventLoopGroup();
try {
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_RCVBUF, 30 * 1024 * 1024) // 系统接收缓冲区 2MB
.childOption(ChannelOption.SO_RCVBUF, 30 * 1024 * 1024) // 子通道接收缓冲区 2MB
.childOption(ChannelOption.RCVBUF_ALLOCATOR,
new AdaptiveRecvByteBufAllocator(64, 512 * 1024, 30 * 1024 * 1024)) // 自适应缓冲区
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) {
ChannelPipeline pipeline = ch.pipeline();
// 添加字符串编解码器
//pipeline.addLast(new StringDecoder(StandardCharsets.UTF_8));
//pipeline.addLast(new StringEncoder(StandardCharsets.UTF_8));
// 添加MQTT解码器(核心:自动解析MQTT帧结构,处理分片)
// 参数:最大消息长度(根据图片大小调整,如20MB)
pipeline.addLast("mqttDecoder", new MqttDecoder(30 * 1024 * 1024));
// 添加MQTT编码器(发送响应时自动编码为MQTT协议格式)
pipeline.addLast("mqttEncoder", MqttEncoder.INSTANCE);
// 添加自定义处理器
// 再添加您原有的业务处理器
pipeline.addLast(new TcpMqttServerHandler(nettyService));
}
});
channelFuture = bootstrap.bind(properties.getPort()).sync();
System.out.println("Netty TCP Server started on port: " + properties.getPort());
// 异步关闭监听
channelFuture.channel().closeFuture().addListener(future -> {
System.out.println("Netty TCP Server channel closed");
});
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException("Netty server start interrupted", e);
} catch (Exception e) {
destroy();
throw new RuntimeException("Failed to start Netty server", e);
}
}
@Override
public void destroy() {
if (channelFuture != null) {
channelFuture.channel().close();
}
if (bossGroup != null) {
bossGroup.shutdownGracefully();
}
if (workerGroup != null) {
workerGroup.shutdownGracefully();
}
System.out.println("Netty TCP Server resources released");
}
public boolean isRunning() {
return channelFuture != null && channelFuture.channel().isActive();
}
}
bash
package com.cqcloud.platform.handler;
import com.cqcloud.platform.service.NettyService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* Netty TCP服务器入口点
* 实际启动由NettyServerFactory处理
*/
@Component
public class NettyTcpHandler {
private final NettyService nettyService;
@Autowired
public NettyTcpHandler(NettyService nettyService) {
this.nettyService = nettyService;
}
/**
* 保持原有的静态方法兼容性
*/
public static void start() {
System.out.println("Netty server is now managed by Spring Boot auto-configuration");
System.out.println("Please configure netty.server properties in application.yml");
}
public NettyService getNettyService() {
return nettyService;
}
}
bash
package com.cqcloud.platform.handler;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
import com.cqcloud.platform.common.matter.utils.SpringContextHolder;
import com.cqcloud.platform.entity.TcHeartbeatResponse;
import com.cqcloud.platform.service.NettyService;
import com.cqcloud.platform.tcsf.service.BarrierMqttService;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.mqtt.*;
import io.netty.util.AttributeKey;
import lombok.extern.slf4j.Slf4j;
import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* 支持 MQTT 遗嘱(LWT)的高并发安全服务端处理器(仅支持 QoS=0)
* 设备上行主题:/device/{camId}/update
* 服务端下行主题:/device/{camId}/get
* 遗嘱主题:/device/{camId}/will
*/
@Slf4j
public class TcpMqttServerHandler extends SimpleChannelInboundHandler<MqttMessage> {
private final NettyService iotPushService;
private static final ObjectMapper JSON_MAPPER = new ObjectMapper();
private static final ConcurrentMap<String, Channel> CAM_CHANNEL_MAP = new ConcurrentHashMap<>();
private static final AttributeKey<String> CAM_ID_KEY = AttributeKey.valueOf("camId");
private static final AttributeKey<MqttConnectMessage> CONNECT_MESSAGE_KEY = AttributeKey.valueOf("connectMessage");
private static final Pattern DEVICE_TOPIC_PATTERN = Pattern.compile("^/device/([^/]+)/.*$");
public TcpMqttServerHandler(NettyService iotPushService) {
this.iotPushService = iotPushService;
}
@Override
protected void channelRead0(ChannelHandlerContext ctx, MqttMessage msg) throws Exception {
// 记录收到的原始消息类型
System.out.println("时间:"+LocalDateTime.now()+" [MQTT] 收到消息类型: " + msg.fixedHeader().messageType());
switch (msg.fixedHeader().messageType()) {
case CONNECT:
handleConnect(ctx, (MqttConnectMessage) msg);
break;
case PINGREQ:
ctx.writeAndFlush(new MqttMessage(
new MqttFixedHeader(MqttMessageType.PINGRESP, false, MqttQoS.AT_MOST_ONCE, false, 0)
));
break;
// 主要业务入口 处理 PUBLISH
case PUBLISH:
handlePublish(ctx, (MqttPublishMessage) msg);
break;
case DISCONNECT:
ctx.close();
break;
case SUBSCRIBE:
handleSubscribe(ctx, (MqttSubscribeMessage) msg);
break;
default:
System.out.println("时间:"+LocalDateTime.now()+" [MQTT] 忽略不支持的消息类型: " + msg.fixedHeader().messageType());
break;
}
}
/**
* 处理设备 CONNECT 连接请求
*/
private void handleConnect(ChannelHandlerContext ctx, MqttConnectMessage connectMsg) {
String clientId = connectMsg.payload().clientIdentifier();
boolean hasWill = connectMsg.variableHeader().isWillFlag();
String willTopic = hasWill ? connectMsg.payload().willTopic() : "N/A";
System.out.println("时间:"+LocalDateTime.now()+" [CONN] 设备尝试连接 - clientId: " + clientId + ", 含遗嘱: " + hasWill + ", 遗嘱主题: " + willTopic);
// 保存 CONNECT 消息供后续 LWT 使用
ctx.channel().attr(CONNECT_MESSAGE_KEY).set(connectMsg);
// 回复 CONNACK
ctx.writeAndFlush(new MqttConnAckMessage(
new MqttFixedHeader(MqttMessageType.CONNACK, false, MqttQoS.AT_MOST_ONCE, false, 0),
new MqttConnAckVariableHeader(MqttConnectReturnCode.CONNECTION_ACCEPTED, false)
));
System.out.println("时间:"+LocalDateTime.now()+" [CONN] 已接受设备连接 - clientId: " + clientId);
}
/**
* 处理设备 PUBLISH 消息
*/
private void handlePublish(ChannelHandlerContext ctx, MqttPublishMessage publishMsg) {
// 解析主题
String topic = publishMsg.variableHeader().topicName();
// 解析负载
ByteBuf payload = publishMsg.payload();
// 解析 JSON
byte[] bytes = new byte[payload.readableBytes()];
// 读取字节
payload.readBytes(bytes);
// 解析 JSON
String rawPayload = new String(bytes, StandardCharsets.UTF_8);
// 绑定 camId
bindCamIdIfNecessary(ctx, topic);
// 提取 JSON
String json = extractJson(rawPayload);
if (json == null) {
System.err.println("[ERROR] 无效 JSON 负载,丢弃消息 - 内容: " + rawPayload);
return;
}
try {
// 解析 JSON
JsonNode root = JSON_MAPPER.readTree(json);
// 命令
String cmd = root.path("cmd").asText("");
// camId
String camId = ctx.channel().attr(CAM_ID_KEY).get();
// 心跳
if ("heartbeat".equals(cmd)) {
System.out.println("时间:"+LocalDateTime.now()+" [HEARTBEAT] 收到心跳请求 - camId: " + camId + ", msgId: " + root.path("msgId").asText(""));
BarrierMqttService barrierMqttService = SpringContextHolder.getBean(BarrierMqttService.class);
Object object = barrierMqttService.isPcAllowPass(camId);
String response = String.valueOf(object);
if(StrUtil.isNotEmpty(response)){
System.out.println("时间:"+LocalDateTime.now()+" 心跳应答内容: " + object);
sendToDevice(camId, object.toString());
return;
}
// 回复心跳
replyHeartbeat(ctx, root);
return;
}
System.out.println("时间:"+LocalDateTime.now()+" [BUSINESS] 处理业务消息 - camId: " + camId + ", cmd: " + cmd);
List<String> responses = iotPushService.handleMessage(camId, json);
// 响应数据接口
if (responses != null && !responses.isEmpty()) {
System.out.println("时间:"+LocalDateTime.now()+" [RESPONSE] 业务层返回 " + responses.size() + " 条响应,准备下发");
for (String resp : responses) {
// 业务发送响应消息
sendToDevice(camId, resp);
}
}
} catch (Exception e) {
System.err.println("[EXCEPTION] 处理 PUBLISH 消息异常 - 主题: " + topic + ", 错误: " + e.getMessage());
}
}
/**
* 处理 SUBSCRIBE 请求(仅回复 SUBACK,不实际维护订阅关系)
*/
private void handleSubscribe(ChannelHandlerContext ctx, MqttSubscribeMessage subscribeMsg) {
MqttMessageIdVariableHeader messageIdVarHeader = subscribeMsg.variableHeader();
int messageId = messageIdVarHeader.messageId();
// 获取所有订阅的主题
List<MqttTopicSubscription> topicSubscriptions = subscribeMsg.payload().topicSubscriptions();
System.out.println("时间:"+LocalDateTime.now()+" [SUBSCRIBE] 收到订阅请求 - messageId: " + messageId);
for (MqttTopicSubscription subscription : topicSubscriptions) {
String topic = subscription.topicName();
MqttQoS qos = subscription.qualityOfService();
System.out.println("时间:"+LocalDateTime.now()+" [SUBSCRIBE] 客户端请求订阅主题: " + topic + ", QoS: " + qos.value());
// 可选:校验主题是否合法(例如必须是 /device/xxx/get)
// if (!isValidDownstreamTopic(topic)) { /* 拒绝 */ }
}
// 构造 SUBACK 响应(全部授予 QoS=0,因本服务仅支持 QoS=0)
int[] grantedQos = new int[topicSubscriptions.size()];
for (int i = 0; i < grantedQos.length; i++) {
// 强制降级为 QoS=0(与你的 publish 一致)
grantedQos[i] = 0;
}
MqttSubAckPayload subAckPayload = new MqttSubAckPayload(grantedQos);
MqttSubAckMessage subAckMessage = new MqttSubAckMessage(
new MqttFixedHeader(MqttMessageType.SUBACK, false, MqttQoS.AT_MOST_ONCE, false, 0),
messageIdVarHeader,
subAckPayload
);
ctx.writeAndFlush(subAckMessage);
System.out.println("时间:"+LocalDateTime.now()+" [SUBSCRIBE] 已回复 SUBACK - messageId: " + messageId);
}
/**
* 绑定 camId 到 Channel
*/
private void bindCamIdIfNecessary(ChannelHandlerContext ctx, String topic) {
String existing = ctx.channel().attr(CAM_ID_KEY).get();
if (existing != null) {
// 已绑定,无需重复操作
return;
}
Matcher m = DEVICE_TOPIC_PATTERN.matcher(topic);
if (m.matches()) {
String camId = m.group(1);
ctx.channel().attr(CAM_ID_KEY).set(camId);
CAM_CHANNEL_MAP.put(camId, ctx.channel());
System.out.println("时间:"+LocalDateTime.now()+" [BIND] 成功绑定 camId 到通道 - camId: " + camId);
} else {
System.out.println("时间:"+LocalDateTime.now()+" [WARN] 无法从主题提取 camId - 主题: " + topic);
}
}
/**
* 回复心跳
*/
private void replyHeartbeat(ChannelHandlerContext ctx, JsonNode req) {
ObjectNode rsp = JSON_MAPPER.createObjectNode();
rsp.put("cmd", "heartbeatRsp");
rsp.put("msgId", req.path("msgId").asText(""));
rsp.put("status", "ok");
String camId = ctx.channel().attr(CAM_ID_KEY).get();
if (camId != null) {
// 发送心跳响应
sendToDevice(camId, rsp.toString());
//System.out.println("时间:"+LocalDateTime.now()+" [HEARTBEAT] 已回复心跳 - camId: " + camId);
} else {
System.err.println("[ERROR] 心跳回复失败:camId 未绑定");
}
}
// ==================== 遗嘱处理核心逻辑 ====================
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
String camId = ctx.channel().attr(CAM_ID_KEY).get();
if (camId != null) {
CAM_CHANNEL_MAP.remove(camId);
System.out.println("时间:"+LocalDateTime.now()+" [DISCONNECT] 设备通道非活跃,已移除映射 - camId: " + camId);
} else {
System.out.println("时间:"+LocalDateTime.now()+" [DISCONNECT] 通道关闭,但未绑定 camId");
}
// 检查是否需要触发遗嘱
MqttConnectMessage connectMsg = ctx.channel().attr(CONNECT_MESSAGE_KEY).get();
if (connectMsg != null && connectMsg.variableHeader().isWillFlag()) {
String willTopic = connectMsg.payload().willTopic();
byte[] willMessage = connectMsg.payload().willMessageInBytes();
String willPayloadStr = new String(willMessage, StandardCharsets.UTF_8);
System.out.println("时间:"+LocalDateTime.now()+" [LWT] 检测到遗嘱标志,准备发布遗嘱 - 主题: " + willTopic);
Matcher m = DEVICE_TOPIC_PATTERN.matcher(willTopic);
if (m.matches()) {
String extractedCamId = m.group(1);
System.out.println("时间:"+LocalDateTime.now()+" [LWT] 遗嘱 camId 提取成功: " + extractedCamId + ",内容: " + willPayloadStr);
// 构造遗嘱消息(QoS=0)
MqttPublishMessage willPublish = new MqttPublishMessage(
new MqttFixedHeader(MqttMessageType.PUBLISH, false, MqttQoS.AT_MOST_ONCE,
connectMsg.variableHeader().isWillRetain(), willMessage.length),
new MqttPublishVariableHeader(willTopic, 0),
Unpooled.copiedBuffer(willMessage)
);
// ⚠️ 注意:此处通道已 inactive,不能通过 ctx 发送!
// 实际应通过内部消息总线或另一个 MQTT 客户端发布
System.out.println("时间:"+LocalDateTime.now()+" [LWT] 遗嘱消息构造完成,需通过其他方式广播(当前仅打印)");
System.out.println("时间:"+LocalDateTime.now()+" [LWT][CONTENT] " + willPayloadStr);
// TODO: 在真实系统中,此处应调用消息中间件或数据库记录
// 例如:kafkaProducer.send(willTopic, willPayloadStr);
} else {
System.err.println("[LWT][ERROR] 遗嘱主题格式非法,拒绝发布 - 主题: " + willTopic);
}
} else {
System.out.println("时间:"+LocalDateTime.now()+" [LWT] 无遗嘱设置或连接信息缺失,跳过遗嘱发布");
}
super.channelInactive(ctx);
}
// ==================== 主动下发方法 ====================
public static void sendToDevice(String camId, String payload) {
Channel channel = CAM_CHANNEL_MAP.get(camId);
if (channel == null || !channel.isActive()) {
System.err.println("[SEND][ERROR] 目标设备离线或通道无效 - camId: " + camId);
return;
}
String topic = "/device/" + camId + "/get";
byte[] bytes = payload.getBytes(StandardCharsets.UTF_8);
MqttPublishMessage msg = new MqttPublishMessage(
new MqttFixedHeader(MqttMessageType.PUBLISH, false, MqttQoS.AT_MOST_ONCE, false, bytes.length),
new MqttPublishVariableHeader(topic, 0),
Unpooled.copiedBuffer(bytes)
);
channel.writeAndFlush(msg);
// 设置心跳响应记录实体(从JSON对象)
setTcHeartbeatResponse(camId,String.valueOf(msg),payload);
System.out.println("时间:"+LocalDateTime.now()+" [SEND] 已向设备下发消息 - camId: " + camId + ", 主题: " + topic + ", 内容: " + payload);
}
// ==================== 异常处理 ====================
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
String camId = ctx.channel().attr(CAM_ID_KEY).get();
System.err.println("[EXCEPTION] 通道异常 - camId: " + camId + ", 原因: " + cause.getMessage());
cause.printStackTrace();
ctx.close();
}
// ==================== 工具方法 ====================
private static String extractJson(String s) {
int i = s.indexOf('{');
int j = s.lastIndexOf('}');
if (i >= 0 && j > i) {
return s.substring(i, j + 1);
}
return null;
}
/**
* 设置心跳响应记录实体(从JSON对象)
* @param responseContent 系统回复内容
* @return 配置好的实体对象
*/
private static void setTcHeartbeatResponse(String camId,String msg,String responseContent) {
TcHeartbeatResponse result = new TcHeartbeatResponse();
// 设置响应字段
result.setCmd(camId);
result.setOriginalJson(msg);
result.setResponseContent(responseContent);
// 设置系统字段
result.setGmtCreate(LocalDateTime.now());
result.setGmtModified(LocalDateTime.now());
result.setDelFlag(0); // 正常状态
CompletableFuture.runAsync(() -> {
try {
result.setId(String.valueOf(IdWorker.getId()));
result.insert();
} catch (Exception e) {
// 处理异常
log.error("收到识别结果固定记录插入失败", e);
}
});
}
}
bash
package com.cqcloud.platform.service;
import com.cqcloud.platform.dto.PlateResultData;
import com.google.gson.JsonObject;
import java.util.List;
/**
* @author weimeilayer@gmail.com
* @date 💓💕2023年9月8日🐬🐇💓💕
*/
public interface NettyService {
/**
* 处理消息
* @param camId
* @param jsonMessage
* @return
*/
List<String> handleMessage(String camId, String jsonMessage);
/**
* 设置车牌识别结果
* @param data 识别结果(为 MqttParkingControl.convertToPlateResultData(requestBody) 对象)
* @param originalMessage 保存原始JSON消息
* @param responseContent 发送下发内容
*/
void setTcPlateRecognitionResult(PlateResultData data, JsonObject originalMessage, String responseContent);
/**
* 设置心跳响应
* @param responseContent 发送下发内容
*/
void setTcPlateRecognitionResultHeart(String devno,String carLicense,String responseContent);
}
bash
package com.cqcloud.platform.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
import com.cqcloud.platform.common.RandomUtil;
import com.cqcloud.platform.common.matter.utils.SpringContextHolder;
import com.cqcloud.platform.dto.PlateResultData;
import com.cqcloud.platform.entity.SysSocialDetails;
import com.cqcloud.platform.entity.TcPlateRecognitionResult;
import com.cqcloud.platform.req.Req_BaGateRequestBody;
import com.cqcloud.platform.service.NettyService;
import com.cqcloud.platform.service.SysFileService;
import com.cqcloud.platform.service.SysSocialDetailsService;
import com.cqcloud.platform.tcsf.service.BarrierMqttService;
import com.cqcloud.platform.tcsf.service.BaseWebService;
import com.cqcloud.platform.tcsf.service.task.ParkingHandleTask;
import com.cqcloud.platform.tcsf.utils.DateTimeUtil;
import com.cqcloud.platform.vo.CpCarLog;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import java.util.*;
import java.util.concurrent.CompletableFuture;
/**
* @author weimeilayer@gmail.com
* @date 💓💕2023年9月8日🐬🐇💓💕
*/
@Slf4j
@Service
@AllArgsConstructor
public class NettyServiceImpl implements NettyService {
private final BarrierMqttService barrierMqttService;
@Override
public List<String> handleMessage(String camId, String message) {
//System.out.println("Service层处理消息: " + message);
JsonObject jsonMessage = JsonParser.parseString(message).getAsJsonObject();
try {
String cmd = jsonMessage.get("cmd").getAsString();
String deviceId = jsonMessage.get("devId").getAsString();
System.out.println("解析到命令: " + cmd + ", 设备ID: " + deviceId);
if ("plateResult".equals(cmd)) {
return handlePlateResult(jsonMessage, deviceId);
}
} catch (Exception e) {
System.out.println("消息不是JSON格式"+message);
}
// 如果移除 返回默认心跳消息
return handleHeartbeat(jsonMessage,camId);
}
/**
* 处理车牌识别结果消息
* @param message 包含车牌识别结果的JSON对象,应包含content字段,content中包含plateNum和isWhitelist
* @param deviceId 设备ID
* @return 响应命令列表
*/
private List<String> handlePlateResult(JsonObject message, String deviceId) {
try {
// 提取并封装所有字段
PlateResultData data = extractPlateResultData(message);
String id = data.getPlateNum() + deviceId;
Long t1 = BaseWebService.reqMsgList.get(id);
BaseWebService.reqMsgList.put(id, System.currentTimeMillis());
// 使用转换方法创建请求体
Req_BaGateRequestBody requestBody = convertToRequestBody(data);
// 调用识别 MQTT服务
if (!StringUtils.isEmpty(data.getPlateNum()) && !StringUtils.isEmpty(deviceId)) {
try {
barrierMqttService.checkInMqtt(requestBody);
//BaseWebService.reqMsgList.remove(id);
} catch (Exception e) {
System.out.println("处理结果异常: " + data.getPlateNum());
}
}
} catch (Exception e) {
System.out.println("处理结果时出错: " + e.getMessage());
}
// 如果报错 返回默认心跳消息
return handleHeartbeat(message,deviceId);
}
/**
* 检查手动测试配置
* 如果启用则返回重定向URL,否则返回null
*/
private List<String> checkManualTestConfig(String camId, String configType) {
List<String> resultUrls = new ArrayList<>();
try {
SysSocialDetails condition = new SysSocialDetails();
condition.setAppId(camId);
condition.setType(configType);
SysSocialDetailsService sysSocialDetailsService = SpringContextHolder.getBean(SysSocialDetailsService.class);
List<SysSocialDetails> social = sysSocialDetailsService.list(new QueryWrapper<>(condition));
if (social != null && !social.isEmpty()) {
for (SysSocialDetails item : social) {
if ("7".equals(item.getHearderKey())) {
String url = item.getUrl();
System.out.println("收到识别结果内容: " + url);
resultUrls.add(url);
}
}
}
} catch (Exception e) {
log.error("检查手动测试配置失败", e);
}
return resultUrls;
}
// 提取并封装数据的方法
private PlateResultData extractPlateResultData(JsonObject message) {
PlateResultData data = new PlateResultData();
// 提取顶层字段
data.setCmd(message.get("cmd").getAsString());
data.setMsgId(message.get("msgId").getAsString());
data.setDevId(message.get("devId").getAsString());
data.setParkId(message.get("parkId").getAsString());
data.setDevIp(message.get("devIp").getAsString());
data.setUtcTs(message.get("utcTs").getAsLong());
// 提取content对象中的字段
JsonObject content = message.getAsJsonObject("content");
data.setType(content.get("type").getAsString());
data.setPlateNum(content.get("plateNum").getAsString());
return data;
}
/**
* 处理设备心跳消息
*
* @param message 心跳消息内容,类型为JsonObject
* @param deviceId 设备唯一标识符
* @return 包含心跳响应消息的字符串列表
*/
private List<String> handleHeartbeat(JsonObject message, String deviceId) {
List<String> responses = new ArrayList<>();
System.out.println("处理心跳消息,设备: " + deviceId);
// 调用生成心跳响应的方法
String heartbeatResponse = generateHeartbeatResponse(message);
// 添加响应
responses.add(heartbeatResponse);
return responses;
}
/**
* 处理IO状态变化消息
*
* @param message 包含IO状态信息的JSON对象
* @param deviceId 设备标识符
* @return 响应消息列表
*/
private List<String> handleIoStatus(JsonObject message, String deviceId) {
List<String> responses = new ArrayList<>();
System.out.println("处理IO状态变化,设备: " + deviceId);
// 处理IO状态逻辑
return responses;
}
/**
* 处理RS485数据
*
* @param message 包含RS485数据的JSON对象
* @param deviceId 设备ID
* @return 响应消息列表
*/
private List<String> handleRs485Data(JsonObject message, String deviceId) {
List<String> responses = new ArrayList<>();
System.out.println("处理RS485数据,设备: " + deviceId);
// 处理RS485数据逻辑
return responses;
}
/**
* 处理设备信息
*
* @param message 包含设备信息的JSON对象
* @param deviceId 设备标识符
* @return 处理结果的响应列表
*/
private List<String> handleDeviceInfo(JsonObject message, String deviceId) {
List<String> responses = new ArrayList<>();
System.out.println("处理设备信息,设备: " + deviceId);
// 处理设备信息逻辑
return responses;
}
/**
* 处理未知命令的消息
*
* @param message 包含命令信息的JSON对象
* @param deviceId 发送命令的设备ID
* @return 包含错误响应的字符串列表
*/
private List<String> handleUnknownCommand(JsonObject message, String deviceId) {
List<String> responses = new ArrayList<>();
System.out.println("处理未知命令,设备: " + deviceId);
// 返回错误响应
String errorResponse = generateErrorResponse(message, "unknown_command");
responses.add(errorResponse);
return responses;
}
/**
* 处理原始消息并生成响应列表
*
* @param message 待处理的原始消息字符串
* @return 包含处理结果的字符串列表
*/
private List<String> handleRawMessage(String message) {
List<String> responses = new ArrayList<>();
System.out.println("处理原始消息: " + message);
// 处理非JSON格式的原始消息
return responses;
}
/**
* 生成单个数据的RS485透传指令(兼容旧版本)
*/
private String generateRs485TransmitCommand(String originalMsgId, String data, int channel) {
List<String> dataList = new ArrayList<>();
dataList.add(data);
return generateRs485TransmitCommand(originalMsgId, dataList, channel);
}
/**
* 工具方法:文本转16进制字符串
*/
private static String textToHexString(String text) {
try {
byte[] bytes = text.getBytes("GBK"); // 显示屏通常使用GBK编码
StringBuilder hexBuilder = new StringBuilder();
for (byte b : bytes) {
hexBuilder.append(String.format("%02X", b));
}
return hexBuilder.toString();
} catch (Exception e) {
return "";
}
}
/**
* 生成错误响应
*/
private String generateErrorResponse(JsonObject message, String errorType) {
String msgId = message.has("msgId") ? message.get("msgId").getAsString() : generateMessageId();
JsonObject response = new JsonObject();
response.addProperty("cmd", "errorRsp");
response.addProperty("msgId", msgId);
response.addProperty("status", errorType);
return response.toString();
}
/**
* 生成符合文档格式的消息ID(20位:13位毫秒时间+7位随机数)
*/
private static String generateMessageId() {
long millis = System.currentTimeMillis();
String random = String.format("%07d", new Random().nextInt(10000000));
return millis + random;
}
}
控制器 自动下发测试
bash
@SasIgnore(value = false)
@PostMapping("/testMqtt")
public String testMqtt(@RequestBody MqttPushDto dto) {
String camId = Optional.ofNullable(dto.getCamId())
.filter(s -> !s.trim().isEmpty()) // 等价于 isNotBlank
.filter(s -> !s.isEmpty()) // 等价于 isNotEmpty
.orElse("18xxxxx");
String iotNum = Optional.ofNullable(dto.getIoNum())
.filter(s -> !s.trim().isEmpty()) // 等价于 isNotBlank
.filter(s -> !s.isEmpty()) // 等价于 isNotEmpty
.orElse("io1");
MqttCommandSender.sendIoOutput(camId, iotNum, "on");
return "ok";
}