Commit bb1210a17a3a9b584fefbfee0b52a8ce56b9962c
1 parent
a912be64
feat:【IoT 物联网】重构 TCP 协议处理
Showing
18 changed files
with
2860 additions
and
433 deletions
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/IotTcpDeviceMessageCodec.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.iot.gateway.codec.tcp; | ||
| 2 | + | ||
| 3 | +import cn.hutool.core.util.ObjectUtil; | ||
| 4 | +import cn.hutool.core.util.StrUtil; | ||
| 5 | +import cn.hutool.json.JSONException; | ||
| 6 | +import cn.hutool.json.JSONObject; | ||
| 7 | +import cn.hutool.json.JSONUtil; | ||
| 8 | +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; | ||
| 9 | +import cn.iocoder.yudao.module.iot.gateway.codec.IotDeviceMessageCodec; | ||
| 10 | +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.protocol.TcpDataDecoder; | ||
| 11 | +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.protocol.TcpDataEncoder; | ||
| 12 | +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.protocol.TcpDataPackage; | ||
| 13 | +import io.vertx.core.buffer.Buffer; | ||
| 14 | +import lombok.extern.slf4j.Slf4j; | ||
| 15 | +import org.springframework.stereotype.Component; | ||
| 16 | + | ||
| 17 | +import java.nio.charset.StandardCharsets; | ||
| 18 | +import java.util.Map; | ||
| 19 | +import java.util.Objects; | ||
| 20 | +import java.util.concurrent.ConcurrentHashMap; | ||
| 21 | + | ||
| 22 | +/** | ||
| 23 | + * TCP {@link IotDeviceMessage} 编解码器 | ||
| 24 | + * <p> | ||
| 25 | + * 参考 EMQX 设计理念: | ||
| 26 | + * 1. 高性能编解码 | ||
| 27 | + * 2. 容错机制 | ||
| 28 | + * 3. 缓存优化 | ||
| 29 | + * 4. 监控统计 | ||
| 30 | + * 5. 资源管理 | ||
| 31 | + * | ||
| 32 | + * @author 芋道源码 | ||
| 33 | + */ | ||
| 34 | +@Component | ||
| 35 | +@Slf4j | ||
| 36 | +public class IotTcpDeviceMessageCodec implements IotDeviceMessageCodec { | ||
| 37 | + | ||
| 38 | + /** | ||
| 39 | + * 编解码器类型 | ||
| 40 | + */ | ||
| 41 | + public static final String TYPE = "tcp"; | ||
| 42 | + | ||
| 43 | + // ==================== 方法映射 ==================== | ||
| 44 | + | ||
| 45 | + /** | ||
| 46 | + * 消息方法到功能码的映射 | ||
| 47 | + */ | ||
| 48 | + private static final Map<String, Short> METHOD_TO_CODE_MAP = new ConcurrentHashMap<>(); | ||
| 49 | + | ||
| 50 | + /** | ||
| 51 | + * 功能码到消息方法的映射 | ||
| 52 | + */ | ||
| 53 | + private static final Map<Short, String> CODE_TO_METHOD_MAP = new ConcurrentHashMap<>(); | ||
| 54 | + | ||
| 55 | + static { | ||
| 56 | + // 初始化方法映射 | ||
| 57 | + initializeMethodMappings(); | ||
| 58 | + } | ||
| 59 | + | ||
| 60 | + // ==================== 缓存管理 ==================== | ||
| 61 | + | ||
| 62 | + /** | ||
| 63 | + * JSON 缓存,提升编解码性能 | ||
| 64 | + */ | ||
| 65 | + private final Map<String, JSONObject> jsonCache = new ConcurrentHashMap<>(); | ||
| 66 | + | ||
| 67 | + /** | ||
| 68 | + * 缓存最大大小 | ||
| 69 | + */ | ||
| 70 | + private static final int MAX_CACHE_SIZE = 1000; | ||
| 71 | + | ||
| 72 | + // ==================== 常量定义 ==================== | ||
| 73 | + | ||
| 74 | + /** | ||
| 75 | + * 负载字段名 | ||
| 76 | + */ | ||
| 77 | + public static class PayloadField { | ||
| 78 | + public static final String TIMESTAMP = "timestamp"; | ||
| 79 | + public static final String MESSAGE_ID = "msgId"; | ||
| 80 | + public static final String DEVICE_ID = "deviceId"; | ||
| 81 | + public static final String PARAMS = "params"; | ||
| 82 | + public static final String DATA = "data"; | ||
| 83 | + public static final String CODE = "code"; | ||
| 84 | + public static final String MESSAGE = "message"; | ||
| 85 | + } | ||
| 86 | + | ||
| 87 | + /** | ||
| 88 | + * 消息方法映射 | ||
| 89 | + */ | ||
| 90 | + public static class MessageMethod { | ||
| 91 | + public static final String PROPERTY_POST = "thing.property.post"; | ||
| 92 | + public static final String PROPERTY_SET = "thing.property.set"; | ||
| 93 | + public static final String PROPERTY_GET = "thing.property.get"; | ||
| 94 | + public static final String EVENT_POST = "thing.event.post"; | ||
| 95 | + public static final String SERVICE_INVOKE = "thing.service.invoke"; | ||
| 96 | + public static final String CONFIG_PUSH = "thing.config.push"; | ||
| 97 | + public static final String OTA_UPGRADE = "thing.ota.upgrade"; | ||
| 98 | + public static final String STATE_ONLINE = "thing.state.online"; | ||
| 99 | + public static final String STATE_OFFLINE = "thing.state.offline"; | ||
| 100 | + } | ||
| 101 | + | ||
| 102 | + // ==================== 初始化方法 ==================== | ||
| 103 | + | ||
| 104 | + /** | ||
| 105 | + * 初始化方法映射 | ||
| 106 | + */ | ||
| 107 | + private static void initializeMethodMappings() { | ||
| 108 | + METHOD_TO_CODE_MAP.put(MessageMethod.PROPERTY_POST, TcpDataPackage.CODE_DATA_UP); | ||
| 109 | + METHOD_TO_CODE_MAP.put(MessageMethod.PROPERTY_SET, TcpDataPackage.CODE_PROPERTY_SET); | ||
| 110 | + METHOD_TO_CODE_MAP.put(MessageMethod.PROPERTY_GET, TcpDataPackage.CODE_PROPERTY_GET); | ||
| 111 | + METHOD_TO_CODE_MAP.put(MessageMethod.EVENT_POST, TcpDataPackage.CODE_EVENT_UP); | ||
| 112 | + METHOD_TO_CODE_MAP.put(MessageMethod.SERVICE_INVOKE, TcpDataPackage.CODE_SERVICE_INVOKE); | ||
| 113 | + METHOD_TO_CODE_MAP.put(MessageMethod.CONFIG_PUSH, TcpDataPackage.CODE_DATA_DOWN); | ||
| 114 | + METHOD_TO_CODE_MAP.put(MessageMethod.OTA_UPGRADE, TcpDataPackage.CODE_DATA_DOWN); | ||
| 115 | + METHOD_TO_CODE_MAP.put(MessageMethod.STATE_ONLINE, TcpDataPackage.CODE_HEARTBEAT); | ||
| 116 | + METHOD_TO_CODE_MAP.put(MessageMethod.STATE_OFFLINE, TcpDataPackage.CODE_HEARTBEAT); | ||
| 117 | + | ||
| 118 | + // 反向映射 | ||
| 119 | + METHOD_TO_CODE_MAP.forEach((method, code) -> CODE_TO_METHOD_MAP.put(code, method)); | ||
| 120 | + } | ||
| 121 | + | ||
| 122 | + // ==================== 编解码方法 ==================== | ||
| 123 | + | ||
| 124 | + @Override | ||
| 125 | + public byte[] encode(IotDeviceMessage message) { | ||
| 126 | + validateEncodeParams(message); | ||
| 127 | + | ||
| 128 | + try { | ||
| 129 | + if (log.isDebugEnabled()) { | ||
| 130 | + log.debug("[encode][开始编码 TCP 消息] 方法: {}, 消息ID: {}", | ||
| 131 | + message.getMethod(), message.getRequestId()); | ||
| 132 | + } | ||
| 133 | + | ||
| 134 | + // 1. 获取功能码 | ||
| 135 | + short code = getCodeByMethodSafely(message.getMethod()); | ||
| 136 | + | ||
| 137 | + // 2. 构建负载 | ||
| 138 | + String payload = buildPayloadOptimized(message); | ||
| 139 | + | ||
| 140 | + // 3. 构建 TCP 数据包 | ||
| 141 | + TcpDataPackage dataPackage = TcpDataPackage.builder() | ||
| 142 | + .addr("") // 地址在发送时由调用方设置 | ||
| 143 | + .code(code) | ||
| 144 | + .mid((short) 0) // 消息序号在发送时由调用方设置 | ||
| 145 | + .payload(payload) | ||
| 146 | + .build(); | ||
| 147 | + | ||
| 148 | + // 4. 编码为字节流 | ||
| 149 | + Buffer buffer = TcpDataEncoder.encode(dataPackage); | ||
| 150 | + byte[] result = buffer.getBytes(); | ||
| 151 | + | ||
| 152 | + // 5. 统计信息 | ||
| 153 | + if (log.isDebugEnabled()) { | ||
| 154 | + log.debug("[encode][TCP 消息编码成功] 方法: {}, 数据长度: {}", | ||
| 155 | + message.getMethod(), result.length); | ||
| 156 | + } | ||
| 157 | + | ||
| 158 | + return result; | ||
| 159 | + | ||
| 160 | + } catch (Exception e) { | ||
| 161 | + log.error("[encode][TCP 消息编码失败] 消息: {}", message, e); | ||
| 162 | + throw new TcpCodecException("TCP 消息编码失败", e); | ||
| 163 | + } | ||
| 164 | + } | ||
| 165 | + | ||
| 166 | + @Override | ||
| 167 | + public IotDeviceMessage decode(byte[] bytes) { | ||
| 168 | + validateDecodeParams(bytes); | ||
| 169 | + | ||
| 170 | + try { | ||
| 171 | + if (log.isDebugEnabled()) { | ||
| 172 | + log.debug("[decode][开始解码 TCP 消息] 数据长度: {}", bytes.length); | ||
| 173 | + } | ||
| 174 | + | ||
| 175 | + // 1. 解码 TCP 数据包 | ||
| 176 | + Buffer buffer = Buffer.buffer(bytes); | ||
| 177 | + TcpDataPackage dataPackage = TcpDataDecoder.decode(buffer); | ||
| 178 | + | ||
| 179 | + // 2. 获取消息方法 | ||
| 180 | + String method = getMethodByCodeSafely(dataPackage.getCode()); | ||
| 181 | + | ||
| 182 | + // 3. 解析负载数据 | ||
| 183 | + Object params = parsePayloadOptimized(dataPackage.getPayload()); | ||
| 184 | + | ||
| 185 | + // 4. 构建 IoT 设备消息 | ||
| 186 | + IotDeviceMessage message = IotDeviceMessage.requestOf(method, params); | ||
| 187 | + | ||
| 188 | + // 5. 统计信息 | ||
| 189 | + if (log.isDebugEnabled()) { | ||
| 190 | + log.debug("[decode][TCP 消息解码成功] 方法: {}, 功能码: {}", | ||
| 191 | + method, dataPackage.getCode()); | ||
| 192 | + } | ||
| 193 | + | ||
| 194 | + return message; | ||
| 195 | + | ||
| 196 | + } catch (Exception e) { | ||
| 197 | + log.error("[decode][TCP 消息解码失败] 数据长度: {}, 数据内容: {}", | ||
| 198 | + bytes.length, truncateData(bytes, 100), e); | ||
| 199 | + throw new TcpCodecException("TCP 消息解码失败", e); | ||
| 200 | + } | ||
| 201 | + } | ||
| 202 | + | ||
| 203 | + @Override | ||
| 204 | + public String type() { | ||
| 205 | + return TYPE; | ||
| 206 | + } | ||
| 207 | + | ||
| 208 | + // ==================== 内部辅助方法 ==================== | ||
| 209 | + | ||
| 210 | + /** | ||
| 211 | + * 验证编码参数 | ||
| 212 | + */ | ||
| 213 | + private void validateEncodeParams(IotDeviceMessage message) { | ||
| 214 | + if (Objects.isNull(message)) { | ||
| 215 | + throw new IllegalArgumentException("IoT 设备消息不能为空"); | ||
| 216 | + } | ||
| 217 | + if (StrUtil.isEmpty(message.getMethod())) { | ||
| 218 | + throw new IllegalArgumentException("消息方法不能为空"); | ||
| 219 | + } | ||
| 220 | + } | ||
| 221 | + | ||
| 222 | + /** | ||
| 223 | + * 验证解码参数 | ||
| 224 | + */ | ||
| 225 | + private void validateDecodeParams(byte[] bytes) { | ||
| 226 | + if (Objects.isNull(bytes) || bytes.length == 0) { | ||
| 227 | + throw new IllegalArgumentException("待解码数据不能为空"); | ||
| 228 | + } | ||
| 229 | + if (bytes.length > 1024 * 1024) { // 1MB 限制 | ||
| 230 | + throw new IllegalArgumentException("数据包过大,超过1MB限制"); | ||
| 231 | + } | ||
| 232 | + } | ||
| 233 | + | ||
| 234 | + /** | ||
| 235 | + * 安全获取功能码 | ||
| 236 | + */ | ||
| 237 | + private short getCodeByMethodSafely(String method) { | ||
| 238 | + Short code = METHOD_TO_CODE_MAP.get(method); | ||
| 239 | + if (code == null) { | ||
| 240 | + log.warn("[getCodeByMethodSafely][未知的消息方法: {},使用默认功能码]", method); | ||
| 241 | + return TcpDataPackage.CODE_DATA_UP; // 默认为数据上报 | ||
| 242 | + } | ||
| 243 | + return code; | ||
| 244 | + } | ||
| 245 | + | ||
| 246 | + /** | ||
| 247 | + * 安全获取消息方法 | ||
| 248 | + */ | ||
| 249 | + private String getMethodByCodeSafely(short code) { | ||
| 250 | + String method = CODE_TO_METHOD_MAP.get(code); | ||
| 251 | + if (method == null) { | ||
| 252 | + log.warn("[getMethodByCodeSafely][未知的功能码: {},使用默认方法]", code); | ||
| 253 | + return MessageMethod.PROPERTY_POST; // 默认为属性上报 | ||
| 254 | + } | ||
| 255 | + return method; | ||
| 256 | + } | ||
| 257 | + | ||
| 258 | + /** | ||
| 259 | + * 优化的负载构建 | ||
| 260 | + */ | ||
| 261 | + private String buildPayloadOptimized(IotDeviceMessage message) { | ||
| 262 | + // 使用缓存键 | ||
| 263 | + String cacheKey = message.getMethod() + "_" + message.getRequestId(); | ||
| 264 | + JSONObject cachedPayload = jsonCache.get(cacheKey); | ||
| 265 | + | ||
| 266 | + if (cachedPayload != null) { | ||
| 267 | + // 更新时间戳 | ||
| 268 | + cachedPayload.set(PayloadField.TIMESTAMP, System.currentTimeMillis()); | ||
| 269 | + return cachedPayload.toString(); | ||
| 270 | + } | ||
| 271 | + | ||
| 272 | + // 创建新的负载 | ||
| 273 | + JSONObject payload = new JSONObject(); | ||
| 274 | + | ||
| 275 | + // 添加基础字段 | ||
| 276 | + addToPayloadIfNotNull(payload, PayloadField.MESSAGE_ID, message.getRequestId()); | ||
| 277 | + addToPayloadIfNotNull(payload, PayloadField.DEVICE_ID, message.getDeviceId()); | ||
| 278 | + addToPayloadIfNotNull(payload, PayloadField.PARAMS, message.getParams()); | ||
| 279 | + addToPayloadIfNotNull(payload, PayloadField.DATA, message.getData()); | ||
| 280 | + addToPayloadIfNotNull(payload, PayloadField.CODE, message.getCode()); | ||
| 281 | + addToPayloadIfNotEmpty(payload, PayloadField.MESSAGE, message.getMsg()); | ||
| 282 | + | ||
| 283 | + // 添加时间戳 | ||
| 284 | + payload.set(PayloadField.TIMESTAMP, System.currentTimeMillis()); | ||
| 285 | + | ||
| 286 | + // 缓存管理 | ||
| 287 | + if (jsonCache.size() < MAX_CACHE_SIZE) { | ||
| 288 | + jsonCache.put(cacheKey, payload); | ||
| 289 | + } else { | ||
| 290 | + cleanJsonCacheIfNeeded(); | ||
| 291 | + } | ||
| 292 | + | ||
| 293 | + return payload.toString(); | ||
| 294 | + } | ||
| 295 | + | ||
| 296 | + /** | ||
| 297 | + * 优化的负载解析 | ||
| 298 | + */ | ||
| 299 | + private Object parsePayloadOptimized(String payload) { | ||
| 300 | + if (StrUtil.isEmpty(payload)) { | ||
| 301 | + return null; | ||
| 302 | + } | ||
| 303 | + | ||
| 304 | + try { | ||
| 305 | + // 尝试从缓存获取 | ||
| 306 | + JSONObject cachedJson = jsonCache.get(payload); | ||
| 307 | + if (cachedJson != null) { | ||
| 308 | + return cachedJson.containsKey(PayloadField.PARAMS) ? cachedJson.get(PayloadField.PARAMS) : cachedJson; | ||
| 309 | + } | ||
| 310 | + | ||
| 311 | + // 解析 JSON 对象 | ||
| 312 | + JSONObject jsonObject = JSONUtil.parseObj(payload); | ||
| 313 | + | ||
| 314 | + // 缓存解析结果 | ||
| 315 | + if (jsonCache.size() < MAX_CACHE_SIZE) { | ||
| 316 | + jsonCache.put(payload, jsonObject); | ||
| 317 | + } | ||
| 318 | + | ||
| 319 | + return jsonObject.containsKey(PayloadField.PARAMS) ? jsonObject.get(PayloadField.PARAMS) : jsonObject; | ||
| 320 | + | ||
| 321 | + } catch (JSONException e) { | ||
| 322 | + log.warn("[parsePayloadOptimized][负载解析为JSON失败,返回原始字符串] 负载: {}", payload); | ||
| 323 | + return payload; | ||
| 324 | + } catch (Exception e) { | ||
| 325 | + log.error("[parsePayloadOptimized][负载解析异常] 负载: {}", payload, e); | ||
| 326 | + return payload; | ||
| 327 | + } | ||
| 328 | + } | ||
| 329 | + | ||
| 330 | + /** | ||
| 331 | + * 添加非空值到负载 | ||
| 332 | + */ | ||
| 333 | + private void addToPayloadIfNotNull(JSONObject json, String key, Object value) { | ||
| 334 | + if (ObjectUtil.isNotNull(value)) { | ||
| 335 | + json.set(key, value); | ||
| 336 | + } | ||
| 337 | + } | ||
| 338 | + | ||
| 339 | + /** | ||
| 340 | + * 添加非空字符串到负载 | ||
| 341 | + */ | ||
| 342 | + private void addToPayloadIfNotEmpty(JSONObject json, String key, String value) { | ||
| 343 | + if (StrUtil.isNotEmpty(value)) { | ||
| 344 | + json.set(key, value); | ||
| 345 | + } | ||
| 346 | + } | ||
| 347 | + | ||
| 348 | + /** | ||
| 349 | + * 清理JSON缓存 | ||
| 350 | + */ | ||
| 351 | + private void cleanJsonCacheIfNeeded() { | ||
| 352 | + if (jsonCache.size() > MAX_CACHE_SIZE) { | ||
| 353 | + // 清理一半的缓存 | ||
| 354 | + int clearCount = jsonCache.size() / 2; | ||
| 355 | + jsonCache.entrySet().removeIf(entry -> clearCount > 0 && Math.random() < 0.5); | ||
| 356 | + | ||
| 357 | + if (log.isDebugEnabled()) { | ||
| 358 | + log.debug("[cleanJsonCacheIfNeeded][JSON 缓存已清理] 当前缓存大小: {}", jsonCache.size()); | ||
| 359 | + } | ||
| 360 | + } | ||
| 361 | + } | ||
| 362 | + | ||
| 363 | + /** | ||
| 364 | + * 截断数据用于日志输出 | ||
| 365 | + */ | ||
| 366 | + private String truncateData(byte[] data, int maxLength) { | ||
| 367 | + if (data.length <= maxLength) { | ||
| 368 | + return new String(data, StandardCharsets.UTF_8); | ||
| 369 | + } | ||
| 370 | + | ||
| 371 | + byte[] truncated = new byte[maxLength]; | ||
| 372 | + System.arraycopy(data, 0, truncated, 0, maxLength); | ||
| 373 | + return new String(truncated, StandardCharsets.UTF_8) + "...(截断)"; | ||
| 374 | + } | ||
| 375 | + | ||
| 376 | + // ==================== 自定义异常 ==================== | ||
| 377 | + | ||
| 378 | + /** | ||
| 379 | + * TCP 编解码异常 | ||
| 380 | + */ | ||
| 381 | + public static class TcpCodecException extends RuntimeException { | ||
| 382 | + public TcpCodecException(String message) { | ||
| 383 | + super(message); | ||
| 384 | + } | ||
| 385 | + | ||
| 386 | + public TcpCodecException(String message, Throwable cause) { | ||
| 387 | + super(message, cause); | ||
| 388 | + } | ||
| 389 | + } | ||
| 390 | +} | ||
| 0 | \ No newline at end of file | 391 | \ No newline at end of file |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayConfiguration.java
| @@ -7,10 +7,9 @@ import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxDownstreamSubscr | @@ -7,10 +7,9 @@ import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxDownstreamSubscr | ||
| 7 | import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxUpstreamProtocol; | 7 | import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxUpstreamProtocol; |
| 8 | import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpDownstreamSubscriber; | 8 | import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpDownstreamSubscriber; |
| 9 | import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpUpstreamProtocol; | 9 | import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpUpstreamProtocol; |
| 10 | -import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpConnectionManager; | ||
| 11 | -import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.router.IotTcpDownstreamHandler; | ||
| 12 | import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpDownstreamSubscriber; | 10 | import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpDownstreamSubscriber; |
| 13 | import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpUpstreamProtocol; | 11 | import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpUpstreamProtocol; |
| 12 | +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.manager.TcpDeviceConnectionManager; | ||
| 14 | import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService; | 13 | import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService; |
| 15 | import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; | 14 | import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; |
| 16 | import io.vertx.core.Vertx; | 15 | import io.vertx.core.Vertx; |
| @@ -85,26 +84,33 @@ public class IotGatewayConfiguration { | @@ -85,26 +84,33 @@ public class IotGatewayConfiguration { | ||
| 85 | @Slf4j | 84 | @Slf4j |
| 86 | public static class TcpProtocolConfiguration { | 85 | public static class TcpProtocolConfiguration { |
| 87 | 86 | ||
| 88 | - // TODO @haohao:close | ||
| 89 | - @Bean | 87 | + @Bean(destroyMethod = "close") |
| 90 | public Vertx tcpVertx() { | 88 | public Vertx tcpVertx() { |
| 91 | return Vertx.vertx(); | 89 | return Vertx.vertx(); |
| 92 | } | 90 | } |
| 93 | 91 | ||
| 94 | @Bean | 92 | @Bean |
| 95 | - public IotTcpUpstreamProtocol iotTcpUpstreamProtocol(Vertx tcpVertx, IotGatewayProperties gatewayProperties, | ||
| 96 | - IotTcpConnectionManager connectionManager, | 93 | + public TcpDeviceConnectionManager tcpDeviceConnectionManager() { |
| 94 | + return new TcpDeviceConnectionManager(); | ||
| 95 | + } | ||
| 96 | + | ||
| 97 | + @Bean | ||
| 98 | + public IotTcpUpstreamProtocol iotTcpUpstreamProtocol(IotGatewayProperties gatewayProperties, | ||
| 99 | + TcpDeviceConnectionManager connectionManager, | ||
| 100 | + IotDeviceService deviceService, | ||
| 97 | IotDeviceMessageService messageService, | 101 | IotDeviceMessageService messageService, |
| 98 | - IotDeviceService deviceService, IotDeviceCommonApi deviceApi) { | ||
| 99 | - return new IotTcpUpstreamProtocol(tcpVertx, gatewayProperties, connectionManager, | ||
| 100 | - messageService, deviceService, deviceApi); | 102 | + IotDeviceCommonApi deviceApi, |
| 103 | + Vertx tcpVertx) { | ||
| 104 | + return new IotTcpUpstreamProtocol(gatewayProperties.getProtocol().getTcp(), connectionManager, | ||
| 105 | + deviceService, messageService, deviceApi, tcpVertx); | ||
| 101 | } | 106 | } |
| 102 | 107 | ||
| 103 | @Bean | 108 | @Bean |
| 104 | - public IotTcpDownstreamSubscriber iotTcpDownstreamSubscriber(IotTcpUpstreamProtocol tcpUpstreamProtocol, | ||
| 105 | - IotMessageBus messageBus, | ||
| 106 | - IotTcpDownstreamHandler downstreamHandler) { | ||
| 107 | - return new IotTcpDownstreamSubscriber(tcpUpstreamProtocol, messageBus, downstreamHandler); | 109 | + public IotTcpDownstreamSubscriber iotTcpDownstreamSubscriber(IotTcpUpstreamProtocol protocolHandler, |
| 110 | + TcpDeviceConnectionManager connectionManager, | ||
| 111 | + IotDeviceMessageService messageService, | ||
| 112 | + IotMessageBus messageBus) { | ||
| 113 | + return new IotTcpDownstreamSubscriber(protocolHandler, connectionManager, messageService, messageBus); | ||
| 108 | } | 114 | } |
| 109 | 115 | ||
| 110 | } | 116 | } |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayProperties.java
| @@ -116,22 +116,6 @@ public class IotGatewayProperties { | @@ -116,22 +116,6 @@ public class IotGatewayProperties { | ||
| 116 | } | 116 | } |
| 117 | 117 | ||
| 118 | @Data | 118 | @Data |
| 119 | - public static class TcpProperties { | ||
| 120 | - | ||
| 121 | - /** | ||
| 122 | - * 是否开启 | ||
| 123 | - */ | ||
| 124 | - @NotNull(message = "是否开启不能为空") | ||
| 125 | - private Boolean enabled; | ||
| 126 | - | ||
| 127 | - /** | ||
| 128 | - * 服务端口(默认:8093) | ||
| 129 | - */ | ||
| 130 | - private Integer serverPort = 8093; | ||
| 131 | - | ||
| 132 | - } | ||
| 133 | - | ||
| 134 | - @Data | ||
| 135 | public static class EmqxProperties { | 119 | public static class EmqxProperties { |
| 136 | 120 | ||
| 137 | /** | 121 | /** |
| @@ -300,4 +284,45 @@ public class IotGatewayProperties { | @@ -300,4 +284,45 @@ public class IotGatewayProperties { | ||
| 300 | 284 | ||
| 301 | } | 285 | } |
| 302 | 286 | ||
| 287 | + @Data | ||
| 288 | + public static class TcpProperties { | ||
| 289 | + | ||
| 290 | + /** | ||
| 291 | + * 是否开启 | ||
| 292 | + */ | ||
| 293 | + @NotNull(message = "是否开启不能为空") | ||
| 294 | + private Boolean enabled; | ||
| 295 | + | ||
| 296 | + /** | ||
| 297 | + * 服务器端口 | ||
| 298 | + */ | ||
| 299 | + private Integer port = 8091; | ||
| 300 | + | ||
| 301 | + /** | ||
| 302 | + * 心跳超时时间(毫秒) | ||
| 303 | + */ | ||
| 304 | + private Long keepAliveTimeoutMs = 30000L; | ||
| 305 | + | ||
| 306 | + /** | ||
| 307 | + * 最大连接数 | ||
| 308 | + */ | ||
| 309 | + private Integer maxConnections = 1000; | ||
| 310 | + | ||
| 311 | + /** | ||
| 312 | + * 是否启用SSL | ||
| 313 | + */ | ||
| 314 | + private Boolean sslEnabled = false; | ||
| 315 | + | ||
| 316 | + /** | ||
| 317 | + * SSL证书路径 | ||
| 318 | + */ | ||
| 319 | + private String sslCertPath; | ||
| 320 | + | ||
| 321 | + /** | ||
| 322 | + * SSL私钥路径 | ||
| 323 | + */ | ||
| 324 | + private String sslKeyPath; | ||
| 325 | + | ||
| 326 | + } | ||
| 327 | + | ||
| 303 | } | 328 | } |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpConnectionManager.java deleted
| 1 | -package cn.iocoder.yudao.module.iot.gateway.protocol.tcp; | ||
| 2 | - | ||
| 3 | -import io.vertx.core.net.NetSocket; | ||
| 4 | -import lombok.extern.slf4j.Slf4j; | ||
| 5 | -import org.springframework.stereotype.Component; | ||
| 6 | - | ||
| 7 | -import java.util.concurrent.ConcurrentHashMap; | ||
| 8 | -import java.util.concurrent.ConcurrentMap; | ||
| 9 | - | ||
| 10 | -/** | ||
| 11 | - * IoT TCP 连接管理器 | ||
| 12 | - * | ||
| 13 | - * @author 芋道源码 | ||
| 14 | - */ | ||
| 15 | -@Component | ||
| 16 | -@Slf4j | ||
| 17 | -public class IotTcpConnectionManager { | ||
| 18 | - | ||
| 19 | - // TODO @haohao:要考虑,相同设备,多次连接的情况哇? | ||
| 20 | - /** | ||
| 21 | - * 连接集合 | ||
| 22 | - * | ||
| 23 | - * key:设备唯一标识 | ||
| 24 | - */ | ||
| 25 | - private final ConcurrentMap<String, NetSocket> connectionMap = new ConcurrentHashMap<>(); | ||
| 26 | - | ||
| 27 | - /** | ||
| 28 | - * 添加一个新连接 | ||
| 29 | - * | ||
| 30 | - * @param deviceId 设备唯一标识 | ||
| 31 | - * @param socket Netty Channel | ||
| 32 | - */ | ||
| 33 | - public void addConnection(String deviceId, NetSocket socket) { | ||
| 34 | - log.info("[addConnection][设备({}) 连接({})]", deviceId, socket.remoteAddress()); | ||
| 35 | - connectionMap.put(deviceId, socket); | ||
| 36 | - } | ||
| 37 | - | ||
| 38 | - /** | ||
| 39 | - * 根据设备 ID 获取连接 | ||
| 40 | - * | ||
| 41 | - * @param deviceId 设备 ID | ||
| 42 | - * @return 连接 | ||
| 43 | - */ | ||
| 44 | - public NetSocket getConnection(String deviceId) { | ||
| 45 | - return connectionMap.get(deviceId); | ||
| 46 | - } | ||
| 47 | - | ||
| 48 | - /** | ||
| 49 | - * 移除指定连接 | ||
| 50 | - * | ||
| 51 | - * @param socket Netty Channel | ||
| 52 | - */ | ||
| 53 | - public void removeConnection(NetSocket socket) { | ||
| 54 | - // TODO @haohao:vertx 的 socket,有没办法设置一些属性,类似 netty 的;目的是,避免遍历 connectionMap 去操作哈; | ||
| 55 | - connectionMap.entrySet().stream() | ||
| 56 | - .filter(entry -> entry.getValue().equals(socket)) | ||
| 57 | - .findFirst() | ||
| 58 | - .ifPresent(entry -> { | ||
| 59 | - log.info("[removeConnection][设备({}) 断开连接({})]", entry.getKey(), socket.remoteAddress()); | ||
| 60 | - connectionMap.remove(entry.getKey()); | ||
| 61 | - }); | ||
| 62 | - } | ||
| 63 | - | ||
| 64 | -} | ||
| 65 | \ No newline at end of file | 0 | \ No newline at end of file |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpDownstreamSubscriber.java
| 1 | -package cn.iocoder.yudao.module.iot.gateway.protocol.tcp; | ||
| 2 | - | ||
| 3 | -import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; | ||
| 4 | -import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageSubscriber; | ||
| 5 | -import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; | ||
| 6 | -import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; | ||
| 7 | -import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.router.IotTcpDownstreamHandler; | ||
| 8 | -import jakarta.annotation.PostConstruct; | ||
| 9 | -import lombok.RequiredArgsConstructor; | ||
| 10 | -import lombok.extern.slf4j.Slf4j; | ||
| 11 | - | ||
| 12 | -/** | ||
| 13 | - * IoT 网关 TCP 订阅者:接收下行给设备的消息 | ||
| 14 | - * | ||
| 15 | - * @author 芋道源码 | ||
| 16 | - */ | ||
| 17 | -@RequiredArgsConstructor | ||
| 18 | -@Slf4j | ||
| 19 | -public class IotTcpDownstreamSubscriber implements IotMessageSubscriber<IotDeviceMessage> { | ||
| 20 | - | ||
| 21 | - private final IotTcpUpstreamProtocol protocol; | ||
| 22 | - | ||
| 23 | - private final IotMessageBus messageBus; | ||
| 24 | - | ||
| 25 | - private final IotTcpDownstreamHandler downstreamHandler; | ||
| 26 | - | ||
| 27 | - @PostConstruct | ||
| 28 | - public void init() { | ||
| 29 | - messageBus.register(this); | ||
| 30 | - } | ||
| 31 | - | ||
| 32 | - @Override | ||
| 33 | - public String getTopic() { | ||
| 34 | - return IotDeviceMessageUtils.buildMessageBusGatewayDeviceMessageTopic(protocol.getServerId()); | ||
| 35 | - } | ||
| 36 | - | ||
| 37 | - @Override | ||
| 38 | - public String getGroup() { | ||
| 39 | - // 保证点对点消费,需要保证独立的 Group,所以使用 Topic 作为 Group | ||
| 40 | - return getTopic(); | ||
| 41 | - } | ||
| 42 | - | ||
| 43 | - @Override | ||
| 44 | - public void onMessage(IotDeviceMessage message) { | ||
| 45 | - log.debug("[onMessage][接收到下行消息, messageId: {}, method: {}, deviceId: {}]", | ||
| 46 | - message.getId(), message.getMethod(), message.getDeviceId()); | ||
| 47 | - try { | ||
| 48 | - // 1. 校验 | ||
| 49 | - String method = message.getMethod(); | ||
| 50 | - if (method == null) { | ||
| 51 | - log.warn("[onMessage][消息方法为空, messageId: {}, deviceId: {}]", | ||
| 52 | - message.getId(), message.getDeviceId()); | ||
| 53 | - return; | ||
| 54 | - } | ||
| 55 | - | ||
| 56 | - // 2. 处理下行消息 | ||
| 57 | - downstreamHandler.handle(message); | ||
| 58 | - } catch (Exception e) { | ||
| 59 | - log.error("[onMessage][处理下行消息失败, messageId: {}, method: {}, deviceId: {}]", | ||
| 60 | - message.getId(), message.getMethod(), message.getDeviceId(), e); | ||
| 61 | - } | ||
| 62 | - } | ||
| 63 | - | 1 | +package cn.iocoder.yudao.module.iot.gateway.protocol.tcp; |
| 2 | + | ||
| 3 | +import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; | ||
| 4 | +import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageSubscriber; | ||
| 5 | +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; | ||
| 6 | +import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; | ||
| 7 | +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.manager.TcpDeviceConnectionManager; | ||
| 8 | +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.router.IotTcpDownstreamHandler; | ||
| 9 | +import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; | ||
| 10 | +import jakarta.annotation.PostConstruct; | ||
| 11 | +import jakarta.annotation.PreDestroy; | ||
| 12 | +import lombok.RequiredArgsConstructor; | ||
| 13 | +import lombok.extern.slf4j.Slf4j; | ||
| 14 | + | ||
| 15 | +import java.util.concurrent.atomic.AtomicBoolean; | ||
| 16 | +import java.util.concurrent.atomic.AtomicLong; | ||
| 17 | + | ||
| 18 | +/** | ||
| 19 | + * IoT 网关 TCP 下游订阅者:接收下行给设备的消息 | ||
| 20 | + * <p> | ||
| 21 | + * 参考 EMQX 设计理念: | ||
| 22 | + * 1. 高性能消息路由 | ||
| 23 | + * 2. 容错机制 | ||
| 24 | + * 3. 状态监控 | ||
| 25 | + * 4. 资源管理 | ||
| 26 | + * | ||
| 27 | + * @author 芋道源码 | ||
| 28 | + */ | ||
| 29 | +@RequiredArgsConstructor | ||
| 30 | +@Slf4j | ||
| 31 | +public class IotTcpDownstreamSubscriber implements IotMessageSubscriber<IotDeviceMessage> { | ||
| 32 | + | ||
| 33 | + private final IotTcpUpstreamProtocol protocolHandler; | ||
| 34 | + | ||
| 35 | + private final TcpDeviceConnectionManager connectionManager; | ||
| 36 | + | ||
| 37 | + private final IotDeviceMessageService messageService; | ||
| 38 | + | ||
| 39 | + private final IotMessageBus messageBus; | ||
| 40 | + | ||
| 41 | + private volatile IotTcpDownstreamHandler downstreamHandler; | ||
| 42 | + | ||
| 43 | + private final AtomicBoolean initialized = new AtomicBoolean(false); | ||
| 44 | + | ||
| 45 | + private final AtomicLong processedMessages = new AtomicLong(0); | ||
| 46 | + | ||
| 47 | + private final AtomicLong failedMessages = new AtomicLong(0); | ||
| 48 | + | ||
| 49 | + @PostConstruct | ||
| 50 | + public void init() { | ||
| 51 | + if (!initialized.compareAndSet(false, true)) { | ||
| 52 | + log.warn("[init][TCP 下游消息订阅者已初始化,跳过重复初始化]"); | ||
| 53 | + return; | ||
| 54 | + } | ||
| 55 | + | ||
| 56 | + try { | ||
| 57 | + // 初始化下游处理器 | ||
| 58 | + downstreamHandler = new IotTcpDownstreamHandler(connectionManager, messageService); | ||
| 59 | + | ||
| 60 | + // 注册到消息总线 | ||
| 61 | + messageBus.register(this); | ||
| 62 | + | ||
| 63 | + log.info("[init][TCP 下游消息订阅者初始化完成] Topic: {}, Group: {}", | ||
| 64 | + getTopic(), getGroup()); | ||
| 65 | + } catch (Exception e) { | ||
| 66 | + initialized.set(false); | ||
| 67 | + log.error("[init][TCP 下游消息订阅者初始化失败]", e); | ||
| 68 | + throw new RuntimeException("TCP 下游消息订阅者初始化失败", e); | ||
| 69 | + } | ||
| 70 | + } | ||
| 71 | + | ||
| 72 | + @PreDestroy | ||
| 73 | + public void destroy() { | ||
| 74 | + if (!initialized.get()) { | ||
| 75 | + return; | ||
| 76 | + } | ||
| 77 | + | ||
| 78 | + try { | ||
| 79 | + log.info("[destroy][TCP 下游消息订阅者已关闭] 处理消息数: {}, 失败消息数: {}", | ||
| 80 | + processedMessages.get(), failedMessages.get()); | ||
| 81 | + } catch (Exception e) { | ||
| 82 | + log.error("[destroy][TCP 下游消息订阅者关闭失败]", e); | ||
| 83 | + } finally { | ||
| 84 | + initialized.set(false); | ||
| 85 | + } | ||
| 86 | + } | ||
| 87 | + | ||
| 88 | + @Override | ||
| 89 | + public String getTopic() { | ||
| 90 | + return IotDeviceMessageUtils.buildMessageBusGatewayDeviceMessageTopic(protocolHandler.getServerId()); | ||
| 91 | + } | ||
| 92 | + | ||
| 93 | + @Override | ||
| 94 | + public String getGroup() { | ||
| 95 | + return "tcp-downstream-" + protocolHandler.getServerId(); | ||
| 96 | + } | ||
| 97 | + | ||
| 98 | + @Override | ||
| 99 | + public void onMessage(IotDeviceMessage message) { | ||
| 100 | + if (!initialized.get()) { | ||
| 101 | + log.warn("[onMessage][订阅者未初始化,跳过消息处理]"); | ||
| 102 | + return; | ||
| 103 | + } | ||
| 104 | + | ||
| 105 | + long startTime = System.currentTimeMillis(); | ||
| 106 | + | ||
| 107 | + try { | ||
| 108 | + processedMessages.incrementAndGet(); | ||
| 109 | + | ||
| 110 | + if (log.isDebugEnabled()) { | ||
| 111 | + log.debug("[onMessage][收到下行消息] 设备ID: {}, 方法: {}, 消息ID: {}", | ||
| 112 | + message.getDeviceId(), message.getMethod(), message.getId()); | ||
| 113 | + } | ||
| 114 | + | ||
| 115 | + // 参数校验 | ||
| 116 | + if (message.getDeviceId() == null) { | ||
| 117 | + log.warn("[onMessage][下行消息设备ID为空,跳过处理] 消息: {}", message); | ||
| 118 | + return; | ||
| 119 | + } | ||
| 120 | + | ||
| 121 | + // 检查连接状态 | ||
| 122 | + if (connectionManager.getClientByDeviceId(message.getDeviceId()) == null) { | ||
| 123 | + log.warn("[onMessage][设备({})离线,跳过下行消息] 方法: {}", | ||
| 124 | + message.getDeviceId(), message.getMethod()); | ||
| 125 | + return; | ||
| 126 | + } | ||
| 127 | + | ||
| 128 | + // 处理下行消息 | ||
| 129 | + downstreamHandler.handle(message); | ||
| 130 | + | ||
| 131 | + // 性能监控 | ||
| 132 | + long processTime = System.currentTimeMillis() - startTime; | ||
| 133 | + if (processTime > 1000) { // 超过1秒的慢消息 | ||
| 134 | + log.warn("[onMessage][慢消息处理] 设备ID: {}, 方法: {}, 耗时: {}ms", | ||
| 135 | + message.getDeviceId(), message.getMethod(), processTime); | ||
| 136 | + } | ||
| 137 | + | ||
| 138 | + } catch (Exception e) { | ||
| 139 | + failedMessages.incrementAndGet(); | ||
| 140 | + log.error("[onMessage][处理下行消息失败] 设备ID: {}, 方法: {}, 消息: {}", | ||
| 141 | + message.getDeviceId(), message.getMethod(), message, e); | ||
| 142 | + } | ||
| 143 | + } | ||
| 144 | + | ||
| 145 | + /** | ||
| 146 | + * 获取订阅者统计信息 | ||
| 147 | + */ | ||
| 148 | + public String getSubscriberStatistics() { | ||
| 149 | + return String.format("TCP下游订阅者 - 已处理: %d, 失败: %d, 成功率: %.2f%%", | ||
| 150 | + processedMessages.get(), | ||
| 151 | + failedMessages.get(), | ||
| 152 | + processedMessages.get() > 0 | ||
| 153 | + ? (double) (processedMessages.get() - failedMessages.get()) / processedMessages.get() * 100 | ||
| 154 | + : 0.0); | ||
| 155 | + } | ||
| 156 | + | ||
| 157 | + /** | ||
| 158 | + * 检查订阅者健康状态 | ||
| 159 | + */ | ||
| 160 | + public boolean isHealthy() { | ||
| 161 | + return initialized.get() && downstreamHandler != null; | ||
| 162 | + } | ||
| 64 | } | 163 | } |
| 65 | \ No newline at end of file | 164 | \ No newline at end of file |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpUpstreamProtocol.java
| 1 | -package cn.iocoder.yudao.module.iot.gateway.protocol.tcp; | ||
| 2 | - | ||
| 3 | -import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; | ||
| 4 | -import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; | ||
| 5 | -import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties; | ||
| 6 | -import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.router.IotTcpConnectionHandler; | ||
| 7 | -import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService; | ||
| 8 | -import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; | ||
| 9 | -import io.vertx.core.Vertx; | ||
| 10 | -import io.vertx.core.net.NetServer; | ||
| 11 | -import jakarta.annotation.PostConstruct; | ||
| 12 | -import jakarta.annotation.PreDestroy; | ||
| 13 | -import lombok.Getter; | ||
| 14 | -import lombok.RequiredArgsConstructor; | ||
| 15 | -import lombok.extern.slf4j.Slf4j; | ||
| 16 | - | ||
| 17 | -/** | ||
| 18 | - * IoT 网关 TCP 协议:接收设备上行消息 | ||
| 19 | - * | ||
| 20 | - * @author 芋道源码 | ||
| 21 | - */ | ||
| 22 | -@Slf4j | ||
| 23 | -@RequiredArgsConstructor | ||
| 24 | -public class IotTcpUpstreamProtocol { | ||
| 25 | - | ||
| 26 | - private final Vertx vertx; | ||
| 27 | - | ||
| 28 | - private final IotGatewayProperties gatewayProperties; | ||
| 29 | - | ||
| 30 | - private final IotTcpConnectionManager connectionManager; | ||
| 31 | - | ||
| 32 | - private final IotDeviceMessageService messageService; | ||
| 33 | - | ||
| 34 | - private final IotDeviceService deviceService; | ||
| 35 | - | ||
| 36 | - private final IotDeviceCommonApi deviceApi; | ||
| 37 | - | ||
| 38 | - @Getter | ||
| 39 | - private String serverId; | ||
| 40 | - | ||
| 41 | - private NetServer netServer; | ||
| 42 | - | ||
| 43 | - @PostConstruct | ||
| 44 | - public void start() { | ||
| 45 | - // 1. 初始化参数 | ||
| 46 | - IotGatewayProperties.TcpProperties tcpProperties = gatewayProperties.getProtocol().getTcp(); | ||
| 47 | - this.serverId = IotDeviceMessageUtils.generateServerId(tcpProperties.getServerPort()); | ||
| 48 | - | ||
| 49 | - // 2. 创建 TCP 服务器 | ||
| 50 | - netServer = vertx.createNetServer(); | ||
| 51 | - netServer.connectHandler(socket -> { | ||
| 52 | - new IotTcpConnectionHandler(socket, connectionManager, | ||
| 53 | - messageService, deviceService, deviceApi, serverId).start(); | ||
| 54 | - }); | ||
| 55 | - | ||
| 56 | - // 3. 启动 TCP 服务器 | ||
| 57 | - netServer.listen(tcpProperties.getServerPort()) | ||
| 58 | - .onSuccess(server -> log.info("[start][IoT 网关 TCP 服务启动成功,端口:{}]", server.actualPort())) | ||
| 59 | - .onFailure(e -> log.error("[start][IoT 网关 TCP 服务启动失败]", e)); | ||
| 60 | - } | ||
| 61 | - | ||
| 62 | - @PreDestroy | ||
| 63 | - public void stop() { | ||
| 64 | - if (netServer != null) { | ||
| 65 | - netServer.close() | ||
| 66 | - .onSuccess(v -> log.info("[stop][IoT 网关 TCP 服务已停止]")) | ||
| 67 | - .onFailure(e -> log.error("[stop][IoT 网关 TCP 服务停止失败]", e)); | ||
| 68 | - } | ||
| 69 | - } | ||
| 70 | - | 1 | +package cn.iocoder.yudao.module.iot.gateway.protocol.tcp; |
| 2 | + | ||
| 3 | +import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; | ||
| 4 | +import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; | ||
| 5 | +import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties; | ||
| 6 | +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.manager.TcpDeviceConnectionManager; | ||
| 7 | +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.router.IotTcpUpstreamHandler; | ||
| 8 | +import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService; | ||
| 9 | +import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; | ||
| 10 | +import io.vertx.core.Vertx; | ||
| 11 | +import io.vertx.core.net.NetServer; | ||
| 12 | +import io.vertx.core.net.NetServerOptions; | ||
| 13 | +import io.vertx.core.net.PemKeyCertOptions; | ||
| 14 | +import jakarta.annotation.PostConstruct; | ||
| 15 | +import jakarta.annotation.PreDestroy; | ||
| 16 | +import lombok.Getter; | ||
| 17 | +import lombok.extern.slf4j.Slf4j; | ||
| 18 | + | ||
| 19 | +import java.util.concurrent.CountDownLatch; | ||
| 20 | +import java.util.concurrent.TimeUnit; | ||
| 21 | +import java.util.concurrent.atomic.AtomicReference; | ||
| 22 | + | ||
| 23 | +/** | ||
| 24 | + * IoT 网关 TCP 协议:接收设备上行消息 | ||
| 25 | + * <p> | ||
| 26 | + * 负责接收设备上行消息,支持: | ||
| 27 | + * 1. 设备注册 | ||
| 28 | + * 2. 心跳保活 | ||
| 29 | + * 3. 属性上报 | ||
| 30 | + * 4. 事件上报 | ||
| 31 | + * 5. 设备连接管理 | ||
| 32 | + * | ||
| 33 | + * @author 芋道源码 | ||
| 34 | + */ | ||
| 35 | +@Slf4j | ||
| 36 | +public class IotTcpUpstreamProtocol { | ||
| 37 | + | ||
| 38 | + private final IotGatewayProperties.TcpProperties tcpProperties; | ||
| 39 | + | ||
| 40 | + private final TcpDeviceConnectionManager connectionManager; | ||
| 41 | + | ||
| 42 | + private final IotDeviceService deviceService; | ||
| 43 | + | ||
| 44 | + private final IotDeviceMessageService messageService; | ||
| 45 | + | ||
| 46 | + private final IotDeviceCommonApi deviceApi; | ||
| 47 | + | ||
| 48 | + private final Vertx vertx; | ||
| 49 | + | ||
| 50 | + @Getter | ||
| 51 | + private final String serverId; | ||
| 52 | + | ||
| 53 | + private NetServer netServer; | ||
| 54 | + | ||
| 55 | + public IotTcpUpstreamProtocol(IotGatewayProperties.TcpProperties tcpProperties, | ||
| 56 | + TcpDeviceConnectionManager connectionManager, | ||
| 57 | + IotDeviceService deviceService, | ||
| 58 | + IotDeviceMessageService messageService, | ||
| 59 | + IotDeviceCommonApi deviceApi, | ||
| 60 | + Vertx vertx) { | ||
| 61 | + this.tcpProperties = tcpProperties; | ||
| 62 | + this.connectionManager = connectionManager; | ||
| 63 | + this.deviceService = deviceService; | ||
| 64 | + this.messageService = messageService; | ||
| 65 | + this.deviceApi = deviceApi; | ||
| 66 | + this.vertx = vertx; | ||
| 67 | + this.serverId = IotDeviceMessageUtils.generateServerId(tcpProperties.getPort()); | ||
| 68 | + } | ||
| 69 | + | ||
| 70 | + @PostConstruct | ||
| 71 | + public void start() { | ||
| 72 | + // 1. 启动 TCP 服务器 | ||
| 73 | + try { | ||
| 74 | + startTcpServer(); | ||
| 75 | + log.info("[start][IoT 网关 TCP 协议处理器启动完成,服务器ID: {}]", serverId); | ||
| 76 | + } catch (Exception e) { | ||
| 77 | + log.error("[start][IoT 网关 TCP 协议处理器启动失败]", e); | ||
| 78 | + // 抛出异常,中断 Spring 容器启动 | ||
| 79 | + throw new RuntimeException("IoT 网关 TCP 协议处理器启动失败", e); | ||
| 80 | + } | ||
| 81 | + } | ||
| 82 | + | ||
| 83 | + @PreDestroy | ||
| 84 | + public void stop() { | ||
| 85 | + if (netServer != null) { | ||
| 86 | + stopTcpServer(); | ||
| 87 | + log.info("[stop][IoT 网关 TCP 协议处理器已停止]"); | ||
| 88 | + } | ||
| 89 | + } | ||
| 90 | + | ||
| 91 | + /** | ||
| 92 | + * 启动 TCP 服务器 | ||
| 93 | + */ | ||
| 94 | + private void startTcpServer() { | ||
| 95 | + // 1. 创建服务器选项 | ||
| 96 | + NetServerOptions options = new NetServerOptions() | ||
| 97 | + .setPort(tcpProperties.getPort()) | ||
| 98 | + .setTcpKeepAlive(true) | ||
| 99 | + .setTcpNoDelay(true) | ||
| 100 | + .setReuseAddress(true); | ||
| 101 | + | ||
| 102 | + // 2. 配置 SSL(如果启用) | ||
| 103 | + if (Boolean.TRUE.equals(tcpProperties.getSslEnabled())) { | ||
| 104 | + PemKeyCertOptions pemKeyCertOptions = new PemKeyCertOptions() | ||
| 105 | + .setKeyPath(tcpProperties.getSslKeyPath()) | ||
| 106 | + .setCertPath(tcpProperties.getSslCertPath()); | ||
| 107 | + options.setSsl(true).setKeyCertOptions(pemKeyCertOptions); | ||
| 108 | + } | ||
| 109 | + | ||
| 110 | + // 3. 创建 TCP 服务器 | ||
| 111 | + netServer = vertx.createNetServer(options); | ||
| 112 | + | ||
| 113 | + // 4. 设置连接处理器 | ||
| 114 | + netServer.connectHandler(socket -> { | ||
| 115 | + log.info("[startTcpServer][新设备连接: {}]", socket.remoteAddress()); | ||
| 116 | + IotTcpUpstreamHandler handler = new IotTcpUpstreamHandler( | ||
| 117 | + tcpProperties, connectionManager, deviceService, messageService, deviceApi, serverId); | ||
| 118 | + handler.handle(socket); | ||
| 119 | + }); | ||
| 120 | + | ||
| 121 | + // 5. 同步启动服务器,等待结果 | ||
| 122 | + CountDownLatch latch = new CountDownLatch(1); | ||
| 123 | + AtomicReference<Throwable> failure = new AtomicReference<>(); | ||
| 124 | + netServer.listen(result -> { | ||
| 125 | + if (result.succeeded()) { | ||
| 126 | + log.info("[startTcpServer][TCP 服务器启动成功] 端口: {}, 服务器ID: {}", | ||
| 127 | + result.result().actualPort(), serverId); | ||
| 128 | + } else { | ||
| 129 | + log.error("[startTcpServer][TCP 服务器启动失败]", result.cause()); | ||
| 130 | + failure.set(result.cause()); | ||
| 131 | + } | ||
| 132 | + latch.countDown(); | ||
| 133 | + }); | ||
| 134 | + | ||
| 135 | + // 6. 等待启动结果,设置超时 | ||
| 136 | + try { | ||
| 137 | + if (!latch.await(10, TimeUnit.SECONDS)) { | ||
| 138 | + throw new RuntimeException("TCP 服务器启动超时"); | ||
| 139 | + } | ||
| 140 | + if (failure.get() != null) { | ||
| 141 | + throw new RuntimeException("TCP 服务器启动失败", failure.get()); | ||
| 142 | + } | ||
| 143 | + } catch (InterruptedException e) { | ||
| 144 | + Thread.currentThread().interrupt(); | ||
| 145 | + throw new RuntimeException("TCP 服务器启动被中断", e); | ||
| 146 | + } | ||
| 147 | + } | ||
| 148 | + | ||
| 149 | + /** | ||
| 150 | + * 停止 TCP 服务器 | ||
| 151 | + */ | ||
| 152 | + private void stopTcpServer() { | ||
| 153 | + if (netServer == null) { | ||
| 154 | + return; | ||
| 155 | + } | ||
| 156 | + log.info("[stopTcpServer][准备关闭 TCP 服务器]"); | ||
| 157 | + CountDownLatch latch = new CountDownLatch(1); | ||
| 158 | + // 异步关闭,并使用 Latch 等待结果 | ||
| 159 | + netServer.close(result -> { | ||
| 160 | + if (result.succeeded()) { | ||
| 161 | + log.info("[stopTcpServer][IoT 网关 TCP 协议处理器已停止]"); | ||
| 162 | + } else { | ||
| 163 | + log.warn("[stopTcpServer][TCP 服务器关闭失败]", result.cause()); | ||
| 164 | + } | ||
| 165 | + latch.countDown(); | ||
| 166 | + }); | ||
| 167 | + | ||
| 168 | + try { | ||
| 169 | + // 等待关闭完成,设置超时 | ||
| 170 | + if (!latch.await(10, TimeUnit.SECONDS)) { | ||
| 171 | + log.warn("[stopTcpServer][关闭 TCP 服务器超时]"); | ||
| 172 | + } | ||
| 173 | + } catch (InterruptedException e) { | ||
| 174 | + Thread.currentThread().interrupt(); | ||
| 175 | + log.warn("[stopTcpServer][等待 TCP 服务器关闭被中断]", e); | ||
| 176 | + } | ||
| 177 | + } | ||
| 71 | } | 178 | } |
| 72 | \ No newline at end of file | 179 | \ No newline at end of file |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/client/TcpDeviceClient.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.iot.gateway.protocol.tcp.client; | ||
| 2 | + | ||
| 3 | +import io.vertx.core.buffer.Buffer; | ||
| 4 | +import io.vertx.core.net.NetSocket; | ||
| 5 | +import io.vertx.core.parsetools.RecordParser; | ||
| 6 | +import lombok.Getter; | ||
| 7 | +import lombok.Setter; | ||
| 8 | +import lombok.extern.slf4j.Slf4j; | ||
| 9 | + | ||
| 10 | +import java.util.concurrent.atomic.AtomicBoolean; | ||
| 11 | + | ||
| 12 | +/** | ||
| 13 | + * TCP 设备客户端 | ||
| 14 | + * <p> | ||
| 15 | + * 封装设备连接的基本信息和操作。 | ||
| 16 | + * 该类中的状态变更(如 authenticated, closed)使用 AtomicBoolean 保证原子性。 | ||
| 17 | + * 对 socket 的操作应在 Vert.x Event Loop 线程中执行,以避免并发问题。 | ||
| 18 | + * | ||
| 19 | + * @author 芋道源码 | ||
| 20 | + */ | ||
| 21 | +@Slf4j | ||
| 22 | +public class TcpDeviceClient { | ||
| 23 | + | ||
| 24 | + @Getter | ||
| 25 | + private final String clientId; | ||
| 26 | + | ||
| 27 | + @Getter | ||
| 28 | + @Setter | ||
| 29 | + private String deviceAddr; // 从 final 移除,因为在注册后才设置 | ||
| 30 | + | ||
| 31 | + @Getter | ||
| 32 | + @Setter | ||
| 33 | + private String productKey; | ||
| 34 | + | ||
| 35 | + @Getter | ||
| 36 | + @Setter | ||
| 37 | + private String deviceName; | ||
| 38 | + | ||
| 39 | + @Getter | ||
| 40 | + @Setter | ||
| 41 | + private Long deviceId; | ||
| 42 | + | ||
| 43 | + @Getter | ||
| 44 | + private NetSocket socket; | ||
| 45 | + | ||
| 46 | + @Getter | ||
| 47 | + @Setter | ||
| 48 | + private RecordParser parser; | ||
| 49 | + | ||
| 50 | + @Getter | ||
| 51 | + private final long keepAliveTimeoutMs; // 改为 final,通过构造函数注入 | ||
| 52 | + | ||
| 53 | + private volatile long lastKeepAliveTime; | ||
| 54 | + | ||
| 55 | + private final AtomicBoolean authenticated = new AtomicBoolean(false); | ||
| 56 | + private final AtomicBoolean closed = new AtomicBoolean(false); | ||
| 57 | + | ||
| 58 | + /** | ||
| 59 | + * 构造函数 | ||
| 60 | + * | ||
| 61 | + * @param clientId 客户端ID,全局唯一 | ||
| 62 | + * @param keepAliveTimeoutMs 心跳超时时间(毫秒),从配置中读取 | ||
| 63 | + */ | ||
| 64 | + public TcpDeviceClient(String clientId, long keepAliveTimeoutMs) { | ||
| 65 | + this.clientId = clientId; | ||
| 66 | + this.keepAliveTimeoutMs = keepAliveTimeoutMs; | ||
| 67 | + this.lastKeepAliveTime = System.currentTimeMillis(); | ||
| 68 | + } | ||
| 69 | + | ||
| 70 | + /** | ||
| 71 | + * 绑定网络套接字,并设置相关处理器。 | ||
| 72 | + * 此方法应在 Vert.x Event Loop 线程中调用。 | ||
| 73 | + * | ||
| 74 | + * @param socket 网络套接字 | ||
| 75 | + */ | ||
| 76 | + public void setSocket(NetSocket socket) { | ||
| 77 | + // 无需 synchronized,Vert.x 保证了同一个 socket 的事件在同一个 Event Loop 中处理 | ||
| 78 | + if (this.socket != null && this.socket != socket) { | ||
| 79 | + log.warn("[setSocket][客户端({})] 正在用新的 socket 替换旧的,旧 socket 将被关闭。", clientId); | ||
| 80 | + this.socket.close(); | ||
| 81 | + } | ||
| 82 | + | ||
| 83 | + this.socket = socket; | ||
| 84 | + | ||
| 85 | + if (socket != null) { | ||
| 86 | + // 1. 设置关闭处理器 | ||
| 87 | + socket.closeHandler(v -> { | ||
| 88 | + log.info("[setSocket][设备客户端({})的连接已由远端关闭]", clientId); | ||
| 89 | + shutdown(); // 统一调用 shutdown 进行资源清理 | ||
| 90 | + }); | ||
| 91 | + | ||
| 92 | + // 2. 设置异常处理器 | ||
| 93 | + socket.exceptionHandler(e -> { | ||
| 94 | + log.error("[setSocket][设备客户端({})连接出现异常]", clientId, e); | ||
| 95 | + shutdown(); // 出现异常时也关闭连接 | ||
| 96 | + }); | ||
| 97 | + | ||
| 98 | + // 3. 设置数据处理器 | ||
| 99 | + socket.handler(buffer -> { | ||
| 100 | + // 任何数据往来都表示连接是活跃的 | ||
| 101 | + keepAlive(); | ||
| 102 | + | ||
| 103 | + if (parser != null) { | ||
| 104 | + parser.handle(buffer); | ||
| 105 | + } else { | ||
| 106 | + log.warn("[setSocket][设备客户端({})] 未设置解析器(parser),原始数据被忽略: {}", clientId, buffer.toString()); | ||
| 107 | + } | ||
| 108 | + }); | ||
| 109 | + } | ||
| 110 | + } | ||
| 111 | + | ||
| 112 | + /** | ||
| 113 | + * 更新心跳时间,表示设备仍然活跃。 | ||
| 114 | + */ | ||
| 115 | + public void keepAlive() { | ||
| 116 | + this.lastKeepAliveTime = System.currentTimeMillis(); | ||
| 117 | + } | ||
| 118 | + | ||
| 119 | + /** | ||
| 120 | + * 检查连接是否在线。 | ||
| 121 | + * 判断标准:未被主动关闭、socket 存在、且在心跳超时时间内。 | ||
| 122 | + * | ||
| 123 | + * @return 是否在线 | ||
| 124 | + */ | ||
| 125 | + public boolean isOnline() { | ||
| 126 | + if (closed.get() || socket == null) { | ||
| 127 | + return false; | ||
| 128 | + } | ||
| 129 | + long idleTime = System.currentTimeMillis() - lastKeepAliveTime; | ||
| 130 | + return idleTime < keepAliveTimeoutMs; | ||
| 131 | + } | ||
| 132 | + | ||
| 133 | + public boolean isAuthenticated() { | ||
| 134 | + return authenticated.get(); | ||
| 135 | + } | ||
| 136 | + | ||
| 137 | + public void setAuthenticated(boolean authenticated) { | ||
| 138 | + this.authenticated.set(authenticated); | ||
| 139 | + } | ||
| 140 | + | ||
| 141 | + /** | ||
| 142 | + * 向设备发送消息。 | ||
| 143 | + * | ||
| 144 | + * @param buffer 消息内容 | ||
| 145 | + */ | ||
| 146 | + public void sendMessage(Buffer buffer) { | ||
| 147 | + if (closed.get() || socket == null) { | ||
| 148 | + log.warn("[sendMessage][设备客户端({})连接已关闭,无法发送消息]", clientId); | ||
| 149 | + return; | ||
| 150 | + } | ||
| 151 | + | ||
| 152 | + // Vert.x 的 write 是异步的,不会阻塞 | ||
| 153 | + socket.write(buffer, result -> { | ||
| 154 | + if (result.succeeded()) { | ||
| 155 | + log.debug("[sendMessage][设备客户端({})发送消息成功]", clientId); | ||
| 156 | + // 发送成功也更新心跳,表示连接活跃 | ||
| 157 | + keepAlive(); | ||
| 158 | + } else { | ||
| 159 | + log.error("[sendMessage][设备客户端({})发送消息失败]", clientId, result.cause()); | ||
| 160 | + // 发送失败可能意味着连接已断开,主动关闭 | ||
| 161 | + shutdown(); | ||
| 162 | + } | ||
| 163 | + }); | ||
| 164 | + } | ||
| 165 | + | ||
| 166 | + /** | ||
| 167 | + * 关闭客户端连接并清理资源。 | ||
| 168 | + * 这是一个幂等操作,可以被多次安全调用。 | ||
| 169 | + */ | ||
| 170 | + public void shutdown() { | ||
| 171 | + // 使用原子操作保证只执行一次关闭逻辑 | ||
| 172 | + if (closed.getAndSet(true)) { | ||
| 173 | + return; | ||
| 174 | + } | ||
| 175 | + | ||
| 176 | + log.info("[shutdown][正在关闭设备客户端连接: {}]", clientId); | ||
| 177 | + | ||
| 178 | + // 先将 socket 引用置空,再关闭,避免并发问题 | ||
| 179 | + NetSocket socketToClose = this.socket; | ||
| 180 | + this.socket = null; | ||
| 181 | + | ||
| 182 | + if (socketToClose != null) { | ||
| 183 | + try { | ||
| 184 | + // close 是异步的,但我们在这里不关心其结果,因为我们已经将客户端标记为关闭 | ||
| 185 | + socketToClose.close(); | ||
| 186 | + } catch (Exception e) { | ||
| 187 | + log.warn("[shutdown][关闭TCP连接时出现异常,可能已被关闭]", e); | ||
| 188 | + } | ||
| 189 | + } | ||
| 190 | + | ||
| 191 | + // 重置认证状态 | ||
| 192 | + authenticated.set(false); | ||
| 193 | + } | ||
| 194 | + | ||
| 195 | + public String getConnectionInfo() { | ||
| 196 | + NetSocket currentSocket = this.socket; | ||
| 197 | + if (currentSocket != null && currentSocket.remoteAddress() != null) { | ||
| 198 | + return currentSocket.remoteAddress().toString(); | ||
| 199 | + } | ||
| 200 | + return "disconnected"; | ||
| 201 | + } | ||
| 202 | + | ||
| 203 | + public long getLastKeepAliveTime() { | ||
| 204 | + return lastKeepAliveTime; | ||
| 205 | + } | ||
| 206 | + | ||
| 207 | + @Override | ||
| 208 | + public String toString() { | ||
| 209 | + return "TcpDeviceClient{" + | ||
| 210 | + "clientId='" + clientId + '\'' + | ||
| 211 | + ", deviceAddr='" + deviceAddr + '\'' + | ||
| 212 | + ", deviceId=" + deviceId + | ||
| 213 | + ", authenticated=" + authenticated.get() + | ||
| 214 | + ", online=" + isOnline() + | ||
| 215 | + ", connection=" + getConnectionInfo() + | ||
| 216 | + '}'; | ||
| 217 | + } | ||
| 218 | +} | ||
| 0 | \ No newline at end of file | 219 | \ No newline at end of file |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/manager/TcpDeviceConnectionManager.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.iot.gateway.protocol.tcp.manager; | ||
| 2 | + | ||
| 3 | +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.client.TcpDeviceClient; | ||
| 4 | +import io.vertx.core.buffer.Buffer; | ||
| 5 | +import io.vertx.core.net.NetSocket; | ||
| 6 | +import lombok.extern.slf4j.Slf4j; | ||
| 7 | +import org.springframework.stereotype.Component; | ||
| 8 | + | ||
| 9 | +import java.util.concurrent.*; | ||
| 10 | +import java.util.concurrent.atomic.AtomicLong; | ||
| 11 | +import java.util.concurrent.locks.ReentrantReadWriteLock; | ||
| 12 | + | ||
| 13 | +/** | ||
| 14 | + * TCP 设备连接管理器 | ||
| 15 | + * <p> | ||
| 16 | + * 参考 EMQX 设计理念: | ||
| 17 | + * 1. 高性能连接管理 | ||
| 18 | + * 2. 连接池和资源管理 | ||
| 19 | + * 3. 流量控制 | ||
| 20 | + * 4. 监控统计 | ||
| 21 | + * 5. 自动清理和容错 | ||
| 22 | + * | ||
| 23 | + * @author 芋道源码 | ||
| 24 | + */ | ||
| 25 | +@Component | ||
| 26 | +@Slf4j | ||
| 27 | +public class TcpDeviceConnectionManager { | ||
| 28 | + | ||
| 29 | + // ==================== 连接存储 ==================== | ||
| 30 | + | ||
| 31 | + /** | ||
| 32 | + * 设备客户端映射 | ||
| 33 | + * Key: 设备地址, Value: 设备客户端 | ||
| 34 | + */ | ||
| 35 | + private final ConcurrentMap<String, TcpDeviceClient> clientMap = new ConcurrentHashMap<>(); | ||
| 36 | + | ||
| 37 | + /** | ||
| 38 | + * 设备ID到设备地址的映射 | ||
| 39 | + * Key: 设备ID, Value: 设备地址 | ||
| 40 | + */ | ||
| 41 | + private final ConcurrentMap<Long, String> deviceIdToAddrMap = new ConcurrentHashMap<>(); | ||
| 42 | + | ||
| 43 | + /** | ||
| 44 | + * 套接字到客户端的映射,用于快速查找 | ||
| 45 | + * Key: NetSocket, Value: 设备地址 | ||
| 46 | + */ | ||
| 47 | + private final ConcurrentMap<NetSocket, String> socketToAddrMap = new ConcurrentHashMap<>(); | ||
| 48 | + | ||
| 49 | + // ==================== 读写锁 ==================== | ||
| 50 | + | ||
| 51 | + private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); | ||
| 52 | + private final ReentrantReadWriteLock.ReadLock readLock = lock.readLock(); | ||
| 53 | + private final ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock(); | ||
| 54 | + | ||
| 55 | + // ==================== 定时任务 ==================== | ||
| 56 | + | ||
| 57 | + /** | ||
| 58 | + * 定时任务执行器 | ||
| 59 | + */ | ||
| 60 | + private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(3, r -> { | ||
| 61 | + Thread t = new Thread(r, "tcp-connection-manager"); | ||
| 62 | + t.setDaemon(true); | ||
| 63 | + return t; | ||
| 64 | + }); | ||
| 65 | + | ||
| 66 | + // ==================== 统计信息 ==================== | ||
| 67 | + | ||
| 68 | + private final AtomicLong totalConnections = new AtomicLong(0); | ||
| 69 | + private final AtomicLong totalDisconnections = new AtomicLong(0); | ||
| 70 | + private final AtomicLong totalMessages = new AtomicLong(0); | ||
| 71 | + private final AtomicLong totalFailedMessages = new AtomicLong(0); | ||
| 72 | + private final AtomicLong totalBytes = new AtomicLong(0); | ||
| 73 | + | ||
| 74 | + // ==================== 配置参数 ==================== | ||
| 75 | + | ||
| 76 | + private static final int MAX_CONNECTIONS = 10000; | ||
| 77 | + private static final int HEARTBEAT_CHECK_INTERVAL = 30; // 秒 | ||
| 78 | + private static final int CONNECTION_CLEANUP_INTERVAL = 60; // 秒 | ||
| 79 | + private static final int STATS_LOG_INTERVAL = 300; // 秒 | ||
| 80 | + | ||
| 81 | + /** | ||
| 82 | + * 构造函数,启动定时任务 | ||
| 83 | + */ | ||
| 84 | + public TcpDeviceConnectionManager() { | ||
| 85 | + startScheduledTasks(); | ||
| 86 | + } | ||
| 87 | + | ||
| 88 | + /** | ||
| 89 | + * 启动定时任务 | ||
| 90 | + */ | ||
| 91 | + private void startScheduledTasks() { | ||
| 92 | + // 心跳检查任务 | ||
| 93 | + scheduler.scheduleAtFixedRate(this::checkHeartbeat, | ||
| 94 | + HEARTBEAT_CHECK_INTERVAL, HEARTBEAT_CHECK_INTERVAL, TimeUnit.SECONDS); | ||
| 95 | + | ||
| 96 | + // 连接清理任务 | ||
| 97 | + scheduler.scheduleAtFixedRate(this::cleanupConnections, | ||
| 98 | + CONNECTION_CLEANUP_INTERVAL, CONNECTION_CLEANUP_INTERVAL, TimeUnit.SECONDS); | ||
| 99 | + | ||
| 100 | + // 统计日志任务 | ||
| 101 | + scheduler.scheduleAtFixedRate(this::logStatistics, | ||
| 102 | + STATS_LOG_INTERVAL, STATS_LOG_INTERVAL, TimeUnit.SECONDS); | ||
| 103 | + } | ||
| 104 | + | ||
| 105 | + /** | ||
| 106 | + * 添加设备客户端 | ||
| 107 | + */ | ||
| 108 | + public boolean addClient(String deviceAddr, TcpDeviceClient client) { | ||
| 109 | + if (clientMap.size() >= MAX_CONNECTIONS) { | ||
| 110 | + log.warn("[addClient][连接数已达上限({}),拒绝新连接: {}]", MAX_CONNECTIONS, deviceAddr); | ||
| 111 | + return false; | ||
| 112 | + } | ||
| 113 | + | ||
| 114 | + writeLock.lock(); | ||
| 115 | + try { | ||
| 116 | + log.info("[addClient][添加设备客户端: {}]", deviceAddr); | ||
| 117 | + | ||
| 118 | + // 关闭之前的连接(如果存在) | ||
| 119 | + TcpDeviceClient existingClient = clientMap.get(deviceAddr); | ||
| 120 | + if (existingClient != null) { | ||
| 121 | + log.warn("[addClient][设备({})已存在连接,关闭旧连接]", deviceAddr); | ||
| 122 | + removeClientInternal(deviceAddr, existingClient); | ||
| 123 | + } | ||
| 124 | + | ||
| 125 | + // 添加新连接 | ||
| 126 | + clientMap.put(deviceAddr, client); | ||
| 127 | + | ||
| 128 | + // 添加套接字映射 | ||
| 129 | + if (client.getSocket() != null) { | ||
| 130 | + socketToAddrMap.put(client.getSocket(), deviceAddr); | ||
| 131 | + } | ||
| 132 | + | ||
| 133 | + // 如果客户端已设置设备ID,更新映射 | ||
| 134 | + if (client.getDeviceId() != null) { | ||
| 135 | + deviceIdToAddrMap.put(client.getDeviceId(), deviceAddr); | ||
| 136 | + } | ||
| 137 | + | ||
| 138 | + totalConnections.incrementAndGet(); | ||
| 139 | + return true; | ||
| 140 | + | ||
| 141 | + } finally { | ||
| 142 | + writeLock.unlock(); | ||
| 143 | + } | ||
| 144 | + } | ||
| 145 | + | ||
| 146 | + /** | ||
| 147 | + * 移除设备客户端 | ||
| 148 | + */ | ||
| 149 | + public void removeClient(String deviceAddr) { | ||
| 150 | + writeLock.lock(); | ||
| 151 | + try { | ||
| 152 | + TcpDeviceClient client = clientMap.get(deviceAddr); | ||
| 153 | + if (client != null) { | ||
| 154 | + removeClientInternal(deviceAddr, client); | ||
| 155 | + } | ||
| 156 | + } finally { | ||
| 157 | + writeLock.unlock(); | ||
| 158 | + } | ||
| 159 | + } | ||
| 160 | + | ||
| 161 | + /** | ||
| 162 | + * 内部移除客户端方法(无锁) | ||
| 163 | + */ | ||
| 164 | + private void removeClientInternal(String deviceAddr, TcpDeviceClient client) { | ||
| 165 | + log.info("[removeClient][移除设备客户端: {}]", deviceAddr); | ||
| 166 | + | ||
| 167 | + // 从映射中移除 | ||
| 168 | + clientMap.remove(deviceAddr); | ||
| 169 | + | ||
| 170 | + // 移除套接字映射 | ||
| 171 | + if (client.getSocket() != null) { | ||
| 172 | + socketToAddrMap.remove(client.getSocket()); | ||
| 173 | + } | ||
| 174 | + | ||
| 175 | + // 移除设备ID映射 | ||
| 176 | + if (client.getDeviceId() != null) { | ||
| 177 | + deviceIdToAddrMap.remove(client.getDeviceId()); | ||
| 178 | + } | ||
| 179 | + | ||
| 180 | + // 关闭连接 | ||
| 181 | + client.shutdown(); | ||
| 182 | + | ||
| 183 | + totalDisconnections.incrementAndGet(); | ||
| 184 | + } | ||
| 185 | + | ||
| 186 | + /** | ||
| 187 | + * 通过设备地址获取客户端 | ||
| 188 | + */ | ||
| 189 | + public TcpDeviceClient getClient(String deviceAddr) { | ||
| 190 | + readLock.lock(); | ||
| 191 | + try { | ||
| 192 | + return clientMap.get(deviceAddr); | ||
| 193 | + } finally { | ||
| 194 | + readLock.unlock(); | ||
| 195 | + } | ||
| 196 | + } | ||
| 197 | + | ||
| 198 | + /** | ||
| 199 | + * 通过设备ID获取客户端 | ||
| 200 | + */ | ||
| 201 | + public TcpDeviceClient getClientByDeviceId(Long deviceId) { | ||
| 202 | + readLock.lock(); | ||
| 203 | + try { | ||
| 204 | + String deviceAddr = deviceIdToAddrMap.get(deviceId); | ||
| 205 | + return deviceAddr != null ? clientMap.get(deviceAddr) : null; | ||
| 206 | + } finally { | ||
| 207 | + readLock.unlock(); | ||
| 208 | + } | ||
| 209 | + } | ||
| 210 | + | ||
| 211 | + /** | ||
| 212 | + * 通过网络连接获取客户端 | ||
| 213 | + */ | ||
| 214 | + public TcpDeviceClient getClientBySocket(NetSocket socket) { | ||
| 215 | + readLock.lock(); | ||
| 216 | + try { | ||
| 217 | + String deviceAddr = socketToAddrMap.get(socket); | ||
| 218 | + return deviceAddr != null ? clientMap.get(deviceAddr) : null; | ||
| 219 | + } finally { | ||
| 220 | + readLock.unlock(); | ||
| 221 | + } | ||
| 222 | + } | ||
| 223 | + | ||
| 224 | + /** | ||
| 225 | + * 检查设备是否在线 | ||
| 226 | + */ | ||
| 227 | + public boolean isDeviceOnline(Long deviceId) { | ||
| 228 | + TcpDeviceClient client = getClientByDeviceId(deviceId); | ||
| 229 | + return client != null && client.isOnline(); | ||
| 230 | + } | ||
| 231 | + | ||
| 232 | + /** | ||
| 233 | + * 设置设备ID映射 | ||
| 234 | + */ | ||
| 235 | + public void setDeviceIdMapping(String deviceAddr, Long deviceId) { | ||
| 236 | + writeLock.lock(); | ||
| 237 | + try { | ||
| 238 | + TcpDeviceClient client = clientMap.get(deviceAddr); | ||
| 239 | + if (client != null) { | ||
| 240 | + client.setDeviceId(deviceId); | ||
| 241 | + deviceIdToAddrMap.put(deviceId, deviceAddr); | ||
| 242 | + log.debug("[setDeviceIdMapping][设置设备ID映射: {} -> {}]", deviceAddr, deviceId); | ||
| 243 | + } | ||
| 244 | + } finally { | ||
| 245 | + writeLock.unlock(); | ||
| 246 | + } | ||
| 247 | + } | ||
| 248 | + | ||
| 249 | + /** | ||
| 250 | + * 发送消息给设备 | ||
| 251 | + */ | ||
| 252 | + public boolean sendMessage(String deviceAddr, Buffer buffer) { | ||
| 253 | + TcpDeviceClient client = getClient(deviceAddr); | ||
| 254 | + if (client != null && client.isOnline()) { | ||
| 255 | + try { | ||
| 256 | + client.sendMessage(buffer); | ||
| 257 | + totalMessages.incrementAndGet(); | ||
| 258 | + totalBytes.addAndGet(buffer.length()); | ||
| 259 | + return true; | ||
| 260 | + } catch (Exception e) { | ||
| 261 | + totalFailedMessages.incrementAndGet(); | ||
| 262 | + log.error("[sendMessage][发送消息失败] 设备地址: {}", deviceAddr, e); | ||
| 263 | + return false; | ||
| 264 | + } | ||
| 265 | + } | ||
| 266 | + log.warn("[sendMessage][设备({})不在线,无法发送消息]", deviceAddr); | ||
| 267 | + return false; | ||
| 268 | + } | ||
| 269 | + | ||
| 270 | + /** | ||
| 271 | + * 通过设备ID发送消息 | ||
| 272 | + */ | ||
| 273 | + public boolean sendMessageByDeviceId(Long deviceId, Buffer buffer) { | ||
| 274 | + TcpDeviceClient client = getClientByDeviceId(deviceId); | ||
| 275 | + if (client != null && client.isOnline()) { | ||
| 276 | + try { | ||
| 277 | + client.sendMessage(buffer); | ||
| 278 | + totalMessages.incrementAndGet(); | ||
| 279 | + totalBytes.addAndGet(buffer.length()); | ||
| 280 | + return true; | ||
| 281 | + } catch (Exception e) { | ||
| 282 | + totalFailedMessages.incrementAndGet(); | ||
| 283 | + log.error("[sendMessageByDeviceId][发送消息失败] 设备ID: {}", deviceId, e); | ||
| 284 | + return false; | ||
| 285 | + } | ||
| 286 | + } | ||
| 287 | + log.warn("[sendMessageByDeviceId][设备ID({})不在线,无法发送消息]", deviceId); | ||
| 288 | + return false; | ||
| 289 | + } | ||
| 290 | + | ||
| 291 | + /** | ||
| 292 | + * 广播消息给所有在线设备 | ||
| 293 | + */ | ||
| 294 | + public int broadcastMessage(Buffer buffer) { | ||
| 295 | + int successCount = 0; | ||
| 296 | + readLock.lock(); | ||
| 297 | + try { | ||
| 298 | + for (TcpDeviceClient client : clientMap.values()) { | ||
| 299 | + if (client.isOnline()) { | ||
| 300 | + try { | ||
| 301 | + client.sendMessage(buffer); | ||
| 302 | + successCount++; | ||
| 303 | + } catch (Exception e) { | ||
| 304 | + log.error("[broadcastMessage][广播消息失败] 设备: {}", client.getDeviceAddr(), e); | ||
| 305 | + } | ||
| 306 | + } | ||
| 307 | + } | ||
| 308 | + } finally { | ||
| 309 | + readLock.unlock(); | ||
| 310 | + } | ||
| 311 | + | ||
| 312 | + totalMessages.addAndGet(successCount); | ||
| 313 | + totalBytes.addAndGet((long) successCount * buffer.length()); | ||
| 314 | + return successCount; | ||
| 315 | + } | ||
| 316 | + | ||
| 317 | + /** | ||
| 318 | + * 获取在线设备数量 | ||
| 319 | + */ | ||
| 320 | + public int getOnlineCount() { | ||
| 321 | + readLock.lock(); | ||
| 322 | + try { | ||
| 323 | + return (int) clientMap.values().stream() | ||
| 324 | + .filter(TcpDeviceClient::isOnline) | ||
| 325 | + .count(); | ||
| 326 | + } finally { | ||
| 327 | + readLock.unlock(); | ||
| 328 | + } | ||
| 329 | + } | ||
| 330 | + | ||
| 331 | + /** | ||
| 332 | + * 获取总连接数 | ||
| 333 | + */ | ||
| 334 | + public int getTotalCount() { | ||
| 335 | + return clientMap.size(); | ||
| 336 | + } | ||
| 337 | + | ||
| 338 | + /** | ||
| 339 | + * 获取认证设备数量 | ||
| 340 | + */ | ||
| 341 | + public int getAuthenticatedCount() { | ||
| 342 | + readLock.lock(); | ||
| 343 | + try { | ||
| 344 | + return (int) clientMap.values().stream() | ||
| 345 | + .filter(TcpDeviceClient::isAuthenticated) | ||
| 346 | + .count(); | ||
| 347 | + } finally { | ||
| 348 | + readLock.unlock(); | ||
| 349 | + } | ||
| 350 | + } | ||
| 351 | + | ||
| 352 | + /** | ||
| 353 | + * 心跳检查任务 | ||
| 354 | + */ | ||
| 355 | + private void checkHeartbeat() { | ||
| 356 | + try { | ||
| 357 | + long currentTime = System.currentTimeMillis(); | ||
| 358 | + int offlineCount = 0; | ||
| 359 | + | ||
| 360 | + readLock.lock(); | ||
| 361 | + try { | ||
| 362 | + for (TcpDeviceClient client : clientMap.values()) { | ||
| 363 | + if (!client.isOnline()) { | ||
| 364 | + offlineCount++; | ||
| 365 | + } | ||
| 366 | + } | ||
| 367 | + } finally { | ||
| 368 | + readLock.unlock(); | ||
| 369 | + } | ||
| 370 | + | ||
| 371 | + if (offlineCount > 0) { | ||
| 372 | + log.info("[checkHeartbeat][发现{}个离线设备,将在清理任务中处理]", offlineCount); | ||
| 373 | + } | ||
| 374 | + } catch (Exception e) { | ||
| 375 | + log.error("[checkHeartbeat][心跳检查任务异常]", e); | ||
| 376 | + } | ||
| 377 | + } | ||
| 378 | + | ||
| 379 | + /** | ||
| 380 | + * 连接清理任务 | ||
| 381 | + */ | ||
| 382 | + private void cleanupConnections() { | ||
| 383 | + try { | ||
| 384 | + int beforeSize = clientMap.size(); | ||
| 385 | + | ||
| 386 | + writeLock.lock(); | ||
| 387 | + try { | ||
| 388 | + clientMap.entrySet().removeIf(entry -> { | ||
| 389 | + TcpDeviceClient client = entry.getValue(); | ||
| 390 | + if (!client.isOnline()) { | ||
| 391 | + log.debug("[cleanupConnections][清理离线连接: {}]", entry.getKey()); | ||
| 392 | + | ||
| 393 | + // 清理相关映射 | ||
| 394 | + if (client.getSocket() != null) { | ||
| 395 | + socketToAddrMap.remove(client.getSocket()); | ||
| 396 | + } | ||
| 397 | + if (client.getDeviceId() != null) { | ||
| 398 | + deviceIdToAddrMap.remove(client.getDeviceId()); | ||
| 399 | + } | ||
| 400 | + | ||
| 401 | + client.shutdown(); | ||
| 402 | + totalDisconnections.incrementAndGet(); | ||
| 403 | + return true; | ||
| 404 | + } | ||
| 405 | + return false; | ||
| 406 | + }); | ||
| 407 | + } finally { | ||
| 408 | + writeLock.unlock(); | ||
| 409 | + } | ||
| 410 | + | ||
| 411 | + int afterSize = clientMap.size(); | ||
| 412 | + if (beforeSize != afterSize) { | ||
| 413 | + log.info("[cleanupConnections][清理完成] 连接数: {} -> {}, 清理数: {}", | ||
| 414 | + beforeSize, afterSize, beforeSize - afterSize); | ||
| 415 | + } | ||
| 416 | + } catch (Exception e) { | ||
| 417 | + log.error("[cleanupConnections][连接清理任务异常]", e); | ||
| 418 | + } | ||
| 419 | + } | ||
| 420 | + | ||
| 421 | + /** | ||
| 422 | + * 统计日志任务 | ||
| 423 | + */ | ||
| 424 | + private void logStatistics() { | ||
| 425 | + try { | ||
| 426 | + long totalConn = totalConnections.get(); | ||
| 427 | + long totalDisconn = totalDisconnections.get(); | ||
| 428 | + long totalMsg = totalMessages.get(); | ||
| 429 | + long totalFailedMsg = totalFailedMessages.get(); | ||
| 430 | + long totalBytesValue = totalBytes.get(); | ||
| 431 | + | ||
| 432 | + log.info("[logStatistics][连接统计] 总连接: {}, 总断开: {}, 当前在线: {}, 认证设备: {}, " + | ||
| 433 | + "总消息: {}, 失败消息: {}, 总字节: {}", | ||
| 434 | + totalConn, totalDisconn, getOnlineCount(), getAuthenticatedCount(), | ||
| 435 | + totalMsg, totalFailedMsg, totalBytesValue); | ||
| 436 | + } catch (Exception e) { | ||
| 437 | + log.error("[logStatistics][统计日志任务异常]", e); | ||
| 438 | + } | ||
| 439 | + } | ||
| 440 | + | ||
| 441 | + /** | ||
| 442 | + * 关闭连接管理器 | ||
| 443 | + */ | ||
| 444 | + public void shutdown() { | ||
| 445 | + log.info("[shutdown][关闭TCP连接管理器]"); | ||
| 446 | + | ||
| 447 | + // 关闭定时任务 | ||
| 448 | + scheduler.shutdown(); | ||
| 449 | + try { | ||
| 450 | + if (!scheduler.awaitTermination(10, TimeUnit.SECONDS)) { | ||
| 451 | + scheduler.shutdownNow(); | ||
| 452 | + } | ||
| 453 | + } catch (InterruptedException e) { | ||
| 454 | + scheduler.shutdownNow(); | ||
| 455 | + Thread.currentThread().interrupt(); | ||
| 456 | + } | ||
| 457 | + | ||
| 458 | + // 关闭所有连接 | ||
| 459 | + writeLock.lock(); | ||
| 460 | + try { | ||
| 461 | + clientMap.values().forEach(TcpDeviceClient::shutdown); | ||
| 462 | + clientMap.clear(); | ||
| 463 | + deviceIdToAddrMap.clear(); | ||
| 464 | + socketToAddrMap.clear(); | ||
| 465 | + } finally { | ||
| 466 | + writeLock.unlock(); | ||
| 467 | + } | ||
| 468 | + } | ||
| 469 | + | ||
| 470 | + /** | ||
| 471 | + * 获取连接状态信息 | ||
| 472 | + */ | ||
| 473 | + public String getConnectionStatus() { | ||
| 474 | + return String.format("总连接数: %d, 在线设备: %d, 认证设备: %d, 成功率: %.2f%%", | ||
| 475 | + getTotalCount(), getOnlineCount(), getAuthenticatedCount(), | ||
| 476 | + totalMessages.get() > 0 | ||
| 477 | + ? (double) (totalMessages.get() - totalFailedMessages.get()) / totalMessages.get() * 100 | ||
| 478 | + : 0.0); | ||
| 479 | + } | ||
| 480 | + | ||
| 481 | + /** | ||
| 482 | + * 获取详细统计信息 | ||
| 483 | + */ | ||
| 484 | + public String getDetailedStatistics() { | ||
| 485 | + return String.format( | ||
| 486 | + "TCP连接管理器统计:\n" + | ||
| 487 | + "- 当前连接数: %d\n" + | ||
| 488 | + "- 在线设备数: %d\n" + | ||
| 489 | + "- 认证设备数: %d\n" + | ||
| 490 | + "- 历史总连接: %d\n" + | ||
| 491 | + "- 历史总断开: %d\n" + | ||
| 492 | + "- 总消息数: %d\n" + | ||
| 493 | + "- 失败消息数: %d\n" + | ||
| 494 | + "- 总字节数: %d\n" + | ||
| 495 | + "- 消息成功率: %.2f%%", | ||
| 496 | + getTotalCount(), getOnlineCount(), getAuthenticatedCount(), | ||
| 497 | + totalConnections.get(), totalDisconnections.get(), | ||
| 498 | + totalMessages.get(), totalFailedMessages.get(), totalBytes.get(), | ||
| 499 | + totalMessages.get() > 0 | ||
| 500 | + ? (double) (totalMessages.get() - totalFailedMessages.get()) / totalMessages.get() * 100 | ||
| 501 | + : 0.0); | ||
| 502 | + } | ||
| 503 | +} | ||
| 0 | \ No newline at end of file | 504 | \ No newline at end of file |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/package-info.java deleted
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/protocol/TcpDataDecoder.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.iot.gateway.protocol.tcp.protocol; | ||
| 2 | + | ||
| 3 | +import io.vertx.core.buffer.Buffer; | ||
| 4 | +import lombok.extern.slf4j.Slf4j; | ||
| 5 | + | ||
| 6 | +/** | ||
| 7 | + * TCP 数据解码器 | ||
| 8 | + * <p> | ||
| 9 | + * 负责将字节流解码为 TcpDataPackage 对象 | ||
| 10 | + * <p> | ||
| 11 | + * 数据包格式: | ||
| 12 | + * 包头(4字节长度) | 设备地址长度(2字节) | 设备地址(不定长) | 功能码(2字节) | 消息序号(2字节) | 包体(不定长) | ||
| 13 | + * | ||
| 14 | + * @author 芋道源码 | ||
| 15 | + */ | ||
| 16 | +@Slf4j | ||
| 17 | +public class TcpDataDecoder { | ||
| 18 | + | ||
| 19 | + /** | ||
| 20 | + * 解码数据包 | ||
| 21 | + * | ||
| 22 | + * @param buffer 数据缓冲区 | ||
| 23 | + * @return 解码后的数据包 | ||
| 24 | + * @throws IllegalArgumentException 如果数据包格式不正确 | ||
| 25 | + */ | ||
| 26 | + public static TcpDataPackage decode(Buffer buffer) { | ||
| 27 | + if (buffer == null || buffer.length() < 8) { | ||
| 28 | + throw new IllegalArgumentException("数据包长度不足"); | ||
| 29 | + } | ||
| 30 | + | ||
| 31 | + try { | ||
| 32 | + int index = 0; | ||
| 33 | + | ||
| 34 | + // 1. 获取设备地址长度(2字节) | ||
| 35 | + short addrLength = buffer.getShort(index); | ||
| 36 | + index += 2; | ||
| 37 | + | ||
| 38 | + // 2. 校验数据包长度 | ||
| 39 | + int expectedLength = 2 + addrLength + 2 + 2; // 地址长度 + 地址 + 功能码 + 消息序号 | ||
| 40 | + if (buffer.length() < expectedLength) { | ||
| 41 | + throw new IllegalArgumentException("数据包长度不足,期望至少 " + expectedLength + " 字节"); | ||
| 42 | + } | ||
| 43 | + | ||
| 44 | + // 3. 获取设备地址 | ||
| 45 | + String addr = buffer.getBuffer(index, index + addrLength).toString(); | ||
| 46 | + index += addrLength; | ||
| 47 | + | ||
| 48 | + // 4. 获取功能码(2字节) | ||
| 49 | + short code = buffer.getShort(index); | ||
| 50 | + index += 2; | ||
| 51 | + | ||
| 52 | + // 5. 获取消息序号(2字节) | ||
| 53 | + short mid = buffer.getShort(index); | ||
| 54 | + index += 2; | ||
| 55 | + | ||
| 56 | + // 6. 获取包体数据 | ||
| 57 | + String payload = ""; | ||
| 58 | + if (index < buffer.length()) { | ||
| 59 | + payload = buffer.getString(index, buffer.length()); | ||
| 60 | + } | ||
| 61 | + | ||
| 62 | + // 7. 构建数据包对象 | ||
| 63 | + TcpDataPackage dataPackage = TcpDataPackage.builder() | ||
| 64 | + .addrLength((int) addrLength) | ||
| 65 | + .addr(addr) | ||
| 66 | + .code(code) | ||
| 67 | + .mid(mid) | ||
| 68 | + .payload(payload) | ||
| 69 | + .build(); | ||
| 70 | + | ||
| 71 | + log.debug("[decode][解码成功] 设备地址: {}, 功能码: {}, 消息序号: {}, 包体长度: {}", | ||
| 72 | + addr, dataPackage.getCodeDescription(), mid, payload.length()); | ||
| 73 | + | ||
| 74 | + return dataPackage; | ||
| 75 | + | ||
| 76 | + } catch (Exception e) { | ||
| 77 | + log.error("[decode][解码失败] 数据: {}", buffer.toString(), e); | ||
| 78 | + throw new IllegalArgumentException("数据包解码失败: " + e.getMessage(), e); | ||
| 79 | + } | ||
| 80 | + } | ||
| 81 | + | ||
| 82 | + /** | ||
| 83 | + * 校验数据包格式 | ||
| 84 | + * | ||
| 85 | + * @param buffer 数据缓冲区 | ||
| 86 | + * @return 校验结果 | ||
| 87 | + */ | ||
| 88 | + public static boolean validate(Buffer buffer) { | ||
| 89 | + try { | ||
| 90 | + decode(buffer); | ||
| 91 | + return true; | ||
| 92 | + } catch (Exception e) { | ||
| 93 | + log.warn("[validate][数据包格式校验失败] 数据: {}, 错误: {}", buffer.toString(), e.getMessage()); | ||
| 94 | + return false; | ||
| 95 | + } | ||
| 96 | + } | ||
| 97 | +} | ||
| 0 | \ No newline at end of file | 98 | \ No newline at end of file |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/protocol/TcpDataEncoder.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.iot.gateway.protocol.tcp.protocol; | ||
| 2 | + | ||
| 3 | +import io.vertx.core.buffer.Buffer; | ||
| 4 | +import lombok.extern.slf4j.Slf4j; | ||
| 5 | + | ||
| 6 | +/** | ||
| 7 | + * TCP 数据编码器 | ||
| 8 | + * <p> | ||
| 9 | + * 负责将 TcpDataPackage 对象编码为字节流 | ||
| 10 | + * <p> | ||
| 11 | + * 数据包格式: | ||
| 12 | + * 包头(4字节长度) | 设备地址长度(2字节) | 设备地址(不定长) | 功能码(2字节) | 消息序号(2字节) | 包体(不定长) | ||
| 13 | + * | ||
| 14 | + * @author 芋道源码 | ||
| 15 | + */ | ||
| 16 | +@Slf4j | ||
| 17 | +public class TcpDataEncoder { | ||
| 18 | + | ||
| 19 | + /** | ||
| 20 | + * 编码数据包 | ||
| 21 | + * | ||
| 22 | + * @param dataPackage 数据包对象 | ||
| 23 | + * @return 编码后的字节流 | ||
| 24 | + * @throws IllegalArgumentException 如果数据包对象不正确 | ||
| 25 | + */ | ||
| 26 | + public static Buffer encode(TcpDataPackage dataPackage) { | ||
| 27 | + if (dataPackage == null) { | ||
| 28 | + throw new IllegalArgumentException("数据包对象不能为空"); | ||
| 29 | + } | ||
| 30 | + | ||
| 31 | + if (dataPackage.getAddr() == null || dataPackage.getAddr().isEmpty()) { | ||
| 32 | + throw new IllegalArgumentException("设备地址不能为空"); | ||
| 33 | + } | ||
| 34 | + | ||
| 35 | + if (dataPackage.getPayload() == null) { | ||
| 36 | + dataPackage.setPayload(""); | ||
| 37 | + } | ||
| 38 | + | ||
| 39 | + try { | ||
| 40 | + Buffer buffer = Buffer.buffer(); | ||
| 41 | + | ||
| 42 | + // 1. 计算包体长度(除了包头4字节) | ||
| 43 | + int payloadLength = dataPackage.getPayload().getBytes().length; | ||
| 44 | + int totalLength = 2 + dataPackage.getAddr().length() + 2 + 2 + payloadLength; | ||
| 45 | + | ||
| 46 | + // 2. 写入包头:总长度(4字节) | ||
| 47 | + buffer.appendInt(totalLength); | ||
| 48 | + | ||
| 49 | + // 3. 写入设备地址长度(2字节) | ||
| 50 | + buffer.appendShort((short) dataPackage.getAddr().length()); | ||
| 51 | + | ||
| 52 | + // 4. 写入设备地址(不定长) | ||
| 53 | + buffer.appendBytes(dataPackage.getAddr().getBytes()); | ||
| 54 | + | ||
| 55 | + // 5. 写入功能码(2字节) | ||
| 56 | + buffer.appendShort(dataPackage.getCode()); | ||
| 57 | + | ||
| 58 | + // 6. 写入消息序号(2字节) | ||
| 59 | + buffer.appendShort(dataPackage.getMid()); | ||
| 60 | + | ||
| 61 | + // 7. 写入包体数据(不定长) | ||
| 62 | + buffer.appendBytes(dataPackage.getPayload().getBytes()); | ||
| 63 | + | ||
| 64 | + log.debug("[encode][编码成功] 设备地址: {}, 功能码: {}, 消息序号: {}, 总长度: {}", | ||
| 65 | + dataPackage.getAddr(), dataPackage.getCodeDescription(), | ||
| 66 | + dataPackage.getMid(), buffer.length()); | ||
| 67 | + | ||
| 68 | + return buffer; | ||
| 69 | + | ||
| 70 | + } catch (Exception e) { | ||
| 71 | + log.error("[encode][编码失败] 数据包: {}", dataPackage, e); | ||
| 72 | + throw new IllegalArgumentException("数据包编码失败: " + e.getMessage(), e); | ||
| 73 | + } | ||
| 74 | + } | ||
| 75 | + | ||
| 76 | + /** | ||
| 77 | + * 创建注册回复数据包 | ||
| 78 | + * | ||
| 79 | + * @param addr 设备地址 | ||
| 80 | + * @param mid 消息序号 | ||
| 81 | + * @param success 是否成功 | ||
| 82 | + * @return 编码后的数据包 | ||
| 83 | + */ | ||
| 84 | + public static Buffer createRegisterReply(String addr, short mid, boolean success) { | ||
| 85 | + String payload = success ? "0" : "1"; // 0表示成功,1表示失败 | ||
| 86 | + | ||
| 87 | + TcpDataPackage dataPackage = TcpDataPackage.builder() | ||
| 88 | + .addr(addr) | ||
| 89 | + .code(TcpDataPackage.CODE_REGISTER_REPLY) | ||
| 90 | + .mid(mid) | ||
| 91 | + .payload(payload) | ||
| 92 | + .build(); | ||
| 93 | + | ||
| 94 | + return encode(dataPackage); | ||
| 95 | + } | ||
| 96 | + | ||
| 97 | + /** | ||
| 98 | + * 创建数据下发数据包 | ||
| 99 | + * | ||
| 100 | + * @param addr 设备地址 | ||
| 101 | + * @param mid 消息序号 | ||
| 102 | + * @param data 下发数据 | ||
| 103 | + * @return 编码后的数据包 | ||
| 104 | + */ | ||
| 105 | + public static Buffer createDataDownPackage(String addr, short mid, String data) { | ||
| 106 | + TcpDataPackage dataPackage = TcpDataPackage.builder() | ||
| 107 | + .addr(addr) | ||
| 108 | + .code(TcpDataPackage.CODE_DATA_DOWN) | ||
| 109 | + .mid(mid) | ||
| 110 | + .payload(data) | ||
| 111 | + .build(); | ||
| 112 | + | ||
| 113 | + return encode(dataPackage); | ||
| 114 | + } | ||
| 115 | + | ||
| 116 | + /** | ||
| 117 | + * 创建服务调用数据包 | ||
| 118 | + * | ||
| 119 | + * @param addr 设备地址 | ||
| 120 | + * @param mid 消息序号 | ||
| 121 | + * @param serviceData 服务数据 | ||
| 122 | + * @return 编码后的数据包 | ||
| 123 | + */ | ||
| 124 | + public static Buffer createServiceInvokePackage(String addr, short mid, String serviceData) { | ||
| 125 | + TcpDataPackage dataPackage = TcpDataPackage.builder() | ||
| 126 | + .addr(addr) | ||
| 127 | + .code(TcpDataPackage.CODE_SERVICE_INVOKE) | ||
| 128 | + .mid(mid) | ||
| 129 | + .payload(serviceData) | ||
| 130 | + .build(); | ||
| 131 | + | ||
| 132 | + return encode(dataPackage); | ||
| 133 | + } | ||
| 134 | + | ||
| 135 | + /** | ||
| 136 | + * 创建属性设置数据包 | ||
| 137 | + * | ||
| 138 | + * @param addr 设备地址 | ||
| 139 | + * @param mid 消息序号 | ||
| 140 | + * @param propertyData 属性数据 | ||
| 141 | + * @return 编码后的数据包 | ||
| 142 | + */ | ||
| 143 | + public static Buffer createPropertySetPackage(String addr, short mid, String propertyData) { | ||
| 144 | + TcpDataPackage dataPackage = TcpDataPackage.builder() | ||
| 145 | + .addr(addr) | ||
| 146 | + .code(TcpDataPackage.CODE_PROPERTY_SET) | ||
| 147 | + .mid(mid) | ||
| 148 | + .payload(propertyData) | ||
| 149 | + .build(); | ||
| 150 | + | ||
| 151 | + return encode(dataPackage); | ||
| 152 | + } | ||
| 153 | + | ||
| 154 | + /** | ||
| 155 | + * 创建属性获取数据包 | ||
| 156 | + * | ||
| 157 | + * @param addr 设备地址 | ||
| 158 | + * @param mid 消息序号 | ||
| 159 | + * @param propertyNames 属性名称列表 | ||
| 160 | + * @return 编码后的数据包 | ||
| 161 | + */ | ||
| 162 | + public static Buffer createPropertyGetPackage(String addr, short mid, String propertyNames) { | ||
| 163 | + TcpDataPackage dataPackage = TcpDataPackage.builder() | ||
| 164 | + .addr(addr) | ||
| 165 | + .code(TcpDataPackage.CODE_PROPERTY_GET) | ||
| 166 | + .mid(mid) | ||
| 167 | + .payload(propertyNames) | ||
| 168 | + .build(); | ||
| 169 | + | ||
| 170 | + return encode(dataPackage); | ||
| 171 | + } | ||
| 172 | +} | ||
| 0 | \ No newline at end of file | 173 | \ No newline at end of file |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/protocol/TcpDataPackage.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.iot.gateway.protocol.tcp.protocol; | ||
| 2 | + | ||
| 3 | +import lombok.AllArgsConstructor; | ||
| 4 | +import lombok.Builder; | ||
| 5 | +import lombok.Data; | ||
| 6 | +import lombok.NoArgsConstructor; | ||
| 7 | + | ||
| 8 | +/** | ||
| 9 | + * TCP 数据包协议定义 | ||
| 10 | + * <p> | ||
| 11 | + * 数据包格式: | ||
| 12 | + * 包头(4字节长度) | 设备地址长度(2字节) | 设备地址(不定长) | 功能码(2字节) | 消息序号(2字节) | 包体(不定长) | ||
| 13 | + * | ||
| 14 | + * @author 芋道源码 | ||
| 15 | + */ | ||
| 16 | +@Data | ||
| 17 | +@AllArgsConstructor | ||
| 18 | +@NoArgsConstructor | ||
| 19 | +@Builder | ||
| 20 | +public class TcpDataPackage { | ||
| 21 | + | ||
| 22 | + // ==================== 功能码定义 ==================== | ||
| 23 | + | ||
| 24 | + /** | ||
| 25 | + * 设备注册 | ||
| 26 | + */ | ||
| 27 | + public static final short CODE_REGISTER = 10; | ||
| 28 | + /** | ||
| 29 | + * 注册回复 | ||
| 30 | + */ | ||
| 31 | + public static final short CODE_REGISTER_REPLY = 11; | ||
| 32 | + /** | ||
| 33 | + * 心跳 | ||
| 34 | + */ | ||
| 35 | + public static final short CODE_HEARTBEAT = 20; | ||
| 36 | + /** | ||
| 37 | + * 数据上报 | ||
| 38 | + */ | ||
| 39 | + public static final short CODE_DATA_UP = 30; | ||
| 40 | + /** | ||
| 41 | + * 事件上报 | ||
| 42 | + */ | ||
| 43 | + public static final short CODE_EVENT_UP = 40; | ||
| 44 | + /** | ||
| 45 | + * 数据下发 | ||
| 46 | + */ | ||
| 47 | + public static final short CODE_DATA_DOWN = 50; | ||
| 48 | + /** | ||
| 49 | + * 服务调用 | ||
| 50 | + */ | ||
| 51 | + public static final short CODE_SERVICE_INVOKE = 60; | ||
| 52 | + /** | ||
| 53 | + * 属性设置 | ||
| 54 | + */ | ||
| 55 | + public static final short CODE_PROPERTY_SET = 70; | ||
| 56 | + /** | ||
| 57 | + * 属性获取 | ||
| 58 | + */ | ||
| 59 | + public static final short CODE_PROPERTY_GET = 80; | ||
| 60 | + | ||
| 61 | + // ==================== 数据包字段 ==================== | ||
| 62 | + | ||
| 63 | + /** | ||
| 64 | + * 设备地址长度 | ||
| 65 | + */ | ||
| 66 | + private Integer addrLength; | ||
| 67 | + | ||
| 68 | + /** | ||
| 69 | + * 设备地址 | ||
| 70 | + */ | ||
| 71 | + private String addr; | ||
| 72 | + | ||
| 73 | + /** | ||
| 74 | + * 功能码 | ||
| 75 | + */ | ||
| 76 | + private short code; | ||
| 77 | + | ||
| 78 | + /** | ||
| 79 | + * 消息序号 | ||
| 80 | + */ | ||
| 81 | + private short mid; | ||
| 82 | + | ||
| 83 | + /** | ||
| 84 | + * 包体数据 | ||
| 85 | + */ | ||
| 86 | + private String payload; | ||
| 87 | + | ||
| 88 | + // ==================== 辅助方法 ==================== | ||
| 89 | + | ||
| 90 | + /** | ||
| 91 | + * 是否为注册消息 | ||
| 92 | + */ | ||
| 93 | + public boolean isRegisterMessage() { | ||
| 94 | + return code == CODE_REGISTER; | ||
| 95 | + } | ||
| 96 | + | ||
| 97 | + /** | ||
| 98 | + * 是否为心跳消息 | ||
| 99 | + */ | ||
| 100 | + public boolean isHeartbeatMessage() { | ||
| 101 | + return code == CODE_HEARTBEAT; | ||
| 102 | + } | ||
| 103 | + | ||
| 104 | + /** | ||
| 105 | + * 是否为数据上报消息 | ||
| 106 | + */ | ||
| 107 | + public boolean isDataUpMessage() { | ||
| 108 | + return code == CODE_DATA_UP; | ||
| 109 | + } | ||
| 110 | + | ||
| 111 | + /** | ||
| 112 | + * 是否为事件上报消息 | ||
| 113 | + */ | ||
| 114 | + public boolean isEventUpMessage() { | ||
| 115 | + return code == CODE_EVENT_UP; | ||
| 116 | + } | ||
| 117 | + | ||
| 118 | + /** | ||
| 119 | + * 是否为下行消息 | ||
| 120 | + */ | ||
| 121 | + public boolean isDownstreamMessage() { | ||
| 122 | + return code == CODE_DATA_DOWN || code == CODE_SERVICE_INVOKE || | ||
| 123 | + code == CODE_PROPERTY_SET || code == CODE_PROPERTY_GET; | ||
| 124 | + } | ||
| 125 | + | ||
| 126 | + /** | ||
| 127 | + * 获取功能码描述 | ||
| 128 | + */ | ||
| 129 | + public String getCodeDescription() { | ||
| 130 | + switch (code) { | ||
| 131 | + case CODE_REGISTER: | ||
| 132 | + return "设备注册"; | ||
| 133 | + case CODE_REGISTER_REPLY: | ||
| 134 | + return "注册回复"; | ||
| 135 | + case CODE_HEARTBEAT: | ||
| 136 | + return "心跳"; | ||
| 137 | + case CODE_DATA_UP: | ||
| 138 | + return "数据上报"; | ||
| 139 | + case CODE_EVENT_UP: | ||
| 140 | + return "事件上报"; | ||
| 141 | + case CODE_DATA_DOWN: | ||
| 142 | + return "数据下发"; | ||
| 143 | + case CODE_SERVICE_INVOKE: | ||
| 144 | + return "服务调用"; | ||
| 145 | + case CODE_PROPERTY_SET: | ||
| 146 | + return "属性设置"; | ||
| 147 | + case CODE_PROPERTY_GET: | ||
| 148 | + return "属性获取"; | ||
| 149 | + default: | ||
| 150 | + return "未知功能码"; | ||
| 151 | + } | ||
| 152 | + } | ||
| 153 | +} | ||
| 0 | \ No newline at end of file | 154 | \ No newline at end of file |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/protocol/TcpDataReader.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.iot.gateway.protocol.tcp.protocol; | ||
| 2 | + | ||
| 3 | +import io.vertx.core.Handler; | ||
| 4 | +import io.vertx.core.buffer.Buffer; | ||
| 5 | +import io.vertx.core.parsetools.RecordParser; | ||
| 6 | +import lombok.extern.slf4j.Slf4j; | ||
| 7 | + | ||
| 8 | +import java.util.function.Consumer; | ||
| 9 | + | ||
| 10 | +/** | ||
| 11 | + * TCP 数据读取器 | ||
| 12 | + * <p> | ||
| 13 | + * 负责从 TCP 流中读取完整的数据包 | ||
| 14 | + * <p> | ||
| 15 | + * 数据包格式: | ||
| 16 | + * 包头(4字节长度) | 设备地址长度(2字节) | 设备地址(不定长) | 功能码(2字节) | 消息序号(2字节) | 包体(不定长) | ||
| 17 | + * | ||
| 18 | + * @author 芋道源码 | ||
| 19 | + */ | ||
| 20 | +@Slf4j | ||
| 21 | +public class TcpDataReader { | ||
| 22 | + | ||
| 23 | + /** | ||
| 24 | + * 创建数据包解析器 | ||
| 25 | + * | ||
| 26 | + * @param receiveHandler 接收处理器 | ||
| 27 | + * @return RecordParser 解析器 | ||
| 28 | + */ | ||
| 29 | + public static RecordParser createParser(Consumer<Buffer> receiveHandler) { | ||
| 30 | + // 首先读取4字节的长度信息 | ||
| 31 | + RecordParser parser = RecordParser.newFixed(4); | ||
| 32 | + | ||
| 33 | + // 设置处理器 | ||
| 34 | + parser.setOutput(new Handler<Buffer>() { | ||
| 35 | + // 当前数据包的长度,-1表示还没有读取到长度信息 | ||
| 36 | + private int dataLength = -1; | ||
| 37 | + | ||
| 38 | + @Override | ||
| 39 | + public void handle(Buffer buffer) { | ||
| 40 | + try { | ||
| 41 | + // 如果还没有读取到长度信息 | ||
| 42 | + if (dataLength == -1) { | ||
| 43 | + // 从包头中读取数据长度 | ||
| 44 | + dataLength = buffer.getInt(0); | ||
| 45 | + | ||
| 46 | + // 校验数据长度 | ||
| 47 | + if (dataLength <= 0 || dataLength > 1024 * 1024) { // 最大1MB | ||
| 48 | + log.error("[handle][无效的数据包长度: {}]", dataLength); | ||
| 49 | + reset(); | ||
| 50 | + return; | ||
| 51 | + } | ||
| 52 | + | ||
| 53 | + // 切换到读取数据模式 | ||
| 54 | + parser.fixedSizeMode(dataLength); | ||
| 55 | + | ||
| 56 | + log.debug("[handle][读取到数据包长度: {}]", dataLength); | ||
| 57 | + } else { | ||
| 58 | + // 读取到完整的数据包 | ||
| 59 | + log.debug("[handle][读取到完整数据包,长度: {}]", buffer.length()); | ||
| 60 | + | ||
| 61 | + // 处理数据包 | ||
| 62 | + try { | ||
| 63 | + receiveHandler.accept(buffer); | ||
| 64 | + } catch (Exception e) { | ||
| 65 | + log.error("[handle][处理数据包失败]", e); | ||
| 66 | + } | ||
| 67 | + | ||
| 68 | + // 重置状态,准备读取下一个数据包 | ||
| 69 | + reset(); | ||
| 70 | + } | ||
| 71 | + } catch (Exception e) { | ||
| 72 | + log.error("[handle][数据包处理异常]", e); | ||
| 73 | + reset(); | ||
| 74 | + } | ||
| 75 | + } | ||
| 76 | + | ||
| 77 | + /** | ||
| 78 | + * 重置解析器状态 | ||
| 79 | + */ | ||
| 80 | + private void reset() { | ||
| 81 | + dataLength = -1; | ||
| 82 | + parser.fixedSizeMode(4); | ||
| 83 | + } | ||
| 84 | + }); | ||
| 85 | + | ||
| 86 | + return parser; | ||
| 87 | + } | ||
| 88 | + | ||
| 89 | + /** | ||
| 90 | + * 创建带异常处理的数据包解析器 | ||
| 91 | + * | ||
| 92 | + * @param receiveHandler 接收处理器 | ||
| 93 | + * @param exceptionHandler 异常处理器 | ||
| 94 | + * @return RecordParser 解析器 | ||
| 95 | + */ | ||
| 96 | + public static RecordParser createParserWithExceptionHandler( | ||
| 97 | + Consumer<Buffer> receiveHandler, | ||
| 98 | + Consumer<Throwable> exceptionHandler) { | ||
| 99 | + | ||
| 100 | + RecordParser parser = RecordParser.newFixed(4); | ||
| 101 | + | ||
| 102 | + parser.setOutput(new Handler<Buffer>() { | ||
| 103 | + private int dataLength = -1; | ||
| 104 | + | ||
| 105 | + @Override | ||
| 106 | + public void handle(Buffer buffer) { | ||
| 107 | + try { | ||
| 108 | + if (dataLength == -1) { | ||
| 109 | + dataLength = buffer.getInt(0); | ||
| 110 | + | ||
| 111 | + if (dataLength <= 0 || dataLength > 1024 * 1024) { | ||
| 112 | + throw new IllegalArgumentException("无效的数据包长度: " + dataLength); | ||
| 113 | + } | ||
| 114 | + | ||
| 115 | + parser.fixedSizeMode(dataLength); | ||
| 116 | + log.debug("[handle][读取到数据包长度: {}]", dataLength); | ||
| 117 | + } else { | ||
| 118 | + log.debug("[handle][读取到完整数据包,长度: {}]", buffer.length()); | ||
| 119 | + | ||
| 120 | + try { | ||
| 121 | + receiveHandler.accept(buffer); | ||
| 122 | + } catch (Exception e) { | ||
| 123 | + exceptionHandler.accept(e); | ||
| 124 | + } | ||
| 125 | + | ||
| 126 | + reset(); | ||
| 127 | + } | ||
| 128 | + } catch (Exception e) { | ||
| 129 | + exceptionHandler.accept(e); | ||
| 130 | + reset(); | ||
| 131 | + } | ||
| 132 | + } | ||
| 133 | + | ||
| 134 | + private void reset() { | ||
| 135 | + dataLength = -1; | ||
| 136 | + parser.fixedSizeMode(4); | ||
| 137 | + } | ||
| 138 | + }); | ||
| 139 | + | ||
| 140 | + return parser; | ||
| 141 | + } | ||
| 142 | + | ||
| 143 | + /** | ||
| 144 | + * 创建简单的数据包解析器(用于测试) | ||
| 145 | + * | ||
| 146 | + * @param receiveHandler 接收处理器 | ||
| 147 | + * @return RecordParser 解析器 | ||
| 148 | + */ | ||
| 149 | + public static RecordParser createSimpleParser(Consumer<TcpDataPackage> receiveHandler) { | ||
| 150 | + return createParser(buffer -> { | ||
| 151 | + try { | ||
| 152 | + TcpDataPackage dataPackage = TcpDataDecoder.decode(buffer); | ||
| 153 | + receiveHandler.accept(dataPackage); | ||
| 154 | + } catch (Exception e) { | ||
| 155 | + log.error("[createSimpleParser][解码数据包失败]", e); | ||
| 156 | + } | ||
| 157 | + }); | ||
| 158 | + } | ||
| 159 | +} | ||
| 0 | \ No newline at end of file | 160 | \ No newline at end of file |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/router/IotTcpConnectionHandler.java deleted
| 1 | -package cn.iocoder.yudao.module.iot.gateway.protocol.tcp.router; | ||
| 2 | - | ||
| 3 | -import cn.hutool.core.util.BooleanUtil; | ||
| 4 | -import cn.iocoder.yudao.framework.common.pojo.CommonResult; | ||
| 5 | -import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; | ||
| 6 | -import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; | ||
| 7 | -import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO; | ||
| 8 | -import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; | ||
| 9 | -import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpConnectionManager; | ||
| 10 | -import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService; | ||
| 11 | -import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; | ||
| 12 | -import io.vertx.core.Handler; | ||
| 13 | -import io.vertx.core.buffer.Buffer; | ||
| 14 | -import io.vertx.core.net.NetSocket; | ||
| 15 | -import io.vertx.core.parsetools.RecordParser; | ||
| 16 | -import lombok.RequiredArgsConstructor; | ||
| 17 | -import lombok.extern.slf4j.Slf4j; | ||
| 18 | - | ||
| 19 | -/** | ||
| 20 | - * IoT TCP 连接处理器 | ||
| 21 | - * <p> | ||
| 22 | - * 核心负责: | ||
| 23 | - * 1. 【认证】创建连接后,设备需要发送认证消息,认证通过后,才能进行后续的通信 | ||
| 24 | - * 2. 【消息处理】接收设备发送的消息,解码后,发送到消息队列 | ||
| 25 | - * 3. 【断开】设备断开连接后,清理资源 | ||
| 26 | - * | ||
| 27 | - * @author 芋道源码 | ||
| 28 | - */ | ||
| 29 | -@RequiredArgsConstructor | ||
| 30 | -@Slf4j | ||
| 31 | -public class IotTcpConnectionHandler implements Handler<Buffer> { | ||
| 32 | - | ||
| 33 | - private final NetSocket socket; | ||
| 34 | - /** | ||
| 35 | - * 是否已认证 | ||
| 36 | - */ | ||
| 37 | - private boolean authenticated = false; | ||
| 38 | - /** | ||
| 39 | - * 设备信息 | ||
| 40 | - */ | ||
| 41 | - private IotDeviceRespDTO device; | ||
| 42 | - | ||
| 43 | - private final IotTcpConnectionManager connectionManager; | ||
| 44 | - | ||
| 45 | - private final IotDeviceMessageService messageService; | ||
| 46 | - | ||
| 47 | - private final IotDeviceService deviceService; | ||
| 48 | - | ||
| 49 | - private final IotDeviceCommonApi deviceApi; | ||
| 50 | - | ||
| 51 | - private final String serverId; | ||
| 52 | - | ||
| 53 | - public void start() { | ||
| 54 | - // 1. 设置解析器 | ||
| 55 | - final RecordParser parser = RecordParser.newDelimited("\n", this); | ||
| 56 | - socket.handler(parser); | ||
| 57 | - | ||
| 58 | - // 2. 设置处理器 | ||
| 59 | - socket.closeHandler(v -> handleConnectionClose()); | ||
| 60 | - socket.exceptionHandler(this::handleException); | ||
| 61 | - } | ||
| 62 | - | ||
| 63 | - @Override | ||
| 64 | - public void handle(Buffer buffer) { | ||
| 65 | - log.info("[handle][接收到数据: {}]", buffer); | ||
| 66 | - try { | ||
| 67 | - // TODO @haohao:可以调研下,做个对比表格哈; | ||
| 68 | - // 1. 处理认证 | ||
| 69 | - if (!authenticated) { | ||
| 70 | - handleAuthentication(buffer); | ||
| 71 | - return; | ||
| 72 | - } | ||
| 73 | - // 2. 处理消息 | ||
| 74 | - handleMessage(buffer); | ||
| 75 | - } catch (Exception e) { | ||
| 76 | - log.error("[handle][处理异常]", e); | ||
| 77 | - socket.close(); | ||
| 78 | - } | ||
| 79 | - } | ||
| 80 | - | ||
| 81 | - private void handleAuthentication(Buffer buffer) { | ||
| 82 | - // 1. 解析认证信息 | ||
| 83 | - // TODO @芋艿:这里的认证协议,需要和设备端约定。默认为 productKey,deviceName,password | ||
| 84 | - // TODO @haohao:这里,要不也 json 解析?类似 http 是 { | ||
| 85 | - // "clientId": "4aymZgOTOOCrDKRT.small", | ||
| 86 | - // "username": "small&4aymZgOTOOCrDKRT", | ||
| 87 | - // "password": "509e2b08f7598eb139d276388c600435913ba4c94cd0d50aebc5c0d1855bcb75" | ||
| 88 | - //} | ||
| 89 | - String[] parts = buffer.toString().split(","); | ||
| 90 | - if (parts.length != 3) { | ||
| 91 | - log.error("[handleAuthentication][认证信息({})格式不正确]", buffer); | ||
| 92 | - socket.close(); | ||
| 93 | - return; | ||
| 94 | - } | ||
| 95 | - String productKey = parts[0]; | ||
| 96 | - String deviceName = parts[1]; | ||
| 97 | - String password = parts[2]; | ||
| 98 | - | ||
| 99 | - // 2. 执行认证 | ||
| 100 | - CommonResult<Boolean> authResult = deviceApi.authDevice(new IotDeviceAuthReqDTO() | ||
| 101 | - .setClientId(socket.remoteAddress().toString()).setUsername(productKey + "/" + deviceName) | ||
| 102 | - .setPassword(password)); | ||
| 103 | - if (authResult.isError() || !BooleanUtil.isTrue(authResult.getData())) { | ||
| 104 | - log.error("[handleAuthentication][认证失败,productKey({}) deviceName({}) password({})]", productKey, deviceName, | ||
| 105 | - password); | ||
| 106 | - socket.close(); | ||
| 107 | - return; | ||
| 108 | - } | ||
| 109 | - | ||
| 110 | - // 3. 认证成功 | ||
| 111 | - this.authenticated = true; | ||
| 112 | - this.device = deviceService.getDeviceFromCache(productKey, deviceName); | ||
| 113 | - connectionManager.addConnection(String.valueOf(device.getId()), socket); | ||
| 114 | - | ||
| 115 | - // 4. 发送上线消息 | ||
| 116 | - IotDeviceMessage message = IotDeviceMessage.buildStateUpdateOnline(); | ||
| 117 | - messageService.sendDeviceMessage(message, productKey, deviceName, serverId); | ||
| 118 | - log.info("[handleAuthentication][认证成功]"); | ||
| 119 | - } | ||
| 120 | - | ||
| 121 | - private void handleMessage(Buffer buffer) { | ||
| 122 | - // 1. 解码消息 | ||
| 123 | - IotDeviceMessage message = messageService.decodeDeviceMessage(buffer.getBytes(), | ||
| 124 | - device.getProductKey(), device.getDeviceName()); | ||
| 125 | - if (message == null) { | ||
| 126 | - log.warn("[handleMessage][解码消息失败]"); | ||
| 127 | - return; | ||
| 128 | - } | ||
| 129 | - // 2. 发送消息到队列 | ||
| 130 | - messageService.sendDeviceMessage(message, device.getProductKey(), device.getDeviceName(), serverId); | ||
| 131 | - } | ||
| 132 | - | ||
| 133 | - private void handleConnectionClose() { | ||
| 134 | - // 1. 移除连接 | ||
| 135 | - connectionManager.removeConnection(socket); | ||
| 136 | - // 2. 发送离线消息 | ||
| 137 | - if (device != null) { | ||
| 138 | - IotDeviceMessage message = IotDeviceMessage.buildStateOffline(); | ||
| 139 | - messageService.sendDeviceMessage(message, device.getProductKey(), device.getDeviceName(), serverId); | ||
| 140 | - } | ||
| 141 | - } | ||
| 142 | - | ||
| 143 | - private void handleException(Throwable e) { | ||
| 144 | - log.error("[handleException][连接({}) 发生异常]", socket.remoteAddress(), e); | ||
| 145 | - socket.close(); | ||
| 146 | - } | ||
| 147 | - | ||
| 148 | -} | ||
| 149 | \ No newline at end of file | 0 | \ No newline at end of file |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/router/IotTcpDownstreamHandler.java
| 1 | -package cn.iocoder.yudao.module.iot.gateway.protocol.tcp.router; | ||
| 2 | - | ||
| 3 | -import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; | ||
| 4 | -import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpConnectionManager; | ||
| 5 | -import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; | ||
| 6 | -import io.vertx.core.buffer.Buffer; | ||
| 7 | -import io.vertx.core.net.NetSocket; | ||
| 8 | -import lombok.RequiredArgsConstructor; | ||
| 9 | -import lombok.extern.slf4j.Slf4j; | ||
| 10 | -import org.springframework.stereotype.Component; | ||
| 11 | - | ||
| 12 | -/** | ||
| 13 | - * IoT 网关 TCP 下行消息处理器 | ||
| 14 | - * <p> | ||
| 15 | - * 从消息总线接收到下行消息,然后发布到 TCP 连接,从而被设备所接收 | ||
| 16 | - * | ||
| 17 | - * @author 芋道源码 | ||
| 18 | - */ | ||
| 19 | -@Slf4j | ||
| 20 | -@Component | ||
| 21 | -@RequiredArgsConstructor | ||
| 22 | -public class IotTcpDownstreamHandler { | ||
| 23 | - | ||
| 24 | - private final IotTcpConnectionManager connectionManager; | ||
| 25 | - | ||
| 26 | - private final IotDeviceMessageService messageService; | ||
| 27 | - | ||
| 28 | - /** | ||
| 29 | - * 处理下行消息 | ||
| 30 | - * | ||
| 31 | - * @param message 设备消息 | ||
| 32 | - */ | ||
| 33 | - public void handle(IotDeviceMessage message) { | ||
| 34 | - // 1. 获取设备对应的连接 | ||
| 35 | - NetSocket socket = connectionManager.getConnection(String.valueOf(message.getDeviceId())); | ||
| 36 | - if (socket == null) { | ||
| 37 | - log.error("[handle][设备({})的连接不存在]", message.getDeviceId()); | ||
| 38 | - return; | ||
| 39 | - } | ||
| 40 | - | ||
| 41 | - // 2. 编码消息 | ||
| 42 | - byte[] bytes = messageService.encodeDeviceMessage(message, null, null); | ||
| 43 | - | ||
| 44 | - // 3. 发送消息 | ||
| 45 | - socket.write(Buffer.buffer(bytes)); | ||
| 46 | - // TODO @芋艿:这里的换行符,需要和设备端约定 | ||
| 47 | - // TODO @haohao:tcp 要不定长?很少 \n 哈。然后有个 magic number;可以参考 dubbo rpc; | ||
| 48 | - socket.write("\n"); | ||
| 49 | - } | ||
| 50 | - | 1 | +package cn.iocoder.yudao.module.iot.gateway.protocol.tcp.router; |
| 2 | + | ||
| 3 | +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; | ||
| 4 | +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; | ||
| 5 | +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.client.TcpDeviceClient; | ||
| 6 | +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.manager.TcpDeviceConnectionManager; | ||
| 7 | +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.protocol.TcpDataDecoder; | ||
| 8 | +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.protocol.TcpDataEncoder; | ||
| 9 | +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.protocol.TcpDataPackage; | ||
| 10 | +import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; | ||
| 11 | +import com.alibaba.fastjson.JSON; | ||
| 12 | +import io.vertx.core.buffer.Buffer; | ||
| 13 | +import lombok.extern.slf4j.Slf4j; | ||
| 14 | + | ||
| 15 | +/** | ||
| 16 | + * IoT 网关 TCP 下行消息处理器 | ||
| 17 | + * <p> | ||
| 18 | + * 负责处理从业务系统发送到设备的下行消息,包括: | ||
| 19 | + * 1. 属性设置 | ||
| 20 | + * 2. 服务调用 | ||
| 21 | + * 3. 属性获取 | ||
| 22 | + * 4. 配置下发 | ||
| 23 | + * 5. OTA 升级 | ||
| 24 | + * | ||
| 25 | + * @author 芋道源码 | ||
| 26 | + */ | ||
| 27 | +@Slf4j | ||
| 28 | +public class IotTcpDownstreamHandler { | ||
| 29 | + | ||
| 30 | + private final TcpDeviceConnectionManager connectionManager; | ||
| 31 | + | ||
| 32 | + private final IotDeviceMessageService messageService; | ||
| 33 | + | ||
| 34 | + public IotTcpDownstreamHandler(TcpDeviceConnectionManager connectionManager, | ||
| 35 | + IotDeviceMessageService messageService) { | ||
| 36 | + this.connectionManager = connectionManager; | ||
| 37 | + this.messageService = messageService; | ||
| 38 | + } | ||
| 39 | + | ||
| 40 | + /** | ||
| 41 | + * 处理下行消息 | ||
| 42 | + * | ||
| 43 | + * @param message 设备消息 | ||
| 44 | + */ | ||
| 45 | + public void handle(IotDeviceMessage message) { | ||
| 46 | + try { | ||
| 47 | + log.info("[handle][处理下行消息] 设备ID: {}, 方法: {}, 消息ID: {}", | ||
| 48 | + message.getDeviceId(), message.getMethod(), message.getId()); | ||
| 49 | + | ||
| 50 | + // 1. 获取设备连接 | ||
| 51 | + TcpDeviceClient client = connectionManager.getClientByDeviceId(message.getDeviceId()); | ||
| 52 | + if (client == null || !client.isOnline()) { | ||
| 53 | + log.error("[handle][设备({})不在线,无法发送下行消息]", message.getDeviceId()); | ||
| 54 | + return; | ||
| 55 | + } | ||
| 56 | + | ||
| 57 | + // 2. 根据消息方法处理不同类型的下行消息 | ||
| 58 | + switch (message.getMethod()) { | ||
| 59 | + case "thing.property.set": | ||
| 60 | + handlePropertySet(client, message); | ||
| 61 | + break; | ||
| 62 | + case "thing.property.get": | ||
| 63 | + handlePropertyGet(client, message); | ||
| 64 | + break; | ||
| 65 | + case "thing.service.invoke": | ||
| 66 | + handleServiceInvoke(client, message); | ||
| 67 | + break; | ||
| 68 | + case "thing.config.push": | ||
| 69 | + handleConfigPush(client, message); | ||
| 70 | + break; | ||
| 71 | + case "thing.ota.upgrade": | ||
| 72 | + handleOtaUpgrade(client, message); | ||
| 73 | + break; | ||
| 74 | + default: | ||
| 75 | + log.warn("[handle][未知的下行消息方法: {}]", message.getMethod()); | ||
| 76 | + break; | ||
| 77 | + } | ||
| 78 | + | ||
| 79 | + } catch (Exception e) { | ||
| 80 | + log.error("[handle][处理下行消息失败]", e); | ||
| 81 | + } | ||
| 82 | + } | ||
| 83 | + | ||
| 84 | + /** | ||
| 85 | + * 处理属性设置 | ||
| 86 | + * | ||
| 87 | + * @param client 设备客户端 | ||
| 88 | + * @param message 设备消息 | ||
| 89 | + */ | ||
| 90 | + private void handlePropertySet(TcpDeviceClient client, IotDeviceMessage message) { | ||
| 91 | + try { | ||
| 92 | + log.info("[handlePropertySet][属性设置] 设备地址: {}, 属性: {}", | ||
| 93 | + client.getDeviceAddr(), message.getParams()); | ||
| 94 | + | ||
| 95 | + // 使用编解码器发送消息,降级处理使用原始编码 | ||
| 96 | + sendMessageWithCodec(client, message, "handlePropertySet", () -> { | ||
| 97 | + String payload = JSON.toJSONString(message.getParams()); | ||
| 98 | + short mid = generateMessageId(); | ||
| 99 | + | ||
| 100 | + Buffer buffer = TcpDataEncoder.createPropertySetPackage( | ||
| 101 | + client.getDeviceAddr(), mid, payload); | ||
| 102 | + client.sendMessage(buffer); | ||
| 103 | + | ||
| 104 | + log.debug("[handlePropertySet][属性设置消息已发送(降级)] 设备地址: {}, 消息序号: {}", | ||
| 105 | + client.getDeviceAddr(), mid); | ||
| 106 | + }); | ||
| 107 | + | ||
| 108 | + } catch (Exception e) { | ||
| 109 | + log.error("[handlePropertySet][属性设置失败]", e); | ||
| 110 | + } | ||
| 111 | + } | ||
| 112 | + | ||
| 113 | + /** | ||
| 114 | + * 处理属性获取 | ||
| 115 | + * | ||
| 116 | + * @param client 设备客户端 | ||
| 117 | + * @param message 设备消息 | ||
| 118 | + */ | ||
| 119 | + private void handlePropertyGet(TcpDeviceClient client, IotDeviceMessage message) { | ||
| 120 | + try { | ||
| 121 | + log.info("[handlePropertyGet][属性获取] 设备地址: {}, 属性列表: {}", | ||
| 122 | + client.getDeviceAddr(), message.getParams()); | ||
| 123 | + | ||
| 124 | + // 使用编解码器发送消息,降级处理使用原始编码 | ||
| 125 | + sendMessageWithCodec(client, message, "handlePropertyGet", () -> { | ||
| 126 | + String payload = JSON.toJSONString(message.getParams()); | ||
| 127 | + short mid = generateMessageId(); | ||
| 128 | + | ||
| 129 | + Buffer buffer = TcpDataEncoder.createPropertyGetPackage( | ||
| 130 | + client.getDeviceAddr(), mid, payload); | ||
| 131 | + client.sendMessage(buffer); | ||
| 132 | + | ||
| 133 | + log.debug("[handlePropertyGet][属性获取消息已发送(降级)] 设备地址: {}, 消息序号: {}", | ||
| 134 | + client.getDeviceAddr(), mid); | ||
| 135 | + }); | ||
| 136 | + | ||
| 137 | + } catch (Exception e) { | ||
| 138 | + log.error("[handlePropertyGet][属性获取失败]", e); | ||
| 139 | + } | ||
| 140 | + } | ||
| 141 | + | ||
| 142 | + /** | ||
| 143 | + * 处理服务调用 | ||
| 144 | + * | ||
| 145 | + * @param client 设备客户端 | ||
| 146 | + * @param message 设备消息 | ||
| 147 | + */ | ||
| 148 | + private void handleServiceInvoke(TcpDeviceClient client, IotDeviceMessage message) { | ||
| 149 | + try { | ||
| 150 | + log.info("[handleServiceInvoke][服务调用] 设备地址: {}, 服务参数: {}", | ||
| 151 | + client.getDeviceAddr(), message.getParams()); | ||
| 152 | + | ||
| 153 | + // 1. 构建服务调用数据包 | ||
| 154 | + String payload = JSON.toJSONString(message.getParams()); | ||
| 155 | + short mid = generateMessageId(); | ||
| 156 | + | ||
| 157 | + Buffer buffer = TcpDataEncoder.createServiceInvokePackage( | ||
| 158 | + client.getDeviceAddr(), mid, payload); | ||
| 159 | + | ||
| 160 | + // 2. 发送消息 | ||
| 161 | + client.sendMessage(buffer); | ||
| 162 | + | ||
| 163 | + log.debug("[handleServiceInvoke][服务调用消息已发送] 设备地址: {}, 消息序号: {}", | ||
| 164 | + client.getDeviceAddr(), mid); | ||
| 165 | + | ||
| 166 | + } catch (Exception e) { | ||
| 167 | + log.error("[handleServiceInvoke][服务调用失败]", e); | ||
| 168 | + } | ||
| 169 | + } | ||
| 170 | + | ||
| 171 | + /** | ||
| 172 | + * 处理配置推送 | ||
| 173 | + * | ||
| 174 | + * @param client 设备客户端 | ||
| 175 | + * @param message 设备消息 | ||
| 176 | + */ | ||
| 177 | + private void handleConfigPush(TcpDeviceClient client, IotDeviceMessage message) { | ||
| 178 | + try { | ||
| 179 | + log.info("[handleConfigPush][配置推送] 设备地址: {}, 配置: {}", | ||
| 180 | + client.getDeviceAddr(), message.getParams()); | ||
| 181 | + | ||
| 182 | + // 1. 构建配置推送数据包 | ||
| 183 | + String payload = JSON.toJSONString(message.getParams()); | ||
| 184 | + short mid = generateMessageId(); | ||
| 185 | + | ||
| 186 | + Buffer buffer = TcpDataEncoder.createDataDownPackage( | ||
| 187 | + client.getDeviceAddr(), mid, payload); | ||
| 188 | + | ||
| 189 | + // 2. 发送消息 | ||
| 190 | + client.sendMessage(buffer); | ||
| 191 | + | ||
| 192 | + log.debug("[handleConfigPush][配置推送消息已发送] 设备地址: {}, 消息序号: {}", | ||
| 193 | + client.getDeviceAddr(), mid); | ||
| 194 | + | ||
| 195 | + } catch (Exception e) { | ||
| 196 | + log.error("[handleConfigPush][配置推送失败]", e); | ||
| 197 | + } | ||
| 198 | + } | ||
| 199 | + | ||
| 200 | + /** | ||
| 201 | + * 处理 OTA 升级 | ||
| 202 | + * | ||
| 203 | + * @param client 设备客户端 | ||
| 204 | + * @param message 设备消息 | ||
| 205 | + */ | ||
| 206 | + private void handleOtaUpgrade(TcpDeviceClient client, IotDeviceMessage message) { | ||
| 207 | + try { | ||
| 208 | + log.info("[handleOtaUpgrade][OTA升级] 设备地址: {}, 升级信息: {}", | ||
| 209 | + client.getDeviceAddr(), message.getParams()); | ||
| 210 | + | ||
| 211 | + // 1. 构建 OTA 升级数据包 | ||
| 212 | + String payload = JSON.toJSONString(message.getParams()); | ||
| 213 | + short mid = generateMessageId(); | ||
| 214 | + | ||
| 215 | + Buffer buffer = TcpDataEncoder.createDataDownPackage( | ||
| 216 | + client.getDeviceAddr(), mid, payload); | ||
| 217 | + | ||
| 218 | + // 2. 发送消息 | ||
| 219 | + client.sendMessage(buffer); | ||
| 220 | + | ||
| 221 | + log.debug("[handleOtaUpgrade][OTA升级消息已发送] 设备地址: {}, 消息序号: {}", | ||
| 222 | + client.getDeviceAddr(), mid); | ||
| 223 | + | ||
| 224 | + } catch (Exception e) { | ||
| 225 | + log.error("[handleOtaUpgrade][OTA升级失败]", e); | ||
| 226 | + } | ||
| 227 | + } | ||
| 228 | + | ||
| 229 | + /** | ||
| 230 | + * 处理自定义下行消息 | ||
| 231 | + * | ||
| 232 | + * @param client 设备客户端 | ||
| 233 | + * @param message 设备消息 | ||
| 234 | + * @param code 功能码 | ||
| 235 | + */ | ||
| 236 | + private void handleCustomMessage(TcpDeviceClient client, IotDeviceMessage message, short code) { | ||
| 237 | + try { | ||
| 238 | + log.info("[handleCustomMessage][自定义消息] 设备地址: {}, 功能码: {}, 数据: {}", | ||
| 239 | + client.getDeviceAddr(), code, message.getParams()); | ||
| 240 | + | ||
| 241 | + // 1. 构建自定义数据包 | ||
| 242 | + String payload = JSON.toJSONString(message.getParams()); | ||
| 243 | + short mid = generateMessageId(); | ||
| 244 | + | ||
| 245 | + TcpDataPackage dataPackage = TcpDataPackage.builder() | ||
| 246 | + .addr(client.getDeviceAddr()) | ||
| 247 | + .code(code) | ||
| 248 | + .mid(mid) | ||
| 249 | + .payload(payload) | ||
| 250 | + .build(); | ||
| 251 | + | ||
| 252 | + Buffer buffer = TcpDataEncoder.encode(dataPackage); | ||
| 253 | + | ||
| 254 | + // 2. 发送消息 | ||
| 255 | + client.sendMessage(buffer); | ||
| 256 | + | ||
| 257 | + log.debug("[handleCustomMessage][自定义消息已发送] 设备地址: {}, 功能码: {}, 消息序号: {}", | ||
| 258 | + client.getDeviceAddr(), code, mid); | ||
| 259 | + | ||
| 260 | + } catch (Exception e) { | ||
| 261 | + log.error("[handleCustomMessage][自定义消息发送失败]", e); | ||
| 262 | + } | ||
| 263 | + } | ||
| 264 | + | ||
| 265 | + /** | ||
| 266 | + * 批量发送下行消息 | ||
| 267 | + * | ||
| 268 | + * @param deviceIds 设备ID列表 | ||
| 269 | + * @param message 设备消息 | ||
| 270 | + */ | ||
| 271 | + public void broadcastMessage(Long[] deviceIds, IotDeviceMessage message) { | ||
| 272 | + try { | ||
| 273 | + log.info("[broadcastMessage][批量发送消息] 设备数量: {}, 方法: {}", | ||
| 274 | + deviceIds.length, message.getMethod()); | ||
| 275 | + | ||
| 276 | + for (Long deviceId : deviceIds) { | ||
| 277 | + // 创建副本消息(避免消息ID冲突) | ||
| 278 | + IotDeviceMessage copyMessage = IotDeviceMessage.of( | ||
| 279 | + message.getRequestId(), | ||
| 280 | + message.getMethod(), | ||
| 281 | + message.getParams(), | ||
| 282 | + message.getData(), | ||
| 283 | + message.getCode(), | ||
| 284 | + message.getMsg()); | ||
| 285 | + copyMessage.setDeviceId(deviceId); | ||
| 286 | + | ||
| 287 | + // 处理单个设备消息 | ||
| 288 | + handle(copyMessage); | ||
| 289 | + } | ||
| 290 | + | ||
| 291 | + } catch (Exception e) { | ||
| 292 | + log.error("[broadcastMessage][批量发送消息失败]", e); | ||
| 293 | + } | ||
| 294 | + } | ||
| 295 | + | ||
| 296 | + /** | ||
| 297 | + * 检查设备是否支持指定方法 | ||
| 298 | + * | ||
| 299 | + * @param client 设备客户端 | ||
| 300 | + * @param method 消息方法 | ||
| 301 | + * @return 是否支持 | ||
| 302 | + */ | ||
| 303 | + private boolean isMethodSupported(TcpDeviceClient client, String method) { | ||
| 304 | + // TODO: 可以根据设备类型或产品信息判断是否支持特定方法 | ||
| 305 | + return IotDeviceMessageMethodEnum.of(method) != null; | ||
| 306 | + } | ||
| 307 | + | ||
| 308 | + /** | ||
| 309 | + * 生成消息序号 | ||
| 310 | + * | ||
| 311 | + * @return 消息序号 | ||
| 312 | + */ | ||
| 313 | + private short generateMessageId() { | ||
| 314 | + return (short) (System.currentTimeMillis() % Short.MAX_VALUE); | ||
| 315 | + } | ||
| 316 | + | ||
| 317 | + /** | ||
| 318 | + * 使用编解码器发送消息 | ||
| 319 | + * | ||
| 320 | + * @param client 设备客户端 | ||
| 321 | + * @param message 设备消息 | ||
| 322 | + * @param methodName 方法名称 | ||
| 323 | + * @param fallbackAction 降级处理逻辑 | ||
| 324 | + */ | ||
| 325 | + private void sendMessageWithCodec(TcpDeviceClient client, IotDeviceMessage message, | ||
| 326 | + String methodName, Runnable fallbackAction) { | ||
| 327 | + try { | ||
| 328 | + // 1. 使用编解码器编码消息 | ||
| 329 | + byte[] messageBytes = messageService.encodeDeviceMessage( | ||
| 330 | + message, client.getProductKey(), client.getDeviceName()); | ||
| 331 | + | ||
| 332 | + // 2. 解析编码后的数据包并设置设备地址和消息序号 | ||
| 333 | + Buffer buffer = Buffer.buffer(messageBytes); | ||
| 334 | + TcpDataPackage dataPackage = TcpDataDecoder.decode(buffer); | ||
| 335 | + dataPackage.setAddr(client.getDeviceAddr()); | ||
| 336 | + dataPackage.setMid(generateMessageId()); | ||
| 337 | + | ||
| 338 | + // 3. 重新编码并发送 | ||
| 339 | + Buffer finalBuffer = TcpDataEncoder.encode(dataPackage); | ||
| 340 | + client.sendMessage(finalBuffer); | ||
| 341 | + | ||
| 342 | + log.debug("[{}][消息已发送] 设备地址: {}, 消息序号: {}", | ||
| 343 | + methodName, client.getDeviceAddr(), dataPackage.getMid()); | ||
| 344 | + | ||
| 345 | + } catch (Exception e) { | ||
| 346 | + log.warn("[{}][使用编解码器编码失败,降级使用原始编码] 错误: {}", | ||
| 347 | + methodName, e.getMessage()); | ||
| 348 | + | ||
| 349 | + // 执行降级处理 | ||
| 350 | + if (fallbackAction != null) { | ||
| 351 | + fallbackAction.run(); | ||
| 352 | + } | ||
| 353 | + } | ||
| 354 | + } | ||
| 355 | + | ||
| 356 | + /** | ||
| 357 | + * 获取连接统计信息 | ||
| 358 | + * | ||
| 359 | + * @return 连接统计信息 | ||
| 360 | + */ | ||
| 361 | + public String getHandlerStatistics() { | ||
| 362 | + return String.format("TCP下游处理器 - %s", connectionManager.getConnectionStatus()); | ||
| 363 | + } | ||
| 51 | } | 364 | } |
| 52 | \ No newline at end of file | 365 | \ No newline at end of file |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/router/IotTcpUpstreamHandler.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.iot.gateway.protocol.tcp.router; | ||
| 2 | + | ||
| 3 | +import cn.hutool.core.util.IdUtil; | ||
| 4 | +import cn.hutool.json.JSONObject; | ||
| 5 | +import cn.hutool.json.JSONUtil; | ||
| 6 | +import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; | ||
| 7 | +import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO; | ||
| 8 | +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; | ||
| 9 | +import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties; | ||
| 10 | +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.client.TcpDeviceClient; | ||
| 11 | +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.manager.TcpDeviceConnectionManager; | ||
| 12 | +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.protocol.TcpDataDecoder; | ||
| 13 | +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.protocol.TcpDataEncoder; | ||
| 14 | +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.protocol.TcpDataPackage; | ||
| 15 | +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.protocol.TcpDataReader; | ||
| 16 | +import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService; | ||
| 17 | +import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; | ||
| 18 | +import io.vertx.core.Handler; | ||
| 19 | +import io.vertx.core.buffer.Buffer; | ||
| 20 | +import io.vertx.core.net.NetSocket; | ||
| 21 | +import io.vertx.core.parsetools.RecordParser; | ||
| 22 | +import lombok.RequiredArgsConstructor; | ||
| 23 | +import lombok.extern.slf4j.Slf4j; | ||
| 24 | + | ||
| 25 | +/** | ||
| 26 | + * IoT 网关 TCP 上行消息处理器 | ||
| 27 | + * <p> | ||
| 28 | + * 核心负责: | ||
| 29 | + * 1. 【设备注册】设备连接后发送注册消息,注册成功后可以进行通信 | ||
| 30 | + * 2. 【心跳处理】定期接收设备心跳消息,维持连接状态 | ||
| 31 | + * 3. 【数据上报】接收设备数据上报和事件上报 | ||
| 32 | + * 4. 【连接管理】管理连接的建立、维护和清理 | ||
| 33 | + * | ||
| 34 | + * @author 芋道源码 | ||
| 35 | + */ | ||
| 36 | +@RequiredArgsConstructor | ||
| 37 | +@Slf4j | ||
| 38 | +public class IotTcpUpstreamHandler implements Handler<NetSocket> { | ||
| 39 | + | ||
| 40 | + private final IotGatewayProperties.TcpProperties tcpConfig; | ||
| 41 | + | ||
| 42 | + private final TcpDeviceConnectionManager connectionManager; | ||
| 43 | + | ||
| 44 | + private final IotDeviceService deviceService; | ||
| 45 | + | ||
| 46 | + private final IotDeviceMessageService messageService; | ||
| 47 | + | ||
| 48 | + private final IotDeviceCommonApi deviceApi; | ||
| 49 | + | ||
| 50 | + private final String serverId; | ||
| 51 | + | ||
| 52 | + @Override | ||
| 53 | + public void handle(NetSocket socket) { | ||
| 54 | + log.info("[handle][收到设备连接: {}]", socket.remoteAddress()); | ||
| 55 | + | ||
| 56 | + // 创建客户端ID和设备客户端 | ||
| 57 | + String clientId = IdUtil.simpleUUID() + "_" + socket.remoteAddress(); | ||
| 58 | + TcpDeviceClient client = new TcpDeviceClient(clientId, tcpConfig.getKeepAliveTimeoutMs()); | ||
| 59 | + | ||
| 60 | + try { | ||
| 61 | + // 设置连接异常和关闭处理 | ||
| 62 | + socket.exceptionHandler(ex -> { | ||
| 63 | + log.error("[handle][连接({})异常]", socket.remoteAddress(), ex); | ||
| 64 | + handleConnectionClose(client); | ||
| 65 | + }); | ||
| 66 | + | ||
| 67 | + socket.closeHandler(v -> { | ||
| 68 | + log.info("[handle][连接({})关闭]", socket.remoteAddress()); | ||
| 69 | + handleConnectionClose(client); | ||
| 70 | + }); | ||
| 71 | + | ||
| 72 | + // 设置网络连接 | ||
| 73 | + client.setSocket(socket); | ||
| 74 | + | ||
| 75 | + // 创建数据解析器 | ||
| 76 | + RecordParser parser = TcpDataReader.createParser(buffer -> { | ||
| 77 | + try { | ||
| 78 | + handleDataPackage(client, buffer); | ||
| 79 | + } catch (Exception e) { | ||
| 80 | + log.error("[handle][处理数据包异常]", e); | ||
| 81 | + } | ||
| 82 | + }); | ||
| 83 | + | ||
| 84 | + // 设置解析器 | ||
| 85 | + client.setParser(parser); | ||
| 86 | + | ||
| 87 | + log.info("[handle][设备连接处理器初始化完成: {}]", clientId); | ||
| 88 | + | ||
| 89 | + } catch (Exception e) { | ||
| 90 | + log.error("[handle][初始化连接处理器失败]", e); | ||
| 91 | + client.shutdown(); | ||
| 92 | + } | ||
| 93 | + } | ||
| 94 | + | ||
| 95 | + /** | ||
| 96 | + * 处理数据包 | ||
| 97 | + * | ||
| 98 | + * @param client 设备客户端 | ||
| 99 | + * @param buffer 数据缓冲区 | ||
| 100 | + */ | ||
| 101 | + private void handleDataPackage(TcpDeviceClient client, io.vertx.core.buffer.Buffer buffer) { | ||
| 102 | + try { | ||
| 103 | + // 解码数据包 | ||
| 104 | + TcpDataPackage dataPackage = TcpDataDecoder.decode(buffer); | ||
| 105 | + | ||
| 106 | + log.info("[handleDataPackage][接收数据包] 设备地址: {}, 功能码: {}, 消息序号: {}", | ||
| 107 | + dataPackage.getAddr(), dataPackage.getCodeDescription(), dataPackage.getMid()); | ||
| 108 | + | ||
| 109 | + // 根据功能码处理不同类型的消息 | ||
| 110 | + switch (dataPackage.getCode()) { | ||
| 111 | + case TcpDataPackage.CODE_REGISTER: | ||
| 112 | + handleDeviceRegister(client, dataPackage); | ||
| 113 | + break; | ||
| 114 | + case TcpDataPackage.CODE_HEARTBEAT: | ||
| 115 | + handleHeartbeat(client, dataPackage); | ||
| 116 | + break; | ||
| 117 | + case TcpDataPackage.CODE_DATA_UP: | ||
| 118 | + handleDataUp(client, dataPackage); | ||
| 119 | + break; | ||
| 120 | + case TcpDataPackage.CODE_EVENT_UP: | ||
| 121 | + handleEventUp(client, dataPackage); | ||
| 122 | + break; | ||
| 123 | + default: | ||
| 124 | + log.warn("[handleDataPackage][未知功能码: {}]", dataPackage.getCode()); | ||
| 125 | + break; | ||
| 126 | + } | ||
| 127 | + | ||
| 128 | + } catch (Exception e) { | ||
| 129 | + log.error("[handleDataPackage][处理数据包失败]", e); | ||
| 130 | + } | ||
| 131 | + } | ||
| 132 | + | ||
| 133 | + /** | ||
| 134 | + * 处理设备注册 | ||
| 135 | + * | ||
| 136 | + * @param client 设备客户端 | ||
| 137 | + * @param dataPackage 数据包 | ||
| 138 | + */ | ||
| 139 | + private void handleDeviceRegister(TcpDeviceClient client, TcpDataPackage dataPackage) { | ||
| 140 | + try { | ||
| 141 | + String deviceAddr = dataPackage.getAddr(); | ||
| 142 | + String productKey = dataPackage.getPayload(); | ||
| 143 | + | ||
| 144 | + log.info("[handleDeviceRegister][设备注册] 设备地址: {}, 产品密钥: {}", deviceAddr, productKey); | ||
| 145 | + | ||
| 146 | + // 获取设备信息 | ||
| 147 | + IotDeviceRespDTO device = deviceService.getDeviceFromCache(productKey, deviceAddr); | ||
| 148 | + if (device == null) { | ||
| 149 | + log.error("[handleDeviceRegister][设备不存在: {} - {}]", productKey, deviceAddr); | ||
| 150 | + sendRegisterReply(client, dataPackage, false); | ||
| 151 | + return; | ||
| 152 | + } | ||
| 153 | + | ||
| 154 | + // 更新客户端信息 | ||
| 155 | + client.setProductKey(productKey); | ||
| 156 | + client.setDeviceName(deviceAddr); | ||
| 157 | + client.setDeviceId(device.getId()); | ||
| 158 | + client.setAuthenticated(true); | ||
| 159 | + | ||
| 160 | + // 添加到连接管理器 | ||
| 161 | + connectionManager.addClient(deviceAddr, client); | ||
| 162 | + connectionManager.setDeviceIdMapping(deviceAddr, device.getId()); | ||
| 163 | + | ||
| 164 | + // 发送设备上线消息 | ||
| 165 | + IotDeviceMessage onlineMessage = IotDeviceMessage.buildStateUpdateOnline(); | ||
| 166 | + messageService.sendDeviceMessage(onlineMessage, productKey, deviceAddr, serverId); | ||
| 167 | + | ||
| 168 | + // 发送注册成功回复 | ||
| 169 | + sendRegisterReply(client, dataPackage, true); | ||
| 170 | + | ||
| 171 | + log.info("[handleDeviceRegister][设备注册成功] 设备地址: {}, 设备ID: {}", deviceAddr, device.getId()); | ||
| 172 | + | ||
| 173 | + } catch (Exception e) { | ||
| 174 | + log.error("[handleDeviceRegister][设备注册失败]", e); | ||
| 175 | + sendRegisterReply(client, dataPackage, false); | ||
| 176 | + } | ||
| 177 | + } | ||
| 178 | + | ||
| 179 | + /** | ||
| 180 | + * 处理心跳 | ||
| 181 | + * | ||
| 182 | + * @param client 设备客户端 | ||
| 183 | + * @param dataPackage 数据包 | ||
| 184 | + */ | ||
| 185 | + private void handleHeartbeat(TcpDeviceClient client, TcpDataPackage dataPackage) { | ||
| 186 | + try { | ||
| 187 | + String deviceAddr = dataPackage.getAddr(); | ||
| 188 | + | ||
| 189 | + log.debug("[handleHeartbeat][收到心跳] 设备地址: {}", deviceAddr); | ||
| 190 | + | ||
| 191 | + // 更新心跳时间 | ||
| 192 | + client.keepAlive(); | ||
| 193 | + | ||
| 194 | + // 发送心跳回复(可选) | ||
| 195 | + // sendHeartbeatReply(client, dataPackage); | ||
| 196 | + | ||
| 197 | + } catch (Exception e) { | ||
| 198 | + log.error("[handleHeartbeat][处理心跳失败]", e); | ||
| 199 | + } | ||
| 200 | + } | ||
| 201 | + | ||
| 202 | + /** | ||
| 203 | + * 处理数据上报 | ||
| 204 | + * | ||
| 205 | + * @param client 设备客户端 | ||
| 206 | + * @param dataPackage 数据包 | ||
| 207 | + */ | ||
| 208 | + private void handleDataUp(TcpDeviceClient client, TcpDataPackage dataPackage) { | ||
| 209 | + try { | ||
| 210 | + String deviceAddr = dataPackage.getAddr(); | ||
| 211 | + String payload = dataPackage.getPayload(); | ||
| 212 | + | ||
| 213 | + log.info("[handleDataUp][数据上报] 设备地址: {}, 数据: {}", deviceAddr, payload); | ||
| 214 | + | ||
| 215 | + // 检查设备是否已认证 | ||
| 216 | + if (!client.isAuthenticated()) { | ||
| 217 | + log.warn("[handleDataUp][设备未认证,忽略数据上报: {}]", deviceAddr); | ||
| 218 | + return; | ||
| 219 | + } | ||
| 220 | + | ||
| 221 | + // 使用 IotDeviceMessageService 解码消息 | ||
| 222 | + try { | ||
| 223 | + // 1. 将 TCP 数据包重新编码为字节数组 | ||
| 224 | + Buffer buffer = TcpDataEncoder.encode(dataPackage); | ||
| 225 | + byte[] messageBytes = buffer.getBytes(); | ||
| 226 | + | ||
| 227 | + // 2. 使用 messageService 解码消息 | ||
| 228 | + IotDeviceMessage message = messageService.decodeDeviceMessage( | ||
| 229 | + messageBytes, client.getProductKey(), client.getDeviceName()); | ||
| 230 | + | ||
| 231 | + // 3. 发送解码后的消息 | ||
| 232 | + messageService.sendDeviceMessage(message, client.getProductKey(), client.getDeviceName(), serverId); | ||
| 233 | + | ||
| 234 | + } catch (Exception e) { | ||
| 235 | + log.warn("[handleDataUp][使用编解码器解码失败,降级使用原始解析] 错误: {}", e.getMessage()); | ||
| 236 | + | ||
| 237 | + // 降级处理:使用原始方式解析数据 | ||
| 238 | + JSONObject dataJson = JSONUtil.parseObj(payload); | ||
| 239 | + IotDeviceMessage message = IotDeviceMessage.requestOf("thing.property.post", dataJson); | ||
| 240 | + messageService.sendDeviceMessage(message, client.getProductKey(), client.getDeviceName(), serverId); | ||
| 241 | + } | ||
| 242 | + | ||
| 243 | + // 发送数据上报回复 | ||
| 244 | + sendDataUpReply(client, dataPackage); | ||
| 245 | + | ||
| 246 | + } catch (Exception e) { | ||
| 247 | + log.error("[handleDataUp][处理数据上报失败]", e); | ||
| 248 | + } | ||
| 249 | + } | ||
| 250 | + | ||
| 251 | + /** | ||
| 252 | + * 处理事件上报 | ||
| 253 | + * | ||
| 254 | + * @param client 设备客户端 | ||
| 255 | + * @param dataPackage 数据包 | ||
| 256 | + */ | ||
| 257 | + private void handleEventUp(TcpDeviceClient client, TcpDataPackage dataPackage) { | ||
| 258 | + try { | ||
| 259 | + String deviceAddr = dataPackage.getAddr(); | ||
| 260 | + String payload = dataPackage.getPayload(); | ||
| 261 | + | ||
| 262 | + log.info("[handleEventUp][事件上报] 设备地址: {}, 数据: {}", deviceAddr, payload); | ||
| 263 | + | ||
| 264 | + // 检查设备是否已认证 | ||
| 265 | + if (!client.isAuthenticated()) { | ||
| 266 | + log.warn("[handleEventUp][设备未认证,忽略事件上报: {}]", deviceAddr); | ||
| 267 | + return; | ||
| 268 | + } | ||
| 269 | + | ||
| 270 | + // 使用 IotDeviceMessageService 解码消息 | ||
| 271 | + try { | ||
| 272 | + // 1. 将 TCP 数据包重新编码为字节数组 | ||
| 273 | + Buffer buffer = TcpDataEncoder.encode(dataPackage); | ||
| 274 | + byte[] messageBytes = buffer.getBytes(); | ||
| 275 | + | ||
| 276 | + // 2. 使用 messageService 解码消息 | ||
| 277 | + IotDeviceMessage message = messageService.decodeDeviceMessage( | ||
| 278 | + messageBytes, client.getProductKey(), client.getDeviceName()); | ||
| 279 | + | ||
| 280 | + // 3. 发送解码后的消息 | ||
| 281 | + messageService.sendDeviceMessage(message, client.getProductKey(), client.getDeviceName(), serverId); | ||
| 282 | + | ||
| 283 | + } catch (Exception e) { | ||
| 284 | + log.warn("[handleEventUp][使用编解码器解码失败,降级使用原始解析] 错误: {}", e.getMessage()); | ||
| 285 | + | ||
| 286 | + // 降级处理:使用原始方式解析数据 | ||
| 287 | + JSONObject eventJson = JSONUtil.parseObj(payload); | ||
| 288 | + IotDeviceMessage message = IotDeviceMessage.requestOf("thing.event.post", eventJson); | ||
| 289 | + messageService.sendDeviceMessage(message, client.getProductKey(), client.getDeviceName(), serverId); | ||
| 290 | + } | ||
| 291 | + | ||
| 292 | + // 发送事件上报回复 | ||
| 293 | + sendEventUpReply(client, dataPackage); | ||
| 294 | + | ||
| 295 | + } catch (Exception e) { | ||
| 296 | + log.error("[handleEventUp][处理事件上报失败]", e); | ||
| 297 | + } | ||
| 298 | + } | ||
| 299 | + | ||
| 300 | + /** | ||
| 301 | + * 发送注册回复 | ||
| 302 | + * | ||
| 303 | + * @param client 设备客户端 | ||
| 304 | + * @param dataPackage 原始数据包 | ||
| 305 | + * @param success 是否成功 | ||
| 306 | + */ | ||
| 307 | + private void sendRegisterReply(TcpDeviceClient client, TcpDataPackage dataPackage, boolean success) { | ||
| 308 | + try { | ||
| 309 | + io.vertx.core.buffer.Buffer replyBuffer = TcpDataEncoder.createRegisterReply( | ||
| 310 | + dataPackage.getAddr(), dataPackage.getMid(), success); | ||
| 311 | + client.sendMessage(replyBuffer); | ||
| 312 | + | ||
| 313 | + log.debug("[sendRegisterReply][发送注册回复] 设备地址: {}, 结果: {}", | ||
| 314 | + dataPackage.getAddr(), success ? "成功" : "失败"); | ||
| 315 | + } catch (Exception e) { | ||
| 316 | + log.error("[sendRegisterReply][发送注册回复失败]", e); | ||
| 317 | + } | ||
| 318 | + } | ||
| 319 | + | ||
| 320 | + /** | ||
| 321 | + * 发送数据上报回复 | ||
| 322 | + * | ||
| 323 | + * @param client 设备客户端 | ||
| 324 | + * @param dataPackage 原始数据包 | ||
| 325 | + */ | ||
| 326 | + private void sendDataUpReply(TcpDeviceClient client, TcpDataPackage dataPackage) { | ||
| 327 | + try { | ||
| 328 | + TcpDataPackage replyPackage = TcpDataPackage.builder() | ||
| 329 | + .addr(dataPackage.getAddr()) | ||
| 330 | + .code(TcpDataPackage.CODE_DATA_UP) | ||
| 331 | + .mid(dataPackage.getMid()) | ||
| 332 | + .payload("0") // 0表示成功 | ||
| 333 | + .build(); | ||
| 334 | + | ||
| 335 | + io.vertx.core.buffer.Buffer replyBuffer = TcpDataEncoder.encode(replyPackage); | ||
| 336 | + client.sendMessage(replyBuffer); | ||
| 337 | + | ||
| 338 | + } catch (Exception e) { | ||
| 339 | + log.error("[sendDataUpReply][发送数据上报回复失败]", e); | ||
| 340 | + } | ||
| 341 | + } | ||
| 342 | + | ||
| 343 | + /** | ||
| 344 | + * 发送事件上报回复 | ||
| 345 | + * | ||
| 346 | + * @param client 设备客户端 | ||
| 347 | + * @param dataPackage 原始数据包 | ||
| 348 | + */ | ||
| 349 | + private void sendEventUpReply(TcpDeviceClient client, TcpDataPackage dataPackage) { | ||
| 350 | + try { | ||
| 351 | + TcpDataPackage replyPackage = TcpDataPackage.builder() | ||
| 352 | + .addr(dataPackage.getAddr()) | ||
| 353 | + .code(TcpDataPackage.CODE_EVENT_UP) | ||
| 354 | + .mid(dataPackage.getMid()) | ||
| 355 | + .payload("0") // 0表示成功 | ||
| 356 | + .build(); | ||
| 357 | + | ||
| 358 | + io.vertx.core.buffer.Buffer replyBuffer = TcpDataEncoder.encode(replyPackage); | ||
| 359 | + client.sendMessage(replyBuffer); | ||
| 360 | + | ||
| 361 | + } catch (Exception e) { | ||
| 362 | + log.error("[sendEventUpReply][发送事件上报回复失败]", e); | ||
| 363 | + } | ||
| 364 | + } | ||
| 365 | + | ||
| 366 | + /** | ||
| 367 | + * 处理连接关闭 | ||
| 368 | + * | ||
| 369 | + * @param client 设备客户端 | ||
| 370 | + */ | ||
| 371 | + private void handleConnectionClose(TcpDeviceClient client) { | ||
| 372 | + try { | ||
| 373 | + String deviceAddr = client.getDeviceAddr(); | ||
| 374 | + | ||
| 375 | + // 发送设备离线消息 | ||
| 376 | + if (client.isAuthenticated()) { | ||
| 377 | + IotDeviceMessage offlineMessage = IotDeviceMessage.buildStateOffline(); | ||
| 378 | + messageService.sendDeviceMessage(offlineMessage, | ||
| 379 | + client.getProductKey(), client.getDeviceName(), serverId); | ||
| 380 | + } | ||
| 381 | + | ||
| 382 | + // 从连接管理器移除 | ||
| 383 | + if (deviceAddr != null) { | ||
| 384 | + connectionManager.removeClient(deviceAddr); | ||
| 385 | + } | ||
| 386 | + | ||
| 387 | + log.info("[handleConnectionClose][处理连接关闭完成] 设备地址: {}", deviceAddr); | ||
| 388 | + | ||
| 389 | + } catch (Exception e) { | ||
| 390 | + log.error("[handleConnectionClose][处理连接关闭失败]", e); | ||
| 391 | + } | ||
| 392 | + } | ||
| 393 | +} | ||
| 0 | \ No newline at end of file | 394 | \ No newline at end of file |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/service/device/message/IotDeviceMessageServiceImpl.java
| @@ -6,7 +6,7 @@ import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO; | @@ -6,7 +6,7 @@ import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO; | ||
| 6 | import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; | 6 | import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; |
| 7 | import cn.iocoder.yudao.module.iot.core.mq.producer.IotDeviceMessageProducer; | 7 | import cn.iocoder.yudao.module.iot.core.mq.producer.IotDeviceMessageProducer; |
| 8 | import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; | 8 | import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; |
| 9 | -import cn.iocoder.yudao.module.iot.gateway.codec.alink.IotAlinkDeviceMessageCodec; | 9 | +import cn.iocoder.yudao.module.iot.gateway.codec.IotDeviceMessageCodec; |
| 10 | import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService; | 10 | import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService; |
| 11 | import jakarta.annotation.Resource; | 11 | import jakarta.annotation.Resource; |
| 12 | import lombok.extern.slf4j.Slf4j; | 12 | import lombok.extern.slf4j.Slf4j; |
| @@ -31,7 +31,7 @@ public class IotDeviceMessageServiceImpl implements IotDeviceMessageService { | @@ -31,7 +31,7 @@ public class IotDeviceMessageServiceImpl implements IotDeviceMessageService { | ||
| 31 | /** | 31 | /** |
| 32 | * 编解码器 | 32 | * 编解码器 |
| 33 | */ | 33 | */ |
| 34 | - private final Map<String, IotAlinkDeviceMessageCodec> codes; | 34 | + private final Map<String, IotDeviceMessageCodec> codes; |
| 35 | 35 | ||
| 36 | @Resource | 36 | @Resource |
| 37 | private IotDeviceService deviceService; | 37 | private IotDeviceService deviceService; |
| @@ -39,8 +39,8 @@ public class IotDeviceMessageServiceImpl implements IotDeviceMessageService { | @@ -39,8 +39,8 @@ public class IotDeviceMessageServiceImpl implements IotDeviceMessageService { | ||
| 39 | @Resource | 39 | @Resource |
| 40 | private IotDeviceMessageProducer deviceMessageProducer; | 40 | private IotDeviceMessageProducer deviceMessageProducer; |
| 41 | 41 | ||
| 42 | - public IotDeviceMessageServiceImpl(List<IotAlinkDeviceMessageCodec> codes) { | ||
| 43 | - this.codes = CollectionUtils.convertMap(codes, IotAlinkDeviceMessageCodec::type); | 42 | + public IotDeviceMessageServiceImpl(List<IotDeviceMessageCodec> codes) { |
| 43 | + this.codes = CollectionUtils.convertMap(codes, IotDeviceMessageCodec::type); | ||
| 44 | } | 44 | } |
| 45 | 45 | ||
| 46 | @Override | 46 | @Override |
| @@ -52,7 +52,7 @@ public class IotDeviceMessageServiceImpl implements IotDeviceMessageService { | @@ -52,7 +52,7 @@ public class IotDeviceMessageServiceImpl implements IotDeviceMessageService { | ||
| 52 | throw exception(DEVICE_NOT_EXISTS, productKey, deviceName); | 52 | throw exception(DEVICE_NOT_EXISTS, productKey, deviceName); |
| 53 | } | 53 | } |
| 54 | // 1.2 获取编解码器 | 54 | // 1.2 获取编解码器 |
| 55 | - IotAlinkDeviceMessageCodec codec = codes.get(device.getCodecType()); | 55 | + IotDeviceMessageCodec codec = codes.get(device.getCodecType()); |
| 56 | if (codec == null) { | 56 | if (codec == null) { |
| 57 | throw new IllegalArgumentException(StrUtil.format("编解码器({}) 不存在", device.getCodecType())); | 57 | throw new IllegalArgumentException(StrUtil.format("编解码器({}) 不存在", device.getCodecType())); |
| 58 | } | 58 | } |
| @@ -70,7 +70,7 @@ public class IotDeviceMessageServiceImpl implements IotDeviceMessageService { | @@ -70,7 +70,7 @@ public class IotDeviceMessageServiceImpl implements IotDeviceMessageService { | ||
| 70 | throw exception(DEVICE_NOT_EXISTS, productKey, deviceName); | 70 | throw exception(DEVICE_NOT_EXISTS, productKey, deviceName); |
| 71 | } | 71 | } |
| 72 | // 1.2 获取编解码器 | 72 | // 1.2 获取编解码器 |
| 73 | - IotAlinkDeviceMessageCodec codec = codes.get(device.getCodecType()); | 73 | + IotDeviceMessageCodec codec = codes.get(device.getCodecType()); |
| 74 | if (codec == null) { | 74 | if (codec == null) { |
| 75 | throw new IllegalArgumentException(StrUtil.format("编解码器({}) 不存在", device.getCodecType())); | 75 | throw new IllegalArgumentException(StrUtil.format("编解码器({}) 不存在", device.getCodecType())); |
| 76 | } | 76 | } |
yudao-module-iot/yudao-module-iot-gateway/src/main/resources/application.yaml
| @@ -79,8 +79,13 @@ yudao: | @@ -79,8 +79,13 @@ yudao: | ||
| 79 | # 针对引入的 TCP 组件的配置 | 79 | # 针对引入的 TCP 组件的配置 |
| 80 | # ==================================== | 80 | # ==================================== |
| 81 | tcp: | 81 | tcp: |
| 82 | - enabled: true | ||
| 83 | - server-port: 8093 | 82 | + enabled: false |
| 83 | + port: 8091 | ||
| 84 | + keep-alive-timeout-ms: 30000 | ||
| 85 | + max-connections: 1000 | ||
| 86 | + ssl-enabled: false | ||
| 87 | + ssl-cert-path: "classpath:certs/client.jks" | ||
| 88 | + ssl-key-path: "classpath:certs/client.jks" | ||
| 84 | 89 | ||
| 85 | --- #################### 日志相关配置 #################### | 90 | --- #################### 日志相关配置 #################### |
| 86 | 91 |