当前位置: 首页 > ai >正文

MQTT 与 Java 框架集成:Spring Boot 实战(三)

五、典型场景:物联网设备管理系统实战

5.1 设备上下线管理

在物联网设备管理系统中,实时掌握设备的在线状态至关重要 。我们通过系统主题监听设备连接状态 ,实现设备上下线的实时感知 。在 EMQX 等 MQTT Broker 中,提供了以$SYS开头的系统主题 ,用于发布设备的连接与断开事件 。例如,订阅$SYS/brokers/${node}/clients/${clientid}/connected主题,可以监听设备上线事件 ;订阅$SYS/brokers/${node}/clients/${clientid}/disconnected主题,可监听设备下线事件 。

为了维护设备在线列表,我们使用 Redis 作为存储介质 。Redis 具有高性能、低延迟的特点,能够快速处理读写操作 ,非常适合存储设备在线状态这种频繁更新的数据 。当监听到设备上线事件时,将设备 ID 作为键,上线时间等相关信息作为值,存储到 Redis 中 。例如,使用 Redis 的SET命令:

 

import redis.clients.jedis.Jedis;

public class DeviceOnlineManager {

private Jedis jedis;

public DeviceOnlineManager() {

jedis = new Jedis("localhost", 6379);

}

public void handleDeviceOnline(String deviceId) {

long currentTime = System.currentTimeMillis();

jedis.setex("device:online:" + deviceId, 3600, String.valueOf(currentTime));

}

public void handleDeviceOffline(String deviceId) {

jedis.del("device:online:" + deviceId);

}

public boolean isDeviceOnline(String deviceId) {

return jedis.exists("device:online:" + deviceId);

}

}

在上述代码中,handleDeviceOnline方法在设备上线时被调用 ,使用SETex命令设置设备在线信息,并设置过期时间为 1 小时 ,确保离线设备的信息在一段时间后自动删除 。handleDeviceOffline方法在设备下线时被调用 ,通过DEL命令删除设备在线信息 。isDeviceOnline方法用于查询设备是否在线 ,通过EXISTS命令判断设备在线信息是否存在 。

为了实现设备状态实时同步与异常报警,我们结合 Spring 的事件驱动机制 。创建DeviceStatusChangeListener类,实现ApplicationListener接口 。当设备上下线事件发生时,发布自定义的DeviceStatusChangeEvent事件 ,在DeviceStatusChangeListener中监听该事件 。

 

import org.springframework.context.ApplicationListener;

import org.springframework.stereotype.Component;

@Component

public class DeviceStatusChangeListener implements ApplicationListener<DeviceStatusChangeEvent> {

@Override

public void onApplicationEvent(DeviceStatusChangeEvent event) {

String deviceId = event.getDeviceId();

boolean isOnline = event.isOnline();

if (isOnline) {

// 设备上线处理逻辑,如记录日志、通知相关系统

System.out.println("Device " + deviceId + " is online.");

} else {

// 设备下线处理逻辑,如发送报警通知

System.out.println("Device " + deviceId + " is offline. Sending alarm...");

// 发送报警邮件或短信的逻辑

}

}

}

当设备长时间离线时,通过邮件或短信等方式发送报警通知 ,确保管理人员能够及时知晓设备异常情况 ,采取相应措施,保障物联网系统的稳定运行 。

5.2 指令下发与数据上报

5.2.1 下行通道

在物联网设备管理系统中,下行通道负责将控制指令从服务端发送到设备端 。我们通过device/command/{deviceId}主题推送控制指令 。例如,在控制空调开关机时,向device/command/airConditioner001主题发布指令消息 。

 

import org.eclipse.paho.client.mqttv3.MqttClient;

import org.eclipse.paho.client.mqttv3.MqttException;

import org.eclipse.paho.client.mqttv3.MqttMessage;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.stereotype.Service;

@Service

public class CommandSender {

@Autowired

private MqttClient mqttClient;

public void sendCommand(String deviceId, String command, int qos) {

String topic = "device/command/" + deviceId;

try {

MqttMessage message = new MqttMessage(command.getBytes());

message.setQos(qos);

mqttClient.publish(topic, message);

} catch (MqttException e) {

e.printStackTrace();

}

}

}

