├── .gitignore ├── LICENSE ├── README.md ├── app.js ├── config └── mqtt.json.example ├── ecosystem.config.js ├── mqtt-protocol.js ├── package.json └── utils ├── config-manager.js └── mqtt_config_v2.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .env 3 | build/ 4 | dist/ 5 | package-lock.json 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Xiaoxia 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MQTT+UDP 到 WebSocket 桥接服务 2 | 3 | ## 项目概述 4 | 5 | 这是一个用于物联网设备通信的桥接服务,实现了MQTT和UDP协议到WebSocket的转换。该服务允许设备通过MQTT协议进行控制消息传输,同时通过UDP协议高效传输音频数据,并将这些数据桥接到WebSocket服务。 6 | 7 | ## 功能特点 8 | 9 | - **多协议支持**: 同时支持MQTT、UDP和WebSocket协议 10 | - **音频数据传输**: 专为音频数据流优化的传输机制 11 | - **加密通信**: 使用AES-128-CTR加密UDP数据传输 12 | - **会话管理**: 完整的设备会话生命周期管理 13 | - **自动重连**: 连接断开时自动重连机制 14 | - **心跳检测**: 定期检查连接活跃状态 15 | - **开发/生产环境配置**: 支持不同环境的配置切换 16 | 17 | ## 技术架构 18 | 19 | - **MQTT服务器**: 处理设备控制消息 20 | - **UDP服务器**: 处理高效的音频数据传输 21 | - **WebSocket客户端**: 连接到聊天服务器 22 | - **桥接层**: 在不同协议间转换和路由消息 23 | 24 | ## 项目结构 25 | 26 | ``` 27 | ├── app.js # 主应用入口 28 | ├── mqtt-protocol.js # MQTT协议实现 29 | ├── ecosystem.config.js # PM2配置文件 30 | ├── package.json # 项目依赖 31 | ├── .env # 环境变量配置 32 | ├── utils/ 33 | │ ├── config-manager.js # 配置管理工具 34 | │ ├── mqtt_config_v2.js # MQTT配置验证工具 35 | │ └── weixinAlert.js # 微信告警工具 36 | └── config/ # 配置文件目录 37 | ``` 38 | 39 | ## 依赖项 40 | 41 | - **debug**: 调试日志输出 42 | - **dotenv**: 环境变量管理 43 | - **ws**: WebSocket客户端 44 | - **events**: Node.js 事件模块 45 | 46 | ## 安装要求 47 | 48 | - Node.js 14.x 或更高版本 49 | - npm 或 yarn 包管理器 50 | - PM2 (用于生产环境部署) 51 | 52 | ## 安装步骤 53 | 54 | 1. 克隆仓库 55 | ```bash 56 | git clone <仓库地址> 57 | cd mqtt-websocket-bridge 58 | ``` 59 | 60 | 2. 安装依赖 61 | ```bash 62 | npm install 63 | ``` 64 | 65 | 3. 创建配置文件 66 | ```bash 67 | mkdir -p config 68 | cp config/mqtt.json.example config/mqtt.json 69 | ``` 70 | 71 | 4. 编辑配置文件 `config/mqtt.json`,设置适当的参数 72 | 73 | ## 配置说明 74 | 75 | 配置文件 `config/mqtt.json` 需要包含以下内容: 76 | 77 | ```json 78 | { 79 | "debug": false, 80 | "development": { 81 | "mac_addresss": ["aa:bb:cc:dd:ee:ff"], 82 | "chat_servers": ["wss://dev-chat-server.example.com/ws"] 83 | }, 84 | "production": { 85 | "chat_servers": ["wss://chat-server.example.com/ws"] 86 | } 87 | } 88 | ``` 89 | 90 | ## 环境变量 91 | 92 | 创建 `.env` 文件并设置以下环境变量: 93 | 94 | ``` 95 | MQTT_PORT=1883 # MQTT服务器端口 96 | UDP_PORT=8884 # UDP服务器端口 97 | PUBLIC_IP=your-ip # 服务器公网IP 98 | ``` 99 | 100 | ## 运行服务 101 | 102 | ### 开发环境 103 | 104 | ```bash 105 | # 直接运行 106 | node app.js 107 | 108 | # 调试模式运行 109 | DEBUG=mqtt-server node app.js 110 | ``` 111 | 112 | ### 生产环境 (使用PM2) 113 | 114 | ```bash 115 | # 安装PM2 116 | npm install -g pm2 117 | 118 | # 启动服务 119 | pm2 start ecosystem.config.js 120 | 121 | # 查看日志 122 | pm2 logs xz-mqtt 123 | 124 | # 监控服务 125 | pm2 monit 126 | ``` 127 | 128 | 服务将在以下端口启动: 129 | - MQTT 服务器: 端口 1883 (可通过环境变量修改) 130 | - UDP 服务器: 端口 8884 (可通过环境变量修改) 131 | 132 | ## 协议说明 133 | 134 | ### 设备连接流程 135 | 136 | 1. 设备通过MQTT协议连接到服务器 137 | 2. 设备发送 `hello` 消息,包含音频参数和特性 138 | 3. 服务器创建WebSocket连接到聊天服务器 139 | 4. 服务器返回UDP连接参数给设备 140 | 5. 设备通过UDP发送音频数据 141 | 6. 服务器将音频数据转发到WebSocket 142 | 7. WebSocket返回的控制消息通过MQTT发送给设备 143 | 144 | ### 消息格式 145 | 146 | #### Hello 消息 (设备 -> 服务器) 147 | ```json 148 | { 149 | "type": "hello", 150 | "version": 3, 151 | "audio_params": { ... }, 152 | "features": { ... } 153 | } 154 | ``` 155 | 156 | #### Hello 响应 (服务器 -> 设备) 157 | ```json 158 | { 159 | "type": "hello", 160 | "version": 3, 161 | "session_id": "uuid", 162 | "transport": "udp", 163 | "udp": { 164 | "server": "server-ip", 165 | "port": 8884, 166 | "encryption": "aes-128-ctr", 167 | "key": "hex-encoded-key", 168 | "nonce": "hex-encoded-nonce" 169 | }, 170 | "audio_params": { ... } 171 | } 172 | ``` 173 | 174 | ## 安全说明 175 | 176 | - UDP通信使用AES-128-CTR加密 177 | - 每个会话使用唯一的加密密钥 178 | - 使用序列号防止重放攻击 179 | - 设备通过MAC地址进行身份验证 180 | - 支持设备分组和UUID验证 181 | 182 | ## 性能优化 183 | 184 | - 使用预分配的缓冲区减少内存分配 185 | - UDP协议用于高效传输音频数据 186 | - 定期清理不活跃的连接 187 | - 连接数和活跃连接数监控 188 | - 支持多聊天服务器负载均衡 189 | 190 | ## 故障排除 191 | 192 | - 检查设备MAC地址格式是否正确 193 | - 确保UDP端口在防火墙中开放 194 | - 启用调试模式查看详细日志 195 | - 检查配置文件中的聊天服务器地址是否正确 196 | - 验证设备认证信息是否正确 197 | 198 | ## 开发指南 199 | 200 | ### 添加新功能 201 | 202 | 1. 修改 `mqtt-protocol.js` 以支持新的MQTT功能 203 | 2. 在 `MQTTConnection` 类中添加新的消息处理方法 204 | 3. 更新配置管理器以支持新的配置选项 205 | 4. 在 `WebSocketBridge` 类中添加新的WebSocket处理逻辑 206 | 207 | ### 调试技巧 208 | 209 | ```bash 210 | # 启用所有调试输出 211 | DEBUG=* node app.js 212 | 213 | # 只启用MQTT服务器调试 214 | DEBUG=mqtt-server node app.js 215 | ``` 216 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | // Description: MQTT+UDP 到 WebSocket 的桥接 2 | // Author: terrence@tenclass.com 3 | // Date: 2025-03-12 4 | 5 | require('dotenv').config(); 6 | const net = require('net'); 7 | const debugModule = require('debug'); 8 | const debug = debugModule('mqtt-server'); 9 | const crypto = require('crypto'); 10 | const dgram = require('dgram'); 11 | const Emitter = require('events'); 12 | const WebSocket = require('ws'); 13 | const { MQTTProtocol } = require('./mqtt-protocol'); 14 | const { ConfigManager } = require('./utils/config-manager'); 15 | const { validateMqttCredentials } = require('./utils/mqtt_config_v2'); 16 | 17 | 18 | function setDebugEnabled(enabled) { 19 | if (enabled) { 20 | debugModule.enable('mqtt-server'); 21 | } else { 22 | debugModule.disable(); 23 | } 24 | } 25 | 26 | const configManager = new ConfigManager('mqtt.json'); 27 | configManager.on('configChanged', (config) => { 28 | setDebugEnabled(config.debug); 29 | }); 30 | 31 | setDebugEnabled(configManager.get('debug')); 32 | 33 | class WebSocketBridge extends Emitter { 34 | constructor(connection, protocolVersion, macAddress, uuid, userData) { 35 | super(); 36 | this.connection = connection; 37 | this.macAddress = macAddress; 38 | this.uuid = uuid; 39 | this.userData = userData; 40 | this.wsClient = null; 41 | this.protocolVersion = protocolVersion; 42 | this.deviceSaidGoodbye = false; 43 | this.initializeChatServer(); 44 | } 45 | 46 | initializeChatServer() { 47 | const devMacAddresss = configManager.get('development')?.mac_addresss || []; 48 | let chatServers; 49 | if (devMacAddresss.includes(this.macAddress)) { 50 | chatServers = configManager.get('development')?.chat_servers; 51 | } else { 52 | chatServers = configManager.get('production')?.chat_servers; 53 | } 54 | if (!chatServers) { 55 | throw new Error(`未找到 ${this.macAddress} 的聊天服务器`); 56 | } 57 | this.chatServer = chatServers[Math.floor(Math.random() * chatServers.length)]; 58 | } 59 | 60 | async connect(audio_params, features) { 61 | return new Promise((resolve, reject) => { 62 | const headers = { 63 | 'device-id': this.macAddress, 64 | 'protocol-version': '2', 65 | 'authorization': `Bearer test-token` 66 | }; 67 | if (this.uuid) { 68 | headers['client-id'] = this.uuid; 69 | } 70 | if (this.userData && this.userData.ip) { 71 | headers['x-forwarded-for'] = this.userData.ip; 72 | } 73 | this.wsClient = new WebSocket(this.chatServer, { headers }); 74 | 75 | this.wsClient.on('open', () => { 76 | this.sendJson({ 77 | type: 'hello', 78 | version: 2, 79 | transport: 'websocket', 80 | audio_params, 81 | features 82 | }); 83 | }); 84 | 85 | this.wsClient.on('message', (data, isBinary) => { 86 | if (isBinary) { 87 | const timestamp = data.readUInt32BE(8); 88 | const opusLength = data.readUInt32BE(12); 89 | const opus = data.subarray(16, 16 + opusLength); 90 | // 二进制数据通过UDP发送 91 | this.connection.sendUdpMessage(opus, timestamp); 92 | } else { 93 | // JSON数据通过MQTT发送 94 | const message = JSON.parse(data.toString()); 95 | if (message.type === 'hello') { 96 | resolve(message); 97 | } else if (message.type === 'mcp' && 98 | this.connection.mcpCachedTools && 99 | ['initialize','notifications/initialized', 'tools/list'].includes(message.payload.method)) { 100 | this.connection.onMcpMessageFromBridge(message); 101 | } else { 102 | this.connection.sendMqttMessage(JSON.stringify(message)); 103 | } 104 | } 105 | }); 106 | 107 | this.wsClient.on('error', (error) => { 108 | console.error(`WebSocket error for device ${this.macAddress}:`, error); 109 | this.emit('close'); 110 | reject(error); 111 | }); 112 | 113 | this.wsClient.on('close', () => { 114 | this.emit('close'); 115 | }); 116 | }); 117 | } 118 | 119 | sendJson(message) { 120 | if (this.wsClient && this.wsClient.readyState === WebSocket.OPEN) { 121 | this.wsClient.send(JSON.stringify(message)); 122 | } 123 | } 124 | 125 | sendAudio(opus, timestamp) { 126 | if (this.wsClient && this.wsClient.readyState === WebSocket.OPEN) { 127 | const buffer = Buffer.alloc(16 + opus.length); 128 | buffer.writeUInt32BE(timestamp, 8); 129 | buffer.writeUInt32BE(opus.length, 12); 130 | buffer.set(opus, 16); 131 | this.wsClient.send(buffer, { binary: true }); 132 | } 133 | } 134 | 135 | isAlive() { 136 | return this.wsClient && this.wsClient.readyState === WebSocket.OPEN; 137 | } 138 | 139 | close() { 140 | if (this.wsClient) { 141 | this.wsClient.close(); 142 | this.wsClient = null; 143 | } 144 | } 145 | } 146 | 147 | const MacAddressRegex = /^[0-9a-f]{2}(:[0-9a-f]{2}){5}$/; 148 | 149 | /** 150 | * MQTT连接类 151 | * 负责应用层逻辑处理 152 | */ 153 | class MQTTConnection { 154 | constructor(socket, connectionId, server) { 155 | this.server = server; 156 | this.connectionId = connectionId; 157 | this.clientId = null; 158 | this.username = null; 159 | this.password = null; 160 | this.bridge = null; 161 | this.udp = { 162 | remoteAddress: null, 163 | cookie: null, 164 | localSequence: 0, 165 | remoteSequence: 0 166 | }; 167 | this.headerBuffer = Buffer.alloc(16); 168 | this.mcpPendingRequests = {}; 169 | 170 | // 创建协议处理器,并传入socket 171 | this.protocol = new MQTTProtocol(socket, configManager); 172 | 173 | this.setupProtocolHandlers(); 174 | } 175 | 176 | setupProtocolHandlers() { 177 | // 设置协议事件处理 178 | this.protocol.on('connect', (connectData) => { 179 | this.handleConnect(connectData); 180 | }); 181 | 182 | this.protocol.on('publish', (publishData) => { 183 | this.handlePublish(publishData); 184 | }); 185 | 186 | this.protocol.on('subscribe', (subscribeData) => { 187 | this.handleSubscribe(subscribeData); 188 | }); 189 | 190 | this.protocol.on('disconnect', () => { 191 | this.handleDisconnect(); 192 | }); 193 | 194 | this.protocol.on('close', () => { 195 | debug(`${this.clientId} 客户端断开连接`); 196 | this.server.removeConnection(this); 197 | }); 198 | 199 | this.protocol.on('error', (err) => { 200 | debug(`${this.clientId} 连接错误:`, err); 201 | this.close(); 202 | }); 203 | 204 | this.protocol.on('protocolError', (err) => { 205 | debug(`${this.clientId} 协议错误:`, err); 206 | this.close(); 207 | }); 208 | } 209 | 210 | handleConnect(connectData) { 211 | this.clientId = connectData.clientId; 212 | this.username = connectData.username; 213 | this.password = connectData.password; 214 | 215 | debug('客户端连接:', { 216 | clientId: this.clientId, 217 | username: this.username, 218 | password: this.password, 219 | protocol: connectData.protocol, 220 | protocolLevel: connectData.protocolLevel, 221 | keepAlive: connectData.keepAlive 222 | }); 223 | 224 | const parts = this.clientId.split('@@@'); 225 | if (parts.length === 3) { // GID_test@@@mac_address@@@uuid 226 | const validated = validateMqttCredentials(this.clientId, this.username, this.password); 227 | this.groupId = validated.groupId; 228 | this.macAddress = validated.macAddress; 229 | this.uuid = validated.uuid; 230 | this.userData = validated.userData; 231 | } else if (parts.length === 2) { // GID_test@@@mac_address 232 | this.groupId = parts[0]; 233 | this.macAddress = parts[1].replace(/_/g, ':'); 234 | if (!MacAddressRegex.test(this.macAddress)) { 235 | debug('无效的 macAddress:', this.macAddress); 236 | this.close(); 237 | return; 238 | } 239 | } else { 240 | debug('无效的 clientId:', this.clientId); 241 | this.close(); 242 | return; 243 | } 244 | this.replyTo = `devices/p2p/${parts[1]}`; 245 | 246 | this.server.addConnection(this); 247 | this.initializeDeviceTools(); 248 | } 249 | 250 | handleSubscribe(subscribeData) { 251 | debug('客户端订阅主题:', { 252 | clientId: this.clientId, 253 | topic: subscribeData.topic, 254 | packetId: subscribeData.packetId 255 | }); 256 | 257 | // 发送 SUBACK 258 | this.protocol.sendSuback(subscribeData.packetId, 0); 259 | } 260 | 261 | handleDisconnect() { 262 | debug('收到断开连接请求:', { clientId: this.clientId }); 263 | // 清理连接 264 | this.server.removeConnection(this); 265 | } 266 | 267 | close() { 268 | this.closing = true; 269 | // 清理所有未完成的 MCP 请求 270 | for (const request of Object.values(this.mcpPendingRequests)) { 271 | request.reject(new Error('Connection closed')); 272 | } 273 | this.mcpPendingRequests = {}; 274 | 275 | if (this.bridge) { 276 | this.bridge.close(); 277 | this.bridge = null; 278 | } else { 279 | this.protocol.close(); 280 | } 281 | } 282 | 283 | checkKeepAlive() { 284 | const now = Date.now(); 285 | const keepAliveInterval = this.protocol.getKeepAliveInterval(); 286 | 287 | // 如果keepAliveInterval为0,表示不需要心跳检查 288 | if (keepAliveInterval === 0 || !this.protocol.isConnected) return; 289 | 290 | const lastActivity = this.protocol.getLastActivity(); 291 | const timeSinceLastActivity = now - lastActivity; 292 | 293 | // 如果超过心跳间隔,关闭连接 294 | if (timeSinceLastActivity > keepAliveInterval) { 295 | debug('心跳超时,关闭连接:', this.clientId); 296 | this.close(); 297 | } 298 | } 299 | 300 | handlePublish(publishData) { 301 | debug('收到发布消息:', { 302 | clientId: this.clientId, 303 | topic: publishData.topic, 304 | payload: publishData.payload, 305 | qos: publishData.qos 306 | }); 307 | 308 | if (publishData.qos !== 0) { 309 | debug('不支持的 QoS 级别:', publishData.qos, '关闭连接'); 310 | this.close(); 311 | return; 312 | } 313 | 314 | const json = JSON.parse(publishData.payload); 315 | if (json.type === 'hello') { 316 | if (json.version !== 3) { 317 | debug('不支持的协议版本:', json.version, '关闭连接'); 318 | this.close(); 319 | return; 320 | } 321 | this.parseHelloMessage(json).catch(error => { 322 | debug('处理 hello 消息失败:', error); 323 | this.close(); 324 | }); 325 | } else { 326 | this.parseOtherMessage(json).catch(error => { 327 | debug('处理其他消息失败:', error); 328 | this.close(); 329 | }); 330 | } 331 | } 332 | 333 | sendMqttMessage(payload) { 334 | debug(`发送消息到 ${this.replyTo}: ${payload}`); 335 | this.protocol.sendPublish(this.replyTo, payload, 0, false, false); 336 | } 337 | 338 | sendUdpMessage(payload, timestamp) { 339 | if (!this.udp.remoteAddress) { 340 | debug(`设备 ${this.clientId} 未连接,无法发送 UDP 消息`); 341 | return; 342 | } 343 | this.udp.localSequence++; 344 | const header = this.generateUdpHeader(payload.length, timestamp, this.udp.localSequence); 345 | const cipher = crypto.createCipheriv(this.udp.encryption, this.udp.key, header); 346 | const message = Buffer.concat([header, cipher.update(payload), cipher.final()]); 347 | this.server.sendUdpMessage(message, this.udp.remoteAddress); 348 | } 349 | 350 | generateUdpHeader(length, timestamp, sequence) { 351 | // 重用预分配的缓冲区 352 | this.headerBuffer.writeUInt8(1, 0); 353 | this.headerBuffer.writeUInt16BE(length, 2); 354 | this.headerBuffer.writeUInt32BE(this.connectionId, 4); 355 | this.headerBuffer.writeUInt32BE(timestamp, 8); 356 | this.headerBuffer.writeUInt32BE(sequence, 12); 357 | return Buffer.from(this.headerBuffer); // 返回副本以避免并发问题 358 | } 359 | 360 | async parseHelloMessage(json) { 361 | this.udp = { 362 | ...this.udp, 363 | key: crypto.randomBytes(16), 364 | nonce: this.generateUdpHeader(0, 0, 0), 365 | encryption: 'aes-128-ctr', 366 | remoteSequence: 0, 367 | localSequence: 0, 368 | startTime: Date.now() 369 | } 370 | 371 | if (this.bridge) { 372 | debug(`${this.clientId} 收到重复 hello 消息,关闭之前的 bridge`); 373 | this.bridge.close(); 374 | await new Promise(resolve => setTimeout(resolve, 100)); 375 | } 376 | this.bridge = new WebSocketBridge(this, json.version, this.macAddress, this.uuid, this.userData); 377 | this.bridge.on('close', () => { 378 | const seconds = (Date.now() - this.udp.startTime) / 1000; 379 | console.log(`通话结束: ${this.clientId} Session: ${this.udp.session_id} Duration: ${seconds}s`); 380 | this.sendMqttMessage(JSON.stringify({ type: 'goodbye', session_id: this.udp.session_id })); 381 | this.bridge = null; 382 | if (this.closing) { 383 | this.protocol.close(); 384 | } 385 | }); 386 | 387 | try { 388 | console.log(`通话开始: ${this.clientId} Protocol: ${json.version} ${this.bridge.chatServer}`); 389 | const helloReply = await this.bridge.connect(json.audio_params, json.features); 390 | this.udp.session_id = helloReply.session_id; 391 | this.sendMqttMessage(JSON.stringify({ 392 | type: 'hello', 393 | version: json.version, 394 | session_id: this.udp.session_id, 395 | transport: 'udp', 396 | udp: { 397 | server: this.server.publicIp, 398 | port: this.server.udpPort, 399 | encryption: this.udp.encryption, 400 | key: this.udp.key.toString('hex'), 401 | nonce: this.udp.nonce.toString('hex'), 402 | }, 403 | audio_params: helloReply.audio_params 404 | })); 405 | } catch (error) { 406 | this.sendMqttMessage(JSON.stringify({ type: 'error', message: '处理 hello 消息失败' })); 407 | console.error(`${this.clientId} 处理 hello 消息失败: ${error}`); 408 | } 409 | } 410 | 411 | async parseOtherMessage(json) { 412 | if (json.type === 'mcp') { 413 | const { id, error, result } = json.payload; 414 | const request = this.mcpPendingRequests[id]; 415 | if (request) { 416 | delete this.mcpPendingRequests[id]; 417 | if (error) { 418 | request.reject(new Error(error.message)); 419 | } else { 420 | request.resolve(result); 421 | } 422 | return; 423 | } 424 | } 425 | 426 | if (!this.bridge) { 427 | if (json.type !== 'goodbye') { 428 | this.sendMqttMessage(JSON.stringify({ type: 'goodbye', session_id: json.session_id })); 429 | } 430 | return; 431 | } 432 | 433 | if (json.type === 'goodbye') { 434 | this.bridge.close(); 435 | this.bridge = null; 436 | return; 437 | } 438 | 439 | this.bridge.sendJson(json); 440 | } 441 | 442 | onUdpMessage(rinfo, message, payloadLength, timestamp, sequence) { 443 | if (!this.bridge) { 444 | return; 445 | } 446 | if (this.udp.remoteAddress !== rinfo) { 447 | this.udp.remoteAddress = rinfo; 448 | } 449 | if (sequence < this.udp.remoteSequence) { 450 | return; 451 | } 452 | 453 | // 处理加密数据 454 | const header = message.slice(0, 16); 455 | const encryptedPayload = message.slice(16, 16 + payloadLength); 456 | const cipher = crypto.createDecipheriv(this.udp.encryption, this.udp.key, header); 457 | const payload = Buffer.concat([cipher.update(encryptedPayload), cipher.final()]); 458 | 459 | this.bridge.sendAudio(payload, timestamp); 460 | this.udp.remoteSequence = sequence; 461 | } 462 | 463 | isAlive() { 464 | return this.bridge && this.bridge.isAlive(); 465 | } 466 | 467 | // Cache device tools to MQTTConnection 468 | async initializeDeviceTools() { 469 | this.mcpRequestId = 10000; 470 | this.mcpPendingRequests = {}; 471 | this.mcpCachedTools = []; 472 | 473 | try { 474 | const mcpClient = configManager.get('mcp_client') || {}; 475 | const capabilities = mcpClient.capabilities || {}; 476 | const clientInfo = mcpClient.client_info || { 477 | name: 'xiaozhi-mqtt-client', 478 | version: '1.0.0' 479 | }; 480 | this.mcpCachedInitialize = await this.sendMcpRequest('initialize', { 481 | protocolVersion: '2024-11-05', 482 | capabilities, 483 | clientInfo 484 | }); 485 | this.sendMqttMessage(JSON.stringify({ 486 | type: 'mcp', 487 | payload: { jsonrpc: '2.0', method: 'notifications/initialized' } 488 | })); 489 | 490 | // list tools 491 | let cursor = undefined; 492 | const maxToolsCount = configManager.get('mcp_client.max_tools_count') || 32; 493 | do { 494 | const { tools, nextCursor } = await this.sendMcpRequest('tools/list', { cursor }); 495 | if (tools.length === 0 || (this.mcpCachedTools.length + tools.length) > maxToolsCount) { 496 | break; 497 | } 498 | this.mcpCachedTools = this.mcpCachedTools.concat(tools); 499 | cursor = nextCursor; 500 | } while (cursor !== undefined); 501 | debug('初始化设备工具成功:', this.mcpCachedTools); 502 | } catch (error) { 503 | debug("Error initializing device tools", error); 504 | } 505 | } 506 | 507 | sendMcpRequest(method, params) { 508 | const id = this.mcpRequestId++; 509 | return new Promise((resolve, reject) => { 510 | this.mcpPendingRequests[id] = { resolve, reject }; 511 | this.sendMqttMessage(JSON.stringify({ 512 | type: 'mcp', 513 | payload: { jsonrpc: '2.0', method, id, params } 514 | })); 515 | }); 516 | } 517 | 518 | onMcpMessageFromBridge(message) { 519 | const { method, id, params } = message.payload; 520 | if (method === 'initialize') { 521 | this.bridge.sendJson({ 522 | type: 'mcp', 523 | payload: { jsonrpc: '2.0', id, result: this.mcpCachedInitialize } 524 | }); 525 | } else if (method === 'tools/list') { 526 | this.bridge.sendJson({ 527 | type: 'mcp', 528 | payload: { jsonrpc: '2.0', id, result: { tools: this.mcpCachedTools } } 529 | }); 530 | } else if (method === 'notifications/initialized') { 531 | // do nothing 532 | } 533 | } 534 | } 535 | 536 | class MQTTServer { 537 | constructor() { 538 | this.mqttPort = parseInt(process.env.MQTT_PORT) || 1883; 539 | this.udpPort = parseInt(process.env.UDP_PORT) || this.mqttPort; 540 | this.publicIp = process.env.PUBLIC_IP || 'mqtt.xiaozhi.me'; 541 | this.connections = new Map(); // clientId -> MQTTConnection 542 | this.keepAliveTimer = null; 543 | this.keepAliveCheckInterval = 1000; // 默认每1秒检查一次 544 | 545 | this.headerBuffer = Buffer.alloc(16); 546 | } 547 | 548 | generateNewConnectionId() { 549 | // 生成一个32位不重复的整数 550 | let id; 551 | do { 552 | id = Math.floor(Math.random() * 0xFFFFFFFF); 553 | } while (this.connections.has(id)); 554 | return id; 555 | } 556 | 557 | start() { 558 | this.mqttServer = net.createServer((socket) => { 559 | const connectionId = this.generateNewConnectionId(); 560 | debug(`新客户端连接: ${connectionId}`); 561 | new MQTTConnection(socket, connectionId, this); 562 | }); 563 | 564 | this.mqttServer.listen(this.mqttPort, () => { 565 | console.warn(`MQTT 服务器正在监听端口 ${this.mqttPort}`); 566 | }); 567 | 568 | 569 | this.udpServer = dgram.createSocket('udp4'); 570 | this.udpServer.on('message', this.onUdpMessage.bind(this)); 571 | this.udpServer.on('error', err => { 572 | console.error('UDP 错误', err); 573 | setTimeout(() => { process.exit(1); }, 1000); 574 | }); 575 | this.udpServer.bind(this.udpPort, () => { 576 | console.warn(`UDP 服务器正在监听 ${this.publicIp}:${this.udpPort}`); 577 | }); 578 | 579 | // 启动全局心跳检查定时器 580 | this.setupKeepAliveTimer(); 581 | } 582 | 583 | /** 584 | * 设置全局心跳检查定时器 585 | */ 586 | setupKeepAliveTimer() { 587 | // 清除现有定时器 588 | this.clearKeepAliveTimer(); 589 | this.lastConnectionCount = 0; 590 | this.lastActiveConnectionCount = 0; 591 | 592 | // 设置新的定时器 593 | this.keepAliveTimer = setInterval(() => { 594 | // 检查所有连接的心跳状态 595 | for (const connection of this.connections.values()) { 596 | connection.checkKeepAlive(); 597 | } 598 | 599 | const activeCount = Array.from(this.connections.values()).filter(connection => connection.isAlive()).length; 600 | if (activeCount !== this.lastActiveConnectionCount || this.connections.size !== this.lastConnectionCount) { 601 | console.log(`连接数: ${this.connections.size}, 活跃数: ${activeCount}`); 602 | this.lastActiveConnectionCount = activeCount; 603 | this.lastConnectionCount = this.connections.size; 604 | } 605 | }, this.keepAliveCheckInterval); 606 | } 607 | 608 | /** 609 | * 清除心跳检查定时器 610 | */ 611 | clearKeepAliveTimer() { 612 | if (this.keepAliveTimer) { 613 | clearInterval(this.keepAliveTimer); 614 | this.keepAliveTimer = null; 615 | } 616 | } 617 | 618 | addConnection(connection) { 619 | // 检查是否已存在相同 clientId 的连接 620 | for (const [key, value] of this.connections.entries()) { 621 | if (value.clientId === connection.clientId) { 622 | debug(`${connection.clientId} 已存在连接,关闭旧连接`); 623 | value.close(); 624 | } 625 | } 626 | this.connections.set(connection.connectionId, connection); 627 | } 628 | 629 | removeConnection(connection) { 630 | debug(`关闭连接: ${connection.connectionId}`); 631 | if (this.connections.has(connection.connectionId)) { 632 | this.connections.delete(connection.connectionId); 633 | } 634 | } 635 | 636 | sendUdpMessage(message, remoteAddress) { 637 | this.udpServer.send(message, remoteAddress.port, remoteAddress.address); 638 | } 639 | 640 | onUdpMessage(message, rinfo) { 641 | // message format: [type: 1u, flag: 1u, payloadLength: 2u, cookie: 4u, timestamp: 4u, sequence: 4u, payload: n] 642 | if (message.length < 16) { 643 | console.warn('收到不完整的 UDP Header', rinfo); 644 | return; 645 | } 646 | 647 | try { 648 | const type = message.readUInt8(0); 649 | if (type !== 1) return; 650 | 651 | const payloadLength = message.readUInt16BE(2); 652 | if (message.length < 16 + payloadLength) return; 653 | 654 | const connectionId = message.readUInt32BE(4); 655 | const connection = this.connections.get(connectionId); 656 | if (!connection) return; 657 | 658 | const timestamp = message.readUInt32BE(8); 659 | const sequence = message.readUInt32BE(12); 660 | 661 | connection.onUdpMessage(rinfo, message, payloadLength, timestamp, sequence); 662 | } catch (error) { 663 | console.error('UDP 消息处理错误:', error); 664 | } 665 | } 666 | 667 | /** 668 | * 停止服务器 669 | */ 670 | async stop() { 671 | if (this.stopping) { 672 | return; 673 | } 674 | this.stopping = true; 675 | // 清除心跳检查定时器 676 | this.clearKeepAliveTimer(); 677 | 678 | if (this.connections.size > 0) { 679 | console.warn(`等待 ${this.connections.size} 个连接关闭`); 680 | for (const connection of this.connections.values()) { 681 | connection.close(); 682 | } 683 | await new Promise(resolve => setTimeout(resolve, 300)); 684 | debug('等待连接关闭完成'); 685 | this.connections.clear(); 686 | } 687 | 688 | if (this.udpServer) { 689 | this.udpServer.close(); 690 | this.udpServer = null; 691 | console.warn('UDP 服务器已停止'); 692 | } 693 | 694 | // 关闭MQTT服务器 695 | if (this.mqttServer) { 696 | this.mqttServer.close(); 697 | this.mqttServer = null; 698 | console.warn('MQTT 服务器已停止'); 699 | } 700 | 701 | process.exit(0); 702 | } 703 | } 704 | 705 | // 创建并启动服务器 706 | const server = new MQTTServer(); 707 | server.start(); 708 | process.on('SIGINT', () => { 709 | console.warn('收到 SIGINT 信号,开始关闭'); 710 | server.stop(); 711 | }); 712 | -------------------------------------------------------------------------------- /config/mqtt.json.example: -------------------------------------------------------------------------------- 1 | { 2 | "production": { 3 | "chat_servers": [ 4 | "ws://example:8080" 5 | ] 6 | }, 7 | "development": { 8 | "chat_servers": [ 9 | "ws://example:8180" 10 | ], 11 | "mac_addresss": [ 12 | ] 13 | }, 14 | "debug": false, 15 | "max_mqtt_payload_size": 8192, 16 | "mcp_client": { 17 | "capabilities": { 18 | }, 19 | "client_info": { 20 | "name": "xiaozhi-mqtt-client", 21 | "version": "1.0.0" 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /ecosystem.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "apps": [ 3 | { 4 | "name": "xz-mqtt", 5 | "script": "app.js", 6 | "time": true 7 | } 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /mqtt-protocol.js: -------------------------------------------------------------------------------- 1 | const debug = require('debug')('mqtt-server'); 2 | const EventEmitter = require('events'); 3 | 4 | // MQTT 固定头部的类型 5 | const PacketType = { 6 | CONNECT: 1, 7 | CONNACK: 2, 8 | PUBLISH: 3, 9 | SUBSCRIBE: 8, 10 | SUBACK: 9, 11 | PINGREQ: 12, 12 | PINGRESP: 13, 13 | DISCONNECT: 14 // 添加 DISCONNECT 14 | }; 15 | 16 | /** 17 | * MQTT协议处理类 18 | * 负责MQTT协议的解析和封装,以及心跳维持 19 | */ 20 | class MQTTProtocol extends EventEmitter { 21 | constructor(socket, configManager) { 22 | super(); 23 | this.socket = socket; 24 | this.buffer = Buffer.alloc(0); 25 | this.isConnected = false; 26 | this.keepAliveInterval = 0; 27 | this.lastActivity = Date.now(); 28 | this.configManager = configManager; 29 | 30 | this.setupSocketHandlers(); 31 | } 32 | 33 | /** 34 | * 设置Socket事件处理 35 | */ 36 | setupSocketHandlers() { 37 | this.socket.on('data', (data) => { 38 | this.lastActivity = Date.now(); 39 | this.buffer = Buffer.concat([this.buffer, data]); 40 | this.processBuffer(); 41 | }); 42 | 43 | this.socket.on('close', () => { 44 | this.emit('close'); 45 | }); 46 | 47 | this.socket.on('error', (err) => { 48 | this.emit('error', err); 49 | }); 50 | } 51 | 52 | /** 53 | * 处理缓冲区中的所有完整消息 54 | */ 55 | processBuffer() { 56 | const maxPayloadSize = this.configManager.get('max_mqtt_payload_size') || 8192; 57 | // 持续处理缓冲区中的数据,直到没有完整的消息可以处理 58 | while (this.buffer.length > 0) { 59 | // 至少需要2个字节才能开始解析(1字节固定头部 + 至少1字节的剩余长度) 60 | if (this.buffer.length < 2) return; 61 | 62 | try { 63 | // 获取消息类型 64 | const firstByte = this.buffer[0]; 65 | const type = (firstByte >> 4); 66 | 67 | // 解析剩余长度 68 | const { value: remainingLength, bytesRead } = this.decodeRemainingLength(this.buffer); 69 | 70 | // 计算整个消息的长度 71 | const messageLength = 1 + bytesRead + remainingLength; 72 | if (messageLength > maxPayloadSize) { 73 | debug('消息长度超过最大限制:', messageLength); 74 | this.emit('protocolError', new Error(`消息长度超过最大限制: ${messageLength}`)); 75 | return; 76 | } 77 | 78 | // 检查缓冲区中是否有完整的消息 79 | if (this.buffer.length < messageLength) { 80 | // 消息不完整,等待更多数据 81 | return; 82 | } 83 | 84 | // 提取完整的消息 85 | const message = this.buffer.subarray(0, messageLength); 86 | if (!this.isConnected && type !== PacketType.CONNECT) { 87 | debug('未连接时收到非CONNECT消息,关闭连接'); 88 | this.socket.end(); 89 | return; 90 | } 91 | 92 | // 根据消息类型处理 93 | switch (type) { 94 | case PacketType.CONNECT: 95 | this.parseConnect(message); 96 | break; 97 | case PacketType.PUBLISH: 98 | this.parsePublish(message); 99 | break; 100 | case PacketType.SUBSCRIBE: 101 | this.parseSubscribe(message); 102 | break; 103 | case PacketType.PINGREQ: 104 | this.parsePingReq(message); 105 | break; 106 | case PacketType.DISCONNECT: 107 | this.parseDisconnect(message); 108 | break; 109 | default: 110 | debug('未处理的包类型:', type, message); 111 | this.emit('protocolError', new Error(`未处理的包类型: ${type}`)); 112 | } 113 | 114 | // 从缓冲区中移除已处理的消息 115 | this.buffer = this.buffer.subarray(messageLength); 116 | 117 | } catch (err) { 118 | // 如果解析出错,可能是数据不完整,等待更多数据 119 | if (err.message === 'Malformed Remaining Length') { 120 | return; 121 | } 122 | // 其他错误可能是协议错误,清空缓冲区并发出错误事件 123 | this.buffer = Buffer.alloc(0); 124 | this.emit('protocolError', err); 125 | return; 126 | } 127 | } 128 | } 129 | 130 | /** 131 | * 解析MQTT报文中的Remaining Length字段 132 | * @param {Buffer} buffer - 消息缓冲区 133 | * @returns {{value: number, bytesRead: number}} 包含解析的值和读取的字节数 134 | */ 135 | decodeRemainingLength(buffer) { 136 | let multiplier = 1; 137 | let value = 0; 138 | let bytesRead = 0; 139 | let digit; 140 | 141 | do { 142 | if (bytesRead >= 4 || bytesRead >= buffer.length - 1) { 143 | throw new Error('Malformed Remaining Length'); 144 | } 145 | 146 | digit = buffer[bytesRead + 1]; 147 | bytesRead++; 148 | 149 | value += (digit & 127) * multiplier; 150 | multiplier *= 128; 151 | 152 | } while ((digit & 128) !== 0); 153 | 154 | return { value, bytesRead }; 155 | } 156 | 157 | /** 158 | * 编码MQTT报文中的Remaining Length字段 159 | * @param {number} length - 要编码的长度值 160 | * @returns {{bytes: Buffer, bytesLength: number}} 包含编码后的字节和字节长度 161 | */ 162 | encodeRemainingLength(length) { 163 | let digit; 164 | const bytes = Buffer.alloc(4); // 最多4个字节 165 | let bytesLength = 0; 166 | 167 | do { 168 | digit = length % 128; 169 | length = Math.floor(length / 128); 170 | // 如果还有更多字节,设置最高位 171 | if (length > 0) { 172 | digit |= 0x80; 173 | } 174 | bytes[bytesLength++] = digit; 175 | } while (length > 0 && bytesLength < 4); 176 | 177 | return { bytes, bytesLength }; 178 | } 179 | 180 | /** 181 | * 解析CONNECT消息 182 | * @param {Buffer} message - 完整的CONNECT消息 183 | */ 184 | parseConnect(message) { 185 | // 解析剩余长度 186 | const { value: remainingLength, bytesRead } = this.decodeRemainingLength(message); 187 | 188 | // 固定头部之后的位置 (MQTT固定头部第一个字节 + Remaining Length字段的字节) 189 | const headerLength = 1 + bytesRead; 190 | 191 | // 从可变头部开始位置读取协议名长度 192 | const protocolLength = message.readUInt16BE(headerLength); 193 | const protocol = message.toString('utf8', headerLength + 2, headerLength + 2 + protocolLength); 194 | 195 | // 更新位置指针,跳过协议名 196 | let pos = headerLength + 2 + protocolLength; 197 | 198 | // 协议级别,4为MQTT 3.1.1 199 | const protocolLevel = message[pos]; 200 | 201 | // 检查协议版本 202 | if (protocolLevel !== 4) { // 4 表示 MQTT 3.1.1 203 | debug('不支持的协议版本:', protocolLevel); 204 | // 发送 CONNACK,使用不支持的协议版本的返回码 (0x01) 205 | this.sendConnack(1, false); 206 | // 关闭连接 207 | this.socket.end(); 208 | return; 209 | } 210 | 211 | pos += 1; 212 | 213 | // 连接标志 214 | const connectFlags = message[pos]; 215 | const hasUsername = (connectFlags & 0x80) !== 0; 216 | const hasPassword = (connectFlags & 0x40) !== 0; 217 | const cleanSession = (connectFlags & 0x02) !== 0; 218 | pos += 1; 219 | 220 | // 保持连接时间 221 | const keepAlive = message.readUInt16BE(pos); 222 | pos += 2; 223 | 224 | // 解析 clientId 225 | const clientIdLength = message.readUInt16BE(pos); 226 | pos += 2; 227 | const clientId = message.toString('utf8', pos, pos + clientIdLength); 228 | pos += clientIdLength; 229 | 230 | // 解析 username(如果存在) 231 | let username = ''; 232 | if (hasUsername) { 233 | const usernameLength = message.readUInt16BE(pos); 234 | pos += 2; 235 | username = message.toString('utf8', pos, pos + usernameLength); 236 | pos += usernameLength; 237 | } 238 | 239 | // 解析 password(如果存在) 240 | let password = ''; 241 | if (hasPassword) { 242 | const passwordLength = message.readUInt16BE(pos); 243 | pos += 2; 244 | password = message.toString('utf8', pos, pos + passwordLength); 245 | pos += passwordLength; 246 | } 247 | 248 | // 设置心跳间隔(客户端指定的keepAlive值的1.5倍,单位为秒) 249 | this.keepAliveInterval = keepAlive * 1000 * 1.5; 250 | 251 | // 发送 CONNACK 252 | this.sendConnack(0, false); 253 | 254 | // 标记为已连接 255 | this.isConnected = true; 256 | 257 | // 发出连接事件 258 | this.emit('connect', { 259 | clientId, 260 | protocol, 261 | protocolLevel, 262 | keepAlive, 263 | username, 264 | password, 265 | cleanSession 266 | }); 267 | } 268 | 269 | /** 270 | * 解析PUBLISH消息 271 | * @param {Buffer} message - 完整的PUBLISH消息 272 | */ 273 | parsePublish(message) { 274 | // 从第一个字节中提取QoS级别(bits 1-2) 275 | const firstByte = message[0]; 276 | const qos = (firstByte & 0x06) >> 1; // 0x06 是二进制 00000110,用于掩码提取QoS位 277 | const dup = (firstByte & 0x08) !== 0; // 0x08 是二进制 00001000,用于掩码提取DUP标志 278 | const retain = (firstByte & 0x01) !== 0; // 0x01 是二进制 00000001,用于掩码提取RETAIN标志 279 | 280 | // 使用通用方法解析剩余长度 281 | const { value: remainingLength, bytesRead } = this.decodeRemainingLength(message); 282 | 283 | // 固定头部之后的位置 (MQTT固定头部第一个字节 + Remaining Length字段的字节) 284 | const headerLength = 1 + bytesRead; 285 | 286 | // 解析主题 287 | const topicLength = message.readUInt16BE(headerLength); 288 | const topic = message.toString('utf8', headerLength + 2, headerLength + 2 + topicLength); 289 | 290 | // 对于QoS > 0,包含消息ID 291 | let packetId = null; 292 | let payloadStart = headerLength + 2 + topicLength; 293 | 294 | if (qos > 0) { 295 | packetId = message.readUInt16BE(payloadStart); 296 | payloadStart += 2; 297 | } 298 | 299 | // 解析有效载荷 300 | const payload = message.slice(payloadStart).toString('utf8'); 301 | 302 | // 发出发布事件 303 | this.emit('publish', { 304 | topic, 305 | payload, 306 | qos, 307 | dup, 308 | retain, 309 | packetId 310 | }); 311 | } 312 | 313 | /** 314 | * 解析SUBSCRIBE消息 315 | * @param {Buffer} message - 完整的SUBSCRIBE消息 316 | */ 317 | parseSubscribe(message) { 318 | const packetId = message.readUInt16BE(2); 319 | const topicLength = message.readUInt16BE(4); 320 | const topic = message.toString('utf8', 6, 6 + topicLength); 321 | const qos = message[6 + topicLength]; // QoS值 322 | 323 | // 发出订阅事件 324 | this.emit('subscribe', { 325 | packetId, 326 | topic, 327 | qos 328 | }); 329 | } 330 | 331 | /** 332 | * 解析PINGREQ消息 333 | * @param {Buffer} message - 完整的PINGREQ消息 334 | */ 335 | parsePingReq(message) { 336 | debug('收到心跳请求'); 337 | 338 | // 发送 PINGRESP 339 | this.sendPingResp(); 340 | 341 | debug('已发送心跳响应'); 342 | } 343 | 344 | /** 345 | * 解析DISCONNECT消息 346 | * @param {Buffer} message - 完整的DISCONNECT消息 347 | */ 348 | parseDisconnect(message) { 349 | // 标记为未连接 350 | this.isConnected = false; 351 | 352 | // 发出断开连接事件 353 | this.emit('disconnect'); 354 | 355 | // 关闭 socket 356 | this.socket.end(); 357 | } 358 | 359 | /** 360 | * 发送CONNACK消息 361 | * @param {number} returnCode - 返回码 362 | * @param {boolean} sessionPresent - 会话存在标志 363 | */ 364 | sendConnack(returnCode = 0, sessionPresent = false) { 365 | if (!this.socket.writable) return; 366 | 367 | const packet = Buffer.from([ 368 | PacketType.CONNACK << 4, 369 | 2, // Remaining length 370 | sessionPresent ? 1 : 0, // Connect acknowledge flags 371 | returnCode // Return code 372 | ]); 373 | 374 | this.socket.write(packet); 375 | } 376 | 377 | /** 378 | * 发送PUBLISH消息 379 | * @param {string} topic - 主题 380 | * @param {string} payload - 有效载荷 381 | * @param {number} qos - QoS级别 382 | * @param {boolean} dup - 重复标志 383 | * @param {boolean} retain - 保留标志 384 | * @param {number} packetId - 包ID(仅QoS > 0时需要) 385 | */ 386 | sendPublish(topic, payload, qos = 0, dup = false, retain = false, packetId = null) { 387 | if (!this.isConnected || !this.socket.writable) return; 388 | 389 | const topicLength = Buffer.byteLength(topic); 390 | const payloadLength = Buffer.byteLength(payload); 391 | 392 | // 计算剩余长度 393 | let remainingLength = 2 + topicLength + payloadLength; 394 | 395 | // 如果QoS > 0,需要包含包ID 396 | if (qos > 0 && packetId) { 397 | remainingLength += 2; 398 | } 399 | 400 | // 编码可变长度 401 | const { bytes: remainingLengthBytes, bytesLength: remainingLengthSize } = this.encodeRemainingLength(remainingLength); 402 | 403 | // 分配缓冲区:固定头部(1字节) + 可变长度字段 + 剩余长度值 404 | const packet = Buffer.alloc(1 + remainingLengthSize + remainingLength); 405 | 406 | // 写入固定头部 407 | let firstByte = PacketType.PUBLISH << 4; 408 | if (dup) firstByte |= 0x08; 409 | if (qos > 0) firstByte |= (qos << 1); 410 | if (retain) firstByte |= 0x01; 411 | 412 | packet[0] = firstByte; 413 | 414 | // 写入可变长度字段 415 | remainingLengthBytes.copy(packet, 1, 0, remainingLengthSize); 416 | 417 | // 写入主题长度和主题 418 | const variableHeaderStart = 1 + remainingLengthSize; 419 | packet.writeUInt16BE(topicLength, variableHeaderStart); 420 | packet.write(topic, variableHeaderStart + 2); 421 | 422 | // 如果QoS > 0,写入包ID 423 | let payloadStart = variableHeaderStart + 2 + topicLength; 424 | if (qos > 0 && packetId) { 425 | packet.writeUInt16BE(packetId, payloadStart); 426 | payloadStart += 2; 427 | } 428 | 429 | // 写入有效载荷 430 | packet.write(payload, payloadStart); 431 | 432 | this.socket.write(packet); 433 | this.lastActivity = Date.now(); 434 | } 435 | 436 | /** 437 | * 发送SUBACK消息 438 | * @param {number} packetId - 包ID 439 | * @param {number} returnCode - 返回码 440 | */ 441 | sendSuback(packetId, returnCode = 0) { 442 | if (!this.isConnected || !this.socket.writable) return; 443 | 444 | const packet = Buffer.from([ 445 | PacketType.SUBACK << 4, 446 | 3, // Remaining length 447 | packetId >> 8, // Packet ID MSB 448 | packetId & 0xFF, // Packet ID LSB 449 | returnCode // Return code 450 | ]); 451 | 452 | this.socket.write(packet); 453 | this.lastActivity = Date.now(); 454 | } 455 | 456 | /** 457 | * 发送PINGRESP消息 458 | */ 459 | sendPingResp() { 460 | if (!this.isConnected || !this.socket.writable) return; 461 | 462 | const packet = Buffer.from([ 463 | PacketType.PINGRESP << 4, // Fixed header 464 | 0 // Remaining length 465 | ]); 466 | 467 | this.socket.write(packet); 468 | this.lastActivity = Date.now(); 469 | } 470 | 471 | /** 472 | * 获取上次活动时间 473 | */ 474 | getLastActivity() { 475 | return this.lastActivity; 476 | } 477 | 478 | /** 479 | * 获取心跳间隔 480 | */ 481 | getKeepAliveInterval() { 482 | return this.keepAliveInterval; 483 | } 484 | 485 | /** 486 | * 清空缓冲区 487 | */ 488 | clearBuffer() { 489 | this.buffer = Buffer.alloc(0); 490 | } 491 | 492 | /** 493 | * 关闭连接 494 | */ 495 | close() { 496 | if (this.socket) { 497 | this.socket.destroy(); 498 | } 499 | } 500 | } 501 | 502 | // 导出 PacketType 和 MQTTProtocol 类 503 | module.exports = { 504 | PacketType, 505 | MQTTProtocol 506 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "simple-mqtt-server", 3 | "version": "1.0.0", 4 | "description": "一个简单的 MQTT 服务器实现,支持 QoS 0", 5 | "main": "app.js", 6 | "scripts": { 7 | "start": "node app.js" 8 | }, 9 | "dependencies": { 10 | "debug": "^4.3.4", 11 | "dotenv": "^16.4.7", 12 | "ws": "^8.18.1" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /utils/config-manager.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const EventEmitter = require('events'); 4 | 5 | class ConfigManager extends EventEmitter { 6 | constructor(fileName) { 7 | super(); 8 | this.config = {}; // 移除默认的 apiKeys 配置 9 | this.configPath = path.join(__dirname, "..", "config", fileName); 10 | this.loadConfig(); 11 | this.watchConfig(); 12 | // 添加防抖计时器变量 13 | this.watchDebounceTimer = null; 14 | } 15 | 16 | loadConfig() { 17 | try { 18 | const data = fs.readFileSync(this.configPath, 'utf8'); 19 | const newConfig = JSON.parse(data); 20 | 21 | // 检测配置是否发生变化 22 | if (JSON.stringify(this.config) !== JSON.stringify(newConfig)) { 23 | console.log('配置已更新', this.configPath); 24 | this.config = newConfig; 25 | // 发出配置更新事件 26 | this.emit('configChanged', this.config); 27 | } 28 | } catch (error) { 29 | console.error('加载配置出错:', error, this.configPath); 30 | if (error.code === 'ENOENT') { 31 | this.createEmptyConfig(); 32 | } 33 | } 34 | } 35 | 36 | createEmptyConfig() { 37 | try { 38 | const dir = path.dirname(this.configPath); 39 | if (!fs.existsSync(dir)) { 40 | fs.mkdirSync(dir, { recursive: true }); 41 | } 42 | const defaultConfig = {}; // 空配置对象 43 | fs.writeFileSync(this.configPath, JSON.stringify(defaultConfig, null, 2)); 44 | console.log('已创建空配置文件', this.configPath); 45 | } catch (error) { 46 | console.error('创建空配置文件出错:', error, this.configPath); 47 | } 48 | } 49 | 50 | watchConfig() { 51 | fs.watch(path.dirname(this.configPath), (eventType, filename) => { 52 | if (filename === path.basename(this.configPath) && eventType === 'change') { 53 | // 清除之前的计时器 54 | if (this.watchDebounceTimer) { 55 | clearTimeout(this.watchDebounceTimer); 56 | } 57 | // 设置新的计时器,300ms 后执行 58 | this.watchDebounceTimer = setTimeout(() => { 59 | this.loadConfig(); 60 | }, 300); 61 | } 62 | }); 63 | } 64 | 65 | // 获取配置的方法 66 | getConfig() { 67 | return this.config; 68 | } 69 | 70 | // 获取特定配置项的方法 71 | get(key) { 72 | return this.config[key]; 73 | } 74 | } 75 | 76 | module.exports = { 77 | ConfigManager 78 | }; -------------------------------------------------------------------------------- /utils/mqtt_config_v2.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config(); 2 | const crypto = require('crypto'); 3 | 4 | 5 | function generatePasswordSignature(content, secretKey) { 6 | // Create an HMAC object using SHA256 and the secretKey 7 | const hmac = crypto.createHmac('sha256', secretKey); 8 | 9 | // Update the HMAC object with the clientId 10 | hmac.update(content); 11 | 12 | // Generate the HMAC digest in binary format 13 | const binarySignature = hmac.digest(); 14 | 15 | // Encode the binary signature to Base64 16 | const base64Signature = binarySignature.toString('base64'); 17 | 18 | return base64Signature; 19 | } 20 | 21 | function validateMqttCredentials(clientId, username, password) { 22 | // 验证密码签名 23 | const signatureKey = process.env.MQTT_SIGNATURE_KEY; 24 | if (signatureKey) { 25 | const expectedSignature = generatePasswordSignature(clientId + '|' + username, signatureKey); 26 | if (password !== expectedSignature) { 27 | throw new Error('密码签名验证失败'); 28 | } 29 | } else { 30 | console.warn('缺少MQTT_SIGNATURE_KEY环境变量,跳过密码签名验证'); 31 | } 32 | 33 | // 验证clientId 34 | if (!clientId || typeof clientId !== 'string') { 35 | throw new Error('clientId必须是非空字符串'); 36 | } 37 | 38 | // 验证clientId格式(必须包含@@@分隔符) 39 | const clientIdParts = clientId.split('@@@'); 40 | // 新版本 MQTT 参数 41 | if (clientIdParts.length !== 3) { 42 | throw new Error('clientId格式错误,必须包含@@@分隔符'); 43 | } 44 | 45 | // 验证username 46 | if (!username || typeof username !== 'string') { 47 | throw new Error('username必须是非空字符串'); 48 | } 49 | 50 | // 尝试解码username(应该是base64编码的JSON) 51 | let userData; 52 | try { 53 | const decodedUsername = Buffer.from(username, 'base64').toString(); 54 | userData = JSON.parse(decodedUsername); 55 | } catch (error) { 56 | throw new Error('username不是有效的base64编码JSON'); 57 | } 58 | 59 | // 解析clientId中的信息 60 | const [groupId, macAddress, uuid] = clientIdParts; 61 | 62 | // 如果验证成功,返回解析后的有用信息 63 | return { 64 | groupId, 65 | macAddress: macAddress.replace(/_/g, ':'), 66 | uuid, 67 | userData 68 | }; 69 | } 70 | 71 | function generateMqttConfig(groupId, macAddress, uuid, userData) { 72 | const endpoint = process.env.MQTT_ENDPOINT; 73 | const signatureKey = process.env.MQTT_SIGNATURE_KEY; 74 | if (!signatureKey) { 75 | console.warn('No signature key, skip generating MQTT config'); 76 | return; 77 | } 78 | const deviceIdNoColon = macAddress.replace(/:/g, '_'); 79 | const clientId = `${groupId}@@@${deviceIdNoColon}@@@${uuid}`; 80 | const username = Buffer.from(JSON.stringify(userData)).toString('base64'); 81 | const password = generatePasswordSignature(clientId + '|' + username, signatureKey); 82 | return { 83 | endpoint, 84 | port: 8883, 85 | client_id: clientId, 86 | username, 87 | password, 88 | publish_topic: 'device-server', 89 | subscribe_topic: 'null' // 旧版本固件不返回此字段会出错 90 | } 91 | } 92 | 93 | module.exports = { 94 | generateMqttConfig, 95 | validateMqttCredentials 96 | } 97 | 98 | if (require.main === module) { 99 | const config = generateMqttConfig('GID_test', '11:22:33:44:55:66', '36c98363-3656-43cb-a00f-8bced2391a90', { ip: '222.222.222.222' }); 100 | console.log('config', config); 101 | const credentials = validateMqttCredentials(config.client_id, config.username, config.password); 102 | console.log('credentials', credentials); 103 | } 104 | --------------------------------------------------------------------------------