MQTT 与 Protocol Buffers
MQTT 协议详解
MQTT (Message Queuing Telemetry Transport) 是一种轻量级的发布/订阅消息传输协议,专为物联网 (IoT) 和机器对机器 (M2M) 通信而设计。
MQTT 核心概念
1. Broker(代理服务器)
- MQTT Broker 是消息传输的中间件,负责接收来自客户端的消息,并根据主题过滤消息,然后分发给订阅了相应主题的客户端
- 所有消息都必须通过 Broker 进行传递
2. Client(客户端)
- 发布消息和/或订阅主题的设备或应用程序
- 可以是发布者(Publisher)、订阅者(Subscriber)或两者兼而有之
3. Topic(主题)
- 主题是消息的分类标识符,类似于文件系统中的路径
- 示例:
/sys/aaa/bbb/property/get - 支持通配符:
+:单级通配符(如:+/+/status)#:多级通配符(如:/sensor/#)
4. Publish(发布)
- 客户端向特定主题发送消息的过程
- 消息发送到 Broker,由 Broker 负责分发给订阅者
5. Subscribe(订阅)
- 客户端向 Broker 注册对特定主题感兴趣的过程
- 订阅后,客户端会收到发布到该主题的所有消息
6. QoS(服务质量等级)
MQTT 提供三种 QoS 级别,确保消息传输的可靠性:
- QoS 0(最多一次):消息发送一次,不保证送达,性能最高
- QoS 1(至少一次):消息至少送达一次,可能重复,需要确认
- QoS 2(仅一次):消息仅送达一次,性能最低但最可靠
7. Retain(保留消息)
- 当消息设置为 retain 时,Broker 会保存该消息
- 新的订阅者订阅主题时,会立即收到最后一条保留消息
8. ClientId(客户端 ID)
- 每个 MQTT 客户端必须有一个唯一的客户端标识符
- 用于 Broker 识别和跟踪客户端
MQTT 连接流程
- 建立连接:客户端向 Broker 发送 CONNECT 消息(包含 clientId、用户名、密码等)
- 连接确认:Broker 返回 CONNACK 消息确认连接
- 订阅主题:客户端发送 SUBSCRIBE 消息订阅感兴趣的主题
- 发布消息:客户端可以发布消息到任何主题
- 接收消息:订阅了相应主题的客户端会收到消息
- 断开连接:客户端发送 DISCONNECT 消息断开连接
MQTT 在物联网中的应用
在物联网场景中,MQTT 通常用于:
- 设备状态监控:设备定期发布状态信息
- 远程控制:通过发布控制命令来控制设备
- 事件通知:设备事件(如报警、故障)的实时通知
- 数据采集:传感器数据的实时采集和传输
MQTT 消息结构
┌─────────────────┬─────────────────┬─────────────────┐
│ 固定报头 │ 可变报头 │ 消息体 │
│ (2-4 bytes) │ (0-65535 bytes)│ (0-268435455) │
└─────────────────┴─────────────────┴─────────────────┘
主题设计最佳实践
-
层级结构: 使用
/分隔符创建清晰的层级device/room1/sensor/temperature device/room1/sensor/humidity -
命名规范: 使用小写字母、数字和连字符
✅ device/room-1/sensor/temp ❌ Device/Room1/Sensor/Temp -
通配符使用:
+: 单级通配符 (device/+/sensor/temp)#: 多级通配符 (device/#)
Protocol Buffers 详解
Protocol Buffers (protobuf) 是 Google 开发的一种语言无关、平台无关的序列化数据结构的方法。它比 JSON 更小、更快、更简单。
- 高效: 比 JSON 小 3-10 倍,快 20-100 倍
- 类型安全: 强类型定义,编译时检查
- 向后兼容: 支持字段演进和版本兼容
- 跨语言: 支持多种编程语言
- 代码生成: 自动生成序列化/反序列化代码
基本语法
syntax = "proto3";
package device;
// 消息定义
message DeviceStatus {
string device_id = 1; // 设备ID
int64 timestamp = 2; // 时间戳
StatusType status = 3; // 状态类型
repeated SensorData sensors = 4; // 传感器数据列表
}
// 枚举定义
enum StatusType {
UNKNOWN = 0;
ONLINE = 1;
OFFLINE = 2;
ERROR = 3;
}
// 嵌套消息
message SensorData {
string sensor_id = 1;
float value = 2;
string unit = 3;
}
数据类型映射
| Protocol Buffers | TypeScript | 描述 |
|---|---|---|
string | string | 字符串 |
int32, int64 | number | 整数 |
float, double | number | 浮点数 |
bool | boolean | 布尔值 |
bytes | Uint8Array | 字节数组 |
repeated | Array<T> | 数组 |
map<K,V> | Map<K,V> | 映射 |
MQTT + Protocol Buffers 的优点
- 物联网场景: MQTT 专为 IoT 设计,protobuf 提供高效序列化
- 带宽优化: protobuf 比 JSON 更小,适合低带宽环境
- 类型安全: protobuf 提供强类型,减少运行时错误
- 性能优势: 序列化/反序列化速度快
架构设计
┌─────────────┐ MQTT ┌─────────────┐ MQTT ┌─────────────┐
│ Device │ ──────────► │ Broker │ ──────────► │ Backend │
│ (Client) │ protobuf │ (Server) │ protobuf │ (Client) │
└─────────────┘ └─────────────┘ └─────────────┘
主题设计模式
# 设备状态
device/{device_id}/status
# 传感器数据
device/{device_id}/sensor/{sensor_type}
# 设备控制
device/{device_id}/control/{command}
# 系统消息
system/{service}/notification
Class MqttClient
1. 连接管理机制
1.1 连接创建流程
// MqttClient 连接创建的完整流程
async createConnection(config: MqttConnectionConfig, JWT: string): Promise<void>
流程说明:
- 连接检查:如果已有连接,先断开旧连接
- 状态设置:设置 loading 状态,清除错误状态
- 配置提取:从外部配置中提取重试相关参数
- 认证处理:
- 如果使用
authApi:调用认证 API,使用 JWT 解密认证信息 - 如果使用
brokerUrl:直接使用配置的连接信息
- 如果使用
- 建立连接:创建 MQTT 客户端实例并连接
- 事件绑定:设置各种事件监听器(connect、disconnect、error 等)
- 超时处理:设置连接超时定时器(默认 30 秒)
- 错误处理:如果连接失败,触发重试机制
1.2 认证解密机制
当使用 authApi 方式连接时,MqttClient 会执行以下解密流程:
// 1. 调用认证 API 获取加密的认证信息
const { code, data } = await config.authApi();
// 2. 使用 JWT 生成解密密钥(SHA256 哈希)
const digest = CryptoJS.SHA256(JWT.split(' ')[1]);
// 3. 使用 AES 解密(CFB 模式)
const decode = CryptoJS.AES.decrypt(data, digest, {
iv: CryptoJS.enc.Utf8.parse('ojsajkqjwk1w2dfg'),
mode: CryptoJS.mode.CFB,
});
// 4. 解析解密后的 JSON 数据
const decodeRes: ResponseMqttSignCrypto = JSON.parse(decode.toString(CryptoJS.enc.Utf8));
// 5. 提取连接信息(protocol, url, port, path, userName, password)
const { protocol, url: hostname, port, path, userName, password } = decodeRes;
// 6. 构建连接 URL
const url = `${protocol}://${hostname}:${port}${path}`;
解密后的数据结构:
interface ResponseMqttSignCrypto {
protocol: string; // 协议类型(ws/wss)
url: string; // 主机地址
port: string; // 端口号
path: string; // 路径
userName: string; // 用户名
password: string; // 密码
}
1.3 连接超时机制
// 默认连接超时时间:30 秒
private connectTimeoutMs = 30000;
// 超时处理逻辑
const connectTimeout = setTimeout(() => {
if (!this.state.connected) {
console.error('MQTT connection timeout');
this.setState({
loading: false,
error: new Error('Connection timeout'),
connected: false,
});
this.handleDisconnection(config, JWT); // 触发重连
}
}, this.connectTimeoutMs);
说明:
- 如果 30 秒内未建立连接,会触发超时处理
- 超时后会设置错误状态并触发重连机制
- 连接成功后会自动清除超时定时器
2. 重连机制详解
2.1 重试配置
interface MqttRetryConfig {
maxRetries: number; // 最大重试次数(默认 3)
retryDelay: number; // 重试延迟(毫秒,默认 3000)
retryBackoff: number; // 重试退避倍数(默认 2)
}
2.2 指数退避算法
// 重试延迟计算:指数退避
const delay = this.retryConfig.retryDelay * Math.pow(this.retryConfig.retryBackoff, this.retryCount - 1);
// 示例:
// 第 1 次重试:3000ms * 2^0 = 3000ms
// 第 2 次重试:3000ms * 2^1 = 6000ms
// 第 3 次重试:3000ms * 2^2 = 12000ms
退避策略优势:
- 避免频繁重试造成服务器压力
- 给网络恢复留出时间
- 降低系统资源消耗
2.3 重连触发条件
重连机制会在以下情况自动触发:
- 连接失败:首次连接或重连失败时
- 连接断开:收到
disconnect事件时 - 连接错误:收到
error事件时 - 连接关闭:收到
close事件时 - 连接超时:连接超时未建立时
重连限制:
- 达到最大重试次数后停止重试
- 组件销毁后不会触发重连(
isDestroyed标志) - 正在重试中不会重复触发
2.4 重连状态回调
// 重试开始
onRetryStart?: (retryCount: number) => void;
// 重试失败
onRetryFailed?: (retryCount: number, error: Error) => void;
// 达到最大重试次数
onMaxRetriesReached?: () => void;
3. 订阅管理机制
3.1 设备 SN 与主题映射
MqttClient 内部维护了一个设备序列号(SN)与主题的映射关系,用于快速定位消息来源:
// 映射关系结构
private state: MqttClientState = {
snList: string[]; // 设备 SN 列表
topicList: string[]; // 主题列表
snMap: Map<string, string[]>; // SN -> Topics 映射
};
映射构建算法:
private buildSnMap(snList: string[], topics: string[]): Map<string, string[]> {
const snMap = new Map<string, string[]>();
// 1. 为每个 SN 预先创建空数组
snList.forEach((sn) => snMap.set(sn, []));
// 2. 遍历 topics,将匹配的 topic 添加到对应的 SN
topics.forEach((topic) => {
const matchedSn = snList.find((sn) => topic.includes(sn));
if (matchedSn) {
const snTopics = snMap.get(matchedSn);
if (snTopics && !snTopics.includes(topic)) {
snTopics.push(topic);
}
}
});
return snMap;
}
匹配规则:
- 通过
topic.includes(sn)判断主题是否属于某个设备 - 示例:主题
/sys/95/DEVICE_001/thing/property/cmd_reply匹配 SNDEVICE_001
3.2 重复订阅检测
// 订阅时的重复检测
const newTopics = topics.filter((topic) => !this.state.topicList.includes(topic));
if (newTopics.length === 0) {
console.warn('All topics already subscribed');
return;
}
// 只订阅新主题,避免重复订阅
this.client.subscribe(newTopics, (error) => {
// 订阅回调处理
});
优势:
- 避免重复订阅造成资源浪费
- 减少网络流量
- 保持订阅列表的简洁性
3.3 订阅状态管理
// 添加订阅时更新状态
private updateSubscriptionState(snList: string[], topics: string[], isAdd: boolean): void {
if (isAdd) {
// 合并新的 SN 和主题
const newSnList = [...new Set([...this.state.snList, ...snList])];
const newTopicList = [...new Set([...this.state.topicList, ...topics])];
// 合并映射关系
const newSnMap = new Map(this.state.snMap);
const currentSnMap = this.buildSnMap(snList, topics);
currentSnMap.forEach((topics, sn) => {
const existingTopics = newSnMap.get(sn) || [];
const mergedTopics = [...new Set([...existingTopics, ...topics])];
newSnMap.set(sn, mergedTopics);
});
this.setState({
snList: newSnList,
topicList: newTopicList,
snMap: newSnMap,
});
} else {
// 移除订阅时的状态更新
// ...
}
}
4. 消息处理机制
4.1 消息回调设置
private setMessageCallback(callback: (params: MqttMessageCallbackParams) => void): void {
// 1. 检查回调函数是否相同,避免重复设置
if (this.currentCallback === callback && this.currentMessageHandler) {
return;
}
// 2. 移除旧的监听器
if (this.currentMessageHandler) {
this.client.removeListener('message', this.currentMessageHandler);
}
// 3. 创建新的消息处理器
this.currentMessageHandler = (topic: string, payload: Buffer | Uint8Array | string) => {
// 4. 检查是否是已订阅的主题
if (this.state.topicList.includes(topic)) {
// 5. 根据主题查找对应的 SN
const sn = this.state.snList.find((sn) =>
this.state.snMap.get(sn)?.includes(topic)
);
// 6. 调用回调函数,传递消息参数
callback({
topic,
payload,
sn: sn || '',
snMap: this.state.snMap,
});
}
};
// 7. 注册新的监听器
this.client.on('message', this.currentMessageHandler);
}
消息处理流程:
- 接收 MQTT 消息事件
- 检查主题是否在订阅列表中
- 根据主题查找对应的设备 SN
- 构造消息回调参数
- 调用用户注册的回调函数
4.2 消息过滤
只有已订阅的主题消息才会被处理:
if (this.state.topicList.includes(topic)) {
// 处理消息
}
这样可以避免处理无关的消息,提高性能。
5. 事件系统机制
5.1 事件监听器设置
private setupEventListeners(client: Client, config: MqttConnectionConfig, JWT: string): void {
// connect:连接成功
client.on('connect', () => {
this.setState({ connected: true, loading: false, error: null });
this.resetRetryState();
this.eventCallbacks.onConnect?.();
});
// disconnect:连接断开
client.on('disconnect', () => {
this.setState({ connected: false });
this.eventCallbacks.onDisconnect?.();
this.handleDisconnection(config, JWT);
});
// error:连接错误
client.on('error', (error) => {
this.setState({ error, connected: false });
this.eventCallbacks.onError?.(error);
this.handleDisconnection(config, JWT);
});
// reconnect:重连中
client.on('reconnect', () => {
this.setState({ connected: false, loading: true, error: null });
this.eventCallbacks.onReconnect?.();
});
// offline:离线
client.on('offline', () => {
this.setState({ connected: false });
this.eventCallbacks.onOffline?.();
});
// online:在线
client.on('online', () => {
this.setState({ connected: true });
this.eventCallbacks.onOnline?.();
});
// close:连接关闭
client.on('close', () => {
this.setState({ connected: false });
this.eventCallbacks.onDisconnect?.();
this.handleDisconnection(config, JWT);
});
}
5.2 事件回调链
MQTT 客户端事件 → MqttClient 状态更新 → 事件回调 → Hook 状态更新 → 组件重新渲染
6. 状态管理机制
6.1 状态结构
interface MqttClientState {
client: Client | null; // MQTT 客户端实例
loading: boolean; // 连接加载状态
error: Error | null; // 连接错误信息
connected: boolean; // 连接状态
snList: string[]; // 设备 SN 列表
topicList: string[]; // 主题列表
snMap: Map<string, string[]>; // 设备 SN 与主题的映射关系
}
6.2 状态更新流程
private setState(updates: Partial<MqttClientState>): void {
// 1. 合并状态更新
this.state = { ...this.state, ...updates };
// 2. 通知所有监听器状态已变化
this.notifyStateChange();
}
private notifyStateChange(): void {
// 3. 遍历所有监听器并调用
this.stateListeners.forEach((listener) => {
try {
listener({ ...this.state });
} catch (error) {
console.error('State listener error:', error);
}
});
}
6.3 状态监听器
// 添加状态监听器
addStateListener(listener: (state: MqttClientState) => void): () => void {
this.stateListeners.add(listener);
// 立即调用一次,确保组件获取最新状态
listener({ ...this.state });
// 返回移除监听器的函数
return () => {
this.stateListeners.delete(listener);
};
}
使用场景:
- Hook 可以通过状态监听器同步 MqttClient 的状态
- 组件可以实时获取连接状态、订阅状态等信息
7. 资源清理机制
7.1 断开连接流程
disconnect(): void {
if (!this.client) return;
try {
// 1. 取消所有订阅
if (this.state.topicList.length > 0) {
this.client.unsubscribe(this.state.topicList);
}
// 2. 移除消息监听器
if (this.currentMessageHandler) {
this.client.removeListener('message', this.currentMessageHandler);
}
// 3. 断开连接
this.client.end(false);
} catch (error) {
console.error('Error during disconnect:', error);
} finally {
// 4. 清理重试状态
this.clearRetryTimeout();
this.resetRetryState();
// 5. 清理资源
this.client = null;
this.currentMessageHandler = null;
this.currentCallback = null;
// 6. 清空状态
this.setState({
client: null,
connected: false,
loading: false,
error: null,
topicList: [],
snList: [],
snMap: new Map<string, string[]>(),
});
}
}
7.2 销毁机制
destroy(): void {
// 1. 标记为已销毁,防止重连
this.isDestroyed = true;
// 2. 断开连接
this.disconnect();
// 3. 清理所有监听器
this.removeAllStateListeners();
// 4. 重置状态
this.state = {
client: null,
loading: false,
error: null,
connected: false,
snList: [],
topicList: [],
snMap: new Map<string, string[]>(),
};
}
关键点:
isDestroyed标志防止组件销毁后自动重连- 清理所有监听器,防止内存泄漏
- 重置所有状态,确保干净的状态
8. 发布消息机制
8.1 发布流程
publish(topic: string, payload: Buffer | Uint8Array | string, options: MqttPublishOptions = {}): boolean {
// 1. 检查连接状态
if (!this.getConnected()) {
console.error('MQTT client not available for publishing');
return false;
}
// 2. 验证参数
if (!topic || !payload) {
console.error('Invalid topic or payload for publishing');
return false;
}
try {
// 3. 发布消息
this.client.publish(topic, payload, {
qos: options.qos || 0,
retain: options.retain || false,
});
return true;
} catch (error) {
console.error('publish error:', error);
return false;
}
}
8.2 发布选项说明
- QoS 0(默认):消息发送一次,不保证送达,性能最高
- QoS 1:消息至少送达一次,可能重复,需要确认
- QoS 2:消息仅送达一次,性能最低但最可靠
- Retain:是否保留消息,新订阅者会立即收到最后一条保留消息
9. 辅助方法
9.1 连接状态查询
// 获取连接状态
getConnected(): boolean {
return this.state.connected && this.client !== null;
}
// 检查是否正在重连
isReconnecting(): boolean {
return this.state.loading && !this.state.connected && this.client !== null;
}
9.2 订阅状态查询
// 检查是否已订阅指定主题
isSubscribed(topic: string): boolean {
return this.state.topicList.includes(topic);
}
// 检查是否已订阅指定设备 SN
isDeviceSubscribed(sn: string): boolean {
return this.state.snList.includes(sn);
}
// 获取设备 SN 对应的主题列表
getDeviceTopics(sn: string): string[] {
return this.state.snMap.get(sn) || [];
}
9.3 重试状态查询
getRetryStatus(): MqttRetryStatus {
return {
retryCount: this.retryCount,
maxRetries: this.retryConfig.maxRetries,
isRetrying: this.isRetrying,
};
}
注意事项
-
自动连接: Hook 会在组件挂载时自动创建连接,无需手动调用
createConnection()(除非需要手动管理)。 -
自动清理: 组件卸载时会自动断开连接并清理资源,无需手动调用
disconnect()。 -
重复订阅:
MqttClient内部会自动处理重复订阅,多次订阅同一主题不会产生错误。 -
ClientId 生成: 如果未提供
clientId,Hook 会自动生成一个基于用户信息的 clientId,格式为:${baseClientId}_hook。 -
JWT 优先级: 如果同时提供了 Hook 参数中的
JWT和useAuth中的JWT,优先使用参数中的JWT。 -
初始订阅时机: 初始订阅会在连接成功后自动执行。
-
状态更新: Hook 内部使用
useState管理订阅状态,确保状态更新能触发组件重新渲染。
自定义Mqtt通用hook - 完整代码
1. Protocol Buffers 定义
// device.proto
syntax = "proto3";
package device;
// 设备状态消息
message DeviceStatus {
string device_id = 1;
int64 timestamp = 2;
StatusType status = 3;
string location = 4;
repeated SensorData sensors = 5;
}
// 传感器数据
message SensorData {
string sensor_id = 1;
SensorType type = 2;
float value = 3;
string unit = 4;
int64 timestamp = 5;
}
// 设备控制消息
message DeviceControl {
string device_id = 1;
string command = 2;
map<string, string> parameters = 3;
int64 timestamp = 4;
}
// 枚举定义
enum StatusType {
UNKNOWN = 0;
ONLINE = 1;
OFFLINE = 2;
ERROR = 3;
MAINTENANCE = 4;
}
enum SensorType {
TEMPERATURE = 0;
HUMIDITY = 1;
PRESSURE = 2;
LIGHT = 3;
MOTION = 4;
}
2. 生成的 TypeScript 类型
// device_pb.ts (自动生成)
export interface DeviceStatus {
deviceId: string;
timestamp: number;
status: StatusType;
location: string;
sensors: SensorData[];
}
export interface SensorData {
sensorId: string;
type: SensorType;
value: number;
unit: string;
timestamp: number;
}
export interface DeviceControl {
deviceId: string;
command: string;
parameters: Map<string, string>;
timestamp: number;
}
export enum StatusType {
UNKNOWN = 0,
ONLINE = 1,
OFFLINE = 2,
ERROR = 3,
MAINTENANCE = 4,
}
export enum SensorType {
TEMPERATURE = 0,
HUMIDITY = 1,
PRESSURE = 2,
LIGHT = 3,
MOTION = 4,
}
Mqtt Hook
useMqttHook 是一个 自定义 Hook,用于组件中管理 MQTT 连接、订阅、取消订阅和发布消息等功能。该 Hook 底层封装了 MqttClient 类,主要用于解决直接操作 mqtt 实例时,状态更新不触发组件更新的问题。
MqttClient 核心架构
useMqttHook 内部使用了 MqttClient 类来管理 MQTT 连接。MqttClient 是一个功能完整的 MQTT 客户端管理类,提供了以下核心功能:
1. 连接管理
- 自动重连机制:支持可配置的重试次数、重试延迟和退避策略
- 连接超时处理:可设置连接超时时间(默认 30 秒)
- 状态管理:实时跟踪连接状态(connected、loading、error)
2. 订阅管理
- 设备 SN 与主题映射:自动构建和维护设备序列号(SN)与主题的映射关系
- 重复订阅检测:自动过滤已订阅的主题,避免重复订阅
- 批量订阅支持:支持一次性订阅多个设备的多个主题
3. 消息处理
- 统一消息回调:所有订阅的消息通过统一的回调函数处理
- 自动 SN 识别:根据主题自动识别对应的设备 SN
- 消息过滤:只处理已订阅主题的消息
4. 事件系统
- 完整的事件回调:支持连接、断开、错误、重连、离线、在线等事件
- 重试状态回调:提供重试开始、失败、达到最大次数等回调
- 订阅状态回调:提供订阅成功、失败回调
5. 状态监听
- 状态监听器模式:支持注册状态变化监听器
- 状态同步:确保组件能够及时响应状态变化
6. 资源管理
- 自动清理:组件卸载时自动断开连接并清理资源
- 防止内存泄漏:正确移除事件监听器和定时器
特性
- ✅ 自动连接管理
- ✅ 支持初始订阅配置
- ✅ 自动重连机制
- ✅ 完整的事件回调支持
- ✅ 自动清理资源
- ✅ TypeScript 类型支持
- ✅ 支持两种连接方式(认证 API 或直接连接)
安装
确保项目中已安装必要的依赖:
npm install mqtt
基本用法
1. 简单示例
import { useMqttHook } from '@/lib/useMqttHook';
function MyComponent() {
const { publish, subscribe, unsubscribe } = useMqttHook(
{
authApi: async () => {
// 返回认证信息的 API 调用
return { code: '0', data: 'mqtt_auth_data' };
},
},
'', // JWT 令牌(可选)
{
onConnect: () => {
console.log('MQTT 连接成功');
},
onDisconnect: () => {
console.log('MQTT 连接断开');
},
},
{
snList: ['SN_001'],
topics: ['/sys/SN_001/property/get'],
onMessage: (params) => {
console.log('收到消息:', params);
},
}
);
const handlePublish = () => {
const payload = Buffer.from('Hello MQTT');
publish('/sys/SN_001/property/set', payload);
};
return <button onClick={handlePublish}>发送消息</button>;
}
2. 使用直接连接方式
const { publish, subscribe } = useMqttHook(
{
brokerUrl: 'ws://mqtt.example.com:8083',
username: 'your_username',
password: 'your_password',
clientId: 'custom_client_id',
},
''
);
API 参考
参数
useMqttHook(mqttConfig, JWT?, eventCallbacks?, initialSubscription?)
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
mqttConfig | Partial<MqttConnectionConfig> | 是 | MQTT 连接配置 |
JWT | string | 否 | JWT 令牌,默认为空字符串 |
eventCallbacks | Partial<MqttEventCallbacks> | 否 | 事件回调函数集合 |
initialSubscription | MqttSubscriptionConfig | 否 | 初始订阅配置 |
MQTT 连接配置 (MqttConnectionConfig)
支持两种连接方式,二选一:
方式一:使用认证 API
{
clientId?: string; // 客户端 ID(可选,会自动生成)
authApi: () => PromiseResponseAPI<string>; // 获取认证信息的函数
maxRetries?: number; // 最大重试次数,默认 3
retryDelay?: number; // 重试延迟(毫秒),默认 3000
retryBackoff?: number; // 重试退避倍数,默认 2
}
方式二:使用直接连接
{
clientId?: string; // 客户端 ID(可选,会自动生成)
brokerUrl: string; // MQTT 代理服务器地址
username?: string; // 用户名
password?: string; // 密码
maxRetries?: number; // 最大重试次数
retryDelay?: number; // 重试延迟(毫秒)
retryBackoff?: number; // 重试退避倍数
}
事件回调 (MqttEventCallbacks)
interface MqttEventCallbacks {
onConnect?: () => void; // 连接成功回调
onDisconnect?: () => void; // 连接断开回调
onError?: (error: Error) => void; // 连接错误回调
onReconnect?: () => void; // 重连成功回调
onOffline?: () => void; // 离线回调
onOnline?: () => void; // 在线回调
onRetryStart?: (retryCount: number) => void; // 重试开始回调
onRetryFailed?: (retryCount: number, error: Error) => void; // 重试失败回调
onMaxRetriesReached?: () => void; // 达到最大重试次数回调
onSubscribe?: (snList: string[], topics: string[]) => void; // 订阅成功回调
onSubscribeError?: (error: Error) => void; // 订阅失败回调
}
初始订阅配置 (MqttSubscriptionConfig)
interface MqttSubscriptionConfig {
snList: string[]; // 设备 SN 列表
topics: string[]; // 主题列表
onMessage: (params: MqttMessageCallbackParams) => void; // 消息回调函数
}
消息回调参数 (MqttMessageCallbackParams)
interface MqttMessageCallbackParams {
topic: string; // 主题
payload: Buffer | Uint8Array | string; // 消息载荷
sn: string; // 设备 SN
snMap: Map<string, string[]>; // 设备 SN 与主题的映射关系
}
返回值
{
mqttInstance: MqttClient; // MQTT 客户端实例
isSubscribing: boolean; // 是否正在订阅
subscribe: (snList: string[], topics: string[], callback: (params: MqttMessageCallbackParams) => void) => Promise<void>; // 订阅方法
unsubscribe: (topics: string[]) => Promise<void>; // 取消订阅方法
publish: (topic: string, payload: Buffer | Uint8Array, options?: MqttPublishOptions) => void; // 发布消息方法
disconnect: () => void; // 断开连接方法
createConnection: () => Promise<void>; // 手动创建连接方法
addSubscription: (subscription: MqttSubscriptionConfig) => Promise<void>; // 添加订阅方法
}
3. MQTT 封装类实现
3.1 .d.ts 类型定义
import type { Client, IClientOptions } from 'mqtt';
/**
* 后端接口响应 API 字段类型
*/
export type ApiResponse<T = undefined> = Promise<{
code: '0' | string;
message: string;
data: T | undefined;
}>;
/**
* MQTT状态
*/
export interface MqttClientState {
/** MQTT客户端实例 */
client: Client | null;
/** 连接加载状态 */
loading: boolean;
/** 连接错误信息 */
error: Error | null;
/** 连接状态 */
connected: boolean;
/** 设备SN列表 */
snList: string[];
/** 主题列表 */
topicList: string[];
/** 设备SN与主题的映射关系 */
snMap: Map<string, string[]>;
}
/**
* MQTT订阅配置
*/
export interface MqttSubscriptionConfig {
/** 设备SN列表 */
snList: string[];
/** 主题列表 */
topics: string[];
/** 消息回调函数 */
onMessage: (params: MqttMessageCallbackParams) => void;
}
/**
* MQTT事件回调接口
*/
export interface MqttEventCallbacks {
/** 连接成功回调 */
onConnect?: (() => void) | null;
/** 连接断开回调 */
onDisconnect?: (() => void) | null;
/** 连接错误回调 */
onError?: ((error: Error) => void) | null;
/** 重连回调 */
onReconnect?: (() => void) | null;
/** 离线回调 */
onOffline?: (() => void) | null;
/** 在线回调 */
onOnline?: (() => void) | null;
/** 重试开始回调 */
onRetryStart?: ((retryCount: number) => void) | null;
/** 重试失败回调 */
onRetryFailed?: ((retryCount: number, error: Error) => void) | null;
/** 达到最大重试次数回调 */
onMaxRetriesReached?: (() => void) | null;
/** 订阅成功回调 */
onSubscribe?: ((snList: string[], topics: string[]) => void) | null;
/** 订阅失败回调 */
onSubscribeError?: ((error: Error) => void) | null;
}
/**
* MQTT重试配置
*/
export interface MqttRetryConfig {
/** 最大重试次数 */
maxRetries: number;
/** 重试延迟时间(毫秒) */
retryDelay: number;
/** 重试退避倍数 */
retryBackoff: number;
}
/**
* MQTT重试状态
*/
export interface MqttRetryStatus {
/** 当前重试次数 */
retryCount: number;
/** 最大重试次数 */
maxRetries: number;
/** 是否正在重试 */
isRetrying: boolean;
}
/**
* MQTT消息回调参数
*/
export interface MqttMessageCallbackParams {
/** 主题 */
topic: string;
/** 消息载荷 */
payload: Buffer | Uint8Array | string;
/** 设备SN */
sn: string;
/** 设备SN与主题的映射关系 */
snMap: Map<string, string[]>;
}
/**
* MQTT发布选项
*/
export interface MqttPublishOptions {
/** QoS等级 (0, 1, 2) */
qos?: 0 | 1 | 2;
/** 是否保留消息 */
retain?: boolean;
}
/**
* MQTT连接基础配置参数
*/
interface MqttConnectionBaseConfig extends IClientOptions, Partial<MqttRetryConfig> {
/** 客户端ID, 规则: epweb_${随机字符串}_${安装商ID} */
clientId: string;
}
/**
* MQTT连接所需配置参数 - 使用认证API
*/
export interface MqttConnectionConfigWithAuth extends MqttConnectionBaseConfig {
/** 获取通信认证信息的函数 */
authApi: () => ApiResponse<string>;
/** 连接地址 - 使用认证API时不需要 */
brokerUrl?: never;
}
/**
* MQTT连接所需配置参数 - 使用直接连接
*/
export interface MqttConnectionConfigWithBroker extends MqttConnectionBaseConfig {
/** 获取通信认证信息的函数 - 使用直接连接时不需要 */
authApi?: never;
/** 连接地址 */
brokerUrl: string;
}
/**
* MQTT连接所需配置参数 - 二选一:authApi 或 brokerUrl
*/
export type MqttConnectionConfig = MqttConnectionConfigWithAuth | MqttConnectionConfigWithBroker;
/** MQTT 签名解密后的响应数据 */
export interface ResponseMqttSignCrypto {
protocol: string;
url: string;
port: string;
path: string;
userName: string;
password: string;
}
/**
* 下发指令消息体类型(这里的类型根据具体的业务做调整)
* 值为10进制 / 16进制
*/
export interface CommandMessage {
/** 指令ID */
cmdId: number;
/** 指令功能码 */
cmdFunc: number;
/** 目的地址 */
dest?: number;
src?: number;
dSrc?: number;
dDest?: number;
/** 加密类型 */
encType?: number;
/** 校验类型 */
checkType?: number;
/** 是否需要应答 */
needAck?: 0 | 1;
/** 序列号 */
seq?: number;
/** 是否是应答消息 */
isAck?: 0 | 1;
/** 协议版本号 */
version?: number;
/** 数据版本号 */
payloadVer?: number;
/** 数据长度 */
dataLen?: number;
/** 数据载荷 */
pdata?: Buffer;
/** 来源 */
from?: string;
/** 设备SN */
deviceSn?: string;
[key: string]: any;
}
3.2 MqttClient 基类定义
- 支持两种连接方式:
authApi、brokeUrl; topic维护,批量订阅、自动去重;- 自动重连,支持配置最大重试次数、延迟等;
- 事件毁掉,获取连接状态,离开自动销毁连接;
import CryptoJS from 'crypto-js';
import type { Client } from 'mqtt';
import { connect } from 'mqtt';
import type {
MqttClientState,
MqttConnectionConfig,
ResponseMqttSignCrypto,
MqttEventCallbacks,
MqttRetryConfig,
MqttRetryStatus,
MqttMessageCallbackParams,
MqttPublishOptions,
} from './mqtt.d';
import omit from 'lodash-es/omit';
/**
* MQTT 客户端管理类
*/
export class MqttClient {
private client: Client | null = null;
private state: MqttClientState = {
client: null,
loading: false,
error: null,
connected: false,
snList: [],
topicList: [],
snMap: new Map<string, string[]>(),
};
// 状态变化监听器
private stateListeners: Set<(state: MqttClientState) => void> = new Set();
// 重试机制相关
private retryConfig: MqttRetryConfig = {
maxRetries: 3,
retryDelay: 3000,
retryBackoff: 2,
};
private retryCount = 0;
private retryTimeout: NodeJS.Timeout | null = null;
private isRetrying = false;
private connectTimeoutMs = 30000; // 默认连接超时时间
// 事件回调接口
private eventCallbacks: MqttEventCallbacks = {
onConnect: null,
onDisconnect: null,
onError: null,
onReconnect: null,
onOffline: null,
onOnline: null,
onRetryStart: null,
onRetryFailed: null,
onMaxRetriesReached: null,
};
// 当前消息处理器引用,用于精确移除监听器
private currentMessageHandler: ((topic: string, payload: Buffer | Uint8Array | string) => void) | null = null;
// 当前的回调函数引用
private currentCallback: ((params: MqttMessageCallbackParams) => void) | null = null;
// 是否已销毁,防止销毁后自动重连
private isDestroyed = false;
/**
* @description 获取客户端实例
* @returns {Client | null} MQTT客户端实例
*/
getClient(): Client | null {
return this.client;
}
/**
* @description 设置事件回调
* @param {Partial<MqttEventCallbacks>} callbacks 事件回调函数
*/
setEventCallbacks(callbacks: Partial<MqttEventCallbacks>): void {
this.eventCallbacks = { ...this.eventCallbacks, ...callbacks };
}
/**
* @description 设置重试配置
* @param {Partial<MqttRetryConfig>} config 重试配置
*/
setRetryConfig(config: Partial<MqttRetryConfig>): void {
this.retryConfig = { ...this.retryConfig, ...config };
}
/**
* @description 从外部配置中提取重试相关参数
* @param {MqttConnectionConfig} config 外部传入的配置
*/
private extractRetryConfigFromExternal(config: MqttConnectionConfig): void {
// 如果外部传入了重试相关配置,使用外部配置
if (config.maxRetries !== undefined) {
this.retryConfig.maxRetries = config.maxRetries;
}
if (config.retryDelay !== undefined) {
this.retryConfig.retryDelay = config.retryDelay;
}
if (config.retryBackoff !== undefined) {
this.retryConfig.retryBackoff = config.retryBackoff;
}
}
/**
* @description 获取重试状态
*/
getRetryStatus(): MqttRetryStatus {
return {
retryCount: this.retryCount,
maxRetries: this.retryConfig.maxRetries,
isRetrying: this.isRetrying,
};
}
/**
* @description 清理重试定时器
*/
private clearRetryTimeout(): void {
if (this.retryTimeout) {
clearTimeout(this.retryTimeout);
this.retryTimeout = null;
}
}
/**
* @description 重置重试状态
*/
private resetRetryState(): void {
this.retryCount = 0;
this.isRetrying = false;
this.clearRetryTimeout();
}
/**
* @description 执行重试连接
*/
private async retryConnection(config: MqttConnectionConfig, JWT: string): Promise<void> {
if (this.retryCount >= this.retryConfig.maxRetries || this.isRetrying) {
if (this.retryCount >= this.retryConfig.maxRetries) {
console.warn('Max retry attempts reached');
this.eventCallbacks.onMaxRetriesReached?.();
}
return;
}
this.isRetrying = true;
this.retryCount += 1;
const delay = this.retryConfig.retryDelay * Math.pow(this.retryConfig.retryBackoff, this.retryCount - 1);
console.log(`Retrying connection in ${delay}ms (attempt ${this.retryCount}/${this.retryConfig.maxRetries})`);
this.eventCallbacks.onRetryStart?.(this.retryCount);
this.retryTimeout = setTimeout(async () => {
try {
await this.createConnection(config, JWT);
this.resetRetryState();
} catch (error) {
console.warn(`Connection retry ${this.retryCount} failed:`, error);
this.eventCallbacks.onRetryFailed?.(this.retryCount, error instanceof Error ? error : new Error('Retry failed'));
this.isRetrying = false;
// 使用 setTimeout 避免递归调用
setTimeout(() => this.retryConnection(config, JWT), 100);
}
}, delay);
}
/**
* @description 获取状态
* @returns {MqttClientState} MQTT客户端状态
*/
getState(): MqttClientState {
return { ...this.state };
}
/**
* @description 设置状态
* @param {Partial<MqttClientState>} updates 更新状态
* @param {Client} updates.client MQTT客户端实例
* @param {boolean} updates.loading 连接加载状态
* @param {Error | null} updates.error 连接错误信息
* @param {boolean} updates.connected 连接状态
* @returns {void} void
*/
private setState(updates: Partial<MqttClientState>): void {
this.state = { ...this.state, ...updates };
// 通知所有监听器状态已变化
this.notifyStateChange();
}
/**
* @description 通知状态变化
* @returns {void} void
*/
private notifyStateChange(): void {
this.stateListeners.forEach((listener) => {
try {
listener({ ...this.state });
} catch (error) {
console.error('State listener error:', error);
}
});
}
/**
* @description 构建SN到主题的映射关系
* @param {string[]} snList 设备SN列表
* @param {string[]} topics 主题列表
* @returns {Map<string, string[]>} SN到主题的映射
*/
private buildSnMap(snList: string[], topics: string[]): Map<string, string[]> {
const snMap = new Map<string, string[]>();
// 为每个SN预先创建空数组
snList.forEach((sn) => snMap.set(sn, []));
// 优化:使用更高效的匹配算法
// 遍历topics,将匹配的topic添加到对应的SN
topics.forEach((topic) => {
// 使用find而不是forEach,找到第一个匹配的SN就停止
const matchedSn = snList.find((sn) => topic.includes(sn));
if (matchedSn) {
const snTopics = snMap.get(matchedSn);
if (snTopics && !snTopics.includes(topic)) {
snTopics.push(topic);
}
}
});
return snMap;
}
/**
* @description 更新订阅状态
* @param {string[]} snList 设备SN列表
* @param {string[]} topics 主题列表
* @param {boolean} isAdd 是否为添加操作
* @returns {void} void
*/
private updateSubscriptionState(snList: string[], topics: string[], isAdd: boolean): void {
if (isAdd) {
// 添加订阅
const newSnList = [...new Set([...this.state.snList, ...snList])];
const newTopicList = [...new Set([...this.state.topicList, ...topics])];
// 合并映射关系
const newSnMap = new Map(this.state.snMap);
const currentSnMap = this.buildSnMap(snList, topics);
currentSnMap.forEach((topics, sn) => {
const existingTopics = newSnMap.get(sn) || [];
const mergedTopics = [...new Set([...existingTopics, ...topics])];
newSnMap.set(sn, mergedTopics);
});
this.setState({
snList: newSnList,
topicList: newTopicList,
snMap: newSnMap,
});
} else {
// 移除订阅
const remainingTopics = this.state.topicList.filter((topic) => !topics.includes(topic));
const remainingSnList = this.state.snList.filter((sn) => {
const snTopics = this.state.snMap.get(sn) || [];
return snTopics.some((topic) => remainingTopics.includes(topic));
});
const newSnMap = this.buildSnMap(remainingSnList, remainingTopics);
this.setState({
topicList: remainingTopics,
snList: remainingSnList,
snMap: newSnMap,
});
}
}
/**
* @description 设置消息回调函数
* @param {function} callback 消息回调函数
* @returns {void} void
*/
private setMessageCallback(callback: (params: MqttMessageCallbackParams) => void): void {
if (this.client) {
// 如果回调函数相同,不需要重新设置
if (this.currentCallback === callback && this.currentMessageHandler) {
return;
}
// 只移除当前实例的message监听器,避免影响其他实例
if (this.currentMessageHandler) {
this.client.removeListener('message', this.currentMessageHandler);
}
// 保存当前的回调函数引用
this.currentCallback = callback;
// 保存当前的消息处理器引用,以便后续移除
this.currentMessageHandler = (topic: string, payload: Buffer | Uint8Array | string) => {
try {
// 是所订阅的主题, 接收消息体, 回调传出
if (this.state.topicList.includes(topic)) {
const sn = this.state.snList.find((sn) => this.state.snMap.get(sn)?.includes(topic));
callback({
topic,
payload,
sn: sn || '',
snMap: this.state.snMap,
});
}
} catch (error) {
const err = error instanceof Error ? error : new Error('subscribe message error');
console.error('Subscribe message error:', err.message);
}
};
this.client.on('message', this.currentMessageHandler);
}
}
/**
* @description 获取连接状态
* @returns {boolean} 连接状态
*/
getConnected(): boolean {
return this.state.connected && this.client !== null;
}
/**
* @description 检查是否正在重连
* @returns {boolean} 是否正在重连
*/
isReconnecting(): boolean {
return this.state.loading && !this.state.connected && this.client !== null;
}
/**
* @async
* @description 创建Mqtt连接
* @param {MqttConnectionConfig} config MQTT连接配置
* @param {string} config.getWebSocketAuthInfo 获取WebSocket认证信息的函数
* @param {string} config.clientId 客户端ID
* @param {function} successCallback 连接成功后的回调函数
* @param {function} errorCallback 连接失败后的回调函数
* @returns {Promise<void>} Promise<void>
*/
async createConnection(config: MqttConnectionConfig, JWT: string): Promise<void> {
// 如果已有连接,先断开
if (this.client) {
this.disconnect();
}
this.setState({ loading: true, error: null });
// 从外部配置中提取重试相关参数
this.extractRetryConfigFromExternal(config);
try {
let url: string;
let username: string;
let password: string;
// 如果提供了 authApi,使用认证流程
if ('authApi' in config && config.authApi) {
const { code, data } = await config.authApi();
if (code !== '0' || !data || !JWT) throw new Error('MQTT AUTH ERROR');
// 解密签名
const digest = CryptoJS.SHA256(JWT.split(' ')[1]);
const iv = CryptoJS.enc.Utf8.parse('ojsajkqjwk1w2dfg');
const decode = CryptoJS.AES.decrypt(data, digest, {
iv: iv,
mode: CryptoJS.mode.CFB,
});
const decodeRes: ResponseMqttSignCrypto = JSON.parse(decode.toString(CryptoJS.enc.Utf8));
// 创建mqtt连接
const { protocol, url: hostname, port, path, userName, password: pwd } = decodeRes;
url = `${protocol}://${hostname}:${port}${path}`;
username = userName;
password = pwd;
} else {
// 使用直接配置的 brokerUrl
if (!('brokerUrl' in config) || !config.brokerUrl) {
throw new Error('Either authApi or brokerUrl must be provided');
}
url = config.brokerUrl;
username = config.username || '';
password = config.password || '';
}
// 提取外部传入的连接超时时间,用于我们的重连机制
this.connectTimeoutMs = config.connectTimeout || 30000;
const client = connect(url, {
clientId: config.clientId,
keepalive: 30,
protocolVersion: 4,
clean: true,
reconnectPeriod: 0, // 禁用自动重连,由上层逻辑控制
connectTimeout: this.connectTimeoutMs,
username,
password,
...omit(config, ['authApi', 'clientId', 'reconnectPeriod', 'brokerUrl']),
});
// 设置事件监听器
this.setupEventListeners(client, config, JWT);
// 设置连接超时处理
const connectTimeout = setTimeout(() => {
if (!this.state.connected) {
console.error('MQTT connection timeout');
this.setState({
loading: false,
error: new Error('Connection timeout'),
connected: false,
});
this.handleDisconnection(config, JWT);
}
}, this.connectTimeoutMs); // 使用复用的连接超时时间
// 监听连接成功,清除超时定时器
client.once('connect', () => {
clearTimeout(connectTimeout);
});
// 设置客户端实例(但不立即设置连接状态,等待connect事件)
this.client = client;
} catch (err) {
const error = err instanceof Error ? err : new Error('MQTT CONNECTION ERROR');
this.setState({
client: null,
loading: false,
error,
connected: false,
});
// 触发重试
await this.retryConnection(config, JWT);
}
}
/**
* @description 设置事件监听器
*/
private setupEventListeners(client: Client, config: MqttConnectionConfig, JWT: string): void {
// 监听连接成功
client.on('connect', () => {
console.info('MQTT CONNECT SUCCESS - Event triggered');
this.setState({ connected: true, loading: false, error: null });
this.resetRetryState(); // 重置重试状态
this.eventCallbacks.onConnect?.();
});
// 监听连接断开
client.on('disconnect', () => {
console.warn('MQTT DISCONNECTED');
this.setState({ connected: false });
this.eventCallbacks.onDisconnect?.();
this.handleDisconnection(config, JWT);
});
// 监听连接错误
client.on('error', (error) => {
console.error('MQTT ERROR:', error);
this.setState({
error: error instanceof Error ? error : new Error('MQTT connection error'),
connected: false,
});
this.eventCallbacks.onError?.(error);
this.handleDisconnection(config, JWT);
});
// 监听重连
client.on('reconnect', () => {
console.info('MQTT RECONNECT');
this.setState({ connected: false, loading: true, error: null });
this.eventCallbacks.onReconnect?.();
});
// 监听离线
client.on('offline', () => {
console.warn('MQTT OFFLINE');
this.setState({ connected: false });
this.eventCallbacks.onOffline?.();
});
// 监听在线
client.on('online', () => {
console.log('MQTT ONLINE');
this.setState({ connected: true });
this.eventCallbacks.onOnline?.();
});
// 监听连接关闭
client.on('close', () => {
console.warn('MQTT CONNECTION CLOSED');
this.setState({ connected: false });
this.eventCallbacks.onDisconnect?.();
this.handleDisconnection(config, JWT);
});
}
/**
* @description 处理连接断开
*/
private handleDisconnection(config: MqttConnectionConfig, JWT: string): void {
// 如果已销毁,不进行重连
if (this.isDestroyed) {
console.log('MqttClient is destroyed, skipping reconnection');
return;
}
// 如果不在重试中且未达到最大重试次数,则触发重连
if (!this.isRetrying && this.retryCount < this.retryConfig.maxRetries) {
console.log('Connection lost, attempting to reconnect...');
this.retryConnection(config, JWT);
}
}
/**
* @description 断开MQTT连接
* @returns {void} void
*/
disconnect(): void {
if (!this.client) return;
try {
// 取消所有订阅
if (this.state.topicList.length > 0) {
this.client.unsubscribe(this.state.topicList);
}
// 只移除当前实例的message监听器,避免影响其他实例
if (this.currentMessageHandler) {
this.client.removeListener('message', this.currentMessageHandler);
}
// 断开连接
this.client.end(false);
} catch (error) {
console.error('Error during disconnect:', error);
} finally {
// 清理重试状态
this.clearRetryTimeout();
this.resetRetryState();
// 清理资源
this.client = null;
this.currentMessageHandler = null;
this.currentCallback = null;
// 清空状态
this.setState({
client: null,
connected: false,
loading: false,
error: null,
topicList: [],
snList: [],
snMap: new Map<string, string[]>(),
});
}
}
/**
* @description 订阅主题
* @param {string[]} snList 设备SN列表
* @param {string[]} topics 主题列表
* @param {function} callback 回调函数
* @param {string} callback.topic 主题
* @param {Buffer | Uint8Array | string} callback.payload 回执消息内容
* @param {string[]} callback.snList 设备SN列表
* @param {Map<string, string[]>} callback.snMap 设备SN与主题的映射关系
* @param {function} successCallback 订阅成功后的回调函数
* @param {function} errorCallback 订阅失败后的回调函数
* @returns {void} void
*/
subscribe(
snList: string[],
topics: string[],
callback: (params: MqttMessageCallbackParams) => void,
successCallback?: () => void,
errorCallback?: (error: Error) => void,
): void {
if (!this.client) {
console.warn('MQTT client not initialized, cannot subscribe');
return;
}
if (!this.state.connected) {
console.warn('MQTT client not connected, cannot subscribe');
return;
}
// 过滤出新的主题(避免重复订阅)
const newTopics = topics.filter((topic) => !this.state.topicList.includes(topic));
if (newTopics.length === 0) {
console.warn('All topics already subscribed');
return;
}
// 设置消息回调
this.setMessageCallback(callback);
// 更新订阅状态
this.updateSubscriptionState(snList, topics, true);
// 只订阅新主题
this.client.subscribe(newTopics, (error) => {
if (error) {
console.error('Subscribe error:', error);
// 订阅失败时回滚状态
this.updateSubscriptionState(snList, topics, false);
errorCallback && errorCallback(error);
return;
}
console.info('Successfully subscribed to topics:', newTopics);
successCallback && successCallback();
});
}
/**
* @description 取消订阅主题
* @param {string[]} topics 主题列表 (可选, 如果不传则取消所有订阅)
* @returns {void} void
*/
unsubscribe(topics?: string[]): void {
if (!this.client) {
console.error('MQTT NO CONNECTED');
return;
}
try {
const topicsToUnsubscribe = topics && topics.length > 0 ? topics : this.state.topicList;
// 取消订阅主题
if (topicsToUnsubscribe.length > 0) {
this.client.unsubscribe(topicsToUnsubscribe);
}
// 清空状态
if (!topics || topics.length === 0) {
this.setState({
topicList: [],
snList: [],
snMap: new Map<string, string[]>(),
});
} else {
// 部分取消订阅,更新状态
this.updateSubscriptionState([], topics, false);
}
} catch (error) {
const err = error instanceof Error ? error : new Error('unsubscribe error');
console.error('unsubscribe error:', err.message);
throw err;
}
}
/**
* @description 发布消息
* @param {string} topic 主题
* @param {Buffer | Uint8Array | string} payload 消息内容
* @param {object} options 发布选项
* @param {number} options.qos QoS等级 (0, 1, 2)
* @param {boolean} options.retain 是否保留消息
* @returns {boolean} 发送是否成功
*/
publish(topic: string, payload: Buffer | Uint8Array | string, options: MqttPublishOptions = {}): boolean {
if (!this.getConnected()) {
console.error('MQTT client not available for publishing');
return false;
}
if (!topic || !payload) {
console.error('Invalid topic or payload for publishing');
return false;
}
try {
const client = this.client;
if (!client) return false;
const messagePayload = payload as any;
client.publish(topic, messagePayload, {
qos: options.qos || 0,
retain: options.retain || false,
});
return true;
} catch (error) {
const err = error instanceof Error ? error : new Error('publish error');
console.error('publish error:', err.message);
return false;
}
}
/**
* @description 检查是否已订阅指定主题
* @param {string} topic 主题
* @returns {boolean} 是否已订阅
*/
isSubscribed(topic: string): boolean {
return this.state.topicList.includes(topic);
}
/**
* @description 检查是否已订阅指定设备SN
* @param {string} sn 设备SN
* @returns {boolean} 是否已订阅
*/
isDeviceSubscribed(sn: string): boolean {
return this.state.snList.includes(sn);
}
/**
* @description 获取设备SN对应的主题列表
* @param {string} sn 设备SN
* @returns {string[]} 主题列表
*/
getDeviceTopics(sn: string): string[] {
return this.state.snMap.get(sn) || [];
}
/**
* @description 添加状态变化监听器
* @param {function} listener 状态变化回调函数
* @returns {function} 移除监听器的函数
*/
addStateListener(listener: (state: MqttClientState) => void): () => void {
this.stateListeners.add(listener);
// 立即调用一次,确保组件获取最新状态
listener({ ...this.state });
// 返回移除监听器的函数
return () => {
this.stateListeners.delete(listener);
};
}
/**
* @description 移除所有状态监听器
* @returns {void} void
*/
removeAllStateListeners(): void {
this.stateListeners.clear();
}
/**
* @description 销毁客户端,清理所有资源
* @returns {void} void
*/
destroy(): void {
// 标记为已销毁,防止重连
this.isDestroyed = true;
// 断开连接
this.disconnect();
// 清理所有监听器
this.removeAllStateListeners();
// 重置状态
this.state = {
client: null,
loading: false,
error: null,
connected: false,
snList: [],
topicList: [],
snMap: new Map<string, string[]>(),
};
}
}
3.3 useMqttHook
- 生命周期自动化:自动挂载连接、销毁;
- 状态同步:确保UI层数据更新;
// 获取用户信息和mqtt鉴权信息
import { useAuth } from '';
// 生产mqtt clientId
import { createClientId } from '';
import { useMount } from 'ahooks';
import { useEffect, useState } from 'react';
import { MqttClient } from './MqttClient';
import type { MqttConnectionConfig, MqttEventCallbacks, MqttMessageCallbackParams, MqttPublishOptions, MqttSubscriptionConfig } from './mqtt.d';
/**
* MQTT Hook - 主要是为了解决直接操作mqtt实例, 状态更新不触发组件更新问题
* 提供MQTT连接、订阅、取消订阅、发布消息等功能
* @param {Partial<MqttConnectionConfig>} mqttConfig - MQTT连接配置
* @param {string} JWT - JWT令牌
* @param {Partial<MqttEventCallbacks>} eventCallbacks - MQTT事件回调函数
* @param {MqttSubscriptionConfig} initialSubscription - 初始订阅配置
*/
export const useMqttHook = (
mqttConfig: Partial<MqttConnectionConfig>,
JWT = '',
eventCallbacks?: Partial<MqttEventCallbacks>,
initialSubscription?: MqttSubscriptionConfig,
) => {
// 用于创建mqtt实例的clientId, 外部有传入则使用props
const { userInfo, JWT: jwtAuth = '' } = useAuth();
const [isSubscribing, setIsSubscribing] = useState(false);
// 自动订阅初始订阅配置
const autoSubscribe = async (subscription: MqttSubscriptionConfig, callbacks?: Partial<MqttEventCallbacks>) => {
if (isSubscribing) return;
setIsSubscribing(true);
try {
await mqttInstance.subscribe(subscription.snList, subscription.topics, subscription.onMessage);
callbacks?.onSubscribe?.(subscription.snList, subscription.topics);
} catch (error) {
const err = error instanceof Error ? error : new Error('Subscribe failed');
console.error('Auto subscribe failed:', err);
callbacks?.onSubscribeError?.(err);
} finally {
setIsSubscribing(false);
}
};
// 生成事件回调,让UI层自己管理连接状态
const generateEventCallbacks = (callbacks?: Partial<MqttEventCallbacks>): MqttEventCallbacks => {
return {
onConnect: () => {
console.log('onConnect');
callbacks?.onConnect?.();
// 连接成功后自动订阅初始配置
if (initialSubscription) {
autoSubscribe(initialSubscription, callbacks);
}
},
onDisconnect: () => {
console.log('onDisconnect');
callbacks?.onDisconnect?.();
},
onError: (error: Error) => {
console.log('onError', error);
callbacks?.onError?.(error);
},
onReconnect: () => {
console.log('onReconnect');
callbacks?.onReconnect?.();
},
onOffline: () => {
console.log('onOffline');
callbacks?.onOffline?.();
},
onOnline: () => {
console.log('onOnline');
callbacks?.onOnline?.();
},
onRetryStart: (retryCount: number) => {
console.log('onRetryStart', retryCount);
callbacks?.onRetryStart?.(retryCount);
},
onRetryFailed: (retryCount: number, error: Error) => {
console.log('onRetryFailed', retryCount, error);
callbacks?.onRetryFailed?.(retryCount, error);
},
onMaxRetriesReached: () => {
console.log('onMaxRetriesReached');
callbacks?.onMaxRetriesReached?.();
},
};
};
const [mqttInstance] = useState<MqttClient>(() => {
const instance = new MqttClient();
instance.setEventCallbacks(generateEventCallbacks(eventCallbacks));
return instance;
});
const createConnection = async (): Promise<void> => {
// 如果已经连接,直接返回
if (mqttInstance.getConnected()) {
console.warn('MQTT already connected, skipping connection');
return;
}
// 额外添加 _hook 标识,避免与 Mqoa、context 冲突
const baseClientId = userInfo ? createClientId(userInfo.enterpriseId, userInfo.userId) : 'default_mqtt_clientid_provider';
const mqttClientId = `${baseClientId}_hook`;
try {
const baseConfig = {
clientId: mqttConfig.clientId || mqttClientId,
maxRetries: mqttConfig.maxRetries,
retryDelay: mqttConfig.retryDelay,
retryBackoff: mqttConfig.retryBackoff,
};
const connectionConfig = mqttConfig.authApi
? { ...baseConfig, authApi: mqttConfig.authApi }
: { ...baseConfig, brokerUrl: mqttConfig.brokerUrl || '', username: mqttConfig.username, password: mqttConfig.password };
await mqttInstance.createConnection(connectionConfig as MqttConnectionConfig, JWT || jwtAuth);
} catch (error) {
const err = error instanceof Error ? error : new Error('Connection failed');
console.error('Connection failed:', err);
}
};
/**
* 新增额外的订阅topic, MqttClient内部会自动处理重复订阅
* @param {MqttSubscriptionConfig} subscription 订阅配置
* @returns {Promise<void>} Promise<void>
*/
const addSubscription = async (subscription: MqttSubscriptionConfig) => {
try {
await mqttInstance.subscribe(subscription.snList, subscription.topics, subscription.onMessage);
eventCallbacks?.onSubscribe?.(subscription.snList, subscription.topics);
} catch (error) {
const err = error instanceof Error ? error : new Error('Add subscription failed');
console.error('Add subscription failed:', err);
eventCallbacks?.onSubscribeError?.(err);
}
};
// 自动连接 - 只在组件首次挂载时执行一次
useMount(() => {
createConnection();
});
// 组件卸载时清理资源
useEffect(() => {
return () => {
mqttInstance.destroy();
};
}, [mqttInstance]);
return {
mqttInstance,
isSubscribing,
subscribe: (snList: string[], topics: string[], callback: (params: MqttMessageCallbackParams) => void) =>
mqttInstance.subscribe(snList, topics, callback),
unsubscribe: (topics: string[]) => mqttInstance.unsubscribe(topics),
publish: (topic: string, payload: Buffer | Uint8Array, options?: MqttPublishOptions) => mqttInstance.publish(topic, payload, options),
disconnect: mqttInstance.disconnect,
createConnection,
addSubscription,
};
};
3.4 字节转对象
/**
* 用户数据属性
*/
export interface XfTpUserDataT {
// 网络连接,0:断开,1:连接
netSta: number;
// 蓝牙连接,0:断开,1:连接
bleSta: number;
// 用户数据属性
dataAttri: number;
// 自定义数据,适合变化不频繁的自定义数据, 长度为 10
customData: string;
}
/**
* 拓扑节点
*/
export interface XfTpNode {
sn: string;
userData: XfTpUserDataT;
index: number;
parentIndex: number; // 0
}
/**
* 拓扑结构
*/
export interface Topology {
// 产品 SN, 长度为 16
sn: string;
// 节点数量
nodeNum: number;
node: XfTpNode[];
}
/**
* 产品类型Bean
*/
export interface ProductTypeBean {
productType: number;
count: number;
}
/**
* 字节工具类
*/
class ByteUtils {
/**
* 将字节数组转换为整数(小端序)
*/
static bytesToInt(bytes: Uint8Array, offset: number): number {
return bytes[offset] | (bytes[offset + 1] << 8) | (bytes[offset + 2] << 16) | (bytes[offset + 3] << 24);
}
/**
* 将字节数组转换为字符串
*/
static bytesToString(bytes: Uint8Array, start: number, end: number): string {
const slice = bytes.slice(start, end);
return new TextDecoder('utf-8').decode(slice).trim();
}
}
/**
* 日志工具类
*/
class EFLog {
static e(tag: string, message: string): void {
console.error(`[${tag}] ${message}`);
}
}
/**
* 将 byte 数组转换为拓扑数据
*/
export function convertToTopology(byteArray: Uint8Array): Topology | null {
if (byteArray.length < 17) {
return null;
}
try {
const sn = ByteUtils.bytesToString(byteArray, 0, 16);
const nodeNum = ByteUtils.bytesToInt(byteArray, 16);
const nodes: XfTpNode[] = [];
let offset = 20;
for (let i = 0; i < nodeNum; i++) {
const itemSn = ByteUtils.bytesToString(byteArray, offset, offset + 16);
offset += 16;
const sta = ByteUtils.bytesToInt(byteArray, offset);
const netSta = (sta >> 0) & 0b1;
const bleSta = (sta >> 1) & 0b1;
const dataAttri = (sta >> 2) & 0xff;
const customData = ByteUtils.bytesToString(byteArray, offset + 4, offset + 14);
const userData: XfTpUserDataT = {
netSta,
bleSta,
dataAttri,
customData,
};
offset += 14;
const index = byteArray[offset];
const parentIndex = byteArray[offset + 1];
offset += 2;
nodes.push({
sn: itemSn,
userData,
index,
parentIndex,
});
}
return {
sn,
nodeNum,
node: nodes,
};
} catch (e) {
EFLog.e('convertToTopology', `convertToTopology: ${e instanceof Error ? e.stack : String(e)}`);
return null;
}
}
/**
* 将字节数组转换为拓扑数据(兼容ArrayBuffer输入)
*/
export function convertToTopologyFromBuffer(buffer: ArrayBuffer): Topology | null {
return convertToTopology(new Uint8Array(buffer));
}
/**
* 将字节数组转换为拓扑数据(兼容number[]输入)
*/
export function convertToTopologyFromArray(bytes: number[]): Topology | null {
return convertToTopology(new Uint8Array(bytes));
}
3.5 hook使用示例
import { useMqttHook } from '@/hook/useMqttHook';
import { useState } from 'react';
import { iot, common } from '@/proto/iot';
export default function Test() {
const [loading, setLoading] = useState(true);
// 处理MQTT消息的回调函数
const handleMqttMessage = (params: any) => {
try {
const uint8Array = new Uint8Array(params.payload);
const headers = common.Send_Header_Msg.decode(uint8Array).msg;
for (const header of headers) {
const { cmdFunc, cmdId, pdata } = header;
if (cmdFunc === 0x35) {
if (cmdId === 0xbc) {
const data = iot.IotConfig1547Ack.decode(pdata as Uint8Array);
console.log(params.topic, data);
// 拿到数据后更新状态, 更新UI层
}
}
}
} catch (e) {
console.error('解析preGridTestSetting返回数据失败:', e);
setLoading(false);
}
};
const { publish } = useMqttHook(
{
authApi: () => Promise.resolve({ code: '0', data: { token: 'token' } }),
},
'',
{
onConnect: () => {
setLoading(false);
},
onDisconnect: () => {
setLoading(false);
},
onReconnect: () => {
setLoading(false);
},
},
{
snList: ['aaaaa'],
topics: [`/ep/userId/aaaaa/thing/property/set_reply`],
onMessage: handleMqttMessage,
},
);
const handlePublish = () => {
const topic = `/ep/userId/aaaaa/thing/property/set`;
setLoading(true);
// 通过pb协议将要传输的数据做转换
const pdata = iot.IotConfig1547.create({
a: 1,
b: 2,
c: 3,
});
const buffer_pdata = iot.IotConfig1547.encode(pdata).finish();
// 实际发送的消息体
const message = {
msg: [
{
dest: 0x35,
cmdId: 0xbc,
cmdFunc: 0x35,
src: 0x20,
dSrc: 0x01,
dDest: 0x01,
needAck: 0x01,
seq: Date.now(),
version: 3,
payloadVer: 1,
dataLen: buffer_pdata.length,
pdata: buffer_pdata,
from: 'web',
},
],
};
const header_message = common.Send_Header_Msg.create(message);
const buffer = common.Send_Header_Msg.encode(header_message).finish();
publish(topic, buffer);
};
return (
<div>
<button onClick={handlePublish}>Publish</button>
</div>
);
}