在上述代码中,sendCommand方法接收设备 ID、控制指令和 QoS 等级作为参数 。构建device/command/{deviceId}主题,创建MqttMessage对象并设置消息内容和 QoS 等级 ,然后通过mqttClient.publish方法发布指令消息 。

为了确保指令能够可靠送达设备,我们采用 QoS 1 保障 ,并实现超时重传机制 。在CommandSender类中,添加超时重传逻辑:

 

import org.eclipse.paho.client.mqttv3.IMqttDeliveryToken;

import org.eclipse.paho.client.mqttv3.MqttCallback;

import org.eclipse.paho.client.mqttv3.MqttException;

public class CommandSender {

private static final int TIMEOUT = 5000; // 超时时间5秒

public void sendCommand(String deviceId, String command, int qos) {

String topic = "device/command/" + deviceId;

try {

MqttMessage message = new MqttMessage(command.getBytes());

message.setQos(qos);

IMqttDeliveryToken token = mqttClient.publish(topic, message);

long startTime = System.currentTimeMillis();

while (!token.isComplete()) {

if (System.currentTimeMillis() - startTime > TIMEOUT) {

// 超时,重传

System.out.println("Command delivery timed out, retrying...");

mqttClient.publish(topic, message);

startTime = System.currentTimeMillis();

}

Thread.sleep(100);

}

} catch (MqttException | InterruptedException e) {

e.printStackTrace();

}

}

}

在发布指令消息后,通过IMqttDeliveryToken的isComplete方法判断消息是否送达 。设置超时时间为 5 秒 ,如果在超时时间内消息未送达,则重新发布指令消息 ,直到消息成功送达或达到最大重传次数 ,确保设备能够及时接收到控制指令 。

5.2.2 上行通道

设备通过device/data/{deviceId}主题上报状态数据 ,如温度、湿度等 。在服务端,我们使用 Spring Integration 的消息通道来接收设备上报的数据 。配置MqttInboundChannelAdapter,将device/data/{deviceId}主题的消息映射到指定的消息通道 。

 

import org.springframework.context.annotation.Bean;

import org.springframework.context.annotation.Configuration;

import org.springframework.integration.annotation.ServiceActivator;

import org.springframework.integration.channel.DirectChannel;

import org.springframework.integration.mqtt.inbound.MqttPahoMessageDrivenChannelAdapter;

import org.springframework.integration.mqtt.support.DefaultPahoMessageConverter;

import org.springframework.messaging.MessageChannel;

@Configuration

public class MqttConfig {

@Bean

public MessageChannel mqttInputChannel() {

return new DirectChannel();

}

@Bean

public MqttPahoMessageDrivenChannelAdapter mqttInbound() {

MqttPahoMessageDrivenChannelAdapter adapter =

new MqttPahoMessageDrivenChannelAdapter("tcp://broker.example.com:1883", "clientId", "device/data/#");

adapter.setCompletionTimeout(5000);

adapter.setConverter(new DefaultPahoMessageConverter());

adapter.setOutputChannel(mqttInputChannel());

return adapter;

}

@ServiceActivator(inputChannel = "mqttInputChannel")

public void handleDeviceData(String message) {

// 处理设备上报的数据,如存储到数据库

System.out.println("Received device data: " + message);

}

}

在上述配置中,mqttInbound方法创建MqttPahoMessageDrivenChannelAdapter,设置 MQTT Broker 地址、客户端 ID 和订阅主题device/data/# ,表示订阅所有以device/data/开头的主题 。将接收到的消息转换为字符串类型,并发送到mqttInputChannel消息通道 。handleDeviceData方法作为服务激活器,处理mqttInputChannel通道接收到的设备数据 。

为了实现数据异步处理流水线,我们集成 Kafka 。将设备上报的数据发送到 Kafka 主题,利用 Kafka 的高吞吐量和分布式特性,进行数据的异步处理 。在 Spring Boot 项目中添加 Kafka 依赖:

 

<dependency>

<groupId>org.springframework.kafka</groupId>

<artifactId>spring-kafka</artifactId>

</dependency>

配置 Kafka 生产者,将设备数据发送到 Kafka 主题:

 

import org.apache.kafka.clients.producer.ProducerConfig;

