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
package cn.iocoder.yudao.module.iot.gateway.protocol.udp.manager;
 
import cn.hutool.core.util.ObjUtil;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import io.vertx.core.buffer.Buffer;
import io.vertx.core.datagram.DatagramSocket;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
 
import java.net.InetSocketAddress;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
 
/**
 * IoT 网关 UDP 会话管理器
 * <p>
 * 基于 Guava Cache 实现会话的自动过期清理:
 * 1. 管理设备会话信息(设备 ID -> 地址映射)
 * 2. 自动清理超时会话(expireAfterAccess)
 * 3. 限制最大会话数(maximumSize)
 *
 * @author 芋道源码
 */
@Slf4j
public class IotUdpSessionManager {
 
    /**
     * 设备会话缓存:设备 ID -> 会话信息
     * <p>
     * 使用 Guava Cache 自动管理过期:expireAfterAccess:每次访问(get/put)自动刷新过期时间
     */
    private final Cache<Long, SessionInfo> deviceSessionCache;
 
    private final int maxSessions;
 
    public IotUdpSessionManager(int maxSessions, long sessionTimeoutMs) {
        this.maxSessions = maxSessions;
        this.deviceSessionCache = CacheBuilder.newBuilder()
                .maximumSize(maxSessions)
                .expireAfterAccess(sessionTimeoutMs, TimeUnit.MILLISECONDS)
                .build();
    }
 
    /**
     * 注册设备会话
     *
     * @param deviceId    设备 ID
     * @param sessionInfo 会话信息
     */
    public synchronized void registerSession(Long deviceId, SessionInfo sessionInfo) {
        // 检查是否为新设备,且会话数已达上限(同步方法确保检查和注册的原子性)
        if (deviceSessionCache.getIfPresent(deviceId) == null
                && deviceSessionCache.size() >= maxSessions) {
            throw new IllegalStateException("会话数已达上限: " + maxSessions);
        }
        // 注册会话
        deviceSessionCache.put(deviceId, sessionInfo);
        log.info("[registerSession][注册设备会话,设备 ID: {},地址: {},productKey: {},deviceName: {}]",
                deviceId, buildAddressKey(sessionInfo.getAddress()),
                sessionInfo.getProductKey(), sessionInfo.getDeviceName());
    }
 
    /**
     * 获取会话信息
     * <p>
     * 注意:调用此方法会自动刷新会话的过期时间
     *
     * @param deviceId 设备 ID
     * @return 会话信息,不存在则返回 null
     */
    public SessionInfo getSession(Long deviceId) {
        return deviceSessionCache.getIfPresent(deviceId);
    }
 
    /**
     * 更新设备会话地址(设备地址变更时调用)
     * <p>
     * 注意:getIfPresent 已自动刷新过期时间,无需重新 put
     *
     * @param deviceId   设备 ID
     * @param newAddress 新地址
     */
    public void updateSessionAddress(Long deviceId, InetSocketAddress newAddress) {
        // 地址未变化,无需更新
        SessionInfo sessionInfo = deviceSessionCache.getIfPresent(deviceId);
        if (sessionInfo == null) {
            return;
        }
        if (ObjUtil.equals(newAddress, sessionInfo.getAddress())) {
            return;
        }
 
        // 更新地址
        String oldAddressKey = buildAddressKey(sessionInfo.getAddress());
        sessionInfo.setAddress(newAddress);
        log.debug("[updateSessionAddress][更新设备地址,设备 ID: {},旧地址: {},新地址: {}]",
                deviceId, oldAddressKey, buildAddressKey(newAddress));
    }
 
    /**
     * 发送消息到设备
     *
     * @param deviceId 设备 ID
     * @param data     数据
     * @param socket   UDP Socket
     * @return 是否发送成功
     */
    public boolean sendToDevice(Long deviceId, byte[] data, DatagramSocket socket) {
        SessionInfo sessionInfo = deviceSessionCache.getIfPresent(deviceId);
        if (sessionInfo == null || sessionInfo.getAddress() == null) {
            log.warn("[sendToDevice][设备会话不存在,设备 ID: {}]", deviceId);
            return false;
        }
        InetSocketAddress address = sessionInfo.getAddress();
        try {
            // 使用 CompletableFuture 同步等待发送结果
            CompletableFuture<Boolean> future = new CompletableFuture<>();
            socket.send(Buffer.buffer(data), address.getPort(), address.getHostString(), result -> {
                if (result.succeeded()) {
                    log.debug("[sendToDevice][发送消息成功,设备 ID: {},地址: {},数据长度: {} 字节]",
                            deviceId, buildAddressKey(address), data.length);
                    future.complete(true);
                } else {
                    log.error("[sendToDevice][发送消息失败,设备 ID: {},地址: {}]",
                            deviceId, buildAddressKey(address), result.cause());
                    future.complete(false);
                }
            });
            // 同步等待结果,超时 5 秒
            return future.get(5, TimeUnit.SECONDS);
        } catch (Exception e) {
            log.error("[sendToDevice][发送消息异常,设备 ID: {}]", deviceId, e);
            return false;
        }
    }
 
    /**
     * 构建地址 Key(用于日志输出)
     *
     * @param address 地址
     * @return 地址 Key
     */
    public String buildAddressKey(InetSocketAddress address) {
        return address.getHostString() + ":" + address.getPort();
    }
 
    /**
     * 会话信息
     */
    @Data
    public static class SessionInfo {
 
        /**
         * 设备 ID
         */
        private Long deviceId;
        /**
         * 产品 Key
         */
        private String productKey;
        /**
         * 设备名称
         */
        private String deviceName;
 
        /**
         * 设备地址
         */
        private InetSocketAddress address;
 
    }
 
}