mqtt gateway project

This commit is contained in:
marlonz
2025-07-05 13:47:49 +09:30
parent aed54bb3a6
commit 076aaae944
12 changed files with 1899 additions and 0 deletions
+4
View File
@@ -0,0 +1,4 @@
node_modules/
build/
dist/
package-lock.json
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 Xiaoxia
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+215
View File
@@ -0,0 +1,215 @@
# MQTT+UDP 到 WebSocket 桥接服务
## 项目概述
这是一个用于物联网设备通信的桥接服务,实现了MQTT和UDP协议到WebSocket的转换。该服务允许设备通过MQTT协议进行控制消息传输,同时通过UDP协议高效传输音频数据,并将这些数据桥接到WebSocket服务。
## 功能特点
- **多协议支持**: 同时支持MQTT、UDP和WebSocket协议
- **音频数据传输**: 专为音频数据流优化的传输机制
- **加密通信**: 使用AES-128-CTR加密UDP数据传输
- **会话管理**: 完整的设备会话生命周期管理
- **自动重连**: 连接断开时自动重连机制
- **心跳检测**: 定期检查连接活跃状态
- **开发/生产环境配置**: 支持不同环境的配置切换
## 技术架构
- **MQTT服务器**: 处理设备控制消息
- **UDP服务器**: 处理高效的音频数据传输
- **WebSocket客户端**: 连接到聊天服务器
- **桥接层**: 在不同协议间转换和路由消息
## 项目结构
```
├── app.js # 主应用入口
├── mqtt-protocol.js # MQTT协议实现
├── ecosystem.config.js # PM2配置文件
├── package.json # 项目依赖
├── .env # 环境变量配置
├── utils/
│ ├── config-manager.js # 配置管理工具
│ ├── mqtt_config_v2.js # MQTT配置验证工具
│ └── weixinAlert.js # 微信告警工具
└── config/ # 配置文件目录
```
## 依赖项
- **debug**: 调试日志输出
- **dotenv**: 环境变量管理
- **ws**: WebSocket客户端
- **events**: Node.js 事件模块
## 安装要求
- Node.js 14.x 或更高版本
- npm 或 yarn 包管理器
- PM2 (用于生产环境部署)
## 安装步骤
1. 克隆仓库
```bash
git clone <仓库地址>
cd mqtt-websocket-bridge
```
2. 安装依赖
```bash
npm install
```
3. 创建配置文件
```bash
mkdir -p config
cp config/mqtt.json.example config/mqtt.json
```
4. 编辑配置文件 `config/mqtt.json`,设置适当的参数
## 配置说明
配置文件 `config/mqtt.json` 需要包含以下内容:
```json
{
"debug": false,
"development": {
"mac_addresss": ["aa:bb:cc:dd:ee:ff"],
"chat_servers": ["wss://dev-chat-server.example.com/ws"]
},
"production": {
"chat_servers": ["wss://chat-server.example.com/ws"]
}
}
```
## 环境变量
创建 `.env` 文件并设置以下环境变量:
```
MQTT_PORT=1883 # MQTT服务器端口
UDP_PORT=8884 # UDP服务器端口
PUBLIC_IP=your-ip # 服务器公网IP
```
## 运行服务
### 开发环境
```bash
# 直接运行
node app.js
# 调试模式运行
DEBUG=mqtt-server node app.js
```
### 生产环境 (使用PM2)
```bash
# 安装PM2
npm install -g pm2
# 启动服务
pm2 start ecosystem.config.js
# 查看日志
pm2 logs xz-mqtt
# 监控服务
pm2 monit
```
服务将在以下端口启动:
- MQTT 服务器: 端口 1883 (可通过环境变量修改)
- UDP 服务器: 端口 8884 (可通过环境变量修改)
## 协议说明
### 设备连接流程
1. 设备通过MQTT协议连接到服务器
2. 设备发送 `hello` 消息,包含音频参数和特性
3. 服务器创建WebSocket连接到聊天服务器
4. 服务器返回UDP连接参数给设备
5. 设备通过UDP发送音频数据
6. 服务器将音频数据转发到WebSocket
7. WebSocket返回的控制消息通过MQTT发送给设备
### 消息格式
#### Hello 消息 (设备 -> 服务器)
```json
{
"type": "hello",
"version": 3,
"audio_params": { ... },
"features": { ... }
}
```
#### Hello 响应 (服务器 -> 设备)
```json
{
"type": "hello",
"version": 3,
"session_id": "uuid",
"transport": "udp",
"udp": {
"server": "server-ip",
"port": 8884,
"encryption": "aes-128-ctr",
"key": "hex-encoded-key",
"nonce": "hex-encoded-nonce"
},
"audio_params": { ... }
}
```
## 安全说明
- UDP通信使用AES-128-CTR加密
- 每个会话使用唯一的加密密钥
- 使用序列号防止重放攻击
- 设备通过MAC地址进行身份验证
- 支持设备分组和UUID验证
## 性能优化
- 使用预分配的缓冲区减少内存分配
- UDP协议用于高效传输音频数据
- 定期清理不活跃的连接
- 连接数和活跃连接数监控
- 支持多聊天服务器负载均衡
## 故障排除
- 检查设备MAC地址格式是否正确
- 确保UDP端口在防火墙中开放
- 启用调试模式查看详细日志
- 检查配置文件中的聊天服务器地址是否正确
- 验证设备认证信息是否正确
## 开发指南
### 添加新功能
1. 修改 `mqtt-protocol.js` 以支持新的MQTT功能
2.`MQTTConnection` 类中添加新的消息处理方法
3. 更新配置管理器以支持新的配置选项
4.`WebSocketBridge` 类中添加新的WebSocket处理逻辑
### 调试技巧
```bash
# 启用所有调试输出
DEBUG=* node app.js
# 只启用MQTT服务器调试
DEBUG=mqtt-server node app.js
```
+623
View File
@@ -0,0 +1,623 @@
// Description: MQTT+UDP 到 WebSocket 的桥接
// Author: terrence@tenclass.com
// Date: 2025-03-12
require('dotenv').config();
const net = require('net');
const debugModule = require('debug');
const debug = debugModule('mqtt-server');
const crypto = require('crypto');
const dgram = require('dgram');
const Emitter = require('events');
const WebSocket = require('ws');
const { MQTTProtocol } = require('./mqtt-protocol');
const { ConfigManager } = require('./utils/config-manager');
const { validateMqttCredentials } = require('./utils/mqtt_config_v2');
function setDebugEnabled(enabled) {
if (enabled) {
debugModule.enable('mqtt-server');
} else {
debugModule.disable();
}
}
const configManager = new ConfigManager('mqtt.json');
configManager.on('configChanged', (config) => {
setDebugEnabled(config.debug);
});
setDebugEnabled(configManager.get('debug'));
class WebSocketBridge extends Emitter {
constructor(connection, protocolVersion, macAddress, uuid, userData) {
super();
this.connection = connection;
this.macAddress = macAddress;
this.uuid = uuid;
this.userData = userData;
this.wsClient = null;
this.protocolVersion = protocolVersion;
this.deviceSaidGoodbye = false;
this.initializeChatServer();
}
initializeChatServer() {
const devMacAddresss = configManager.get('development')?.mac_addresss || [];
let chatServers;
if (devMacAddresss.includes(this.macAddress)) {
chatServers = configManager.get('development')?.chat_servers;
} else {
chatServers = configManager.get('production')?.chat_servers;
}
if (!chatServers) {
throw new Error(`未找到 ${this.macAddress} 的聊天服务器`);
}
this.chatServer = chatServers[Math.floor(Math.random() * chatServers.length)];
}
async connect(audio_params, features) {
return new Promise((resolve, reject) => {
const headers = {
'device-id': this.macAddress,
'protocol-version': '2',
'authorization': `Bearer test-token`
};
if (this.uuid) {
headers['client-id'] = this.uuid;
}
if (this.userData && this.userData.ip) {
headers['x-forwarded-for'] = this.userData.ip;
}
this.wsClient = new WebSocket(this.chatServer, { headers });
this.wsClient.on('open', () => {
this.sendJson({
type: 'hello',
version: 2,
transport: 'websocket',
audio_params,
features
});
});
this.wsClient.on('message', (data, isBinary) => {
if (isBinary) {
const timestamp = data.readUInt32BE(8);
const opusLength = data.readUInt32BE(12);
const opus = data.subarray(16, 16 + opusLength);
// 二进制数据通过UDP发送
this.connection.sendUdpMessage(opus, timestamp);
} else {
// JSON数据通过MQTT发送
const message = JSON.parse(data.toString());
if (message.type === 'hello') {
resolve(message);
} else {
this.connection.sendMqttMessage(JSON.stringify(message));
}
}
});
this.wsClient.on('error', (error) => {
console.error(`WebSocket error for device ${this.macAddress}:`, error);
this.emit('close');
reject(error);
});
this.wsClient.on('close', () => {
this.emit('close');
});
});
}
sendJson(message) {
if (this.wsClient && this.wsClient.readyState === WebSocket.OPEN) {
this.wsClient.send(JSON.stringify(message));
}
}
sendAudio(opus, timestamp) {
if (this.wsClient && this.wsClient.readyState === WebSocket.OPEN) {
const buffer = Buffer.alloc(16 + opus.length);
buffer.writeUInt32BE(timestamp, 8);
buffer.writeUInt32BE(opus.length, 12);
buffer.set(opus, 16);
this.wsClient.send(buffer, { binary: true });
}
}
isAlive() {
return this.wsClient && this.wsClient.readyState === WebSocket.OPEN;
}
close() {
if (this.wsClient) {
this.wsClient.close();
this.wsClient = null;
}
}
}
const MacAddressRegex = /^[0-9a-f]{2}(:[0-9a-f]{2}){5}$/;
/**
* MQTT连接类
* 负责应用层逻辑处理
*/
class MQTTConnection {
constructor(socket, connectionId, server) {
this.server = server;
this.connectionId = connectionId;
this.clientId = null;
this.username = null;
this.password = null;
this.bridge = null;
this.udp = {
remoteAddress: null,
cookie: null,
localSequence: 0,
remoteSequence: 0
};
this.headerBuffer = Buffer.alloc(16);
// 创建协议处理器,并传入socket
this.protocol = new MQTTProtocol(socket);
this.setupProtocolHandlers();
}
setupProtocolHandlers() {
// 设置协议事件处理
this.protocol.on('connect', (connectData) => {
this.handleConnect(connectData);
});
this.protocol.on('publish', (publishData) => {
this.handlePublish(publishData);
});
this.protocol.on('subscribe', (subscribeData) => {
this.handleSubscribe(subscribeData);
});
this.protocol.on('disconnect', () => {
this.handleDisconnect();
});
this.protocol.on('close', () => {
debug(`${this.clientId} 客户端断开连接`);
this.server.removeConnection(this);
});
this.protocol.on('error', (err) => {
debug(`${this.clientId} 连接错误:`, err);
this.close();
});
this.protocol.on('protocolError', (err) => {
debug(`${this.clientId} 协议错误:`, err);
this.close();
});
}
handleConnect(connectData) {
this.clientId = connectData.clientId;
this.username = connectData.username;
this.password = connectData.password;
debug('客户端连接:', {
clientId: this.clientId,
username: this.username,
password: this.password,
protocol: connectData.protocol,
protocolLevel: connectData.protocolLevel,
keepAlive: connectData.keepAlive
});
const parts = this.clientId.split('@@@');
if (parts.length === 3) { // GID_test@@@mac_address@@@uuid
try {
const validated = validateMqttCredentials(this.clientId, this.username, this.password);
this.groupId = validated.groupId;
this.macAddress = validated.macAddress;
this.uuid = validated.uuid;
this.userData = validated.userData;
} catch (error) {
debug('MQTT凭据验证失败:', error.message);
this.close();
return;
}
} else if (parts.length === 2) { // GID_test@@@mac_address
this.groupId = parts[0];
this.macAddress = parts[1].replace(/_/g, ':');
if (!MacAddressRegex.test(this.macAddress)) {
debug('无效的 macAddress:', this.macAddress);
this.close();
return;
}
} else {
debug('无效的 clientId:', this.clientId);
this.close();
return;
}
this.replyTo = `devices/p2p/${parts[1]}`;
this.server.addConnection(this);
}
handleSubscribe(subscribeData) {
debug('客户端订阅主题:', {
clientId: this.clientId,
topic: subscribeData.topic,
packetId: subscribeData.packetId
});
// 发送 SUBACK
this.protocol.sendSuback(subscribeData.packetId, 0);
}
handleDisconnect() {
debug('收到断开连接请求:', { clientId: this.clientId });
// 清理连接
this.server.removeConnection(this);
}
close() {
this.closing = true;
if (this.bridge) {
this.bridge.close();
this.bridge = null;
} else {
this.protocol.close();
}
}
checkKeepAlive() {
const now = Date.now();
const keepAliveInterval = this.protocol.getKeepAliveInterval();
// 如果keepAliveInterval为0,表示不需要心跳检查
if (keepAliveInterval === 0 || !this.protocol.isConnected) return;
const lastActivity = this.protocol.getLastActivity();
const timeSinceLastActivity = now - lastActivity;
// 如果超过心跳间隔,关闭连接
if (timeSinceLastActivity > keepAliveInterval) {
debug('心跳超时,关闭连接:', this.clientId);
this.close();
}
}
handlePublish(publishData) {
debug('收到发布消息:', {
clientId: this.clientId,
topic: publishData.topic,
payload: publishData.payload,
qos: publishData.qos
});
if (publishData.qos !== 0) {
debug('不支持的 QoS 级别:', publishData.qos, '关闭连接');
this.close();
return;
}
const json = JSON.parse(publishData.payload);
if (json.type === 'hello') {
if (json.version !== 3) {
debug('不支持的协议版本:', json.version, '关闭连接');
this.close();
return;
}
this.parseHelloMessage(json).catch(error => {
debug('处理 hello 消息失败:', error);
this.close();
});
} else {
this.parseOtherMessage(json).catch(error => {
debug('处理其他消息失败:', error);
this.close();
});
}
}
sendMqttMessage(payload) {
debug(`发送消息到 ${this.replyTo}: ${payload}`);
this.protocol.sendPublish(this.replyTo, payload, 0, false, false);
}
sendUdpMessage(payload, timestamp) {
if (!this.udp.remoteAddress) {
debug(`设备 ${this.clientId} 未连接,无法发送 UDP 消息`);
return;
}
this.udp.localSequence++;
const header = this.generateUdpHeader(payload.length, timestamp, this.udp.localSequence);
const cipher = crypto.createCipheriv(this.udp.encryption, this.udp.key, header);
const message = Buffer.concat([header, cipher.update(payload), cipher.final()]);
this.server.sendUdpMessage(message, this.udp.remoteAddress);
}
generateUdpHeader(length, timestamp, sequence) {
// 重用预分配的缓冲区
this.headerBuffer.writeUInt8(1, 0);
this.headerBuffer.writeUInt16BE(length, 2);
this.headerBuffer.writeUInt32BE(this.connectionId, 4);
this.headerBuffer.writeUInt32BE(timestamp, 8);
this.headerBuffer.writeUInt32BE(sequence, 12);
return Buffer.from(this.headerBuffer); // 返回副本以避免并发问题
}
async parseHelloMessage(json) {
this.udp = {
...this.udp,
key: crypto.randomBytes(16),
nonce: this.generateUdpHeader(0, 0, 0),
encryption: 'aes-128-ctr',
remoteSequence: 0,
localSequence: 0,
startTime: Date.now()
}
if (this.bridge) {
debug(`${this.clientId} 收到重复 hello 消息,关闭之前的 bridge`);
this.bridge.close();
await new Promise(resolve => setTimeout(resolve, 100));
}
this.bridge = new WebSocketBridge(this, json.version, this.macAddress, this.uuid, this.userData);
this.bridge.on('close', () => {
const seconds = (Date.now() - this.udp.startTime) / 1000;
console.log(`通话结束: ${this.clientId} Session: ${this.udp.session_id} Duration: ${seconds}s`);
this.sendMqttMessage(JSON.stringify({ type: 'goodbye', session_id: this.udp.session_id }));
this.bridge = null;
if (this.closing) {
this.protocol.close();
}
});
try {
console.log(`通话开始: ${this.clientId} Protocol: ${json.version} ${this.bridge.chatServer}`);
const helloReply = await this.bridge.connect(json.audio_params, json.features);
this.udp.session_id = helloReply.session_id;
this.sendMqttMessage(JSON.stringify({
type: 'hello',
version: json.version,
session_id: this.udp.session_id,
transport: 'udp',
udp: {
server: this.server.publicIp,
port: this.server.udpPort,
encryption: this.udp.encryption,
key: this.udp.key.toString('hex'),
nonce: this.udp.nonce.toString('hex'),
},
audio_params: helloReply.audio_params
}));
} catch (error) {
this.sendMqttMessage(JSON.stringify({ type: 'error', message: '处理 hello 消息失败' }));
console.error(`${this.clientId} 处理 hello 消息失败: ${error}`);
}
}
async parseOtherMessage(json) {
if (!this.bridge) {
if (json.type !== 'goodbye') {
this.sendMqttMessage(JSON.stringify({ type: 'goodbye', session_id: json.session_id }));
}
return;
}
if (json.type === 'goodbye') {
this.bridge.close();
this.bridge = null;
return;
}
this.bridge.sendJson(json);
}
onUdpMessage(rinfo, message, payloadLength, timestamp, sequence) {
if (!this.bridge) {
return;
}
if (this.udp.remoteAddress !== rinfo) {
this.udp.remoteAddress = rinfo;
}
if (sequence < this.udp.remoteSequence) {
return;
}
// 处理加密数据
const header = message.slice(0, 16);
const encryptedPayload = message.slice(16, 16 + payloadLength);
const cipher = crypto.createDecipheriv(this.udp.encryption, this.udp.key, header);
const payload = Buffer.concat([cipher.update(encryptedPayload), cipher.final()]);
this.bridge.sendAudio(payload, timestamp);
this.udp.remoteSequence = sequence;
}
isAlive() {
return this.bridge && this.bridge.isAlive();
}
}
class MQTTServer {
constructor() {
this.mqttPort = parseInt(process.env.MQTT_PORT) || 1883;
this.udpPort = parseInt(process.env.UDP_PORT) || this.mqttPort;
this.publicIp = process.env.PUBLIC_IP || 'mqtt.xiaozhi.me';
this.connections = new Map(); // clientId -> MQTTConnection
this.keepAliveTimer = null;
this.keepAliveCheckInterval = 1000; // 默认每1秒检查一次
this.headerBuffer = Buffer.alloc(16);
}
generateNewConnectionId() {
// 生成一个32位不重复的整数
let id;
do {
id = Math.floor(Math.random() * 0xFFFFFFFF);
} while (this.connections.has(id));
return id;
}
start() {
this.mqttServer = net.createServer((socket) => {
const connectionId = this.generateNewConnectionId();
debug(`新客户端连接: ${connectionId}`);
new MQTTConnection(socket, connectionId, this);
});
this.mqttServer.listen(this.mqttPort, () => {
console.warn(`MQTT 服务器正在监听端口 ${this.mqttPort}`);
});
this.udpServer = dgram.createSocket('udp4');
this.udpServer.on('message', this.onUdpMessage.bind(this));
this.udpServer.on('error', err => {
console.error('UDP 错误', err);
setTimeout(() => { process.exit(1); }, 1000);
});
this.udpServer.bind(this.udpPort, () => {
console.warn(`UDP 服务器正在监听 ${this.publicIp}:${this.udpPort}`);
});
// 启动全局心跳检查定时器
this.setupKeepAliveTimer();
}
/**
* 设置全局心跳检查定时器
*/
setupKeepAliveTimer() {
// 清除现有定时器
this.clearKeepAliveTimer();
this.lastConnectionCount = 0;
this.lastActiveConnectionCount = 0;
// 设置新的定时器
this.keepAliveTimer = setInterval(() => {
// 检查所有连接的心跳状态
for (const connection of this.connections.values()) {
connection.checkKeepAlive();
}
const activeCount = Array.from(this.connections.values()).filter(connection => connection.isAlive()).length;
if (activeCount !== this.lastActiveConnectionCount || this.connections.size !== this.lastConnectionCount) {
console.log(`连接数: ${this.connections.size}, 活跃数: ${activeCount}`);
this.lastActiveConnectionCount = activeCount;
this.lastConnectionCount = this.connections.size;
}
}, this.keepAliveCheckInterval);
}
/**
* 清除心跳检查定时器
*/
clearKeepAliveTimer() {
if (this.keepAliveTimer) {
clearInterval(this.keepAliveTimer);
this.keepAliveTimer = null;
}
}
addConnection(connection) {
// 检查是否已存在相同 clientId 的连接
for (const [key, value] of this.connections.entries()) {
if (value.clientId === connection.clientId) {
debug(`${connection.clientId} 已存在连接,关闭旧连接`);
value.close();
}
}
this.connections.set(connection.connectionId, connection);
}
removeConnection(connection) {
debug(`关闭连接: ${connection.connectionId}`);
if (this.connections.has(connection.connectionId)) {
this.connections.delete(connection.connectionId);
}
}
sendUdpMessage(message, remoteAddress) {
this.udpServer.send(message, remoteAddress.port, remoteAddress.address);
}
onUdpMessage(message, rinfo) {
// message format: [type: 1u, flag: 1u, payloadLength: 2u, cookie: 4u, timestamp: 4u, sequence: 4u, payload: n]
if (message.length < 16) {
console.warn('收到不完整的 UDP Header', rinfo);
return;
}
try {
const type = message.readUInt8(0);
if (type !== 1) return;
const payloadLength = message.readUInt16BE(2);
if (message.length < 16 + payloadLength) return;
const connectionId = message.readUInt32BE(4);
const connection = this.connections.get(connectionId);
if (!connection) return;
const timestamp = message.readUInt32BE(8);
const sequence = message.readUInt32BE(12);
connection.onUdpMessage(rinfo, message, payloadLength, timestamp, sequence);
} catch (error) {
console.error('UDP 消息处理错误:', error);
}
}
/**
* 停止服务器
*/
async stop() {
if (this.stopping) {
return;
}
this.stopping = true;
// 清除心跳检查定时器
this.clearKeepAliveTimer();
if (this.connections.size > 0) {
console.warn(`等待 ${this.connections.size} 个连接关闭`);
for (const connection of this.connections.values()) {
connection.close();
}
await new Promise(resolve => setTimeout(resolve, 300));
debug('等待连接关闭完成');
this.connections.clear();
}
if (this.udpServer) {
this.udpServer.close();
this.udpServer = null;
console.warn('UDP 服务器已停止');
}
// 关闭MQTT服务器
if (this.mqttServer) {
this.mqttServer.close();
this.mqttServer = null;
console.warn('MQTT 服务器已停止');
}
process.exit(0);
}
}
// 创建并启动服务器
const server = new MQTTServer();
server.start();
process.on('SIGINT', () => {
console.warn('收到 SIGINT 信号,开始关闭');
server.stop();
});
@@ -0,0 +1,15 @@
{
"production": {
"chat_servers": [
"ws://example:8080"
]
},
"development": {
"chat_servers": [
"ws://example:8180"
],
"mac_addresss": [
]
},
"debug": false
}
+12
View File
@@ -0,0 +1,12 @@
{
"production": {
"chat_servers": [
"ws://192.168.68.66:8000/xiaozhi/v1/"
]
},
"development": {
"chat_servers": ["ws://192.168.68.66:8000/xiaozhi/v1/"],
"mac_addresss": ["d8:43:ae:3e:4b:5a"]
},
"debug": false
}
+9
View File
@@ -0,0 +1,9 @@
module.exports = {
"apps": [
{
"name": "xz-mqtt",
"script": "app.js",
"time": true
}
]
}
@@ -0,0 +1,81 @@
#!/usr/bin/env node
/**
* MQTT签名密钥生成器
* 用于生成MQTT_SIGNATURE_KEY环境变量
*/
const crypto = require('crypto');
function generateSecureKey(length = 32) {
// 生成随机字节
const randomBytes = crypto.randomBytes(length);
// 转换为base64字符串
const base64Key = randomBytes.toString('base64');
return base64Key;
}
function generateHexKey(length = 32) {
// 生成随机字节
const randomBytes = crypto.randomBytes(length);
// 转换为十六进制字符串
const hexKey = randomBytes.toString('hex');
return hexKey;
}
function generateUUIDKey() {
// 使用UUID v4作为密钥
const uuid = crypto.randomUUID();
return uuid;
}
console.log('='.repeat(60));
console.log('MQTT签名密钥生成器');
console.log('='.repeat(60));
console.log('\n1. Base64格式密钥 (推荐):');
const base64Key = generateSecureKey();
console.log(` ${base64Key}`);
console.log('\n2. 十六进制格式密钥:');
const hexKey = generateHexKey();
console.log(` ${hexKey}`);
console.log('\n3. UUID格式密钥:');
const uuidKey = generateUUIDKey();
console.log(` ${uuidKey}`);
console.log('\n='.repeat(60));
console.log('使用方法:');
console.log('='.repeat(60));
console.log('\n在Windows PowerShell中设置环境变量:');
console.log(`$env:MQTT_SIGNATURE_KEY="${base64Key}"`);
console.log('\n在Windows CMD中设置环境变量:');
console.log(`set MQTT_SIGNATURE_KEY=${base64Key}`);
console.log('\n在Linux/macOS中设置环境变量:');
console.log(`export MQTT_SIGNATURE_KEY="${base64Key}"`);
console.log('\n在.env文件中设置:');
console.log(`MQTT_SIGNATURE_KEY=${base64Key}`);
console.log('\n='.repeat(60));
console.log('注意事项:');
console.log('='.repeat(60));
console.log('1. 请妥善保管生成的密钥,不要泄露给他人');
console.log('2. 在生产环境中,建议使用更长的密钥 (64字节)');
console.log('3. 密钥设置后需要重启MQTT服务器才能生效');
console.log('4. 客户端连接时需要使用相同的密钥生成密码签名');
// 如果提供了命令行参数,生成指定长度的密钥
if (process.argv[2]) {
const customLength = parseInt(process.argv[2]);
if (customLength > 0) {
console.log(`\n自定义长度密钥 (${customLength}字节):`);
console.log(` ${generateSecureKey(customLength)}`);
}
}
+499
View File
@@ -0,0 +1,499 @@
const debug = require('debug')('mqtt-server');
const EventEmitter = require('events');
// MQTT 固定头部的类型
const PacketType = {
CONNECT: 1,
CONNACK: 2,
PUBLISH: 3,
SUBSCRIBE: 8,
SUBACK: 9,
PINGREQ: 12,
PINGRESP: 13,
DISCONNECT: 14 // 添加 DISCONNECT
};
/**
* MQTT协议处理类
* 负责MQTT协议的解析和封装,以及心跳维持
*/
class MQTTProtocol extends EventEmitter {
constructor(socket) {
super();
this.socket = socket;
this.buffer = Buffer.alloc(0);
this.isConnected = false;
this.keepAliveInterval = 0;
this.lastActivity = Date.now();
this.setupSocketHandlers();
}
/**
* 设置Socket事件处理
*/
setupSocketHandlers() {
this.socket.on('data', (data) => {
this.lastActivity = Date.now();
this.buffer = Buffer.concat([this.buffer, data]);
this.processBuffer();
});
this.socket.on('close', () => {
this.emit('close');
});
this.socket.on('error', (err) => {
this.emit('error', err);
});
}
/**
* 处理缓冲区中的所有完整消息
*/
processBuffer() {
// 持续处理缓冲区中的数据,直到没有完整的消息可以处理
while (this.buffer.length > 0) {
// 至少需要2个字节才能开始解析(1字节固定头部 + 至少1字节的剩余长度)
if (this.buffer.length < 2) return;
try {
// 获取消息类型
const firstByte = this.buffer[0];
const type = (firstByte >> 4);
// 解析剩余长度
const { value: remainingLength, bytesRead } = this.decodeRemainingLength(this.buffer);
// 计算整个消息的长度
const messageLength = 1 + bytesRead + remainingLength;
// 检查缓冲区中是否有完整的消息
if (this.buffer.length < messageLength) {
// 消息不完整,等待更多数据
return;
}
// 提取完整的消息
const message = this.buffer.subarray(0, messageLength);
if (!this.isConnected && type !== PacketType.CONNECT) {
debug('未连接时收到非CONNECT消息,关闭连接');
this.socket.end();
return;
}
// 根据消息类型处理
switch (type) {
case PacketType.CONNECT:
this.parseConnect(message);
break;
case PacketType.PUBLISH:
this.parsePublish(message);
break;
case PacketType.SUBSCRIBE:
this.parseSubscribe(message);
break;
case PacketType.PINGREQ:
this.parsePingReq(message);
break;
case PacketType.DISCONNECT:
this.parseDisconnect(message);
break;
default:
debug('未处理的包类型:', type, message);
this.emit('protocolError', new Error(`未处理的包类型: ${type}`));
}
// 从缓冲区中移除已处理的消息
this.buffer = this.buffer.subarray(messageLength);
} catch (err) {
// 如果解析出错,可能是数据不完整,等待更多数据
if (err.message === 'Malformed Remaining Length') {
return;
}
// 其他错误可能是协议错误,清空缓冲区并发出错误事件
this.buffer = Buffer.alloc(0);
this.emit('protocolError', err);
return;
}
}
}
/**
* 解析MQTT报文中的Remaining Length字段
* @param {Buffer} buffer - 消息缓冲区
* @returns {{value: number, bytesRead: number}} 包含解析的值和读取的字节数
*/
decodeRemainingLength(buffer) {
let multiplier = 1;
let value = 0;
let bytesRead = 0;
let digit;
do {
if (bytesRead >= 4 || bytesRead >= buffer.length - 1) {
throw new Error('Malformed Remaining Length');
}
digit = buffer[bytesRead + 1];
bytesRead++;
value += (digit & 127) * multiplier;
multiplier *= 128;
} while ((digit & 128) !== 0);
return { value, bytesRead };
}
/**
* 编码MQTT报文中的Remaining Length字段
* @param {number} length - 要编码的长度值
* @returns {{bytes: Buffer, bytesLength: number}} 包含编码后的字节和字节长度
*/
encodeRemainingLength(length) {
let digit;
const bytes = Buffer.alloc(4); // 最多4个字节
let bytesLength = 0;
do {
digit = length % 128;
length = Math.floor(length / 128);
// 如果还有更多字节,设置最高位
if (length > 0) {
digit |= 0x80;
}
bytes[bytesLength++] = digit;
} while (length > 0 && bytesLength < 4);
return { bytes, bytesLength };
}
/**
* 解析CONNECT消息
* @param {Buffer} message - 完整的CONNECT消息
*/
parseConnect(message) {
// 解析剩余长度
const { value: remainingLength, bytesRead } = this.decodeRemainingLength(message);
// 固定头部之后的位置 (MQTT固定头部第一个字节 + Remaining Length字段的字节)
const headerLength = 1 + bytesRead;
// 从可变头部开始位置读取协议名长度
const protocolLength = message.readUInt16BE(headerLength);
const protocol = message.toString('utf8', headerLength + 2, headerLength + 2 + protocolLength);
// 更新位置指针,跳过协议名
let pos = headerLength + 2 + protocolLength;
// 协议级别,4为MQTT 3.1.1
const protocolLevel = message[pos];
// 检查协议版本
if (protocolLevel !== 4) { // 4 表示 MQTT 3.1.1
debug('不支持的协议版本:', protocolLevel);
// 发送 CONNACK,使用不支持的协议版本的返回码 (0x01)
this.sendConnack(1, false);
// 关闭连接
this.socket.end();
return;
}
pos += 1;
// 连接标志
const connectFlags = message[pos];
const hasUsername = (connectFlags & 0x80) !== 0;
const hasPassword = (connectFlags & 0x40) !== 0;
const cleanSession = (connectFlags & 0x02) !== 0;
pos += 1;
// 保持连接时间
const keepAlive = message.readUInt16BE(pos);
pos += 2;
// 解析 clientId
const clientIdLength = message.readUInt16BE(pos);
pos += 2;
const clientId = message.toString('utf8', pos, pos + clientIdLength);
pos += clientIdLength;
// 解析 username(如果存在)
let username = '';
if (hasUsername) {
const usernameLength = message.readUInt16BE(pos);
pos += 2;
username = message.toString('utf8', pos, pos + usernameLength);
pos += usernameLength;
}
// 解析 password(如果存在)
let password = '';
if (hasPassword) {
const passwordLength = message.readUInt16BE(pos);
pos += 2;
password = message.toString('utf8', pos, pos + passwordLength);
pos += passwordLength;
}
// 设置心跳间隔(客户端指定的keepAlive值的1.5倍,单位为秒)
this.keepAliveInterval = keepAlive * 1000 * 1.5;
// 发送 CONNACK
this.sendConnack(0, false);
// 标记为已连接
this.isConnected = true;
// 发出连接事件
this.emit('connect', {
clientId,
protocol,
protocolLevel,
keepAlive,
username,
password,
cleanSession
});
}
/**
* 解析PUBLISH消息
* @param {Buffer} message - 完整的PUBLISH消息
*/
parsePublish(message) {
// 从第一个字节中提取QoS级别(bits 1-2)
const firstByte = message[0];
const qos = (firstByte & 0x06) >> 1; // 0x06 是二进制 00000110,用于掩码提取QoS位
const dup = (firstByte & 0x08) !== 0; // 0x08 是二进制 00001000,用于掩码提取DUP标志
const retain = (firstByte & 0x01) !== 0; // 0x01 是二进制 00000001,用于掩码提取RETAIN标志
// 使用通用方法解析剩余长度
const { value: remainingLength, bytesRead } = this.decodeRemainingLength(message);
// 固定头部之后的位置 (MQTT固定头部第一个字节 + Remaining Length字段的字节)
const headerLength = 1 + bytesRead;
// 解析主题
const topicLength = message.readUInt16BE(headerLength);
const topic = message.toString('utf8', headerLength + 2, headerLength + 2 + topicLength);
// 对于QoS > 0,包含消息ID
let packetId = null;
let payloadStart = headerLength + 2 + topicLength;
if (qos > 0) {
packetId = message.readUInt16BE(payloadStart);
payloadStart += 2;
}
// 解析有效载荷
const payload = message.slice(payloadStart).toString('utf8');
// 发出发布事件
this.emit('publish', {
topic,
payload,
qos,
dup,
retain,
packetId
});
}
/**
* 解析SUBSCRIBE消息
* @param {Buffer} message - 完整的SUBSCRIBE消息
*/
parseSubscribe(message) {
const packetId = message.readUInt16BE(2);
const topicLength = message.readUInt16BE(4);
const topic = message.toString('utf8', 6, 6 + topicLength);
const qos = message[6 + topicLength]; // QoS值
// 发出订阅事件
this.emit('subscribe', {
packetId,
topic,
qos
});
}
/**
* 解析PINGREQ消息
* @param {Buffer} message - 完整的PINGREQ消息
*/
parsePingReq(message) {
debug('收到心跳请求');
// 发送 PINGRESP
this.sendPingResp();
debug('已发送心跳响应');
}
/**
* 解析DISCONNECT消息
* @param {Buffer} message - 完整的DISCONNECT消息
*/
parseDisconnect(message) {
// 标记为未连接
this.isConnected = false;
// 发出断开连接事件
this.emit('disconnect');
// 关闭 socket
this.socket.end();
}
/**
* 发送CONNACK消息
* @param {number} returnCode - 返回码
* @param {boolean} sessionPresent - 会话存在标志
*/
sendConnack(returnCode = 0, sessionPresent = false) {
if (!this.socket.writable) return;
const packet = Buffer.from([
PacketType.CONNACK << 4,
2, // Remaining length
sessionPresent ? 1 : 0, // Connect acknowledge flags
returnCode // Return code
]);
this.socket.write(packet);
}
/**
* 发送PUBLISH消息
* @param {string} topic - 主题
* @param {string} payload - 有效载荷
* @param {number} qos - QoS级别
* @param {boolean} dup - 重复标志
* @param {boolean} retain - 保留标志
* @param {number} packetId - 包ID(仅QoS > 0时需要)
*/
sendPublish(topic, payload, qos = 0, dup = false, retain = false, packetId = null) {
if (!this.isConnected || !this.socket.writable) return;
const topicLength = Buffer.byteLength(topic);
const payloadLength = Buffer.byteLength(payload);
// 计算剩余长度
let remainingLength = 2 + topicLength + payloadLength;
// 如果QoS > 0,需要包含包ID
if (qos > 0 && packetId) {
remainingLength += 2;
}
// 编码可变长度
const { bytes: remainingLengthBytes, bytesLength: remainingLengthSize } = this.encodeRemainingLength(remainingLength);
// 分配缓冲区:固定头部(1字节) + 可变长度字段 + 剩余长度值
const packet = Buffer.alloc(1 + remainingLengthSize + remainingLength);
// 写入固定头部
let firstByte = PacketType.PUBLISH << 4;
if (dup) firstByte |= 0x08;
if (qos > 0) firstByte |= (qos << 1);
if (retain) firstByte |= 0x01;
packet[0] = firstByte;
// 写入可变长度字段
remainingLengthBytes.copy(packet, 1, 0, remainingLengthSize);
// 写入主题长度和主题
const variableHeaderStart = 1 + remainingLengthSize;
packet.writeUInt16BE(topicLength, variableHeaderStart);
packet.write(topic, variableHeaderStart + 2);
// 如果QoS > 0,写入包ID
let payloadStart = variableHeaderStart + 2 + topicLength;
if (qos > 0 && packetId) {
packet.writeUInt16BE(packetId, payloadStart);
payloadStart += 2;
}
// 写入有效载荷
packet.write(payload, payloadStart);
this.socket.write(packet);
this.lastActivity = Date.now();
}
/**
* 发送SUBACK消息
* @param {number} packetId - 包ID
* @param {number} returnCode - 返回码
*/
sendSuback(packetId, returnCode = 0) {
if (!this.isConnected || !this.socket.writable) return;
const packet = Buffer.from([
PacketType.SUBACK << 4,
3, // Remaining length
packetId >> 8, // Packet ID MSB
packetId & 0xFF, // Packet ID LSB
returnCode // Return code
]);
this.socket.write(packet);
this.lastActivity = Date.now();
}
/**
* 发送PINGRESP消息
*/
sendPingResp() {
if (!this.isConnected || !this.socket.writable) return;
const packet = Buffer.from([
PacketType.PINGRESP << 4, // Fixed header
0 // Remaining length
]);
this.socket.write(packet);
this.lastActivity = Date.now();
}
/**
* 获取上次活动时间
*/
getLastActivity() {
return this.lastActivity;
}
/**
* 获取心跳间隔
*/
getKeepAliveInterval() {
return this.keepAliveInterval;
}
/**
* 清空缓冲区
*/
clearBuffer() {
this.buffer = Buffer.alloc(0);
}
/**
* 关闭连接
*/
close() {
if (this.socket.writable) {
this.socket.end();
}
}
}
// 导出 PacketType 和 MQTTProtocol 类
module.exports = {
PacketType,
MQTTProtocol
};
+78
View File
@@ -0,0 +1,78 @@
const fs = require('fs');
const path = require('path');
const EventEmitter = require('events');
class ConfigManager extends EventEmitter {
constructor(fileName) {
super();
this.config = {}; // 移除默认的 apiKeys 配置
this.configPath = path.join(__dirname, "..", "config", fileName);
this.loadConfig();
this.watchConfig();
// 添加防抖计时器变量
this.watchDebounceTimer = null;
}
loadConfig() {
try {
const data = fs.readFileSync(this.configPath, 'utf8');
const newConfig = JSON.parse(data);
// 检测配置是否发生变化
if (JSON.stringify(this.config) !== JSON.stringify(newConfig)) {
console.log('配置已更新', this.configPath);
this.config = newConfig;
// 发出配置更新事件
this.emit('configChanged', this.config);
}
} catch (error) {
console.error('加载配置出错:', error, this.configPath);
if (error.code === 'ENOENT') {
this.createEmptyConfig();
}
}
}
createEmptyConfig() {
try {
const dir = path.dirname(this.configPath);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
const defaultConfig = {}; // 空配置对象
fs.writeFileSync(this.configPath, JSON.stringify(defaultConfig, null, 2));
console.log('已创建空配置文件', this.configPath);
} catch (error) {
console.error('创建空配置文件出错:', error, this.configPath);
}
}
watchConfig() {
fs.watch(path.dirname(this.configPath), (eventType, filename) => {
if (filename === path.basename(this.configPath) && eventType === 'change') {
// 清除之前的计时器
if (this.watchDebounceTimer) {
clearTimeout(this.watchDebounceTimer);
}
// 设置新的计时器,300ms 后执行
this.watchDebounceTimer = setTimeout(() => {
this.loadConfig();
}, 300);
}
});
}
// 获取配置的方法
getConfig() {
return this.config;
}
// 获取特定配置项的方法
get(key) {
return this.config[key];
}
}
module.exports = {
ConfigManager
};
+103
View File
@@ -0,0 +1,103 @@
require('dotenv').config();
const crypto = require('crypto');
function generatePasswordSignature(content, secretKey) {
// Create an HMAC object using SHA256 and the secretKey
const hmac = crypto.createHmac('sha256', secretKey);
// Update the HMAC object with the clientId
hmac.update(content);
// Generate the HMAC digest in binary format
const binarySignature = hmac.digest();
// Encode the binary signature to Base64
const base64Signature = binarySignature.toString('base64');
return base64Signature;
}
function validateMqttCredentials(clientId, username, password) {
// 验证密码签名
const signatureKey = process.env.MQTT_SIGNATURE_KEY;
if (signatureKey) {
const expectedSignature = generatePasswordSignature(clientId + '|' + username, signatureKey);
if (password !== expectedSignature) {
throw new Error('密码签名验证失败');
}
} else {
console.warn('缺少MQTT_SIGNATURE_KEY环境变量,跳过密码签名验证');
}
// 验证clientId
if (!clientId || typeof clientId !== 'string') {
throw new Error('clientId必须是非空字符串');
}
// 验证clientId格式(必须包含@@@分隔符)
const clientIdParts = clientId.split('@@@');
// 新版本 MQTT 参数
if (clientIdParts.length !== 3) {
throw new Error('clientId格式错误,必须包含@@@分隔符');
}
// 验证username
if (!username || typeof username !== 'string') {
throw new Error('username必须是非空字符串');
}
// 尝试解码username(应该是base64编码的JSON
let userData;
try {
const decodedUsername = Buffer.from(username, 'base64').toString();
userData = JSON.parse(decodedUsername);
} catch (error) {
throw new Error('username不是有效的base64编码JSON');
}
// 解析clientId中的信息
const [groupId, macAddress, uuid] = clientIdParts;
// 如果验证成功,返回解析后的有用信息
return {
groupId,
macAddress: macAddress.replace(/_/g, ':'),
uuid,
userData
};
}
function generateMqttConfig(groupId, macAddress, uuid, userData) {
const endpoint = process.env.MQTT_ENDPOINT;
const signatureKey = process.env.MQTT_SIGNATURE_KEY;
if (!signatureKey) {
console.warn('No signature key, skip generating MQTT config');
return;
}
const deviceIdNoColon = macAddress.replace(/:/g, '_');
const clientId = `${groupId}@@@${deviceIdNoColon}@@@${uuid}`;
const username = Buffer.from(JSON.stringify(userData)).toString('base64');
const password = generatePasswordSignature(clientId + '|' + username, signatureKey);
return {
endpoint,
port: 8883,
client_id: clientId,
username,
password,
publish_topic: 'device-server',
subscribe_topic: 'null' // 旧版本固件不返回此字段会出错
}
}
module.exports = {
generateMqttConfig,
validateMqttCredentials
}
if (require.main === module) {
const config = generateMqttConfig('GID_test', '11:22:33:44:55:66', '36c98363-3656-43cb-a00f-8bced2391a90', { ip: '222.222.222.222' });
console.log('config', config);
const credentials = validateMqttCredentials(config.client_id, config.username, config.password);
console.log('credentials', credentials);
}
+239
View File
@@ -0,0 +1,239 @@
# MQTT网关转发功能测试指南
本文档说明如何测试MQTT网关的转发功能是否配置成功。
## 快速测试
### 1. 基础连接测试
运行快速测试脚本:
```bash
node quick-test.js
```
这个脚本会:
- ✅ 检查配置文件
- ✅ 测试MQTT服务器连接(端口1883)
- ✅ 测试WebSocket后端服务连接
- ✅ 显示测试结果和建议
### 2. 完整功能测试
运行完整测试脚本:
```bash
node test-mqtt.js
```
这个脚本会:
- 🔗 连接到MQTT服务器
- 🔐 使用配置的MAC地址进行认证
- 📝 订阅回复主题
- 📤 发送hello消息测试WebSocket转发
- 📥 等待并显示服务器响应
- 🧪 发送其他测试消息
## 手动测试步骤
### 1. 启动MQTT网关
```bash
node app.js
```
应该看到类似输出:
```
MQTT 服务器启动在端口 1883
UDP 服务器启动在端口 1883
连接数: 0, 活跃数: 0
```
### 2. 检查配置文件
确认 `config/mqtt.json` 配置正确:
```json
{
"production": {
"chat_servers": [
"ws://192.168.68.66:8000/xiaozhi/v1/"
]
},
"development": {
"chat_servers": ["ws://192.168.68.66:8000/xiaozhi/v1/"],
"mac_addresss": ["d8:43:ae:3e:4b:5a"]
},
"debug": false
}
```
### 3. 使用MQTT客户端工具测试
#### 使用mosquitto客户端:
**连接测试:**
```bash
mosquitto_pub -h localhost -p 1883 -t "test/topic" -m "hello"
```
**订阅测试:**
```bash
mosquitto_sub -h localhost -p 1883 -t "devices/p2p/d8_43_ae_3e_4b_5a"
```
#### 使用MQTT.fx或其他GUI工具:
1. **连接设置:**
- 服务器:localhost
- 端口:1883
- 客户端ID`GID_test@@@d8_43_ae_3e_4b_5a`
- 用户名:test_user
- 密码:test_password
2. **发布测试消息:**
- 主题:`test/topic`
- 消息:
```json
{
"type": "hello",
"version": 3,
"transport": "mqtt",
"audio_params": {
"sample_rate": 16000,
"channels": 1,
"format": "opus"
},
"features": ["tts", "asr"]
}
```
3. **订阅回复主题:**
- 主题:`devices/p2p/d8_43_ae_3e_4b_5a`
## 测试结果判断
### ✅ 成功指标
1. **MQTT连接成功:**
```
✓ 成功连接到MQTT服务器 localhost:1883
✓ MQTT连接成功
```
2. **WebSocket转发成功:**
```
✓ WebSocket服务器连接成功
✓ 收到PUBLISH消息
```
3. **服务器日志正常:**
```
客户端连接: { clientId: 'GID_test@@@d8_43_ae_3e_4b_5a', ... }
收到发布消息: { topic: 'test/topic', payload: '...', qos: 0 }
```
### ❌ 常见问题
1. **MQTT服务器连接失败:**
```
✗ 连接错误: connect ECONNREFUSED 127.0.0.1:1883
```
**解决方案:** 确保运行 `node app.js` 启动MQTT网关
2. **WebSocket连接失败:**
```
✗ WebSocket服务器连接失败: connect ECONNREFUSED
```
**解决方案:** 检查WebSocket后端服务是否运行,网络是否可达
3. **认证失败:**
```
无效的 clientId: xxx
```
**解决方案:** 检查客户端ID格式是否为 `GID_test@@@mac_address`
4. **MAC地址不匹配:**
```
未找到 xx:xx:xx:xx:xx:xx 的聊天服务器
```
**解决方案:** 确认MAC地址在配置文件的 `mac_addresss` 列表中
## 调试技巧
### 1. 开启调试模式
修改 `config/mqtt.json`
```json
{
"debug": true
}
```
或设置环境变量:
```bash
DEBUG=mqtt-server node app.js
```
### 2. 查看详细日志
调试模式下会显示详细的连接和消息处理信息:
```
mqtt-server 客户端连接: { clientId: '...', username: '...', ... }
mqtt-server 收到发布消息: { topic: '...', payload: '...', qos: 0 }
mqtt-server 发送消息到 devices/p2p/...: {...}
```
### 3. 网络连通性测试
**测试WebSocket服务器:**
```bash
curl -I http://192.168.68.66:8000/xiaozhi/v1/
```
**测试MQTT端口:**
```bash
telnet localhost 1883
```
## 性能测试
### 并发连接测试
可以修改测试脚本创建多个并发连接:
```javascript
// 创建多个测试客户端
const clients = [];
for (let i = 0; i < 10; i++) {
const client = new MQTTTestClient();
clients.push(client);
// 连接和测试...
}
```
### 消息吞吐量测试
发送大量消息测试转发性能:
```javascript
// 发送100条消息
for (let i = 0; i < 100; i++) {
client.publish('test/topic', `测试消息 ${i}`);
}
```
## 故障排除清单
- [ ] MQTT网关服务是否运行 (`node app.js`)
- [ ] 端口1883是否被占用
- [ ] 配置文件格式是否正确
- [ ] WebSocket后端服务是否运行
- [ ] 网络连接是否正常
- [ ] MAC地址是否在配置的白名单中
- [ ] 客户端ID格式是否正确
- [ ] 防火墙是否阻止连接
## 总结
通过以上测试方法,你可以全面验证MQTT网关的转发功能是否配置成功。建议先运行快速测试,如果有问题再进行详细的手动测试和调试。