import org.springframework.context.annotation.Bean;

import org.springframework.context.annotation.Configuration;

import org.springframework.kafka.core.DefaultKafkaProducerFactory;

import org.springframework.kafka.core.KafkaTemplate;

import org.springframework.kafka.core.ProducerFactory;

import java.util.HashMap;

import java.util.Map;

@Configuration

public class KafkaConfig {

@Bean

public Map<String, Object> producerConfigs() {

Map<String, Object> props = new HashMap<>();

props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092");

props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringSerializer");

props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringSerializer");

return props;

}

@Bean

public ProducerFactory<String, String> producerFactory() {

return new DefaultKafkaProducerFactory<>(producerConfigs());

}

@Bean

public KafkaTemplate<String, String> kafkaTemplate() {

return new KafkaTemplate<>(producerFactory());

}

}

在handleDeviceData方法中,使用KafkaTemplate将设备数据发送到 Kafka 主题:

 

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.kafka.core.KafkaTemplate;

import org.springframework.messaging.handler.annotation.ServiceActivator;

import org.springframework.stereotype.Service;

@Service

public class DeviceDataHandler {

private static final String KAFKA_TOPIC = "device-data-topic";

@Autowired

private KafkaTemplate<String, String> kafkaTemplate;

@ServiceActivator(inputChannel = "mqttInputChannel")

public void handleDeviceData(String message) {

kafkaTemplate.send(KAFKA_TOPIC, message);

System.out.println("Sent device data to Kafka: " + message);

}

}

通过这种方式,设备上报的数据被发送到 Kafka 主题,后续可以通过 Kafka 消费者进行数据的存储、分析等处理 ,实现数据的高效异步处理 ,提升系统的整体性能和扩展性 。

5.3 多协议适配层设计

在实际的物联网项目中,设备可能采用多种不同的协议进行通信,如 Modbus、CoAP 等 。为了实现设备数据到 MQTT 消息的标准化转换,提升系统扩展性,我们构建统一消息转换接口 。

定义ProtocolConverter接口,作为不同协议转换的统一抽象 。该接口包含一个convert方法,接收原始协议数据和设备 ID 作为参数,返回转换后的 MQTT 消息 。

 

public interface ProtocolConverter {

String convert(String rawData, String deviceId);

}

针对 Modbus 协议,创建ModbusConverter类实现ProtocolConverter接口 。Modbus 协议常用于工业自动化领域,设备通过 Modbus 协议将数据发送到网关 。在ModbusConverter中,解析 Modbus 数据帧,提取设备数据,并将其转换为 MQTT 消息格式 。例如,假设 Modbus 数据帧包含设备 ID、温度、湿度等信息:

 

public class ModbusConverter implements ProtocolConverter {

@Override

public String convert(String rawData, String deviceId) {

// 解析Modbus数据帧

// 假设解析后得到温度和湿度数据

double temperature = parseTemperature(rawData);

double humidity = parseHumidity(rawData);

// 构建MQTT消息

String mqttMessage = "{\"deviceId\":\"" + deviceId + "\",\"temperature\":" + temperature + ",\"humidity\":" + humidity + "}";

return mqttMessage;

}

private double parseTemperature(String rawData) {

// 实际的Modbus温度数据解析逻辑

return 25.0; // 示例返回值

}

private double parseHumidity(String rawData) {

// 实际的Modbus湿度数据解析逻辑

return 50.0; // 示例返回值

}

}

对于 CoAP 协议,创建CoapConverter类实现ProtocolConverter接口 。CoAP 协议是一种专为物联网设计的应用层协议,适用于资源受限的设备 。在CoapConverter中,处理 CoAP 消息格式,提取设备数据并转换为 MQTT 消息 。假设 CoAP 消息以 JSON 格式传输设备数据:

 

import com.google.gson.JsonObject;

import com.google.gson.JsonParser;

public class CoapConverter implements ProtocolConverter {

@Override

public String convert(String rawData, String deviceId) {

JsonObject jsonObject = JsonParser.parseString(rawData).getAsJsonObject();

double temperature = jsonObject.get("temperature").getAsDouble();

double humidity = jsonObject.get("humidity").getAsDouble();

String mqttMessage = "{\"deviceId\":\"" + deviceId + "\",\"temperature\":" + temperature + ",\"humidity\":" + humidity + "}";

return mqttMessage;

}

}

