/** * 微信小程序 MQTT 适配库 v2.0 * 修复 WebSocket 连接问题,增加调试日志 */ !function(root) { 'use strict'; if (typeof exports === 'object' && typeof module !== 'undefined') { module.exports = factory(); } else if (typeof define === 'function' && define.amd) { define(factory); } else { root.mqtt = factory(); } function factory() { // 微信小程序 WebSocket 适配器 class WxWebSocket { constructor(url, protocols) { console.log('[WxWebSocket] 初始化:', url, protocols); this.url = url; this.protocols = protocols; this.readyState = 0; // CONNECTING this._socket = null; this.onopen = null; this.onclose = null; this.onerror = null; this.onmessage = null; this._connect(); } _connect() { const that = this; console.log('[WxWebSocket] 开始连接:', this.url); try { // 使用 SocketTask 进行连接 const socketTask = wx.connectSocket({ url: this.url, protocols: this.protocols ? [this.protocols] : [], success: () => { console.log('[WxWebSocket] 连接请求已发送'); }, fail: (err) => { console.error('[WxWebSocket] 连接失败:', err); that.readyState = 3; that.onerror && that.onerror(new Error('WebSocket connection failed: ' + JSON.stringify(err))); } }); this._socket = socketTask; // 监听特定连接的事件 socketTask.onOpen((res) => { console.log('[WxWebSocket] 连接已打开'); that.readyState = 1; // OPEN that.onopen && that.onopen({ type: 'open' }); }); socketTask.onMessage((res) => { console.log('[WxWebSocket] 收到消息, 数据类型:', typeof res.data); if (that.onmessage) { let data = res.data; if (typeof data === 'string') { data = that._stringToArrayBuffer(data); } that.onmessage({ data }); } }); socketTask.onError((err) => { console.error('[WxWebSocket] 连接错误:', err); that.readyState = 3; that.onerror && that.onerror(new Error('WebSocket error: ' + JSON.stringify(err))); }); socketTask.onClose((res) => { console.log('[WxWebSocket] 连接关闭:', res); that.readyState = 3; that.onclose && that.onclose(res); }); } catch (err) { console.error('[WxWebSocket] 创建连接异常:', err); that.readyState = 3; that.onerror && that.onerror(err); } } _stringToArrayBuffer(str) { const buffer = new ArrayBuffer(str.length); const view = new Uint8Array(buffer); for (let i = 0; i < str.length; i++) { view[i] = str.charCodeAt(i); } return buffer; } send(data, callback) { if (this.readyState !== 1) { console.error('[WxWebSocket] 发送失败: 连接未打开, readyState:', this.readyState); callback && callback(new Error('WebSocket is not open')); return; } try { let sendData = data; if (data instanceof ArrayBuffer) { sendData = data; } else if (typeof data === 'string') { sendData = data; } console.log('[WxWebSocket] 发送数据, 大小:', sendData.byteLength || sendData.length); this._socket.send({ data: sendData, success: () => { console.log('[WxWebSocket] 发送成功'); callback && callback(); }, fail: (err) => { console.error('[WxWebSocket] 发送失败:', err); callback && callback(new Error('Send failed: ' + JSON.stringify(err))); } }); } catch (err) { console.error('[WxWebSocket] 发送异常:', err); callback && callback(err); } } close() { console.log('[WxWebSocket] 关闭连接, readyState:', this.readyState); if (this.readyState === 3) return; this.readyState = 2; // CLOSING this._socket.close({ success: () => { console.log('[WxWebSocket] 关闭成功'); } }); } } // MQTT 客户端 class MqttClient { constructor(url, options) { console.log('[MqttClient] 初始化客户端, URL:', url, 'Options:', options); this.url = url; this.options = options || {}; this.ws = null; this.connected = false; this.messageId = 1; this.subscriptions = new Map(); this.onConnect = null; this.onMessage = null; this.onError = null; this.onClose = null; this.onReconnect = null; // 连接参数 this.clientId = options.clientId || 'mqtt_' + Math.random().toString(16).substr(2, 8); this.username = options.username; this.password = options.password; this.keepalive = options.keepalive || 60; this.clean = options.clean !== false; this.reconnectPeriod = options.reconnectPeriod || 5000; this.connectTimeout = options.connectTimeout || 30000; this._connectTimer = null; this._reconnectTimer = null; this._keepaliveTimer = null; // 心跳探活:PINGREQ 发出后等 PINGRESP 的时限(默认 keepalive/3,至少 3s) this._pingTimer = null; this._pingTimeout = options.pingTimeout || Math.max(3000, Math.floor((options.keepalive || 60) * 1000 / 3)); // 终止标记:end() 置位,此后一切自动重连都必须停手 // (这是"手动断开后 5s 又连回来"的根因防线) this._ended = false; // 连接代号:每次 connect() 自增,用于作废上一代 socket 的迟到回调 this._connToken = 0; // 建连 Promise 的结算句柄(原实现成功路径永不 resolve) this._pendingConnect = null; // QoS>0 未确认报文:messageId -> { topic, message, qos, retain, messageId, attempts, timer, callback } this._inflight = new Map(); this._pubackTimeout = options.pubackTimeout || 5000; this._maxInflightRetry = options.maxInflightRetry || 3; this._maxInflight = options.maxInflight || 50; } connect() { // 新建连必须清掉终止标记,否则 end() 之后永远连不上 this._ended = false; const token = ++this._connToken; return new Promise((resolve, reject) => { console.log('[MqttClient] 开始连接, 超时时间:', this.connectTimeout); this._pendingConnect = { resolve, reject }; const timeoutId = setTimeout(() => { if (this.connected) return; console.error('[MqttClient] 连接超时'); const ws = this.ws; if (ws) { try { ws.close(); } catch (e) { /* 忽略 */ } } const err = new Error('Connection timeout'); this.onError && this.onError(err); if (this._pendingConnect) { const pending = this._pendingConnect; this._pendingConnect = null; pending.reject(err); } }, this.connectTimeout); try { this.ws = new WxWebSocket(this.url, 'mqtt'); this.ws.onopen = () => { if (token !== this._connToken) return; // 上一代 socket 的迟到回调,丢弃 console.log('[MqttClient] WebSocket 已打开,发送 MQTT CONNECT'); clearTimeout(timeoutId); this._sendConnect(); }; this.ws.onmessage = (event) => { if (token !== this._connToken) return; this._handleMessage(event.data); }; this.ws.onerror = (err) => { if (token !== this._connToken) return; console.error('[MqttClient] WebSocket 错误:', err); clearTimeout(timeoutId); this.connected = false; this.onError && this.onError(err); if (this._pendingConnect) { const pending = this._pendingConnect; this._pendingConnect = null; pending.reject(err); } }; this.ws.onclose = () => { if (token !== this._connToken) return; console.log('[MqttClient] WebSocket 关闭'); clearTimeout(timeoutId); this.connected = false; this._stopKeepalive(); this._failInflight('连接已关闭'); this.onClose && this.onClose(); // 主动 end() 后不得自连:_ended 是 end() 设下的终止标记 if (!this._ended) this._scheduleReconnect(); }; } catch (err) { console.error('[MqttClient] 创建 WebSocket 异常:', err); clearTimeout(timeoutId); if (this._pendingConnect) { const pending = this._pendingConnect; this._pendingConnect = null; pending.reject(err); } } }); } _sendConnect() { console.log('[MqttClient] 构建 CONNECT 包'); const packet = this._buildConnectPacket(); console.log('[MqttClient] CONNECT 包大小:', packet.byteLength, 'bytes'); const ws = this.ws; if (!ws) { this.onError && this.onError(new Error('WebSocket 未就绪')); return; } ws.send(packet, (err) => { if (err) { console.error('[MqttClient] 发送 CONNECT 失败:', err); this.onError && this.onError(err); } else { console.log('[MqttClient] CONNECT 包已发送,等待 CONNACK'); } }); } _buildConnectPacket() { const protocolId = 'MQTT'; const protocolVersion = 4; const clientId = this.clientId; // Flags: Clean Session + Username + Password let flags = 0x02; // Clean Session if (this.username) flags |= 0x80; if (this.password) flags |= 0x40; const keepalive = this.keepalive; // 构建 Variable Header const variableHeader = [ ...this._encodeString(protocolId), protocolVersion, flags, (keepalive >> 8) & 0xFF, keepalive & 0xFF ]; // 构建 Payload let payload = this._encodeString(clientId); if (this.username) { payload = payload.concat(this._encodeString(this.username)); } if (this.password) { payload = payload.concat(this._encodeString(this.password)); } // 构建 Fixed Header const remainingLength = variableHeader.length + payload.length; const fixedHeader = [0x10, ...this._encodeRemainingLength(remainingLength)]; // 合并为一个数组 const packet = fixedHeader.concat(variableHeader).concat(payload); // 转换为 ArrayBuffer return new Uint8Array(packet).buffer; } _encodeString(str) { const length = str.length; const bytes = []; bytes.push((length >> 8) & 0xFF); bytes.push(length & 0xFF); for (let i = 0; i < str.length; i++) { bytes.push(str.charCodeAt(i)); } return bytes; } _encodeRemainingLength(length) { const result = []; let x = length; do { let byte = x % 128; x = Math.floor(x / 128); if (x > 0) byte |= 0x80; result.push(byte); } while (x > 0); return result; } _handleMessage(data) { const view = new Uint8Array(data); const type = view[0] >> 4; console.log('[MqttClient] 收到消息, 类型:', type, '长度:', view.length); switch (type) { case 2: // CONNACK this._handleConnack(view); break; case 3: // PUBLISH this._handlePublish(view); break; case 4: // PUBACK this._handlePuback(view); break; case 9: // SUBACK console.log('[MqttClient] 收到 SUBACK'); break; case 13: // PINGRESP console.log('[MqttClient] 收到 PINGRESP'); if (this._pingTimer) { clearTimeout(this._pingTimer); this._pingTimer = null; } break; } } _handleConnack(view) { console.log('[MqttClient] 收到 CONNACK, 长度:', view.length); const flags = view[2]; const returnCode = view[3]; console.log('[MqttClient] CONNACK flags:', flags, 'returnCode:', returnCode); if (returnCode === 0) { this.connected = true; console.log('[MqttClient] 连接成功'); this._startKeepalive(); if (this._pendingConnect) { const pending = this._pendingConnect; this._pendingConnect = null; pending.resolve(); } this.onConnect && this.onConnect(); return; } const errors = [ '', 'Unacceptable protocol version', 'Identifier rejected', 'Server unavailable', 'Bad username or password', 'Not authorized' ]; const errorMsg = errors[returnCode] || 'Unknown error'; console.error('[MqttClient] 连接被拒绝:', errorMsg); const err = new Error('Connection refused: ' + errorMsg); // 凭证类拒绝(4=用户名密码错 / 5=未授权):重试一万次也不会好, // 必须置终止标记挡住自动重连,否则会以 reconnectPeriod 为间隔无限撞 broker const credentialRejected = returnCode === 4 || returnCode === 5; if (credentialRejected) this._ended = true; if (this._pendingConnect) { const pending = this._pendingConnect; this._pendingConnect = null; pending.reject(err); } this.onError && this.onError(err); if (credentialRejected) { const ws = this.ws; if (ws) { try { ws.close(); } catch (e) { /* 忽略 */ } } } } /** * 解析 MQTT Remaining Length(变长编码,最多 4 字节) * 原实现硬编码 "pos = 2",topic 稍长(Remaining Length > 127)就会解析错位 * @returns {{ value: number, bytes: number }} */ _decodeRemainingLength(view, start) { let multiplier = 1; let value = 0; let pos = start; let encodedByte = 0; do { if (pos >= view.length) break; encodedByte = view[pos++]; value += (encodedByte & 0x7F) * multiplier; multiplier *= 128; } while ((encodedByte & 0x80) !== 0); return { value, bytes: pos - start }; } /** * 字节数组转字符串(分块) * 原实现 String.fromCharCode(...payload) 在载荷较大时会超出调用栈参数上限直接抛栈溢出 */ _bytesToString(bytes) { let out = ''; const CHUNK = 4096; for (let i = 0; i < bytes.length; i += CHUNK) { out += String.fromCharCode.apply(null, bytes.subarray(i, i + CHUNK)); } return out; } _handlePublish(view) { console.log('[MqttClient] 收到 PUBLISH 消息'); const head = this._decodeRemainingLength(view, 1); let pos = 1 + head.bytes; const topicLength = (view[pos] << 8) | view[pos + 1]; pos += 2; const topic = this._bytesToString(view.subarray(pos, pos + topicLength)); pos += topicLength; const qos = (view[0] >> 1) & 0x03; if (qos > 0) pos += 2; // 跳过 messageId const payload = view.subarray(pos); console.log('[MqttClient] 主题:', topic, '载荷长度:', payload.length); const self = this; const message = { payload: payload, toString: () => self._bytesToString(payload), }; this.onMessage && this.onMessage(topic, message); // 订阅自带的回调也要分发(原实现 subscriptions 只写不读,等于死代码) const sub = this.subscriptions.get(topic); if (sub) sub(topic, message); } /** PUBACK:结算对应在途 QoS1 报文 */ _handlePuback(view) { const messageId = (view[2] << 8) | view[3]; const entry = this._inflight.get(messageId); if (!entry) { console.log('[MqttClient] 收到 PUBACK(无匹配在途消息), id:', messageId); return; } console.log('[MqttClient] 收到 PUBACK, id:', messageId); if (entry.timer) clearTimeout(entry.timer); entry.timer = null; this._inflight.delete(messageId); entry.callback && entry.callback(null); } /** 链路断开:所有在途 QoS1 报文立即回调失败,交给上层落离线缓存(至少一次投递) */ _failInflight(reason) { if (this._inflight.size === 0) return; const entries = Array.from(this._inflight.values()); this._inflight.clear(); entries.forEach((entry) => { if (entry.timer) clearTimeout(entry.timer); entry.timer = null; entry.callback && entry.callback(new Error(reason || '链路已断开')); }); } _startKeepalive() { const interval = Math.max(5, this.keepalive) * 1000; console.log('[MqttClient] 启动心跳, 间隔:', this.keepalive, '秒'); this._stopKeepalive(); this._keepaliveTimer = setInterval(() => { if (!this.connected) return; this._sendPing(); }, interval); } _stopKeepalive() { if (this._keepaliveTimer) { clearInterval(this._keepaliveTimer); this._keepaliveTimer = null; } if (this._pingTimer) { clearTimeout(this._pingTimer); this._pingTimer = null; } } /** * 发送 PINGREQ 并启动 PINGRESP 等待 * * 工业现场必须能识别"假在线":基站切换 / NAT 超时 / 设备侧断电等场景下 * TCP 不会立刻关闭,只等 socket 事件会让 connected 永远为 true。 * 原实现只发不等,PINGRESP 判死完全没做。 */ _sendPing() { const ws = this.ws; if (!ws) return; console.log('[MqttClient] 发送 PINGREQ'); ws.send(new Uint8Array([0xC0, 0x00]).buffer); if (this._pingTimer) return; this._pingTimer = setTimeout(() => { this._pingTimer = null; if (!this.connected) return; console.warn('[MqttClient] PINGRESP 超时(' + this._pingTimeout + 'ms),判定链路已死'); this._onLinkDead(); }, this._pingTimeout); } /** 心跳判死:清理本地状态,按"连接关闭"语义交给上层重连 */ _onLinkDead() { const err = new Error('心跳超时,链路判定为断开'); this._connToken++; // 作废当前 socket 的迟到回调 this.connected = false; this._stopKeepalive(); this._failInflight('心跳超时'); this.onError && this.onError(err); this.onClose && this.onClose(); const ws = this.ws; this.ws = null; if (ws) { try { ws.close(); } catch (e) { /* 忽略 */ } } if (!this._ended) this._scheduleReconnect(); } subscribe(topic, options, callback) { if (typeof options === 'function') { callback = options; options = { qos: 0 }; } console.log('[MqttClient] 订阅主题:', topic, 'QoS:', options.qos); const qos = options.qos || 0; const messageId = this.messageId++; // Variable Header: Message ID const variableHeader = [ (messageId >> 8) & 0xFF, messageId & 0xFF ]; // Payload: Topic Filter + QoS const topicData = this._encodeString(topic); const payload = topicData.concat([qos]); // Fixed Header const remainingLength = variableHeader.length + payload.length; const fixedHeader = [0x82, ...this._encodeRemainingLength(remainingLength)]; const packet = fixedHeader.concat(variableHeader).concat(payload); this.subscriptions.set(topic, callback); const ws = this.ws; if (!ws) { callback && callback(new Error('WebSocket 未就绪')); return; } ws.send(new Uint8Array(packet).buffer, (err) => { if (err) { console.error('[MqttClient] 订阅失败:', err); this.onError && this.onError(err); } else { console.log('[MqttClient] 订阅包已发送'); } }); } publish(topic, message, options, callback) { if (typeof options === 'function') { callback = options; options = { qos: 0 }; } options = options || {}; const qos = options.qos || 0; const retain = options.retain ? 1 : 0; console.log('[MqttClient] 发布消息, 主题:', topic, 'QoS:', qos); if (qos === 0) { // QoS0:无确认语义,send 成功即视为已发出 const messageId = 0; let packet; try { packet = this._buildPublishPacket(topic, message, qos, retain, 0, messageId); } catch (e) { callback && callback(e); return; } const ws = this.ws; if (!ws) { callback && callback(new Error('WebSocket 未就绪')); return; } ws.send(packet, (err) => { callback && callback(err); }); return; } // QoS1:必须等 PUBACK 才算投递成功。未确认前保留报文,超时按 dup 重发, // 重试用尽才回调失败 —— 上层据此把数据落离线缓存,实现"至少一次"投递。 // 原实现发完就回调成功,QoS1 是名不副实的假保证。 if (this._inflight.size >= this._maxInflight) { callback && callback(new Error('在途消息过多(' + this._inflight.size + '),请稍后重试')); return; } const messageId = this.messageId++; const entry = { topic: topic, message: message, qos: qos, retain: retain, messageId: messageId, attempts: 0, timer: null, callback: callback || null, }; this._inflight.set(messageId, entry); this._sendInflight(entry); } /** 按 QoS/dup/messageId 拼 PUBLISH 报文 */ _buildPublishPacket(topic, message, qos, retain, dup, messageId) { const flags = (dup << 3) | (qos << 1) | retain; const topicData = this._encodeString(topic); let variableHeader = topicData; if (qos > 0) { variableHeader = variableHeader.concat([ (messageId >> 8) & 0xFF, messageId & 0xFF ]); } let payload = []; if (typeof message === 'string') { for (let i = 0; i < message.length; i++) { payload.push(message.charCodeAt(i)); } } else if (message instanceof ArrayBuffer) { payload = Array.from(new Uint8Array(message)); } else if (message && message.buffer instanceof ArrayBuffer) { payload = Array.from(new Uint8Array(message.buffer)); } const remainingLength = variableHeader.length + payload.length; const fixedHeader = [0x30 | flags, ...this._encodeRemainingLength(remainingLength)]; const packet = fixedHeader.concat(variableHeader).concat(payload); return new Uint8Array(packet).buffer; } /** 发送(或重发)一条在途 QoS1 报文,并挂上 PUBACK 超时计时器 */ _sendInflight(entry) { entry.attempts++; const dup = entry.attempts > 1 ? 1 : 0; let packet; try { packet = this._buildPublishPacket(entry.topic, entry.message, entry.qos, entry.retain, dup, entry.messageId); } catch (e) { this._failInflightEntry(entry, e); return; } const ws = this.ws; if (!ws) { this._failInflightEntry(entry, new Error('WebSocket 未就绪')); return; } ws.send(packet, (err) => { if (err) { this._failInflightEntry(entry, err); return; } if (entry.timer) clearTimeout(entry.timer); entry.timer = setTimeout(() => { entry.timer = null; if (!this._inflight.has(entry.messageId)) return; if (entry.attempts >= this._maxInflightRetry) { this._inflight.delete(entry.messageId); entry.callback && entry.callback(new Error('PUBACK 超时,已重试 ' + entry.attempts + ' 次')); return; } console.warn('[MqttClient] PUBACK 超时,重发 id:', entry.messageId, '第', entry.attempts + 1, '次'); this._sendInflight(entry); }, this._pubackTimeout); }); } _failInflightEntry(entry, err) { if (entry.timer) clearTimeout(entry.timer); entry.timer = null; this._inflight.delete(entry.messageId); entry.callback && entry.callback(err); } unsubscribe(topic, callback) { console.log('[MqttClient] 取消订阅:', topic); const messageId = this.messageId++; const topicData = this._encodeString(topic); const variableHeader = [ (messageId >> 8) & 0xFF, messageId & 0xFF ]; const remainingLength = variableHeader.length + topicData.length; const fixedHeader = [0xA2, ...this._encodeRemainingLength(remainingLength)]; const packet = fixedHeader.concat(variableHeader).concat(topicData); this.subscriptions.delete(topic); const ws = this.ws; if (!ws) { callback && callback(null); return; } ws.send(new Uint8Array(packet).buffer, (err) => { callback && callback(err); }); } /** * 断开连接(终止性操作) * * 关键:必须在关闭 socket 之前置 _ended。ws.close() 会触发 onclose, * onclose 里会排一次自动重连 —— 晚置位就拦不住,这正是一度出现 * "点断开后 5s 又自己连回来"的根因。force 语义统一:本实现一律彻底终止。 */ end(force, callback) { if (typeof force === 'function') { callback = force; force = false; } console.log('[MqttClient] 断开连接'); this._ended = true; this._connToken++; // 作废当前 socket 的迟到回调 this._stopKeepalive(); if (this._reconnectTimer) { clearTimeout(this._reconnectTimer); this._reconnectTimer = null; } this._failInflight('连接已主动关闭'); this.connected = false; if (this._pendingConnect) { const pending = this._pendingConnect; this._pendingConnect = null; pending.reject(new Error('连接已主动关闭')); } const ws = this.ws; this.ws = null; // 立即脱钩:后续 send/close 不再打到半死 socket const finish = () => { callback && callback(); }; if (!ws) { finish(); return; } // 发送 DISCONNECT 包(尽力而为,失败不影响终止) try { ws.send(new Uint8Array([0xE0, 0x00]).buffer); } catch (e) { /* 忽略 */ } setTimeout(() => { try { ws.close(); } catch (e) { /* 忽略 */ } finish(); }, 100); } _scheduleReconnect() { // 主动 end() 后不再自连:这是"断开"能真正生效的唯一致命防线 if (this._ended) return; if (this.reconnectPeriod > 0 && !this._reconnectTimer) { console.log('[MqttClient] 计划重连, 延迟:', this.reconnectPeriod); this._reconnectTimer = setTimeout(() => { this._reconnectTimer = null; if (this._ended) return; this.onReconnect && this.onReconnect(); this.connect().catch(err => { console.error('[MqttClient] 重连失败:', err && err.message); this.onError && this.onError(err); }); }, this.reconnectPeriod); } } /** 是否正处于"等待自动重连"状态(供上层查询,替代原不存在的同名属性) */ get reconnecting() { return !!this._reconnectTimer; } on(event, callback) { switch (event) { case 'connect': this.onConnect = callback; break; case 'message': this.onMessage = callback; break; case 'error': this.onError = callback; break; case 'close': this.onClose = callback; break; case 'reconnect': this.onReconnect = callback; break; } } } // 导出连接函数 function connect(url, options) { console.log('[MQTT] 创建客户端连接:', url); const client = new MqttClient(url, options); // 首次建连结果由 onConnect / onError 事件交付;这里吞掉 Promise rejection, // 避免"事件 + Promise"双通道产生未处理的 Promise 拒绝 client.connect().catch(() => {}); return client; } return { connect, MqttClient }; } }(typeof self !== 'undefined' ? self : this);