本章用于记录实现前端与设备的通信功能所面临的问题和当前解决的思路。
PS:首先声明下。因为本人后端能力不强,因此这个设计肯定不是非常好的。但是我尽力能想到的方法了。望各位大佬还可以提供宝贵的建议。
由于目前client和server代码是分开编写的,目的是不想让MQTT和express的代码互相嵌套(2年前写的互相套什么app.post()套一堆client.on 然后client.on 又套数据库的查询...,自己都不知道当时是怎么跑得起来的。并且用之前的想法测试,都失败了(迷惑而神奇?!))但是目前面临的问题:router的 res 需要携带数据返回。如何将MQTT的message携带的消息放入 res 并且保证不会返回出错。这个问题还需要优化。希望各位大佬可以指点!
需求
在本系统中,针对用户发送控制设备或读取设备信息的GET/POST请求后,服务端需要发送MQTT协议给硬件,同时也需要监听硬件返回的信息从而返回给用户。整体流程如图所示(画的有点抽象)
举个例子:当用户点击获取设备温度的按钮时,会发生GET/POST请求给我们使用Express搭建的服务端(对应图中的1)。服务端接收到这个请求后,会发送特定主题的对应的MQTT消息给broker,由broker转发根据主题转发给设备(对应图中的2与3)。当设备收到该主题的消息时(假设主题是:我需要你告诉我目前你的温度),会发布一条消息给broker(假设主题是:这是设备目前的温度)。那么此时client就需要提前订阅这个主题的消息,以便可以接收到。当该消息通过broker转发给client后(对应5),我们的服务端需要获取到这个消息的内容,然后转发给前端(对应6)。
目前想到的思路
其实 1-6都好解决,但是问题的关键在于client收到了MQTT消息后,如何传递给服务端?
在MQTT中,client是通过client.on("messgae", (topic, message) => { })方法监听收到的消息。也就是说我们只能在这个回调函数中拿到设备发送过来的消息。且由于我们的client.js 与 app.js 是两个独立的文件。因此我目前想并尝试了4种方法:
- 将
client导出 (也就是整个逻辑仅有一个client统一接收和发送消息)
function controlDevcie(req, res, next) {
const { userId, deviceId, message } = req.body;
client.publish(`publish/${userId}/${deviceId}`, message, (err) => {
if (err) {
res.sendStatus(500);
}
});
client.subscribe(`subscribe/${userId}/${deviceId}`, (err) => {
if (err) {
res.sendStatus(502);
}
client.on("message", (topic, message) => {
if (topic === `subscribe/${userId}/${deviceId}`) {
console.log("Service 收到信息", message.toString());
res.json({
code: 200,
message: "操作成功",
data: message.toString(),
});
client.unsubscribe(`subscribe/${userId}/${deviceId}`);
}
});
});
}
module.exports = { controlDevcie };
- 在
app.js中编写对应获取数据的函数,将函数放入client.on("message")监听事件的回调函数中。也就是我们服务端的client始终监听(on 方法),当收到用户的请求时,就发布一个消息。 - 将
client与服务端之间再弄一个http通信。也就是user - server - client - broker - device - 每次遇到这种请求的时候,
Server自动为用户创建一个client,依次让client上线、发布(对应图中的2)、订阅、监听(对应图中的5)。并在失败/成功的时候返回这个消息(对应6),然后使用end()主动将其下线。(这也是目前采用的方法。)即我们收到了用户需要控制设备的消息,然后new 一个client专门针对这个消息来处理,然后将这个对象收到的消息返回。让res = 这个消息即可。(这样的好处就是我们的 app.post 请求的整体逻辑和读取数据库一样。都是在回调函数中return res)
目前存在的问题
目前针对这4种方案可能存在的问题:
- 由于client.on() 方法实际上是在在每次调用时添加一个新的消息监听器,而不是替换掉之前的监听器。意味着每当你发送一个请求时,都会添加一个新的消息监听器,导致在接收到消息时会触发多个监听器,从而多次发送 HTTP 响应。那么如果我们加上
client.removeAllListeners("message")移除之前添加的消息监听器呢?这会出现另外一个问题:多个用户发送请求的时候,前面的用户接收不到消息的情况。 - 这个函数按道理只会触发一次(用户发送请求的时候触发)。但放到 on 里面会多次执行,也就是每当收到消息的时候,这个函数都会被触发。并且如何去确定是哪一个 res 对象,这个好像不太能实现。
- 延迟可能会提升,多了一个Http请求的时间。且需要判断下返回给哪个请求。
- 对broker的负载变高了,多个client一直上下线,发布订阅等。资源可能有点浪费
对于方案1的改进:
我们可以封装一个messageListener 函数用于执行 client.on('message')的回调, 在该回调函数中,来移除监听器。因为在每次请求处理前都定义了一个新的 messageListener 函数,并且在处理完消息后移除了该函数。这样可以确保每次请求都有自己独立的消息监听器,并且在处理完消息后移除监听器,避免了上述出现的问题。
async function controlDevcie(req, res, next) {
const { userId, deviceId, message } = req.body;
// 定义 MQTT 客户端消息监听回调函数
function messageListener(topic, receivedMessage) {
if (topic === `subscribe/${userId}/${deviceId}`) {
console.log("Service 收到信息", receivedMessage.toString());
// 发送 HTTP 响应
res.json({
code: 200,
message: "操作成功",
data: receivedMessage.toString(),
});
// 取消订阅消息
MQTTclient.unsubscribe(`subscribe/${userId}/${deviceId}`);
// 移除消息监听器,避免重复处理
MQTTclient.removeListener("message", messageListener);
}
}
// 订阅消息
MQTTclient.subscribe(`subscribe/${userId}/${deviceId}`, (err) => {
if (err) {
return res.sendStatus(502);
}
// 成功订阅后,添加消息监听器
MQTTclient.on("message", messageListener);
});
// 发布消息
MQTTclient.publish(`publish/${userId}/${deviceId}`, message, (err) => {
if (err) {
return res.sendStatus(500);
}
});
}
module.exports = { controlDevcie };
目前对于方案4的代码:
我们把整个流程(2-6)可以统一封装成一个函数sendMQTT:传入的参数包括:发布的主题和消息(对应流程3),订阅的主题(对应流程5),以及当前请求所要返回的res对象。我们在这个函数里获取MQTT的消息数据并返回。
const mqtt = require("mqtt");
function sendMQTT(pubTopic, subTopic, message, res) {
const client = mqtt.connect("mqtt://localhost:9000");
client.publish(pubTopic, message, (err) => {
if (err) {
res.sendStatus(500);
client.end();
}
});
client.subscribe(subTopic, (err) => {
if (err) {
res.sendStatus(502);
client.end();
}
});
client.on("message", (topic, message) => {
if (topic === subTopic) {
res.json(message);
}
client.end();
});
}
module.exports = sendMQTT;
然后是服务端获取前端请求的路由:这里我们设置前端传来的是id
// 用于测试客户端与服务端之间的通信
const express = require("express");
const mqtt = require("mqtt");
const sendMQTT = require("../utils/sendMQTT");
const app = express();
app.get("/get/:id", (req, res) => {
const id = req.params.id;
sendMQTT(`pub${id}`, `sub${id}`, `getDeviceData`, res);
});
app.post("/post", (req, res) => {
console.log(req);
res.send("Hello, World!");
});
// app.use("/devices", deviceRouter);
app.listen(3000, () => {
console.log("Server is running on port 3000");
});
module.exports = app;
broker 还是和之前一样。 我们先启动broker,然后启动 express 服务端。在 postman 中模拟。流程如下:
-
postman 创建一个 MQTT 连接broker,并订阅所需要的主题 (
pubTopic) -
使用 postman 发送对应的get请求。
-
使用 postman 发布消息(主题为
subTopic) -
通过两个 postman 查看收到的消息。