在服务端,创建ProtocolAdapter类,根据设备使用的协议类型,选择相应的协议转换器进行数据转换 。ProtocolAdapter类维护一个ProtocolConverter实例的映射表,根据协议类型获取对应的转换器 。

 

import java.util.HashMap;

import java.util.Map;

public class ProtocolAdapter {

private Map<String, ProtocolConverter> converterMap;

public ProtocolAdapter() {

converterMap = new HashMap<>();

converterMap.put("modbus", new ModbusConverter());

converterMap.put("coap", new CoapConverter());

}

public String convert(String protocol, String rawData, String deviceId) {

ProtocolConverter converter = converterMap.get(protocol);

if (converter != null) {

return converter.convert(rawData, deviceId);

}

throw new IllegalArgumentException("Unsupported protocol: " + protocol);

}

}

在处理设备数据时,调用ProtocolAdapter的convert方法,传入协议类型、原始数据和设备 ID,即可得到转换后的 MQTT 消息 ,实现了不同协议设备数据到 MQTT 消息的统一转换,使系统能够无缝集成多种协议的设备,提高了系统的兼容性和扩展性 。

六、问题排查与最佳实践

6.1 常见故障诊断

在 MQTT 与 Spring Boot 集成的过程中,难免会遇到各种问题,快速准确地诊断和解决这些问题是保障系统稳定运行的关键。以下是一些常见问题及对应的解决方法:

问题现象

可能原因

解决方法

连接超时

Broker 未启动 / 端口被占用

检查 Broker 状态,确保防火墙开放 1883 端口

消息重复接收

QoS 配置不当 / 重连时未清除旧会话

设置 或使用 QoS2 等级

内存泄漏

未正确释放 Netty 资源

实现 资源清理逻辑

当出现连接超时问题时,首先要检查 MQTT Broker 是否正常启动 。可以通过命令行工具(如ps -ef | grep emqx查看 EMQ X 进程)或查看日志文件来确认 。若 Broker 未启动,需按照正确的启动步骤启动 。若端口被占用,使用netstat -ano | grep 1883命令查看占用 1883 端口的进程 ,并停止该进程,或者修改 MQTT Broker 配置文件,更换一个未被占用的端口 。同时,检查防火墙设置,确保 1883 端口已开放,可通过iptables -L -n命令查看防火墙规则 ,若未开放,添加相应规则,如iptables -A INPUT -p tcp --dport 1883 -j ACCEPT 。

消息重复接收可能是由于 QoS 配置不当导致 。如果在发布消息时使用了 QoS 1,并且网络不稳定,可能会导致消息重复发送 。可以将 QoS 等级调整为 QoS 2,确保消息仅被接收一次 。在重连时未清除旧会话也可能导致消息重复接收 。在MqttConnectOptions中,将cleanSession设置为true ,这样在重连时会清除旧会话,避免重复接收消息 。

内存泄漏问题通常与未正确释放 Netty 资源有关 。在 Netty 的ChannelHandler中,确保在channelInactive方法中正确释放资源 。例如,关闭打开的文件句柄、取消定时任务等 。如果在ChannelHandler中使用了ByteBuf,在使用完毕后调用release方法释放内存 ,避免内存泄漏,确保系统的稳定性和性能 。通过对这些常见问题的诊断与解决,能够有效提升 MQTT 与 Spring Boot 集成系统的可靠性 。

6.2 安全加固方案

  1. 传输层加密:启用 TLS/SSL(端口 8883),配置证书验证(SSLContextFactory)。在 MQTT Broker 配置文件中,如 EMQ X 的emqx.conf文件,添加 TLS 相关配置:
 

listener.ssl.external = 0.0.0.0:8883

listener.ssl.external.keyfile = /etc/emqx/certs/server.key

listener.ssl.external.certfile = /etc/emqx/certs/server.crt

listener.ssl.external.cacertfile = /etc/emqx/certs/ca.crt

在客户端,使用SSLContextFactory创建SSLSocketFactory,并设置到MqttConnectOptions中:

 

import javax.net.ssl.SSLContext;

