2026-06-24 f4bd1f3c89d906131495a0aca5aaf82966378510
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager;
 
import cn.hutool.core.util.StrUtil;
import io.netty.handler.codec.mqtt.MqttQoS;
import io.vertx.core.buffer.Buffer;
import io.vertx.mqtt.MqttEndpoint;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
 
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
 
/**
 * IoT 网关 MQTT 连接管理器
 * <p>
 * 统一管理 MQTT 连接的认证状态、设备会话和消息发送功能:
 * 1. 管理 MQTT 连接的认证状态
 * 2. 管理设备会话和在线状态
 * 3. 管理消息发送到设备
 *
 * @author 芋道源码
 */
@Slf4j
@Component
public class IotMqttConnectionManager {
 
    /**
     * 未知地址常量(当获取端点地址失败时使用)
     */
    private static final String UNKNOWN_ADDRESS = "unknown";
 
    /**
     * 连接信息映射:MqttEndpoint -> 连接信息
     */
    private final Map<MqttEndpoint, ConnectionInfo> connectionMap = new ConcurrentHashMap<>();
 
    /**
     * 设备 ID -> MqttEndpoint 的映射
     */
    private final Map<Long, MqttEndpoint> deviceEndpointMap = new ConcurrentHashMap<>();
 
    /**
     * 安全获取 endpoint 地址
     * <p>
     * 优先从缓存获取地址,缓存为空时再尝试实时获取
     *
     * @param endpoint MQTT 连接端点
     * @return 地址字符串,获取失败时返回 "unknown"
     */
    public String getEndpointAddress(MqttEndpoint endpoint) {
        String realTimeAddress = UNKNOWN_ADDRESS;
        if (endpoint == null) {
            return realTimeAddress;
        }
 
        // 1. 优先从缓存获取(避免连接关闭时的异常)
        ConnectionInfo connectionInfo = connectionMap.get(endpoint);
        if (connectionInfo != null && StrUtil.isNotBlank(connectionInfo.getRemoteAddress())) {
            return connectionInfo.getRemoteAddress();
        }
 
        // 2. 缓存为空时尝试实时获取
        try {
            realTimeAddress = endpoint.remoteAddress().toString();
        } catch (Exception ignored) {
            // 连接已关闭,忽略异常
        }
        return realTimeAddress;
    }
 
    /**
     * 注册设备连接(包含认证信息)
     *
     * @param endpoint       MQTT 连接端点
     * @param connectionInfo 连接信息
     */
    public void registerConnection(MqttEndpoint endpoint, ConnectionInfo connectionInfo) {
        Long deviceId = connectionInfo.getDeviceId();
        // 如果设备已有其他连接,先清理旧连接
        MqttEndpoint oldEndpoint = deviceEndpointMap.get(deviceId);
        if (oldEndpoint != null && oldEndpoint != endpoint) {
            log.info("[registerConnection][设备已有其他连接,断开旧连接,设备 ID: {},旧连接: {}]",
                    deviceId, getEndpointAddress(oldEndpoint));
            // 先清理映射,再关闭连接(避免旧连接处理器干扰)
            connectionMap.remove(oldEndpoint);
            oldEndpoint.close();
        }
 
        // 注册新连接
        connectionMap.put(endpoint, connectionInfo);
        deviceEndpointMap.put(deviceId, endpoint);
        log.info("[registerConnection][注册设备连接,设备 ID: {},连接: {},productKey: {},deviceName: {}]",
                deviceId, getEndpointAddress(endpoint), connectionInfo.getProductKey(), connectionInfo.getDeviceName());
    }
 
    /**
     * 注销设备连接
     *
     * @param endpoint MQTT 连接端点
     */
    public void unregisterConnection(MqttEndpoint endpoint) {
        ConnectionInfo connectionInfo = connectionMap.remove(endpoint);
        if (connectionInfo == null) {
            return;
        }
        Long deviceId = connectionInfo.getDeviceId();
        deviceEndpointMap.remove(deviceId);
        log.info("[unregisterConnection][注销设备连接,设备 ID: {},连接: {}]", deviceId, getEndpointAddress(endpoint));
    }
 
    /**
     * 获取连接信息
     */
    public ConnectionInfo getConnectionInfo(MqttEndpoint endpoint) {
        return connectionMap.get(endpoint);
    }
 
    /**
     * 根据设备 ID 获取连接信息
     *
     * @param deviceId 设备 ID
     * @return 连接信息
     */
    public ConnectionInfo getConnectionInfoByDeviceId(Long deviceId) {
        // 通过设备 ID 获取连接端点
        MqttEndpoint endpoint = getDeviceEndpoint(deviceId);
        if (endpoint == null) {
            return null;
        }
        // 获取连接信息
        return getConnectionInfo(endpoint);
    }
 
    /**
     * 发送消息到设备
     *
     * @param deviceId 设备 ID
     * @param topic    主题
     * @param payload  消息内容
     * @param qos      服务质量
     * @param retain   是否保留消息
     * @return 是否发送成功
     */
    public boolean sendToDevice(Long deviceId, String topic, byte[] payload, int qos, boolean retain) {
        MqttEndpoint endpoint = deviceEndpointMap.get(deviceId);
        if (endpoint == null) {
            log.warn("[sendToDevice][设备离线,无法发送消息,设备 ID: {},主题: {}]", deviceId, topic);
            return false;
        }
 
        try {
            endpoint.publish(topic, Buffer.buffer(payload), MqttQoS.valueOf(qos), false, retain);
            log.debug("[sendToDevice][发送消息成功,设备 ID: {},主题: {},QoS: {}]", deviceId, topic, qos);
            return true;
        } catch (Exception e) {
            log.error("[sendToDevice][发送消息失败,设备 ID: {},主题: {},错误: {}]", deviceId, topic, e.getMessage());
            return false;
        }
    }
 
    /**
     * 获取设备连接端点
     */
    public MqttEndpoint getDeviceEndpoint(Long deviceId) {
        return deviceEndpointMap.get(deviceId);
    }
 
    /**
     * 关闭所有连接
     */
    public void closeAll() {
        // 1. 先复制再清空,避免 closeHandler 回调时并发修改
        List<MqttEndpoint> endpoints = new ArrayList<>(connectionMap.keySet());
        connectionMap.clear();
        deviceEndpointMap.clear();
        // 2. 关闭所有连接(closeHandler 中 unregisterConnection 发现 map 为空会安全跳过)
        for (MqttEndpoint endpoint : endpoints) {
            try {
                endpoint.close();
            } catch (Exception ignored) {
                // 连接可能已关闭,忽略异常
            }
        }
    }
 
    /**
     * 连接信息
     */
    @Data
    public static class ConnectionInfo {
 
        /**
         * 设备 ID
         */
        private Long deviceId;
        /**
         * 产品 Key
         */
        private String productKey;
        /**
         * 设备名称
         */
        private String deviceName;
 
        /**
         * 连接地址
         */
        private String remoteAddress;
 
    }
 
}