IoT 设备监控系统实战:基于 EMQX 的 MQTT 连接监控与数据格式指纹识别

一、项目背景

在物联网应用中,设备通过 MQTT 协议连接到 EMQX 服务器,我们需要实时监控设备的连接状态、上下线事件,以及设备上报数据的格式变化。本文介绍如何构建一个完整的 IoT 设备监控系统,实现对 EMQX 的深度监控。

二、系统架构

text

复制代码
┌─────────────────────────────────────────────────────────────┐
│                      EMQX 服务器                             │
│  ┌──────────────┐  ┌──────────────┐  ┌──────────────┐      │
│  │ 设备A (在线) │  │ 设备B (在线) │  │ 设备C (离线) │      │
│  └──────────────┘  └──────────────┘  └──────────────┘      │
└─────────────────────────────────────────────────────────────┘
                              ↓
                    MQTT 订阅 $SYS 主题
                              ↓
┌─────────────────────────────────────────────────────────────┐
│                      监控系统                                │
│  ┌─────────────────────────────────────────────────────┐    │
│  │ 1. MQTT 连接管理(自动重连、Token 缓存)              │    │
│  │ 2. 设备状态监控(在线/离线/被抢占)                   │    │
│  │ 3. 认证失败监控                                      │    │
│  │ 4. 数据格式识别与指纹统计                            │    │
│  │ 5. 定时清理任务                                      │    │
│  └─────────────────────────────────────────────────────┘    │
└─────────────────────────────────────────────────────────────┘
                              ↓
┌─────────────────────────────────────────────────────────────┐
│                      数据存储(H2)                          │
│  ├── device_status(设备当前状态)                          │
│  ├── device_status_history(设备历史记录)                   │
│  ├── data_format_stats(数据格式统计)                      │
│  └── mqtt_config(MQTT 配置)                               │
└─────────────────────────────────────────────────────────────┘

三、核心功能实现

1. MQTT 连接管理

监控系统作为 MQTT 客户端连接到 EMQX,订阅系统主题获取设备状态变化。

java

复制代码
@Component
@Slf4j
public class EmqxMqttClient implements MqttCallback {
    
    @Value("${emqx.host}")
    private String mqttHost;
    
    @Value("${emqx.username}")
    private String mqttUsername;
    
    @Value("${emqx.password}")
    private String mqttPassword;
    
    // 订阅的系统主题
    private static final String[] TOPICS = {
        "$SYS/brokers/+/clients/+/connected",      // 设备上线
        "$SYS/brokers/+/clients/+/disconnected",   // 设备下线
        "$SYS/brokers/+/metrics/authentication/failure"  // 认证失败
    };
    
    private void connectMQTT() {
        MqttConnectOptions options = new MqttConnectOptions();
        options.setUserName(mqttUsername);
        options.setPassword(mqttPassword.toCharArray());
        options.setAutomaticReconnect(true);  // 自动重连
        options.setKeepAliveInterval(60);
        
        mqttClient = new MqttClient(mqttHost, mqttClientId);
        mqttClient.setCallback(this);
        mqttClient.connect(options);
        mqttClient.subscribe(TOPICS);
    }
}

2. 设备状态监控

通过订阅 EMQX 的系统主题,实时获取设备上下线事件。

java

复制代码
@Override
public void messageArrived(String topic, MqttMessage message) {
    JSONObject json = JSON.parseObject(new String(message.getPayload()));
    String clientid = json.getString("clientid");
    String ip = json.getString("ip_address");
    
    if (topic.endsWith("/connected")) {
        saveDevice(clientid, "ONLINE", ip, username, 0);
        log.info("设备上线: clientid={}, ip={}", clientid, ip);
    } 
    else if (topic.endsWith("/disconnected")) {
        String reason = json.getString("reason");
        if ("discarded".equals(reason)) {
            saveDevice(clientid, "TAKEOVER", ip, username, 1);
            log.warn("设备被抢占: clientid={}", clientid);
        } else {
            saveDevice(clientid, "OFFLINE", ip, username, 0);
            log.info("设备离线: clientid={}", clientid);
        }
    }
}

3. 在线设备同步

启动时通过 EMQX REST API 拉取当前在线设备列表,保证状态一致性。

java

复制代码
private void loadOnlineClients() {
    String token = getToken();  // API 登录获取 Token
    
    int page = 1;
    while (true) {
        HttpGet get = new HttpGet(apiUrl + "/clients?limit=100&page=" + page);
        get.setHeader("Authorization", "Bearer " + token);
        
        JSONArray data = JSON.parseObject(response).getJSONArray("data");
        for (JSONObject item : data) {
            saveDevice(item.getString("clientid"), "ONLINE", 
                      item.getString("ipaddress"), 
                      item.getString("username"), 0);
        }
        if (data.size() < 100) break;
        page++;
    }
}