import javax.net.ssl.SSLContextFactory;

import javax.net.ssl.SSLSocketFactory;

import org.eclipse.paho.client.mqttv3.MqttConnectOptions;

public class MqttSslClient {

public static void main(String[] args) throws Exception {

SSLContext sslContext = SSLContextFactory.getDefault();

SSLSocketFactory sslSocketFactory = sslContext.getSocketFactory();

MqttConnectOptions options = new MqttConnectOptions();

options.setSocketFactory(sslSocketFactory);

options.setServerURIs(new String[]{"ssl://broker.example.com:8883"});

// 其他配置

}

}

  1. 认证授权:集成 OAuth2 或 JWT 实现客户端身份认证,通过 EMQ X 规则引擎对接 MySQL/Redis 进行权限校验。在 Spring Boot 项目中添加 OAuth2 和 JWT 相关依赖:
 

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>

</dependency>

<dependency>

<groupId>io.jsonwebtoken</groupId>

<artifactId>jjwt</artifactId>

</dependency>

配置 OAuth2 授权服务器和资源服务器 。在 EMQ X 中,通过规则引擎对接 MySQL 或 Redis 。例如,对接 MySQL 进行权限校验,创建 MySQL 表存储用户权限信息:

 

CREATE TABLE mqtt_permissions (

id INT AUTO_INCREMENT PRIMARY KEY,

username VARCHAR(50) NOT NULL,

topic VARCHAR(255) NOT NULL,

access INT NOT NULL

);

在 EMQ X 规则引擎中配置规则,查询 MySQL 表进行权限校验 。

3. 流量控制:设置连接速率限制(Netty ChannelHandler 实现令牌桶算法),防范 DDoS 攻击。创建自定义的ChannelHandler实现令牌桶算法 。在ChannelHandler的channelRead方法中,使用令牌桶算法进行流量控制 。例如,使用 Guava 库中的RateLimiter实现令牌桶算法:

 

import com.google.common.util.concurrent.RateLimiter;

import io.netty.channel.ChannelHandlerContext;

import io.netty.channel.ChannelInboundHandlerAdapter;

public class RateLimitHandler extends ChannelInboundHandlerAdapter {

private final RateLimiter rateLimiter;

public RateLimitHandler(double permitsPerSecond) {

rateLimiter = RateLimiter.create(permitsPerSecond);

}

@Override

public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {

if (rateLimiter.tryAcquire()) {

ctx.fireChannelRead(msg);

} else {

// 处理流量超限,如关闭连接或返回错误信息

ctx.close();

}

}

}

在 Netty 的ServerBootstrap中添加RateLimitHandler:

 

ServerBootstrap b = new ServerBootstrap();

b.group(bossGroup, workerGroup)

.channel(NioServerSocketChannel.class)

.childHandler(new ChannelInitializer<SocketChannel>() {

@Override

protected void initChannel(SocketChannel ch) throws Exception {

ch.pipeline().addLast(new RateLimitHandler(100)); // 每秒100个连接

// 其他ChannelHandler

}

});

通过这些安全加固方案,能够有效提升 MQTT 系统的安全性和稳定性 ,保护系统免受各种安全威胁 。

七、总结与拓展

7.1 技术价值总结

通过 Spring Boot 与 MQTT 的深度集成,我们成功搭建了一个高效、可靠的消息通信系统,在物联网、工业互联网等领域展现出巨大的技术价值。在物联网设备管理系统中,实现了从设备接入到业务处理的全链路高效通信 。相比传统 HTTP 协议,MQTT 协议的轻量级设计使得网络开销降低了 70% 以上 ,这对于资源受限的物联网设备来说至关重要,大大减少了设备的功耗和流量消耗 。

在性能方面,通过合理配置线程池、优化消息处理逻辑以及采用批量处理和消息压缩技术,系统能够支持百万级设备并发接入 ,满足了大规模物联网应用的需求 。在工业互联网场景中,大量的工业设备需要实时上报数据和接收控制指令,基于 Spring Boot 和 MQTT 构建的系统能够稳定高效地处理这些消息,为工业生产的智能化和自动化提供了可靠的技术底座 。

