为什么需要独立后端
PipeMonitor 的 STM32 设备通过 DTU 透传上云,MQTT 消息需要被解析、存储和对外暴露。Flutter 应用需要 REST API 查询历史数据和下发命令。这套逻辑需要一个后端服务来承载。
三容器架构
# docker-compose.yml 核心服务
services:
pipe-monitor-mysql: # 数据持久化
pipe-monitor-mqtt: # 设备消息 Broker
pipe-monitor-api: # HTTP/WS 服务
MySQL 容器
低频遥测写入,默认按小内存实例运行:
pipe-monitor-mysql:
image: mysql:8.4
mem_limit: "384m"
command:
- --performance-schema=OFF
- --innodb-buffer-pool-size=64M
- --max-connections=40
PipeMonitor 的数据写入频率很低(10 秒一帧,每帧 ~500 字节),不需要高性能 MySQL 配置。关闭 Performance Schema、限制 buffer pool 和连接数,384MB 内存足够稳定运行。
Mosquitto MQTT Broker
pipe-monitor-mqtt:
image: eclipse-mosquitto:2
ports:
- "1883:1883"
volumes:
- ./pipe-monitor-mqtt/mosquitto.conf:/mosquitto/config/mosquitto.conf:ro
- ./pipe-monitor-mqtt/aclfile:/mosquitto/config/aclfile:ro
设备通过公网 1883 端口连接,强制账号密码和 ACL 控制。
API 服务
pipe-monitor-api:
build:
context: ./services/pipe-monitor-api
environment:
MQTT_UP_TOPIC: "device/FM001/up"
MQTT_DOWN_TOPIC_TEMPLATE: "device/{dev}/down"
DB_HOST: "pipe-monitor-mysql"
API 容器同时连接 MQTT Broker 和 MySQL,作为设备数据和用户请求之间的桥梁。
Node.js API 服务
JWT 鉴权
// 登录接口:用户不存在与密码错误返回同一种响应,避免账号枚举攻击
const matched = user ? await verifyPassword(password, user.passwordHash) : false;
if (!user || !matched) {
res.status(401).json({ error: "invalid_credentials" });
return;
}
JWT 密钥必须通过环境变量注入一个 32 位以上的随机字符串,如果缺失或长度不足,服务启动时直接退出:
if (jwtSecretIsFallback || jwtSecret.length < 32) {
console.error("JWT_SECRET 未配置或长度不足 32 字符,启动中止");
process.exit(1);
}
REST API 路由
| 路由 | 方法 | 用途 |
|---|---|---|
/health | GET | 健康检查(公开) |
/api/auth/login | POST | 登录(公开) |
/api/latest | GET | 最新一帧遥测 |
/api/history | GET | 历史曲线数据(游标分页) |
/api/alarms | GET | 告警列表 |
/api/status | GET | 链路状态 |
/api/commands/upload-period | POST | 下发上报周期 |
所有 /api/* 路由(除 login 外)都经过 JWT 鉴权中间件。
命令下发:seq 整型溢出修复
服务端通过 MQTT 下发命令,命令序号初始用 Date.now() 生成 13 位时间戳,但 STM32 固件用 int32 解析 JSON 整型——1.7e12 远超 2^31-1,会被截断到错误值,设备回复的 cmd_seq 与 App 等待的 seq 对不上。
修复方案:改用进程内单调递增计数器,配合 mod 限幅:
let cmdSeqCounter = 0;
function nextCmdSeq() {
cmdSeqCounter = (cmdSeqCounter + 1) % 2_000_000_000;
return cmdSeqCounter;
}
WebSocket 实时推送
/ws/live 端点提供实时数据推送,客户端通过 ?token=<JWT> 携带鉴权令牌。WS 连接上可以收到:
- hello 帧:连接建立时推送当前最新数据快照
- message 帧:每次 MQTT 收到新遥测帧时实时推送
WS 和 REST 共享同一份运行时状态 state,保证实时流和请求口径一致。
优雅关闭
function shutdown(signal) {
live.close(); // 先停实时推送
mqttClient.end(false, () => { // 再停 MQTT
server.close(() => { // 再停 HTTP
dbStore.close().finally(() => process.exit(0));
});
});
// 15s 超时强退,避免容器编排被迫 SIGKILL
setTimeout(() => process.exit(1), 15000).unref();
}
MQTT Topic 设计
device/<deviceId>/up # 设备上行:tele / alarm / ack
device/<deviceId>/down # 云端下行:cmd JSON
up:设备通过 DTU 透传上来的 JSON 帧,每行以\n结尾down:云端下发的一行cmdJSON,设备收到后执行并回复 ack
安全加固
helmet中间件提供 X-Frame-Options、HSTS、默认 CSP 等响应头加固app.disable("x-powered-by")隐藏 Express 指纹- 请求体限制 128KB
- MQTT 强制账号密码 + ACL 白名单
小结
- 三容器架构:MySQL + Mosquitto + Node.js API,各司其职
- JWT 鉴权 + 防枚举登录 + 密钥强制注入
- 游标分页的 history 接口,避免深度分页性能问题
- WebSocket 实时推送,与 REST 共享同一运行时状态
- 命令序号使用进程内单调计数器,修复 32 位截断问题
- 优雅关闭保证容器编排不会被迫 SIGKILL