4. 数据格式指纹识别

这是系统的核心创新点:通过分析数据特征生成指纹,自动识别和归类设备上报的数据格式。

识别策略:

数据类型 识别方式 指纹生成
JSON 提取字段名 + 值类型结构 MD5(结构)
混合格式(如 HEADER{...}rtcm3) 提取 JSON + 前后缀归一化 MD5(前缀特征 + JSON指纹 + 后缀特征)
二进制/十六进制 长度、熵值、帧头检测 MD5(长度类别+帧头+熵值类别)
Base64 解码后长度 + 特征 MD5(解码长度 + 帧头)

JSON 指纹生成示例:

java

复制代码
public class JsonRecognizer {
    public FormatResult recognize(String payload) {
        JSONObject json = JSON.parseObject(payload);
        
        // 提取结构:字段名 + 值类型
        Map<String, String> structure = new LinkedHashMap<>();
        for (Map.Entry<String, Object> entry : json.entrySet()) {
            structure.put(entry.getKey(), getTypeName(entry.getValue()));
        }
        
        String structureStr = structure.entrySet().stream()
            .map(e -> e.getKey() + ":" + e.getValue())
            .collect(Collectors.joining(","));
        
        String fingerprint = md5(structureStr);
        return FormatResult.json(structureStr, fingerprint);
    }
}

混合格式识别:

java

复制代码
public class MixedFormatRecognizer {
    public FormatResult recognize(String payload) {
        // 查找 JSON 部分
        Pattern pattern = Pattern.compile("\\{[^{}]*\\}");
        Matcher matcher = pattern.matcher(payload);
        
        if (matcher.find()) {
            String prefix = payload.substring(0, matcher.start());
            String jsonPart = matcher.group();
            String suffix = payload.substring(matcher.end());
            
            // 归一化前缀/后缀(去除数字)
            String prefixNorm = prefix.replaceAll("[0-9]", "");
            String suffixNorm = suffix.replaceAll("[0-9]", "");
            
            String jsonFingerprint = generateJsonFingerprint(jsonPart);
            String fingerprint = md5(prefixNorm + "|" + jsonFingerprint + "|" + suffixNorm);
            
            return FormatResult.mixed(fingerprint, 
                "prefix:" + prefixNorm + "|json|suffix:" + suffixNorm, 
                truncate(payload, 100));
        }
        return null;
    }
}

二进制识别(熵值计算):

java

复制代码
private double calculateEntropy(String s) {
    int[] freq = new int[256];
    for (char c : s.toCharArray()) {
        freq[c & 0xFF]++;
    }
    
    double entropy = 0;
    for (int count : freq) {
        if (count > 0) {
            double p = (double) count / s.length();
            entropy -= p * (Math.log(p) / Math.log(2));
        }
    }
    return entropy;
}

5. 格式统计与存储

sql

复制代码
CREATE TABLE data_format_stats (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    client_id VARCHAR(100) NOT NULL,
    fingerprint VARCHAR(64) NOT NULL,
    format_type VARCHAR(50) NOT NULL,
    structure_desc VARCHAR(500),
    sample_payload TEXT,
    strmsg TEXT,
    first_seen DATETIME NOT NULL,
    last_seen DATETIME NOT NULL,
    occurrence_count INT DEFAULT 1,
    UNIQUE KEY uk_client_fingerprint (client_id, fingerprint)
);

6. 定时清理任务

java

复制代码
@Component
@Slf4j
public class DeviceCleanupTask {
    
    // 每天凌晨2点:离线超过2周的设备移到历史表
    @Scheduled(cron = "0 0 2 * * ?")
    public void archiveOfflineDevices() {
        LocalDateTime expireTime = LocalDateTime.now().minusWeeks(2);
        List<DeviceStatus> offlineDevices = 
            repository.findByStatusAndUpdateTimeBefore("OFFLINE", expireTime);
        
        for (DeviceStatus device : offlineDevices) {
            DeviceStatusHistory history = new DeviceStatusHistory();
            history.setClientid(device.getClientid());
            history.setCreateTime(device.getUpdateTime());
            historyRepository.save(history);
            repository.delete(device);
        }
    }
    
    // 每天凌晨3点:彻底删除半年以上的历史记录
    @Scheduled(cron = "0 0 3 * * ?")
    public void cleanHistory() {
        LocalDateTime expireTime = LocalDateTime.now().minusMonths(6);
        int deleted = historyRepository.deleteByCreateTimeBefore(expireTime);
        log.info("清理完成,删除 {} 条历史记录", deleted);
    }
}

7. 动态配置管理

支持运行时修改 EMQX 连接配置,无需重启。

java