7.2 生态扩展方向

  1. 边缘计算集成:结合 Spring Edge 实现边缘节点 MQTT 消息预处理 。在工业生产现场,边缘设备可以实时采集大量的传感器数据,通过在边缘节点部署 Spring Edge,并集成 MQTT 客户端,能够在本地对这些数据进行初步处理和分析 。例如,对传感器数据进行异常检测,只将有价值的信息上报到云端,减少了数据传输量,提高了系统的响应速度 。同时,利用边缘计算的本地计算能力,还可以实现设备的本地控制,即使在网络断开的情况下,设备也能继续运行 。
  1. 云平台对接:支持 AWS IoT Core / 阿里云 IoT 等第三方 MQTT 服务迁移 。随着业务的发展,企业可能需要将现有的 MQTT 服务迁移到更具扩展性和可靠性的云平台上 。AWS IoT Core 和阿里云 IoT 等云平台提供了丰富的功能和高可用性的服务 。在迁移过程中,需要根据云平台的规范和要求,调整 MQTT 客户端的配置和消息处理逻辑 。例如,在 AWS IoT Core 中,需要使用其提供的认证机制和消息格式 ;在阿里云 IoT 中,要按照其物模型和 Topic 规则进行开发 。通过对接云平台,企业可以利用云平台的优势,如弹性扩展、安全防护等,进一步提升系统的性能和可靠性 。
  1. 可视化管理:开发基于 Vue 的设备监控控制台,实时展示消息链路追踪与故障定位 。通过 Vue 框架构建一个直观、易用的设备监控控制台,能够实时展示设备的在线状态、消息收发情况以及消息链路追踪信息 。在消息链路追踪方面,利用分布式链路追踪技术(如 Zipkin、SkyWalking 等),将 MQTT 消息的传递过程可视化,当出现故障时,可以快速定位问题所在 。同时,控制台还可以提供故障报警功能,当设备出现异常或消息传输失败时,及时通知管理员 。通过可视化管理,管理员能够更方便地监控和管理整个 MQTT 系统,提高系统的运维效率 。

通过以上系统化实践,开发者可快速掌握 MQTT 与 Spring Boot 集成的核心技术点,从容应对物联网场景下的实时消息通信需求。完整示例代码已同步至 GitHub 仓库(https://github.com/yourusername/mqtt-spring-boot-demo ),欢迎下载调试 。

http://www.xdnf.cn/news/20264.html

相关文章:

  • RAG提示词分解
  • CentOS系统管理:useradd命令的全面解析
  • Vllm-0.10.1:通过vllm bench serve测试TTFT、TPOT、ITL、E2EL四个指标
  • 多线程任务执行窗体框架jjychengTaskWinForm
  • 浅析Linux内核scatter-gather list实现
  • SQL 实战指南:电商订单数据分析(订单 / 用户 / 商品表关联 + 统计需求)
  • WordPress过滤文章插入链接rel属性noopener noreferrer值
  • 开源与定制化对比:哪种在线教育系统源码更适合教育培训APP开发?
  • 企业微信智能表格高效使用指南
  • Kafka Exactly-Once 语义深度解析与性能优化实践指南
  • 串口发送数据
  • 如何离线安装 VirtualMachinePlatform
  • 基于STM32单片机的家庭医护血氧体温血压吃药监测APP系统
  • 万字长文详解 MyCat 分表分库:从 0 到 1 构建高可用订单系统
  • 能发弹幕的简单视频网站
  • 计算机网络:调制解调器
  • Docker-volume数据卷
  • 为什么固态硬盘断电后数据还能保存不丢失?
  • 【LeetCode热题100道笔记】二叉树展开为链表
  • 激光频率梳 3D 轮廓测量 - 油路板的凹槽深度和平面度测量
  • Spring核心-Bean周期
  • ElmentUI之DateTimePicker 日期时间选择器
  • 避免使用非const全局变量:C++中的最佳实践 (C++ Core Guidelines)
  • SQLSERVER数据备份
  • Java8 Comparator接口 和 List Steam 排序使用案例
  • 人工智能在医学图像中的应用:从机器学习到深度学习
  • 技术方案详解:如何安全移动已安装软件?
  • C语言精讲(视频教程)
  • 打包 Uniapp
  • Redisson分布式锁:看门狗机制与续期原理