888 lines
30 KiB
JavaScript
888 lines
30 KiB
JavaScript
/**
|
||
* 微信小程序 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); |