MQTT - IOT通讯

6 阅读14分钟

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 连接流程

  1. 建立连接:客户端向 Broker 发送 CONNECT 消息(包含 clientId、用户名、密码等)
  2. 连接确认:Broker 返回 CONNACK 消息确认连接
  3. 订阅主题:客户端发送 SUBSCRIBE 消息订阅感兴趣的主题
  4. 发布消息:客户端可以发布消息到任何主题
  5. 接收消息:订阅了相应主题的客户端会收到消息
  6. 断开连接:客户端发送 DISCONNECT 消息断开连接

MQTT 在物联网中的应用

在物联网场景中,MQTT 通常用于:

  • 设备状态监控:设备定期发布状态信息
  • 远程控制:通过发布控制命令来控制设备
  • 事件通知:设备事件(如报警、故障)的实时通知
  • 数据采集:传感器数据的实时采集和传输

MQTT 消息结构

┌─────────────────┬─────────────────┬─────────────────┐
│   固定报头       │    可变报头      │     消息体       │
│  (2-4 bytes)    │  (0-65535 bytes)│  (0-268435455)  │
└─────────────────┴─────────────────┴─────────────────┘

主题设计最佳实践

  1. 层级结构: 使用 / 分隔符创建清晰的层级

    device/room1/sensor/temperature
    device/room1/sensor/humidity
    
  2. 命名规范: 使用小写字母、数字和连字符

    ✅ device/room-1/sensor/temp
    ❌ Device/Room1/Sensor/Temp
    
  3. 通配符使用:

    • +: 单级通配符 (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 BuffersTypeScript描述
stringstring字符串
int32, int64number整数
float, doublenumber浮点数
boolboolean布尔值
bytesUint8Array字节数组
repeatedArray<T>数组
map<K,V>Map<K,V>映射

MQTT + Protocol Buffers 的优点

  1. 物联网场景: MQTT 专为 IoT 设计,protobuf 提供高效序列化
  2. 带宽优化: protobuf 比 JSON 更小,适合低带宽环境
  3. 类型安全: protobuf 提供强类型,减少运行时错误
  4. 性能优势: 序列化/反序列化速度快

架构设计

┌─────────────┐    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>

流程说明:

  1. 连接检查:如果已有连接,先断开旧连接
  2. 状态设置:设置 loading 状态,清除错误状态
  3. 配置提取:从外部配置中提取重试相关参数
  4. 认证处理
    • 如果使用 authApi:调用认证 API,使用 JWT 解密认证信息
    • 如果使用 brokerUrl:直接使用配置的连接信息
  5. 建立连接:创建 MQTT 客户端实例并连接
  6. 事件绑定:设置各种事件监听器(connect、disconnect、error 等)
  7. 超时处理:设置连接超时定时器(默认 30 秒)
  8. 错误处理:如果连接失败,触发重试机制
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 重连触发条件

重连机制会在以下情况自动触发:

  1. 连接失败:首次连接或重连失败时
  2. 连接断开:收到 disconnect 事件时
  3. 连接错误:收到 error 事件时
  4. 连接关闭:收到 close 事件时
  5. 连接超时:连接超时未建立时

重连限制:

  • 达到最大重试次数后停止重试
  • 组件销毁后不会触发重连(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 匹配 SN DEVICE_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);
}

消息处理流程:

  1. 接收 MQTT 消息事件
  2. 检查主题是否在订阅列表中
  3. 根据主题查找对应的设备 SN
  4. 构造消息回调参数
  5. 调用用户注册的回调函数
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,
    };
}

注意事项

  1. 自动连接: Hook 会在组件挂载时自动创建连接,无需手动调用 createConnection()(除非需要手动管理)。

  2. 自动清理: 组件卸载时会自动断开连接并清理资源,无需手动调用 disconnect()

  3. 重复订阅: MqttClient 内部会自动处理重复订阅,多次订阅同一主题不会产生错误。

  4. ClientId 生成: 如果未提供 clientId,Hook 会自动生成一个基于用户信息的 clientId,格式为:${baseClientId}_hook

  5. JWT 优先级: 如果同时提供了 Hook 参数中的 JWTuseAuth 中的 JWT,优先使用参数中的 JWT

  6. 初始订阅时机: 初始订阅会在连接成功后自动执行。

  7. 状态更新: 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?)
参数类型必填说明
mqttConfigPartial<MqttConnectionConfig>MQTT 连接配置
JWTstringJWT 令牌,默认为空字符串
eventCallbacksPartial<MqttEventCallbacks>事件回调函数集合
initialSubscriptionMqttSubscriptionConfig初始订阅配置
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 基类定义
  • 支持两种连接方式:authApibrokeUrl
  • 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>
    );
}