复制代码
public synchronized boolean reconnectWithNewConfig(MqttConfigDTO newConfig) {
    // 1. 断开现有连接
    if (mqttClient != null && mqttClient.isConnected()) {
        mqttClient.disconnect(1000);
        mqttClient.close();
    }
    
    // 2. 更新配置
    this.mqttHost = newConfig.getHost();
    this.mqttUsername = newConfig.getUsername();
    this.mqttPassword = newConfig.getPassword();
    this.mqttClientId = newConfig.getClientId();
    
    // 3. 保存到数据库
    saveConfigToDatabase(newConfig);
    
    // 4. 重新连接
    connectMQTT();
    return isConnected();
}

四、前端监控界面

1. 设备统计面板

展示设备总数、在线数、离线数、被抢占数、认证失败次数等关键指标。

2. 设备列表

客户端ID 用户名 状态 IP地址 上次IP 抢占次数 更新时间
device_001 user1 在线 192.168.1.100 - 0 2024-01-15 10:30:25
device_002 user2 离线 192.168.1.101 192.168.1.100 1 2024-01-15 09:20:10

3. 数据格式统计

按设备查看数据格式分布,展示每种格式的出现次数、首次/最后出现时间、原始消息样本。

五、性能优化

1. Token 缓存

避免频繁调用 EMQX API 登录:

java

复制代码
private volatile String cachedToken;
private volatile Instant tokenExpiryTime;

private String getToken() throws Exception {
    if (cachedToken != null && 
        Instant.now().isBefore(tokenExpiryTime.minus(5, ChronoUnit.MINUTES))) {
        return cachedToken;
    }
    cachedToken = fetchToken();
    tokenExpiryTime = Instant.now().plus(1, ChronoUnit.HOURS);
    return cachedToken;
}

2. 批量处理

使用 Scheduled 定时批量写入,减少数据库压力:

java

复制代码
private final Map<String, FormatCounter> pendingUpdates = new ConcurrentHashMap<>();

@Scheduled(fixedDelay = 5000)
public void flushToDatabase() {
    Map<String, FormatCounter> toFlush = new HashMap<>(pendingUpdates);
    pendingUpdates.clear();
    repository.batchUpdate(toFlush);
}

3. 内存缓存

指纹缓存避免重复计算:

java

复制代码
private final Cache<String, FormatResult> fingerprintCache = Caffeine.newBuilder()
    .maximumSize(10000)
    .expireAfterWrite(1, TimeUnit.HOURS)
    .build();

六、关键技术点总结

技术点 实现方案
MQTT 连接 Eclipse Paho,自动重连
设备状态监控 订阅 EMQX $SYS 主题
在线设备同步 EMQX REST API v5
数据格式识别 规则引擎(JSON/混合/二进制)
格式指纹 MD5 结构特征
数据存储 H2 数据库
定时任务 Spring @Scheduled
配置管理 数据库存储 + 动态重连
前端框架 原生 HTML/CSS/JS

七、运行效果

系统启动后,监控界面实时显示:

  • ✅ MQTT 连接状态

  • ✅ 设备总数、在线数、离线数

  • ✅ 认证失败次数统计

  • ✅ 设备上下线实时事件

  • ✅ 数据格式分布统计

八、项目总结

本文实现了一个完整的 IoT 设备监控系统,具备以下特点:

  1. 实时性:通过 MQTT 订阅实时接收设备状态变化

  2. 准确性:启动时同步在线设备列表,保证状态一致

  3. 智能化:数据格式指纹识别,自动归类设备上报的数据

  4. 可维护:动态配置管理,支持运行时切换 EMQX 连接

  5. 高性能:Token 缓存、批量处理、内存缓存优化

这套监控系统可以快速接入任何基于 EMQX 的 IoT 项目,帮助运维人员实时掌握设备状态和数据格式变化。

十、参考资料


前端(AI):

相关推荐
铭毅天下2 小时前
EasySearch Rules 规则语法速查手册
开发语言·前端·javascript·ecmascript
YMWM_2 小时前
print(f“{s!r}“)解释
开发语言·r语言
愤豆2 小时前
05-Java语言核心-语法特性--模块化系统详解
java·开发语言·python
bksczm2 小时前
文件流(fstream)
java·开发语言
NGC_66112 小时前
Java 线程池阻塞队列与拒绝策略
java·开发语言
AI-Ming2 小时前
程序员转行学习 AI 大模型: 踩坑记录:服务器内存不够,程序被killed
服务器·人工智能·python·gpt·深度学习·学习·agi
小碗羊肉2 小时前
【从零开始学Java | 第二十二篇】List集合
java·开发语言
m0_716765232 小时前
C++提高编程--STL常用容器(set/multiset、map/multimap容器)详解
java·开发语言·c++·经验分享·学习·青少年编程·visual studio
2401_873544923 小时前
使用Python处理计算机图形学(PIL/Pillow)
jvm·数据库·python