一、项目背景
在物联网应用中,设备通过 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 设备监控系统,具备以下特点:
-
实时性:通过 MQTT 订阅实时接收设备状态变化
-
准确性:启动时同步在线设备列表,保证状态一致
-
智能化:数据格式指纹识别,自动归类设备上报的数据
-
可维护:动态配置管理,支持运行时切换 EMQX 连接
-
高性能:Token 缓存、批量处理、内存缓存优化
这套监控系统可以快速接入任何基于 EMQX 的 IoT 项目,帮助运维人员实时掌握设备状态和数据格式变化。
十、参考资料
前端(AI):
