Commit bd8052f56b5254fb073d4b396fcb8326897c79c0
1 parent
6a117c9d
feat:【IoT 物联网】新增 TCP 二进制和 JSON 编解码器,重构 TCP 协议处理逻辑
Showing
19 changed files
with
1864 additions
and
2571 deletions
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/IotTcpBinaryDeviceMessageCodec.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.iot.gateway.codec.tcp; | |
| 2 | + | |
| 3 | +import cn.hutool.core.util.StrUtil; | |
| 4 | +import cn.hutool.json.JSONObject; | |
| 5 | +import cn.hutool.json.JSONUtil; | |
| 6 | +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; | |
| 7 | +import cn.iocoder.yudao.module.iot.gateway.codec.IotDeviceMessageCodec; | |
| 8 | +import io.vertx.core.buffer.Buffer; | |
| 9 | +import lombok.Data; | |
| 10 | +import lombok.extern.slf4j.Slf4j; | |
| 11 | +import org.springframework.stereotype.Component; | |
| 12 | + | |
| 13 | +/** | |
| 14 | + * TCP 二进制格式 {@link IotDeviceMessage} 编解码器 | |
| 15 | + * | |
| 16 | + * 使用自定义二进制协议格式: | |
| 17 | + * 包头(4字节) | 地址长度(2字节) | 设备地址(变长) | 功能码(2字节) | 消息序号(2字节) | 包体数据(变长) | |
| 18 | + * | |
| 19 | + * @author 芋道源码 | |
| 20 | + */ | |
| 21 | +@Component | |
| 22 | +@Slf4j | |
| 23 | +public class IotTcpBinaryDeviceMessageCodec implements IotDeviceMessageCodec { | |
| 24 | + | |
| 25 | + /** | |
| 26 | + * 编解码器类型 | |
| 27 | + */ | |
| 28 | + public static final String TYPE = "TCP_BINARY"; | |
| 29 | + | |
| 30 | + // ==================== 常量定义 ==================== | |
| 31 | + | |
| 32 | + @Override | |
| 33 | + public byte[] encode(IotDeviceMessage message) { | |
| 34 | + if (message == null || StrUtil.isEmpty(message.getMethod())) { | |
| 35 | + throw new IllegalArgumentException("消息或方法不能为空"); | |
| 36 | + } | |
| 37 | + | |
| 38 | + try { | |
| 39 | + // 1. 确定功能码(只支持数据上报和心跳) | |
| 40 | + short code = MessageMethod.STATE_ONLINE.equals(message.getMethod()) ? | |
| 41 | + TcpDataPackage.CODE_HEARTBEAT : TcpDataPackage.CODE_MESSAGE_UP; | |
| 42 | + | |
| 43 | + // 2. 构建简化负载 | |
| 44 | + String payload = buildSimplePayload(message); | |
| 45 | + | |
| 46 | + // 3. 构建 TCP 数据包 | |
| 47 | + String deviceAddr = message.getDeviceId() != null ? String.valueOf(message.getDeviceId()) : "default"; | |
| 48 | + short mid = (short) (System.currentTimeMillis() % Short.MAX_VALUE); | |
| 49 | + TcpDataPackage dataPackage = new TcpDataPackage(deviceAddr, code, mid, payload); | |
| 50 | + | |
| 51 | + // 4. 编码为字节流 | |
| 52 | + return encodeTcpDataPackage(dataPackage).getBytes(); | |
| 53 | + } catch (Exception e) { | |
| 54 | + log.error("[encode][编码失败] 方法: {}", message.getMethod(), e); | |
| 55 | + throw new TcpCodecException("TCP 消息编码失败", e); | |
| 56 | + } | |
| 57 | + } | |
| 58 | + | |
| 59 | + @Override | |
| 60 | + public IotDeviceMessage decode(byte[] bytes) { | |
| 61 | + if (bytes == null || bytes.length == 0) { | |
| 62 | + throw new IllegalArgumentException("待解码数据不能为空"); | |
| 63 | + } | |
| 64 | + | |
| 65 | + try { | |
| 66 | + // 1. 解码 TCP 数据包 | |
| 67 | + TcpDataPackage dataPackage = decodeTcpDataPackage(Buffer.buffer(bytes)); | |
| 68 | + | |
| 69 | + // 2. 根据功能码确定方法 | |
| 70 | + String method = (dataPackage.getCode() == TcpDataPackage.CODE_HEARTBEAT) ? | |
| 71 | + MessageMethod.STATE_ONLINE : MessageMethod.PROPERTY_POST; | |
| 72 | + | |
| 73 | + // 3. 解析负载数据和请求ID | |
| 74 | + PayloadInfo payloadInfo = parsePayloadInfo(dataPackage.getPayload()); | |
| 75 | + | |
| 76 | + // 4. 构建 IoT 设备消息(设置完整的必要参数) | |
| 77 | + IotDeviceMessage message = IotDeviceMessage.requestOf( | |
| 78 | + payloadInfo.getRequestId(), method, payloadInfo.getParams()); | |
| 79 | + | |
| 80 | + // 5. 设置设备相关信息 | |
| 81 | + Long deviceId = parseDeviceId(dataPackage.getAddr()); | |
| 82 | + message.setDeviceId(deviceId); | |
| 83 | + | |
| 84 | + // 6. 设置TCP协议相关信息 | |
| 85 | + message.setServerId(generateServerId(dataPackage)); | |
| 86 | + | |
| 87 | + // 7. 设置租户ID(TODO: 后续可以从设备信息中获取) | |
| 88 | + // message.setTenantId(getTenantIdByDeviceId(deviceId)); | |
| 89 | + | |
| 90 | + if (log.isDebugEnabled()) { | |
| 91 | + log.debug("[decode][解码成功] 设备ID: {}, 方法: {}, 请求ID: {}, 消息ID: {}", | |
| 92 | + deviceId, method, message.getRequestId(), message.getId()); | |
| 93 | + } | |
| 94 | + | |
| 95 | + return message; | |
| 96 | + } catch (Exception e) { | |
| 97 | + log.error("[decode][解码失败] 数据长度: {}", bytes.length, e); | |
| 98 | + throw new TcpCodecException("TCP 消息解码失败", e); | |
| 99 | + } | |
| 100 | + } | |
| 101 | + | |
| 102 | + @Override | |
| 103 | + public String type() { | |
| 104 | + return TYPE; | |
| 105 | + } | |
| 106 | + | |
| 107 | + /** | |
| 108 | + * 构建完整负载 | |
| 109 | + */ | |
| 110 | + private String buildSimplePayload(IotDeviceMessage message) { | |
| 111 | + JSONObject payload = new JSONObject(); | |
| 112 | + | |
| 113 | + // 核心字段 | |
| 114 | + payload.set(PayloadField.METHOD, message.getMethod()); | |
| 115 | + if (message.getParams() != null) { | |
| 116 | + payload.set(PayloadField.PARAMS, message.getParams()); | |
| 117 | + } | |
| 118 | + | |
| 119 | + // 标识字段 | |
| 120 | + if (StrUtil.isNotEmpty(message.getRequestId())) { | |
| 121 | + payload.set(PayloadField.REQUEST_ID, message.getRequestId()); | |
| 122 | + } | |
| 123 | + if (StrUtil.isNotEmpty(message.getId())) { | |
| 124 | + payload.set(PayloadField.MESSAGE_ID, message.getId()); | |
| 125 | + } | |
| 126 | + | |
| 127 | + // 时间戳 | |
| 128 | + payload.set(PayloadField.TIMESTAMP, System.currentTimeMillis()); | |
| 129 | + | |
| 130 | + return payload.toString(); | |
| 131 | + } | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + // ==================== 编解码方法 ==================== | |
| 136 | + | |
| 137 | + /** | |
| 138 | + * 解析负载信息(包含requestId和params) | |
| 139 | + */ | |
| 140 | + private PayloadInfo parsePayloadInfo(String payload) { | |
| 141 | + if (StrUtil.isEmpty(payload)) { | |
| 142 | + return new PayloadInfo(null, null); | |
| 143 | + } | |
| 144 | + | |
| 145 | + try { | |
| 146 | + JSONObject jsonObject = JSONUtil.parseObj(payload); | |
| 147 | + String requestId = jsonObject.getStr(PayloadField.REQUEST_ID); | |
| 148 | + if (StrUtil.isEmpty(requestId)) { | |
| 149 | + requestId = jsonObject.getStr(PayloadField.MESSAGE_ID); | |
| 150 | + } | |
| 151 | + Object params = jsonObject.get(PayloadField.PARAMS); | |
| 152 | + return new PayloadInfo(requestId, params); | |
| 153 | + } catch (Exception e) { | |
| 154 | + log.warn("[parsePayloadInfo][解析失败,返回原始字符串] 负载: {}", payload); | |
| 155 | + return new PayloadInfo(null, payload); | |
| 156 | + } | |
| 157 | + } | |
| 158 | + | |
| 159 | + /** | |
| 160 | + * 从设备地址解析设备ID | |
| 161 | + * | |
| 162 | + * @param deviceAddr 设备地址字符串 | |
| 163 | + * @return 设备ID | |
| 164 | + */ | |
| 165 | + private Long parseDeviceId(String deviceAddr) { | |
| 166 | + if (StrUtil.isEmpty(deviceAddr)) { | |
| 167 | + log.warn("[parseDeviceId][设备地址为空,返回默认ID]"); | |
| 168 | + return 0L; | |
| 169 | + } | |
| 170 | + | |
| 171 | + try { | |
| 172 | + // 尝试直接解析为Long | |
| 173 | + return Long.parseLong(deviceAddr); | |
| 174 | + } catch (NumberFormatException e) { | |
| 175 | + // 如果不是纯数字,可以使用哈希值或其他策略 | |
| 176 | + log.warn("[parseDeviceId][设备地址不是数字格式: {},使用哈希值]", deviceAddr); | |
| 177 | + return (long) deviceAddr.hashCode(); | |
| 178 | + } | |
| 179 | + } | |
| 180 | + | |
| 181 | + /** | |
| 182 | + * 生成服务ID | |
| 183 | + * | |
| 184 | + * @param dataPackage TCP数据包 | |
| 185 | + * @return 服务ID | |
| 186 | + */ | |
| 187 | + private String generateServerId(TcpDataPackage dataPackage) { | |
| 188 | + // 使用协议类型 + 设备地址 + 消息序号生成唯一的服务ID | |
| 189 | + return String.format("tcp_%s_%d", dataPackage.getAddr(), dataPackage.getMid()); | |
| 190 | + } | |
| 191 | + | |
| 192 | + // ==================== 内部辅助方法 ==================== | |
| 193 | + | |
| 194 | + /** | |
| 195 | + * 编码 TCP 数据包 | |
| 196 | + * | |
| 197 | + * @param dataPackage 数据包对象 | |
| 198 | + * @return 编码后的字节流 | |
| 199 | + * @throws IllegalArgumentException 如果数据包对象不正确 | |
| 200 | + */ | |
| 201 | + private Buffer encodeTcpDataPackage(TcpDataPackage dataPackage) { | |
| 202 | + if (dataPackage == null) { | |
| 203 | + throw new IllegalArgumentException("数据包对象不能为空"); | |
| 204 | + } | |
| 205 | + | |
| 206 | + // 验证数据包 | |
| 207 | + if (dataPackage.getAddr() == null || dataPackage.getAddr().isEmpty()) { | |
| 208 | + throw new IllegalArgumentException("设备地址不能为空"); | |
| 209 | + } | |
| 210 | + if (dataPackage.getPayload() == null) { | |
| 211 | + throw new IllegalArgumentException("负载不能为空"); | |
| 212 | + } | |
| 213 | + | |
| 214 | + try { | |
| 215 | + Buffer buffer = Buffer.buffer(); | |
| 216 | + | |
| 217 | + // 1. 计算包体长度(除了包头 4 字节) | |
| 218 | + int payloadLength = dataPackage.getPayload().getBytes().length; | |
| 219 | + int totalLength = 2 + dataPackage.getAddr().length() + 2 + 2 + payloadLength; | |
| 220 | + | |
| 221 | + // 2.1 写入包头:总长度(4 字节) | |
| 222 | + buffer.appendInt(totalLength); | |
| 223 | + // 2.2 写入设备地址长度(2 字节) | |
| 224 | + buffer.appendShort((short) dataPackage.getAddr().length()); | |
| 225 | + // 2.3 写入设备地址(不定长) | |
| 226 | + buffer.appendBytes(dataPackage.getAddr().getBytes()); | |
| 227 | + // 2.4 写入功能码(2 字节) | |
| 228 | + buffer.appendShort(dataPackage.getCode()); | |
| 229 | + // 2.5 写入消息序号(2 字节) | |
| 230 | + buffer.appendShort(dataPackage.getMid()); | |
| 231 | + // 2.6 写入包体数据(不定长) | |
| 232 | + buffer.appendBytes(dataPackage.getPayload().getBytes()); | |
| 233 | + | |
| 234 | + if (log.isDebugEnabled()) { | |
| 235 | + log.debug("[encodeTcpDataPackage][编码成功] 设备地址: {}, 功能码: {}, 消息序号: {}, 总长度: {}", | |
| 236 | + dataPackage.getAddr(), dataPackage.getCode(), dataPackage.getMid(), buffer.length()); | |
| 237 | + } | |
| 238 | + return buffer; | |
| 239 | + } catch (Exception e) { | |
| 240 | + log.error("[encodeTcpDataPackage][编码失败] 数据包: {}", dataPackage, e); | |
| 241 | + throw new IllegalArgumentException("数据包编码失败: " + e.getMessage(), e); | |
| 242 | + } | |
| 243 | + } | |
| 244 | + | |
| 245 | + /** | |
| 246 | + * 解码 TCP 数据包 | |
| 247 | + * | |
| 248 | + * @param buffer 数据缓冲区 | |
| 249 | + * @return 解码后的数据包 | |
| 250 | + * @throws IllegalArgumentException 如果数据包格式不正确 | |
| 251 | + */ | |
| 252 | + private TcpDataPackage decodeTcpDataPackage(Buffer buffer) { | |
| 253 | + if (buffer == null || buffer.length() < 8) { | |
| 254 | + throw new IllegalArgumentException("数据包长度不足"); | |
| 255 | + } | |
| 256 | + | |
| 257 | + try { | |
| 258 | + int index = 0; | |
| 259 | + | |
| 260 | + // 1.1 跳过包头(4字节) | |
| 261 | + index += 4; | |
| 262 | + | |
| 263 | + // 1.2 获取设备地址长度(2字节) | |
| 264 | + short addrLength = buffer.getShort(index); | |
| 265 | + index += 2; | |
| 266 | + | |
| 267 | + // 1.3 获取设备地址 | |
| 268 | + String addr = buffer.getBuffer(index, index + addrLength).toString(); | |
| 269 | + index += addrLength; | |
| 270 | + | |
| 271 | + // 1.4 获取功能码(2字节) | |
| 272 | + short code = buffer.getShort(index); | |
| 273 | + index += 2; | |
| 274 | + | |
| 275 | + // 1.5 获取消息序号(2字节) | |
| 276 | + short mid = buffer.getShort(index); | |
| 277 | + index += 2; | |
| 278 | + | |
| 279 | + // 1.6 获取包体数据 | |
| 280 | + String payload = ""; | |
| 281 | + if (index < buffer.length()) { | |
| 282 | + payload = buffer.getString(index, buffer.length()); | |
| 283 | + } | |
| 284 | + | |
| 285 | + // 2. 构建数据包对象 | |
| 286 | + TcpDataPackage dataPackage = new TcpDataPackage(addr, code, mid, payload); | |
| 287 | + | |
| 288 | + if (log.isDebugEnabled()) { | |
| 289 | + log.debug("[decodeTcpDataPackage][解码成功] 设备地址: {}, 功能码: {}, 消息序号: {}, 包体长度: {}", | |
| 290 | + addr, code, mid, payload.length()); | |
| 291 | + } | |
| 292 | + return dataPackage; | |
| 293 | + } catch (Exception e) { | |
| 294 | + log.error("[decodeTcpDataPackage][解码失败] 数据长度: {}", buffer.length(), e); | |
| 295 | + throw new IllegalArgumentException("数据包解码失败: " + e.getMessage(), e); | |
| 296 | + } | |
| 297 | + } | |
| 298 | + | |
| 299 | + /** | |
| 300 | + * 消息方法常量 | |
| 301 | + */ | |
| 302 | + public static class MessageMethod { | |
| 303 | + public static final String PROPERTY_POST = "thing.property.post"; // 数据上报 | |
| 304 | + public static final String STATE_ONLINE = "thing.state.online"; // 心跳 | |
| 305 | + } | |
| 306 | + | |
| 307 | + /** | |
| 308 | + * 负载字段名 | |
| 309 | + */ | |
| 310 | + private static class PayloadField { | |
| 311 | + public static final String METHOD = "method"; | |
| 312 | + public static final String PARAMS = "params"; | |
| 313 | + public static final String TIMESTAMP = "timestamp"; | |
| 314 | + public static final String REQUEST_ID = "requestId"; | |
| 315 | + public static final String MESSAGE_ID = "msgId"; | |
| 316 | + } | |
| 317 | + | |
| 318 | + // ==================== TCP 数据包编解码方法 ==================== | |
| 319 | + | |
| 320 | + /** | |
| 321 | + * 负载信息类 | |
| 322 | + */ | |
| 323 | + private static class PayloadInfo { | |
| 324 | + private String requestId; | |
| 325 | + private Object params; | |
| 326 | + | |
| 327 | + public PayloadInfo(String requestId, Object params) { | |
| 328 | + this.requestId = requestId; | |
| 329 | + this.params = params; | |
| 330 | + } | |
| 331 | + | |
| 332 | + public String getRequestId() { return requestId; } | |
| 333 | + public Object getParams() { return params; } | |
| 334 | + } | |
| 335 | + | |
| 336 | + /** | |
| 337 | + * TCP 数据包内部类 | |
| 338 | + */ | |
| 339 | + @Data | |
| 340 | + private static class TcpDataPackage { | |
| 341 | + // 功能码定义 | |
| 342 | + public static final short CODE_REGISTER = 10; | |
| 343 | + public static final short CODE_REGISTER_REPLY = 11; | |
| 344 | + public static final short CODE_HEARTBEAT = 20; | |
| 345 | + public static final short CODE_HEARTBEAT_REPLY = 21; | |
| 346 | + public static final short CODE_MESSAGE_UP = 30; | |
| 347 | + public static final short CODE_MESSAGE_DOWN = 40; | |
| 348 | + | |
| 349 | + private String addr; | |
| 350 | + private short code; | |
| 351 | + private short mid; | |
| 352 | + private String payload; | |
| 353 | + | |
| 354 | + public TcpDataPackage(String addr, short code, short mid, String payload) { | |
| 355 | + this.addr = addr; | |
| 356 | + this.code = code; | |
| 357 | + this.mid = mid; | |
| 358 | + this.payload = payload; | |
| 359 | + } | |
| 360 | + } | |
| 361 | + | |
| 362 | + // ==================== 自定义异常 ==================== | |
| 363 | + | |
| 364 | + /** | |
| 365 | + * TCP 编解码异常 | |
| 366 | + */ | |
| 367 | + public static class TcpCodecException extends RuntimeException { | |
| 368 | + | |
| 369 | + public TcpCodecException(String message) { | |
| 370 | + super(message); | |
| 371 | + } | |
| 372 | + | |
| 373 | + public TcpCodecException(String message, Throwable cause) { | |
| 374 | + super(message, cause); | |
| 375 | + } | |
| 376 | + | |
| 377 | + } | |
| 378 | +} | |
| 0 | 379 | \ No newline at end of file | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/IotTcpCodecManager.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.iot.gateway.codec.tcp; | |
| 2 | + | |
| 3 | +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; | |
| 4 | +import cn.iocoder.yudao.module.iot.gateway.codec.IotDeviceMessageCodec; | |
| 5 | +import lombok.extern.slf4j.Slf4j; | |
| 6 | +import org.springframework.beans.factory.annotation.Autowired; | |
| 7 | +import org.springframework.stereotype.Component; | |
| 8 | + | |
| 9 | +/** | |
| 10 | + * TCP编解码器管理器(简化版) | |
| 11 | + * | |
| 12 | + * 核心功能: | |
| 13 | + * - 自动协议检测(二进制 vs JSON) | |
| 14 | + * - 统一编解码接口 | |
| 15 | + * - 默认使用JSON协议 | |
| 16 | + * | |
| 17 | + * @author 芋道源码 | |
| 18 | + */ | |
| 19 | +@Slf4j | |
| 20 | +@Component | |
| 21 | +public class IotTcpCodecManager implements IotDeviceMessageCodec { | |
| 22 | + | |
| 23 | + public static final String TYPE = "TCP"; | |
| 24 | + | |
| 25 | + @Autowired | |
| 26 | + private IotTcpBinaryDeviceMessageCodec binaryCodec; | |
| 27 | + | |
| 28 | + @Autowired | |
| 29 | + private IotTcpJsonDeviceMessageCodec jsonCodec; | |
| 30 | + | |
| 31 | + /** | |
| 32 | + * 当前默认协议(JSON) | |
| 33 | + */ | |
| 34 | + private boolean useJsonByDefault = true; | |
| 35 | + | |
| 36 | + @Override | |
| 37 | + public String type() { | |
| 38 | + return TYPE; | |
| 39 | + } | |
| 40 | + | |
| 41 | + @Override | |
| 42 | + public byte[] encode(IotDeviceMessage message) { | |
| 43 | + // 默认使用JSON协议编码 | |
| 44 | + return jsonCodec.encode(message); | |
| 45 | + } | |
| 46 | + | |
| 47 | + @Override | |
| 48 | + public IotDeviceMessage decode(byte[] bytes) { | |
| 49 | + // 自动检测协议类型并解码 | |
| 50 | + if (isJsonFormat(bytes)) { | |
| 51 | + if (log.isDebugEnabled()) { | |
| 52 | + log.debug("[decode][检测到JSON协议] 数据长度: {}字节", bytes.length); | |
| 53 | + } | |
| 54 | + return jsonCodec.decode(bytes); | |
| 55 | + } else { | |
| 56 | + if (log.isDebugEnabled()) { | |
| 57 | + log.debug("[decode][检测到二进制协议] 数据长度: {}字节", bytes.length); | |
| 58 | + } | |
| 59 | + return binaryCodec.decode(bytes); | |
| 60 | + } | |
| 61 | + } | |
| 62 | + | |
| 63 | + // ==================== 便捷方法 ==================== | |
| 64 | + | |
| 65 | + /** | |
| 66 | + * 使用JSON协议编码 | |
| 67 | + */ | |
| 68 | + public byte[] encodeJson(IotDeviceMessage message) { | |
| 69 | + return jsonCodec.encode(message); | |
| 70 | + } | |
| 71 | + | |
| 72 | + /** | |
| 73 | + * 使用二进制协议编码 | |
| 74 | + */ | |
| 75 | + public byte[] encodeBinary(IotDeviceMessage message) { | |
| 76 | + return binaryCodec.encode(message); | |
| 77 | + } | |
| 78 | + | |
| 79 | + /** | |
| 80 | + * 获取当前默认协议 | |
| 81 | + */ | |
| 82 | + public String getDefaultProtocol() { | |
| 83 | + return useJsonByDefault ? "JSON" : "BINARY"; | |
| 84 | + } | |
| 85 | + | |
| 86 | + /** | |
| 87 | + * 设置默认协议 | |
| 88 | + */ | |
| 89 | + public void setDefaultProtocol(boolean useJson) { | |
| 90 | + this.useJsonByDefault = useJson; | |
| 91 | + log.info("[setDefaultProtocol][设置默认协议] 使用JSON: {}", useJson); | |
| 92 | + } | |
| 93 | + | |
| 94 | + // ==================== 内部方法 ==================== | |
| 95 | + | |
| 96 | + /** | |
| 97 | + * 检测是否为JSON格式 | |
| 98 | + * | |
| 99 | + * 检测规则: | |
| 100 | + * 1. 数据以 '{' 开头 | |
| 101 | + * 2. 包含 "method" 或 "id" 字段 | |
| 102 | + */ | |
| 103 | + private boolean isJsonFormat(byte[] bytes) { | |
| 104 | + if (bytes == null || bytes.length == 0) { | |
| 105 | + return useJsonByDefault; | |
| 106 | + } | |
| 107 | + | |
| 108 | + try { | |
| 109 | + // 检测JSON格式:以 '{' 开头 | |
| 110 | + if (bytes[0] == '{') { | |
| 111 | + // 进一步验证是否为有效JSON | |
| 112 | + String jsonStr = new String(bytes, 0, Math.min(bytes.length, 100)); | |
| 113 | + return jsonStr.contains("\"method\"") || jsonStr.contains("\"id\""); | |
| 114 | + } | |
| 115 | + | |
| 116 | + // 检测二进制格式:长度 >= 8 且符合二进制协议结构 | |
| 117 | + if (bytes.length >= 8) { | |
| 118 | + // 读取包头(前4字节表示后续数据长度) | |
| 119 | + int expectedLength = ((bytes[0] & 0xFF) << 24) | | |
| 120 | + ((bytes[1] & 0xFF) << 16) | | |
| 121 | + ((bytes[2] & 0xFF) << 8) | | |
| 122 | + (bytes[3] & 0xFF); | |
| 123 | + | |
| 124 | + // 验证长度是否合理 | |
| 125 | + if (expectedLength == bytes.length - 4 && expectedLength > 0 && expectedLength < 1024 * 1024) { | |
| 126 | + return false; // 二进制格式 | |
| 127 | + } | |
| 128 | + } | |
| 129 | + } catch (Exception e) { | |
| 130 | + log.warn("[isJsonFormat][协议检测异常] 使用默认协议: {}", getDefaultProtocol(), e); | |
| 131 | + } | |
| 132 | + | |
| 133 | + // 默认使用当前设置的协议类型 | |
| 134 | + return useJsonByDefault; | |
| 135 | + } | |
| 136 | +} | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/IotTcpDeviceMessageCodec.java deleted
| 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 | - // TODO @haohao:有没可能去掉这个 code 到 method 的映射哈? | |
| 58 | - initializeMethodMappings(); | |
| 59 | - } | |
| 60 | - | |
| 61 | - // ==================== 缓存管理 ==================== | |
| 62 | - | |
| 63 | - /** | |
| 64 | - * JSON 缓存,提升编解码性能 | |
| 65 | - */ | |
| 66 | - private final Map<String, JSONObject> jsonCache = new ConcurrentHashMap<>(); | |
| 67 | - | |
| 68 | - /** | |
| 69 | - * 缓存最大大小 | |
| 70 | - */ | |
| 71 | - private static final int MAX_CACHE_SIZE = 1000; | |
| 72 | - | |
| 73 | - // ==================== 常量定义 ==================== | |
| 74 | - | |
| 75 | - /** | |
| 76 | - * 负载字段名 | |
| 77 | - */ | |
| 78 | - public static class PayloadField { | |
| 79 | - | |
| 80 | - public static final String TIMESTAMP = "timestamp"; | |
| 81 | - public static final String MESSAGE_ID = "msgId"; | |
| 82 | - public static final String DEVICE_ID = "deviceId"; | |
| 83 | - public static final String PARAMS = "params"; | |
| 84 | - public static final String DATA = "data"; | |
| 85 | - public static final String CODE = "code"; | |
| 86 | - public static final String MESSAGE = "message"; | |
| 87 | - | |
| 88 | - } | |
| 89 | - | |
| 90 | - /** | |
| 91 | - * 消息方法映射 | |
| 92 | - */ | |
| 93 | - public static class MessageMethod { | |
| 94 | - | |
| 95 | - public static final String PROPERTY_POST = "thing.property.post"; | |
| 96 | - public static final String PROPERTY_SET = "thing.property.set"; | |
| 97 | - public static final String PROPERTY_GET = "thing.property.get"; | |
| 98 | - public static final String EVENT_POST = "thing.event.post"; | |
| 99 | - public static final String SERVICE_INVOKE = "thing.service.invoke"; | |
| 100 | - public static final String CONFIG_PUSH = "thing.config.push"; | |
| 101 | - public static final String OTA_UPGRADE = "thing.ota.upgrade"; | |
| 102 | - public static final String STATE_ONLINE = "thing.state.online"; | |
| 103 | - public static final String STATE_OFFLINE = "thing.state.offline"; | |
| 104 | - | |
| 105 | - } | |
| 106 | - | |
| 107 | - // ==================== 初始化方法 ==================== | |
| 108 | - | |
| 109 | - /** | |
| 110 | - * 初始化方法映射 | |
| 111 | - */ | |
| 112 | - private static void initializeMethodMappings() { | |
| 113 | - METHOD_TO_CODE_MAP.put(MessageMethod.PROPERTY_POST, TcpDataPackage.CODE_DATA_UP); | |
| 114 | - METHOD_TO_CODE_MAP.put(MessageMethod.PROPERTY_SET, TcpDataPackage.CODE_PROPERTY_SET); | |
| 115 | - METHOD_TO_CODE_MAP.put(MessageMethod.PROPERTY_GET, TcpDataPackage.CODE_PROPERTY_GET); | |
| 116 | - METHOD_TO_CODE_MAP.put(MessageMethod.EVENT_POST, TcpDataPackage.CODE_EVENT_UP); | |
| 117 | - METHOD_TO_CODE_MAP.put(MessageMethod.SERVICE_INVOKE, TcpDataPackage.CODE_SERVICE_INVOKE); | |
| 118 | - METHOD_TO_CODE_MAP.put(MessageMethod.CONFIG_PUSH, TcpDataPackage.CODE_DATA_DOWN); | |
| 119 | - METHOD_TO_CODE_MAP.put(MessageMethod.OTA_UPGRADE, TcpDataPackage.CODE_DATA_DOWN); | |
| 120 | - METHOD_TO_CODE_MAP.put(MessageMethod.STATE_ONLINE, TcpDataPackage.CODE_HEARTBEAT); | |
| 121 | - METHOD_TO_CODE_MAP.put(MessageMethod.STATE_OFFLINE, TcpDataPackage.CODE_HEARTBEAT); | |
| 122 | - | |
| 123 | - // 反向映射 | |
| 124 | - METHOD_TO_CODE_MAP.forEach((method, code) -> CODE_TO_METHOD_MAP.put(code, method)); | |
| 125 | - } | |
| 126 | - | |
| 127 | - // ==================== 编解码方法 ==================== | |
| 128 | - | |
| 129 | - @Override | |
| 130 | - public byte[] encode(IotDeviceMessage message) { | |
| 131 | - validateEncodeParams(message); | |
| 132 | - | |
| 133 | - try { | |
| 134 | - if (log.isDebugEnabled()) { | |
| 135 | - log.debug("[encode][开始编码 TCP 消息] 方法: {}, 消息ID: {}", | |
| 136 | - message.getMethod(), message.getRequestId()); | |
| 137 | - } | |
| 138 | - | |
| 139 | - // 1. 获取功能码 | |
| 140 | - short code = getCodeByMethodSafely(message.getMethod()); | |
| 141 | - | |
| 142 | - // 2. 构建负载 | |
| 143 | - String payload = buildPayloadOptimized(message); | |
| 144 | - | |
| 145 | - // 3. 构建 TCP 数据包 | |
| 146 | - TcpDataPackage dataPackage = TcpDataPackage.builder() | |
| 147 | - .addr("") | |
| 148 | - .code(code) | |
| 149 | - .mid((short) 0) | |
| 150 | - .payload(payload) | |
| 151 | - .build(); | |
| 152 | - | |
| 153 | - // 4. 编码为字节流 | |
| 154 | - Buffer buffer = TcpDataEncoder.encode(dataPackage); | |
| 155 | - byte[] result = buffer.getBytes(); | |
| 156 | - | |
| 157 | - // 5. 统计信息 | |
| 158 | - if (log.isDebugEnabled()) { | |
| 159 | - log.debug("[encode][TCP 消息编码成功] 方法: {}, 数据长度: {}", | |
| 160 | - message.getMethod(), result.length); | |
| 161 | - } | |
| 162 | - return result; | |
| 163 | - } catch (Exception e) { | |
| 164 | - log.error("[encode][TCP 消息编码失败] 消息: {}", message, e); | |
| 165 | - throw new TcpCodecException("TCP 消息编码失败", e); | |
| 166 | - } | |
| 167 | - } | |
| 168 | - | |
| 169 | - @Override | |
| 170 | - public IotDeviceMessage decode(byte[] bytes) { | |
| 171 | - validateDecodeParams(bytes); | |
| 172 | - | |
| 173 | - try { | |
| 174 | - if (log.isDebugEnabled()) { | |
| 175 | - log.debug("[decode][开始解码 TCP 消息] 数据长度: {}", bytes.length); | |
| 176 | - } | |
| 177 | - | |
| 178 | - // 1. 解码 TCP 数据包 | |
| 179 | - Buffer buffer = Buffer.buffer(bytes); | |
| 180 | - TcpDataPackage dataPackage = TcpDataDecoder.decode(buffer); | |
| 181 | - // 2. 获取消息方法 | |
| 182 | - String method = getMethodByCodeSafely(dataPackage.getCode()); | |
| 183 | - // 3. 解析负载数据 | |
| 184 | - Object params = parsePayloadOptimized(dataPackage.getPayload()); | |
| 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 | - return message; | |
| 194 | - } catch (Exception e) { | |
| 195 | - log.error("[decode][TCP 消息解码失败] 数据长度: {}, 数据内容: {}", | |
| 196 | - bytes.length, truncateData(bytes, 100), e); | |
| 197 | - throw new TcpCodecException("TCP 消息解码失败", e); | |
| 198 | - } | |
| 199 | - } | |
| 200 | - | |
| 201 | - @Override | |
| 202 | - public String type() { | |
| 203 | - return TYPE; | |
| 204 | - } | |
| 205 | - | |
| 206 | - // ==================== 内部辅助方法 ==================== | |
| 207 | - | |
| 208 | - /** | |
| 209 | - * 验证编码参数 | |
| 210 | - */ | |
| 211 | - private void validateEncodeParams(IotDeviceMessage message) { | |
| 212 | - if (Objects.isNull(message)) { | |
| 213 | - throw new IllegalArgumentException("IoT 设备消息不能为空"); | |
| 214 | - } | |
| 215 | - if (StrUtil.isEmpty(message.getMethod())) { | |
| 216 | - throw new IllegalArgumentException("消息方法不能为空"); | |
| 217 | - } | |
| 218 | - } | |
| 219 | - | |
| 220 | - /** | |
| 221 | - * 验证解码参数 | |
| 222 | - */ | |
| 223 | - private void validateDecodeParams(byte[] bytes) { | |
| 224 | - if (Objects.isNull(bytes) || bytes.length == 0) { | |
| 225 | - throw new IllegalArgumentException("待解码数据不能为空"); | |
| 226 | - } | |
| 227 | - if (bytes.length > 1024 * 1024) { | |
| 228 | - throw new IllegalArgumentException("数据包过大,超过 1MB 限制"); | |
| 229 | - } | |
| 230 | - } | |
| 231 | - | |
| 232 | - /** | |
| 233 | - * 安全获取功能码 | |
| 234 | - */ | |
| 235 | - private short getCodeByMethodSafely(String method) { | |
| 236 | - Short code = METHOD_TO_CODE_MAP.get(method); | |
| 237 | - // 默认为数据上报 | |
| 238 | - if (code == null) { | |
| 239 | - log.warn("[getCodeByMethodSafely][未知的消息方法: {},使用默认功能码]", method); | |
| 240 | - return TcpDataPackage.CODE_DATA_UP; | |
| 241 | - } | |
| 242 | - return code; | |
| 243 | - } | |
| 244 | - | |
| 245 | - /** | |
| 246 | - * 安全获取消息方法 | |
| 247 | - */ | |
| 248 | - private String getMethodByCodeSafely(short code) { | |
| 249 | - String method = CODE_TO_METHOD_MAP.get(code); | |
| 250 | - if (method == null) { | |
| 251 | - log.warn("[getMethodByCodeSafely][未知的功能码: {},使用默认方法]", code); | |
| 252 | - return MessageMethod.PROPERTY_POST; // 默认为属性上报 | |
| 253 | - } | |
| 254 | - return method; | |
| 255 | - } | |
| 256 | - | |
| 257 | - /** | |
| 258 | - * 优化的负载构建 | |
| 259 | - */ | |
| 260 | - private String buildPayloadOptimized(IotDeviceMessage message) { | |
| 261 | - // 使用缓存键 | |
| 262 | - // TODO @haohao:是不是不用缓存哈? | |
| 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 | - addToPayloadIfNotNull(payload, PayloadField.MESSAGE_ID, message.getRequestId()); | |
| 276 | - addToPayloadIfNotNull(payload, PayloadField.DEVICE_ID, message.getDeviceId()); | |
| 277 | - addToPayloadIfNotNull(payload, PayloadField.PARAMS, message.getParams()); | |
| 278 | - addToPayloadIfNotNull(payload, PayloadField.DATA, message.getData()); | |
| 279 | - addToPayloadIfNotNull(payload, PayloadField.CODE, message.getCode()); | |
| 280 | - addToPayloadIfNotEmpty(payload, PayloadField.MESSAGE, message.getMsg()); | |
| 281 | - // 添加时间戳 | |
| 282 | - payload.set(PayloadField.TIMESTAMP, System.currentTimeMillis()); | |
| 283 | - | |
| 284 | - // 缓存管理 | |
| 285 | - if (jsonCache.size() < MAX_CACHE_SIZE) { | |
| 286 | - jsonCache.put(cacheKey, payload); | |
| 287 | - } else { | |
| 288 | - cleanJsonCacheIfNeeded(); | |
| 289 | - } | |
| 290 | - | |
| 291 | - return payload.toString(); | |
| 292 | - } | |
| 293 | - | |
| 294 | - /** | |
| 295 | - * 优化的负载解析 | |
| 296 | - */ | |
| 297 | - private Object parsePayloadOptimized(String payload) { | |
| 298 | - if (StrUtil.isEmpty(payload)) { | |
| 299 | - return null; | |
| 300 | - } | |
| 301 | - | |
| 302 | - try { | |
| 303 | - // 尝试从缓存获取 | |
| 304 | - JSONObject cachedJson = jsonCache.get(payload); | |
| 305 | - if (cachedJson != null) { | |
| 306 | - return cachedJson.containsKey(PayloadField.PARAMS) ? cachedJson.get(PayloadField.PARAMS) : cachedJson; | |
| 307 | - } | |
| 308 | - | |
| 309 | - // 解析 JSON 对象 | |
| 310 | - JSONObject jsonObject = JSONUtil.parseObj(payload); | |
| 311 | - | |
| 312 | - // 缓存解析结果 | |
| 313 | - if (jsonCache.size() < MAX_CACHE_SIZE) { | |
| 314 | - jsonCache.put(payload, jsonObject); | |
| 315 | - } | |
| 316 | - | |
| 317 | - return jsonObject.containsKey(PayloadField.PARAMS) ? jsonObject.get(PayloadField.PARAMS) : jsonObject; | |
| 318 | - } catch (JSONException e) { | |
| 319 | - log.warn("[parsePayloadOptimized][负载解析为JSON失败,返回原始字符串] 负载: {}", payload); | |
| 320 | - return payload; | |
| 321 | - } catch (Exception e) { | |
| 322 | - log.error("[parsePayloadOptimized][负载解析异常] 负载: {}", payload, e); | |
| 323 | - return payload; | |
| 324 | - } | |
| 325 | - } | |
| 326 | - | |
| 327 | - /** | |
| 328 | - * 添加非空值到负载 | |
| 329 | - */ | |
| 330 | - private void addToPayloadIfNotNull(JSONObject json, String key, Object value) { | |
| 331 | - if (ObjectUtil.isNotNull(value)) { | |
| 332 | - json.set(key, value); | |
| 333 | - } | |
| 334 | - } | |
| 335 | - | |
| 336 | - /** | |
| 337 | - * 添加非空字符串到负载 | |
| 338 | - */ | |
| 339 | - private void addToPayloadIfNotEmpty(JSONObject json, String key, String value) { | |
| 340 | - if (StrUtil.isNotEmpty(value)) { | |
| 341 | - json.set(key, value); | |
| 342 | - } | |
| 343 | - } | |
| 344 | - | |
| 345 | - /** | |
| 346 | - * 清理JSON缓存 | |
| 347 | - */ | |
| 348 | - private void cleanJsonCacheIfNeeded() { | |
| 349 | - if (jsonCache.size() > MAX_CACHE_SIZE) { | |
| 350 | - // 清理一半的缓存 | |
| 351 | - int clearCount = jsonCache.size() / 2; | |
| 352 | - jsonCache.entrySet().removeIf(entry -> clearCount > 0 && Math.random() < 0.5); | |
| 353 | - | |
| 354 | - if (log.isDebugEnabled()) { | |
| 355 | - log.debug("[cleanJsonCacheIfNeeded][JSON 缓存已清理] 当前缓存大小: {}", jsonCache.size()); | |
| 356 | - } | |
| 357 | - } | |
| 358 | - } | |
| 359 | - | |
| 360 | - /** | |
| 361 | - * 截断数据用于日志输出 | |
| 362 | - */ | |
| 363 | - private String truncateData(byte[] data, int maxLength) { | |
| 364 | - if (data.length <= maxLength) { | |
| 365 | - return new String(data, StandardCharsets.UTF_8); | |
| 366 | - } | |
| 367 | - | |
| 368 | - byte[] truncated = new byte[maxLength]; | |
| 369 | - System.arraycopy(data, 0, truncated, 0, maxLength); | |
| 370 | - return new String(truncated, StandardCharsets.UTF_8) + "...(截断)"; | |
| 371 | - } | |
| 372 | - | |
| 373 | - // ==================== 自定义异常 ==================== | |
| 374 | - | |
| 375 | - /** | |
| 376 | - * TCP 编解码异常 | |
| 377 | - */ | |
| 378 | - public static class TcpCodecException extends RuntimeException { | |
| 379 | - | |
| 380 | - public TcpCodecException(String message) { | |
| 381 | - super(message); | |
| 382 | - } | |
| 383 | - | |
| 384 | - public TcpCodecException(String message, Throwable cause) { | |
| 385 | - super(message, cause); | |
| 386 | - } | |
| 387 | - | |
| 388 | - } | |
| 389 | -} | |
| 390 | 0 | \ No newline at end of file |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/IotTcpJsonDeviceMessageCodec.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.iot.gateway.codec.tcp; | |
| 2 | + | |
| 3 | +import cn.hutool.core.util.IdUtil; | |
| 4 | +import cn.hutool.core.util.StrUtil; | |
| 5 | +import cn.hutool.json.JSONObject; | |
| 6 | +import cn.hutool.json.JSONUtil; | |
| 7 | +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; | |
| 8 | +import cn.iocoder.yudao.module.iot.gateway.codec.IotDeviceMessageCodec; | |
| 9 | +import lombok.extern.slf4j.Slf4j; | |
| 10 | +import org.springframework.stereotype.Component; | |
| 11 | + | |
| 12 | +import java.nio.charset.StandardCharsets; | |
| 13 | + | |
| 14 | +/** | |
| 15 | + * TCP JSON格式 {@link IotDeviceMessage} 编解码器 | |
| 16 | + * | |
| 17 | + * 采用纯JSON格式传输,参考EMQX和HTTP模块的数据格式 | |
| 18 | + * | |
| 19 | + * JSON消息格式: | |
| 20 | + * { | |
| 21 | + * "id": "消息ID", | |
| 22 | + * "method": "消息方法", | |
| 23 | + * "deviceId": "设备ID", | |
| 24 | + * "productKey": "产品Key", | |
| 25 | + * "deviceName": "设备名称", | |
| 26 | + * "params": {...}, | |
| 27 | + * "timestamp": 时间戳 | |
| 28 | + * } | |
| 29 | + * | |
| 30 | + * @author 芋道源码 | |
| 31 | + */ | |
| 32 | +@Slf4j | |
| 33 | +@Component | |
| 34 | +public class IotTcpJsonDeviceMessageCodec implements IotDeviceMessageCodec { | |
| 35 | + | |
| 36 | + public static final String TYPE = "TCP_JSON"; | |
| 37 | + | |
| 38 | + // ==================== 常量定义 ==================== | |
| 39 | + | |
| 40 | + @Override | |
| 41 | + public String type() { | |
| 42 | + return TYPE; | |
| 43 | + } | |
| 44 | + | |
| 45 | + @Override | |
| 46 | + public byte[] encode(IotDeviceMessage message) { | |
| 47 | + if (message == null || StrUtil.isEmpty(message.getMethod())) { | |
| 48 | + throw new IllegalArgumentException("消息或方法不能为空"); | |
| 49 | + } | |
| 50 | + | |
| 51 | + try { | |
| 52 | + // 构建JSON消息 | |
| 53 | + JSONObject jsonMessage = buildJsonMessage(message); | |
| 54 | + | |
| 55 | + // 转换为字节数组 | |
| 56 | + String jsonString = jsonMessage.toString(); | |
| 57 | + byte[] result = jsonString.getBytes(StandardCharsets.UTF_8); | |
| 58 | + | |
| 59 | + if (log.isDebugEnabled()) { | |
| 60 | + log.debug("[encode][编码成功] 方法: {}, JSON长度: {}字节, 内容: {}", | |
| 61 | + message.getMethod(), result.length, jsonString); | |
| 62 | + } | |
| 63 | + | |
| 64 | + return result; | |
| 65 | + } catch (Exception e) { | |
| 66 | + log.error("[encode][编码失败] 方法: {}", message.getMethod(), e); | |
| 67 | + throw new RuntimeException("JSON消息编码失败", e); | |
| 68 | + } | |
| 69 | + } | |
| 70 | + | |
| 71 | + // ==================== 编解码方法 ==================== | |
| 72 | + | |
| 73 | + @Override | |
| 74 | + public IotDeviceMessage decode(byte[] bytes) { | |
| 75 | + if (bytes == null || bytes.length == 0) { | |
| 76 | + throw new IllegalArgumentException("待解码数据不能为空"); | |
| 77 | + } | |
| 78 | + | |
| 79 | + try { | |
| 80 | + // 转换为JSON字符串 | |
| 81 | + String jsonString = new String(bytes, StandardCharsets.UTF_8); | |
| 82 | + | |
| 83 | + if (log.isDebugEnabled()) { | |
| 84 | + log.debug("[decode][开始解码] JSON长度: {}字节, 内容: {}", bytes.length, jsonString); | |
| 85 | + } | |
| 86 | + | |
| 87 | + // 解析JSON消息 | |
| 88 | + JSONObject jsonMessage = JSONUtil.parseObj(jsonString); | |
| 89 | + | |
| 90 | + // 构建IoT设备消息 | |
| 91 | + IotDeviceMessage message = parseJsonMessage(jsonMessage); | |
| 92 | + | |
| 93 | + if (log.isDebugEnabled()) { | |
| 94 | + log.debug("[decode][解码成功] 消息ID: {}, 方法: {}, 设备ID: {}", | |
| 95 | + message.getId(), message.getMethod(), message.getDeviceId()); | |
| 96 | + } | |
| 97 | + | |
| 98 | + return message; | |
| 99 | + } catch (Exception e) { | |
| 100 | + log.error("[decode][解码失败] 数据长度: {}", bytes.length, e); | |
| 101 | + throw new RuntimeException("JSON消息解码失败", e); | |
| 102 | + } | |
| 103 | + } | |
| 104 | + | |
| 105 | + /** | |
| 106 | + * 编码数据上报消息 | |
| 107 | + */ | |
| 108 | + public byte[] encodeDataReport(Object params, Long deviceId, String productKey, String deviceName) { | |
| 109 | + IotDeviceMessage message = createMessage(MessageMethod.PROPERTY_POST, params, deviceId, productKey, deviceName); | |
| 110 | + return encode(message); | |
| 111 | + } | |
| 112 | + | |
| 113 | + /** | |
| 114 | + * 编码心跳消息 | |
| 115 | + */ | |
| 116 | + public byte[] encodeHeartbeat(Long deviceId, String productKey, String deviceName) { | |
| 117 | + IotDeviceMessage message = createMessage(MessageMethod.STATE_ONLINE, null, deviceId, productKey, deviceName); | |
| 118 | + return encode(message); | |
| 119 | + } | |
| 120 | + | |
| 121 | + // ==================== 便捷方法 ==================== | |
| 122 | + | |
| 123 | + /** | |
| 124 | + * 编码事件上报消息 | |
| 125 | + */ | |
| 126 | + public byte[] encodeEventReport(Object params, Long deviceId, String productKey, String deviceName) { | |
| 127 | + IotDeviceMessage message = createMessage(MessageMethod.EVENT_POST, params, deviceId, productKey, deviceName); | |
| 128 | + return encode(message); | |
| 129 | + } | |
| 130 | + | |
| 131 | + /** | |
| 132 | + * 构建JSON消息 | |
| 133 | + */ | |
| 134 | + private JSONObject buildJsonMessage(IotDeviceMessage message) { | |
| 135 | + JSONObject jsonMessage = new JSONObject(); | |
| 136 | + | |
| 137 | + // 基础字段 | |
| 138 | + jsonMessage.set(JsonField.ID, StrUtil.isNotEmpty(message.getId()) ? message.getId() : IdUtil.fastSimpleUUID()); | |
| 139 | + jsonMessage.set(JsonField.METHOD, message.getMethod()); | |
| 140 | + jsonMessage.set(JsonField.TIMESTAMP, System.currentTimeMillis()); | |
| 141 | + | |
| 142 | + // 设备信息 | |
| 143 | + if (message.getDeviceId() != null) { | |
| 144 | + jsonMessage.set(JsonField.DEVICE_ID, message.getDeviceId()); | |
| 145 | + } | |
| 146 | + | |
| 147 | + // 参数 | |
| 148 | + if (message.getParams() != null) { | |
| 149 | + jsonMessage.set(JsonField.PARAMS, message.getParams()); | |
| 150 | + } | |
| 151 | + | |
| 152 | + // 响应码和消息(用于下行消息) | |
| 153 | + if (message.getCode() != null) { | |
| 154 | + jsonMessage.set(JsonField.CODE, message.getCode()); | |
| 155 | + } | |
| 156 | + if (StrUtil.isNotEmpty(message.getMsg())) { | |
| 157 | + jsonMessage.set(JsonField.MESSAGE, message.getMsg()); | |
| 158 | + } | |
| 159 | + | |
| 160 | + return jsonMessage; | |
| 161 | + } | |
| 162 | + | |
| 163 | + /** | |
| 164 | + * 解析JSON消息 | |
| 165 | + */ | |
| 166 | + private IotDeviceMessage parseJsonMessage(JSONObject jsonMessage) { | |
| 167 | + // 提取基础字段 | |
| 168 | + String id = jsonMessage.getStr(JsonField.ID); | |
| 169 | + String method = jsonMessage.getStr(JsonField.METHOD); | |
| 170 | + Object params = jsonMessage.get(JsonField.PARAMS); | |
| 171 | + | |
| 172 | + // 创建消息对象 | |
| 173 | + IotDeviceMessage message = IotDeviceMessage.requestOf(id, method, params); | |
| 174 | + | |
| 175 | + // 设置设备信息 | |
| 176 | + Long deviceId = jsonMessage.getLong(JsonField.DEVICE_ID); | |
| 177 | + if (deviceId != null) { | |
| 178 | + message.setDeviceId(deviceId); | |
| 179 | + } | |
| 180 | + | |
| 181 | + // 设置响应信息 | |
| 182 | + Integer code = jsonMessage.getInt(JsonField.CODE); | |
| 183 | + if (code != null) { | |
| 184 | + message.setCode(code); | |
| 185 | + } | |
| 186 | + | |
| 187 | + String msg = jsonMessage.getStr(JsonField.MESSAGE); | |
| 188 | + if (StrUtil.isNotEmpty(msg)) { | |
| 189 | + message.setMsg(msg); | |
| 190 | + } | |
| 191 | + | |
| 192 | + // 设置服务ID(基于JSON格式) | |
| 193 | + message.setServerId(generateServerId(jsonMessage)); | |
| 194 | + | |
| 195 | + return message; | |
| 196 | + } | |
| 197 | + | |
| 198 | + // ==================== 内部辅助方法 ==================== | |
| 199 | + | |
| 200 | + /** | |
| 201 | + * 创建消息对象 | |
| 202 | + */ | |
| 203 | + private IotDeviceMessage createMessage(String method, Object params, Long deviceId, String productKey, String deviceName) { | |
| 204 | + IotDeviceMessage message = IotDeviceMessage.requestOf(method, params); | |
| 205 | + message.setDeviceId(deviceId); | |
| 206 | + return message; | |
| 207 | + } | |
| 208 | + | |
| 209 | + /** | |
| 210 | + * 生成服务ID | |
| 211 | + */ | |
| 212 | + private String generateServerId(JSONObject jsonMessage) { | |
| 213 | + String id = jsonMessage.getStr(JsonField.ID); | |
| 214 | + Long deviceId = jsonMessage.getLong(JsonField.DEVICE_ID); | |
| 215 | + return String.format("tcp_json_%s_%s", deviceId != null ? deviceId : "unknown", | |
| 216 | + StrUtil.isNotEmpty(id) ? id.substring(0, Math.min(8, id.length())) : "noId"); | |
| 217 | + } | |
| 218 | + | |
| 219 | + /** | |
| 220 | + * 消息方法常量 | |
| 221 | + */ | |
| 222 | + public static class MessageMethod { | |
| 223 | + public static final String PROPERTY_POST = "thing.property.post"; // 数据上报 | |
| 224 | + public static final String STATE_ONLINE = "thing.state.online"; // 心跳 | |
| 225 | + public static final String EVENT_POST = "thing.event.post"; // 事件上报 | |
| 226 | + public static final String PROPERTY_SET = "thing.property.set"; // 属性设置 | |
| 227 | + public static final String PROPERTY_GET = "thing.property.get"; // 属性获取 | |
| 228 | + public static final String SERVICE_INVOKE = "thing.service.invoke"; // 服务调用 | |
| 229 | + } | |
| 230 | + | |
| 231 | + /** | |
| 232 | + * JSON字段名(参考EMQX和HTTP模块格式) | |
| 233 | + */ | |
| 234 | + private static class JsonField { | |
| 235 | + public static final String ID = "id"; | |
| 236 | + public static final String METHOD = "method"; | |
| 237 | + public static final String DEVICE_ID = "deviceId"; | |
| 238 | + public static final String PRODUCT_KEY = "productKey"; | |
| 239 | + public static final String DEVICE_NAME = "deviceName"; | |
| 240 | + public static final String PARAMS = "params"; | |
| 241 | + public static final String TIMESTAMP = "timestamp"; | |
| 242 | + public static final String CODE = "code"; | |
| 243 | + public static final String MESSAGE = "message"; | |
| 244 | + } | |
| 245 | +} | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayConfiguration.java
| ... | ... | @@ -2,6 +2,7 @@ package cn.iocoder.yudao.module.iot.gateway.config; |
| 2 | 2 | |
| 3 | 3 | import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; |
| 4 | 4 | import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; |
| 5 | +import cn.iocoder.yudao.module.iot.gateway.codec.tcp.IotTcpCodecManager; | |
| 5 | 6 | import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxAuthEventProtocol; |
| 6 | 7 | import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxDownstreamSubscriber; |
| 7 | 8 | import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxUpstreamProtocol; |
| ... | ... | @@ -9,7 +10,6 @@ import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpDownstreamSubscr |
| 9 | 10 | import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpUpstreamProtocol; |
| 10 | 11 | import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpDownstreamSubscriber; |
| 11 | 12 | import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpUpstreamProtocol; |
| 12 | -import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.manager.TcpDeviceConnectionManager; | |
| 13 | 13 | import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService; |
| 14 | 14 | import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; |
| 15 | 15 | import io.vertx.core.Vertx; |
| ... | ... | @@ -90,27 +90,21 @@ public class IotGatewayConfiguration { |
| 90 | 90 | } |
| 91 | 91 | |
| 92 | 92 | @Bean |
| 93 | - public TcpDeviceConnectionManager tcpDeviceConnectionManager() { | |
| 94 | - return new TcpDeviceConnectionManager(); | |
| 95 | - } | |
| 96 | - | |
| 97 | - @Bean | |
| 98 | 93 | public IotTcpUpstreamProtocol iotTcpUpstreamProtocol(IotGatewayProperties gatewayProperties, |
| 99 | - TcpDeviceConnectionManager connectionManager, | |
| 100 | 94 | IotDeviceService deviceService, |
| 101 | 95 | IotDeviceMessageService messageService, |
| 102 | 96 | IotDeviceCommonApi deviceApi, |
| 97 | + IotTcpCodecManager codecManager, | |
| 103 | 98 | Vertx tcpVertx) { |
| 104 | - return new IotTcpUpstreamProtocol(gatewayProperties.getProtocol().getTcp(), connectionManager, | |
| 105 | - deviceService, messageService, deviceApi, tcpVertx); | |
| 99 | + return new IotTcpUpstreamProtocol(gatewayProperties.getProtocol().getTcp(), | |
| 100 | + deviceService, messageService, deviceApi, codecManager, tcpVertx); | |
| 106 | 101 | } |
| 107 | 102 | |
| 108 | 103 | @Bean |
| 109 | 104 | public IotTcpDownstreamSubscriber iotTcpDownstreamSubscriber(IotTcpUpstreamProtocol protocolHandler, |
| 110 | - TcpDeviceConnectionManager connectionManager, | |
| 111 | 105 | IotDeviceMessageService messageService, |
| 112 | 106 | IotMessageBus messageBus) { |
| 113 | - return new IotTcpDownstreamSubscriber(protocolHandler, connectionManager, messageService, messageBus); | |
| 107 | + return new IotTcpDownstreamSubscriber(protocolHandler, messageService, messageBus); | |
| 114 | 108 | } |
| 115 | 109 | |
| 116 | 110 | } | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpDownstreamSubscriber.java
| ... | ... | @@ -4,161 +4,67 @@ import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; |
| 4 | 4 | import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageSubscriber; |
| 5 | 5 | import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; |
| 6 | 6 | import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; |
| 7 | -import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.manager.TcpDeviceConnectionManager; | |
| 8 | 7 | import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.router.IotTcpDownstreamHandler; |
| 9 | 8 | import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; |
| 10 | 9 | import jakarta.annotation.PostConstruct; |
| 11 | -import jakarta.annotation.PreDestroy; | |
| 12 | -import lombok.RequiredArgsConstructor; | |
| 13 | 10 | import lombok.extern.slf4j.Slf4j; |
| 14 | 11 | |
| 15 | -import java.util.concurrent.atomic.AtomicBoolean; | |
| 16 | -import java.util.concurrent.atomic.AtomicLong; | |
| 17 | - | |
| 18 | 12 | /** |
| 19 | 13 | * IoT 网关 TCP 下游订阅者:接收下行给设备的消息 |
| 20 | - * <p> | |
| 21 | - * 参考 EMQX 设计理念: | |
| 22 | - * 1. 高性能消息路由 | |
| 23 | - * 2. 容错机制 | |
| 24 | - * 3. 状态监控 | |
| 25 | - * 4. 资源管理 | |
| 26 | 14 | * |
| 27 | 15 | * @author 芋道源码 |
| 28 | 16 | */ |
| 29 | -@RequiredArgsConstructor | |
| 30 | 17 | @Slf4j |
| 31 | 18 | public class IotTcpDownstreamSubscriber implements IotMessageSubscriber<IotDeviceMessage> { |
| 32 | 19 | |
| 33 | - private final IotTcpUpstreamProtocol protocolHandler; | |
| 34 | - | |
| 35 | - private final TcpDeviceConnectionManager connectionManager; | |
| 36 | - | |
| 37 | - private final IotDeviceMessageService messageService; | |
| 20 | + private final IotTcpDownstreamHandler downstreamHandler; | |
| 38 | 21 | |
| 39 | 22 | private final IotMessageBus messageBus; |
| 40 | 23 | |
| 41 | - private volatile IotTcpDownstreamHandler downstreamHandler; | |
| 42 | - | |
| 43 | - private final AtomicBoolean initialized = new AtomicBoolean(false); | |
| 44 | - | |
| 45 | - private final AtomicLong processedMessages = new AtomicLong(0); | |
| 24 | + private final IotTcpUpstreamProtocol protocol; | |
| 46 | 25 | |
| 47 | - private final AtomicLong failedMessages = new AtomicLong(0); | |
| 26 | + public IotTcpDownstreamSubscriber(IotTcpUpstreamProtocol protocol, | |
| 27 | + IotDeviceMessageService messageService, | |
| 28 | + IotMessageBus messageBus) { | |
| 29 | + this.protocol = protocol; | |
| 30 | + this.messageBus = messageBus; | |
| 31 | + this.downstreamHandler = new IotTcpDownstreamHandler(messageService); | |
| 32 | + } | |
| 48 | 33 | |
| 49 | 34 | @PostConstruct |
| 50 | 35 | 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 | - } | |
| 36 | + messageBus.register(this); | |
| 86 | 37 | } |
| 87 | 38 | |
| 88 | 39 | @Override |
| 89 | 40 | public String getTopic() { |
| 90 | - return IotDeviceMessageUtils.buildMessageBusGatewayDeviceMessageTopic(protocolHandler.getServerId()); | |
| 41 | + return IotDeviceMessageUtils.buildMessageBusGatewayDeviceMessageTopic(protocol.getServerId()); | |
| 91 | 42 | } |
| 92 | 43 | |
| 93 | 44 | @Override |
| 94 | 45 | public String getGroup() { |
| 95 | - return "tcp-downstream-" + protocolHandler.getServerId(); | |
| 46 | + // 保证点对点消费,需要保证独立的 Group,所以使用 Topic 作为 Group | |
| 47 | + return getTopic(); | |
| 96 | 48 | } |
| 97 | 49 | |
| 98 | 50 | @Override |
| 99 | 51 | public void onMessage(IotDeviceMessage message) { |
| 100 | - if (!initialized.get()) { | |
| 101 | - log.warn("[onMessage][订阅者未初始化,跳过消息处理]"); | |
| 102 | - return; | |
| 103 | - } | |
| 104 | - | |
| 105 | - long startTime = System.currentTimeMillis(); | |
| 106 | - | |
| 52 | + log.debug("[onMessage][接收到下行消息, messageId: {}, method: {}, deviceId: {}]", | |
| 53 | + message.getId(), message.getMethod(), message.getDeviceId()); | |
| 107 | 54 | 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 | - if (message.getDeviceId() == null) { | |
| 116 | - log.warn("[onMessage][下行消息设备 ID 为空,跳过处理] 消息: {}", message); | |
| 117 | - return; | |
| 118 | - } | |
| 119 | - // 检查连接状态 | |
| 120 | - if (connectionManager.getClientByDeviceId(message.getDeviceId()) == null) { | |
| 121 | - log.warn("[onMessage][设备({})离线,跳过下行消息] 方法: {}", | |
| 122 | - message.getDeviceId(), message.getMethod()); | |
| 55 | + // 1. 校验 | |
| 56 | + String method = message.getMethod(); | |
| 57 | + if (method == null) { | |
| 58 | + log.warn("[onMessage][消息方法为空, messageId: {}, deviceId: {}]", | |
| 59 | + message.getId(), message.getDeviceId()); | |
| 123 | 60 | return; |
| 124 | 61 | } |
| 125 | 62 | |
| 126 | - // 处理下行消息 | |
| 63 | + // 2. 处理下行消息 | |
| 127 | 64 | downstreamHandler.handle(message); |
| 128 | - | |
| 129 | - // 性能监控 | |
| 130 | - long processTime = System.currentTimeMillis() - startTime; | |
| 131 | - // TODO @haohao:1000 搞成静态变量; | |
| 132 | - if (processTime > 1000) { // 超过 1 秒的慢消息 | |
| 133 | - log.warn("[onMessage][慢消息处理] 设备ID: {}, 方法: {}, 耗时: {}ms", | |
| 134 | - message.getDeviceId(), message.getMethod(), processTime); | |
| 135 | - } | |
| 136 | 65 | } catch (Exception e) { |
| 137 | - failedMessages.incrementAndGet(); | |
| 138 | - log.error("[onMessage][处理下行消息失败] 设备ID: {}, 方法: {}, 消息: {}", | |
| 139 | - message.getDeviceId(), message.getMethod(), message, e); | |
| 66 | + log.error("[onMessage][处理下行消息失败, messageId: {}, method: {}, deviceId: {}]", | |
| 67 | + message.getId(), message.getMethod(), message.getDeviceId(), e); | |
| 140 | 68 | } |
| 141 | 69 | } |
| 142 | - | |
| 143 | - // TODO @haohao:多余的要不先清理掉; | |
| 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 | - } | |
| 163 | - | |
| 164 | 70 | } |
| 165 | 71 | \ 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
| ... | ... | @@ -2,8 +2,8 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.tcp; |
| 2 | 2 | |
| 3 | 3 | import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; |
| 4 | 4 | import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; |
| 5 | +import cn.iocoder.yudao.module.iot.gateway.codec.tcp.IotTcpCodecManager; | |
| 5 | 6 | import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties; |
| 6 | -import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.manager.TcpDeviceConnectionManager; | |
| 7 | 7 | import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.router.IotTcpUpstreamHandler; |
| 8 | 8 | import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService; |
| 9 | 9 | import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; |
| ... | ... | @@ -16,19 +16,8 @@ import jakarta.annotation.PreDestroy; |
| 16 | 16 | import lombok.Getter; |
| 17 | 17 | import lombok.extern.slf4j.Slf4j; |
| 18 | 18 | |
| 19 | -import java.util.concurrent.CountDownLatch; | |
| 20 | -import java.util.concurrent.TimeUnit; | |
| 21 | -import java.util.concurrent.atomic.AtomicReference; | |
| 22 | - | |
| 23 | 19 | /** |
| 24 | 20 | * IoT 网关 TCP 协议:接收设备上行消息 |
| 25 | - * <p> | |
| 26 | - * 负责接收设备上行消息,支持: | |
| 27 | - * 1. 设备注册 | |
| 28 | - * 2. 心跳保活 | |
| 29 | - * 3. 属性上报 | |
| 30 | - * 4. 事件上报 | |
| 31 | - * 5. 设备连接管理 | |
| 32 | 21 | * |
| 33 | 22 | * @author 芋道源码 |
| 34 | 23 | */ |
| ... | ... | @@ -37,14 +26,14 @@ public class IotTcpUpstreamProtocol { |
| 37 | 26 | |
| 38 | 27 | private final IotGatewayProperties.TcpProperties tcpProperties; |
| 39 | 28 | |
| 40 | - private final TcpDeviceConnectionManager connectionManager; | |
| 41 | - | |
| 42 | 29 | private final IotDeviceService deviceService; |
| 43 | 30 | |
| 44 | 31 | private final IotDeviceMessageService messageService; |
| 45 | 32 | |
| 46 | 33 | private final IotDeviceCommonApi deviceApi; |
| 47 | 34 | |
| 35 | + private final IotTcpCodecManager codecManager; | |
| 36 | + | |
| 48 | 37 | private final Vertx vertx; |
| 49 | 38 | |
| 50 | 39 | @Getter |
| ... | ... | @@ -53,54 +42,30 @@ public class IotTcpUpstreamProtocol { |
| 53 | 42 | private NetServer netServer; |
| 54 | 43 | |
| 55 | 44 | public IotTcpUpstreamProtocol(IotGatewayProperties.TcpProperties tcpProperties, |
| 56 | - TcpDeviceConnectionManager connectionManager, | |
| 57 | - IotDeviceService deviceService, | |
| 58 | - IotDeviceMessageService messageService, | |
| 59 | - IotDeviceCommonApi deviceApi, | |
| 60 | - Vertx vertx) { | |
| 45 | + IotDeviceService deviceService, | |
| 46 | + IotDeviceMessageService messageService, | |
| 47 | + IotDeviceCommonApi deviceApi, | |
| 48 | + IotTcpCodecManager codecManager, | |
| 49 | + Vertx vertx) { | |
| 61 | 50 | this.tcpProperties = tcpProperties; |
| 62 | - this.connectionManager = connectionManager; | |
| 63 | 51 | this.deviceService = deviceService; |
| 64 | 52 | this.messageService = messageService; |
| 65 | 53 | this.deviceApi = deviceApi; |
| 54 | + this.codecManager = codecManager; | |
| 66 | 55 | this.vertx = vertx; |
| 67 | 56 | this.serverId = IotDeviceMessageUtils.generateServerId(tcpProperties.getPort()); |
| 68 | 57 | } |
| 69 | 58 | |
| 70 | 59 | @PostConstruct |
| 71 | 60 | 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 | - // TODO @haohao:同类的,最好使用相同序号前缀,一个方法看起来有段落感。包括同类可以去掉之间的空格。例如说这里的,1. 2. 3. 4. 是初始化;5. 6. 是管理启动 | |
| 96 | - // 1. 创建服务器选项 | |
| 61 | + // 创建服务器选项 | |
| 97 | 62 | NetServerOptions options = new NetServerOptions() |
| 98 | 63 | .setPort(tcpProperties.getPort()) |
| 99 | 64 | .setTcpKeepAlive(true) |
| 100 | 65 | .setTcpNoDelay(true) |
| 101 | 66 | .setReuseAddress(true); |
| 102 | 67 | |
| 103 | - // 2. 配置 SSL(如果启用) | |
| 68 | + // 配置 SSL(如果启用) | |
| 104 | 69 | if (Boolean.TRUE.equals(tcpProperties.getSslEnabled())) { |
| 105 | 70 | PemKeyCertOptions pemKeyCertOptions = new PemKeyCertOptions() |
| 106 | 71 | .setKeyPath(tcpProperties.getSslKeyPath()) |
| ... | ... | @@ -108,72 +73,32 @@ public class IotTcpUpstreamProtocol { |
| 108 | 73 | options.setSsl(true).setKeyCertOptions(pemKeyCertOptions); |
| 109 | 74 | } |
| 110 | 75 | |
| 111 | - // 3. 创建 TCP 服务器 | |
| 76 | + // 创建服务器并设置连接处理器 | |
| 112 | 77 | netServer = vertx.createNetServer(options); |
| 113 | - | |
| 114 | - // 4. 设置连接处理器 | |
| 115 | 78 | netServer.connectHandler(socket -> { |
| 116 | - log.info("[startTcpServer][新设备连接: {}]", socket.remoteAddress()); | |
| 117 | - IotTcpUpstreamHandler handler = new IotTcpUpstreamHandler( | |
| 118 | - tcpProperties, connectionManager, deviceService, messageService, deviceApi, serverId); | |
| 79 | + IotTcpUpstreamHandler handler = new IotTcpUpstreamHandler(this, messageService, codecManager); | |
| 119 | 80 | handler.handle(socket); |
| 120 | 81 | }); |
| 121 | 82 | |
| 122 | - // 5. 同步启动服务器,等待结果 | |
| 123 | - CountDownLatch latch = new CountDownLatch(1); | |
| 124 | - AtomicReference<Throwable> failure = new AtomicReference<>(); | |
| 125 | - netServer.listen(result -> { | |
| 126 | - if (result.succeeded()) { | |
| 127 | - log.info("[startTcpServer][TCP 服务器启动成功] 端口: {}, 服务器ID: {}", | |
| 128 | - result.result().actualPort(), serverId); | |
| 129 | - } else { | |
| 130 | - log.error("[startTcpServer][TCP 服务器启动失败]", result.cause()); | |
| 131 | - failure.set(result.cause()); | |
| 132 | - } | |
| 133 | - latch.countDown(); | |
| 134 | - }); | |
| 135 | - | |
| 136 | - // 6. 等待启动结果,设置超时 | |
| 83 | + // 启动服务器 | |
| 137 | 84 | try { |
| 138 | - if (!latch.await(10, TimeUnit.SECONDS)) { | |
| 139 | - throw new RuntimeException("TCP 服务器启动超时"); | |
| 140 | - } | |
| 141 | - if (failure.get() != null) { | |
| 142 | - throw new RuntimeException("TCP 服务器启动失败", failure.get()); | |
| 143 | - } | |
| 144 | - } catch (InterruptedException e) { | |
| 145 | - Thread.currentThread().interrupt(); | |
| 146 | - throw new RuntimeException("TCP 服务器启动被中断", e); | |
| 85 | + netServer.listen().result(); | |
| 86 | + log.info("[start][IoT 网关 TCP 协议启动成功,端口:{}]", tcpProperties.getPort()); | |
| 87 | + } catch (Exception e) { | |
| 88 | + log.error("[start][IoT 网关 TCP 协议启动失败]", e); | |
| 89 | + throw e; | |
| 147 | 90 | } |
| 148 | 91 | } |
| 149 | 92 | |
| 150 | - /** | |
| 151 | - * 停止 TCP 服务器 | |
| 152 | - */ | |
| 153 | - private void stopTcpServer() { | |
| 154 | - if (netServer == null) { | |
| 155 | - return; | |
| 156 | - } | |
| 157 | - log.info("[stopTcpServer][准备关闭 TCP 服务器]"); | |
| 158 | - CountDownLatch latch = new CountDownLatch(1); | |
| 159 | - // 异步关闭,并使用 Latch 等待结果 | |
| 160 | - netServer.close(result -> { | |
| 161 | - if (result.succeeded()) { | |
| 162 | - log.info("[stopTcpServer][IoT 网关 TCP 协议处理器已停止]"); | |
| 163 | - } else { | |
| 164 | - log.warn("[stopTcpServer][TCP 服务器关闭失败]", result.cause()); | |
| 165 | - } | |
| 166 | - latch.countDown(); | |
| 167 | - }); | |
| 168 | - | |
| 169 | - try { | |
| 170 | - // 等待关闭完成,设置超时 | |
| 171 | - if (!latch.await(10, TimeUnit.SECONDS)) { | |
| 172 | - log.warn("[stopTcpServer][关闭 TCP 服务器超时]"); | |
| 93 | + @PreDestroy | |
| 94 | + public void stop() { | |
| 95 | + if (netServer != null) { | |
| 96 | + try { | |
| 97 | + netServer.close().result(); | |
| 98 | + log.info("[stop][IoT 网关 TCP 协议已停止]"); | |
| 99 | + } catch (Exception e) { | |
| 100 | + log.error("[stop][IoT 网关 TCP 协议停止失败]", e); | |
| 173 | 101 | } |
| 174 | - } catch (InterruptedException e) { | |
| 175 | - Thread.currentThread().interrupt(); | |
| 176 | - log.warn("[stopTcpServer][等待 TCP 服务器关闭被中断]", e); | |
| 177 | 102 | } |
| 178 | 103 | } |
| 179 | 104 | } |
| 180 | 105 | \ 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 deleted
| 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 | - * 该类中的状态变更(如 authenticated, closed)使用 AtomicBoolean 保证原子性。 | |
| 16 | - * 对 socket 的操作应在 Vert.x Event Loop 线程中执行,以避免并发问题。 | |
| 17 | - * | |
| 18 | - * @author 芋道源码 | |
| 19 | - */ | |
| 20 | -@Slf4j | |
| 21 | -public class TcpDeviceClient { | |
| 22 | - | |
| 23 | - @Getter | |
| 24 | - private final String clientId; | |
| 25 | - | |
| 26 | - @Getter | |
| 27 | - @Setter | |
| 28 | - private String deviceAddr; // 从 final 移除,因为在注册后才设置 | |
| 29 | - | |
| 30 | - @Getter | |
| 31 | - @Setter | |
| 32 | - private String productKey; | |
| 33 | - | |
| 34 | - @Getter | |
| 35 | - @Setter | |
| 36 | - private String deviceName; | |
| 37 | - | |
| 38 | - @Getter | |
| 39 | - @Setter | |
| 40 | - private Long deviceId; | |
| 41 | - | |
| 42 | - @Getter | |
| 43 | - private NetSocket socket; | |
| 44 | - | |
| 45 | - @Getter | |
| 46 | - @Setter | |
| 47 | - private RecordParser parser; | |
| 48 | - | |
| 49 | - @Getter | |
| 50 | - private final long keepAliveTimeoutMs; | |
| 51 | - | |
| 52 | - private volatile long lastKeepAliveTime; | |
| 53 | - | |
| 54 | - private final AtomicBoolean authenticated = new AtomicBoolean(false); | |
| 55 | - private final AtomicBoolean closed = new AtomicBoolean(false); | |
| 56 | - | |
| 57 | - /** | |
| 58 | - * 构造函数 | |
| 59 | - * | |
| 60 | - * @param clientId 客户端 ID,全局唯一 | |
| 61 | - * @param keepAliveTimeoutMs 心跳超时时间(毫秒),从配置中读取 | |
| 62 | - */ | |
| 63 | - public TcpDeviceClient(String clientId, long keepAliveTimeoutMs) { | |
| 64 | - this.clientId = clientId; | |
| 65 | - this.keepAliveTimeoutMs = keepAliveTimeoutMs; | |
| 66 | - this.lastKeepAliveTime = System.currentTimeMillis(); | |
| 67 | - } | |
| 68 | - | |
| 69 | - /** | |
| 70 | - * 绑定网络套接字,并设置相关处理器。 | |
| 71 | - * 此方法应在 Vert.x Event Loop 线程中调用 | |
| 72 | - * | |
| 73 | - * @param socket 网络套接字 | |
| 74 | - */ | |
| 75 | - public void setSocket(NetSocket socket) { | |
| 76 | - // 无需 synchronized,Vert.x 保证了同一个 socket 的事件在同一个 Event Loop 中处理 | |
| 77 | - if (this.socket != null && this.socket != socket) { | |
| 78 | - log.warn("[setSocket][客户端({}) 正在用新的 socket 替换旧的,旧 socket 将被关闭]", clientId); | |
| 79 | - this.socket.close(); | |
| 80 | - } | |
| 81 | - this.socket = socket; | |
| 82 | - | |
| 83 | - // 注册处理器 | |
| 84 | - if (socket != null) { | |
| 85 | - // 1. 设置关闭处理器 | |
| 86 | - socket.closeHandler(v -> { | |
| 87 | - log.info("[setSocket][设备客户端({})的连接已由远端关闭]", clientId); | |
| 88 | - shutdown(); // 统一调用 shutdown 进行资源清理 | |
| 89 | - }); | |
| 90 | - | |
| 91 | - // 2. 设置异常处理器 | |
| 92 | - socket.exceptionHandler(e -> { | |
| 93 | - log.error("[setSocket][设备客户端({})连接出现异常]", clientId, e); | |
| 94 | - shutdown(); // 出现异常时也关闭连接 | |
| 95 | - }); | |
| 96 | - | |
| 97 | - // 3. 设置数据处理器 | |
| 98 | - socket.handler(buffer -> { | |
| 99 | - // 任何数据往来都表示连接是活跃的 | |
| 100 | - keepAlive(); | |
| 101 | - | |
| 102 | - if (parser != null) { | |
| 103 | - parser.handle(buffer); | |
| 104 | - } else { | |
| 105 | - log.warn("[setSocket][设备客户端({}) 未设置解析器(parser),原始数据被忽略: {}]", clientId, buffer.toString()); | |
| 106 | - } | |
| 107 | - }); | |
| 108 | - } | |
| 109 | - } | |
| 110 | - | |
| 111 | - /** | |
| 112 | - * 更新心跳时间,表示设备仍然活跃 | |
| 113 | - */ | |
| 114 | - public void keepAlive() { | |
| 115 | - this.lastKeepAliveTime = System.currentTimeMillis(); | |
| 116 | - } | |
| 117 | - | |
| 118 | - /** | |
| 119 | - * 检查连接是否在线 | |
| 120 | - * 判断标准:未被主动关闭、socket 存在、且在心跳超时时间内 | |
| 121 | - * | |
| 122 | - * @return 是否在线 | |
| 123 | - */ | |
| 124 | - public boolean isOnline() { | |
| 125 | - if (closed.get() || socket == null) { | |
| 126 | - return false; | |
| 127 | - } | |
| 128 | - long idleTime = System.currentTimeMillis() - lastKeepAliveTime; | |
| 129 | - return idleTime < keepAliveTimeoutMs; | |
| 130 | - } | |
| 131 | - | |
| 132 | - // TODO @haohao:1)是不是简化下:productKey 和 deviceName 非空,就认为是已认证;2)如果是的话,productKey 和 deviceName 搞成一个设置方法?setAuthenticated(productKey、deviceName) | |
| 133 | - | |
| 134 | - public boolean isAuthenticated() { | |
| 135 | - return authenticated.get(); | |
| 136 | - } | |
| 137 | - | |
| 138 | - public void setAuthenticated(boolean authenticated) { | |
| 139 | - this.authenticated.set(authenticated); | |
| 140 | - } | |
| 141 | - | |
| 142 | - /** | |
| 143 | - * 向设备发送消息 | |
| 144 | - * | |
| 145 | - * @param buffer 消息内容 | |
| 146 | - */ | |
| 147 | - public void sendMessage(Buffer buffer) { | |
| 148 | - if (closed.get() || socket == null) { | |
| 149 | - log.warn("[sendMessage][设备客户端({})连接已关闭,无法发送消息]", clientId); | |
| 150 | - return; | |
| 151 | - } | |
| 152 | - | |
| 153 | - // Vert.x 的 write 是异步的,不会阻塞 | |
| 154 | - socket.write(buffer, result -> { | |
| 155 | - // 发送失败可能意味着连接已断开,主动关闭 | |
| 156 | - if (!result.succeeded()) { | |
| 157 | - log.error("[sendMessage][设备客户端({})发送消息失败]", clientId, result.cause()); | |
| 158 | - shutdown(); | |
| 159 | - return; | |
| 160 | - } | |
| 161 | - | |
| 162 | - // 发送成功也更新心跳,表示连接活跃 | |
| 163 | - if (log.isDebugEnabled()) { | |
| 164 | - log.debug("[sendMessage][设备客户端({})发送消息成功]", clientId); | |
| 165 | - } | |
| 166 | - keepAlive(); | |
| 167 | - }); | |
| 168 | - } | |
| 169 | - | |
| 170 | - // TODO @haohao:是不是叫 close 好点?或者问问大模型 | |
| 171 | - /** | |
| 172 | - * 关闭客户端连接并清理资源。 | |
| 173 | - * 这是一个幂等操作,可以被多次安全调用。 | |
| 174 | - */ | |
| 175 | - public void shutdown() { | |
| 176 | - // 使用原子操作保证只执行一次关闭逻辑 | |
| 177 | - if (closed.getAndSet(true)) { | |
| 178 | - return; | |
| 179 | - } | |
| 180 | - | |
| 181 | - log.info("[shutdown][正在关闭设备客户端连接: {}]", clientId); | |
| 182 | - | |
| 183 | - // 先将 socket 引用置空,再关闭,避免并发问题 | |
| 184 | - NetSocket socketToClose = this.socket; | |
| 185 | - this.socket = null; | |
| 186 | - | |
| 187 | - if (socketToClose != null) { | |
| 188 | - try { | |
| 189 | - // close 是异步的,但我们在这里不关心其结果,因为我们已经将客户端标记为关闭 | |
| 190 | - socketToClose.close(); | |
| 191 | - } catch (Exception e) { | |
| 192 | - log.warn("[shutdown][关闭TCP连接时出现异常,可能已被关闭]", e); | |
| 193 | - } | |
| 194 | - } | |
| 195 | - | |
| 196 | - // 重置认证状态 | |
| 197 | - authenticated.set(false); | |
| 198 | - } | |
| 199 | - | |
| 200 | - public String getConnectionInfo() { | |
| 201 | - NetSocket currentSocket = this.socket; | |
| 202 | - if (currentSocket != null && currentSocket.remoteAddress() != null) { | |
| 203 | - return currentSocket.remoteAddress().toString(); | |
| 204 | - } | |
| 205 | - return "disconnected"; | |
| 206 | - } | |
| 207 | - | |
| 208 | - @Override | |
| 209 | - public String toString() { | |
| 210 | - return "TcpDeviceClient{" + | |
| 211 | - "clientId='" + clientId + '\'' + | |
| 212 | - ", deviceAddr='" + deviceAddr + '\'' + | |
| 213 | - ", deviceId=" + deviceId + | |
| 214 | - ", authenticated=" + authenticated.get() + | |
| 215 | - ", online=" + isOnline() + | |
| 216 | - ", connection=" + getConnectionInfo() + | |
| 217 | - '}'; | |
| 218 | - } | |
| 219 | - | |
| 220 | -} | |
| 221 | 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/manager/TcpDeviceConnectionManager.java deleted
| 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. 流量控制 TODO @haohao:这个要不先去掉 | |
| 20 | - * 4. 监控统计 TODO @haohao:这个要不先去掉 | |
| 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 | - // TODO @haohao:这个要不去掉;目前看着没做 result 的处理; | |
| 110 | - if (clientMap.size() >= MAX_CONNECTIONS) { | |
| 111 | - log.warn("[addClient][连接数已达上限({}),拒绝新连接: {}]", MAX_CONNECTIONS, deviceAddr); | |
| 112 | - return false; | |
| 113 | - } | |
| 114 | - | |
| 115 | - writeLock.lock(); | |
| 116 | - try { | |
| 117 | - log.info("[addClient][添加设备客户端: {}]", deviceAddr); | |
| 118 | - | |
| 119 | - // 关闭之前的连接(如果存在) | |
| 120 | - TcpDeviceClient existingClient = clientMap.get(deviceAddr); | |
| 121 | - if (existingClient != null) { | |
| 122 | - log.warn("[addClient][设备({})已存在连接,关闭旧连接]", deviceAddr); | |
| 123 | - removeClientInternal(deviceAddr, existingClient); | |
| 124 | - } | |
| 125 | - | |
| 126 | - // 添加新连接 | |
| 127 | - clientMap.put(deviceAddr, client); | |
| 128 | - | |
| 129 | - // 添加套接字映射 | |
| 130 | - if (client.getSocket() != null) { | |
| 131 | - socketToAddrMap.put(client.getSocket(), deviceAddr); | |
| 132 | - } | |
| 133 | - | |
| 134 | - // 如果客户端已设置设备 ID,更新映射 | |
| 135 | - if (client.getDeviceId() != null) { | |
| 136 | - deviceIdToAddrMap.put(client.getDeviceId(), deviceAddr); | |
| 137 | - } | |
| 138 | - | |
| 139 | - totalConnections.incrementAndGet(); | |
| 140 | - return true; | |
| 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 | - // TODO @haohao:getClientBySocket、isDeviceOnline、sendMessage、sendMessageByDeviceId、broadcastMessage 用不到的方法,要不先暂时不提供?保持简洁、更容易理解哈。 | |
| 212 | - | |
| 213 | - /** | |
| 214 | - * 通过网络连接获取客户端 | |
| 215 | - */ | |
| 216 | - public TcpDeviceClient getClientBySocket(NetSocket socket) { | |
| 217 | - readLock.lock(); | |
| 218 | - try { | |
| 219 | - String deviceAddr = socketToAddrMap.get(socket); | |
| 220 | - return deviceAddr != null ? clientMap.get(deviceAddr) : null; | |
| 221 | - } finally { | |
| 222 | - readLock.unlock(); | |
| 223 | - } | |
| 224 | - } | |
| 225 | - | |
| 226 | - /** | |
| 227 | - * 检查设备是否在线 | |
| 228 | - */ | |
| 229 | - public boolean isDeviceOnline(Long deviceId) { | |
| 230 | - TcpDeviceClient client = getClientByDeviceId(deviceId); | |
| 231 | - return client != null && client.isOnline(); | |
| 232 | - } | |
| 233 | - | |
| 234 | - /** | |
| 235 | - * 设置设备 ID 映射 | |
| 236 | - */ | |
| 237 | - public void setDeviceIdMapping(String deviceAddr, Long deviceId) { | |
| 238 | - writeLock.lock(); | |
| 239 | - try { | |
| 240 | - TcpDeviceClient client = clientMap.get(deviceAddr); | |
| 241 | - if (client != null) { | |
| 242 | - client.setDeviceId(deviceId); | |
| 243 | - deviceIdToAddrMap.put(deviceId, deviceAddr); | |
| 244 | - log.debug("[setDeviceIdMapping][设置设备ID映射: {} -> {}]", deviceAddr, deviceId); | |
| 245 | - } | |
| 246 | - } finally { | |
| 247 | - writeLock.unlock(); | |
| 248 | - } | |
| 249 | - } | |
| 250 | - | |
| 251 | - /** | |
| 252 | - * 发送消息给设备 | |
| 253 | - */ | |
| 254 | - public boolean sendMessage(String deviceAddr, Buffer buffer) { | |
| 255 | - TcpDeviceClient client = getClient(deviceAddr); | |
| 256 | - if (client != null && client.isOnline()) { | |
| 257 | - try { | |
| 258 | - client.sendMessage(buffer); | |
| 259 | - totalMessages.incrementAndGet(); | |
| 260 | - totalBytes.addAndGet(buffer.length()); | |
| 261 | - return true; | |
| 262 | - } catch (Exception e) { | |
| 263 | - totalFailedMessages.incrementAndGet(); | |
| 264 | - log.error("[sendMessage][发送消息失败] 设备地址: {}", deviceAddr, e); | |
| 265 | - return false; | |
| 266 | - } | |
| 267 | - } | |
| 268 | - log.warn("[sendMessage][设备({})不在线,无法发送消息]", deviceAddr); | |
| 269 | - return false; | |
| 270 | - } | |
| 271 | - | |
| 272 | - /** | |
| 273 | - * 通过设备ID发送消息 | |
| 274 | - */ | |
| 275 | - public boolean sendMessageByDeviceId(Long deviceId, Buffer buffer) { | |
| 276 | - TcpDeviceClient client = getClientByDeviceId(deviceId); | |
| 277 | - if (client != null && client.isOnline()) { | |
| 278 | - try { | |
| 279 | - client.sendMessage(buffer); | |
| 280 | - totalMessages.incrementAndGet(); | |
| 281 | - totalBytes.addAndGet(buffer.length()); | |
| 282 | - return true; | |
| 283 | - } catch (Exception e) { | |
| 284 | - totalFailedMessages.incrementAndGet(); | |
| 285 | - log.error("[sendMessageByDeviceId][发送消息失败] 设备ID: {}", deviceId, e); | |
| 286 | - return false; | |
| 287 | - } | |
| 288 | - } | |
| 289 | - log.warn("[sendMessageByDeviceId][设备ID({})不在线,无法发送消息]", deviceId); | |
| 290 | - return false; | |
| 291 | - } | |
| 292 | - | |
| 293 | - /** | |
| 294 | - * 广播消息给所有在线设备 | |
| 295 | - */ | |
| 296 | - public int broadcastMessage(Buffer buffer) { | |
| 297 | - int successCount = 0; | |
| 298 | - readLock.lock(); | |
| 299 | - try { | |
| 300 | - for (TcpDeviceClient client : clientMap.values()) { | |
| 301 | - if (client.isOnline()) { | |
| 302 | - try { | |
| 303 | - client.sendMessage(buffer); | |
| 304 | - successCount++; | |
| 305 | - } catch (Exception e) { | |
| 306 | - log.error("[broadcastMessage][广播消息失败] 设备: {}", client.getDeviceAddr(), e); | |
| 307 | - } | |
| 308 | - } | |
| 309 | - } | |
| 310 | - } finally { | |
| 311 | - readLock.unlock(); | |
| 312 | - } | |
| 313 | - | |
| 314 | - totalMessages.addAndGet(successCount); | |
| 315 | - totalBytes.addAndGet((long) successCount * buffer.length()); | |
| 316 | - return successCount; | |
| 317 | - } | |
| 318 | - | |
| 319 | - /** | |
| 320 | - * 获取在线设备数量 | |
| 321 | - */ | |
| 322 | - public int getOnlineCount() { | |
| 323 | - readLock.lock(); | |
| 324 | - try { | |
| 325 | - return (int) clientMap.values().stream() | |
| 326 | - .filter(TcpDeviceClient::isOnline) | |
| 327 | - .count(); | |
| 328 | - } finally { | |
| 329 | - readLock.unlock(); | |
| 330 | - } | |
| 331 | - } | |
| 332 | - | |
| 333 | - /** | |
| 334 | - * 获取总连接数 | |
| 335 | - */ | |
| 336 | - public int getTotalCount() { | |
| 337 | - return clientMap.size(); | |
| 338 | - } | |
| 339 | - | |
| 340 | - /** | |
| 341 | - * 获取认证设备数量 | |
| 342 | - */ | |
| 343 | - public int getAuthenticatedCount() { | |
| 344 | - readLock.lock(); | |
| 345 | - try { | |
| 346 | - return (int) clientMap.values().stream() | |
| 347 | - .filter(TcpDeviceClient::isAuthenticated) | |
| 348 | - .count(); | |
| 349 | - } finally { | |
| 350 | - readLock.unlock(); | |
| 351 | - } | |
| 352 | - } | |
| 353 | - | |
| 354 | - // TODO @haohao:心跳超时,需要 close 么? | |
| 355 | - /** | |
| 356 | - * 心跳检查任务 | |
| 357 | - */ | |
| 358 | - private void checkHeartbeat() { | |
| 359 | - try { | |
| 360 | - int offlineCount = 0; | |
| 361 | - | |
| 362 | - readLock.lock(); | |
| 363 | - try { | |
| 364 | - for (TcpDeviceClient client : clientMap.values()) { | |
| 365 | - if (!client.isOnline()) { | |
| 366 | - offlineCount++; | |
| 367 | - } | |
| 368 | - } | |
| 369 | - } finally { | |
| 370 | - readLock.unlock(); | |
| 371 | - } | |
| 372 | - | |
| 373 | - if (offlineCount > 0) { | |
| 374 | - log.info("[checkHeartbeat][发现 {} 个离线设备,将在清理任务中处理]", offlineCount); | |
| 375 | - } | |
| 376 | - } catch (Exception e) { | |
| 377 | - log.error("[checkHeartbeat][心跳检查任务异常]", e); | |
| 378 | - } | |
| 379 | - } | |
| 380 | - | |
| 381 | - /** | |
| 382 | - * 连接清理任务 | |
| 383 | - */ | |
| 384 | - private void cleanupConnections() { | |
| 385 | - try { | |
| 386 | - int beforeSize = clientMap.size(); | |
| 387 | - | |
| 388 | - writeLock.lock(); | |
| 389 | - try { | |
| 390 | - clientMap.entrySet().removeIf(entry -> { | |
| 391 | - TcpDeviceClient client = entry.getValue(); | |
| 392 | - if (!client.isOnline()) { | |
| 393 | - log.debug("[cleanupConnections][清理离线连接: {}]", entry.getKey()); | |
| 394 | - | |
| 395 | - // 清理相关映射 | |
| 396 | - if (client.getSocket() != null) { | |
| 397 | - socketToAddrMap.remove(client.getSocket()); | |
| 398 | - } | |
| 399 | - if (client.getDeviceId() != null) { | |
| 400 | - deviceIdToAddrMap.remove(client.getDeviceId()); | |
| 401 | - } | |
| 402 | - | |
| 403 | - client.shutdown(); | |
| 404 | - totalDisconnections.incrementAndGet(); | |
| 405 | - return true; | |
| 406 | - } | |
| 407 | - return false; | |
| 408 | - }); | |
| 409 | - } finally { | |
| 410 | - writeLock.unlock(); | |
| 411 | - } | |
| 412 | - | |
| 413 | - int afterSize = clientMap.size(); | |
| 414 | - if (beforeSize != afterSize) { | |
| 415 | - log.info("[cleanupConnections][清理完成] 连接数: {} -> {}, 清理数: {}", | |
| 416 | - beforeSize, afterSize, beforeSize - afterSize); | |
| 417 | - } | |
| 418 | - } catch (Exception e) { | |
| 419 | - log.error("[cleanupConnections][连接清理任务异常]", e); | |
| 420 | - } | |
| 421 | - } | |
| 422 | - | |
| 423 | - /** | |
| 424 | - * 统计日志任务 | |
| 425 | - */ | |
| 426 | - private void logStatistics() { | |
| 427 | - try { | |
| 428 | - long totalConn = totalConnections.get(); | |
| 429 | - long totalDisconnections = this.totalDisconnections.get(); | |
| 430 | - long totalMsg = totalMessages.get(); | |
| 431 | - long totalFailedMsg = totalFailedMessages.get(); | |
| 432 | - long totalBytesValue = totalBytes.get(); | |
| 433 | - | |
| 434 | - log.info("[logStatistics][连接统计] 总连接: {}, 总断开: {}, 当前在线: {}, 认证设备: {}, " + | |
| 435 | - "总消息: {}, 失败消息: {}, 总字节: {}", | |
| 436 | - totalConn, totalDisconnections, getOnlineCount(), getAuthenticatedCount(), | |
| 437 | - totalMsg, totalFailedMsg, totalBytesValue); | |
| 438 | - } catch (Exception e) { | |
| 439 | - log.error("[logStatistics][统计日志任务异常]", e); | |
| 440 | - } | |
| 441 | - } | |
| 442 | - | |
| 443 | - /** | |
| 444 | - * 关闭连接管理器 | |
| 445 | - */ | |
| 446 | - public void shutdown() { | |
| 447 | - log.info("[shutdown][关闭TCP连接管理器]"); | |
| 448 | - | |
| 449 | - // 关闭定时任务 | |
| 450 | - scheduler.shutdown(); | |
| 451 | - try { | |
| 452 | - if (!scheduler.awaitTermination(10, TimeUnit.SECONDS)) { | |
| 453 | - scheduler.shutdownNow(); | |
| 454 | - } | |
| 455 | - } catch (InterruptedException e) { | |
| 456 | - scheduler.shutdownNow(); | |
| 457 | - Thread.currentThread().interrupt(); | |
| 458 | - } | |
| 459 | - | |
| 460 | - // 关闭所有连接 | |
| 461 | - writeLock.lock(); | |
| 462 | - try { | |
| 463 | - clientMap.values().forEach(TcpDeviceClient::shutdown); | |
| 464 | - clientMap.clear(); | |
| 465 | - deviceIdToAddrMap.clear(); | |
| 466 | - socketToAddrMap.clear(); | |
| 467 | - } finally { | |
| 468 | - writeLock.unlock(); | |
| 469 | - } | |
| 470 | - } | |
| 471 | - | |
| 472 | - /** | |
| 473 | - * 获取连接状态信息 | |
| 474 | - */ | |
| 475 | - public String getConnectionStatus() { | |
| 476 | - return String.format("总连接数: %d, 在线设备: %d, 认证设备: %d, 成功率: %.2f%%", | |
| 477 | - getTotalCount(), getOnlineCount(), getAuthenticatedCount(), | |
| 478 | - totalMessages.get() > 0 | |
| 479 | - ? (double) (totalMessages.get() - totalFailedMessages.get()) / totalMessages.get() * 100 | |
| 480 | - : 0.0); | |
| 481 | - } | |
| 482 | - | |
| 483 | - /** | |
| 484 | - * 获取详细统计信息 | |
| 485 | - */ | |
| 486 | - public String getDetailedStatistics() { | |
| 487 | - return String.format( | |
| 488 | - "TCP连接管理器统计:\n" + | |
| 489 | - "- 当前连接数: %d\n" + | |
| 490 | - "- 在线设备数: %d\n" + | |
| 491 | - "- 认证设备数: %d\n" + | |
| 492 | - "- 历史总连接: %d\n" + | |
| 493 | - "- 历史总断开: %d\n" + | |
| 494 | - "- 总消息数: %d\n" + | |
| 495 | - "- 失败消息数: %d\n" + | |
| 496 | - "- 总字节数: %d\n" + | |
| 497 | - "- 消息成功率: %.2f%%", | |
| 498 | - getTotalCount(), getOnlineCount(), getAuthenticatedCount(), | |
| 499 | - totalConnections.get(), totalDisconnections.get(), | |
| 500 | - totalMessages.get(), totalFailedMessages.get(), totalBytes.get(), | |
| 501 | - totalMessages.get() > 0 | |
| 502 | - ? (double) (totalMessages.get() - totalFailedMessages.get()) / totalMessages.get() * 100 | |
| 503 | - : 0.0); | |
| 504 | - } | |
| 505 | - | |
| 506 | -} | |
| 507 | 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/protocol/TcpDataDecoder.java deleted
| 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 | -// TODO @haohao:“设备地址长度”是不是不需要。 | |
| 7 | -/** | |
| 8 | - * TCP 数据解码器 | |
| 9 | - * <p> | |
| 10 | - * 负责将字节流解码为 TcpDataPackage 对象 | |
| 11 | - * <p> | |
| 12 | - * 数据包格式: | |
| 13 | - * 包头(4 字节长度) | 设备地址长度(2 字节) | 设备地址(不定长) | 功能码(2 字节) | 消息序号(2 字节) | 包体(不定长) | |
| 14 | - * | |
| 15 | - * @author 芋道源码 | |
| 16 | - */ | |
| 17 | -@Slf4j | |
| 18 | -public class TcpDataDecoder { | |
| 19 | - | |
| 20 | - /** | |
| 21 | - * 解码数据包 | |
| 22 | - * | |
| 23 | - * @param buffer 数据缓冲区 | |
| 24 | - * @return 解码后的数据包 | |
| 25 | - * @throws IllegalArgumentException 如果数据包格式不正确 | |
| 26 | - */ | |
| 27 | - public static TcpDataPackage decode(Buffer buffer) { | |
| 28 | - if (buffer == null || buffer.length() < 8) { | |
| 29 | - throw new IllegalArgumentException("数据包长度不足"); | |
| 30 | - } | |
| 31 | - | |
| 32 | - try { | |
| 33 | - int index = 0; | |
| 34 | - | |
| 35 | - // 1.1 获取设备地址长度(2字节) | |
| 36 | - short addrLength = buffer.getShort(index); | |
| 37 | - index += 2; | |
| 38 | - | |
| 39 | - // 1.2 校验数据包长度 | |
| 40 | - int expectedLength = 2 + addrLength + 2 + 2; // 地址长度 + 地址 + 功能码 + 消息序号 | |
| 41 | - if (buffer.length() < expectedLength) { | |
| 42 | - throw new IllegalArgumentException("数据包长度不足,期望至少 " + expectedLength + " 字节"); | |
| 43 | - } | |
| 44 | - | |
| 45 | - // 1.3 获取设备地址 | |
| 46 | - String addr = buffer.getBuffer(index, index + addrLength).toString(); | |
| 47 | - index += addrLength; | |
| 48 | - | |
| 49 | - // 1.4 获取功能码(2字节) | |
| 50 | - short code = buffer.getShort(index); | |
| 51 | - index += 2; | |
| 52 | - | |
| 53 | - // 1.5 获取消息序号(2字节) | |
| 54 | - short mid = buffer.getShort(index); | |
| 55 | - index += 2; | |
| 56 | - | |
| 57 | - // 1.6 获取包体数据 | |
| 58 | - String payload = ""; | |
| 59 | - if (index < buffer.length()) { | |
| 60 | - payload = buffer.getString(index, buffer.length()); | |
| 61 | - } | |
| 62 | - | |
| 63 | - // 2. 构建数据包对象 | |
| 64 | - TcpDataPackage dataPackage = TcpDataPackage.builder() | |
| 65 | - .addrLength((int) addrLength) | |
| 66 | - .addr(addr) | |
| 67 | - .code(code) | |
| 68 | - .mid(mid) | |
| 69 | - .payload(payload) | |
| 70 | - .build(); | |
| 71 | - | |
| 72 | - log.debug("[decode][解码成功] 设备地址: {}, 功能码: {}, 消息序号: {}, 包体长度: {}", | |
| 73 | - addr, dataPackage.getCodeDescription(), mid, payload.length()); | |
| 74 | - return dataPackage; | |
| 75 | - } catch (Exception e) { | |
| 76 | - log.error("[decode][解码失败] 数据: {}", buffer.toString(), e); | |
| 77 | - throw new IllegalArgumentException("数据包解码失败: " + e.getMessage(), e); | |
| 78 | - } | |
| 79 | - } | |
| 80 | - | |
| 81 | - // TODO @haohao:这个要不去掉,暂时没用到; | |
| 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 | - | |
| 98 | -} | |
| 99 | 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/protocol/TcpDataEncoder.java deleted
| 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 | - if (dataPackage.getAddr() == null || dataPackage.getAddr().isEmpty()) { | |
| 31 | - throw new IllegalArgumentException("设备地址不能为空"); | |
| 32 | - } | |
| 33 | - if (dataPackage.getPayload() == null) { | |
| 34 | - dataPackage.setPayload(""); | |
| 35 | - } | |
| 36 | - | |
| 37 | - try { | |
| 38 | - Buffer buffer = Buffer.buffer(); | |
| 39 | - | |
| 40 | - // 1. 计算包体长度(除了包头 4 字节) | |
| 41 | - int payloadLength = dataPackage.getPayload().getBytes().length; | |
| 42 | - int totalLength = 2 + dataPackage.getAddr().length() + 2 + 2 + payloadLength; | |
| 43 | - | |
| 44 | - // 2.1 写入包头:总长度(4 字节) | |
| 45 | - buffer.appendInt(totalLength); | |
| 46 | - // 2.2 写入设备地址长度(2 字节) | |
| 47 | - buffer.appendShort((short) dataPackage.getAddr().length()); | |
| 48 | - // 2.3 写入设备地址(不定长) | |
| 49 | - buffer.appendBytes(dataPackage.getAddr().getBytes()); | |
| 50 | - // 2.4 写入功能码(2 字节) | |
| 51 | - buffer.appendShort(dataPackage.getCode()); | |
| 52 | - // 2.5 写入消息序号(2 字节) | |
| 53 | - buffer.appendShort(dataPackage.getMid()); | |
| 54 | - // 2.6 写入包体数据(不定长) | |
| 55 | - buffer.appendBytes(dataPackage.getPayload().getBytes()); | |
| 56 | - | |
| 57 | - log.debug("[encode][编码成功] 设备地址: {}, 功能码: {}, 消息序号: {}, 总长度: {}", | |
| 58 | - dataPackage.getAddr(), dataPackage.getCodeDescription(), | |
| 59 | - dataPackage.getMid(), buffer.length()); | |
| 60 | - return buffer; | |
| 61 | - } catch (Exception e) { | |
| 62 | - log.error("[encode][编码失败] 数据包: {}", dataPackage, e); | |
| 63 | - throw new IllegalArgumentException("数据包编码失败: " + e.getMessage(), e); | |
| 64 | - } | |
| 65 | - } | |
| 66 | - | |
| 67 | - /** | |
| 68 | - * 创建注册回复数据包 | |
| 69 | - * | |
| 70 | - * @param addr 设备地址 | |
| 71 | - * @param mid 消息序号 | |
| 72 | - * @param success 是否成功 | |
| 73 | - * @return 编码后的数据包 | |
| 74 | - */ | |
| 75 | - public static Buffer createRegisterReply(String addr, short mid, boolean success) { | |
| 76 | - // TODO @haohao:payload 默认成功、失败,最好讴有个枚举 | |
| 77 | - String payload = success ? "0" : "1"; // 0 表示成功,1 表示失败 | |
| 78 | - TcpDataPackage dataPackage = TcpDataPackage.builder() | |
| 79 | - .addr(addr) | |
| 80 | - .code(TcpDataPackage.CODE_REGISTER_REPLY) | |
| 81 | - .mid(mid) | |
| 82 | - .payload(payload) | |
| 83 | - .build(); | |
| 84 | - return encode(dataPackage); | |
| 85 | - } | |
| 86 | - | |
| 87 | - /** | |
| 88 | - * 创建数据下发数据包 | |
| 89 | - * | |
| 90 | - * @param addr 设备地址 | |
| 91 | - * @param mid 消息序号 | |
| 92 | - * @param data 下发数据 | |
| 93 | - * @return 编码后的数据包 | |
| 94 | - */ | |
| 95 | - public static Buffer createDataDownPackage(String addr, short mid, String data) { | |
| 96 | - TcpDataPackage dataPackage = TcpDataPackage.builder() | |
| 97 | - .addr(addr) | |
| 98 | - .code(TcpDataPackage.CODE_DATA_DOWN) | |
| 99 | - .mid(mid) | |
| 100 | - .payload(data) | |
| 101 | - .build(); | |
| 102 | - return encode(dataPackage); | |
| 103 | - } | |
| 104 | - | |
| 105 | - /** | |
| 106 | - * 创建服务调用数据包 | |
| 107 | - * | |
| 108 | - * @param addr 设备地址 | |
| 109 | - * @param mid 消息序号 | |
| 110 | - * @param serviceData 服务数据 | |
| 111 | - * @return 编码后的数据包 | |
| 112 | - */ | |
| 113 | - public static Buffer createServiceInvokePackage(String addr, short mid, String serviceData) { | |
| 114 | - TcpDataPackage dataPackage = TcpDataPackage.builder() | |
| 115 | - .addr(addr) | |
| 116 | - .code(TcpDataPackage.CODE_SERVICE_INVOKE) | |
| 117 | - .mid(mid) | |
| 118 | - .payload(serviceData) | |
| 119 | - .build(); | |
| 120 | - return encode(dataPackage); | |
| 121 | - } | |
| 122 | - | |
| 123 | - /** | |
| 124 | - * 创建属性设置数据包 | |
| 125 | - * | |
| 126 | - * @param addr 设备地址 | |
| 127 | - * @param mid 消息序号 | |
| 128 | - * @param propertyData 属性数据 | |
| 129 | - * @return 编码后的数据包 | |
| 130 | - */ | |
| 131 | - public static Buffer createPropertySetPackage(String addr, short mid, String propertyData) { | |
| 132 | - TcpDataPackage dataPackage = TcpDataPackage.builder() | |
| 133 | - .addr(addr) | |
| 134 | - .code(TcpDataPackage.CODE_PROPERTY_SET) | |
| 135 | - .mid(mid) | |
| 136 | - .payload(propertyData) | |
| 137 | - .build(); | |
| 138 | - return encode(dataPackage); | |
| 139 | - } | |
| 140 | - | |
| 141 | - /** | |
| 142 | - * 创建属性获取数据包 | |
| 143 | - * | |
| 144 | - * @param addr 设备地址 | |
| 145 | - * @param mid 消息序号 | |
| 146 | - * @param propertyNames 属性名称列表 | |
| 147 | - * @return 编码后的数据包 | |
| 148 | - */ | |
| 149 | - public static Buffer createPropertyGetPackage(String addr, short mid, String propertyNames) { | |
| 150 | - TcpDataPackage dataPackage = TcpDataPackage.builder() | |
| 151 | - .addr(addr) | |
| 152 | - .code(TcpDataPackage.CODE_PROPERTY_GET) | |
| 153 | - .mid(mid) | |
| 154 | - .payload(propertyNames) | |
| 155 | - .build(); | |
| 156 | - return encode(dataPackage); | |
| 157 | - } | |
| 158 | - | |
| 159 | -} | |
| 160 | 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/protocol/TcpDataPackage.java deleted
| 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 | - // TODO @haohao:【重要】一般心跳,服务端会回复一条;回复要搞独立的 code 码,还是继续用原来的,因为 requestId 可以映射; | |
| 33 | - /** | |
| 34 | - * 心跳 | |
| 35 | - */ | |
| 36 | - public static final short CODE_HEARTBEAT = 20; | |
| 37 | - // TODO @haohao:【重要】下面的,是不是融合成消息上行(client -> server),消息下行(server -> client);然后把 method 放到 body 里? | |
| 38 | - /** | |
| 39 | - * 数据上报 | |
| 40 | - */ | |
| 41 | - public static final short CODE_DATA_UP = 30; | |
| 42 | - /** | |
| 43 | - * 事件上报 | |
| 44 | - */ | |
| 45 | - public static final short CODE_EVENT_UP = 40; | |
| 46 | - /** | |
| 47 | - * 数据下发 | |
| 48 | - */ | |
| 49 | - public static final short CODE_DATA_DOWN = 50; | |
| 50 | - /** | |
| 51 | - * 服务调用 | |
| 52 | - */ | |
| 53 | - public static final short CODE_SERVICE_INVOKE = 60; | |
| 54 | - /** | |
| 55 | - * 属性设置 | |
| 56 | - */ | |
| 57 | - public static final short CODE_PROPERTY_SET = 70; | |
| 58 | - /** | |
| 59 | - * 属性获取 | |
| 60 | - */ | |
| 61 | - public static final short CODE_PROPERTY_GET = 80; | |
| 62 | - | |
| 63 | - // ==================== 数据包字段 ==================== | |
| 64 | - | |
| 65 | - // TODO @haohao:设备 addrLength、addr 是不是非必要呀? | |
| 66 | - | |
| 67 | - /** | |
| 68 | - * 设备地址长度 | |
| 69 | - */ | |
| 70 | - private Integer addrLength; | |
| 71 | - | |
| 72 | - /** | |
| 73 | - * 设备地址 | |
| 74 | - */ | |
| 75 | - private String addr; | |
| 76 | - | |
| 77 | - /** | |
| 78 | - * 功能码 | |
| 79 | - */ | |
| 80 | - private short code; | |
| 81 | - | |
| 82 | - /** | |
| 83 | - * 消息序号 | |
| 84 | - */ | |
| 85 | - private short mid; | |
| 86 | - | |
| 87 | - /** | |
| 88 | - * 包体数据 | |
| 89 | - */ | |
| 90 | - private String payload; | |
| 91 | - | |
| 92 | - // ==================== 辅助方法 ==================== | |
| 93 | - | |
| 94 | - // TODO @haohao:用不到的方法,可以清理掉哈; | |
| 95 | - | |
| 96 | - /** | |
| 97 | - * 是否为注册消息 | |
| 98 | - */ | |
| 99 | - public boolean isRegisterMessage() { | |
| 100 | - return code == CODE_REGISTER; | |
| 101 | - } | |
| 102 | - | |
| 103 | - /** | |
| 104 | - * 是否为心跳消息 | |
| 105 | - */ | |
| 106 | - public boolean isHeartbeatMessage() { | |
| 107 | - return code == CODE_HEARTBEAT; | |
| 108 | - } | |
| 109 | - | |
| 110 | - /** | |
| 111 | - * 是否为数据上报消息 | |
| 112 | - */ | |
| 113 | - public boolean isDataUpMessage() { | |
| 114 | - return code == CODE_DATA_UP; | |
| 115 | - } | |
| 116 | - | |
| 117 | - /** | |
| 118 | - * 是否为事件上报消息 | |
| 119 | - */ | |
| 120 | - public boolean isEventUpMessage() { | |
| 121 | - return code == CODE_EVENT_UP; | |
| 122 | - } | |
| 123 | - | |
| 124 | - /** | |
| 125 | - * 是否为下行消息 | |
| 126 | - */ | |
| 127 | - public boolean isDownstreamMessage() { | |
| 128 | - return code == CODE_DATA_DOWN || code == CODE_SERVICE_INVOKE || | |
| 129 | - code == CODE_PROPERTY_SET || code == CODE_PROPERTY_GET; | |
| 130 | - } | |
| 131 | - | |
| 132 | - // TODO @haohao:这个是不是去掉呀?多了一些维护成本; | |
| 133 | - /** | |
| 134 | - * 获取功能码描述 | |
| 135 | - */ | |
| 136 | - public String getCodeDescription() { | |
| 137 | - switch (code) { | |
| 138 | - case CODE_REGISTER: | |
| 139 | - return "设备注册"; | |
| 140 | - case CODE_REGISTER_REPLY: | |
| 141 | - return "注册回复"; | |
| 142 | - case CODE_HEARTBEAT: | |
| 143 | - return "心跳"; | |
| 144 | - case CODE_DATA_UP: | |
| 145 | - return "数据上报"; | |
| 146 | - case CODE_EVENT_UP: | |
| 147 | - return "事件上报"; | |
| 148 | - case CODE_DATA_DOWN: | |
| 149 | - return "数据下发"; | |
| 150 | - case CODE_SERVICE_INVOKE: | |
| 151 | - return "服务调用"; | |
| 152 | - case CODE_PROPERTY_SET: | |
| 153 | - return "属性设置"; | |
| 154 | - case CODE_PROPERTY_GET: | |
| 155 | - return "属性获取"; | |
| 156 | - default: | |
| 157 | - return "未知功能码"; | |
| 158 | - } | |
| 159 | - } | |
| 160 | -} | |
| 161 | 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/protocol/TcpDataReader.java deleted
| 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 | - // 校验数据长度(最大 1 MB) | |
| 47 | - // TODO @haohao:1m 蛮多地方在写死,最好配置管理下。或者有个全局的枚举; | |
| 48 | - if (dataLength <= 0 || dataLength > 1024 * 1024) { | |
| 49 | - log.error("[handle][无效的数据包长度: {}]", dataLength); | |
| 50 | - reset(); | |
| 51 | - return; | |
| 52 | - } | |
| 53 | - | |
| 54 | - // 切换到读取数据模式 | |
| 55 | - parser.fixedSizeMode(dataLength); | |
| 56 | - | |
| 57 | - log.debug("[handle][读取到数据包长度: {}]", dataLength); | |
| 58 | - } else { | |
| 59 | - // 读取到完整的数据包 | |
| 60 | - log.debug("[handle][读取到完整数据包,长度: {}]", buffer.length()); | |
| 61 | - | |
| 62 | - // 处理数据包 | |
| 63 | - try { | |
| 64 | - receiveHandler.accept(buffer); | |
| 65 | - } catch (Exception e) { | |
| 66 | - log.error("[handle][处理数据包失败]", e); | |
| 67 | - } | |
| 68 | - | |
| 69 | - // 重置状态,准备读取下一个数据包 | |
| 70 | - reset(); | |
| 71 | - } | |
| 72 | - } catch (Exception e) { | |
| 73 | - log.error("[handle][数据包处理异常]", e); | |
| 74 | - reset(); | |
| 75 | - } | |
| 76 | - } | |
| 77 | - | |
| 78 | - /** | |
| 79 | - * 重置解析器状态 | |
| 80 | - */ | |
| 81 | - private void reset() { | |
| 82 | - dataLength = -1; | |
| 83 | - parser.fixedSizeMode(4); | |
| 84 | - } | |
| 85 | - }); | |
| 86 | - | |
| 87 | - return parser; | |
| 88 | - } | |
| 89 | - | |
| 90 | - // TODO @haohao:用不到的方法,可以清理掉哈; | |
| 91 | - | |
| 92 | - /** | |
| 93 | - * 创建带异常处理的数据包解析器 | |
| 94 | - * | |
| 95 | - * @param receiveHandler 接收处理器 | |
| 96 | - * @param exceptionHandler 异常处理器 | |
| 97 | - * @return RecordParser 解析器 | |
| 98 | - */ | |
| 99 | - public static RecordParser createParserWithExceptionHandler( | |
| 100 | - Consumer<Buffer> receiveHandler, | |
| 101 | - Consumer<Throwable> exceptionHandler) { | |
| 102 | - | |
| 103 | - RecordParser parser = RecordParser.newFixed(4); | |
| 104 | - | |
| 105 | - parser.setOutput(new Handler<Buffer>() { | |
| 106 | - private int dataLength = -1; | |
| 107 | - | |
| 108 | - @Override | |
| 109 | - public void handle(Buffer buffer) { | |
| 110 | - try { | |
| 111 | - if (dataLength == -1) { | |
| 112 | - dataLength = buffer.getInt(0); | |
| 113 | - | |
| 114 | - if (dataLength <= 0 || dataLength > 1024 * 1024) { | |
| 115 | - throw new IllegalArgumentException("无效的数据包长度: " + dataLength); | |
| 116 | - } | |
| 117 | - | |
| 118 | - parser.fixedSizeMode(dataLength); | |
| 119 | - log.debug("[handle][读取到数据包长度: {}]", dataLength); | |
| 120 | - } else { | |
| 121 | - log.debug("[handle][读取到完整数据包,长度: {}]", buffer.length()); | |
| 122 | - | |
| 123 | - try { | |
| 124 | - receiveHandler.accept(buffer); | |
| 125 | - } catch (Exception e) { | |
| 126 | - exceptionHandler.accept(e); | |
| 127 | - } | |
| 128 | - | |
| 129 | - reset(); | |
| 130 | - } | |
| 131 | - } catch (Exception e) { | |
| 132 | - exceptionHandler.accept(e); | |
| 133 | - reset(); | |
| 134 | - } | |
| 135 | - } | |
| 136 | - | |
| 137 | - private void reset() { | |
| 138 | - dataLength = -1; | |
| 139 | - parser.fixedSizeMode(4); | |
| 140 | - } | |
| 141 | - }); | |
| 142 | - | |
| 143 | - return parser; | |
| 144 | - } | |
| 145 | - | |
| 146 | - /** | |
| 147 | - * 创建简单的数据包解析器(用于测试) | |
| 148 | - * | |
| 149 | - * @param receiveHandler 接收处理器 | |
| 150 | - * @return RecordParser 解析器 | |
| 151 | - */ | |
| 152 | - public static RecordParser createSimpleParser(Consumer<TcpDataPackage> receiveHandler) { | |
| 153 | - return createParser(buffer -> { | |
| 154 | - try { | |
| 155 | - TcpDataPackage dataPackage = TcpDataDecoder.decode(buffer); | |
| 156 | - receiveHandler.accept(dataPackage); | |
| 157 | - } catch (Exception e) { | |
| 158 | - log.error("[createSimpleParser][解码数据包失败]", e); | |
| 159 | - } | |
| 160 | - }); | |
| 161 | - } | |
| 162 | -} | |
| 163 | 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 | 1 | package cn.iocoder.yudao.module.iot.gateway.protocol.tcp.router; |
| 2 | 2 | |
| 3 | -import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; | |
| 4 | 3 | 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; | |
| 4 | +import cn.iocoder.yudao.module.iot.gateway.codec.tcp.IotTcpDeviceMessageCodec; | |
| 10 | 5 | 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 | 6 | import lombok.extern.slf4j.Slf4j; |
| 14 | 7 | |
| 15 | 8 | /** |
| ... | ... | @@ -21,20 +14,21 @@ import lombok.extern.slf4j.Slf4j; |
| 21 | 14 | * 3. 属性获取 |
| 22 | 15 | * 4. 配置下发 |
| 23 | 16 | * 5. OTA 升级 |
| 17 | + * <p> | |
| 18 | + * 注意:由于移除了连接管理器,此处理器主要负责消息的编码和日志记录 | |
| 24 | 19 | * |
| 25 | 20 | * @author 芋道源码 |
| 26 | 21 | */ |
| 27 | 22 | @Slf4j |
| 28 | 23 | public class IotTcpDownstreamHandler { |
| 29 | 24 | |
| 30 | - private final TcpDeviceConnectionManager connectionManager; | |
| 31 | - | |
| 32 | 25 | private final IotDeviceMessageService messageService; |
| 33 | 26 | |
| 34 | - public IotTcpDownstreamHandler(TcpDeviceConnectionManager connectionManager, | |
| 35 | - IotDeviceMessageService messageService) { | |
| 36 | - this.connectionManager = connectionManager; | |
| 27 | + private final IotTcpDeviceMessageCodec codec; | |
| 28 | + | |
| 29 | + public IotTcpDownstreamHandler(IotDeviceMessageService messageService) { | |
| 37 | 30 | this.messageService = messageService; |
| 31 | + this.codec = new IotTcpDeviceMessageCodec(); | |
| 38 | 32 | } |
| 39 | 33 | |
| 40 | 34 | /** |
| ... | ... | @@ -47,315 +41,19 @@ public class IotTcpDownstreamHandler { |
| 47 | 41 | log.info("[handle][处理下行消息] 设备ID: {}, 方法: {}, 消息ID: {}", |
| 48 | 42 | message.getDeviceId(), message.getMethod(), message.getId()); |
| 49 | 43 | |
| 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 | - // TODO @芋艿、@haohao:看看有没什么办法,减少这样的编码。拓展新消息类型,成本高; | |
| 59 | - switch (message.getMethod()) { | |
| 60 | - case "thing.property.set": | |
| 61 | - handlePropertySet(client, message); | |
| 62 | - break; | |
| 63 | - case "thing.property.get": | |
| 64 | - handlePropertyGet(client, message); | |
| 65 | - break; | |
| 66 | - case "thing.service.invoke": | |
| 67 | - handleServiceInvoke(client, message); | |
| 68 | - break; | |
| 69 | - case "thing.config.push": | |
| 70 | - handleConfigPush(client, message); | |
| 71 | - break; | |
| 72 | - case "thing.ota.upgrade": | |
| 73 | - handleOtaUpgrade(client, message); | |
| 74 | - break; | |
| 75 | - default: | |
| 76 | - log.warn("[handle][未知的下行消息方法: {}]", message.getMethod()); | |
| 77 | - break; | |
| 78 | - } | |
| 79 | - } catch (Exception e) { | |
| 80 | - // TODO @haohao:最好消息的内容,打印下; | |
| 81 | - log.error("[handle][处理下行消息失败]", e); | |
| 82 | - } | |
| 83 | - } | |
| 84 | - | |
| 85 | - /** | |
| 86 | - * 处理属性设置 | |
| 87 | - * | |
| 88 | - * @param client 设备客户端 | |
| 89 | - * @param message 设备消息 | |
| 90 | - */ | |
| 91 | - private void handlePropertySet(TcpDeviceClient client, IotDeviceMessage message) { | |
| 92 | - try { | |
| 93 | - log.info("[handlePropertySet][属性设置] 设备地址: {}, 属性: {}", | |
| 94 | - client.getDeviceAddr(), message.getParams()); | |
| 95 | - | |
| 96 | - // 使用编解码器发送消息,降级处理使用原始编码 | |
| 97 | - sendMessageWithCodec(client, message, "handlePropertySet", () -> { | |
| 98 | - String payload = JSON.toJSONString(message.getParams()); | |
| 99 | - short mid = generateMessageId(); | |
| 100 | - | |
| 101 | - Buffer buffer = TcpDataEncoder.createPropertySetPackage( | |
| 102 | - client.getDeviceAddr(), mid, payload); | |
| 103 | - client.sendMessage(buffer); | |
| 44 | + // 编码消息用于日志记录和验证 | |
| 45 | + byte[] encodedMessage = codec.encode(message); | |
| 46 | + log.debug("[handle][消息编码成功] 设备ID: {}, 编码后长度: {} 字节", | |
| 47 | + message.getDeviceId(), encodedMessage.length); | |
| 104 | 48 | |
| 105 | - log.debug("[handlePropertySet][属性设置消息已发送(降级)] 设备地址: {}, 消息序号: {}", | |
| 106 | - client.getDeviceAddr(), mid); | |
| 107 | - }); | |
| 108 | - } catch (Exception e) { | |
| 109 | - log.error("[handlePropertySet][属性设置失败]", e); | |
| 110 | - } | |
| 111 | - } | |
| 49 | + // 记录下行消息处理 | |
| 50 | + log.info("[handle][下行消息处理完成] 设备ID: {}, 方法: {}, 消息内容: {}", | |
| 51 | + message.getDeviceId(), message.getMethod(), message.getParams()); | |
| 112 | 52 | |
| 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 | 53 | } catch (Exception e) { |
| 137 | - log.error("[handlePropertyGet][属性获取失败]", e); | |
| 54 | + log.error("[handle][处理下行消息失败] 设备ID: {}, 方法: {}, 消息内容: {}", | |
| 55 | + message.getDeviceId(), message.getMethod(), message.getParams(), e); | |
| 138 | 56 | } |
| 139 | 57 | } |
| 140 | 58 | |
| 141 | - /** | |
| 142 | - * 处理服务调用 | |
| 143 | - * | |
| 144 | - * @param client 设备客户端 | |
| 145 | - * @param message 设备消息 | |
| 146 | - */ | |
| 147 | - private void handleServiceInvoke(TcpDeviceClient client, IotDeviceMessage message) { | |
| 148 | - try { | |
| 149 | - log.info("[handleServiceInvoke][服务调用] 设备地址: {}, 服务参数: {}", | |
| 150 | - client.getDeviceAddr(), message.getParams()); | |
| 151 | - | |
| 152 | - // 1. 构建服务调用数据包 | |
| 153 | - String payload = JSON.toJSONString(message.getParams()); | |
| 154 | - short mid = generateMessageId(); | |
| 155 | - | |
| 156 | - Buffer buffer = TcpDataEncoder.createServiceInvokePackage( | |
| 157 | - client.getDeviceAddr(), mid, payload); | |
| 158 | - | |
| 159 | - // 2. 发送消息 | |
| 160 | - client.sendMessage(buffer); | |
| 161 | - | |
| 162 | - log.debug("[handleServiceInvoke][服务调用消息已发送] 设备地址: {}, 消息序号: {}", | |
| 163 | - client.getDeviceAddr(), mid); | |
| 164 | - } catch (Exception e) { | |
| 165 | - log.error("[handleServiceInvoke][服务调用失败]", e); | |
| 166 | - } | |
| 167 | - } | |
| 168 | - | |
| 169 | - /** | |
| 170 | - * 处理配置推送 | |
| 171 | - * | |
| 172 | - * @param client 设备客户端 | |
| 173 | - * @param message 设备消息 | |
| 174 | - */ | |
| 175 | - private void handleConfigPush(TcpDeviceClient client, IotDeviceMessage message) { | |
| 176 | - try { | |
| 177 | - log.info("[handleConfigPush][配置推送] 设备地址: {}, 配置: {}", | |
| 178 | - client.getDeviceAddr(), message.getParams()); | |
| 179 | - | |
| 180 | - // 1. 构建配置推送数据包 | |
| 181 | - String payload = JSON.toJSONString(message.getParams()); | |
| 182 | - short mid = generateMessageId(); | |
| 183 | - | |
| 184 | - Buffer buffer = TcpDataEncoder.createDataDownPackage( | |
| 185 | - client.getDeviceAddr(), mid, payload); | |
| 186 | - | |
| 187 | - // 2. 发送消息 | |
| 188 | - client.sendMessage(buffer); | |
| 189 | - | |
| 190 | - log.debug("[handleConfigPush][配置推送消息已发送] 设备地址: {}, 消息序号: {}", | |
| 191 | - client.getDeviceAddr(), mid); | |
| 192 | - } catch (Exception e) { | |
| 193 | - log.error("[handleConfigPush][配置推送失败]", e); | |
| 194 | - } | |
| 195 | - } | |
| 196 | - | |
| 197 | - /** | |
| 198 | - * 处理 OTA 升级 | |
| 199 | - * | |
| 200 | - * @param client 设备客户端 | |
| 201 | - * @param message 设备消息 | |
| 202 | - */ | |
| 203 | - private void handleOtaUpgrade(TcpDeviceClient client, IotDeviceMessage message) { | |
| 204 | - try { | |
| 205 | - log.info("[handleOtaUpgrade][OTA升级] 设备地址: {}, 升级信息: {}", | |
| 206 | - client.getDeviceAddr(), message.getParams()); | |
| 207 | - | |
| 208 | - // 1. 构建 OTA 升级数据包 | |
| 209 | - String payload = JSON.toJSONString(message.getParams()); | |
| 210 | - short mid = generateMessageId(); | |
| 211 | - | |
| 212 | - Buffer buffer = TcpDataEncoder.createDataDownPackage( | |
| 213 | - client.getDeviceAddr(), mid, payload); | |
| 214 | - | |
| 215 | - // 2. 发送消息 | |
| 216 | - client.sendMessage(buffer); | |
| 217 | - | |
| 218 | - log.debug("[handleOtaUpgrade][OTA升级消息已发送] 设备地址: {}, 消息序号: {}", | |
| 219 | - client.getDeviceAddr(), mid); | |
| 220 | - | |
| 221 | - } catch (Exception e) { | |
| 222 | - log.error("[handleOtaUpgrade][OTA升级失败]", e); | |
| 223 | - } | |
| 224 | - } | |
| 225 | - | |
| 226 | - /** | |
| 227 | - * 处理自定义下行消息 | |
| 228 | - * | |
| 229 | - * @param client 设备客户端 | |
| 230 | - * @param message 设备消息 | |
| 231 | - * @param code 功能码 | |
| 232 | - */ | |
| 233 | - private void handleCustomMessage(TcpDeviceClient client, IotDeviceMessage message, short code) { | |
| 234 | - try { | |
| 235 | - log.info("[handleCustomMessage][自定义消息] 设备地址: {}, 功能码: {}, 数据: {}", | |
| 236 | - client.getDeviceAddr(), code, message.getParams()); | |
| 237 | - | |
| 238 | - // 1. 构建自定义数据包 | |
| 239 | - String payload = JSON.toJSONString(message.getParams()); | |
| 240 | - short mid = generateMessageId(); | |
| 241 | - | |
| 242 | - TcpDataPackage dataPackage = TcpDataPackage.builder() | |
| 243 | - .addr(client.getDeviceAddr()) | |
| 244 | - .code(code) | |
| 245 | - .mid(mid) | |
| 246 | - .payload(payload) | |
| 247 | - .build(); | |
| 248 | - | |
| 249 | - Buffer buffer = TcpDataEncoder.encode(dataPackage); | |
| 250 | - | |
| 251 | - // 2. 发送消息 | |
| 252 | - client.sendMessage(buffer); | |
| 253 | - | |
| 254 | - log.debug("[handleCustomMessage][自定义消息已发送] 设备地址: {}, 功能码: {}, 消息序号: {}", | |
| 255 | - client.getDeviceAddr(), code, mid); | |
| 256 | - | |
| 257 | - } catch (Exception e) { | |
| 258 | - log.error("[handleCustomMessage][自定义消息发送失败]", e); | |
| 259 | - } | |
| 260 | - } | |
| 261 | - | |
| 262 | - // TODO @haohao:用不到的,要不暂时不提供; | |
| 263 | - /** | |
| 264 | - * 批量发送下行消息 | |
| 265 | - * | |
| 266 | - * @param deviceIds 设备ID列表 | |
| 267 | - * @param message 设备消息 | |
| 268 | - */ | |
| 269 | - public void broadcastMessage(Long[] deviceIds, IotDeviceMessage message) { | |
| 270 | - try { | |
| 271 | - log.info("[broadcastMessage][批量发送消息] 设备数量: {}, 方法: {}", | |
| 272 | - deviceIds.length, message.getMethod()); | |
| 273 | - | |
| 274 | - for (Long deviceId : deviceIds) { | |
| 275 | - // 创建副本消息(避免消息ID冲突) | |
| 276 | - IotDeviceMessage copyMessage = IotDeviceMessage.of( | |
| 277 | - message.getRequestId(), | |
| 278 | - message.getMethod(), | |
| 279 | - message.getParams(), | |
| 280 | - message.getData(), | |
| 281 | - message.getCode(), | |
| 282 | - message.getMsg()); | |
| 283 | - copyMessage.setDeviceId(deviceId); | |
| 284 | - | |
| 285 | - // 处理单个设备消息 | |
| 286 | - handle(copyMessage); | |
| 287 | - } | |
| 288 | - } catch (Exception e) { | |
| 289 | - log.error("[broadcastMessage][批量发送消息失败]", e); | |
| 290 | - } | |
| 291 | - } | |
| 292 | - | |
| 293 | - /** | |
| 294 | - * 检查设备是否支持指定方法 | |
| 295 | - * | |
| 296 | - * @param client 设备客户端 | |
| 297 | - * @param method 消息方法 | |
| 298 | - * @return 是否支持 | |
| 299 | - */ | |
| 300 | - private boolean isMethodSupported(TcpDeviceClient client, String method) { | |
| 301 | - // TODO: 可以根据设备类型或产品信息判断是否支持特定方法 | |
| 302 | - return IotDeviceMessageMethodEnum.of(method) != null; | |
| 303 | - } | |
| 304 | - | |
| 305 | - /** | |
| 306 | - * 生成消息序号 | |
| 307 | - * | |
| 308 | - * @return 消息序号 | |
| 309 | - */ | |
| 310 | - private short generateMessageId() { | |
| 311 | - return (short) (System.currentTimeMillis() % Short.MAX_VALUE); | |
| 312 | - } | |
| 313 | - | |
| 314 | - /** | |
| 315 | - * 使用编解码器发送消息 | |
| 316 | - * | |
| 317 | - * @param client 设备客户端 | |
| 318 | - * @param message 设备消息 | |
| 319 | - * @param methodName 方法名称 | |
| 320 | - * @param fallbackAction 降级处理逻辑 | |
| 321 | - */ | |
| 322 | - private void sendMessageWithCodec(TcpDeviceClient client, IotDeviceMessage message, | |
| 323 | - String methodName, Runnable fallbackAction) { | |
| 324 | - try { | |
| 325 | - // 1. 使用编解码器编码消息 | |
| 326 | - byte[] messageBytes = messageService.encodeDeviceMessage( | |
| 327 | - message, client.getProductKey(), client.getDeviceName()); | |
| 328 | - | |
| 329 | - // 2. 解析编码后的数据包并设置设备地址和消息序号 | |
| 330 | - Buffer buffer = Buffer.buffer(messageBytes); | |
| 331 | - TcpDataPackage dataPackage = TcpDataDecoder.decode(buffer); | |
| 332 | - dataPackage.setAddr(client.getDeviceAddr()); | |
| 333 | - dataPackage.setMid(generateMessageId()); | |
| 334 | - | |
| 335 | - // 3. 重新编码并发送 | |
| 336 | - Buffer finalBuffer = TcpDataEncoder.encode(dataPackage); | |
| 337 | - client.sendMessage(finalBuffer); | |
| 338 | - | |
| 339 | - log.debug("[{}][消息已发送] 设备地址: {}, 消息序号: {}", | |
| 340 | - methodName, client.getDeviceAddr(), dataPackage.getMid()); | |
| 341 | - } catch (Exception e) { | |
| 342 | - log.warn("[{}][使用编解码器编码失败,降级使用原始编码] 错误: {}", | |
| 343 | - methodName, e.getMessage()); | |
| 344 | - | |
| 345 | - // 执行降级处理 | |
| 346 | - if (fallbackAction != null) { | |
| 347 | - fallbackAction.run(); | |
| 348 | - } | |
| 349 | - } | |
| 350 | - } | |
| 351 | - | |
| 352 | - // TODO @haohao:看看这个要不要删除掉 | |
| 353 | - /** | |
| 354 | - * 获取连接统计信息 | |
| 355 | - * | |
| 356 | - * @return 连接统计信息 | |
| 357 | - */ | |
| 358 | - public String getHandlerStatistics() { | |
| 359 | - return String.format("TCP下游处理器 - %s", connectionManager.getConnectionStatus()); | |
| 360 | - } | |
| 361 | 59 | } |
| 362 | 60 | \ 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
| 1 | 1 | package cn.iocoder.yudao.module.iot.gateway.protocol.tcp.router; |
| 2 | 2 | |
| 3 | 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 | 4 | 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; | |
| 5 | +import cn.iocoder.yudao.module.iot.gateway.codec.tcp.IotTcpCodecManager; | |
| 6 | +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpUpstreamProtocol; | |
| 17 | 7 | import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; |
| 18 | 8 | import io.vertx.core.Handler; |
| 19 | 9 | import io.vertx.core.buffer.Buffer; |
| 20 | 10 | import io.vertx.core.net.NetSocket; |
| 21 | 11 | import io.vertx.core.parsetools.RecordParser; |
| 22 | -import lombok.RequiredArgsConstructor; | |
| 23 | 12 | import lombok.extern.slf4j.Slf4j; |
| 24 | 13 | |
| 25 | 14 | /** |
| 26 | 15 | * IoT 网关 TCP 上行消息处理器 |
| 27 | - * <p> | |
| 28 | - * 核心负责: | |
| 29 | - * 1. 【设备注册】设备连接后发送注册消息,注册成功后可以进行通信 | |
| 30 | - * 2. 【心跳处理】定期接收设备心跳消息,维持连接状态 | |
| 31 | - * 3. 【数据上报】接收设备数据上报和事件上报 | |
| 32 | - * 4. 【连接管理】管理连接的建立、维护和清理 | |
| 33 | 16 | * |
| 34 | 17 | * @author 芋道源码 |
| 35 | 18 | */ |
| 36 | -@RequiredArgsConstructor | |
| 37 | 19 | @Slf4j |
| 38 | 20 | public class IotTcpUpstreamHandler implements Handler<NetSocket> { |
| 39 | 21 | |
| 40 | - private final IotGatewayProperties.TcpProperties tcpConfig; | |
| 41 | - | |
| 42 | - // TODO @haohao:可以把 TcpDeviceConnectionManager 能力放大一点:1)handle 里的 client 初始化,可以拿到 TcpDeviceConnectionManager 里;2)handleDeviceRegister 也是; | |
| 43 | - private final TcpDeviceConnectionManager connectionManager; | |
| 44 | - | |
| 45 | - private final IotDeviceService deviceService; | |
| 46 | - | |
| 47 | - private final IotDeviceMessageService messageService; | |
| 48 | - | |
| 49 | - private final IotDeviceCommonApi deviceApi; | |
| 22 | + private final IotDeviceMessageService deviceMessageService; | |
| 50 | 23 | |
| 51 | 24 | private final String serverId; |
| 52 | 25 | |
| 53 | - @Override | |
| 54 | - public void handle(NetSocket socket) { | |
| 55 | - log.info("[handle][收到设备连接: {}]", socket.remoteAddress()); | |
| 56 | - | |
| 57 | - // 创建客户端 ID 和设备客户端 | |
| 58 | - // TODO @haohao:clientid 给 TcpDeviceClient 生成会简洁一点;减少 upsteramhanlder 的非核心逻辑; | |
| 59 | - String clientId = IdUtil.simpleUUID() + "_" + socket.remoteAddress(); | |
| 60 | - TcpDeviceClient client = new TcpDeviceClient(clientId, tcpConfig.getKeepAliveTimeoutMs()); | |
| 61 | - | |
| 62 | - try { | |
| 63 | - // 设置连接异常和关闭处理 | |
| 64 | - socket.exceptionHandler(ex -> { | |
| 65 | - // TODO @haohao:这里的日志,可能把 clientid 都打上?因为 address 会重复么? | |
| 66 | - log.error("[handle][连接({})异常]", socket.remoteAddress(), ex); | |
| 67 | - handleConnectionClose(client); | |
| 68 | - }); | |
| 69 | - socket.closeHandler(v -> { | |
| 70 | - log.info("[handle][连接({})关闭]", socket.remoteAddress()); | |
| 71 | - handleConnectionClose(client); | |
| 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 | - client.setParser(parser); | |
| 26 | + private final IotTcpCodecManager codecManager; | |
| 84 | 27 | |
| 85 | - // TODO @haohao:socket.remoteAddress()) 打印进去 | |
| 86 | - log.info("[handle][设备连接处理器初始化完成: {}]", clientId); | |
| 87 | - } catch (Exception e) { | |
| 88 | - // TODO @haohao:socket.remoteAddress()) 打印进去 | |
| 89 | - log.error("[handle][初始化连接处理器失败]", e); | |
| 90 | - client.shutdown(); | |
| 91 | - } | |
| 92 | - } | |
| 93 | - | |
| 94 | - /** | |
| 95 | - * 处理数据包 | |
| 96 | - * | |
| 97 | - * @param client 设备客户端 | |
| 98 | - * @param buffer 数据缓冲区 | |
| 99 | - */ | |
| 100 | - private void handleDataPackage(TcpDeviceClient client, io.vertx.core.buffer.Buffer buffer) { | |
| 101 | - try { | |
| 102 | - // 解码数据包 | |
| 103 | - TcpDataPackage dataPackage = TcpDataDecoder.decode(buffer); | |
| 104 | - log.info("[handleDataPackage][接收数据包] 设备地址: {}, 功能码: {}, 消息序号: {}", | |
| 105 | - dataPackage.getAddr(), dataPackage.getCodeDescription(), dataPackage.getMid()); | |
| 106 | - | |
| 107 | - // 根据功能码处理不同类型的消息 | |
| 108 | - switch (dataPackage.getCode()) { | |
| 109 | - // TODO @haohao:【重要】code 要不要改成 opCode。这样和 data 里的 code 好区分; | |
| 110 | - case TcpDataPackage.CODE_REGISTER: | |
| 111 | - handleDeviceRegister(client, dataPackage); | |
| 112 | - break; | |
| 113 | - case TcpDataPackage.CODE_HEARTBEAT: | |
| 114 | - handleHeartbeat(client, dataPackage); | |
| 115 | - break; | |
| 116 | - case TcpDataPackage.CODE_DATA_UP: | |
| 117 | - handleDataUp(client, dataPackage); | |
| 118 | - break; | |
| 119 | - case TcpDataPackage.CODE_EVENT_UP: | |
| 120 | - handleEventUp(client, dataPackage); | |
| 121 | - break; | |
| 122 | - default: | |
| 123 | - log.warn("[handleDataPackage][未知功能码: {}]", dataPackage.getCode()); | |
| 124 | - break; | |
| 125 | - } | |
| 126 | - } catch (Exception e) { | |
| 127 | - // TODO @haohao:最好有 client 标识; | |
| 128 | - log.error("[handleDataPackage][处理数据包失败]", e); | |
| 129 | - } | |
| 130 | - } | |
| 131 | - | |
| 132 | - /** | |
| 133 | - * 处理设备注册 | |
| 134 | - * | |
| 135 | - * @param client 设备客户端 | |
| 136 | - * @param dataPackage 数据包 | |
| 137 | - */ | |
| 138 | - private void handleDeviceRegister(TcpDeviceClient client, TcpDataPackage dataPackage) { | |
| 139 | - try { | |
| 140 | - String deviceAddr = dataPackage.getAddr(); | |
| 141 | - String productKey = dataPackage.getPayload(); | |
| 142 | - log.info("[handleDeviceRegister][设备注册] 设备地址: {}, 产品密钥: {}", deviceAddr, productKey); | |
| 143 | - | |
| 144 | - // 获取设备信息 | |
| 145 | - IotDeviceRespDTO device = deviceService.getDeviceFromCache(productKey, deviceAddr); | |
| 146 | - if (device == null) { | |
| 147 | - log.error("[handleDeviceRegister][设备不存在: {} - {}]", productKey, deviceAddr); | |
| 148 | - sendRegisterReply(client, dataPackage, false); | |
| 149 | - return; | |
| 150 | - } | |
| 151 | - | |
| 152 | - // 更新客户端信息 | |
| 153 | - // TODO @haohao:一个 set 方法,统一处理掉会好点哈; | |
| 154 | - client.setProductKey(productKey); | |
| 155 | - client.setDeviceName(deviceAddr); | |
| 156 | - client.setDeviceId(device.getId()); | |
| 157 | - client.setAuthenticated(true); | |
| 158 | - | |
| 159 | - // 添加到连接管理器 | |
| 160 | - connectionManager.addClient(deviceAddr, client); | |
| 161 | - connectionManager.setDeviceIdMapping(deviceAddr, device.getId()); | |
| 162 | - | |
| 163 | - // 发送设备上线消息 | |
| 164 | - IotDeviceMessage onlineMessage = IotDeviceMessage.buildStateUpdateOnline(); | |
| 165 | - messageService.sendDeviceMessage(onlineMessage, productKey, deviceAddr, serverId); | |
| 166 | - | |
| 167 | - // 发送注册成功回复 | |
| 168 | - sendRegisterReply(client, dataPackage, true); | |
| 169 | - | |
| 170 | - log.info("[handleDeviceRegister][设备注册成功] 设备地址: {}, 设备ID: {}", deviceAddr, device.getId()); | |
| 171 | - } catch (Exception e) { | |
| 172 | - log.error("[handleDeviceRegister][设备注册失败]", e); | |
| 173 | - sendRegisterReply(client, dataPackage, false); | |
| 174 | - } | |
| 175 | - } | |
| 176 | - | |
| 177 | - /** | |
| 178 | - * 处理心跳 | |
| 179 | - * | |
| 180 | - * @param client 设备客户端 | |
| 181 | - * @param dataPackage 数据包 | |
| 182 | - */ | |
| 183 | - private void handleHeartbeat(TcpDeviceClient client, TcpDataPackage dataPackage) { | |
| 184 | - try { | |
| 185 | - String deviceAddr = dataPackage.getAddr(); | |
| 186 | - log.debug("[handleHeartbeat][收到心跳] 设备地址: {}", deviceAddr); | |
| 187 | - | |
| 188 | - // 更新心跳时间 | |
| 189 | - client.keepAlive(); | |
| 190 | - | |
| 191 | - // 发送心跳回复(可选) | |
| 192 | - // sendHeartbeatReply(client, dataPackage); | |
| 193 | - | |
| 194 | - } catch (Exception e) { | |
| 195 | - log.error("[handleHeartbeat][处理心跳失败]", e); | |
| 196 | - } | |
| 28 | + public IotTcpUpstreamHandler(IotTcpUpstreamProtocol protocol, IotDeviceMessageService deviceMessageService, | |
| 29 | + IotTcpCodecManager codecManager) { | |
| 30 | + this.deviceMessageService = deviceMessageService; | |
| 31 | + this.serverId = protocol.getServerId(); | |
| 32 | + this.codecManager = codecManager; | |
| 197 | 33 | } |
| 198 | 34 | |
| 199 | - /** | |
| 200 | - * 处理数据上报 | |
| 201 | - * | |
| 202 | - * @param client 设备客户端 | |
| 203 | - * @param dataPackage 数据包 | |
| 204 | - */ | |
| 205 | - private void handleDataUp(TcpDeviceClient client, TcpDataPackage dataPackage) { | |
| 206 | - try { | |
| 207 | - String deviceAddr = dataPackage.getAddr(); | |
| 208 | - String payload = dataPackage.getPayload(); | |
| 209 | - | |
| 210 | - log.info("[handleDataUp][数据上报] 设备地址: {}, 数据: {}", deviceAddr, payload); | |
| 211 | - | |
| 212 | - // 检查设备是否已认证 | |
| 213 | - if (!client.isAuthenticated()) { | |
| 214 | - log.warn("[handleDataUp][设备未认证,忽略数据上报: {}]", deviceAddr); | |
| 215 | - return; | |
| 216 | - } | |
| 35 | + @Override | |
| 36 | + public void handle(NetSocket socket) { | |
| 37 | + // 生成客户端ID用于日志标识 | |
| 38 | + String clientId = IdUtil.simpleUUID(); | |
| 39 | + log.info("[handle][收到设备连接] clientId: {}, address: {}", clientId, socket.remoteAddress()); | |
| 217 | 40 | |
| 218 | - // 使用 IotDeviceMessageService 解码消息 | |
| 41 | + // 设置解析器 | |
| 42 | + RecordParser parser = RecordParser.newFixed(1024, buffer -> { | |
| 219 | 43 | try { |
| 220 | - // 1. 将 TCP 数据包重新编码为字节数组 | |
| 221 | - Buffer buffer = TcpDataEncoder.encode(dataPackage); | |
| 222 | - byte[] messageBytes = buffer.getBytes(); | |
| 223 | - | |
| 224 | - // 2. 使用 messageService 解码消息 | |
| 225 | - IotDeviceMessage message = messageService.decodeDeviceMessage( | |
| 226 | - messageBytes, client.getProductKey(), client.getDeviceName()); | |
| 227 | - | |
| 228 | - // 3. 发送解码后的消息 | |
| 229 | - messageService.sendDeviceMessage(message, client.getProductKey(), client.getDeviceName(), serverId); | |
| 44 | + handleDataPackage(clientId, buffer); | |
| 230 | 45 | } catch (Exception e) { |
| 231 | - log.warn("[handleDataUp][使用编解码器解码失败,降级使用原始解析] 错误: {}", e.getMessage()); | |
| 232 | - | |
| 233 | - // 降级处理:使用原始方式解析数据 | |
| 234 | - JSONObject dataJson = JSONUtil.parseObj(payload); | |
| 235 | - IotDeviceMessage message = IotDeviceMessage.requestOf("thing.property.post", dataJson); | |
| 236 | - messageService.sendDeviceMessage(message, client.getProductKey(), client.getDeviceName(), serverId); | |
| 46 | + log.error("[handle][处理数据包异常] clientId: {}", clientId, e); | |
| 237 | 47 | } |
| 48 | + }); | |
| 238 | 49 | |
| 239 | - // 发送数据上报回复 | |
| 240 | - sendDataUpReply(client, dataPackage); | |
| 241 | - } catch (Exception e) { | |
| 242 | - log.error("[handleDataUp][处理数据上报失败]", e); | |
| 243 | - } | |
| 244 | - } | |
| 50 | + // 设置异常处理 | |
| 51 | + socket.exceptionHandler(ex -> { | |
| 52 | + log.error("[handle][连接异常] clientId: {}, address: {}", clientId, socket.remoteAddress(), ex); | |
| 53 | + }); | |
| 245 | 54 | |
| 246 | - /** | |
| 247 | - * 处理事件上报 | |
| 248 | - * | |
| 249 | - * @param client 设备客户端 | |
| 250 | - * @param dataPackage 数据包 | |
| 251 | - */ | |
| 252 | - private void handleEventUp(TcpDeviceClient client, TcpDataPackage dataPackage) { | |
| 253 | - try { | |
| 254 | - String deviceAddr = dataPackage.getAddr(); | |
| 255 | - String payload = dataPackage.getPayload(); | |
| 256 | - | |
| 257 | - log.info("[handleEventUp][事件上报] 设备地址: {}, 数据: {}", deviceAddr, payload); | |
| 258 | - | |
| 259 | - // 检查设备是否已认证 | |
| 260 | - if (!client.isAuthenticated()) { | |
| 261 | - log.warn("[handleEventUp][设备未认证,忽略事件上报: {}]", deviceAddr); | |
| 262 | - return; | |
| 263 | - } | |
| 264 | - | |
| 265 | - // 使用 IotDeviceMessageService 解码消息 | |
| 266 | - try { | |
| 267 | - // 1. 将 TCP 数据包重新编码为字节数组 | |
| 268 | - Buffer buffer = TcpDataEncoder.encode(dataPackage); | |
| 269 | - byte[] messageBytes = buffer.getBytes(); | |
| 55 | + socket.closeHandler(v -> { | |
| 56 | + log.info("[handle][连接关闭] clientId: {}, address: {}", clientId, socket.remoteAddress()); | |
| 57 | + }); | |
| 270 | 58 | |
| 271 | - // 2. 使用 messageService 解码消息 | |
| 272 | - IotDeviceMessage message = messageService.decodeDeviceMessage( | |
| 273 | - messageBytes, client.getProductKey(), client.getDeviceName()); | |
| 274 | - | |
| 275 | - // 3. 发送解码后的消息 | |
| 276 | - messageService.sendDeviceMessage(message, client.getProductKey(), client.getDeviceName(), serverId); | |
| 277 | - } catch (Exception e) { | |
| 278 | - log.warn("[handleEventUp][使用编解码器解码失败,降级使用原始解析] 错误: {}", e.getMessage()); | |
| 279 | - | |
| 280 | - // 降级处理:使用原始方式解析数据 | |
| 281 | - // TODO @芋艿:降级处理逻辑; | |
| 282 | - JSONObject eventJson = JSONUtil.parseObj(payload); | |
| 283 | - IotDeviceMessage message = IotDeviceMessage.requestOf("thing.event.post", eventJson); | |
| 284 | - messageService.sendDeviceMessage(message, client.getProductKey(), client.getDeviceName(), serverId); | |
| 285 | - } | |
| 286 | - | |
| 287 | - // 发送事件上报回复 | |
| 288 | - sendEventUpReply(client, dataPackage); | |
| 289 | - } catch (Exception e) { | |
| 290 | - log.error("[handleEventUp][处理事件上报失败]", e); | |
| 291 | - } | |
| 59 | + // 设置数据处理器 | |
| 60 | + socket.handler(parser); | |
| 292 | 61 | } |
| 293 | 62 | |
| 294 | 63 | /** |
| 295 | - * 发送注册回复 | |
| 296 | - * | |
| 297 | - * @param client 设备客户端 | |
| 298 | - * @param dataPackage 原始数据包 | |
| 299 | - * @param success 是否成功 | |
| 64 | + * 处理数据包 | |
| 300 | 65 | */ |
| 301 | - private void sendRegisterReply(TcpDeviceClient client, TcpDataPackage dataPackage, boolean success) { | |
| 66 | + private void handleDataPackage(String clientId, Buffer buffer) { | |
| 302 | 67 | try { |
| 303 | - io.vertx.core.buffer.Buffer replyBuffer = TcpDataEncoder.createRegisterReply( | |
| 304 | - dataPackage.getAddr(), dataPackage.getMid(), success); | |
| 305 | - client.sendMessage(replyBuffer); | |
| 68 | + // 使用编解码器管理器自动检测协议并解码消息 | |
| 69 | + IotDeviceMessage message = codecManager.decode(buffer.getBytes()); | |
| 70 | + log.info("[handleDataPackage][接收数据包] clientId: {}, 方法: {}, 设备ID: {}", | |
| 71 | + clientId, message.getMethod(), message.getDeviceId()); | |
| 306 | 72 | |
| 307 | - log.debug("[sendRegisterReply][发送注册回复] 设备地址: {}, 结果: {}", | |
| 308 | - dataPackage.getAddr(), success ? "成功" : "失败"); | |
| 73 | + // 处理上行消息 | |
| 74 | + handleUpstreamMessage(clientId, message); | |
| 309 | 75 | } catch (Exception e) { |
| 310 | - log.error("[sendRegisterReply][发送注册回复失败]", e); | |
| 76 | + log.error("[handleDataPackage][处理数据包失败] clientId: {}", clientId, e); | |
| 311 | 77 | } |
| 312 | 78 | } |
| 313 | 79 | |
| 314 | 80 | /** |
| 315 | - * 发送数据上报回复 | |
| 316 | - * | |
| 317 | - * @param client 设备客户端 | |
| 318 | - * @param dataPackage 原始数据包 | |
| 81 | + * 处理上行消息 | |
| 319 | 82 | */ |
| 320 | - private void sendDataUpReply(TcpDeviceClient client, TcpDataPackage dataPackage) { | |
| 83 | + private void handleUpstreamMessage(String clientId, IotDeviceMessage message) { | |
| 321 | 84 | try { |
| 322 | - TcpDataPackage replyPackage = TcpDataPackage.builder() | |
| 323 | - .addr(dataPackage.getAddr()) | |
| 324 | - .code(TcpDataPackage.CODE_DATA_UP) | |
| 325 | - .mid(dataPackage.getMid()) | |
| 326 | - .payload("0") // 0 表示成功 TODO @haohao:最好枚举到 TcpDataPackage 里? | |
| 327 | - .build(); | |
| 328 | - | |
| 329 | - io.vertx.core.buffer.Buffer replyBuffer = TcpDataEncoder.encode(replyPackage); | |
| 330 | - client.sendMessage(replyBuffer); | |
| 331 | - } catch (Exception e) { | |
| 332 | - // TODO @haohao:可以有个 client id | |
| 333 | - log.error("[sendDataUpReply][发送数据上报回复失败]", e); | |
| 334 | - } | |
| 335 | - } | |
| 85 | + log.info("[handleUpstreamMessage][上行消息] clientId: {}, 方法: {}, 设备ID: {}", | |
| 86 | + clientId, message.getMethod(), message.getDeviceId()); | |
| 336 | 87 | |
| 337 | - /** | |
| 338 | - * 发送事件上报回复 | |
| 339 | - * | |
| 340 | - * @param client 设备客户端 | |
| 341 | - * @param dataPackage 原始数据包 | |
| 342 | - */ | |
| 343 | - private void sendEventUpReply(TcpDeviceClient client, TcpDataPackage dataPackage) { | |
| 344 | - try { | |
| 345 | - TcpDataPackage replyPackage = TcpDataPackage.builder() | |
| 346 | - .addr(dataPackage.getAddr()) | |
| 347 | - .code(TcpDataPackage.CODE_EVENT_UP) | |
| 348 | - .mid(dataPackage.getMid()) | |
| 349 | - .payload("0") // 0 表示成功 | |
| 350 | - .build(); | |
| 88 | + // 解析设备信息(简化处理) | |
| 89 | + String deviceId = String.valueOf(message.getDeviceId()); | |
| 90 | + String productKey = extractProductKey(deviceId); | |
| 91 | + String deviceName = deviceId; | |
| 351 | 92 | |
| 352 | - io.vertx.core.buffer.Buffer replyBuffer = TcpDataEncoder.encode(replyPackage); | |
| 353 | - client.sendMessage(replyBuffer); | |
| 93 | + // 发送消息到队列 | |
| 94 | + deviceMessageService.sendDeviceMessage(message, productKey, deviceName, serverId); | |
| 354 | 95 | } catch (Exception e) { |
| 355 | - log.error("[sendEventUpReply][发送事件上报回复失败]", e); | |
| 96 | + log.error("[handleUpstreamMessage][处理上行消息失败] clientId: {}", clientId, e); | |
| 356 | 97 | } |
| 357 | 98 | } |
| 358 | 99 | |
| 359 | 100 | /** |
| 360 | - * 处理连接关闭 | |
| 361 | - * | |
| 362 | - * @param client 设备客户端 | |
| 101 | + * 从设备ID中提取产品密钥(简化实现) | |
| 363 | 102 | */ |
| 364 | - private void handleConnectionClose(TcpDeviceClient client) { | |
| 365 | - try { | |
| 366 | - String deviceAddr = client.getDeviceAddr(); | |
| 367 | - | |
| 368 | - // 发送设备离线消息 | |
| 369 | - if (client.isAuthenticated()) { | |
| 370 | - IotDeviceMessage offlineMessage = IotDeviceMessage.buildStateOffline(); | |
| 371 | - messageService.sendDeviceMessage(offlineMessage, | |
| 372 | - client.getProductKey(), client.getDeviceName(), serverId); | |
| 373 | - } | |
| 374 | - | |
| 375 | - // 从连接管理器移除 | |
| 376 | - if (deviceAddr != null) { | |
| 377 | - connectionManager.removeClient(deviceAddr); | |
| 378 | - } | |
| 379 | - | |
| 380 | - log.info("[handleConnectionClose][处理连接关闭完成] 设备地址: {}", deviceAddr); | |
| 381 | - } catch (Exception e) { | |
| 382 | - log.error("[handleConnectionClose][处理连接关闭失败]", e); | |
| 103 | + private String extractProductKey(String deviceId) { | |
| 104 | + // 简化实现:假设设备ID格式为 "productKey_deviceName" | |
| 105 | + if (deviceId != null && deviceId.contains("_")) { | |
| 106 | + return deviceId.split("_")[0]; | |
| 383 | 107 | } |
| 108 | + return "default_product"; | |
| 384 | 109 | } |
| 385 | 110 | } |
| 386 | 111 | \ No newline at end of file | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/TcpBinaryDataPacketExamples.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.iot.gateway.codec.tcp; | |
| 2 | + | |
| 3 | +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; | |
| 4 | +import lombok.extern.slf4j.Slf4j; | |
| 5 | + | |
| 6 | +import java.util.HashMap; | |
| 7 | +import java.util.Map; | |
| 8 | + | |
| 9 | +/** | |
| 10 | + * TCP二进制格式数据包示例 | |
| 11 | + * | |
| 12 | + * 演示如何使用二进制协议创建和解析TCP上报数据包和心跳包 | |
| 13 | + * | |
| 14 | + * 二进制协议格式: | |
| 15 | + * 包头(4字节) | 地址长度(2字节) | 设备地址(变长) | 功能码(2字节) | 消息序号(2字节) | 包体数据(变长) | |
| 16 | + * | |
| 17 | + * @author 芋道源码 | |
| 18 | + */ | |
| 19 | +@Slf4j | |
| 20 | +public class TcpBinaryDataPacketExamples { | |
| 21 | + | |
| 22 | + public static void main(String[] args) { | |
| 23 | + IotTcpBinaryDeviceMessageCodec codec = new IotTcpBinaryDeviceMessageCodec(); | |
| 24 | + | |
| 25 | + // 1. 数据上报包示例 | |
| 26 | + demonstrateDataReport(codec); | |
| 27 | + | |
| 28 | + // 2. 心跳包示例 | |
| 29 | + demonstrateHeartbeat(codec); | |
| 30 | + | |
| 31 | + // 3. 复杂数据上报示例 | |
| 32 | + demonstrateComplexDataReport(codec); | |
| 33 | + } | |
| 34 | + | |
| 35 | + /** | |
| 36 | + * 演示二进制格式数据上报包 | |
| 37 | + */ | |
| 38 | + private static void demonstrateDataReport(IotTcpBinaryDeviceMessageCodec codec) { | |
| 39 | + log.info("=== 二进制格式数据上报包示例 ==="); | |
| 40 | + | |
| 41 | + // 创建传感器数据 | |
| 42 | + Map<String, Object> sensorData = new HashMap<>(); | |
| 43 | + sensorData.put("temperature", 25.5); | |
| 44 | + sensorData.put("humidity", 60.2); | |
| 45 | + sensorData.put("pressure", 1013.25); | |
| 46 | + sensorData.put("battery", 85); | |
| 47 | + | |
| 48 | + // 创建设备消息 | |
| 49 | + IotDeviceMessage message = IotDeviceMessage.requestOf("thing.property.post", sensorData); | |
| 50 | + message.setDeviceId(123456L); | |
| 51 | + | |
| 52 | + // 编码 | |
| 53 | + byte[] packet = codec.encode(message); | |
| 54 | + log.info("编码后数据包长度: {} 字节", packet.length); | |
| 55 | + log.info("编码后数据包(HEX): {}", bytesToHex(packet)); | |
| 56 | + | |
| 57 | + // 解码验证 | |
| 58 | + IotDeviceMessage decoded = codec.decode(packet); | |
| 59 | + log.info("解码后消息ID: {}", decoded.getId()); | |
| 60 | + log.info("解码后请求ID: {}", decoded.getRequestId()); | |
| 61 | + log.info("解码后方法: {}", decoded.getMethod()); | |
| 62 | + log.info("解码后设备ID: {}", decoded.getDeviceId()); | |
| 63 | + log.info("解码后服务ID: {}", decoded.getServerId()); | |
| 64 | + log.info("解码后上报时间: {}", decoded.getReportTime()); | |
| 65 | + log.info("解码后参数: {}", decoded.getParams()); | |
| 66 | + | |
| 67 | + System.out.println(); | |
| 68 | + } | |
| 69 | + | |
| 70 | + /** | |
| 71 | + * 演示二进制格式心跳包 | |
| 72 | + */ | |
| 73 | + private static void demonstrateHeartbeat(IotTcpBinaryDeviceMessageCodec codec) { | |
| 74 | + log.info("=== 二进制格式心跳包示例 ==="); | |
| 75 | + | |
| 76 | + // 创建心跳消息 | |
| 77 | + IotDeviceMessage heartbeat = IotDeviceMessage.requestOf("thing.state.online", null); | |
| 78 | + heartbeat.setDeviceId(123456L); | |
| 79 | + | |
| 80 | + // 编码 | |
| 81 | + byte[] packet = codec.encode(heartbeat); | |
| 82 | + log.info("心跳包长度: {} 字节", packet.length); | |
| 83 | + log.info("心跳包(HEX): {}", bytesToHex(packet)); | |
| 84 | + | |
| 85 | + // 解码验证 | |
| 86 | + IotDeviceMessage decoded = codec.decode(packet); | |
| 87 | + log.info("解码后消息ID: {}", decoded.getId()); | |
| 88 | + log.info("解码后请求ID: {}", decoded.getRequestId()); | |
| 89 | + log.info("解码后方法: {}", decoded.getMethod()); | |
| 90 | + log.info("解码后设备ID: {}", decoded.getDeviceId()); | |
| 91 | + log.info("解码后服务ID: {}", decoded.getServerId()); | |
| 92 | + log.info("解码后参数: {}", decoded.getParams()); | |
| 93 | + | |
| 94 | + System.out.println(); | |
| 95 | + } | |
| 96 | + | |
| 97 | + /** | |
| 98 | + * 演示二进制格式复杂数据上报 | |
| 99 | + */ | |
| 100 | + private static void demonstrateComplexDataReport(IotTcpBinaryDeviceMessageCodec codec) { | |
| 101 | + log.info("=== 二进制格式复杂数据上报示例 ==="); | |
| 102 | + | |
| 103 | + // 创建复杂设备数据 | |
| 104 | + Map<String, Object> deviceData = new HashMap<>(); | |
| 105 | + | |
| 106 | + // 环境数据 | |
| 107 | + Map<String, Object> environment = new HashMap<>(); | |
| 108 | + environment.put("temperature", 23.8); | |
| 109 | + environment.put("humidity", 55.0); | |
| 110 | + environment.put("co2", 420); | |
| 111 | + deviceData.put("environment", environment); | |
| 112 | + | |
| 113 | + // GPS数据 | |
| 114 | + Map<String, Object> location = new HashMap<>(); | |
| 115 | + location.put("latitude", 39.9042); | |
| 116 | + location.put("longitude", 116.4074); | |
| 117 | + location.put("altitude", 43.5); | |
| 118 | + deviceData.put("location", location); | |
| 119 | + | |
| 120 | + // 设备状态 | |
| 121 | + Map<String, Object> status = new HashMap<>(); | |
| 122 | + status.put("battery", 78); | |
| 123 | + status.put("signal", -65); | |
| 124 | + status.put("online", true); | |
| 125 | + deviceData.put("status", status); | |
| 126 | + | |
| 127 | + // 创建设备消息 | |
| 128 | + IotDeviceMessage message = IotDeviceMessage.requestOf("thing.property.post", deviceData); | |
| 129 | + message.setDeviceId(789012L); | |
| 130 | + | |
| 131 | + // 编码 | |
| 132 | + byte[] packet = codec.encode(message); | |
| 133 | + log.info("复杂数据包长度: {} 字节", packet.length); | |
| 134 | + log.info("复杂数据包(HEX): {}", bytesToHex(packet)); | |
| 135 | + | |
| 136 | + // 解码验证 | |
| 137 | + IotDeviceMessage decoded = codec.decode(packet); | |
| 138 | + log.info("解码后消息ID: {}", decoded.getId()); | |
| 139 | + log.info("解码后请求ID: {}", decoded.getRequestId()); | |
| 140 | + log.info("解码后方法: {}", decoded.getMethod()); | |
| 141 | + log.info("解码后设备ID: {}", decoded.getDeviceId()); | |
| 142 | + log.info("解码后服务ID: {}", decoded.getServerId()); | |
| 143 | + log.info("解码后参数: {}", decoded.getParams()); | |
| 144 | + | |
| 145 | + System.out.println(); | |
| 146 | + } | |
| 147 | + | |
| 148 | + /** | |
| 149 | + * 字节数组转十六进制字符串 | |
| 150 | + */ | |
| 151 | + private static String bytesToHex(byte[] bytes) { | |
| 152 | + StringBuilder result = new StringBuilder(); | |
| 153 | + for (byte b : bytes) { | |
| 154 | + result.append(String.format("%02X ", b)); | |
| 155 | + } | |
| 156 | + return result.toString().trim(); | |
| 157 | + } | |
| 158 | + | |
| 159 | + /** | |
| 160 | + * 演示数据包结构分析 | |
| 161 | + */ | |
| 162 | + public static void analyzePacketStructure(byte[] packet) { | |
| 163 | + if (packet.length < 8) { | |
| 164 | + log.error("数据包长度不足"); | |
| 165 | + return; | |
| 166 | + } | |
| 167 | + | |
| 168 | + int index = 0; | |
| 169 | + | |
| 170 | + // 解析包头(4字节) - 后续数据长度 | |
| 171 | + int totalLength = ((packet[index] & 0xFF) << 24) | | |
| 172 | + ((packet[index + 1] & 0xFF) << 16) | | |
| 173 | + ((packet[index + 2] & 0xFF) << 8) | | |
| 174 | + (packet[index + 3] & 0xFF); | |
| 175 | + index += 4; | |
| 176 | + log.info("包头 - 后续数据长度: {} 字节", totalLength); | |
| 177 | + | |
| 178 | + // 解析设备地址长度(2字节) | |
| 179 | + int addrLength = ((packet[index] & 0xFF) << 8) | (packet[index + 1] & 0xFF); | |
| 180 | + index += 2; | |
| 181 | + log.info("设备地址长度: {} 字节", addrLength); | |
| 182 | + | |
| 183 | + // 解析设备地址 | |
| 184 | + String deviceAddr = new String(packet, index, addrLength); | |
| 185 | + index += addrLength; | |
| 186 | + log.info("设备地址: {}", deviceAddr); | |
| 187 | + | |
| 188 | + // 解析功能码(2字节) | |
| 189 | + int functionCode = ((packet[index] & 0xFF) << 8) | (packet[index + 1] & 0xFF); | |
| 190 | + index += 2; | |
| 191 | + log.info("功能码: {} ({})", functionCode, getFunctionCodeName(functionCode)); | |
| 192 | + | |
| 193 | + // 解析消息序号(2字节) | |
| 194 | + int messageId = ((packet[index] & 0xFF) << 8) | (packet[index + 1] & 0xFF); | |
| 195 | + index += 2; | |
| 196 | + log.info("消息序号: {}", messageId); | |
| 197 | + | |
| 198 | + // 解析包体数据 | |
| 199 | + if (index < packet.length) { | |
| 200 | + String payload = new String(packet, index, packet.length - index); | |
| 201 | + log.info("包体数据: {}", payload); | |
| 202 | + } | |
| 203 | + } | |
| 204 | + | |
| 205 | + /** | |
| 206 | + * 获取功能码名称 | |
| 207 | + */ | |
| 208 | + private static String getFunctionCodeName(int code) { | |
| 209 | + switch (code) { | |
| 210 | + case 10: return "设备注册"; | |
| 211 | + case 11: return "注册回复"; | |
| 212 | + case 20: return "心跳请求"; | |
| 213 | + case 21: return "心跳回复"; | |
| 214 | + case 30: return "消息上行"; | |
| 215 | + case 40: return "消息下行"; | |
| 216 | + default: return "未知功能码"; | |
| 217 | + } | |
| 218 | + } | |
| 219 | +} | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/TcpJsonDataPacketExamples.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.iot.gateway.codec.tcp; | |
| 2 | + | |
| 3 | +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; | |
| 4 | +import lombok.extern.slf4j.Slf4j; | |
| 5 | + | |
| 6 | +import java.nio.charset.StandardCharsets; | |
| 7 | +import java.util.HashMap; | |
| 8 | +import java.util.Map; | |
| 9 | + | |
| 10 | +/** | |
| 11 | + * TCP JSON格式数据包示例 | |
| 12 | + * | |
| 13 | + * 演示如何使用新的JSON格式进行TCP消息编解码 | |
| 14 | + * | |
| 15 | + * @author 芋道源码 | |
| 16 | + */ | |
| 17 | +@Slf4j | |
| 18 | +public class TcpJsonDataPacketExamples { | |
| 19 | + | |
| 20 | + public static void main(String[] args) { | |
| 21 | + IotTcpJsonDeviceMessageCodec codec = new IotTcpJsonDeviceMessageCodec(); | |
| 22 | + | |
| 23 | + // 1. 数据上报示例 | |
| 24 | + demonstrateDataReport(codec); | |
| 25 | + | |
| 26 | + // 2. 心跳示例 | |
| 27 | + demonstrateHeartbeat(codec); | |
| 28 | + | |
| 29 | + // 3. 事件上报示例 | |
| 30 | + demonstrateEventReport(codec); | |
| 31 | + | |
| 32 | + // 4. 复杂数据上报示例 | |
| 33 | + demonstrateComplexDataReport(codec); | |
| 34 | + | |
| 35 | + // 5. 便捷方法示例 | |
| 36 | + demonstrateConvenienceMethods(); | |
| 37 | + | |
| 38 | + // 6. EMQX兼容性示例 | |
| 39 | + demonstrateEmqxCompatibility(); | |
| 40 | + } | |
| 41 | + | |
| 42 | + /** | |
| 43 | + * 演示数据上报 | |
| 44 | + */ | |
| 45 | + private static void demonstrateDataReport(IotTcpJsonDeviceMessageCodec codec) { | |
| 46 | + log.info("=== JSON格式数据上报示例 ==="); | |
| 47 | + | |
| 48 | + // 创建传感器数据 | |
| 49 | + Map<String, Object> sensorData = new HashMap<>(); | |
| 50 | + sensorData.put("temperature", 25.5); | |
| 51 | + sensorData.put("humidity", 60.2); | |
| 52 | + sensorData.put("pressure", 1013.25); | |
| 53 | + sensorData.put("battery", 85); | |
| 54 | + | |
| 55 | + // 创建设备消息 | |
| 56 | + IotDeviceMessage message = IotDeviceMessage.requestOf("thing.property.post", sensorData); | |
| 57 | + message.setDeviceId(123456L); | |
| 58 | + | |
| 59 | + // 编码 | |
| 60 | + byte[] packet = codec.encode(message); | |
| 61 | + String jsonString = new String(packet, StandardCharsets.UTF_8); | |
| 62 | + log.info("编码后JSON: {}", jsonString); | |
| 63 | + log.info("数据包长度: {} 字节", packet.length); | |
| 64 | + | |
| 65 | + // 解码验证 | |
| 66 | + IotDeviceMessage decoded = codec.decode(packet); | |
| 67 | + log.info("解码后消息ID: {}", decoded.getId()); | |
| 68 | + log.info("解码后方法: {}", decoded.getMethod()); | |
| 69 | + log.info("解码后设备ID: {}", decoded.getDeviceId()); | |
| 70 | + log.info("解码后服务ID: {}", decoded.getServerId()); | |
| 71 | + log.info("解码后参数: {}", decoded.getParams()); | |
| 72 | + | |
| 73 | + System.out.println(); | |
| 74 | + } | |
| 75 | + | |
| 76 | + /** | |
| 77 | + * 演示心跳 | |
| 78 | + */ | |
| 79 | + private static void demonstrateHeartbeat(IotTcpJsonDeviceMessageCodec codec) { | |
| 80 | + log.info("=== JSON格式心跳示例 ==="); | |
| 81 | + | |
| 82 | + // 创建心跳消息 | |
| 83 | + IotDeviceMessage heartbeat = IotDeviceMessage.requestOf("thing.state.online", null); | |
| 84 | + heartbeat.setDeviceId(123456L); | |
| 85 | + | |
| 86 | + // 编码 | |
| 87 | + byte[] packet = codec.encode(heartbeat); | |
| 88 | + String jsonString = new String(packet, StandardCharsets.UTF_8); | |
| 89 | + log.info("编码后JSON: {}", jsonString); | |
| 90 | + log.info("心跳包长度: {} 字节", packet.length); | |
| 91 | + | |
| 92 | + // 解码验证 | |
| 93 | + IotDeviceMessage decoded = codec.decode(packet); | |
| 94 | + log.info("解码后消息ID: {}", decoded.getId()); | |
| 95 | + log.info("解码后方法: {}", decoded.getMethod()); | |
| 96 | + log.info("解码后设备ID: {}", decoded.getDeviceId()); | |
| 97 | + log.info("解码后服务ID: {}", decoded.getServerId()); | |
| 98 | + | |
| 99 | + System.out.println(); | |
| 100 | + } | |
| 101 | + | |
| 102 | + /** | |
| 103 | + * 演示事件上报 | |
| 104 | + */ | |
| 105 | + private static void demonstrateEventReport(IotTcpJsonDeviceMessageCodec codec) { | |
| 106 | + log.info("=== JSON格式事件上报示例 ==="); | |
| 107 | + | |
| 108 | + // 创建事件数据 | |
| 109 | + Map<String, Object> eventData = new HashMap<>(); | |
| 110 | + eventData.put("eventType", "alarm"); | |
| 111 | + eventData.put("level", "warning"); | |
| 112 | + eventData.put("description", "温度过高"); | |
| 113 | + eventData.put("value", 45.8); | |
| 114 | + | |
| 115 | + // 创建事件消息 | |
| 116 | + IotDeviceMessage event = IotDeviceMessage.requestOf("thing.event.post", eventData); | |
| 117 | + event.setDeviceId(123456L); | |
| 118 | + | |
| 119 | + // 编码 | |
| 120 | + byte[] packet = codec.encode(event); | |
| 121 | + String jsonString = new String(packet, StandardCharsets.UTF_8); | |
| 122 | + log.info("编码后JSON: {}", jsonString); | |
| 123 | + log.info("事件包长度: {} 字节", packet.length); | |
| 124 | + | |
| 125 | + // 解码验证 | |
| 126 | + IotDeviceMessage decoded = codec.decode(packet); | |
| 127 | + log.info("解码后消息ID: {}", decoded.getId()); | |
| 128 | + log.info("解码后方法: {}", decoded.getMethod()); | |
| 129 | + log.info("解码后设备ID: {}", decoded.getDeviceId()); | |
| 130 | + log.info("解码后参数: {}", decoded.getParams()); | |
| 131 | + | |
| 132 | + System.out.println(); | |
| 133 | + } | |
| 134 | + | |
| 135 | + /** | |
| 136 | + * 演示复杂数据上报 | |
| 137 | + */ | |
| 138 | + private static void demonstrateComplexDataReport(IotTcpJsonDeviceMessageCodec codec) { | |
| 139 | + log.info("=== JSON格式复杂数据上报示例 ==="); | |
| 140 | + | |
| 141 | + // 创建复杂设备数据(类似EMQX格式) | |
| 142 | + Map<String, Object> deviceData = new HashMap<>(); | |
| 143 | + | |
| 144 | + // 环境数据 | |
| 145 | + Map<String, Object> environment = new HashMap<>(); | |
| 146 | + environment.put("temperature", 23.8); | |
| 147 | + environment.put("humidity", 55.0); | |
| 148 | + environment.put("co2", 420); | |
| 149 | + environment.put("pm25", 35); | |
| 150 | + deviceData.put("environment", environment); | |
| 151 | + | |
| 152 | + // GPS数据 | |
| 153 | + Map<String, Object> location = new HashMap<>(); | |
| 154 | + location.put("latitude", 39.9042); | |
| 155 | + location.put("longitude", 116.4074); | |
| 156 | + location.put("altitude", 43.5); | |
| 157 | + location.put("speed", 0.0); | |
| 158 | + deviceData.put("location", location); | |
| 159 | + | |
| 160 | + // 设备状态 | |
| 161 | + Map<String, Object> status = new HashMap<>(); | |
| 162 | + status.put("battery", 78); | |
| 163 | + status.put("signal", -65); | |
| 164 | + status.put("online", true); | |
| 165 | + status.put("version", "1.2.3"); | |
| 166 | + deviceData.put("status", status); | |
| 167 | + | |
| 168 | + // 创建设备消息 | |
| 169 | + IotDeviceMessage message = IotDeviceMessage.requestOf("thing.property.post", deviceData); | |
| 170 | + message.setDeviceId(789012L); | |
| 171 | + | |
| 172 | + // 编码 | |
| 173 | + byte[] packet = codec.encode(message); | |
| 174 | + String jsonString = new String(packet, StandardCharsets.UTF_8); | |
| 175 | + log.info("编码后JSON: {}", jsonString); | |
| 176 | + log.info("复杂数据包长度: {} 字节", packet.length); | |
| 177 | + | |
| 178 | + // 解码验证 | |
| 179 | + IotDeviceMessage decoded = codec.decode(packet); | |
| 180 | + log.info("解码后消息ID: {}", decoded.getId()); | |
| 181 | + log.info("解码后方法: {}", decoded.getMethod()); | |
| 182 | + log.info("解码后设备ID: {}", decoded.getDeviceId()); | |
| 183 | + log.info("解码后参数: {}", decoded.getParams()); | |
| 184 | + | |
| 185 | + System.out.println(); | |
| 186 | + } | |
| 187 | + | |
| 188 | + /** | |
| 189 | + * 演示便捷方法 | |
| 190 | + */ | |
| 191 | + private static void demonstrateConvenienceMethods() { | |
| 192 | + log.info("=== 便捷方法示例 ==="); | |
| 193 | + | |
| 194 | + IotTcpJsonDeviceMessageCodec codec = new IotTcpJsonDeviceMessageCodec(); | |
| 195 | + | |
| 196 | + // 使用便捷方法编码数据上报 | |
| 197 | + Map<String, Object> sensorData = Map.of( | |
| 198 | + "temperature", 26.5, | |
| 199 | + "humidity", 58.3 | |
| 200 | + ); | |
| 201 | + byte[] dataPacket = codec.encodeDataReport(sensorData, 123456L, "smart_sensor", "device_001"); | |
| 202 | + log.info("便捷方法编码数据上报: {}", new String(dataPacket, StandardCharsets.UTF_8)); | |
| 203 | + | |
| 204 | + // 使用便捷方法编码心跳 | |
| 205 | + byte[] heartbeatPacket = codec.encodeHeartbeat(123456L, "smart_sensor", "device_001"); | |
| 206 | + log.info("便捷方法编码心跳: {}", new String(heartbeatPacket, StandardCharsets.UTF_8)); | |
| 207 | + | |
| 208 | + // 使用便捷方法编码事件 | |
| 209 | + Map<String, Object> eventData = Map.of( | |
| 210 | + "eventType", "maintenance", | |
| 211 | + "description", "定期维护提醒" | |
| 212 | + ); | |
| 213 | + byte[] eventPacket = codec.encodeEventReport(eventData, 123456L, "smart_sensor", "device_001"); | |
| 214 | + log.info("便捷方法编码事件: {}", new String(eventPacket, StandardCharsets.UTF_8)); | |
| 215 | + | |
| 216 | + System.out.println(); | |
| 217 | + } | |
| 218 | + | |
| 219 | + /** | |
| 220 | + * 演示与EMQX格式的兼容性 | |
| 221 | + */ | |
| 222 | + private static void demonstrateEmqxCompatibility() { | |
| 223 | + log.info("=== EMQX格式兼容性示例 ==="); | |
| 224 | + | |
| 225 | + // 模拟EMQX风格的消息格式 | |
| 226 | + String emqxStyleJson = """ | |
| 227 | + { | |
| 228 | + "id": "msg_001", | |
| 229 | + "method": "thing.property.post", | |
| 230 | + "deviceId": 123456, | |
| 231 | + "params": { | |
| 232 | + "temperature": 25.5, | |
| 233 | + "humidity": 60.2 | |
| 234 | + }, | |
| 235 | + "timestamp": 1642781234567 | |
| 236 | + } | |
| 237 | + """; | |
| 238 | + | |
| 239 | + IotTcpJsonDeviceMessageCodec codec = new IotTcpJsonDeviceMessageCodec(); | |
| 240 | + | |
| 241 | + // 解码EMQX风格的消息 | |
| 242 | + byte[] emqxBytes = emqxStyleJson.getBytes(StandardCharsets.UTF_8); | |
| 243 | + IotDeviceMessage decoded = codec.decode(emqxBytes); | |
| 244 | + | |
| 245 | + log.info("EMQX风格消息解码成功:"); | |
| 246 | + log.info("消息ID: {}", decoded.getId()); | |
| 247 | + log.info("方法: {}", decoded.getMethod()); | |
| 248 | + log.info("设备ID: {}", decoded.getDeviceId()); | |
| 249 | + log.info("参数: {}", decoded.getParams()); | |
| 250 | + | |
| 251 | + System.out.println(); | |
| 252 | + } | |
| 253 | +} | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/test/resources/tcp-binary-packet-examples.md
0 → 100644
| 1 | +# TCP二进制协议数据包格式说明和示例 | |
| 2 | + | |
| 3 | +## 1. 二进制协议概述 | |
| 4 | + | |
| 5 | +TCP二进制协议是一种高效的自定义协议格式,适用于对带宽和性能要求较高的场景。 | |
| 6 | + | |
| 7 | +## 2. 数据包格式 | |
| 8 | + | |
| 9 | +### 2.1 整体结构 | |
| 10 | +``` | |
| 11 | ++----------+----------+----------+----------+----------+----------+ | |
| 12 | +| 包头 | 地址长度 | 设备地址 | 功能码 | 消息序号 | 包体数据 | | |
| 13 | +| 4字节 | 2字节 | 变长 | 2字节 | 2字节 | 变长 | | |
| 14 | ++----------+----------+----------+----------+----------+----------+ | |
| 15 | +``` | |
| 16 | + | |
| 17 | +### 2.2 字段说明 | |
| 18 | + | |
| 19 | +| 字段 | 长度 | 类型 | 说明 | | |
| 20 | +|----------|--------|--------|--------------------------------| | |
| 21 | +| 包头 | 4字节 | int | 后续数据的总长度(不包含包头) | | |
| 22 | +| 地址长度 | 2字节 | short | 设备地址的字节长度 | | |
| 23 | +| 设备地址 | 变长 | string | 设备标识符 | | |
| 24 | +| 功能码 | 2字节 | short | 消息类型标识 | | |
| 25 | +| 消息序号 | 2字节 | short | 消息唯一标识 | | |
| 26 | +| 包体数据 | 变长 | string | JSON格式的消息内容 | | |
| 27 | + | |
| 28 | +### 2.3 功能码定义 | |
| 29 | + | |
| 30 | +| 功能码 | 名称 | 说明 | | |
| 31 | +|--------|----------|--------------------------------| | |
| 32 | +| 10 | 设备注册 | 设备首次连接时的注册请求 | | |
| 33 | +| 11 | 注册回复 | 服务器对注册请求的回复 | | |
| 34 | +| 20 | 心跳请求 | 设备发送的心跳包 | | |
| 35 | +| 21 | 心跳回复 | 服务器对心跳的回复 | | |
| 36 | +| 30 | 消息上行 | 设备向服务器发送的数据 | | |
| 37 | +| 40 | 消息下行 | 服务器向设备发送的指令 | | |
| 38 | + | |
| 39 | +## 3. 二进制数据上报包示例 | |
| 40 | + | |
| 41 | +### 3.1 温度传感器数据上报 | |
| 42 | + | |
| 43 | +**原始数据:** | |
| 44 | +```json | |
| 45 | +{ | |
| 46 | + "method": "thing.property.post", | |
| 47 | + "params": { | |
| 48 | + "temperature": 25.5, | |
| 49 | + "humidity": 60.2, | |
| 50 | + "pressure": 1013.25 | |
| 51 | + }, | |
| 52 | + "timestamp": 1642781234567 | |
| 53 | +} | |
| 54 | +``` | |
| 55 | + | |
| 56 | +**数据包结构:** | |
| 57 | +``` | |
| 58 | +包头: 0x00000045 (69字节) | |
| 59 | +地址长度: 0x0006 (6字节) | |
| 60 | +设备地址: "123456" | |
| 61 | +功能码: 0x001E (30 - 消息上行) | |
| 62 | +消息序号: 0x1234 (4660) | |
| 63 | +包体: JSON字符串 | |
| 64 | +``` | |
| 65 | + | |
| 66 | +**完整十六进制数据包:** | |
| 67 | +``` | |
| 68 | +00 00 00 45 00 06 31 32 33 34 35 36 00 1E 12 34 | |
| 69 | +7B 22 6D 65 74 68 6F 64 22 3A 22 74 68 69 6E 67 | |
| 70 | +2E 70 72 6F 70 65 72 74 79 2E 70 6F 73 74 22 2C | |
| 71 | +22 70 61 72 61 6D 73 22 3A 7B 22 74 65 6D 70 65 | |
| 72 | +72 61 74 75 72 65 22 3A 32 35 2E 35 2C 22 68 75 | |
| 73 | +6D 69 64 69 74 79 22 3A 36 30 2E 32 2C 22 70 72 | |
| 74 | +65 73 73 75 72 65 22 3A 31 30 31 33 2E 32 35 7D | |
| 75 | +2C 22 74 69 6D 65 73 74 61 6D 70 22 3A 31 36 34 | |
| 76 | +32 37 38 31 32 33 34 35 36 37 7D | |
| 77 | +``` | |
| 78 | + | |
| 79 | +### 2.2 GPS定位数据上报 | |
| 80 | + | |
| 81 | +**原始数据:** | |
| 82 | +```json | |
| 83 | +{ | |
| 84 | + "method": "thing.property.post", | |
| 85 | + "params": { | |
| 86 | + "latitude": 39.9042, | |
| 87 | + "longitude": 116.4074, | |
| 88 | + "altitude": 43.5, | |
| 89 | + "speed": 0.0 | |
| 90 | + }, | |
| 91 | + "timestamp": 1642781234567 | |
| 92 | +} | |
| 93 | +``` | |
| 94 | + | |
| 95 | +## 3. 心跳包示例 | |
| 96 | + | |
| 97 | +### 3.1 标准心跳包 | |
| 98 | + | |
| 99 | +**原始数据:** | |
| 100 | +```json | |
| 101 | +{ | |
| 102 | + "method": "thing.state.online", | |
| 103 | + "timestamp": 1642781234567 | |
| 104 | +} | |
| 105 | +``` | |
| 106 | + | |
| 107 | +**数据包结构:** | |
| 108 | +``` | |
| 109 | +包头: 0x00000028 (40字节) | |
| 110 | +地址长度: 0x0006 (6字节) | |
| 111 | +设备地址: "123456" | |
| 112 | +功能码: 0x0014 (20 - 心跳请求) | |
| 113 | +消息序号: 0x5678 (22136) | |
| 114 | +包体: JSON字符串 | |
| 115 | +``` | |
| 116 | + | |
| 117 | +**完整十六进制数据包:** | |
| 118 | +``` | |
| 119 | +00 00 00 28 00 06 31 32 33 34 35 36 00 14 56 78 | |
| 120 | +7B 22 6D 65 74 68 6F 64 22 3A 22 74 68 69 6E 67 | |
| 121 | +2E 73 74 61 74 65 2E 6F 6E 6C 69 6E 65 22 2C 22 | |
| 122 | +74 69 6D 65 73 74 61 6D 70 22 3A 31 36 34 32 37 | |
| 123 | +38 31 32 33 34 35 36 37 7D | |
| 124 | +``` | |
| 125 | + | |
| 126 | +## 4. 复杂数据上报示例 | |
| 127 | + | |
| 128 | +### 4.1 多传感器综合数据 | |
| 129 | + | |
| 130 | +**原始数据:** | |
| 131 | +```json | |
| 132 | +{ | |
| 133 | + "method": "thing.property.post", | |
| 134 | + "params": { | |
| 135 | + "environment": { | |
| 136 | + "temperature": 23.8, | |
| 137 | + "humidity": 55.0, | |
| 138 | + "co2": 420 | |
| 139 | + }, | |
| 140 | + "location": { | |
| 141 | + "latitude": 39.9042, | |
| 142 | + "longitude": 116.4074, | |
| 143 | + "altitude": 43.5 | |
| 144 | + }, | |
| 145 | + "status": { | |
| 146 | + "battery": 78, | |
| 147 | + "signal": -65, | |
| 148 | + "online": true | |
| 149 | + } | |
| 150 | + }, | |
| 151 | + "timestamp": 1642781234567 | |
| 152 | +} | |
| 153 | +``` | |
| 154 | + | |
| 155 | +## 5. 数据包解析步骤 | |
| 156 | + | |
| 157 | +### 5.1 解析流程 | |
| 158 | + | |
| 159 | +1. **读取包头(4字节)** | |
| 160 | + - 获取后续数据的总长度 | |
| 161 | + - 验证数据包完整性 | |
| 162 | + | |
| 163 | +2. **读取设备地址长度(2字节)** | |
| 164 | + - 确定设备地址的字节数 | |
| 165 | + | |
| 166 | +3. **读取设备地址(变长)** | |
| 167 | + - 根据地址长度读取设备标识 | |
| 168 | + | |
| 169 | +4. **读取功能码(2字节)** | |
| 170 | + - 确定消息类型 | |
| 171 | + | |
| 172 | +5. **读取消息序号(2字节)** | |
| 173 | + - 获取消息唯一标识 | |
| 174 | + | |
| 175 | +6. **读取包体数据(变长)** | |
| 176 | + - 解析JSON格式的消息内容 | |
| 177 | + | |
| 178 | +### 5.2 Java解析示例 | |
| 179 | + | |
| 180 | +```java | |
| 181 | +public TcpDataPackage parsePacket(byte[] packet) { | |
| 182 | + int index = 0; | |
| 183 | + | |
| 184 | + // 1. 解析包头 | |
| 185 | + int totalLength = ByteBuffer.wrap(packet, index, 4).getInt(); | |
| 186 | + index += 4; | |
| 187 | + | |
| 188 | + // 2. 解析设备地址长度 | |
| 189 | + short addrLength = ByteBuffer.wrap(packet, index, 2).getShort(); | |
| 190 | + index += 2; | |
| 191 | + | |
| 192 | + // 3. 解析设备地址 | |
| 193 | + String deviceAddr = new String(packet, index, addrLength); | |
| 194 | + index += addrLength; | |
| 195 | + | |
| 196 | + // 4. 解析功能码 | |
| 197 | + short functionCode = ByteBuffer.wrap(packet, index, 2).getShort(); | |
| 198 | + index += 2; | |
| 199 | + | |
| 200 | + // 5. 解析消息序号 | |
| 201 | + short messageId = ByteBuffer.wrap(packet, index, 2).getShort(); | |
| 202 | + index += 2; | |
| 203 | + | |
| 204 | + // 6. 解析包体数据 | |
| 205 | + String payload = new String(packet, index, packet.length - index); | |
| 206 | + | |
| 207 | + return TcpDataPackage.builder() | |
| 208 | + .addr(deviceAddr) | |
| 209 | + .code(functionCode) | |
| 210 | + .mid(messageId) | |
| 211 | + .payload(payload) | |
| 212 | + .build(); | |
| 213 | +} | |
| 214 | +``` | |
| 215 | + | |
| 216 | +## 6. 注意事项 | |
| 217 | + | |
| 218 | +1. **字节序**:所有多字节数据使用大端序(Big-Endian) | |
| 219 | +2. **字符编码**:字符串数据使用UTF-8编码 | |
| 220 | +3. **JSON格式**:包体数据必须是有效的JSON格式 | |
| 221 | +4. **长度限制**:单个数据包建议不超过1MB | |
| 222 | +5. **错误处理**:解析失败时应返回相应的错误码 | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/test/resources/tcp-json-packet-examples.md
0 → 100644
| 1 | +# TCP JSON格式协议说明 | |
| 2 | + | |
| 3 | +## 1. 协议概述 | |
| 4 | + | |
| 5 | +TCP JSON格式协议采用纯JSON格式进行数据传输,参考了EMQX和HTTP模块的数据格式设计,具有以下优势: | |
| 6 | + | |
| 7 | +- **标准化**:使用标准JSON格式,易于解析和处理 | |
| 8 | +- **可读性**:人类可读,便于调试和维护 | |
| 9 | +- **扩展性**:可以轻松添加新字段,向后兼容 | |
| 10 | +- **统一性**:与HTTP模块保持一致的数据格式 | |
| 11 | + | |
| 12 | +## 2. 消息格式 | |
| 13 | + | |
| 14 | +### 2.1 基础消息结构 | |
| 15 | + | |
| 16 | +```json | |
| 17 | +{ | |
| 18 | + "id": "消息唯一标识", | |
| 19 | + "method": "消息方法", | |
| 20 | + "deviceId": "设备ID", | |
| 21 | + "params": { | |
| 22 | + // 消息参数 | |
| 23 | + }, | |
| 24 | + "timestamp": 时间戳 | |
| 25 | +} | |
| 26 | +``` | |
| 27 | + | |
| 28 | +### 2.2 字段说明 | |
| 29 | + | |
| 30 | +| 字段名 | 类型 | 必填 | 说明 | | |
| 31 | +|--------|------|------|------| | |
| 32 | +| id | String | 是 | 消息唯一标识,UUID格式 | | |
| 33 | +| method | String | 是 | 消息方法,如 thing.property.post | | |
| 34 | +| deviceId | Long | 是 | 设备ID | | |
| 35 | +| params | Object | 否 | 消息参数,具体内容根据method而定 | | |
| 36 | +| timestamp | Long | 是 | 时间戳(毫秒) | | |
| 37 | +| code | Integer | 否 | 响应码(下行消息使用) | | |
| 38 | +| message | String | 否 | 响应消息(下行消息使用) | | |
| 39 | + | |
| 40 | +## 3. 消息类型 | |
| 41 | + | |
| 42 | +### 3.1 数据上报 (thing.property.post) | |
| 43 | + | |
| 44 | +设备向服务器上报属性数据。 | |
| 45 | + | |
| 46 | +**示例:** | |
| 47 | +```json | |
| 48 | +{ | |
| 49 | + "id": "8ac6a1db91e64aa9996143fdbac2cbfe", | |
| 50 | + "method": "thing.property.post", | |
| 51 | + "deviceId": 123456, | |
| 52 | + "params": { | |
| 53 | + "temperature": 25.5, | |
| 54 | + "humidity": 60.2, | |
| 55 | + "pressure": 1013.25, | |
| 56 | + "battery": 85 | |
| 57 | + }, | |
| 58 | + "timestamp": 1753111026437 | |
| 59 | +} | |
| 60 | +``` | |
| 61 | + | |
| 62 | +### 3.2 心跳 (thing.state.online) | |
| 63 | + | |
| 64 | +设备向服务器发送心跳保活。 | |
| 65 | + | |
| 66 | +**示例:** | |
| 67 | +```json | |
| 68 | +{ | |
| 69 | + "id": "7db8c4e6408b40f8b2549ddd94f6bb02", | |
| 70 | + "method": "thing.state.online", | |
| 71 | + "deviceId": 123456, | |
| 72 | + "timestamp": 1753111026467 | |
| 73 | +} | |
| 74 | +``` | |
| 75 | + | |
| 76 | +### 3.3 事件上报 (thing.event.post) | |
| 77 | + | |
| 78 | +设备向服务器上报事件信息。 | |
| 79 | + | |
| 80 | +**示例:** | |
| 81 | +```json | |
| 82 | +{ | |
| 83 | + "id": "9e7d72731b854916b1baa5088bd6a907", | |
| 84 | + "method": "thing.event.post", | |
| 85 | + "deviceId": 123456, | |
| 86 | + "params": { | |
| 87 | + "eventType": "alarm", | |
| 88 | + "level": "warning", | |
| 89 | + "description": "温度过高", | |
| 90 | + "value": 45.8 | |
| 91 | + }, | |
| 92 | + "timestamp": 1753111026468 | |
| 93 | +} | |
| 94 | +``` | |
| 95 | + | |
| 96 | +### 3.4 属性设置 (thing.property.set) | |
| 97 | + | |
| 98 | +服务器向设备下发属性设置指令。 | |
| 99 | + | |
| 100 | +**示例:** | |
| 101 | +```json | |
| 102 | +{ | |
| 103 | + "id": "cmd_001", | |
| 104 | + "method": "thing.property.set", | |
| 105 | + "deviceId": 123456, | |
| 106 | + "params": { | |
| 107 | + "targetTemperature": 22.0, | |
| 108 | + "mode": "auto" | |
| 109 | + }, | |
| 110 | + "timestamp": 1753111026469 | |
| 111 | +} | |
| 112 | +``` | |
| 113 | + | |
| 114 | +### 3.5 服务调用 (thing.service.invoke) | |
| 115 | + | |
| 116 | +服务器向设备调用服务。 | |
| 117 | + | |
| 118 | +**示例:** | |
| 119 | +```json | |
| 120 | +{ | |
| 121 | + "id": "service_001", | |
| 122 | + "method": "thing.service.invoke", | |
| 123 | + "deviceId": 123456, | |
| 124 | + "params": { | |
| 125 | + "service": "restart", | |
| 126 | + "args": { | |
| 127 | + "delay": 5 | |
| 128 | + } | |
| 129 | + }, | |
| 130 | + "timestamp": 1753111026470 | |
| 131 | +} | |
| 132 | +``` | |
| 133 | + | |
| 134 | +## 4. 复杂数据示例 | |
| 135 | + | |
| 136 | +### 4.1 多传感器综合数据 | |
| 137 | + | |
| 138 | +```json | |
| 139 | +{ | |
| 140 | + "id": "complex_001", | |
| 141 | + "method": "thing.property.post", | |
| 142 | + "deviceId": 789012, | |
| 143 | + "params": { | |
| 144 | + "environment": { | |
| 145 | + "temperature": 23.8, | |
| 146 | + "humidity": 55.0, | |
| 147 | + "co2": 420, | |
| 148 | + "pm25": 35 | |
| 149 | + }, | |
| 150 | + "location": { | |
| 151 | + "latitude": 39.9042, | |
| 152 | + "longitude": 116.4074, | |
| 153 | + "altitude": 43.5, | |
| 154 | + "speed": 0.0 | |
| 155 | + }, | |
| 156 | + "status": { | |
| 157 | + "battery": 78, | |
| 158 | + "signal": -65, | |
| 159 | + "online": true, | |
| 160 | + "version": "1.2.3" | |
| 161 | + } | |
| 162 | + }, | |
| 163 | + "timestamp": 1753111026471 | |
| 164 | +} | |
| 165 | +``` | |
| 166 | + | |
| 167 | +## 5. 与EMQX格式的兼容性 | |
| 168 | + | |
| 169 | +本协议设计参考了EMQX的消息格式,具有良好的兼容性: | |
| 170 | + | |
| 171 | +### 5.1 EMQX标准格式 | |
| 172 | + | |
| 173 | +```json | |
| 174 | +{ | |
| 175 | + "id": "msg_001", | |
| 176 | + "method": "thing.property.post", | |
| 177 | + "deviceId": 123456, | |
| 178 | + "params": { | |
| 179 | + "temperature": 25.5, | |
| 180 | + "humidity": 60.2 | |
| 181 | + }, | |
| 182 | + "timestamp": 1642781234567 | |
| 183 | +} | |
| 184 | +``` | |
| 185 | + | |
| 186 | +### 5.2 兼容性说明 | |
| 187 | + | |
| 188 | +- ✅ **字段名称**:与EMQX保持一致 | |
| 189 | +- ✅ **数据类型**:完全兼容 | |
| 190 | +- ✅ **消息结构**:结构相同 | |
| 191 | +- ✅ **扩展字段**:支持自定义扩展 | |
| 192 | + | |
| 193 | +## 6. 使用示例 | |
| 194 | + | |
| 195 | +### 6.1 Java编码示例 | |
| 196 | + | |
| 197 | +```java | |
| 198 | +// 创建编解码器 | |
| 199 | +IotTcpJsonDeviceMessageCodec codec = new IotTcpJsonDeviceMessageCodec(); | |
| 200 | + | |
| 201 | +// 创建数据上报消息 | |
| 202 | +Map<String, Object> sensorData = Map.of( | |
| 203 | + "temperature", 25.5, | |
| 204 | + "humidity", 60.2 | |
| 205 | +); | |
| 206 | +IotDeviceMessage message = IotDeviceMessage.requestOf("thing.property.post", sensorData); | |
| 207 | +message.setDeviceId(123456L); | |
| 208 | + | |
| 209 | +// 编码为字节数组 | |
| 210 | +byte[] jsonBytes = codec.encode(message); | |
| 211 | + | |
| 212 | +// 解码 | |
| 213 | +IotDeviceMessage decoded = codec.decode(jsonBytes); | |
| 214 | +``` | |
| 215 | + | |
| 216 | +### 6.2 便捷方法示例 | |
| 217 | + | |
| 218 | +```java | |
| 219 | +// 快速编码数据上报 | |
| 220 | +byte[] dataPacket = codec.encodeDataReport(sensorData, 123456L, "product_key", "device_name"); | |
| 221 | + | |
| 222 | +// 快速编码心跳 | |
| 223 | +byte[] heartbeatPacket = codec.encodeHeartbeat(123456L, "product_key", "device_name"); | |
| 224 | + | |
| 225 | +// 快速编码事件 | |
| 226 | +byte[] eventPacket = codec.encodeEventReport(eventData, 123456L, "product_key", "device_name"); | |
| 227 | +``` | |
| 228 | + | |
| 229 | +## 7. 协议优势 | |
| 230 | + | |
| 231 | +### 7.1 与原TCP二进制协议对比 | |
| 232 | + | |
| 233 | +| 特性 | 二进制协议 | JSON协议 | | |
| 234 | +|------|------------|----------| | |
| 235 | +| 可读性 | 差 | 优秀 | | |
| 236 | +| 调试难度 | 高 | 低 | | |
| 237 | +| 扩展性 | 差 | 优秀 | | |
| 238 | +| 解析复杂度 | 高 | 低 | | |
| 239 | +| 数据大小 | 小 | 稍大 | | |
| 240 | +| 标准化程度 | 低 | 高 | | |
| 241 | + | |
| 242 | +### 7.2 适用场景 | |
| 243 | + | |
| 244 | +- ✅ **开发调试**:JSON格式便于查看和调试 | |
| 245 | +- ✅ **快速集成**:标准JSON格式,集成简单 | |
| 246 | +- ✅ **协议扩展**:可以轻松添加新字段 | |
| 247 | +- ✅ **多语言支持**:JSON格式支持所有主流语言 | |
| 248 | +- ✅ **云平台对接**:与主流IoT云平台格式兼容 | |
| 249 | + | |
| 250 | +## 8. 最佳实践 | |
| 251 | + | |
| 252 | +### 8.1 消息设计建议 | |
| 253 | + | |
| 254 | +1. **保持简洁**:避免过深的嵌套结构 | |
| 255 | +2. **字段命名**:使用驼峰命名法,保持一致性 | |
| 256 | +3. **数据类型**:使用合适的数据类型,避免字符串表示数字 | |
| 257 | +4. **时间戳**:统一使用毫秒级时间戳 | |
| 258 | + | |
| 259 | +### 8.2 性能优化 | |
| 260 | + | |
| 261 | +1. **批量上报**:可以在params中包含多个数据点 | |
| 262 | +2. **压缩传输**:对于大数据量可以考虑gzip压缩 | |
| 263 | +3. **缓存机制**:客户端可以缓存消息,批量发送 | |
| 264 | + | |
| 265 | +### 8.3 错误处理 | |
| 266 | + | |
| 267 | +1. **格式验证**:确保JSON格式正确 | |
| 268 | +2. **字段检查**:验证必填字段是否存在 | |
| 269 | +3. **异常处理**:提供详细的错误信息 | |
| 270 | + | |
| 271 | +## 9. 迁移指南 | |
| 272 | + | |
| 273 | +### 9.1 从二进制协议迁移 | |
| 274 | + | |
| 275 | +1. **保持兼容**:可以同时支持两种协议 | |
| 276 | +2. **逐步迁移**:按设备类型逐步迁移 | |
| 277 | +3. **测试验证**:充分测试新协议的稳定性 | |
| 278 | + | |
| 279 | +### 9.2 配置变更 | |
| 280 | + | |
| 281 | +```java | |
| 282 | +// 在设备配置中指定编解码器类型 | |
| 283 | +device.setCodecType("TCP_JSON"); | |
| 284 | +``` | |
| 285 | + | |
| 286 | +这样就完成了TCP协议向JSON格式的升级,提供了更好的可读性、扩展性和兼容性。 | ... | ... |